feat(daemon): /print-image — print a caller-rendered bitmap through the same encode->verify->print path
The seam for a separate label app to own LAYOUTS while this daemon stays the single owner of the hardware. Takes a base64 PNG at media pixel size plus sku/releaseId, and runs the identical guarded sequence: read, UHF WRITE, verify, SET RFID OFF, geometry, print. A failed encode still spends no label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ce5b2b5db5
commit
7d9a2197b2
@ -33,6 +33,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
import label
|
||||
from PIL import Image
|
||||
import dymo as dymo_mod
|
||||
from urovo import Urovo, UrovoError, epc_encode, epc_decode, status_text
|
||||
|
||||
@ -277,6 +278,64 @@ def do_read_raw():
|
||||
return {'ok': any(banks.values()), 'banks': banks}
|
||||
|
||||
|
||||
def do_print_image(body):
|
||||
"""Print a caller-rendered 1-bit bitmap (base64 PNG), optionally encoding first.
|
||||
|
||||
This is the seam that lets a separate app own LAYOUTS while this daemon stays the single
|
||||
owner of the hardware. do_print renders the Discogs-shaped label itself; anything else --
|
||||
a book, a comic, a toy -- arrives here already drawn, at the media's pixel size, and goes
|
||||
through the exact same encode -> verify -> SET RFID OFF -> print sequence. Same guards,
|
||||
same "no label spent on a failed encode" promise."""
|
||||
import base64, io
|
||||
prof = label.profile(body.get('size'), web_mm=body.get('w'), pitch_mm=body.get('h'),
|
||||
gap_mm=body.get('gap'))
|
||||
encode = bool(body.get('encode', True))
|
||||
sku = str(body.get('sku', '') or '').strip()
|
||||
release_id = str(body.get('releaseId', '') or '0').strip() or '0'
|
||||
try:
|
||||
img = Image.open(io.BytesIO(base64.b64decode(body['png']))).convert('1')
|
||||
except Exception as e:
|
||||
return {'ok': False, 'error': 'bad png: %s' % e}
|
||||
epc = None
|
||||
if encode:
|
||||
try:
|
||||
epc = epc_encode(sku, release_id)
|
||||
except ValueError as e:
|
||||
return {'ok': False, 'error': str(e)}
|
||||
with LOCK, Urovo() as p:
|
||||
before = verified = None
|
||||
if encode:
|
||||
before = p.read_uhf()
|
||||
p.write_uhf(epc)
|
||||
time.sleep(1.0)
|
||||
verified = p.read_uhf() == epc
|
||||
if not verified:
|
||||
return {'ok': False, 'error': 'RFID encode failed verification', 'epc': epc,
|
||||
'epcBefore': before, 'readBack': p.read_uhf(),
|
||||
'note': 'nothing printed -- no label wasted'}
|
||||
p.cmd('SET RFID OFF'); time.sleep(0.4)
|
||||
p.ribbon_off(); time.sleep(0.4)
|
||||
if p.wait_ready(3.0) != 0x00:
|
||||
st = p.recover()
|
||||
if st != 0x00:
|
||||
return {'ok': False, 'error': 'printer not ready', 'status': st,
|
||||
'statusText': status_text(st)}
|
||||
geometry(p, prof); p.cls()
|
||||
p.write(label.to_tspl(img))
|
||||
p.ribbon_off(); p.printlabel(1, 1)
|
||||
time.sleep(2.0)
|
||||
st = p.wait_ready(10.0)
|
||||
nxt = None
|
||||
try:
|
||||
time.sleep(0.3); p.flush_in(); nxt = p.read_uhf()
|
||||
except Exception:
|
||||
pass
|
||||
return {'ok': True, 'sku': sku, 'releaseId': release_id, 'epc': epc, 'epcBefore': before,
|
||||
'verified': verified, 'nextTagAtAntenna': nxt, 'status': st,
|
||||
'statusText': status_text(st), 'profile': prof['name'],
|
||||
'size': [prof['web_mm'], prof['pitch_mm']], 'mode': 'image+encode' if encode else 'image'}
|
||||
|
||||
|
||||
def do_calibrate():
|
||||
with LOCK, Urovo() as p:
|
||||
p.rfid_autocalibrate()
|
||||
@ -371,6 +430,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self._json(do_print_paper(body))
|
||||
if path == '/tag-and-print':
|
||||
return self._json(do_tag_and_print(body))
|
||||
if path == '/print-image':
|
||||
return self._json(do_print_image(body))
|
||||
handler = {'/calibrate': do_calibrate, '/recover': do_recover}.get(path)
|
||||
if handler:
|
||||
return self._json(handler())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user