Add Epson TM-T20II receipt printing (title/body/QR, no ESC/POS knowledge needed)
deploy / deploy (push) Successful in 8s
deploy / deploy (push) Successful in 8s
Custom ReceiptBuilder instead of the escpos gem -- its diacritics handling needed the same manual UTF-8->CP852 encode call anyway, based on earlier experiments in ~/test/epson/.
This commit is contained in:
1 parent
5c06f725f0
commit
afae57aff4
4 files changed
+154
No files matched your search
@@ -3,6 +3,7 @@ require 'sinatra'
|
|||||||
require "sinatra/reloader"
|
require "sinatra/reloader"
|
||||||
require 'tmpdir'
|
require 'tmpdir'
|
||||||
require 'net/http'
|
require 'net/http'
|
||||||
|
require_relative 'lib/receipt_builder'
|
||||||
|
|
||||||
set :protection, :except => :frame_options
|
set :protection, :except => :frame_options
|
||||||
set :environment, :production
|
set :environment, :production
|
||||||
@@ -21,6 +22,8 @@ PRINTER_DPI = 203 # DatamaxONeil E4204B Mark III native head resolution
|
|||||||
PRINTER_DARKNESS = 8 # lower than factory default to stop thermal dot-gain from fusing barcode bars
|
PRINTER_DARKNESS = 8 # lower than factory default to stop thermal dot-gain from fusing barcode bars
|
||||||
TEST_LABEL_PDF = File.join(File.dirname(__FILE__), 'test-label.pdf')
|
TEST_LABEL_PDF = File.join(File.dirname(__FILE__), 'test-label.pdf')
|
||||||
|
|
||||||
|
PRINTER_DEVICE_EPSON = '/dev/usb/lp_epson' # see /etc/udev/rules.d/99-epson.rules
|
||||||
|
|
||||||
BROTHER_PRINTERS = {
|
BROTHER_PRINTERS = {
|
||||||
'ql560' => { port: 8560, label: 'Brother QL-560' },
|
'ql560' => { port: 8560, label: 'Brother QL-560' },
|
||||||
'ql700' => { port: 8700, label: 'Brother QL-700' },
|
'ql700' => { port: 8700, label: 'Brother QL-700' },
|
||||||
@@ -88,6 +91,19 @@ helpers do
|
|||||||
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(data) }
|
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(data) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Builds a simple receipt (bold centered title + left-aligned body,
|
||||||
|
# optional QR code) so callers don't need to know ESC/POS escape codes.
|
||||||
|
def print_receipt(title:, body:, qr: nil)
|
||||||
|
receipt = ReceiptBuilder.new
|
||||||
|
receipt.align(:center).bold.text("#{title}\n").bold(false) unless title.to_s.empty?
|
||||||
|
receipt.align(:left).text("#{body}\n") unless body.to_s.empty?
|
||||||
|
unless qr.to_s.empty?
|
||||||
|
receipt.align(:center).feed(1).qr(qr).feed(1)
|
||||||
|
end
|
||||||
|
receipt.feed(3).cut
|
||||||
|
receipt.print!(PRINTER_DEVICE_EPSON)
|
||||||
|
end
|
||||||
|
|
||||||
# Transparently reverse-proxies the current request to one of the
|
# Transparently reverse-proxies the current request to one of the
|
||||||
# brother_ql_web instances, so all three printers (web UI and API alike)
|
# brother_ql_web instances, so all three printers (web UI and API alike)
|
||||||
# are reachable through this single sinatra port instead of their own
|
# are reachable through this single sinatra port instead of their own
|
||||||
@@ -187,6 +203,20 @@ post '/test-print' do
|
|||||||
redirect '/upload'
|
redirect '/upload'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get '/epson' do
|
||||||
|
erb :epson
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/epson/print' do
|
||||||
|
begin
|
||||||
|
print_receipt(title: params[:title].to_s, body: params[:body].to_s, qr: params[:qr].to_s)
|
||||||
|
flash 'Sent to Epson printer', 'success'
|
||||||
|
rescue => e
|
||||||
|
flash "Epson print failed (#{e.message})", 'danger'
|
||||||
|
end
|
||||||
|
redirect '/epson'
|
||||||
|
end
|
||||||
|
|
||||||
get '/brother' do
|
get '/brother' do
|
||||||
@printers = BROTHER_PRINTERS
|
@printers = BROTHER_PRINTERS
|
||||||
erb :brother_picker
|
erb :brother_picker
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Minimal chainable ESC/POS builder for the Epson TM-T20II.
|
||||||
|
#
|
||||||
|
# Written instead of pulling in the `escpos` gem: the gem's diacritics
|
||||||
|
# handling needed the same manual UTF-8->CP852 `.encode` call anyway, and a
|
||||||
|
# couple of its helpers didn't behave as documented against this printer --
|
||||||
|
# fewer moving parts doing it directly.
|
||||||
|
class ReceiptBuilder
|
||||||
|
INIT = "\x1B\x40".freeze
|
||||||
|
CP852 = "\x1B\x74\x12".freeze # code page for Slovak diacritics
|
||||||
|
|
||||||
|
ALIGN_LEFT = "\x1B\x61\x00".freeze
|
||||||
|
ALIGN_CENT = "\x1B\x61\x01".freeze
|
||||||
|
ALIGN_RIGH = "\x1B\x61\x02".freeze
|
||||||
|
|
||||||
|
BOLD_ON = "\x1B\x45\x01".freeze
|
||||||
|
BOLD_OFF = "\x1B\x45\x00".freeze
|
||||||
|
UNDER_ON = "\x1B\x2D\x01".freeze
|
||||||
|
UNDER_OFF = "\x1B\x2D\x00".freeze
|
||||||
|
|
||||||
|
TXT_NORM = "\x1D\x21\x00".freeze
|
||||||
|
TXT_BIG = "\x1D\x21\x11".freeze # double height and width
|
||||||
|
TXT_HIGH = "\x1D\x21\x01".freeze # double height
|
||||||
|
TXT_WIDE = "\x1D\x21\x10".freeze # double width
|
||||||
|
|
||||||
|
CUT = "\x1D\x56\x00".freeze
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@buffer = "".b
|
||||||
|
init
|
||||||
|
end
|
||||||
|
|
||||||
|
def init
|
||||||
|
@buffer << INIT << CP852
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def text(str)
|
||||||
|
@buffer << str.encode('CP852', invalid: :replace, undef: :replace, replace: '?')
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def align(mode)
|
||||||
|
@buffer << case mode
|
||||||
|
when :center then ALIGN_CENT
|
||||||
|
when :right then ALIGN_RIGH
|
||||||
|
else ALIGN_LEFT
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def bold(enable = true)
|
||||||
|
@buffer << (enable ? BOLD_ON : BOLD_OFF)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def underline(enable = true)
|
||||||
|
@buffer << (enable ? UNDER_ON : UNDER_OFF)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def size(mode)
|
||||||
|
@buffer << case mode
|
||||||
|
when :big then TXT_BIG
|
||||||
|
when :high then TXT_HIGH
|
||||||
|
when :wide then TXT_WIDE
|
||||||
|
else TXT_NORM
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Native ESC/POS QR code
|
||||||
|
def qr(data, size: 6)
|
||||||
|
qr_data = data.b
|
||||||
|
store_len = qr_data.bytesize + 3
|
||||||
|
|
||||||
|
@buffer << "\x1D\x28\x6B\x03\x00\x31\x43" << size.chr
|
||||||
|
@buffer << "\x1D\x28\x6B\x03\x00\x31\x45\x30" # error correction L
|
||||||
|
@buffer << "\x1D\x28\x6B" << [store_len % 256, store_len / 256].pack('CC') << "\x31\x50\x30" << qr_data
|
||||||
|
@buffer << "\x1D\x28\x6B\x03\x00\x31\x51\x30" # print
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
# Native ESC/POS EAN-13
|
||||||
|
def ean13(code)
|
||||||
|
@buffer << "\x1D\x6B\x02" << code.b << "\x00"
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def feed(lines = 3)
|
||||||
|
@buffer << ("\n" * lines)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def cut
|
||||||
|
@buffer << CUT
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def render
|
||||||
|
@buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
def print!(device)
|
||||||
|
File.open(device, 'wb') { |f| f.write(@buffer) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/upload">Shipping labels</a>
|
<a class="nav-link" href="/upload">Shipping labels</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/epson">Receipts</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<span class="navbar-text me-2">Brother:</span>
|
<span class="navbar-text me-2">Brother:</span>
|
||||||
<div class="d-flex gap-1">
|
<div class="d-flex gap-1">
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<form action="/epson/print" method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Title (bold, centered)</label>
|
||||||
|
<input name="title" class="form-control" type="text" id="title" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="body" class="form-label">Body text</label>
|
||||||
|
<textarea name="body" class="form-control" id="body" rows="8"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="qr" class="form-label">QR code content (optional)</label>
|
||||||
|
<input name="qr" class="form-control" type="text" id="qr" />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" type="submit">Print</button>
|
||||||
|
</form>
|
||||||
Reference in new issue
Block a user