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>
|
||
|---|---|---|
| .. | ||
| chafon.py | ||
| daemon.py | ||
| dymo.py | ||
| label.py | ||
| README.md | ||
| setup.sh | ||
| start-daemon.command | ||
| urovo.py | ||
pricegod label daemon (macOS)
One service on localhost:7790 that drives both printers over raw USB:
| device | role | how |
|---|---|---|
| Urovo D812R+ | prints TSPL labels, programs UHF chips | raw TSPL, urovo.py |
| Dymo LabelWriter 450 | prints the paper label | raw raster, dymo.py — no CUPS driver, no print dialog |
| Chafon H-102 | reads tags off the shelf | not this service — see “Chafon” below |
It is the Mac counterpart to the Windows daemon.cs and keeps the same HTTP contract,
so it stays a drop-in for the PriceGod extension's daemon.js client.
Two things worth knowing up front
UHF WRITE commits immediately — it does not wait for PRINT. daemon.cs assumed the
encode only fired together with printlabel. It doesn't. That means the Urovo can program
a chip while printing nothing, which is what makes the split flow (Urovo encodes, Dymo
prints) possible, and it lets the daemon read the chip back to verify an encode before
spending a label on it.
The Chafon works over serial on macOS. ASSESSMENT.md §4 found the gun was HID-keyboard
only on Windows. On the Mac it enumerates a CH340 bridge at /dev/cu.usbserial-* and the
existing Node rfid-daemon speaks its protocol fine, so that dead end is Windows-specific.
No vendor SDK required. ASSESSMENT.md §7 assumed the RFID encode was locked inside
the Windows-only GTSPL_SDK.dll and would need a USB sniff or the Java jar to recover.
It doesn't: the DLL is a managed .NET assembly, and its IL shows every RFID call is a
one-line String.Concat → ASCII → WritePrinter. The wire format below was recovered
from ldstr order + argument order and independently confirmed by a second decompile.
UHF WRITE <fmt>,<start>,<len>,<bank>,"<data>" # the RFID encode
UHF READ <fmt>,<start>,<len>,<bank>
UHF QUERY <fmt>,<pcStatus>,<crcStatus>
UHF GEN2 EPC|TID|USER|ACCESS|KILL <action>,"<pw>"
SET RFID <tagType>,<rw_pos>,<void_printout>,<tryEncode>,<errHandle>,<speed>,<retry>
&DEFAULT,i # rfidSetupDefault &CALIBRATE,A,R # RFIDAutoCalibration
PRINT <set>, <copy> # note the space after the comma
All plain ASCII, one command per line, CRLF-terminated. writeUHF("H",2,12,"E",epc)
emits exactly UHF WRITE H,2,12,E,"<24 hex chars>".
Setup
./setup.sh # brew install libusb + venv with pyusb/pillow/segno
Then double-click start-daemon.command (leave it open while you work).
Files
| File | What |
|---|---|
urovo.py |
Urovo USB transport + the full GTSPL command set, status decoding, fault recovery, SKU↔EPC packing. Runs standalone as a connection test. |
dymo.py |
Dymo LabelWriter 450 raster driver over raw USB. Runs standalone to report status/revision. |
label.py |
Renders the label to a 1-bit bitmap with PIL, resolution-aware (203 dpi Urovo / 300 dpi Dymo). Run standalone to dump previews to /tmp. |
daemon.py |
The :7790 HTTP service. |
Routes
| method | path | body / query | returns |
|---|---|---|---|
| GET | /devices |
— | status of Urovo, Dymo and Chafon in one call |
| GET | /status |
— | Urovo only: {ok,ready,status,statusText,printer} |
| POST | /tag-and-print |
label fields + {sku,releaseId} |
Mode A in one call: Urovo programs the chip, Dymo prints the paper |
| POST | /encode |
{sku,releaseId} |
programs the chip at the antenna and reads it back. Prints nothing, costs no label |
| POST | /print-paper |
label fields + size?, density? |
prints on the Dymo |
| POST | /print-encode |
{sku,releaseId,artist?,title?,genre?,style?,info?,price?,condition?,size?,w?,h?,gap?} |
Urovo one-pass print and encode (needs RFID stock) |
| POST | /write-tag |
alias of /print-encode |
same |
| POST | /print-test |
same body — Urovo prints only, no RFID (safe on chipless stock) | same |
| GET | /read-tag |
— | {ok,epc,sku,releaseId,recognized} |
| GET | /read-raw |
— | every memory bank |
| GET | /preview?size=large&artist=… |
— | PNG of the label, printing nothing |
| GET | /profiles |
— | the media profiles below |
| POST | /calibrate / /recover |
— | RFID auto-calibration / clear a latched fault |
Use /preview while tuning a layout — it costs no labels.
Which flow to use
- Mode A — separate (today, existing stock).
POST /tag-and-print. The Urovo programs the chip on an RFID label; the Dymo prints the paper; you stick the paper over the chip. The encode is verified before the paper prints, so a failed write wastes nothing. - Mode B — one pass (the goal).
POST /print-encodeon direct-thermal RFID paper (ASSESSMENT.md §5). One label, printed and encoded together.
The Dymo, without a driver
The LabelWriter 450 (0922:0020) is a bidirectional USB printer-class device, so the
daemon writes its raster protocol straight to the bulk endpoint. No DYMO driver, no CUPS
queue, no open-window/cmd-P/cmd-W. Command set is from DYMO's own LabelWriter 450 Series
Technical Reference Manual; dymo.py documents each command it uses.
Note the two printers differ in orientation: the Urovo's 24 mm web feeds narrow-edge-first
so the design is rotated 90°, while the Dymo's 57 mm head is wider than the label so the
same design feeds landscape and must not be rotated. label.render(..., rotate=)
handles this, and the resolution difference (203 vs 300 dpi) comes from profile(dpmm=).
Chafon, and the :7790 collision
⚠️ The existing Node daemon at ~/Documents/pliceclogs/rfid-daemon also listens on
:7790. Both cannot run at once. That daemon owns the Chafon serial protocol
(CF [ADDR] [CMD] [LEN] [DATA] [CRC16] @115200) plus the postal-scale and inventory
routes, none of which this service implements — so it is not a full replacement.
Pick one of: run this on --port 7791; keep the Chafon daemon for the gun and run this
one elsewhere; or fold the Chafon protocol in here. Unresolved — decide before wiring the
extension.
Media profiles
Both shop stocks sit on a 24 mm web and feed long-edge-first, so the landscape design is rotated 90° onto the media. Names match the extension's size dropdown.
| profile | label face | pitch | notes |
|---|---|---|---|
small |
51 × 19 mm | 51 mm | older stock; the rest of the 24 mm web is blank filler, design is centred across it |
large |
55 × 24 mm | 55 mm | current stock (default) |
xlarge |
64 × 34 mm | 64 mm | untested |
Each inherits margin_mm (default 1.5), a safe-area inset held back from every edge —
without it the design runs edge-to-edge and the least registration drift clips it.
Override per request with size, or w/h/gap directly.
Gotchas confirmed on macOS
0x08"out of ribbon" is a latch.SET RIBBON OFFalone will not clear it — it also needs aFORMFEED(costs one blank label).recover()/POST /recoverdoes this.- The status byte lies for 1–3 s after a print (transient
0x20/0x31/0x45). Never judge a print by the immediate post-print byte; poll until it settles to0x00. VOID0stamps on chipless stock. If RFID encoding is armed, every label without a chip gets voided.SET RFID OFFstops it;/print-testis the safe route on plain thermal. Note&DEFAULT,iappears to arm RFID, so don't send it casually.- Bidirectional USB is required.
UHF READ/QUERYreply with raw bytes on the bulk-IN endpoint, so CUPSlp -o rawis not sufficient — hence libusb. - After changing stock, calibrate on the printer itself: hold FEED while powering on and wait for 5 beeps.
SKU ↔ EPC scheme (✅ verified — and daemon.cs has it wrong)
The shop's real scheme, matching pliceclogs/rfid-daemon/index.js
(buildEpcPayload / parseEpcData), is a single 96-bit big-endian integer:
value = sku(14 digits) * 10^9 + releaseId
In decimal that reads as [digits 1-14 = SKU][digits 15-23 = releaseId], so any scanner
showing hex can be converted to decimal and split at digit 14. Verified against that
file's own worked example: 20260321001001 + 35332 → 20260321001001000035332.
⚠️
daemon.cs(Windows) uses a different, incompatible layout — 6 bytes SKU + 4 bytes release_id + an0xEC01marker. It was never checked against a real tag.parseEpcDatarejects it outright, so any tag the Windows daemon has written is invisible to the extension. ItsEpcclass needs updating to the scheme above.
epc_decode returns None for foreign/factory EPCs (e.g. E28069…), which is what
stops a stray tag on the shelf being reported as a real product SKU.
Verifying a tag independently
chafon.py is a read-only client for the Chafon gun over its CH340 serial bridge — an
independent check that a chip the Urovo wrote holds what we think it does:
./venv/bin/python chafon.py # inventory the field
Confirmed working: after programming, the Chafon reads back
0000044A5600D4CDA1FAB932 → sku=20260722180000, releaseId=424242, alongside the
untouched factory tags on the rest of the roll.
⚠️ Only one process can hold the serial port, so don't run this while the Node rfid-daemon is up.