diff --git a/app.rb b/app.rb index a258971..55193c4 100644 --- a/app.rb +++ b/app.rb @@ -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 diff --git a/lib/receipt_builder.rb b/lib/receipt_builder.rb new file mode 100644 index 0000000..731695c --- /dev/null +++ b/lib/receipt_builder.rb @@ -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 diff --git a/views/_navbar.erb b/views/_navbar.erb index 65d2acb..6f129ce 100644 --- a/views/_navbar.erb +++ b/views/_navbar.erb @@ -12,6 +12,9 @@