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 'tmpdir'
|
||||
require 'net/http'
|
||||
require_relative 'lib/receipt_builder'
|
||||
|
||||
set :protection, :except => :frame_options
|
||||
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
|
||||
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 = {
|
||||
'ql560' => { port: 8560, label: 'Brother QL-560' },
|
||||
'ql700' => { port: 8700, label: 'Brother QL-700' },
|
||||
@@ -88,6 +91,19 @@ helpers do
|
||||
File.open(PRINTER_DEVICE, 'wb') { |f| f.write(data) }
|
||||
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
|
||||
# brother_ql_web instances, so all three printers (web UI and API alike)
|
||||
# are reachable through this single sinatra port instead of their own
|
||||
@@ -187,6 +203,20 @@ post '/test-print' do
|
||||
redirect '/upload'
|
||||
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
|
||||
@printers = BROTHER_PRINTERS
|
||||
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">
|
||||
<a class="nav-link" href="/upload">Shipping labels</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/epson">Receipts</a>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="navbar-text me-2">Brother:</span>
|
||||
<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