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>
206 lines
10 KiB
Markdown
206 lines
10 KiB
Markdown
# 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>"`.
|
||
|
||
## Install on a new Mac (one command)
|
||
|
||
```bash
|
||
git clone ssh://git@100.71.119.27:222/monster/yourovo.git
|
||
cd yourovo/mac && ./install.sh
|
||
```
|
||
|
||
That installs libusb + the venv, then registers a **launchd agent** so the daemon starts
|
||
at login and restarts itself if it ever dies. **No Terminal window to keep open, and
|
||
nothing to click** — after this it is simply always running. Verified: `kill` the process
|
||
and launchd has it serving again within seconds.
|
||
|
||
Re-run `./install.sh` any time to pull the latest commit and reload. `./install.sh
|
||
uninstall` removes the agent.
|
||
|
||
| | |
|
||
|---|---|
|
||
| logs | `tail -f ~/Library/Logs/pricegod-labeld.log` |
|
||
| restart | `launchctl kickstart -k gui/$(id -u)/com.pricegod.labeld` |
|
||
| check | `curl -s localhost:7790/devices` |
|
||
|
||
For a foreground run while developing (agent must be stopped first), use `./setup.sh`
|
||
then double-click **`start-daemon.command`**.
|
||
|
||
## 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-encode` on 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.
|
||
|
||
The extension calls only six endpoints:
|
||
|
||
| endpoint | owner today |
|
||
|---|---|
|
||
| `/status` `/write-tag` `/read-tag` | ✅ this service |
|
||
| `/weight` `/scale/start` `/scale/stop` | ❌ Node only — DYMO M10 postal scale over USB HID |
|
||
|
||
So the gap to a genuinely single service is **the postal scale, and nothing else**. Once
|
||
`/weight` and `/scale/*` are ported here (the M10 is USB HID `0922:8003/8004`; the parsing
|
||
is ~130 lines of `index.js`), the Node daemon can be retired for the extension's purposes
|
||
and there is one process, one venv, one launchd agent.
|
||
|
||
Until then the honest position is: two services, or no scale. Running this one on
|
||
`--port 7791` behind the Node daemon works but gives one URL and *two* processes, which
|
||
defeats the point of a single launchd agent.
|
||
|
||
## 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
|
||
|
||
1. **`0x08` "out of ribbon" is a latch.** `SET RIBBON OFF` alone will *not* clear it — it
|
||
also needs a `FORMFEED` (costs one blank label). `recover()` / `POST /recover` does this.
|
||
2. **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 to `0x00`.
|
||
3. **`VOID0` stamps on chipless stock.** If RFID encoding is armed, every label without a
|
||
chip gets voided. `SET RFID OFF` stops it; `/print-test` is the safe route on plain
|
||
thermal. Note `&DEFAULT,i` appears to *arm* RFID, so don't send it casually.
|
||
4. **Bidirectional USB is required.** `UHF READ`/`QUERY` reply with raw bytes on the
|
||
bulk-IN endpoint, so CUPS `lp -o raw` is not sufficient — hence libusb.
|
||
5. **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 + an `0xEC01` marker. It was never checked against a real tag.
|
||
> `parseEpcData` rejects it outright, so **any tag the Windows daemon has written is
|
||
> invisible to the extension**. Its `Epc` class 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:
|
||
|
||
```bash
|
||
./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.
|