render() always rotated the landscape design 90 degrees -- right for the 24 mm
narrow stock, which feeds narrow-edge-first, and wrong for the 65 x 35 UHF roll,
whose 65 mm is the WEB. On that roll the rotated design ran along a 35 mm label:
printed sideways and clipped after ~35 mm, seen on the bench.
rotate now defaults from the profile (rfid65: False) and only an explicit
argument overrides it, so the Dymo path (which passes rotate itself) is
untouched. When a label-printer profile is unrotated the canvas is still the
media -- web x pitch, 65 x 35 -- with the 64 x 34 design centred, not an
art-sized bitmap; that art-sized case is kept only for the Dymo, whose head is
wider than its label. Offline render verified 520 x 280 px = 65.0 x 35.0 mm,
upright.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
web 65 / pitch 35 / gap 0 / art 64x34. 65 is the roll width and 35 the label
length: sent the other way round (SIZE 34,65.35) the printer advanced nearly two
labels per feed, which presented as an inlay that never stopped at the antenna.
gap=0 because the inlay fills most of the label and blinds the optical gap
sensor. Proven with a real print-test on this roll -- printer returned Ready, no
VOID -- so callers stop passing w/h/gap overrides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding set_rfid(tag_type=1) ahead of the read-back seemed like cheap insurance
and was the opposite. On this firmware a SET RFID sent while the UHF module is
already active left it answering NOTHING -- READ and QUERY returned no bytes at
all under every tag type, while model()/status() kept working -- and that state
survived every software command tried. It presents exactly like a media
alignment fault ("before: null", chip apparently never in the window) on a
roll whose inlay fills the label. The printer arms RFID itself at power-up.
Leave it. set_rfid()'s default is corrected to 1 (0 is OFF) for any caller that
does use it deliberately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After &CALIBRATE,A,R this printer answered model() but not the one-byte <ESC>!?
status, for minutes, so wait_ready/recover/do_print all reported 'no response'
and refused to print a printer that was in fact Ready. The extended <ESC>!S is
still answered: STX + 4 bytes + ETX CRLF, first byte 0x40+code, which maps
one-to-one onto STATUS_TEXT (0x48 = ribbon latched, 0x40 = Ready). Seen live:
!? silent, !S -> 02 40 40 40 40 03 0D 0A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four devices, two daemons, one front door. This service drives the Urovo D812R+
and the Dymo LabelWriter over raw USB; the Node rfid-daemon on 7790 drives the
Chafon H102 gun and the Dymo M25 scale, and is what the PriceGod extension is
pointed at. Both were binding 7790.
The port was not the whole problem. /status, /read-tag, /write-tag and /recover
exist on BOTH services and mean different things — the gun in your hand versus
the antenna inside the printer. Merging them into one flat namespace would let a
"read-tag" land on whichever daemon answered, which is how you get a tag written
by the wrong device. So this keeps its own names and the Node side proxies
/printer/* here, leaving the extension with a single endpoint on 7790.
They stay separate processes on purpose: this side needs libusb for a real
bidirectional pipe (UHF READ replies as raw bytes on the bulk-IN endpoint, which
is why CUPS 'lp -o raw' cannot drive it) and the other needs node-serialport and
node-hid. Merging would mean porting one language's device stack to the other
for no gain.
--port still wins, UROVO_PORT overrides the default, and neither the launchd
plist nor start-daemon.command pins a port, so install.sh needed no change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
macOS auto-adds a USB LabelWriter 450 with the GENERIC CUPS driver (dymo.ppd,
ModelName "DYMO Label Printer"), which has 10 fixed sizes and no CustomPageSize
block — so a custom paper size made in the page-setup dialog can never reach the
printer. CUPS falls back to w81h252 (Address, 28.6x88.9mm) and the 54x24mm design
prints rotated 90 degrees and shrunk. Cost a bench session to diagnose; the extension's
labels.js was never at fault.
Adds the stock in both feed orientations (pg55x25 / pg55x25L — John's price roll is
the landscape-feed one) plus patch_ppd.py to regenerate. Lives here because mac/dymo.py
drives the same printer.
Must be re-applied after any macOS update or after re-adding the printer, so the file
and the lpadmin invocation are both written down in the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Answers "do we have to start the server in terminal?" -- no. install.sh installs
deps and registers a launchd agent with RunAtLoad + KeepAlive, so the daemon
starts at login and comes back if it dies. Verified by killing the process: back
up and serving within seconds under a new pid. Deploying to the M3 Air is then:
git clone ssh://git@100.71.119.27:222/monster/yourovo.git
cd yourovo/mac && ./install.sh
Also revises the A/B/C recommendation. Grepping the extension shows it calls only
six endpoints: /status, /write-tag, /read-tag (all served here) plus /weight,
/scale/start, /scale/stop (the DYMO M10 postal scale, Node-only). So the gap to a
single service is the scale and nothing else -- which makes option B (Node on
:7791 with this one proxying) the wrong trade: one URL but two processes, two
dependency stacks and two things to keep alive, against a stated goal of one
button on a second machine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SKU<->EPC layout inherited from daemon.cs was WRONG. daemon.cs packs 6 bytes
of SKU + 4 bytes of release_id + an 0xEC01 marker, and flagged itself as
unverified. The shop's actual scheme -- in pliceclogs/rfid-daemon/index.js
(buildEpcPayload/parseEpcData), which is what the extension and every existing
tag use -- is a single 96-bit big-endian integer:
value = sku(14 digits) * 10^9 + releaseId
Checked against that file's own worked example: 20260321001001 + 35332 ->
20260321001001000035332. epc_decode also reproduces its guard, returning None
when value/10^9 exceeds 14 digits, so a factory tag (E28069...) can never be
reported to the operator as a real SKU.
This matters: parseEpcData rejects the EC01 layout outright, so every tag the
Windows daemon has written is invisible to the extension. ASSESSMENT.md now
carries that as a red open item against daemon.cs.
Verified end-to-end with an INDEPENDENT reader. ASSESSMENT.md sec.4 parked the
Chafon as HID-keyboard-only, but that is Windows-specific -- on macOS it exposes
a CH340 bridge and its documented serial protocol works. New chafon.py (read-only:
inventory + read EPC, CRC-16/0x8408 framing) scans the field and sees:
0000044A5600D4CDA1FAB932 -> sku=20260722180000 rel=424242 (Urovo-written)
E28069950000600625F600D3 -> factory/blank (x12, rest of roll)
126D5125FFA000067932EC01 -> factory/blank (earlier EC01 tag, now rejected)
That last line is the bug demonstrated: a tag written earlier today under the old
scheme is unrecognised, exactly as the extension would have treated it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs found on the bench, both from wrong assumptions about the LW450:
1. Orientation. I assumed the 57mm head meant the label feeds landscape. It
doesn't -- the Dymo takes the SAME 24mm narrow-edge-first stock as the Urovo,
so the landscape design needs the identical 90-degree rotation. Rendering
656 dots onto a ~288-dot label also silently clipped the price and QR off the
right-hand edge. Now rotates by default, overridable per request via "rotate",
and the daemon rejects an image wider than the head instead of clipping it.
2. Half-length labels. print_image() sent ESC i (300x600 Barcode/Graphics mode),
where the head stays 300 dpi across but each raster line becomes 1/600" in the
travel direction -- so a 650-line label printed at 27.5mm instead of 55mm, with
the across-head dimension correct. Default is now ESC h (300x300), matching our
square 300 dpi bitmap. graphics_mode=True remains available and now stretches
the image 2x vertically so physical size stays correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Answers "can we direct print to the Dymo via CUPS, or is it the cmd-P dance?":
neither. The LW450 (0922:0020) is a bidirectional USB printer-class device just
like the Urovo, so its raster protocol goes straight to the bulk endpoint -- no
DYMO driver, no CUPS queue, no print dialog. Command set taken from DYMO's own
LabelWriter 450 Series Technical Reference Manual and documented in dymo.py.
Verified on the bench: revision 1750111r53, status 0x03 Ready, 283 raster lines
at 82 bytes/line accepted.
Also corrects an assumption inherited from daemon.cs: UHF WRITE commits
immediately and does NOT wait for PRINT. Confirmed by writing a chip and reading
it back with no PRINT sent. Two consequences:
- the Urovo can program a chip without printing or consuming a label, which is
what makes the Urovo-encodes/Dymo-prints split work at all;
- an encode can be verified BEFORE printing, so a failed write never costs a
label. /print-encode and /encode both do this now.
New routes: /devices (all three devices at a glance), /encode (chip only),
/print-paper (Dymo), /tag-and-print (Mode A in one call).
label.py is now resolution-aware (203 dpi Urovo / 300 dpi Dymo via profile(dpmm=))
and orientation-aware: the Urovo's 24mm web feeds narrow-edge-first so the design
rotates 90 degrees, while the Dymo's 57mm head is wider than the label so the same
design feeds landscape unrotated.
Also fixes tag read parsing: with dataFormat 'H' the printer replies in ASCII hex
TEXT, not raw bytes -- the old code hex-encoded it a second time. flush_in() now
drains fully, since a late reply to one command was being attributed to the next
and silently corrupting reads.
KNOWN ISSUE: the Node rfid-daemon (Chafon + scale + inventory) also binds :7790.
Both cannot run at once and this service does not implement its routes. Unresolved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ASSESSMENT.md §7 assumed the RFID encode was locked inside the Windows-only
GTSPL_SDK.dll and would need a USBPcap sniff or the GTSPL Java jar to recover.
It didn't. The DLL is a managed .NET assembly, and its IL shows every RFID call
is a one-line String.Concat -> ASCII -> WritePrinter. Recovered from ldstr order
plus argument order, and confirmed by a second independent decompile:
UHF WRITE H,2,12,E,"<24 hex>" UHF READ/QUERY ...
UHF GEN2 EPC|TID|USER|ACCESS|KILL <action>,"<pw>"
SET RFID <tagType>,<rw_pos>,<void>,<try>,<err>,<speed>,<retry> (dots, not mm)
&DEFAULT,i &CALIBRATE,A,R PRINT <set>, <copy>
So the whole recipe is plain ASCII over the printer's bulk endpoint and ports to
macOS with no vendor binaries at all.
- mac/urovo.py USB transport + full GTSPL command set, status decoding, the
ribbon-latch recovery, SKU<->EPC packing ported from daemon.cs.
- mac/label.py PIL bitmap renderer -> TSPL BITMAP. Media-profile driven, so both
shop stocks work (small 51x19 and large 55x24 on the 24mm web),
with a safe-area inset so the design can't clip at the edges.
- mac/daemon.py :7790 HTTP service, same routes/JSON/CORS as daemon.cs, plus a
/preview route that renders a label as PNG without printing one.
Verified on the bench from macOS: printer identifies (MODEL:UROVO-D812R), the
latched 0x08 "out of ribbon" clears with SET RIBBON OFF + FORMFEED, and labels
print correctly oriented and dark on the 24x55 stock.
NOT yet verified: the RFID encode itself. It is wired and the printer accepts the
command, but the loaded thermal stock has no inlays so nothing has been written to
a chip. Needs a PET RFID label at the antenna. The SKU<->EPC byte layout also
remains unconfirmed against existing Chafon-written tags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>