feat: key real printers by serial number, not the volatile USB id — closes #40#41
Merged
Conversation
labelle's usb_id embeds the kernel-assigned Bus/Device address, which is reassigned on every re-enumeration — replug, reboot, and this app's own USB power-cycling (power off when idle, on at page load). Keying a printer by it means any saved per-printer state orphans the moment the device re-enumerates. Introduce _printer_id(dev): prefer "serial:<sn>" (stable across re-enumeration), fall back to the usb_id only when the device reports no serial. Use it both in list_printers and in the print/ print_bitmap device resolution so the emitted id always resolves back. Lays the stable-identity groundwork for per-printer settings (#20) and the status/alias work in #38. Closes #40. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the backend’s real-printer identity scheme so USB printers are keyed by a stable serial-number-based ID (serial:<sn>) when available, falling back to the existing volatile USB bus/address ID only when a serial is not reported. This prevents printer identity drift across USB re-enumeration (replug/reboot/power-cycle), which is foundational for durable per-printer persisted settings.
Changes:
- Add
_printer_id(dev)and use it consistently for real-printer IDs inlist_printers(),print_label(), andprint_bitmap(). - Add backend tests covering serial-vs-fallback behavior, list emission, print resolution by serial, and non-match error behavior.
- Update architecture docs to describe the new real-printer ID format; bump project version
1.7.0 -> 1.8.0.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
server/printer_service.py |
Introduces _printer_id() and switches real-printer list/resolve logic to stable serial-based IDs. |
server/tests/test_printer_service.py |
Adds coverage ensuring serial IDs are emitted and resolved correctly (with fallback and error cases). |
docs/ARCHITECTURE.md |
Documents the real-printer ID format change (serial preferred; bus/address fallback). |
package.json |
Increments version to 1.8.0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
szrudi
added a commit
that referenced
this pull request
Jun 13, 2026
Brings in #41 (serial ids), #42 (virtual ids), #44 (default printer selection), #45 (shared state_store + UTF-8). Conflict resolution: - state_store.py / usb_power.py / test_state_store.py: took main's (the #45 versions with UTF-8 read/write + corrected comment + non-ASCII test) — they supersede #39's older extracted copies. - test_smoke.py: kept #39's (superset — adds printer_settings module and the /api/printers/<id>/settings route on top of #45's ownership guard). - useLabelStore setAvailablePrinters: combined main's default-printer selection with #39's availablePrintersLoaded flag. - SettingsBar + its tests, useLabelStore tests, ARCHITECTURE.md: unioned #44's selector changes with #39's persist/load-gate. Frontend 81, backend 276, build green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40.
Why
A real printer's
PrinterInfo.idwas labelle'susb_id:f"Bus {bus:03} Device {address:03}: ID {vendor_product_id}"addressis the kernel-assigned USB device address, reassigned on every re-enumeration — replug, reboot, and (critically) this app's own USB power-save cycle (power off when idle → on at page load). Keying anything by it means the identity drifts constantly on the Pi deployment.What
_printer_id(dev): preferserial:<serial_number>(stable across re-enumeration — the Dymo LabelManager PnP reports one), fall back to theusb_idonly when no serial is present. Used in both places that matter so they always agree:list_printers()— the id emitted to the client,print_label/print_bitmap— resolving a request'sprinter_idback to a device.USB power feature is untouched (keyed by hub/port via VID:PID). Virtual printers untouched (
virtual:<name>already stable). Client is untouched — the id is opaque to it.Why now (stacked under the per-printer-settings PR)
This is the stable-identity foundation that makes per-printer settings (#20, PR #39) actually durable on the hardware — otherwise saved settings would orphan after each idle power-off→on. PR #39 will rebase on top of this and key off the stable id for free. Also feeds the status/alias work in #38.
Tests
_printer_idserial vs usb_id-fallback;list_printersemits the serial id; print resolves a device by its serial id and raises on a non-match. Full backend suite green (223).Versioning
None — stacked under #39; left at
1.7.0so merging triggers no separate release. Ships in the single v1.8.0 that #39 cuts.Note
Edge case left as documented behavior: a device reporting no serial falls back to the volatile usb_id (unavoidable without a serial), and two devices reporting identical serials would collide — neither applies to the single Dymo target.
🤖 Generated with Claude Code