feat(devices): K200L printer driver — live cover/paper status from the J-Speed LAN board

The park-buzi printer is a K200L (Xprinter/ICS XP-K200L; its LAN board and USB
descriptor call it "POS-80"). Its board serves the Rongta's five-row status table
under /prt_status.htm — but the reply carries no HTTP status line or headers, which
node:http rejects, so the Rongta driver could never read it and the unit was filed
in July as "no status page → generic driver" (reachability only).

New `k200l` driver (printer-k200l.ts): prints through the generic ESC/POS device
(same bytes, TCP 9100 or usblp) and reads the page over a raw socket, tolerant of
both the headerless and a proper HTTP reply. Mapping mirrors the Rongta: board
unreachable → offline; page not understood → degraded, never ready; any fault →
degraded naming it; USB → reachability floor. The Rongta driver is untouched.
Tests replay the captured headerless page (devices suite 76). Live against the
lab unit: ready; with the cover open the board reports cover open, paper out,
off-line.

Wiki: new k200l-printer entity (names, network setup from factory
192.168.123.100, board quirks, status page, what it means for park-buzi — over
USB the app never saw cover/paper state at all), cross-links on the Rongta,
status-monitoring, USB-transport and WSL-networking pages (parking-net pinned to
eth1 while the LAN NIC is eth0), index.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-09 12:34:59 +02:00
parent 7e21cf057e
commit 552d87d75b
11 changed files with 645 additions and 5 deletions
+15 -1
View File
@@ -2,7 +2,7 @@
type: concept
tags: [parking, printer, device, monitoring, reliability]
sources: []
updated: 2026-06-14
updated: 2026-09-09
---
# Printer status monitoring
@@ -90,3 +90,17 @@ reads. Full repo typechecks.
Yes/No and degrade on anything unexpected, so this is a confidence check, not a blocker.
- Tying a `degraded`/`offline` entry-dispenser into [[printer-roles-failover]] failover and the
(not-yet-built) entry flow's all-printers-down policy ([[device-input-flow]]).
## K200L — a second status-page board, and a fetch that node:http can't do (2026-09-09)
The park-buzi printer turned out to be a **K200L** (Xprinter/ICS XP-K200L; LAN board "J-Speed
Ethernet WebConfig 1.02", self-named "POS-80"). Its board serves the **same five decoded Yes/No
rows** as the Rongta page — under **`/prt_status.htm`**. Two things kept it invisible until now:
the Rongta driver only knew `/prn_stat.htm` (so in July the unit was filed as "no status page →
generic driver"), and the board's reply carries **no HTTP status line or headers** (HTTP/0.9
style), which `node:http` rejects outright and `curl` shows as an empty `000`. The new **`k200l`**
driver (`printer-k200l.ts`) prints through the generic ESC/POS path and reads the page over a raw
TCP socket, tolerant of both reply shapes; mapping is the Rongta one (unreachable → offline; page
not understood → degraded, never ready; any fault → degraded naming it; else ready; USB →
reachability floor). Bench-verified: cover open → amber "cover open, paper out, printer off-line".
Tests replay the captured headerless reply. Full device notes: [[k200l-printer]].
+46 -2
View File
@@ -2,7 +2,7 @@
type: concept
tags: [parking, device, printer, transport, usb, escpos, provisioning]
sources: []
updated: 2026-08-30
updated: 2026-09-09
status: settled
---
@@ -134,6 +134,11 @@ preselecting the first present device; a saved-but-unplugged path stays selectab
"saved — not present now"; zero devices found falls back to the free-text path + a check-the-cable
hint. The transport option label no longer hardcodes lp0.
> **Superseded 2026-09-09:** the XP-K200L DOES serve a status page — the same table as the Rongta,
> at **`/prt_status.htm`**, without HTTP headers. It now has its own **`k200l`** driver (raw-socket
> fetch; LAN = live cover/paper status, USB = reachability floor). See [[k200l-printer]]. The note
> below is kept for the record.
>
> Driver-choice note for this clone: the ICS XP-K200L does NOT serve the Rongta `/prn_stat.htm`
> status page (checked on hardware at 10.0.10.11 — print socket 9100 open, status page absent),
> so on NETWORK the honest driver is **cashino** (reachability-only monitoring); under `rongta`
@@ -189,6 +194,44 @@ itself is redone.
> = `usblp_open`'s bidirectional read submit failed (printer endpoint state). The theory below
> is kept for the record.
> **Lab reproduction FAILED to reproduce (2026-09-09, later the same day).** The same printer
> unit on the `park-lab` box (a real Linux host, the booth's exact image `stage-2d9bb15`, the
> prod compose with the `/dev/usb` bind-mount, the dev DB snapshot with the USB printer added as
> `booth-receipt`, cards printed via the subscription "Reprint card" path): paper out → open
> cover → load roll → close cover → reprint — **no error, status never stuck offline.** So the
> printer, the app's USB transport and the compose wiring are cleared in isolation. What is left
> is park-buzi's own environment (kernel/USB stack, the physical USB port/hub/cable/power at the
> booth) and/or that container's *history* (weeks of uptime before the first failure — a leaked
> handle needs a prior timeout to exist; a fresh container has none).
>
> **Status: park-buzi closed (staff shortage), everything shut down — evidence pending.** The
> evidence is on the booth's DISK and survives shutdown/reboot: Docker keeps the container log
> under `/var/lib/docker/containers/<id>/`, the kernel journal is persistent. **The day the box
> powers on again (or lands on the bench), pull these FIRST, before deploying anything:**
>
> ```bash
> # 1. the app's own record: the exact error text at every offline/ready transition
> docker logs park-buzi-server-1 2>&1 | grep -E "device-monitor:.*printer.*-> (offline|ready)"
> # 2. what the HOST kernel saw around those times (usblp errors, resets, disconnects)
> sudo journalctl -k --since "-30 days" | grep -i -E "usblp|usb 1-|usb 2-|disconnect|reset"
> # 3. the physical path: hub or direct port? (and note which PSU feeds the printer)
> lsusb -t
> ```
>
> Reading (1): `EBUSY` = a handle stuck inside the server process (usblp allows ONE opener;
> fits "container restart fixes it") → look at the `withTimeout` leak below; `usb open timeout`
> = `open()` blocks in the kernel; `EIO` = `usblp_open`'s bidirectional read submit failed
> (printer/link state). Reading (2): any `USB disconnect` / `reset` / `usblp1: removed` at the
> transition times means the LINK dropped at the booth (cable/port/hub/power) even though the
> unit never dropped on the bench.
>
> **Follow-ups that need no booth (proposed, not built):** (a) `withTimeout` in
> `printer-escpos.ts` abandons the FileHandle when an open/write times out — close it when the
> underlying promise eventually settles, so a timeout can never leave the node held; (b) make
> the monitor self-document the next occurrence: after N consecutive offline polls on a USB
> printer, log the errno, `ls -la /dev/usb`, and who holds the node, so the next failure anywhere
> in the fleet carries its own diagnosis without a person at the booth.
**Not confirmed on hardware — and now contradicted by the bench (above).** The original plan to
confirm at the next occurrence, BEFORE restarting anything:
```bash
@@ -212,7 +255,8 @@ whether the Bus/Device number changes.
passthrough + a udev rule pinning a stable symlink name — reintroduces the renumbering fragility
the directory bind-mount was chosen to avoid, so only worth doing alongside (1)/(2), not instead.
**Printer identity — IDENTIFIED 2026-09-09.** The failing unit is on the dev bench: USB
**Printer identity — IDENTIFIED 2026-09-09.** The failing unit is on the dev bench: a **K200L**
(Xprinter/ICS XP-K200L family — see [[k200l-printer]] for the LAN setup and its status page): USB
`1fc9:2016`, product string **"Printer POS-80"** (0x1fc9 = NXP, the printer's USB controller chip;
"POS-80" is the generic 80 mm ESC/POS designation — no brand in the descriptor, which is why the app
shows "Generic"). Seen via `usbipd list` on the Windows host (busid 8-1). **Dev-bench caveat:** the
+10 -1
View File
@@ -2,7 +2,7 @@
type: reference
tags: [parking, dev-environment, networking, wsl, troubleshooting]
sources: []
updated: 2026-06-15
updated: 2026-09-09
---
# WSL2 Dev Networking (for device testing)
@@ -110,6 +110,15 @@ trap:
Verified on hardware (2026-06-15): after the hook, `10.0.10.121` pings and the real [[lpr-camera]]
Hikvision driver pulls a snapshot with **no** source-forcing (`localAddress` becomes optional).
> **It bit again, 2026-09-09 — and the fix was pinned to the wrong NIC.** Configuring the
> [[k200l-printer]] meant adding `192.168.123.101` beside `10.0.10.203` on the mirrored LAN NIC;
> WSL then sourced 10.0.10.x traffic from the 192.168.123 address: `ping` fine, every HTTP
> connect timing out, `ip route get 10.0.10.7` showing `src 192.168.123.101`. `parking-net.service`
> was active but pins **`eth1`**, and the mirrored LAN NIC is **`eth0`** on this box now — so the
> boot fixer was a no-op. Run `deploy/wsl-fix-route-source.sh eth0` (and fix the unit's argument),
> or `curl --interface 10.0.10.203 …` as a one-off. Interface names are not stable across WSL
> reboots/NIC changes; the script accepts the NIC as an argument for exactly this reason.
## On the real appliance: multi-subnet is a deployment config, not a WSL hack
Production is a **dedicated hardened Linux appliance** ([[disk-os-hardening]]), so the WSL story