docs(wiki): USB printer cover-open field bug writeup; add art-docker-station lab box
Build desktop / desktop (push) Successful in 5m14s
Build & push images / images (push) Successful in 3m1s
CI / check (push) Successful in 43s

Printer investigation (park-buzi): cover-open on the USB thermal
printer wedges its status offline/faulty, surviving a full reboot,
recoverable only via `docker restart server`. Traced sendRawUsb/
probeUsb end-to-end — no persistent handle in the app layer, so the
leading theory is the container's /dev/usb directory bind-mount
retaining a stale view across the printer's physical re-enumeration.
Not yet confirmed on hardware; documented with repro/confirmation
commands and ranked candidate fixes.

Also registers a new lab bench box, "art-docker-station", as a Komodo
Stack (dev tier, same shape as park-lab, its own isolated secret refs).

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-08-30 18:11:34 +02:00
parent 2910672b5a
commit ea8fe22969
4 changed files with 138 additions and 3 deletions
+66 -1
View File
@@ -2,7 +2,7 @@
type: concept
tags: [parking, device, printer, transport, usb, escpos, provisioning]
sources: []
updated: 2026-07-06
updated: 2026-08-30
status: settled
---
@@ -140,5 +140,70 @@ hint. The transport option label no longer hardcodes lp0.
> the monitor would mark a perfectly working printer offline/degraded. Over USB the two drivers
> behave identically (reachability floor), so either works post-fix. See [[rongta-printer]].
## Field bug — cover-open re-enumeration wedges the container's `/dev/usb` view; only `docker restart`, not a host reboot, clears it (investigated 2026-08-30, unconfirmed root cause)
**Symptom (park-buzi, unknown/"Generic" USB printer, model not yet identified — see below):** every
time the booth operator opens the printer's paper-roll cover to reload paper, the printer's status
goes `offline`/faulty in the app and **never self-recovers** — not after the cover closes, not after
a full appliance reboot. The only fix found so far is SSH in and `docker restart server`.
**Ruled out at the application layer.** Traced `sendRawUsb`/`probeUsb` in `printer-escpos.ts`: every
print AND every poll tick (`device-monitor.ts` 8s / `printer-monitor.ts` 5s) does a fresh
`open()` → write/probe → `close()` against the configured `devicePath`. **No fd, socket, or driver
instance is held across calls** — `driver.create(config)` is a throwaway object with no persistent
handle. So a naive "stale Node file descriptor" explanation does not fit this codebase; the
app-layer retry-by-fresh-open-every-poll should self-heal within one poll cycle if the kernel's view
of the device node is current.
**Leading hypothesis: the container's bind-mount of `/dev/usb`, not the Node process, holds the
stale state.** Docker Compose wires the printer in as a **directory bind-mount**
(`docker-compose.prod.yml`, `volumes: - /dev/usb:/dev/usb`), chosen deliberately (per its own
comment) so the app survives the printer renumbering to a different `lpN`. But many USB thermal
printers cut power to their own USB interface board when the cover-open microswitch trips (a
hardware safety/power feature, not just a status flag) — the printer drops off the bus and
re-enumerates, potentially as a new device node, when the cover closes. The **host** kernel picks
this up fine; the **container's mount namespace**, once established, is a known Docker/OverlayFS
sharp edge for `/dev` subtree bind-mounts — it can keep resolving the old node until the mount
itself is redone.
- `docker restart server` recreates the container's mount namespace → the `/dev/usb` bind-mount is
redone against current host state → the new node is picked up → fixed.
- A full host reboot restarts the container too (`restart: always`), but as a boot-time race: if the
container starts before the USB subsystem finishes settling, or the printer re-enumerated some
time *before* the reboot and Docker doesn't necessarily redo an already-satisfied bind-mount
target on a policy-driven restart, the container can come back up still bound to the pre-incident
view. This matches the exact reported asymmetry (reboot doesn't fix it; explicit restart does).
**Not yet confirmed on hardware** — this is the leading theory, not a verified root cause. To
confirm at the next occurrence, BEFORE restarting anything:
```bash
# host:
ls -la /dev/usb/ && stat /dev/usb/lp1
# container:
docker exec server ls -la /dev/usb/ && docker exec server stat /dev/usb/lp1
```
A major:minor or inode mismatch between host and container is the smoking gun. Also worth
capturing on the lab RONGTA (different printer, but same cover-open mechanism is plausible):
`watch -n1 lsusb` + `sudo dmesg -w | grep -i -E 'usb|disconnect'` while cycling the cover, to see
whether the Bus/Device number changes.
**Candidate fixes, not yet implemented** (ranked cheapest-to-most-invasive):
1. A host-side watchdog/udev rule that detects re-enumeration of this printer (match vendor:product
ID) and runs `docker restart server` automatically — turns the manual SSH fix into a self-healing
one without touching app code.
2. Same idea but event-driven via a udev rule or systemd path unit watching `/dev/usb`, rather than
polling.
3. Switch the compose device wiring from the directory bind-mount to a specific `--device=` cgroup
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.
**Open sub-question — printer identity.** The park-buzi unit shows as "Generic (unknown)" in the
app; not yet identified by vendor/product ID. Lab reproduction uses a **RONGTA** unit instead (not
the same hardware), so the lab cannot currently reproduce the park-buzi symptom directly — only
validate the general re-enumeration mechanism. Commands to identify the real park-buzi printer next
time it's reachable via SSH: `lsusb`, `udevadm info -q property -n /dev/usb/lp1`, `udevadm info -a
-n /dev/usb/lp1`. This mirrors the same discovery gap already noted above under "Device discovery"
(sysfs `ieee1284_id` enrichment) — once identified, fold the model into that mechanism's coverage.
Related: [[rongta-printer]], [[printer-status-monitoring]], [[printer-roles-failover]],
[[appliance-provisioning]], [[network-isolation]], [[technology-stack]].