From f98a4a70464f1758ba3abf469ad0fb04b728b2bd Mon Sep 17 00:00:00 2001 From: Fedir Date: Tue, 13 Oct 2026 15:31:52 -0400 Subject: [PATCH] feat(printer): support PT-P710BT over USB --- .github/workflows/deploy.yml | 3 +- CMakeLists.txt | 3 +- README.md | 70 ++++- docs/ADDING-A-MODEL.md | 73 +++-- .../.openspec.yaml | 2 + .../2026-10-13-add-p710bt-support/design.md | 138 +++++++++ .../2026-10-13-add-p710bt-support/proposal.md | 36 +++ .../specs/batch-printing/spec.md | 31 ++ .../specs/printer-device/spec.md | 77 +++++ .../specs/server-architecture/spec.md | 22 ++ .../2026-10-13-add-p710bt-support/tasks.md | 40 +++ .../validation.md | 82 ++++++ openspec/config.yaml | 5 +- openspec/specs/batch-printing/spec.md | 18 +- openspec/specs/printer-device/spec.md | 56 +++- openspec/specs/server-architecture/spec.md | 14 +- scripts/smoke-image.py | 149 +++++++++- src/device_usb.c | 77 +++-- src/device_usb.h | 5 + src/driver.c | 45 ++- src/main.c | 40 ++- src/protocol.c | 30 ++ src/protocol.h | 10 +- src/tables.c | 7 +- src/tables.h | 2 +- tests/FINISHINGS.md | 22 +- tests/P710BT-ACCEPTANCE.md | 278 ++++++++++++++++++ tests/test_device_usb.c | 146 ++++++++- tests/test_driver.c | 180 ++++++++++-- tests/test_png_geometry.c | 112 ++++++- tests/test_protocol.c | 22 +- tests/test_raster.c | 25 ++ tests/test_tables.c | 10 + 33 files changed, 1667 insertions(+), 163 deletions(-) create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/.openspec.yaml create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/design.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/proposal.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/specs/batch-printing/spec.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/specs/printer-device/spec.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/specs/server-architecture/spec.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/tasks.md create mode 100644 openspec/changes/archive/2026-10-13-add-p710bt-support/validation.md create mode 100644 tests/P710BT-ACCEPTANCE.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9634c55..b787276 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,8 @@ jobs: run: | set -e sudo apt-get update && sudo apt-get install -y --no-install-recommends cups-ipp-utils - python3 scripts/smoke-image.py ptouch-deploy:check + python3 scripts/smoke-image.py ptouch-deploy:check --model PT-2730 + python3 scripts/smoke-image.py ptouch-deploy:check --model PT-P710BT - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6221233..dbeb6eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,8 @@ if(NOT PTOUCH_CORE_ONLY) target_link_directories(test_driver PRIVATE ${PAPPL_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) target_compile_options(test_driver PRIVATE ${PAPPL_CFLAGS_OTHER}) add_test(NAME driver_finishings COMMAND test_driver) - set_tests_properties(driver_finishings PROPERTIES TIMEOUT 90) + add_test(NAME driver_finishings_p710bt COMMAND test_driver PT-P710BT) + set_tests_properties(driver_finishings driver_finishings_p710bt PROPERTIES TIMEOUT 90) add_executable(test_pappl_raster tests/test_pappl_raster.c) target_link_libraries(test_pappl_raster PRIVATE ${PAPPL_LIBRARIES} ${PNG_LIBRARIES}) target_include_directories(test_pappl_raster PRIVATE ${PAPPL_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) diff --git a/README.md b/README.md index bf9b188..6a74ed3 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,10 @@ The Brother raster protocol and device table are reused from ## Capabilities -Only the **Brother PT-2730** is print-supported and hardware-certified; -other recognized models do not receive print output. +**Brother PT-2730** remains the default supported, hardware-certified model. +**PT-P710BT** is hardware-certified over USB on 9/12/18 mm tape, selected +explicitly below. Its 6/24 mm coverage is software-only. Other recognized models do not +receive print output. - Prints 1:1 within the tape's imageable width, honoring submitted page geometry; PNG length follows its image when a concrete media length is omitted. @@ -51,13 +53,18 @@ Three layers: ## Supported devices `src/tables.c` recognizes the P-touch USB ids below (transcribed from upstream -`ptdevs[]`). **Only the PT-2730 is print-supported and hardware-certified.** Every -other entry remains recognition metadata and is **not print-supported**: discovery -reports it in diagnostics without offering a printable device. Per-model flag -dispatch (PackBits, P700 init) does not exist yet. All known models are 180 dpi / -128 px max. `PTOUCH_MODEL` defaults to `PT-2730`; unknown or unsupported values -fail before discovery. Multiple matching PT-2730 units block printing as ambiguous, -while a single unit can be unplugged and reattached without replacing its queue. +`ptdevs[]`, plus PT-P710BT). Only PT-2730 and PT-P710BT have implemented print +paths. All other entries remain recognition metadata: discovery reports them +without offering a printable device. Shared protocol flags do not grant support. +All known models are 180 dpi / 128 px max. + +`PTOUCH_MODEL` defaults to `PT-2730` when absent or empty. Select the new printer +with `PTOUCH_MODEL=PT-P710BT`; unknown or unsupported values fail before discovery. +The server creates one stable queue for the selected model and never falls back +when it is absent. One of each supported model can coexist; two matching units +of the selected model block printing as ambiguous. USB renumbering after replug +does not replace the queue. Discovery names an attached but unselected printer +and its required `PTOUCH_MODEL` value. | Model | USB id | flags | Print-supported | |-------|--------|-------|-----------------| @@ -67,6 +74,7 @@ while a single unit can be unplugged and reattached without replacing its queue. | PT-1230PC (PLite) | 04f9:2030 | `PLITE` | recognized only | | PT-2430PC (PLite) | 04f9:2031 | `PLITE` | recognized only | | **PT-2730** | **04f9:2041** | `NONE` | **certified** | +| **PT-P710BT** | **04f9:20af** | `RASTER_PACKBITS \| P700_INIT` | **certified on 9/12/18 mm tape** | | PT-E500 | 04f9:205f | `RASTER_PACKBITS` | recognized only | | PT-H500 | 04f9:205e | `RASTER_PACKBITS \| P700_INIT` | recognized only | | PT-P700 | 04f9:2061 | `RASTER_PACKBITS \| P700_INIT` | recognized only | @@ -104,7 +112,7 @@ ctest --test-dir build-app --output-on-failure `Dockerfile` supplies the development dependencies; `Dockerfile.deploy` builds all targets and runs CTest before installing the app into the runtime image. -These tests use no printer; physical behavior requires separate PT-2730 checks. +These tests use no printer; physical behavior requires acceptance on the affected model. ### Printing notes for clients @@ -208,16 +216,52 @@ Run state lives in memory. Restart during an unfinished run is unsupported; finish it deliberately first. Interrupted output requires printer power-cycle followed by server restart, as described below. +The finishing contract and canonical 32/52/76/120/128-dot tape windows are shared +by both implemented models. PT-2730 retains its legacy eight-byte print +information and uncompressed rows. PT-P710BT uses `ESC i a` raster mode, +automatic status notification, thirteen-byte `ESC i z` print information with +the actual destination row count, a 14-dot feed margin, and TIFF PackBits rows. +Its first page has indication 0 on a freshly initialized closed run; later +pages and healthy cross-batch continuation use 1. Only a fresh P710BT run sends +the 100-byte invalidation and initialization; continuation sends neither. +Finalization still uses deferred FF or late chain-off/EJECT for both models. +The P710BT integrated [hardware gate](tests/P710BT-ACCEPTANCE.md) passed all four +finishing policies, held-run continuation, wrong-media rejection, offline queue +recovery, and explicit uncertain-output recovery. It used 15 complete labels, +including four operator-requested repeat labels. Direct geometry probes remain +separate evidence; 6/24 mm have software coverage only. + ## Configuration Cutting uses the job's standard finishing choice. `PTOUCH_PRECUT`, `PTOUCH_CUT_MODE`, and `PTOUCH_WIDTH_GUARD` are retired and ignored. Mandatory batch precut and strict geometry rejection are intrinsic; no replacement server -knob is needed. `PTOUCH_MODEL` defaults to the supported PT-2730. +knob is needed. `PTOUCH_MODEL` defaults to PT-2730; configure +`PTOUCH_MODEL=PT-P710BT` for the P710BT USB path. This selects one queue, not +all attached printers, and never enables another model as a fallback. `PTOUCH_DEVICE_URI` overrides device discovery with an explicit URI (e.g. -`socket://127.0.0.1:9100`) for testing without a PT-2730; the printer registers -but only connects on an actual print. +`socket://127.0.0.1:9100`) for testing without USB hardware; queue identity and +protocol still follow `PTOUCH_MODEL`, and the printer connects on an actual print. +An explicit `ptouch://Brother/` override must exactly match the configured +supported model; conflicting or unsupported custom USB URIs fail before commands. + +### USB diagnostics + +`ptouch-app probe` enumerates only the custom USB scheme and reports whether +exactly one selected VID/PID is present. It never opens the printer or sends +commands. Set `PTOUCH_MODEL` consistently for diagnostics and the server: + +```sh +PTOUCH_MODEL=PT-P710BT ptouch-app probe +``` + +`PTOUCH_MODEL=PT-P710BT ptouch-app smoke` opens the selected printer and sends +initialization (`ESC @`), then checks that those bytes reached USB. Use it only +on a known closed tape run: initialization can destroy a held run. It is not a +read-only status preflight or the isolated IPP/socket image smoke used in CI. +Neither diagnostic chooses a different attached model when selection is absent +or ambiguous. ## Deploy diff --git a/docs/ADDING-A-MODEL.md b/docs/ADDING-A-MODEL.md index f33aa06..967e5a8 100644 --- a/docs/ADDING-A-MODEL.md +++ b/docs/ADDING-A-MODEL.md @@ -2,8 +2,11 @@ This guide explains how to add a Brother P-touch model to the device table. A new entry makes the model **recognized in metadata**. Discovery and printing remain -restricted to the explicitly supported PT-2730. A table row does not grant -print support. See the support gate below. +restricted to the explicitly supported PT-2730 and PT-P710BT paths. PT-2730 +remains the default; P710BT requires `PTOUCH_MODEL=PT-P710BT`. A table row does +not grant print support. P710BT integrated USB acceptance passed on 9/12/18 mm tape; +6/24 mm remain software-only coverage. +See the support gate below. ## The `pt_dev` fields @@ -43,17 +46,49 @@ Copied from `src/tables.h`: #define FLAG_PLITE 0x04u /* P-Lite mode (not driven) */ ``` -- `FLAG_NONE`: plain uncompressed raster, PT-2730-style. This is the only path - the print code drives today. +- `FLAG_NONE`: plain uncompressed raster, used by PT-2730. - `FLAG_RASTER_PACKBITS`: `sendraster` must wrap each line in a single uncompressed PackBits run (`pt_cmd_sendraster_packbits` in `src/protocol.h`). - `FLAG_P700_INIT`: `rasterstart` uses the P700 variant (`1b 69 61 01`) instead of the default. - `FLAG_PLITE`: the model is in P-Lite mode, which this project does not drive. -Set the flags to describe the model accurately even though the driver does not yet -dispatch on them. They document the protocol the model needs once per-model -dispatch lands. +Flags describe encoding and raster-mode commands, not print support. The driver +uses them only after resolving an explicitly supported model. P710BT has both +`FLAG_RASTER_PACKBITS` and `FLAG_P700_INIT`; it also needs the model-specific +extended page setup below. Sharing these flags does not enable another model. + +## The implemented P710BT command path + +P710BT metadata is `04f9:20af`, 128 dots, normal 180 dpi. Its page setup differs +from PT-2730's legacy eight-byte `ESC i c` command: + +| Operation | Builder / command | +| --- | --- | +| Fresh-run preparation | `pt_cmd_invalidate`: 100 zero bytes, then `pt_cmd_init`: `1b 40`. Omit both when continuing a healthy held run. | +| Raster mode | `pt_cmd_rasterstart` with `FLAG_P700_INIT`: `1b 69 61 01`. | +| Automatic status | `pt_cmd_auto_status(o, true)`: `1b 69 21 00`. | +| Extended page information | `pt_cmd_printinfo_extended`: `1b 69 7a 84 00 mm 00 r0 r1 r2 r3 page 00`. `r0..r3` are the validated destination page's row count, little-endian. | +| Compression | `pt_cmd_compression(o, 2)`: `4d 02` selects TIFF PackBits. | +| Raster line | `pt_cmd_sendraster_packbits` wraps each complete 16-byte head line as `47 11 00 0f` followed by those bytes, a single literal run. | +| Feed margin | `pt_cmd_margin(o, 14)`: `1b 69 64 0e 00`. | + +The extended page command uses width-valid/recovery flags `84`, unspecified +media kind, zero fixed-length and reserved fields. `page` is 0 for a freshly +initialized printer run and 1 for subsequent pages, including the first page of +an IPP batch continuing a healthy held run. Cross-batch indication and finishing +were verified by the integrated physical gate; separate-label probes did not +verify them. Normal resolution and finishing use the existing mode/advanced +builders and batch lifecycle, without half-cut or cut-every-N commands. + +Direct USB geometry probes on 2026-10-12 used this setup with first-page +indication 0, normal resolution, autocut, chain-off, and terminal EJECT. The +operator confirmed complete 52-, 76-, and 120-dot rectangles on 9, 12, and 18 mm +tape. The existing 32/52/76/120/128-dot table is shared by both supported paths; +6 and 24 mm remain software/documentation coverage for P710BT. These observations +establish usable geometry, not integrated finishing, continuation, or recovery +certification. The change's validation summary records retained probe evidence +and any unavailable files separately. ## Adding the entry @@ -97,18 +132,18 @@ do not enable it merely by adding metadata. ## Caveats: recognized is not supported -A new entry is **untested and not print-supported** until: - -- The model is verified on real hardware. Only the PT-2730 (`04f9:2041`) is - hardware-certified. The status read, media detection, and print geometry are all - validated against it and nothing else. -- For PackBits or P700 models (`FLAG_RASTER_PACKBITS`, `FLAG_P700_INIT`), per-model - flag dispatch exists in the driver. The current supported-model gate rejects - these models before output; it does not send them PT-2730 bytes. - -Until then a new row only adds lookup metadata. `pt_usb_supported_model` and the driver identity check must remain -restricted to the implemented, validated print path, and rejected models must -receive no output. +A new entry only adds lookup metadata. Enabling another model requires an +implemented protocol path, consistent queue/USB identity and status handling, +software regressions, and an explicit supported-model gate change. PT-2730 +(`04f9:2041`) and P710BT (`04f9:20af`) are hardware-certified. P710BT's integrated +gate covers 9/12/18 mm USB printing, finishing, continuation, and recovery; +6/24 mm remain software-only. Recognition, pure command tests, and separate +geometry probes alone do not certify another model. + +`pt_usb_supported_model` and the driver identity check must remain restricted +to these two implemented paths. Other catalog entries, including PackBits/P700 +models, remain unsupported and must receive no output. Do not broaden the gate +based on flags or silently fall back to an attached model. ## Source of new PIDs diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/.openspec.yaml b/openspec/changes/archive/2026-10-13-add-p710bt-support/.openspec.yaml new file mode 100644 index 0000000..5fcc9ec --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-13 diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/design.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/design.md new file mode 100644 index 0000000..eb75498 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/design.md @@ -0,0 +1,138 @@ +# Design + +## Context + +See [proposal.md](proposal.md) for motivation and the capability deltas for the behavioral contract. The implementation baseline is refactor commit `0603578` (#47), with the current architectural constraints consolidated in `openspec/specs/server-architecture/spec.md`. The older ADR subtree has been retired. + +[verified] `src/device_usb.c` accepts only the PT-2730 name and exact URI; discovery/opening select its PID directly. `src/main.c` builds its identity for both real USB and socket overrides, and `src/driver.c` repeats the identity restriction and PID-specific status count. Adding a device-table row alone cannot enable P710BT support. + +[verified] The current print callbacks deliberately defer each complete label's print command until the next actual page or successful end-job. They call the legacy raster-mode and eight-byte print-information builders and send uncompressed rows. `pt_printer_state` belongs to the queue; `pt_batch` has constant-size policy/lifecycle state. Page height is already known at `r_startpage`, including streamed jobs with an unknown total number of labels. + +### Prior P710BT evidence + +The USB probe ran on `vault` through the personal user's SSH connection on 2026-10-12. It selected only `04f9:20af`, queried clean loaded media, and sent normal 180 dpi data. Each label had 180 rows, explicitly selected TIFF PackBits, complete 16-byte head lines, autocut, chain-off, and a terminal EJECT. Status reported print-completed followed by receiving-ready with no errors. + +| Loaded tape | Separately printed rectangle widths | Operator observation | Existing imageable width retained | +| --- | --- | --- | --- | +| 9 mm | 50, 51, 52 dots | "all 3 printed fine" | 52 dots | +| 12 mm | 74, 75, 76 dots | "all 3 printed just fine" | 76 dots | +| 18 mm | 112, 116, 120 dots | "all 3 printed fine" | 120 dots | + +The first 76-dot fixture used intentional dashed reference lines and edge steps; its appearance was not a continuous-rectangle failure. The later three continuous rectangles are the relevant 12 mm observation. These sessions establish usable tested windows, not the absolute largest possible width or integrated batch/recovery behavior. The temporary probe initialized each separate label and therefore did not validate cross-job continuation or late chain-off. + +The source/captures/logs are on the local development VM, hostname `openclaw`, at `/tmp/p710bt-geometry-probe.py` and `/tmp/p710bt-{rectangles,9mm,18mm}/`; they are not paths on `vault`. Their existence and hashes were checked on 2026-10-13. This planning revision leaves the raw files there and explicitly accepts their temporary-storage lifetime until task 1.1 preserves them during apply. The durable planning record retains the operator's observations and hashes even if raw files disappear; missing files must be reported, not presented as retained captures. Their widest-label captures have SHA-256: + +- 9 mm / 52 dots: `711d2337f43c23fcd3eea84dcf5527cc03ef11451c02336f2e1b820b0353e323`. +- 12 mm / 76 dots: `7eb5be170e12c0f7c574c725b998655efb6f5e6ba6791aedc4d98e1e6bf13718`. +- 18 mm / 120 dots: `bc07c514b4cbdf0978e42bb75e12d80e54a96a4dfe9ae6d2a63964718032b591`. + +[Brother's raster reference](https://download.brother.com/welcome/docp100064/cv_pte550wp750wp710bt_eng_raster_102.pdf) supplies the P710BT USB identity, mode selection, extended page information, compression, and status definitions. Its narrower published print areas for these tapes differ from the demonstrated windows. The cause is undocumented; this design does not invent a firmware restriction or reduce successfully tested geometry. P710BT 6 and 24 mm physical geometry remains untested, with 32/128-dot values supported by documentation and software coverage. + +## Goals / Non-Goals + +**Goals:** + +- Carry one validated model identity from configuration through queue creation, USB access, status, and printing. +- Add the smallest model-specific protocol boundary needed by P710BT while reusing canonical geometry, packing, PNG placement, and finishing state. +- Preserve PT-2730 default behavior and byte-level regressions. +- Produce separately attributable core, adapter/socket, and physical acceptance evidence. + +**Non-Goals:** + +- A generic printer plug-in framework, simultaneous model queues, automatic model fallback, serial-number selection, or support inferred from catalog flags. +- Bluetooth, higher-resolution mode, half-cut, 3.5 mm tape, wide-head printers, or unrelated dependency-patch changes. +- Production configuration, publishing, or deployment as part of this planning workflow. + +## Decisions + +### 1. Select the model explicitly and use it throughout the existing USB scheme + +Extend the supported-model lookup to PT-2730 and PT-P710BT only. Keep absent/empty `PTOUCH_MODEL` selecting PT-2730. Add the P710BT metadata row with PID `20af`, 128 dots, 180 dpi, and the existing raster-mode/PackBits flags. Flags describe encoding; they do not grant support to other catalog entries. + +Use small helpers for supported URI/model resolution and identity formatting. Discovery counts only the configured model; opening resolves the supported model encoded by the queue's URI. Queue creation rejects a conflicting explicit USB override, while socket overrides remain usable for either selected model without USB status operations. Store the immutable selected `pt_dev` in the printer-owned extension so registration, idle status, and page callbacks use the same model. Derive diagnostics and synthetic device IDs from it; never identify a P710BT as PT-2730. + +Retain fresh libusb contexts, bounded bulk I/O, exactly-one matching VID/PID, and the guard before accessing opaque USB connection data. One P710BT plus one PT-2730 is not ambiguous; two units of the configured model are. Absence does not switch models. + +Discovery also identifies a supported but unselected attached model and explains its PTOUCH_MODEL selection. This is especially useful when the default PT-2730 queue is offline while only P710BT is attached. Emit the diagnostic during discovery, not as repeated idle-poll noise, and never substitute that printer for the configured one. + +Update the existing `probe` and USB `smoke` subcommands with the same selected identity, VID/PID, URI, and diagnostic text. `probe` stays enumeration-only. USB `smoke` intentionally sends initialization and can destroy a held run; document that it is used only on a known closed run and is not the read-only preflight for physical acceptance. Cover these subcommands using fake USB rather than adding physical smoke labels. + +Alternative: add a table row and broaden discovery indiscriminately. Rejected because it leaves model-specific opening, status, and queue identity inconsistent and could expose unimplemented catalog models. + +### 2. Add narrow protocol dispatch at page setup and raster output + +Preserve the legacy PT-2730 builders and their byte sequences. Add pure P710BT command builders for extended print information and explicit compression/automatic-status selection; reuse the existing raster-mode and PackBits-line builders. Select the extended setup explicitly for the implemented P710BT path, rather than assuming every catalog model with a common flag uses it. No generalized profile framework is needed for two supported paths. + +| Operation | PT-2730 | PT-P710BT | +| --- | --- | --- | +| Raster mode | Existing `ESC i R 01` | `ESC i a 01` | +| Page information | Existing eight-byte `ESC i c` | Thirteen-byte `ESC i z`: width, 32-bit little-endian page row count, first/subsequent indicator | +| Raster data | Existing uncompressed rows | Explicit `M 02`; 16-byte literal PackBits run per line | +| Automatic status | Existing behavior | Explicit notification enabled with `ESC i ! 00` | +| Feed margin | Existing behavior | Tested 14-dot normal-resolution margin | + +The P710BT setup uses the tested width-valid/recovery flag `84`, unspecified media-kind field, actual tape width, zero fixed-length field, and reserved bytes zero. Take row count from the validated raster page height; do not use job totals, source PNG height before placement, or the default media length. Enlarge setup command buffers to accommodate the extended command. + +Use page indication 0 for the first actual page of a freshly initialized closed printer run, and 1 for later pages or the first page of an IPP batch continuing a healthy open run. Compute this from actual-label progress and the prior run state, not the IPP batch boundary alone. The manual defines starting/other-page values; applying its other-page value to a held cross-batch run is a design assumption that the single-label probes did not test. Stages 2 and 4 explicitly verify it. If it fails physically, stop and revise the protocol decision before preparing another bounded trial; do not switch values or replay automatically. The spec requires continuation behavior rather than prescribing an unverified batch-reset rule. + +A fresh P710BT run uses the tested invalidate/initialize preparation; healthy continuation omits both. Ensure raster mode and automatic notification for each batch and send extended information, normal-resolution mode, feed margin, and compression selection before that page's rows. The immutable model in queue state drives both setup and line encoding. + +Do not send the unsupported cut-every-N or half-cut commands. Existing auto-cut, chain-on, FF, and late chain-off/EJECT are integrated through the current callbacks; their P710BT physical behavior is an acceptance item, not an assumption established by the single-label probe. + +Alternative: merely change raster-mode flags or try the existing raw path. Rejected as the default design because it omits the extended page command and does not reproduce the complete path already demonstrated on this printer. Brother documents uncompressed output, but testing it is not necessary to implement the verified PackBits path. + +### 3. Keep one geometry and one batch lifecycle + +Retain `pt_tape_geometry`, the 16-byte raster packer, strict width/content checks, and published nominal dimensions with pixel-aligned margins. The observed P710BT 9/12/18 mm windows match the existing table. Both supported models advertise the existing 6/9/12/18/24 mm media; documentation distinguishes the available physical widths from software/documented ones. + +Keep the current `pt_cut_policy`, pending-print lifecycle, mandatory first-label precut, and closed/open/uncertain state. Do not add a second P710BT batch planner, total-count metadata, or page buffer. Status parsing remains the existing 32-byte layout; tests include P710BT's actual replies and fragmented completion/ready notifications. Any failed write or unconfirmed output follows the same conservative recovery latch. + +Alternative: fork the driver or use narrower model-specific margins from the manual. Rejected because the common rendering works on the tested printer, and a second lifecycle would duplicate the refactor's failure and continuation logic. + +### 4. Verify each boundary separately + +- Core: model lookup; exact legacy and extended commands; first/subsequent flags; page lengths; compression selection; PackBits decoding and all 128 pin positions. Keep all five geometry widths covered. +- Fake USB: default/explicit model, both models attached, absent selected model with an actionable unselected-model diagnostic, two same-model devices, renumbering, claim/enumeration errors, URI/identity mismatches, selected-model probe/smoke behavior, and socket guard behavior. +- Adapter/socket: parameterize the existing production-driver/PNG fixtures and runtime image smoke for both selected models, and run both selections in `.github/workflows/deploy.yml` before publication. This is the isolated IPP/socket image smoke, not the initializing USB `smoke` subcommand. Decode the P710BT wire by command lengths and compression mode. Cover four presets, mixed page heights, physical-run page indication across IPP batches, unknown total count, full-width 9/12/18 mm designs, explicit geometry, oversized/wrong tape rejection before setup, incomplete streams, cancellation, status faults, and no replay. Assert that PT-2730 never receives P710BT-only commands. +- Physical: use an isolated P710BT candidate on `vault` with a separately presented gate. The proposed gate contains **11 complete labels**, one incomplete-stream negative trial, wrong-media rejection, and status-only checks. Do not add uncounted helper prints or retries. + +Proposed integrated matrix: + +| Stage | Tape / input / policy | Complete labels | Observation | +| --- | --- | ---: | --- | +| 1 | 12 mm PNG copies, chain-only | 2 | Fresh leader, internal separation, last end held | +| 2 | 12 mm PNG copies, chain-and-final after an idle gap with USB present | 2 | Prior held end released, no discarded new leader, final end cut | +| 3 | 12 mm PNG copies, none | 2 | Fresh leader, joined strip held | +| 4 | 12 mm unknown-count PWG, final-only, two different page heights | 2 | Prior strip released, joined labels retained at their page lengths, one final cut | +| 5 | 12 mm incomplete PWG and blocked valid follower, then power-cycle/candidate restart and explicit full-width recovery PNG | 1 | No terminal command for incomplete input, no automatic replay; only the new 76-dot label prints | +| 5a | On a closed run, load/confirm 9 mm tape and submit a complete job explicitly requesting 12 mm media | 0 | Job aborts; only status requests are allowed, with no initialization, page setup, raster, feed, or cut; the next valid job remains usable | +| 6 | 9 mm full-width PNG queued while offline on a closed run, default trim | 1 | Queue resumes after reconnect/media confirmation; complete 52-dot rectangle at 1:1 | +| 7 | 18 mm full-width PNG, default trim | 1 | Complete 120-dot rectangle at 1:1 | + +Use stage 2 for a 10-second healthy idle gap with USB and power continuously present, and the counted stage-6 label for queued offline recovery after stages 5/5a leave the run closed. Current idle status clears healthy-open state when USB disappears; this change does not promise preservation of a held end across disconnection or power loss. Cassette transitions reuse stages 5a/6/7. Record actual leader waste, cuts, orientation, frame sizes, job status, command bytes, and the operator's physical observations. The geometry-only sessions remain separate evidence. + +[Brother's user guide](https://download.brother.com/welcome/docp100362/pt-p710bt_eng_ug_03.pdf) documents button-based shutdown and separately configurable USB/battery auto-off timers. Before the gate, confirm adequate charge/USB power, close competing mobile printing apps, and disconnect Bluetooth clients. Record the auto-off setting if available, and confirm that the stage-2 power LED and USB presence persist through the 10-second gap without changing printer settings. An unexpected shutdown invalidates that continuation trial and stops the gate; it is not evidence of a page-flag failure. + +For stage 5, actually power off by holding the Power button until the Power LED goes out, then turn it on with a single press and restart only the candidate server. Unplugging USB is not a power-cycle of this battery-powered device. Do not double-press the button, which feeds/cuts tape. For stage 6, disconnect only USB while the already closed-run printer remains powered; verify the candidate reports unavailable, submit the counted job once, then reconnect USB and let that same queued job resume. Neither action adds a helper print or automatically resubmits the job. + +PT-2730 must retain byte-identical print output for equivalent jobs across all four policies, including canonical rendering and command ordering. Those capture/adapter regressions are sufficient when emitted print bytes and physical policy are unchanged. If either changes, identify the difference and prepare a separately bounded PT-2730 physical check before claiming preservation; editing shared callback code alone does not impose that additional printing requirement. + +All P710BT physical acceptance is through the integrated server. Do not extend the existing PT-2730 cut probe: its additional live path is unnecessary for this matrix, and the production callback/socket captures already supply protocol verification. + +## Risks / Trade-offs + +- [Single-label probes do not establish late finalization or healthy continuation] → Keep those behaviors as explicit integrated hardware acceptance; a failing policy blocks certification rather than silently dropping a preset. +- [Broader discovery accidentally drives recognition-only models] → Preserve the explicit two-model support gate and negative tests independently of protocol flags. +- [Extended page information disagrees with actual output after scaling or orientation] → Use the validated destination raster header and assert row counts against decoded captures for each format/page. +- [Reset/invalidate erases an unfinished P710BT run] → Apply fresh-run preparation only to confirmed closed state; continue healthy runs without reset, and preserve uncertain-state blocking. +- [Starting-page semantics during held cross-batch continuation are untested] → Use the explicit fresh-run/continued-run rule above, capture it in software, and verify it in counted stages 2/4; stop rather than silently trying an alternative if physical behavior disagrees. +- [Battery operation or auto-off confuses offline/recovery tests] → Define USB disconnection separately from button power-cycle, and verify uninterrupted power during the bounded idle gap. +- [6/24 mm have no P710BT physical observations] → Keep documentation explicit about software/documentation coverage and the widths actually inspected; do not invent an all-width hardware claim. +- [Temporary diagnostic evidence can disappear before apply] → The planning record names the development VM and intentionally records this retention limitation. Task 1.1 preserves the available files and hashes before implementation changes the probe; absent files are reported while the operator observations remain separately attributable. + +## Migration Plan + +1. Implement locally after an apply request. Complete core, container adapter, and runtime socket checks for both models. Prepare the exact physical fixtures/captures and tape budget before presenting the 11-label gate. +2. Run the authorized gate on an isolated candidate through pfa's SSH connection to `vault`. Verify the previous queue is idle and record its image/configuration; release the P710BT from any competing process only if needed. Keep the old service available where it exclusively owns PT-2730. Stop on any failure, retain evidence, and restore services after confirmed finalization/recovery. +3. Document the successful scope, sync the deltas, validate, and archive. No push, pull request, or merge before archival; publishing and rollout require separate authorization. +4. For an authorized rollout, use the existing container configuration on `vault` to select `PTOUCH_MODEL=PT-P710BT` and the tested image. Retain the previous image/configuration for rollback. Finish any held run before switching models; rollback to PT-2730 requires its explicit model selection and matching hardware, never an automatic fallback. diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/proposal.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/proposal.md new file mode 100644 index 0000000..91e126b --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/proposal.md @@ -0,0 +1,36 @@ +# Proposal + +## Why + +The PT-P710BT is replacing the owner's PT-2730, but the refactored server rejects its USB identity and emits only the older printer's setup commands. Direct USB tests already demonstrate a working P710BT raster path and usable 52-, 76-, and 120-dot print areas on 9, 12, and 18 mm tape, so support can be added without changing the client interface or narrowing those margins. + +## What Changes + +- Add an explicitly supported PT-P710BT USB path (`04f9:20af`), selected with `PTOUCH_MODEL=PT-P710BT`; retain PT-2730 as the default. +- Derive discovery, queue identity, USB opening, status checks, and the existing probe/smoke diagnostics from the selected supported model. Identify an attached but unselected supported printer and the required PTOUCH_MODEL selection without fallback. Keep one queue, exact model selection, same-model ambiguity rejection, and recovery after USB renumbering. +- Add model-specific P710BT page setup and dispatch the existing raster-mode and PackBits builders. Use normal 180 dpi, a 128-dot head, extended print information with the actual page row count, and explicit compression/status settings. +- Reuse the current canonical tape geometry and the refactored streaming batch lifecycle. Preserve four finishing policies, mandatory batch precut, healthy continuation, strict geometry rejection, and uncertain-output recovery. +- Add model-specific core, fake-USB, and IPP/socket regression coverage, with runtime socket smoke for both models in CI, followed by bounded P710BT hardware acceptance. Require byte-identical PT-2730 print regressions and a separate physical check only if its emitted print bytes or command policy change. Preserve the distinction between direct geometry observations and integrated server certification. +- Document model selection, protocol differences, available physical evidence, and unsupported models. Bluetooth, additional printer models, simultaneous queues, and 360 dpi mode are outside this change. + +## Capabilities + +### New Capabilities + +None. USB model selection and its protocol belong to the existing printer-device capability. + +### Modified Capabilities + +- `printer-device`: Extend the supported-model contract to PT-P710BT, make model selection and identity consistent throughout the USB path, and specify the selected model's print protocol. +- `batch-printing`: Make hardware verification apply to the affected supported model and establish that P710BT receives the same finishing/continuation behavior as PT-2730. +- `server-architecture`: Update the canonical supported-hardware constraint from PT-2730-only to the two explicitly selected implemented model paths. + +The `label-rendering` requirements remain authoritative and unchanged; the observed P710BT geometry matches the existing imageable-width table. + +## Impact + +Implementation will affect `src/tables.*`, `src/protocol.*`, `src/device_usb.*`, `src/main.c`, and `src/driver.c`. Tests will extend the existing table/protocol suites, fake USB selection and diagnostic checks, production callback captures, PNG geometry checks, and runtime IPP smoke, including `.github/workflows/deploy.yml`. The existing PT-2730-only cut probe needs no P710BT extension; all new physical acceptance uses the integrated server. README, the model-addition guide, and hardware acceptance instructions will distinguish certified paths from recognition metadata. + +Reuse PAPPL, libusb, libpng, and the existing pinned dependency patches. No new service, parser, image buffer, client attribute, or dependency fork is needed. PT-2730 defaults, byte sequences, rendering, and finishing behavior must retain regression coverage. + +This proposal authorizes planning only. Implementation requires a subsequent apply request; physical integration tests require a concrete bounded gate, and production deployment remains a separate action. Keep change work local until specification sync and archival are complete. diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/batch-printing/spec.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/batch-printing/spec.md new file mode 100644 index 0000000..f913c3c --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/batch-printing/spec.md @@ -0,0 +1,31 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Existing rendering is preserved + +The server SHALL retain 1:1 default printing, strict tape-width rejection, and offline media recovery as defined by label-rendering and printer-device. The finishing choice SHALL NOT change rendered pixels or page dimensions. Advertised formats and copy limits SHALL describe executable behavior. PT-P710BT hardware success SHALL require integrated physical acceptance. PT-2730 SHALL retain byte-identical print regressions; changed PT-2730 print bytes or physical command policy SHALL additionally require a separately bounded physical check before claiming preservation. + +#### Scenario: Copies +- **WHEN** a valid PNG requests copies within its advertised supported range +- **THEN** all requested copies use the resolved finishing policy and the same image geometry + +#### Scenario: Offline printer returns +- **WHEN** queued jobs resume after a printer returns with matching tape +- **THEN** media is confirmed before printing and each job retains its requested finishing choice + +#### Scenario: P710BT finishing equivalence +- **WHEN** a PT-P710BT receives valid PNG-copy or multi-page raster batches using any of the four advertised finishing presets +- **THEN** mandatory first-label precut, internal cuts, held ends, final cuts, and healthy continuation match the existing batch-printing requirements + +#### Scenario: Hardware claims require integrated evidence +- **WHEN** P710BT support is documented as hardware-certified +- **THEN** retained evidence includes real-printer observations of the integrated server's finishing, continuation, rendering, offline recovery, and uncertain-output recovery; direct geometry probes and socket captures alone do not establish that certification + +#### Scenario: Unchanged PT-2730 output +- **WHEN** adding P710BT support leaves PT-2730's emitted print bytes and physical command policy unchanged +- **THEN** legacy capture and adapter regressions establish preservation without requiring new PT-2730 labels + +#### Scenario: Changed PT-2730 output +- **WHEN** implementing support changes PT-2730's emitted print bytes or physical command policy +- **THEN** the change identifies that difference and verifies it through a separately bounded PT-2730 physical check before claiming preservation diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/printer-device/spec.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/printer-device/spec.md new file mode 100644 index 0000000..4a04427 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/printer-device/spec.md @@ -0,0 +1,77 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Only supported hardware receives print output + +The server SHALL create operational queues only for implemented and verified model paths: PT-2730 and PT-P710BT. Recognizing a USB ID SHALL NOT grant print support. Invalid or unsupported model configuration SHALL fail explicitly rather than fall back silently. The default model SHALL remain PT-2730; PT-P710BT SHALL require explicit selection with `PTOUCH_MODEL=PT-P710BT`. + +#### Scenario: Unsupported Brother attached +- **WHEN** discovery finds a model without an implemented print path +- **THEN** no PT-2730 or PT-P710BT raster is sent to that model and the recognized unsupported device is identified in diagnostics + +#### Scenario: Invalid configured model +- **WHEN** the configured model is unknown or unsupported +- **THEN** startup fails with a useful configuration error + +#### Scenario: Default remains compatible +- **WHEN** PTOUCH_MODEL is absent or empty +- **THEN** the queue selects PT-2730 and retains its existing identity and print behavior + +#### Scenario: P710BT explicitly selected +- **WHEN** PTOUCH_MODEL is PT-P710BT +- **THEN** the stable queue identifies itself as PT-P710BT and uses the implemented PT-P710BT print path + +#### Scenario: Recognized metadata does not enable another printer +- **WHEN** a recognized P-touch model other than PT-2730 or PT-P710BT is configured +- **THEN** startup rejects it even if its metadata shares protocol flags with a supported model + +### Requirement: Device selection is unambiguous + +The server SHALL expose one application queue and consistently select its configured supported model. Multiple matching physical devices SHALL be reported as ambiguous; discovery or opening SHALL NOT silently choose the first match. USB renumbering of a single matching device SHALL NOT require queue replacement. PT-2730 SHALL match USB identity 04f9:2041 and PT-P710BT SHALL match 04f9:20af; another attached model SHALL NOT substitute for the selected one. + +#### Scenario: Two matching devices +- **WHEN** two units of the selected supported model match the configured queue +- **THEN** printing is blocked with an ambiguity diagnostic + +#### Scenario: Device replug +- **WHEN** the sole matching printer is unplugged and reattached with another USB node number +- **THEN** the existing queue recovers without selecting a different model or requiring a server restart + +#### Scenario: Both supported models attached +- **WHEN** one PT-2730 and one PT-P710BT are attached and either model is selected +- **THEN** discovery, opening, status, and printing consistently use only the selected model + +#### Scenario: Selected model absent +- **WHEN** PT-P710BT is selected but only PT-2730 is attached, or the reverse +- **THEN** the selected queue remains present and unavailable, diagnostics name the attached but unselected model and the applicable PTOUCH_MODEL selection, and no output is sent to the other printer + +#### Scenario: USB identity conflicts with queue identity +- **WHEN** an explicit supported USB URI names a different model from the configured queue identity +- **THEN** queue creation or opening fails before status or print commands are sent + +## ADDED Requirements + +### Requirement: Selected model controls the print protocol + +The server SHALL send the configured model's implemented USB print protocol. PT-2730 SHALL retain its legacy command and raster encoding. PT-P710BT SHALL use normal 180 dpi, 128-dot TIFF PackBits raster, raster-mode setup, and extended print information with loaded tape width and actual page row count. The page indication SHALL preserve the physical printer run across IPP batches without requiring a total label count. + +#### Scenario: P710BT page setup +- **WHEN** a valid page begins on a selected PT-P710BT +- **THEN** the output selects raster mode and automatic status notification, specifies that page's tape width and row count using the extended print-information command, selects TIFF PackBits, and sends complete 128-dot raster lines + +#### Scenario: Different page lengths in one batch +- **WHEN** a PT-P710BT receives a streaming batch with complete pages of different heights and no declared total page count +- **THEN** each page's print information contains its own height and appropriate physical-run page indication, and normal end of input determines finalization + +#### Scenario: Healthy P710BT continuation +- **WHEN** a PT-P710BT batch follows a healthy unfinished matching-tape run +- **THEN** page setup preserves the physical run and its held end, using a continuation-compatible page indication rather than treating the IPP batch boundary as a fresh printer run + +#### Scenario: PT-2730 regression +- **WHEN** an existing PT-2730 job is submitted after P710BT support is added +- **THEN** its page setup and raster bytes retain the PT-2730 encoding and contain no P710BT-only setup + +#### Scenario: P710BT status failure +- **WHEN** the selected PT-P710BT reports an error or does not confirm receiving-ready after output +- **THEN** the job fails without replay or invented finalization, and uncertain output remains blocked under the existing recovery contract diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/server-architecture/spec.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/server-architecture/spec.md new file mode 100644 index 0000000..dae0d4a --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/specs/server-architecture/spec.md @@ -0,0 +1,22 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Supported and unambiguous hardware + +Only the implemented PT-2730 and PT-P710BT paths SHALL receive print output. Missing or empty `PTOUCH_MODEL` SHALL select PT-2730; PT-P710BT SHALL require explicit selection. Unknown or recognized unsupported models SHALL fail configuration. The stable model URI SHALL resolve by the selected VID/PID, accepting exactly one matching device and blocking zero or multiple matches without selecting by enumeration order. + +#### Scenario: Duplicate supported printers + +- **WHEN** two devices of the selected supported model match the configured queue +- **THEN** the existing queue reports ambiguity and neither device is claimed for printing + +#### Scenario: Two different supported models + +- **WHEN** one PT-2730 and one PT-P710BT are attached and a model is configured +- **THEN** the queue selects only that model, and the other printer does not create ambiguity or receive its output + +#### Scenario: Compatible default + +- **WHEN** no model is configured +- **THEN** the queue retains PT-2730 identity and behavior without automatically adopting an attached PT-P710BT diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/tasks.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/tasks.md new file mode 100644 index 0000000..1b7a5e0 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/tasks.md @@ -0,0 +1,40 @@ +# Tasks + +## 1. Evidence and pure protocol support + +- [x] 1.1 Preserve the available P710BT diagnostic source, rectangle captures, and status logs from `/tmp` on the development VM `openclaw` in a named external validation bundle, recording its host/path, hashes, and operator observations in the change's validation summary; verify hashes against design.md and report files lost before apply separately from the recorded physical observations. +- [x] 1.2 Add PT-P710BT `04f9:20af` metadata with 128-dot/180-dpi and raster-mode/PackBits flags in `src/tables.*`; verify lookup by ID/name, rejection of unknown metadata, and unchanged 32/52/76/120/128-dot geometry in `test_tables`. +- [x] 1.3 Add pure extended print-information, compression-selection, and automatic-status command builders in `src/protocol.*`; verify exact bytes, 32-bit page heights, first/subsequent indication, literal PackBits decoding, and unchanged legacy commands in `test_protocol` and the raster suite. +- [x] 1.4 Update the model-addition guide to document the P710BT command path and the explicit support gate without calling pending integration hardware-certified; review its protocol claims against the builders and retained probe evidence. + +## 2. Model-consistent USB selection and queue identity + +- [x] 2.1 Extend supported-model/URI resolution and identity formatting in `src/device_usb.*` for the two intended models; verify default PT-2730, explicit P710BT, unsupported metadata rejection, and exact URI validation with fake-USB tests. +- [x] 2.2 Make discovery/opening count the selected VID/PID and derive device IDs/diagnostics from that model; verify both models attached, selected model absent, an actionable attached-but-unselected model/PTOUCH_MODEL diagnostic, duplicate same-model devices, renumbering, claim/enumeration errors, and no fallback/output to the other model in `test_device_usb`. +- [x] 2.3 Derive normal/socket-override queue identity and the existing `probe`/USB `smoke` subcommands' VID/PID, URI, and messages from PTOUCH_MODEL in `src/main.c`, and use the immutable selected device in printer state; verify both diagnostic subcommands with fake USB, URI/identity rejection before commands, and socket transport guards without physical helper prints. +- [x] 2.4 Document explicit model selection, its unchanged default, one-queue/no-fallback behavior, enumeration-only `probe`, and the initializing USB `smoke` command's known-closed-run restriction in README; check the text against startup/fake-USB regressions and the architecture delta without recreating the retired ADR subtree. + +## 3. P710BT output through the existing driver lifecycle + +- [x] 3.1 Dispatch model-specific setup in `r_startpage`, with the validated destination row count, page indication 0 for a freshly initialized run and 1 for later/healthy-held continuation pages, notification, 14-dot feed margin, and PackBits; enlarge command buffers and verify exact production-callback captures for both models and cross-IPP-batch flag selection. +- [x] 3.2 Dispatch P710BT PackBits lines in `r_writeline` while retaining canonical geometry and packing; verify decoded full-width 9/12/18 mm rectangles, all five software media widths, and unchanged PT-2730 raster output in the driver/PNG suites. +- [x] 3.3 Apply invalidate/initialize only to a fresh closed P710BT run and preserve the current deferred print, FF, late chain-off/EJECT, and uncertainty latch; verify all four presets, unknown-count mixed-height pages, idle continuation, fragmented P710BT status, cancellation/fault handling, and no terminal replay in production-callback tests. +- [x] 3.4 Update protocol/finishing documentation for model-specific setup while keeping the client finishing contract unchanged; review it against decoded captures and the batch-printing delta, with physical P710BT acceptance still marked pending. + +## 4. Container and runtime integration + +- [x] 4.1 Parameterize the existing IPP/socket and PNG fixtures and `scripts/smoke-image.py` for selected PT-2730 and PT-P710BT, using a length-aware PackBits decoder, and wire both selections into `.github/workflows/deploy.yml`; verify model-correct captures, geometry/input failures, four presets, and invocation of both image-smoke variants before publication. +- [x] 4.2 Run the required full C build/checks (`cmake -S . -B build`, `cmake --build build`, `ctest --test-dir build --output-on-failure`) in the pinned container environment, plus explicit core-only checks and both runtime socket smoke variants; retain successful logs, check the CI wiring, and investigate failures before proceeding. +- [x] 4.3 Prepare the seven positive stages and zero-label wrong-media trial 5a, totaling 11 complete labels, with exact fixtures/captures, commands, timer-aware 10-second idle gap, button power-cycle versus USB-only disconnection, tape budget, and restoration procedure; verify no live output during preparation and present the concrete gate before running it, without extending the PT-2730 cut probe. + +## 5. P710BT physical acceptance and certification + +- [x] 5.1 After concrete-gate authorization, confirm P710BT/12 mm tape on vault through pfa's SSH connection, record the original service/image state, and start the isolated candidate; verify clean idle status/identity, adequate charge/power, auto-off assumptions, and no competing USB/Bluetooth printing client before output, using no initializing USB smoke preflight. +- [x] 5.2 Execute stages 1 through 4 without helper prints, keeping one candidate process and continuous USB/power through the 10-second idle gap; verify cuts/held ends/leader waste, cross-batch page flags, unknown-count lengths, job states, and captures, stopping on a fault or power loss without automatic flag fallback/retry. +- [x] 5.3 Execute the incomplete-stream/blocked-follower trials, actual button power-cycle and candidate restart, and counted stage-5 76-dot recovery label; after loading 9 mm on the closed run, run trial 5a requesting 12 mm with zero page output, then stages 6/7; verify USB-only queued recovery, intact 52/120-dot outlines, no ghost/replay, and 11 complete labels total. +- [x] 5.4 Restore any service temporarily yielded to the candidate and retain commands, captures, status/job logs, hashes, and physical observations; verify restoration and update README/acceptance documentation with the passed integrated scope and the separate software-only 6/24 mm coverage. + +## 6. Final reconciliation + +- [x] 6.1 Review implementation, tests, documentation, and evidence against all three deltas and unchanged rendering; verify P710BT physical acceptance, PT-2730 byte-identical print/adapter regressions, and a separately bounded PT-2730 physical check only if its emitted print bytes or physical command policy changed. +- [x] 6.2 Reconcile the change with main specifications, run strict OpenSpec validation, and complete local sync/archival through the OpenSpec workflow when finalizing; verify no unresolved task or failed acceptance remains before archival and keep pushing, pull requests, and production rollout outside this local implementation scope. diff --git a/openspec/changes/archive/2026-10-13-add-p710bt-support/validation.md b/openspec/changes/archive/2026-10-13-add-p710bt-support/validation.md new file mode 100644 index 0000000..3125f11 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-p710bt-support/validation.md @@ -0,0 +1,82 @@ +# P710BT validation + +## Prior direct-USB geometry evidence + +[verified] On 2026-10-13, all 22 probe source, rectangle fixture/capture, and status-log files were preserved from the development VM's `/tmp` into the external bundle `openclaw:/home/pfa/.local/share/ptouch-validation/p710bt-20261013/`. `SHA256SUMS` and `manifest.json` record every retained file. No source files were missing at apply time. These are local VM paths, not paths on `vault`. + +The three widest captures match the design's SHA-256 values: + +| Tape | Capture | SHA-256 | +| --- | --- | --- | +| 9 mm | `p710bt-9mm/03-52.prn` | `711d2337f43c23fcd3eea84dcf5527cc03ef11451c02336f2e1b820b0353e323` | +| 12 mm | `p710bt-rectangles/03-76.prn` | `7eb5be170e12c0f7c574c725b998655efb6f5e6ba6791aedc4d98e1e6bf13718` | +| 18 mm | `p710bt-18mm/03-120.prn` | `bc07c514b4cbdf0978e42bb75e12d80e54a96a4dfe9ae6d2a63964718032b591` | + +The operator reported all three separate continuous rectangles printed fine on each loaded cassette: 50/51/52 dots on 9 mm, 74/75/76 dots on 12 mm, and 112/116/120 dots on 18 mm. The first dashed 76-dot fixture is separate from the later successful continuous-rectangle trial. These observations establish the tested geometry; they do not certify integrated finishing, continuation, or recovery. Physical 6/24 mm coverage remains untested. + +## Integrated implementation + +[verified] Full configure/build/CTest in the pinned `ptouch-review-cups:20261013` development image passed all 12 tests. Its dependencies are PAPPL 1.4.11 and patched CUPS 2.4.2-3+deb12u9. Core-only configure/build/CTest separately passed all six tests. The local deployment build `ptouch-p710bt:local` passed its embedded 12-test suite, and both `--model PT-2730` and `--model PT-P710BT` runtime IPP/socket image smokes passed. CI invokes both smoke selections before publication. + +[verified] Production-callback checks cover P710BT's exact fresh/held-run setup bytes, destination heights, cross-batch page indication, literal PackBits lines, four policies, unknown-count mixed heights, cancellation, fragmented readiness, and conservative failure cleanup. The P710BT status fixture is seeded from the retained actual 12 mm snapshot and reproduces its printing/completed/receiving-ready replies. Both-model PNG checks preserve exact outlines across all five software widths and reject invalid geometry and input without output. + +[verified] Baseline `4de4587e2f1f3c17ea67d1a28c96d147b50d3283` and candidate PT-2730 production-callback suites emitted byte-identical accepted transcripts, including PNG/raster policy cases, status requests, transfer-failure cases, and the 65/999-copy counting cases: 4,180,474 bytes, SHA-256 `cc9986ca8e2beaea34ad7102c42867e887ebc6d3533fd1b7d6d797ab2046c4b4`. Test-local instrumentation was confined to external source snapshots. PT-2730's emitted print policy was preserved; no additional legacy physical labels are required by this change. + +Successful logs, the two legacy transcripts, prepared fixtures, and candidate image are retained in the external validation bundle. At this preparation stage, integrated hardware acceptance was still pending and no new physical output had been sent; the completed acceptance is recorded below. + +## KISS review + +[verified] The independent implementation review found no speculative overengineering or runtime defect; its real-status fixture gap was corrected and checked. The implementation keeps two explicit model choices, pure command builders, canonical geometry, and the existing constant-size batch lifecycle. It introduces no printer profile framework, second planner, page buffer, dependency, or automatic protocol fallback. Hardware acceptance remains a separate required gate. + +## Prepared hardware gate + +[verified] The [exact gate](../../../../tests/P710BT-ACCEPTANCE.md) was prepared before explicit authorization. Its candidate image, tools, requests, raw pixel expectations, socket captures, and successful software logs are retained at `openclaw:/home/pfa/.local/share/ptouch-validation/p710bt-20261013/integrated-gate/`; all 96 `SHA256SUMS` entries were checked before and after preservation. The candidate image is `ptouch-p710bt:local`, image ID `sha256:501a839dae681174feb7759e9b6ce2769d93d0e6b21104f23602e7642a9df89b`. + +The isolated socket gate reproduced 11 complete simulated labels with exact pixels, 180/216-row heights, fresh and held-run page indications, a 60-row incomplete fragment without finalization, and a blocked follower with zero output. A separate affected-width run confirmed the zero-output wrong-media trial on simulated 9 mm media and successful 52/120-dot fixtures afterward. The initial 9 mm preparation request inherited incompatible margins; its templates now specify the existing canonical margins, preserving strict validation. Simulated cassette changes and process restarts do not establish real offline or power-cycle recovery. Bundled ipptool and capture-shim runtime dependencies were checked with no USB access. + +The concrete physical bound is nine complete labels on 12 mm, one on 9 mm, and one on 18 mm, plus the incomplete/no-output trials. Complete content totals 28.448 cm, excluding leaders, feed allowances, and fragment waste. No SSH operation, USB output, physical label, or production rollout occurred during preparation. At this preparation stage, tasks 5.1–5.4 and final reconciliation were pending; their completed results follow. + +## Initial physical-gate execution record + +The following paragraphs preserve intermediate observations; the authoritative completed outcome follows in “Completed integrated acceptance.” + +The user authorized the concrete gate with “ready” on 2026-10-13. The active bundle is `vault:/tmp/p710bt-acceptance.7nL5Ss/`, candidate container `ptouch-p710bt-acceptance` on local port 18000. All 96 transferred file hashes passed. The original `ptouch` service remains running and unchanged on PT-2730; its image ID is `sha256:acd1f2d0e5b9ebecdd63bd838cc85e70aa0fadfa0c78b2feb6c68706f7419686`. No competing USB owner was found for the sole P710BT, `04f9:20af`, bus 003/device 024. The user was instructed to keep USB power connected and other printing clients closed. No auto-off setting was changed. + +[verified] Preflight confirmed clean idle status, 12 mm media with canonical margins, and an empty candidate queue. Only status requests were emitted before printing. USB power/presence and clean receiving-ready status persisted through a measured 10-second idle interval; the full interval between stage-1 completion and stage-2 submission was longer due to capture inspection and operator observation. + +[verified] Stage 1 completed IPP job 1 with two 180-row pages, indications 0/1, two FF commands, chain on, and no EJECT. Its 7,368-byte capture matches the prepared socket capture after removing status requests; both receiving-ready notifications were consumed without errors. Operator observation: “leader and properly printed rectangle label.” This matches the free leader/first label expected with the last label held; its physical release is checked by stage 2. + +[verified] Stage 2 completed IPP job 2 with two 180-row pages, indications 1/1, no invalidate/init, one FF and one late chain-off/EJECT. Its 7,270-byte capture matches the prepared socket capture after removing status requests; all incoming statuses are clean, with four receiving-ready notifications across stages 1/2. Current physical evidence was copied locally to `/tmp/p710bt-live-evidence`. Physical observations of prior-label release, no new discarded leader, and final separation remain pending. Four complete labels have been submitted in total; stages 3–7 have not run. The candidate remains running for the authorized gate. + +The user then explicitly requested “lets repeat step 1”, authorizing one extra two-label chain-only batch. The overall label bound increases from 11 to 13; this is a manual operator-requested repeat, not an automatic retry. Original stage-2 physical observations remain pending. + +[verified] The repeat completed as IPP job 3 after idle/12 mm preflight, with two 180-row pages, indications 0/1, fresh invalidate/init, two FF commands, and no EJECT. Its normalized 7,368-byte capture matches the original stage-1 socket reference; all incoming statuses remain clean, with six receiving-ready notifications across the three batches. Original stage-1 evidence was preserved in `physical-evidence/stage1-original/`; the new artifacts are `stage1-repeat1.*`, and the original `stage1.*` files were restored. Updated physical evidence was copied to `/tmp/p710bt-live-evidence/`. + +Six complete labels have now been submitted. The operator answered “yes” to confirm a leader and one free rectangle label, with the second label still held, for the repeated stage 1. This supplementary chain-only trial passes its capture, status, and physical criteria. The repeated batch leaves a healthy held run; stages 3–7 remain unexecuted, and the original stage-3 fresh/closed starting condition requires an explicit closing decision before continuation. Original stage-2 physical observations remain pending. No further output was sent. + +## Completed integrated acceptance + +[verified] The user subsequently requested “now step 2”, authorizing another two-label repeat and a **15-label total bound**. Repeated stage 2 completed as job 4 with no invalidate/init and indications 1/1, releasing the held S1 and cutting both S2 labels free; the operator reported “S1 and 2 S2, all cut off.” The normalized capture is 7,270 bytes and matches the socket reference. Original/repeated evidence was preserved separately. This repeat restored the closed-run starting condition for stage 3. + +The USB node change from 024 to 026 was investigated before repeating stage 2. Kernel timestamps show the last enumeration at monotonic 1,063,170.866508, **before** repeated stage 1's first output at 1,063,172.620679860. There was no disconnect during or after that repeated held batch. Its continuation was therefore valid; no flags were changed and no output was retried automatically. `stage2-repeat-preflight-continuity.json` retains that evidence. + +| Trial | Job / outcome | Capture and operator evidence | +| --- | --- | --- | +| 3: none | First process, job 5 completed two 180-row pages; 0/1, two FF, no EJECT | 7,368 normalized bytes; exact pixels. Operator confirmed leader cut off, joined S3 labels, end held. | +| 4: final-only PWG | First process, job 6 completed 180/216-row pages with declared total zero; 1/1, FF then late chain-off/EJECT | 7,990 normalized bytes; exact pixels. Operator confirmed released S3 strip and a separate joined S41/S42 strip, second label longer, final end cut. | +| 5: incomplete | First process, job 7 aborted after 60/180 raster rows | 1,338 normalized bytes, no FF/EJECT. No complete label finalized. | +| 5: blocked follower | First process, job 8 aborted with explicit recovery-required message | Zero output bytes; no automatic reset, feed, cut, or replay. | +| 5: recovery | Operator confirmed the instructed button power-cycle; only the candidate restarted. New process, job 1 completed one 76-dot S5 label | 3,743 normalized bytes, fresh init/indication 0 and EJECT. Operator reported “leader and S5,” with no extra replay reported. | +| 5a: wrong media | With confirmed 9 mm tape, job 2 explicitly requesting 12 mm aborted | Only two status requests; no initialization, setup, raster, feed, or cut. The subsequent valid job remained usable. | +| 6: offline queue | Operator disconnected only USB with power retained. Job 3 accepted once, waited for USB, and completed after reconnect | Zero output while disconnected; whole reconnect interval is 3,743 normalized bytes with exact 52-dot pixels and EJECT. Operator reported “leader and S6” and acknowledged the requested intact-outline check before loading 18 mm. | +| 7: 18 mm | New process, job 4 completed one 120-dot S7 label | 3,743 normalized bytes, exact pixels, fresh indication 0 and EJECT. Operator explicitly confirmed all four rectangle sides complete and label cut free. | + +All positive/negative captures match their prepared socket references after removing only status requests. The incoming packet log contains 15 receiving-ready notifications for the 15 complete labels and no printer error bits. There were **13 complete labels on 12 mm, one on 9 mm, and one on 18 mm**. Complete printed content totals 38.608 cm; actual consumed tape/leader/feed waste was not measured. P710BT 6/24 mm remain software-only, and Bluetooth/360 dpi remain outside the verified scope. + +[verified] Final status was clean/idle on 18 mm with all live-process jobs terminal. The candidate was stopped and removed with no additional print output. The original service is running with the same image, Config, and HostConfig as recorded before the gate. Its StartedAt changed independently from 07:00:09 to 19:01:53 UTC during the session; the gate issued no stop/restart for that service, so uninterrupted original-service uptime is not claimed. The verified image/configuration preservation and final running state are retained in `restoration-verified.json`. + +The final physical evidence was copied from `vault:/tmp/p710bt-acceptance.7nL5Ss/physical-evidence/` to `/tmp/p710bt-live-evidence/`; all 186 remote hashes matched on collection. Operator observations and updated hashes were added locally. Durable evidence belongs under `openclaw:/home/pfa/.local/share/ptouch-validation/p710bt-20261013/integrated-gate/physical-evidence/`. No production image rollout or push occurred. + +## Final reconciliation + +[verified] All three deltas were reviewed against the implementation, regressions, and completed hardware evidence. Main specifications were synced while preserving their Purpose sections and unaffected requirements. The KISS pass retained the existing driver lifecycle, canonical geometry, and two explicit model selections without new frameworks or dependencies. Integration and production rollout were subsequently authorized explicitly by the user. diff --git a/openspec/config.yaml b/openspec/config.yaml index 119b750..5f0686a 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -4,8 +4,9 @@ context: | Read README.md and openspec/specs/server-architecture/spec.md for current behavior and architectural constraints; spikes/FINDINGS.md is historical and some conclusions have been superseded. - Only the Brother PT-2730 is hardware-certified. Other recognized USB models - must not be described as print-supported without protocol work and validation. + Brother PT-2730 and PT-P710BT are hardware-certified; P710BT physical coverage + is 9/12/18 mm over USB, with software-only 6/24 mm coverage. Other recognized + USB models must not be described as print-supported without protocol work and validation. Preserve 1:1 printing, nominal tape widths with real imageable margins, strict width rejection, offline recovery, and cross-job chaining unless the change explicitly revises those contracts. diff --git a/openspec/specs/batch-printing/spec.md b/openspec/specs/batch-printing/spec.md index 97407a4..f47beb1 100644 --- a/openspec/specs/batch-printing/spec.md +++ b/openspec/specs/batch-printing/spec.md @@ -96,7 +96,7 @@ A detected output, raster decode, or printer-status failure SHALL fail the job w ### Requirement: Existing rendering is preserved -The server SHALL retain 1:1 default printing, strict tape-width rejection, and offline media recovery as defined by label-rendering and printer-device. The finishing choice SHALL NOT change rendered pixels or page dimensions. Advertised formats and copy limits SHALL describe executable behavior. Changed physical printing and cutting SHALL be verified on the PT-2730 before claiming hardware success. +The server SHALL retain 1:1 default printing, strict tape-width rejection, and offline media recovery as defined by label-rendering and printer-device. The finishing choice SHALL NOT change rendered pixels or page dimensions. Advertised formats and copy limits SHALL describe executable behavior. PT-P710BT hardware success SHALL require integrated physical acceptance. PT-2730 SHALL retain byte-identical print regressions; changed PT-2730 print bytes or physical command policy SHALL additionally require a separately bounded physical check before claiming preservation. #### Scenario: Copies - **WHEN** a valid PNG requests copies within its advertised supported range @@ -106,6 +106,22 @@ The server SHALL retain 1:1 default printing, strict tape-width rejection, and o - **WHEN** queued jobs resume after a printer returns with matching tape - **THEN** media is confirmed before printing and each job retains its requested finishing choice +#### Scenario: P710BT finishing equivalence +- **WHEN** a PT-P710BT receives valid PNG-copy or multi-page raster batches using any of the four advertised finishing presets +- **THEN** mandatory first-label precut, internal cuts, held ends, final cuts, and healthy continuation match the existing batch-printing requirements + +#### Scenario: Hardware claims require integrated evidence +- **WHEN** P710BT support is documented as hardware-certified +- **THEN** retained evidence includes real-printer observations of the integrated server's finishing, continuation, rendering, offline recovery, and uncertain-output recovery; direct geometry probes and socket captures alone do not establish that certification + +#### Scenario: Unchanged PT-2730 output +- **WHEN** adding P710BT support leaves PT-2730's emitted print bytes and physical command policy unchanged +- **THEN** legacy capture and adapter regressions establish preservation without requiring new PT-2730 labels + +#### Scenario: Changed PT-2730 output +- **WHEN** implementing support changes PT-2730's emitted print bytes or physical command policy +- **THEN** the change identifies that difference and verifies it through a separately bounded PT-2730 physical check before claiming preservation + ### Requirement: Actual pages determine label boundaries One IPP job SHALL define one batch. PNG copies and actual PWG raster or URF labels SHALL receive the selected batch policy without treating copies as document pages. Successful end of input SHALL determine the final label even when the total count is omitted. The server SHALL distinguish normal completion from abort cleanup and never infer batches from queued jobs or idle time. diff --git a/openspec/specs/printer-device/spec.md b/openspec/specs/printer-device/spec.md index fba2bdf..b66a3d9 100644 --- a/openspec/specs/printer-device/spec.md +++ b/openspec/specs/printer-device/spec.md @@ -8,28 +8,52 @@ Expose one stable IPP queue for verified hardware, select its USB device predict ### Requirement: Only supported hardware receives print output -The server SHALL create operational queues only for implemented and verified model paths, initially PT-2730. Recognizing a USB ID SHALL NOT grant print support. Invalid or unsupported model configuration SHALL fail explicitly rather than fall back silently. +The server SHALL create operational queues only for implemented and verified model paths: PT-2730 and PT-P710BT. Recognizing a USB ID SHALL NOT grant print support. Invalid or unsupported model configuration SHALL fail explicitly rather than fall back silently. The default model SHALL remain PT-2730; PT-P710BT SHALL require explicit selection with `PTOUCH_MODEL=PT-P710BT`. #### Scenario: Unsupported Brother attached - **WHEN** discovery finds a model without an implemented print path -- **THEN** no PT-2730 raster is sent to that model and the unsupported device is identified in diagnostics +- **THEN** no PT-2730 or PT-P710BT raster is sent to that model and the recognized unsupported device is identified in diagnostics #### Scenario: Invalid configured model - **WHEN** the configured model is unknown or unsupported - **THEN** startup fails with a useful configuration error +#### Scenario: Default remains compatible +- **WHEN** PTOUCH_MODEL is absent or empty +- **THEN** the queue selects PT-2730 and retains its existing identity and print behavior + +#### Scenario: P710BT explicitly selected +- **WHEN** PTOUCH_MODEL is PT-P710BT +- **THEN** the stable queue identifies itself as PT-P710BT and uses the implemented PT-P710BT print path + +#### Scenario: Recognized metadata does not enable another printer +- **WHEN** a recognized P-touch model other than PT-2730 or PT-P710BT is configured +- **THEN** startup rejects it even if its metadata shares protocol flags with a supported model + ### Requirement: Device selection is unambiguous -The server SHALL expose one application queue and consistently select its configured supported model. Multiple matching physical devices SHALL be reported as ambiguous; discovery or opening SHALL NOT silently choose the first match. USB renumbering of a single matching device SHALL NOT require queue replacement. +The server SHALL expose one application queue and consistently select its configured supported model. Multiple matching physical devices SHALL be reported as ambiguous; discovery or opening SHALL NOT silently choose the first match. USB renumbering of a single matching device SHALL NOT require queue replacement. PT-2730 SHALL match USB identity 04f9:2041 and PT-P710BT SHALL match 04f9:20af; another attached model SHALL NOT substitute for the selected one. #### Scenario: Two matching devices -- **WHEN** two PT-2730 units match the configured queue +- **WHEN** two units of the selected supported model match the configured queue - **THEN** printing is blocked with an ambiguity diagnostic #### Scenario: Device replug - **WHEN** the sole matching printer is unplugged and reattached with another USB node number - **THEN** the existing queue recovers without selecting a different model or requiring a server restart +#### Scenario: Both supported models attached +- **WHEN** one PT-2730 and one PT-P710BT are attached and either model is selected +- **THEN** discovery, opening, status, and printing consistently use only the selected model + +#### Scenario: Selected model absent +- **WHEN** PT-P710BT is selected but only PT-2730 is attached, or the reverse +- **THEN** the selected queue remains present and unavailable, diagnostics name the attached but unselected model and the applicable PTOUCH_MODEL selection, and no output is sent to the other printer + +#### Scenario: USB identity conflicts with queue identity +- **WHEN** an explicit supported USB URI names a different model from the configured queue identity +- **THEN** queue creation or opening fails before status or print commands are sent + ### Requirement: Availability and faults are reported truthfully The server SHALL retain the queue while hardware is absent and report standard IPP reasons for unavailability, missing tape, and printer faults. USB presence alone SHALL NOT establish readiness, and parsed printer error bits SHALL be checked before page raster output. Test transports SHALL NOT invoke USB operations on incompatible connection data. @@ -73,3 +97,27 @@ When interrupted output leaves the printer's buffered label or final-cut result #### Scenario: Rejected job before output - **WHEN** validation rejects a job before page setup or raster is sent - **THEN** a previously healthy unfinished run is not reclassified as uncertain merely because the job was rejected + +### Requirement: Selected model controls the print protocol + +The server SHALL send the configured model's implemented USB print protocol. PT-2730 SHALL retain its legacy command and raster encoding. PT-P710BT SHALL use normal 180 dpi, 128-dot TIFF PackBits raster, raster-mode setup, and extended print information with loaded tape width and actual page row count. The page indication SHALL preserve the physical printer run across IPP batches without requiring a total label count. + +#### Scenario: P710BT page setup +- **WHEN** a valid page begins on a selected PT-P710BT +- **THEN** the output selects raster mode and automatic status notification, specifies that page's tape width and row count using the extended print-information command, selects TIFF PackBits, and sends complete 128-dot raster lines + +#### Scenario: Different page lengths in one batch +- **WHEN** a PT-P710BT receives a streaming batch with complete pages of different heights and no declared total page count +- **THEN** each page's print information contains its own height and appropriate physical-run page indication, and normal end of input determines finalization + +#### Scenario: Healthy P710BT continuation +- **WHEN** a PT-P710BT batch follows a healthy unfinished matching-tape run +- **THEN** page setup preserves the physical run and its held end, using a continuation-compatible page indication rather than treating the IPP batch boundary as a fresh printer run + +#### Scenario: PT-2730 regression +- **WHEN** an existing PT-2730 job is submitted after P710BT support is added +- **THEN** its page setup and raster bytes retain the PT-2730 encoding and contain no P710BT-only setup + +#### Scenario: P710BT status failure +- **WHEN** the selected PT-P710BT reports an error or does not confirm receiving-ready after output +- **THEN** the job fails without replay or invented finalization, and uncertain output remains blocked under the existing recovery contract diff --git a/openspec/specs/server-architecture/spec.md b/openspec/specs/server-architecture/spec.md index ab01f37..fa48352 100644 --- a/openspec/specs/server-architecture/spec.md +++ b/openspec/specs/server-architecture/spec.md @@ -29,13 +29,23 @@ Command builders, device/tape tables, raster packing, status parsing, and the tw ### Requirement: Supported and unambiguous hardware -Only the implemented PT-2730 path SHALL receive print output. Missing or empty `PTOUCH_MODEL` SHALL select PT-2730; unknown or recognized unsupported models SHALL fail configuration. The stable model URI SHALL resolve by VID/PID, accepting exactly one matching device and blocking zero or multiple matches without selecting by enumeration order. +Only the implemented PT-2730 and PT-P710BT paths SHALL receive print output. Missing or empty `PTOUCH_MODEL` SHALL select PT-2730; PT-P710BT SHALL require explicit selection. Unknown or recognized unsupported models SHALL fail configuration. The stable model URI SHALL resolve by the selected VID/PID, accepting exactly one matching device and blocking zero or multiple matches without selecting by enumeration order. #### Scenario: Duplicate supported printers -- **WHEN** two PT-2730 devices match the configured model +- **WHEN** two devices of the selected supported model match the configured queue - **THEN** the existing queue reports ambiguity and neither device is claimed for printing +#### Scenario: Two different supported models + +- **WHEN** one PT-2730 and one PT-P710BT are attached and a model is configured +- **THEN** the queue selects only that model, and the other printer does not create ambiguity or receive its output + +#### Scenario: Compatible default + +- **WHEN** no model is configured +- **THEN** the queue retains PT-2730 identity and behavior without automatically adopting an attached PT-P710BT + ### Requirement: Private USB transport and fresh discovery The adapter SHALL use the internal `ptouch://` scheme with bounded libusb transfers and a fresh context per discovery/open operation. It SHALL avoid the built-in USB device-ID probe that hangs on PT-2730. Status readers SHALL validate the owning printer URI before accessing USB connection data; attached USB hardware SHALL NOT change another transport's type. diff --git a/scripts/smoke-image.py b/scripts/smoke-image.py index eaab29d..c0dd991 100644 --- a/scripts/smoke-image.py +++ b/scripts/smoke-image.py @@ -15,7 +15,112 @@ import zlib sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tests")) -from test_cut_probe import decode, summary +from test_cut_probe import summary + + +def unpackbits(payload): + """Decode one TIFF PackBits row, bounded by the raster packet length.""" + decoded = bytearray() + offset = 0 + while offset < len(payload): + control = payload[offset] + offset += 1 + if control < 128: + count = control + 1 + if count > len(payload) - offset: + raise ValueError("truncated PackBits literal") + decoded.extend(payload[offset:offset + count]) + offset += count + elif control > 128: + if offset == len(payload): + raise ValueError("truncated PackBits repeat") + decoded.extend(payload[offset:offset + 1] * (257 - control)) + offset += 1 + if len(decoded) > 16: + raise ValueError("oversized PackBits row") + if len(decoded) != 16: + raise ValueError("raster row does not contain all 128 head dots") + return bytes(decoded) + + +def decode(data, model): + """Check model setup and normalize raster packets for shared finishing checks.""" + commands = [] + offset = 0 + compressed = False + fresh = False + page_rows = expected_rows = 0 + p710bt = model == "PT-P710BT" + escapes = {ord("R"): (4, "rasterstart"), ord("a"): (4, "rasterstart"), + ord("c"): (8, "printinfo"), ord("z"): (13, "printinfo"), + ord("M"): (4, "mode"), ord("K"): (4, "advanced"), + ord("!"): (4, "notification"), ord("d"): (5, "margin")} + while offset < len(data): + op = data[offset] + if op == 0: + assert p710bt, "PT-2730 received invalidation" + size, name = 1, "invalidate" + elif op == 0x47: + if len(data) - offset < 3 or data[offset + 2] != 0: + raise ValueError("invalid raster header") + size, name = 3 + data[offset + 1], "raster" + elif data[offset:offset + 2] == b"\x1b@": + size, name = 2, "init" + fresh, compressed = True, False + elif op == ord("M"): + size, name = 2, "compression" + elif data[offset:offset + 2] == b"\x1bi": + if len(data) - offset < 3 or data[offset + 2] not in escapes: + raise ValueError("invalid escape command") + size, name = escapes[data[offset + 2]] + elif op in (12, 26): + size, name = 1, "ff" if op == 12 else "eject" + else: + raise ValueError(f"unknown command at {offset}") + if size > len(data) - offset: + raise ValueError(f"truncated {name} at {offset}") + command = data[offset:offset + size] + if name == "compression": + assert p710bt and command == b"M\x02", command + compressed = True + elif name == "rasterstart": + assert command == (b"\x1bia\x01" if p710bt else b"\x1biR\x01"), command + elif name == "notification": + assert p710bt and command == b"\x1bi!\x00", command + elif name == "margin" and p710bt: + assert command == b"\x1bid\x0e\x00", command + elif name == "printinfo": + assert command[:4] == (b"\x1biz\x84" if p710bt else b"\x1bic\x84"), command + if p710bt: + assert command[4] == 0 and command[5] == 12 and command[6] == 0 and command[12] == 0, command + expected_rows = int.from_bytes(command[7:11], "little") + assert command[11] == (0 if fresh else 1), command + fresh = False + elif name == "raster": + assert compressed == p710bt + head = unpackbits(command[3:]) if compressed else command[3:] + assert len(head) == 16 + command = b"G\x10\x00" + head + page_rows += 1 + elif name in ("ff", "eject"): + if p710bt: + assert page_rows == expected_rows, (page_rows, expected_rows) + page_rows = 0 + commands.append((name, command)) + offset += size + assert page_rows == 0, "capture contains an unterminated page" + return commands + + +def check_geometry(commands, copies): + rows = [data[3:] for name, data in commands if name == "raster"] + assert len(rows) == 354 * copies + for page in range(copies): + for y in range(354): + for x in range(128): + actual = bool(rows[page * 354 + y][15 - x // 8] & (1 << (x % 8))) + expected = 26 <= x < 102 and 87 <= y < 267 and (x in (26, 101) or y in (87, 266)) + assert actual == expected, (page, x, y, actual, expected) def run(*args): @@ -23,15 +128,15 @@ def run(*args): stderr=subprocess.STDOUT).stdout -def png(path): +def png(path, width=76): def chunk(name, payload): return (struct.pack("!I", len(payload)) + name + payload + struct.pack("!I", zlib.crc32(name + payload))) # A 76-dot imageable design on 12 mm tape, with explicit 180 dpi metadata. - pixels = b"".join(b"\0" + bytes(0 if x in (0, 75) or y in (0, 179) else 255 - for x in range(76)) for y in range(180)) + pixels = b"".join(b"\0" + bytes(0 if x in (0, width - 1) or y in (0, 179) else 255 + for x in range(width)) for y in range(180)) path.write_bytes(b"\x89PNG\r\n\x1a\n" + - chunk(b"IHDR", struct.pack("!2I5B", 76, 180, 8, 0, 0, 0, 0)) + + chunk(b"IHDR", struct.pack("!2I5B", width, 180, 8, 0, 0, 0, 0)) + chunk(b"pHYs", struct.pack("!2IB", 7087, 7087, 1)) + chunk(b"IDAT", zlib.compress(pixels)) + chunk(b"IEND", b"")) @@ -86,11 +191,11 @@ def ipp(url, directory, text): raise RuntimeError(error.stdout) from error -def submit(url, directory, file, finishing="", copies=1, state=9): +def submit(url, directory, file, finishing="", copies=1, state=9, media="om_12x50mm_12x50mm"): request = operation("Print-Job", f'''ATTR mimeMediaType document-format image/png GROUP job-attributes-tag ATTR integer copies {copies} - ATTR keyword media om_12x50mm_12x50mm + ATTR keyword media {media} ATTR keyword print-scaling none {finishing} FILE "{file}"''', "EXPECT job-id WITH-VALUE >0 DEFINE-VALUE job-id") @@ -103,6 +208,8 @@ def submit(url, directory, file, finishing="", copies=1, state=9): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("image", help="already-built deploy image") + parser.add_argument("--model", choices=("PT-2730", "PT-P710BT"), default="PT-2730", + help="selected printer model (default: PT-2730)") args = parser.parse_args() name = "ptouch-smoke-" + uuid.uuid4().hex[:10] sink = Sink() @@ -110,7 +217,8 @@ def main(): run("docker", "run", "-d", "--name", name, "--add-host", "host.docker.internal:host-gateway", "-p", "127.0.0.1::8000", "-e", - f"PTOUCH_DEVICE_URI=socket://host.docker.internal:{sink.port}", args.image) + f"PTOUCH_DEVICE_URI=socket://host.docker.internal:{sink.port}", + "-e", f"PTOUCH_MODEL={args.model}", args.image) mapping = json.loads(run("docker", "inspect", "--format", "{{json .NetworkSettings.Ports}}", name)) url = f"ipp://127.0.0.1:{mapping['8000/tcp'][0]['HostPort']}/ipp/print/ptouch" @@ -120,12 +228,13 @@ def main(): png(image) attributes = operation("Get-Printer-Attributes", "ATTR keyword requested-attributes all", ''' + EXPECT printer-make-and-model WITH-VALUE "Brother MODEL" EXPECT finishings-supported COUNT 4 WITH-ALL-VALUES 3,11,60,63 EXPECT finishings-default WITH-VALUE 11 EXPECT finishings-col-database OF-TYPE collection EXPECT finishing-template-supported WITH-VALUE ptouch-chain EXPECT document-format-supported WITH-VALUE image/png WITH-DISTINCT-VALUES - EXPECT document-format-supported WITH-ALL-VALUES "/^(application/octet-stream|image/(pwg-raster|urf|png|jpeg))$/"''') + EXPECT document-format-supported WITH-ALL-VALUES "/^(application/octet-stream|image/(pwg-raster|urf|png|jpeg))$/"'''.replace("MODEL", args.model)) for attempt in range(20): try: print(ipp(url, directory, attributes)) @@ -145,7 +254,9 @@ def main(): ) for label, finishing, autocuts, at_end, resets in cases: print(submit(url, directory, image, finishing, copies=3)) - result = summary(decode(sink.take())) + commands = decode(sink.take(), args.model) + check_geometry(commands, 3) + result = summary(commands) pages = result["pages"] assert len(pages) == 3, result assert [p["rows"] for p in pages] == [354] * 3, result @@ -167,19 +278,33 @@ def main(): f'''EXPECT finishings-default WITH-VALUE {value} EXPECT finishings-col-default OF-TYPE {out_type}'''))) print(submit(url, directory, image, copies=2)) - result = summary(decode(sink.take())) + commands = decode(sink.take(), args.model) + check_geometry(commands, 2) + result = summary(commands) assert [p["rows"] for p in result["pages"]] == [354, 354], result assert [p["autocut"] for p in result["pages"]] == [True, False], result assert [p["chain_off_at_print"] for p in result["pages"]] == [False, at_end], result assert [p["finish"] for p in result["pages"]] == ["ff", "eject" if at_end else "ff"], result assert result["init"] == 1 and not result["terminal_commands_after_pages"], result print("default", value, json.dumps(result)) + oversized = directory / "oversized.png" + png(oversized, width=77) + print(submit(url, directory, oversized, state=8)) + assert sink.take() == b"", "Oversized image emitted printer commands" + print(submit(url, directory, image, state=8, media="om_18x50mm_18x50mm")) + assert sink.take() == b"", "Wrong-tape image emitted printer commands" malformed = directory / "malformed.png" malformed.write_bytes(b"\x89PNG\r\n\x1a\n") print(submit(url, directory, malformed, state=8)) wire = sink.take() assert wire == b"", "Malformed PNG emitted printer commands" - print("Image smoke passed: four presets, synchronized defaults, geometry, copies, and input failure.") + print(submit(url, directory, image, "ATTR enum finishings 60")) + commands = decode(sink.take(), args.model) + check_geometry(commands, 1) + result = summary(commands) + assert result["init"] == 0 and result["eject"] == 1, result + assert len(result["pages"]) == 1 and not result["terminal_commands_after_pages"], result + print(args.model, "image smoke passed: four presets, synchronized defaults, geometry, copies, and input failure.") finally: subprocess.run(["docker", "logs", name], check=False) subprocess.run(["docker", "rm", "-f", name], check=False, diff --git a/src/device_usb.c b/src/device_usb.c index 4e069f4..e7f6320 100644 --- a/src/device_usb.c +++ b/src/device_usb.c @@ -22,14 +22,35 @@ const pt_dev *pt_usb_supported_model(const char *model) { if (!model || !*model) model = "PT-2730"; - return !strcmp(model, "PT-2730") ? pt_lookup_name(model) : NULL; + return (!strcmp(model, "PT-2730") || !strcmp(model, "PT-P710BT")) + ? pt_lookup_name(model) : NULL; +} + +const pt_dev *pt_usb_model_from_uri(const char *uri) +{ + const char *prefix = "ptouch://Brother/"; + return uri && !strncmp(uri, prefix, strlen(prefix)) && uri[strlen(prefix)] + ? pt_usb_supported_model(uri + strlen(prefix)) : NULL; +} + +void pt_usb_format_uri(const pt_dev *model, char *buffer, size_t size) +{ + snprintf(buffer, size, "ptouch://Brother/%s", model->name); +} + +void pt_usb_format_id(const pt_dev *model, char *buffer, size_t size) +{ + snprintf(buffer, size, "MFG:Brother;MDL:%s;CMD:PT-CBP;", model->name); } bool pt_usb_is_uri(const char *uri) { - return uri && !strcmp(uri, "ptouch://Brother/PT-2730"); + return pt_usb_model_from_uri(uri) != NULL; } +/* One application queue; registration fixes its discovery selection. */ +static const pt_dev *discovery_model; + static ssize_t count_matches(libusb_device **devs, ssize_t n, uint16_t vid, uint16_t pid, libusb_device **match) { @@ -49,6 +70,7 @@ typedef struct { libusb_context *ctx; /* per-connection context, exited on close */ libusb_device_handle *h; int detached; + const pt_dev *model; } pt_usb_conn; /* Every enumeration uses a FRESH libusb context. A process-lifetime context goes @@ -74,21 +96,36 @@ static bool pt_usb_list(pappl_device_cb_t cb, void *data, if (libusb_get_device_descriptor(devs[i], &d) != 0 || d.idVendor != PT_VID) continue; const pt_dev *dev = pt_lookup_dev(d.idVendor, d.idProduct); - if (!dev || pt_usb_supported_model(dev->name)) + if (!dev || dev == discovery_model) continue; - char message[160]; - snprintf(message, sizeof message, "%s (%04x:%04x) is recognized but not print-supported", - dev->name, d.idVendor, d.idProduct); + char message[200]; + if (pt_usb_supported_model(dev->name)) + snprintf(message, sizeof message, + "%s (%04x:%04x) is attached but unselected; select PTOUCH_MODEL=%s", + dev->name, d.idVendor, d.idProduct, dev->name); + else + snprintf(message, sizeof message, "%s (%04x:%04x) is recognized but not print-supported", + dev->name, d.idVendor, d.idProduct); if (err_cb) err_cb(message, err_data); } - const pt_dev *supported = pt_usb_supported_model(NULL); + const pt_dev *supported = discovery_model; + if (!supported) { + libusb_free_device_list(devs, 1); + libusb_exit(ctx); + return false; + } ssize_t count = count_matches(devs, n, supported->vid, supported->pid, NULL); bool stop = false; - if (count == 1) - stop = cb("PT-2730 (USB)", "ptouch://Brother/PT-2730", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", data); - else if (count > 1 && err_cb) - err_cb("Multiple PT-2730 devices match; USB selection is ambiguous", err_data); + char uri[80], id[100], message[160]; + pt_usb_format_uri(supported, uri, sizeof uri); + pt_usb_format_id(supported, id, sizeof id); + if (count == 1) { + snprintf(message, sizeof message, "%s (USB)", supported->name); + stop = cb(message, uri, id, data); + } else if (count > 1 && err_cb) { + snprintf(message, sizeof message, "Multiple %s devices match; USB selection is ambiguous", supported->name); + err_cb(message, err_data); + } libusb_free_device_list(devs, 1); libusb_exit(ctx); return stop; @@ -97,7 +134,8 @@ static bool pt_usb_list(pappl_device_cb_t cb, void *data, static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const char *name) { (void)name; - if (!pt_usb_is_uri(device_uri)) { + const pt_dev *supported = pt_usb_model_from_uri(device_uri); + if (!supported) { papplDeviceError(device, "Unsupported P-touch device URI: %s", device_uri); return false; } @@ -112,12 +150,13 @@ static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const ch return false; } - const pt_dev *supported = pt_usb_supported_model(NULL); libusb_device *match = NULL; ssize_t count = count_matches(devs, n, supported->vid, supported->pid, &match); if (count != 1) { - papplDeviceError(device, count > 1 ? "Multiple PT-2730 devices match; USB selection is ambiguous" - : "PT-2730 is not attached"); + if (count > 1) + papplDeviceError(device, "Multiple %s devices match; USB selection is ambiguous", supported->name); + else + papplDeviceError(device, "%s is not attached", supported->name); libusb_free_device_list(devs, 1); libusb_exit(ctx); return false; @@ -151,6 +190,7 @@ static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const ch c->ctx = ctx; c->h = h; c->detached = detached; + c->model = supported; papplDeviceSetData(device, c); return true; } @@ -229,8 +269,8 @@ static pappl_preason_t pt_usb_status(pappl_device_t *device) static char *pt_usb_id(pappl_device_t *device, char *buffer, size_t bufsize) { - (void)device; - if (bufsize) snprintf(buffer, bufsize, "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;"); + pt_usb_conn *c = papplDeviceGetData(device); + if (c) pt_usb_format_id(c->model, buffer, bufsize); return buffer; } @@ -260,6 +300,7 @@ bool pt_usb_present(uint16_t vid, uint16_t pid) void pt_usb_register(void) { + discovery_model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); papplDeviceAddScheme("ptouch", PAPPL_DEVTYPE_CUSTOM_LOCAL, pt_usb_list, pt_usb_open, pt_usb_close, pt_usb_read, pt_usb_write, pt_usb_status, pt_usb_id); diff --git a/src/device_usb.h b/src/device_usb.h index 34231ba..bff55c2 100644 --- a/src/device_usb.h +++ b/src/device_usb.h @@ -10,6 +10,11 @@ /* NULL/empty selects the default; recognized models without a print path fail. */ const pt_dev *pt_usb_supported_model(const char *model); +/* Exact supported URI resolution and synthetic identity; no hardware reads. */ +const pt_dev *pt_usb_model_from_uri(const char *uri); +void pt_usb_format_uri(const pt_dev *model, char *buffer, size_t size); +void pt_usb_format_id(const pt_dev *model, char *buffer, size_t size); + /* Exact supported model URI; test/network transports never use USB data. */ bool pt_usb_is_uri(const char *uri); diff --git a/src/driver.c b/src/driver.c index 7aceb06..959851a 100644 --- a/src/driver.c +++ b/src/driver.c @@ -213,6 +213,7 @@ static bool media_col_for(int tape_mm, int length_centimm, int dpi, pappl_media_ typedef enum { PT_RUN_CLOSED, PT_RUN_OPEN, PT_RUN_UNCERTAIN } pt_run; typedef struct { pthread_mutex_t lock; + const pt_dev *model; pt_run run; bool media_confirmed; } pt_printer_state; @@ -387,14 +388,23 @@ static bool r_startpage(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d if (b->pending && !pt_print_pending(j, d, false)) return pt_fail_job(j); bool continuation = pt_chain_get(printer); pt_run_set(printer, PT_RUN_UNCERTAIN); - uint8_t buf[8]; + const pt_dev *model = pt_state(printer)->model; + bool p710bt = model->pid == 0x20af; + uint8_t buf[100]; if (b->first_label && - ((!continuation && !pt_dev_write(d, buf, pt_cmd_init(buf))) || - !pt_dev_write(d, buf, pt_cmd_rasterstart(buf, 0)))) + ((!continuation && p710bt && !pt_dev_write(d, buf, pt_cmd_invalidate(buf))) || + (!continuation && !pt_dev_write(d, buf, pt_cmd_init(buf))) || + !pt_dev_write(d, buf, pt_cmd_rasterstart(buf, model->flags)) || + (p710bt && !pt_dev_write(d, buf, pt_cmd_auto_status(buf, true))))) return pt_fail_job(j); - if (!pt_dev_write(d, buf, pt_cmd_printinfo(buf, b->tape_mm)) || + size_t info_len = p710bt ? pt_cmd_printinfo_extended(buf, b->tape_mm, + o->header.cupsHeight, b->first_label && !continuation ? 0 : 1) : + pt_cmd_printinfo(buf, b->tape_mm); + if (!pt_dev_write(d, buf, info_len) || !pt_dev_write(d, buf, pt_cmd_setmode(buf, pt_cut_autocut(b->policy, b->first_label) ? 0x40 : 0)) || - !pt_dev_write(d, buf, pt_cmd_setadvanced(buf, 0))) + !pt_dev_write(d, buf, pt_cmd_setadvanced(buf, 0)) || + (p710bt && (!pt_dev_write(d, buf, pt_cmd_margin(buf, 14)) || + !pt_dev_write(d, buf, pt_cmd_compression(buf, 2))))) return pt_fail_job(j); b->first_label = false; b->page_active = true; @@ -413,7 +423,11 @@ static bool r_writeline(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d return pt_fail_job(j); uint8_t packed[16], buf[24]; pt_pack_window(packed, l, g.left_px, g.imageable_px); - return pt_dev_write(d, buf, pt_cmd_sendraster(buf, packed, sizeof packed)) || pt_fail_job(j); + const pt_dev *model = pt_state(papplJobGetPrinter(j))->model; + size_t length = model->flags & FLAG_RASTER_PACKBITS ? + pt_cmd_sendraster_packbits(buf, packed, sizeof packed) : + pt_cmd_sendraster(buf, packed, sizeof packed); + return pt_dev_write(d, buf, length) || pt_fail_job(j); } /* Wait for receive-ready, not just print-completed. This is USB flow control, @@ -602,13 +616,13 @@ static bool status_cb(pappl_printer_t *printer) if (pt_recovery_required(printer)) return true; const char *uri = papplPrinterGetDeviceURI(printer); if (!pt_usb_is_uri(uri)) return true; - const pt_dev *model = pt_lookup_name("PT-2730"); + const pt_dev *model = pt_state(printer)->model; ssize_t count = pt_usb_count(model->vid, model->pid); if (count != 1) { papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PT_STATUS_REASONS & ~PAPPL_PREASON_OFFLINE); pt_set_media_confirmed(printer, false); if (!count) pt_chain_set(printer, false); - if (count > 1) papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "Multiple PT-2730 devices match; selection is ambiguous"); + if (count > 1) papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "Multiple %s devices match; selection is ambiguous", model->name); return true; } pappl_device_t *device = papplPrinterOpenDevice(printer); @@ -617,7 +631,7 @@ static bool status_cb(pappl_printer_t *printer) if (papplPrinterGetState(printer) == IPP_PSTATE_PROCESSING) return true; papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PT_STATUS_REASONS & ~PAPPL_PREASON_OFFLINE); pt_set_media_confirmed(printer, false); - papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "PT-2730 is present but cannot be opened or claimed"); + papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "%s is present but cannot be opened or claimed", model->name); return true; } /* Status requests do not initialize or finalize the printer. */ @@ -673,16 +687,23 @@ bool pt_driver_cb(pappl_system_t *system, const char *driver_name, const char *d char model[64]; parse_mdl(device_id, model, sizeof model); - const pt_dev *dev = pt_lookup_name(model); - if (!dev || strcmp(dev->name, "PT-2730")) { + const pt_dev *dev = model[0] ? pt_usb_supported_model(model) : NULL; + if (!dev) { papplLog(system, PAPPL_LOGLEVEL_ERROR, - "Unsupported printer identity '%s'; only PT-2730 is print-supported", model); + "Unsupported printer identity '%s'; select PT-2730 or PT-P710BT", model); + return false; + } + if (device_uri && !strncmp(device_uri, "ptouch://", 9) && + pt_usb_model_from_uri(device_uri) != dev) { + papplLog(system, PAPPL_LOGLEVEL_ERROR, + "Printer identity '%s' does not match USB URI '%s'", model, device_uri); return false; } pt_printer_state *state = calloc(1, sizeof *state); if (!state) return false; if (pthread_mutex_init(&state->lock, NULL)) { free(state); return false; } + state->model = dev; data->extension = state; data->rstartjob_cb = r_startjob; diff --git a/src/main.c b/src/main.c index fa2fae5..0fb65af 100644 --- a/src/main.c +++ b/src/main.c @@ -41,7 +41,9 @@ static int probe_cb(const char *base, int num_options, cups_option_t *options, printf("probing ptouch:// (CUSTOM_LOCAL only)...\n"); papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, probe_print_cb, NULL, probe_error_cb, NULL); printf("probe done\n"); - printf("PRESENT 04f9:2041 = %s\n", pt_usb_present(0x04f9, 0x2041) ? "yes" : "no"); + const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); + printf("PRESENT %04x:%04x = %s\n", model->vid, model->pid, + pt_usb_present(model->vid, model->pid) ? "yes" : "no"); return 0; } @@ -74,14 +76,18 @@ static pappl_system_t *system_cb(int num_options, cups_option_t *options, void * papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); + const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); + char uri[80], id[100]; + pt_usb_format_uri(model, uri, sizeof uri); + pt_usb_format_id(model, id, sizeof id); + /* Test override: create the printer on an explicit URI (e.g. a socket sink) * instead of enumerating ptouch://. Lets the geometry gate run against - * socket://127.0.0.1:9100 with no PT-2730 involved (no physical printing). */ + * socket://127.0.0.1:9100 with no USB printer involved (no physical printing). */ const char *override_uri = getenv("PTOUCH_DEVICE_URI"); if (override_uri && *override_uri) { pappl_printer_t *p = papplPrinterCreate(sys, 0, "ptouch", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", - override_uri); + id, override_uri); papplLog(sys, p ? PAPPL_LOGLEVEL_INFO : PAPPL_LOGLEVEL_ERROR, "%s printer for %s (PTOUCH_DEVICE_URI override)", p ? "Created" : "FAILED to create", override_uri); @@ -90,17 +96,15 @@ static pappl_system_t *system_cb(int num_options, cups_option_t *options, void * /* Always create the configured supported model's stable queue. Discovery * supplies diagnostics, never a first-device fallback or a different model. */ - const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, discovery_cb, NULL, discovery_error_cb, sys); ssize_t matches = pt_usb_count(model->vid, model->pid); pappl_printer_t *p = papplPrinterCreate(sys, 0, "ptouch", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", - "ptouch://Brother/PT-2730"); + id, uri); if (p && matches != 1) papplPrinterSetReasons(p, PAPPL_PREASON_OFFLINE, PAPPL_PREASON_NONE); papplLog(sys, p ? PAPPL_LOGLEVEL_INFO : PAPPL_LOGLEVEL_ERROR, - "%s printer for ptouch://Brother/PT-2730 (%zd matching USB devices)", - p ? "Created" : "FAILED to create", matches); + "%s printer for %s (%d matching USB devices)", + p ? "Created" : "FAILED to create", uri, (int)matches); return sys; } @@ -122,11 +126,13 @@ static void smoke_err_cb(const char *message, void *data) static int run_smoke(void) { pt_usb_register(); - const char *uri = "ptouch://Brother/PT-2730"; + char uri[80]; const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); + pt_usb_format_uri(model, uri, sizeof uri); ssize_t count = pt_usb_count(model->vid, model->pid); if (count != 1) { - fprintf(stderr, "smoke: expected one PT-2730, found %zd (none or ambiguous)\n", count); + papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, discovery_cb, NULL, smoke_err_cb, NULL); + fprintf(stderr, "smoke: expected one %s, found %zd (none or ambiguous)\n", model->name, count); return 2; } pappl_device_t *dev = papplDeviceOpen(uri, "smoke", smoke_err_cb, NULL); @@ -149,11 +155,19 @@ static int run_smoke(void) int main(int argc, char *argv[]) { const char *configured = getenv("PTOUCH_MODEL"); - if (!pt_usb_supported_model(configured)) { - fprintf(stderr, "PTOUCH_MODEL=%s is %s; only PT-2730 is print-supported\n", + const pt_dev *model = pt_usb_supported_model(configured); + if (!model) { + fprintf(stderr, "PTOUCH_MODEL=%s is %s; only PT-2730 and PT-P710BT are print-supported\n", configured, pt_lookup_name(configured) ? "recognized but unsupported" : "unknown"); return EXIT_FAILURE; } + const char *override_uri = getenv("PTOUCH_DEVICE_URI"); + if (override_uri && !strncmp(override_uri, "ptouch://", 9) && + pt_usb_model_from_uri(override_uri) != model) { + fprintf(stderr, "PTOUCH_DEVICE_URI=%s conflicts with PTOUCH_MODEL=%s\n", + override_uri, model->name); + return EXIT_FAILURE; + } if (argc >= 2 && strcmp(argv[1], "smoke") == 0) return run_smoke(); return papplMainloop(argc, argv, "1.0", NULL, diff --git a/src/protocol.c b/src/protocol.c index 6846b7c..2ef320c 100644 --- a/src/protocol.c +++ b/src/protocol.c @@ -2,6 +2,12 @@ #include "protocol.h" #include "tables.h" +size_t pt_cmd_invalidate(uint8_t *o) +{ + memset(o, 0, 100); + return 100; +} + size_t pt_cmd_init(uint8_t *o) { o[0] = 0x1b; o[1] = 0x40; @@ -35,6 +41,30 @@ size_t pt_cmd_printinfo(uint8_t *o, uint8_t mm) return 8; } +size_t pt_cmd_printinfo_extended(uint8_t *o, uint8_t mm, uint32_t rows, uint8_t page) +{ + o[0] = 0x1b; o[1] = 0x69; o[2] = 0x7a; o[3] = 0x84; + o[4] = 0x00; o[5] = mm; o[6] = 0x00; + o[7] = (uint8_t)rows; + o[8] = (uint8_t)(rows >> 8); + o[9] = (uint8_t)(rows >> 16); + o[10] = (uint8_t)(rows >> 24); + o[11] = page; o[12] = 0x00; + return 13; +} + +size_t pt_cmd_compression(uint8_t *o, uint8_t mode) +{ + o[0] = 0x4d; o[1] = mode; + return 2; +} + +size_t pt_cmd_auto_status(uint8_t *o, bool enabled) +{ + o[0] = 0x1b; o[1] = 0x69; o[2] = 0x21; o[3] = enabled ? 0x00 : 0x01; + return 4; +} + size_t pt_cmd_setmode(uint8_t *o, uint8_t flags) { o[0] = 0x1b; o[1] = 0x69; o[2] = 0x4d; o[3] = flags; diff --git a/src/protocol.h b/src/protocol.h index 57a8779..ecf73df 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -3,17 +3,23 @@ #include #include +#include /* Brother P-touch raster command builders. Each writes bytes into the caller's * buffer `o` and returns the number of bytes written. ALL device byte sequences - * live here so the adapter never hardcodes Brother bytes. Byte values verified - * against hannesweisbach/ptouch-print + on PT-2730 hardware in M0. */ + * live here so the adapter never hardcodes Brother bytes. Legacy commands were + * verified on PT-2730; extended setup matches the P710BT geometry probes. */ +size_t pt_cmd_invalidate(uint8_t *o); /* 100 zero bytes */ size_t pt_cmd_init(uint8_t *o); /* 1b 40 ESC @ */ size_t pt_cmd_rasterstart(uint8_t *o, uint32_t flags); /* 1b6952 01 / 1b6961 01 */ size_t pt_cmd_ff(uint8_t *o); /* 0c feed */ size_t pt_cmd_eject(uint8_t *o); /* 1a eject */ size_t pt_cmd_printinfo(uint8_t *o, uint8_t mm); /* 1b6963 84 00 mm 00 00 */ +/* P710BT: 13-byte ESC i z, actual page rows (LE32), page 0 fresh / 1 continued. */ +size_t pt_cmd_printinfo_extended(uint8_t *o, uint8_t mm, uint32_t rows, uint8_t page); +size_t pt_cmd_compression(uint8_t *o, uint8_t mode); /* 4d mode (02 PackBits) */ +size_t pt_cmd_auto_status(uint8_t *o, bool enabled); /* 1b6921 00 on / 01 off */ size_t pt_cmd_setmode(uint8_t *o, uint8_t flags); /* 1b694d */ size_t pt_cmd_setadvanced(uint8_t *o, uint8_t flags); /* 1b694b */ size_t pt_cmd_status_request(uint8_t *o); /* 1b6953 ESC iS */ diff --git a/src/tables.c b/src/tables.c index 15c05ec..4726014 100644 --- a/src/tables.c +++ b/src/tables.c @@ -2,9 +2,9 @@ #include #include "tables.h" -/* P-touch device family (transcribed from hannesweisbach/ptouch-print - * src/libptouch.c). Only the PT-2730 is hardware-certified here; the rest are - * best-effort, untested. All known models are 180dpi / 128px max. */ +/* Recognition metadata, not a print-support gate. PT-2730 and PT-P710BT are + * certified (P710BT on 9/12/18 mm over USB). Other rows are untested. + * All catalogued models use a 180dpi / 128-dot head. */ static const pt_dev g_devs[] = { {0x04f9, 0x2007, "PT-2420PC", 128, 180, FLAG_RASTER_PACKBITS}, {0x04f9, 0x202c, "PT-1230PC", 128, 180, FLAG_NONE}, @@ -20,6 +20,7 @@ static const pt_dev g_devs[] = { {0x04f9, 0x2065, "PT-P750W (PLite)", 128, 180, FLAG_PLITE}, {0x04f9, 0x2073, "PT-D450", 128, 180, FLAG_RASTER_PACKBITS}, {0x04f9, 0x2074, "PT-D600", 128, 180, FLAG_RASTER_PACKBITS}, + {0x04f9, 0x20af, "PT-P710BT", 128, 180, FLAG_RASTER_PACKBITS | FLAG_P700_INIT}, {0, 0, NULL, 0, 0, 0}, }; diff --git a/src/tables.h b/src/tables.h index c83100d..3881562 100644 --- a/src/tables.h +++ b/src/tables.h @@ -28,7 +28,7 @@ const pt_dev *pt_lookup_name(const char *name); /* Imageable px for a tape width in mm; 0 if unknown. */ int pt_tape_px(int mm); -/* PT-2730's canonical physical window at 180 dpi. right_px is exclusive; +/* Canonical 180dpi window shared by the supported models. right_px is exclusive; * margin_centimm is the physical minimum, with pixel-aligned published margins. */ typedef struct { int nominal_px, imageable_px, left_px, right_px, margin_centimm; diff --git a/tests/FINISHINGS.md b/tests/FINISHINGS.md index d674578..b3a92cf 100644 --- a/tests/FINISHINGS.md +++ b/tests/FINISHINGS.md @@ -33,11 +33,18 @@ legacy-driver compatibility, and atomic enum/collection default updates. The raster fixture covers malformed first/next headers, incomplete rows/pages, and negative input callbacks before successful end-job cleanup. -`python3 scripts/smoke-image.py IMAGE` runs the built deploy image, submits real -IPP PNG jobs for all four policies and the omitted default, and captures actual +`python3 scripts/smoke-image.py IMAGE --model PT-2730` and the same command with +`--model PT-P710BT` run the built deploy image, submit real +IPP PNG jobs for all four policies and the omitted default, and capture actual socket output. It checks explicit 50 mm pages, copies, public capabilities, and malformed-image failure without USB access. This requires Docker and ipptool. +Production-driver tests execute the same finishing and failure fixtures for both +models. Length-aware P710BT decoding checks TIFF PackBits rows, actual page +heights, notification, feed margin, and first/subsequent physical-run page flags. +PT-2730 continues to use legacy setup and raw rows, with no P710BT-only commands. +The common deferred-print and closed/open/uncertain lifecycle remains unchanged. + `scripts/check-cups.py /path/to/ptouch-app` runs inside an isolated container with CUPS, cups-filters, Ghostscript, ipptool, and Pillow. It creates an ordinary driverless queue, submits PNG/JPEG, two-label PDFs, and two copies of a PDF with registered finishing @@ -52,6 +59,17 @@ printer observations. ## Physical evidence +P710BT integrated acceptance passed on 2026-10-13 under +[P710BT-ACCEPTANCE.md](P710BT-ACCEPTANCE.md): 15 complete labels including four +operator-requested repeats, all four policies, healthy continuation, unknown-count +mixed lengths, strict wrong-media rejection, USB-only offline resumption, and +button-power-cycle/server-restart recovery without replay. Physical geometry +was checked on 9/12/18 mm; 6/24 mm remain software-only. Every command capture +matches its socket reference after removing status requests. The candidate was +removed; the original service's image/configuration remains unchanged. + +The remaining evidence below concerns PT-2730. + The approved eight-label direct-protocol gate passed on the PT-2730 with 24 mm tape: chain:2 then each:2, and none:2 then end:2, with an observation/idle gap at each batch boundary. The operator confirmed initial leaders cut off, expected diff --git a/tests/P710BT-ACCEPTANCE.md b/tests/P710BT-ACCEPTANCE.md new file mode 100644 index 0000000..3a73f64 --- /dev/null +++ b/tests/P710BT-ACCEPTANCE.md @@ -0,0 +1,278 @@ +# P710BT integrated acceptance gate + +Status: **passed on 2026-10-13; candidate removed, original image/configuration retained**. Preparation sent +IPP jobs to an isolated socket sink only. It did not access `vault`, mount USB, +print labels, deploy a service, or certify physical behavior. + +The candidate is `ptouch-p710bt:local`, explicitly selected with +`PTOUCH_MODEL=PT-P710BT`. The staged bundle is +`openclaw:/tmp/p710bt-integrated-gate-20261013/`; its durable copy is +`openclaw:~/.local/share/ptouch-validation/p710bt-20261013/integrated-gate/`. +It contains `candidate-image.tar.gz`, marked fixtures, generators, an existing +transparent libusb capture shim, per-stage ipptool requests, pixel expectations, +SHA-256 manifests, and isolated socket captures. Retain both software evidence +and the later physical evidence; neither replaces the operator's observations. + +## Bound and observations + +Print **11 complete labels**, plus one deliberately incomplete PWG upload, +one blocked valid follower, and one zero-label wrong-media request. There are +no helper labels, button feed/cuts, automatic resubmissions, or retries. Stop +on any failed criterion, unexpected output, USB/power loss in a continued run, +or unconfirmed status. A changed gate or retry requires a revised concrete gate. + +Operator amendments on 2026-10-13 explicitly requested “lets repeat step 1” and +then “now step 2”, adding one two-copy repeat of each batch. The actual bound +and outcome are **15 complete labels**: the original 11 plus four requested +repeats. Original/repeated captures are retained separately as `stage1-repeat1.*` +and `stage2-repeat1.*`; the second repeat closed the held run before stage 3. +No automatic retry or uncounted helper print occurred. + +Complete content totals **28.448 cm**: ten 180-row labels at 25.4 mm and one +216-row label at 30.48 mm. Initial leaders, feed/cut allowances, and the incomplete +fragment are additional consumption, whose physical amount is not yet measured. +Have at least 0.5 m available across the loaded 12/9/18 mm cassettes; this is a +preflight budget, not a claim about actual consumed tape. Record actual waste. + +Including the four requested repeats, complete printed content totals +**38.608 cm**. Actual tape consumption and leader/feed waste were not measured. +The completed scope includes all four finishing policies, continuous held-run +continuation, unknown-count mixed lengths, 76/52/120-dot rectangles, zero-output +wrong-media rejection, queued USB-only recovery, and explicit uncertainty recovery. +The operator confirmed the expected strips/cuts and the final intact S7 outline. +The retained bundle contains incoming status packets and 186 hashed physical +evidence files; later operator-observation records have their own updated manifest. +The test commands below are the completed execution record, not an instruction +to print additional labels. Physical 6/24 mm coverage remains untested. + +| Stage | Tape / fixture / request | Complete labels | Required observation | +| --- | --- | ---: | --- | +| 1 | 12 mm, `stage1.png`, chain-only, two copies | 2 | Fresh leader removed, internal separation, last end held. Extended page indications 0/1. | +| 2 | 12 mm, `stage2.png`, chain-and-final, two copies after 10 seconds idle | 2 | Prior held end released, no discarded new leader, final end cut. No invalidate/init; page indications 1/1. | +| 3 | 12 mm, `stage3.png`, none, two copies | 2 | Fresh leader removed, two joined labels held. Page indications 0/1. | +| 4 | 12 mm, `stage4.pwg`, final-only, unknown total, 180/216 rows | 2 | Prior strip released, joined labels retain individual lengths, one final cut. No invalidate/init; page indications 1/1. | +| 5 negative | 12 mm, `stage5-incomplete.pwg`, then blocked `stage5.png` | 0 | Fragment aborts without FF/EJECT; valid follower remains blocked with no output or replay. | +| 5 recovery | Button power-cycle and candidate restart, then full-width `stage5.png` | 1 | Only the new S5 label prints, intact 76-dot outline, no S51 fragment/ghost. Fresh page indication 0. | +| 5a | On a closed run, load 9 mm; submit `stage5.png` explicitly requesting 12 mm | 0 | Job aborts before initialization/setup/raster/feed/cut; only status requests allowed. Next valid job remains usable. | +| 6 | 9 mm, powered but USB disconnected, queue `stage6.png` once, default trim | 1 | Candidate reports unavailable; the same queued job resumes after reconnect and confirmed 9 mm media. Intact 52-dot outline at 1:1; final cut. | +| 7 | 18 mm, `stage7.png`, default trim | 1 | Intact 120-dot outline at 1:1; final cut, closed run. | +| | **Total** | **11** | | + +All PNGs have 180 rows at 180 dpi, complete edge outlines, a solid upper-left +marker, a lower-right open box, an arrow, and a stage identifier. Stage 4 uses +S41/S42 and 180/216-row PWG pages with declared total zero. S51 identifies the +incomplete fragment; its file decodes 60 of 180 rows. PNG widths are 76 dots for +stages 1/2/3/5, 52 for stage 6, and 120 for stage 7. Templates request exact +25.4 mm pages and canonical tape margins (12 mm: 63/71, 9 mm: 83/85, +18 mm: 53/57 in 1/100 mm). The stage-4 PWG headers supply their own lengths. + +For each complete capture, require the exact page widths/heights/indications +in the matrix, 16 decoded bytes per row, `M 02`, `ESC i a 01`, automatic status +`ESC i ! 00`, and 14-dot margin `ESC i d 0e 00`. Require fresh invalidation/init +only at closed-run starts, the policy's FF/EJECT count, and exact pixels matching +`socket-evidence/*-page*.head.raw`. `analyse.py` follows command lengths so +raster payload bytes cannot become false commands. Incoming 32-byte statuses +must report no errors and receiving-ready after complete output. Require no +FF/EJECT for the fragment, no page output for its follower or trial 5a, and no +unrequested replay. Compare recorded byte sequences to the corresponding socket +capture after removing only `ESC i S` status requests; unexplained differences +stop the gate. + +## Authorized execution setup + +The following are reviewable commands, **not commands already executed on vault**. +After approval, transfer the retained bundle and image through pfa's SSH connection +to `vault`, using a newly created directory. From openclaw, run: + +```sh +GATE=$(ssh vault mktemp -d /tmp/p710bt-acceptance.XXXXXX) +scp -r "$HOME/.local/share/ptouch-validation/p710bt-20261013/integrated-gate/." "vault:$GATE/" +printf 'On vault, export GATE=%s\n' "$GATE" +ssh -t vault +``` + +On vault, substitute the exact directory just printed, export it for the helper, +verify every retained file, and load only the candidate image: + +```sh +export GATE=/tmp/p710bt-acceptance.ACTUAL_SUFFIX +cd "$GATE" +sha256sum -c SHA256SUMS +gzip -dc candidate-image.tar.gz | sudo docker load +sudo docker image inspect ptouch-p710bt:local > "$GATE/physical-evidence/candidate-image.json" +sudo docker ps > "$GATE/physical-evidence/original-containers.txt" +sudo docker inspect ptouch > "$GATE/physical-evidence/original-service.json" +sudo docker inspect -f '{{.State.Running}}' ptouch > "$GATE/physical-evidence/original-running.txt" +sudo docker exec ptouch ptouch-app jobs -d ptouch > "$GATE/physical-evidence/original-jobs.txt" +``` + +If and only if the recorded idle original `ptouch` container owns this P710BT, +yield it and record that action before starting the candidate: + +```sh +sudo docker stop ptouch +printf 'ptouch\n' > "$GATE/physical-evidence/yielded-services.txt" +``` + +Record other competing owners and their restoration commands before yielding +them; do not stop unrelated services. + +Before starting, record `docker ps`, `docker inspect ptouch`, the previous image, +configuration, running state, and existing job/queue state. Confirm the previous +queue is idle. Identify the sole `04f9:20af` USB device and its current owner. +Yield only a competing process that owns this P710BT, recording how it was stopped; +an idle PT-2730 service may remain running if it owns different hardware. + +Confirm 12 mm tape, adequate battery charge/USB power, clean idle status, no +competing mobile printing app, and disconnected Bluetooth printing clients. Record +USB identity, media, status, candidate image, and auto-off setting if available. +Do not alter auto-off settings. Never use the initializing USB `smoke` command +as preflight. Enumeration-only `probe` and status requests are sufficient. + +The candidate uses a separate local port and a fresh process state, with no +production configuration changes: + +```sh +mkdir -p "$GATE/physical-evidence" +touch "$GATE/physical-evidence/usb-out.bin" +sudo docker run -d --name ptouch-p710bt-acceptance \ + -p 127.0.0.1:18000:8000 \ + --ulimit nofile=65536:65536 \ + --device-cgroup-rule 'c 189:* rwm' \ + -e PTOUCH_MODEL=PT-P710BT \ + -e LD_PRELOAD=/gate/tools/usb-capture.so \ + --mount "type=bind,src=$GATE,dst=/gate,readonly" \ + --mount "type=bind,src=$GATE/physical-evidence,dst=/evidence" \ + --mount type=bind,src=/dev/bus/usb,dst=/dev/bus/usb \ + ptouch-p710bt:local +``` + +The copied ipptool/capture binaries were checked against the candidate runtime +libraries without USB. Confirm that vault uses this prepared architecture; the +first status request verifies ipptool loading before any document is submitted. The IPP URI within that container is +`ipp://127.0.0.1:8000/ipp/print/ptouch`; host status access uses port 18000. +`tools/run-stage.sh` records USB output offsets, runs exactly one named request, +saves its IPP log, slices its command capture, and produces length-aware analysis. +It never automatically retries, recovers, or submits another stage. It requires +`GATE` to identify this copied bundle. `status`/`jobs` requests have no document. + +## Exact stage sequence + +Run the status-only preflight and inspect `media-col-ready`, errors, power, USB +presence, and job state before releasing a stage. Inspect physical output and +capture analysis after every stage; do not run the next stage on failure. + +```sh +"$GATE/tools/run-stage.sh" status +"$GATE/tools/run-stage.sh" jobs +"$GATE/tools/run-stage.sh" stage1 stage1.png +``` + +Leave USB and power continuously present. Record the Power LED and USB presence +before and after a **10-second idle gap**; then release stage 2 once. An unexpected +shutdown invalidates continuation and stops this gate; it does not establish a +page-indication failure. One candidate process spans stages 1 through 4. + +```sh +sleep 10 +"$GATE/tools/run-stage.sh" stage2 stage2.png +"$GATE/tools/run-stage.sh" stage3 stage3.png +"$GATE/tools/run-stage.sh" stage4 stage4.pwg +"$GATE/tools/run-stage.sh" stage5-incomplete stage5-incomplete.pwg +"$GATE/tools/run-stage.sh" stage5-blocked stage5.png +"$GATE/tools/run-stage.sh" jobs +``` + +Inspect aborted jobs and the uncertainty latch before recovery. **Hold the Power +button until the Power LED goes out, then press once to turn it on.** Unplugging +USB does not power-cycle this battery-powered printer. Do not double-press the +button, which feeds/cuts tape. Restart only the candidate after the actual +power-cycle; do not replay failed jobs. Confirm clean 12 mm status, then explicitly +submit the single new recovery label: + +```sh +sudo docker restart ptouch-p710bt-acceptance +"$GATE/tools/run-stage.sh" status +"$GATE/tools/run-stage.sh" stage5-recovery stage5.png +``` + +On this confirmed closed run, load 9 mm and confirm status. Run trial 5a with the +same S5 fixture and an explicit **12 mm** request. Its capture must contain status +requests only, its job must abort, and it must not set output-uncertain. + +```sh +"$GATE/tools/run-stage.sh" status +"$GATE/tools/run-stage.sh" stage5a-wrong-media stage5.png +``` + +For stage 6, disconnect **USB only**, leaving the already closed-run printer +powered. Confirm the candidate is unavailable. Submit the S6 request **once**; +its template stops after job acceptance so it can remain queued. Record its job +ID from the IPP log and inspect pending state. Reconnect USB and confirm 9 mm; +wait for that same job to finish, without resubmitting it: + +```sh +"$GATE/tools/run-stage.sh" status +"$GATE/tools/run-stage.sh" stage6-submit-offline stage6.png +"$GATE/tools/run-stage.sh" jobs +# After USB reconnect, substitute the accepted ID from stage6-submit-offline.ipp.log: +"$GATE/tools/run-stage.sh" wait-completed "" "$STAGE6_JOB_ID" +"$GATE/tools/run-stage.sh" status +``` + +The offline submission/wait slices are separate. Extract the entire interval +from before submission through completion (including intermediate status queries) +to avoid losing output emitted immediately after USB reconnect: + +```sh +python3 "$GATE/tools/capture-stage6.py" +python3 "$GATE/tools/analyse.py" "$GATE/physical-evidence/stage6.prn" +``` + +Load 18 mm only after stage 6 is confirmed closed, +confirm media/status, and print S7 once: + +```sh +"$GATE/tools/run-stage.sh" status +"$GATE/tools/run-stage.sh" stage7 stage7.png +"$GATE/tools/run-stage.sh" jobs +``` + +## Evidence and restoration + +Retain `physical-evidence/usb-out.bin`, `usb-io.jsonl` (incoming status packets +and results), per-stage offsets/captures/analysis, IPP responses/job IDs/states, +candidate logs, image/config records, timestamps, and operator observations of +leader waste, separation, held ends, lengths, orientation, and intact outlines. +Hash the final bundle. Status-only captures must not contain page output. +Socket captures establish command/fixture correctness, not physical acceptance. +P710BT 6/24 mm remain software/documentation coverage, independent of this gate. + +After stage 7 confirms a closed run and receiving-ready, save candidate logs, +stop/remove only `ptouch-p710bt-acceptance`, and restart each previously running +service that was explicitly yielded. Keep original image and configuration +unchanged, and verify restored queue/service state. Do not send a restoration +helper label. If a failure leaves a held or uncertain run, stop and report it; +obtain the recovery decision before returning USB to another process. Do not +silently change page flags, finishing policies, or repeat a trial. + +After confirming closed/ready, run: + +```sh +sudo docker logs ptouch-p710bt-acceptance > "$GATE/physical-evidence/candidate.log" 2>&1 +sudo docker stop ptouch-p710bt-acceptance +sudo docker rm ptouch-p710bt-acceptance +# Only if original-running.txt is true and yielded-services.txt records ptouch: +sudo docker start ptouch +sudo docker inspect ptouch > "$GATE/physical-evidence/restored-service.json" +sudo docker exec ptouch ptouch-app status -d ptouch > "$GATE/physical-evidence/restored-status.txt" +``` + +Restore any other yielded owner using its recorded command. Compare original and +restored image/configuration/running state; retain the final evidence hashes +before copying the bundle back to openclaw. No restart is required for an +original service that remained running throughout. + +Record successful integrated scope only after the operator observations and +captures pass every criterion. Keep this change local until its specifications +are synced and archived; production rollout remains a separate action. diff --git a/tests/test_device_usb.c b/tests/test_device_usb.c index e28abc6..ed29223 100644 --- a/tests/test_device_usb.c +++ b/tests/test_device_usb.c @@ -22,7 +22,41 @@ #undef papplDeviceGetData extern void *papplDeviceGetData(pappl_device_t *device); -static const char *uri = "ptouch://Brother/PT-2730"; +/* Include the production diagnostic callbacks, with queue creation captured so + * startup identity can be checked without listeners or printer commands. */ +static pappl_printer_t *capture_printer_create(pappl_system_t *, int, const char *, + const char *, const char *, const char *); +static bool no_listeners(pappl_system_t *, const char *); +#define papplPrinterCreate capture_printer_create +#define papplSystemAddListeners no_listeners +#define pt_driver_cb unused_driver_cb +#define pt_filter_png unused_filter_png +#define main pt_application_main +#include "../src/main.c" +#undef main +#undef papplPrinterCreate +#undef papplSystemAddListeners + +bool unused_driver_cb(pappl_system_t *s, const char *name, const char *device_uri, + const char *id, pappl_pr_driver_data_t *data, ipp_t **attrs, void *cb) +{ (void)s; (void)name; (void)device_uri; (void)id; (void)data; (void)attrs; (void)cb; return false; } +bool unused_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) +{ (void)job; (void)device; (void)data; return false; } +static char queue_id[100], queue_uri[100]; +static pappl_printer_t *capture_printer_create(pappl_system_t *system, int printer_id, + const char *printer_name, const char *driver_name, + const char *id, const char *device_uri) +{ + (void)system; (void)printer_id; (void)printer_name; (void)driver_name; + snprintf(queue_id, sizeof queue_id, "%s", id); + snprintf(queue_uri, sizeof queue_uri, "%s", device_uri); + return NULL; +} +static bool no_listeners(pappl_system_t *s, const char *name) +{ (void)s; (void)name; return true; } + +static const pt_dev *selected; +static char uri[80]; static struct { uint16_t vid, pid; int node; } devices[4]; static libusb_device *device_list[5]; static ssize_t num_devices; @@ -31,6 +65,8 @@ static int selected_node, claim_result, enumeration_error; static bool detached; static int listed, errors; static char last_error[256]; +static unsigned char last_output[32]; +static int output_bytes; int fake_usb_init(libusb_context **ctx) { @@ -86,6 +122,10 @@ int fake_usb_bulk_transfer(libusb_device_handle *h, unsigned char endpoint, assert(timeout == PT_USB_TIMEOUT_MS || timeout == PT_USB_STATUS_TIMEOUT_MS); transfers++; if (endpoint == PT_EP_IN) memset(data, 0, (size_t)size); + else { + assert(size <= (int)sizeof last_output); + memcpy(last_output, data, (size_t)size); output_bytes += size; + } *transferred = size; return 0; } @@ -95,8 +135,11 @@ void *tracked_device_data(pappl_device_t *device) static bool found(const char *info, const char *device_uri, const char *id, void *data) { (void)data; - assert(!strcmp(info, "PT-2730 (USB)") && !strcmp(device_uri, uri)); - assert(strstr(id, "MDL:PT-2730;")); listed++; + char expected_info[80], expected_id[100]; + snprintf(expected_info, sizeof expected_info, "%s (USB)", selected->name); + pt_usb_format_id(selected, expected_id, sizeof expected_id); + assert(!strcmp(info, expected_info) && !strcmp(device_uri, uri)); + assert(!strcmp(id, expected_id)); listed++; return false; } static void error(const char *message, void *data) @@ -105,7 +148,7 @@ static void inventory(int supported, bool unsupported) { num_devices = supported + (unsupported ? 1 : 0); for (int i = 0; i < supported; i++) { - devices[i].vid = 0x04f9; devices[i].pid = 0x2041; devices[i].node = i + 1; + devices[i].vid = 0x04f9; devices[i].pid = selected->pid; devices[i].node = i + 1; } if (unsupported) { devices[supported].vid = 0x04f9; devices[supported].pid = 0x2061; @@ -114,10 +157,13 @@ static void inventory(int supported, bool unsupported) listed = errors = 0; last_error[0] = '\0'; } -int main(void) +static void test_model(const char *name) { + setenv("PTOUCH_MODEL", name, 1); + selected = pt_usb_supported_model(name); + pt_usb_format_uri(selected, uri, sizeof uri); assert(pt_usb_supported_model(NULL) && pt_usb_supported_model("")); - assert(pt_usb_supported_model("PT-2730")); + assert(pt_usb_supported_model("PT-2730") && pt_usb_supported_model("PT-P710BT")); assert(!pt_usb_supported_model("PT-P700") && !pt_usb_supported_model("typo")); assert(pt_usb_is_uri(uri) && !pt_usb_is_uri("ptouch://Brother/")); assert(!pt_usb_is_uri("ptouch://Brother/PT-P700") && !pt_usb_is_uri(NULL)); @@ -125,8 +171,8 @@ int main(void) for (int count = 0; count <= 2; count++) { inventory(count, false); - assert(pt_usb_count(0x04f9, 0x2041) == count); - assert(pt_usb_present(0x04f9, 0x2041) == (count == 1)); + assert(pt_usb_count(selected->vid, selected->pid) == count); + assert(pt_usb_present(selected->vid, selected->pid) == (count == 1)); assert(!pt_usb_list(found, NULL, error, NULL)); assert(listed == (count == 1)); if (count == 2) assert(errors && strstr(last_error, "ambiguous")); @@ -156,7 +202,7 @@ int main(void) /* Replug changes both the node and enumeration position, never the URI. */ inventory(1, true); devices[0].pid = 0x2061; devices[0].node = 99; - devices[1].pid = 0x2041; devices[1].node = 42; + devices[1].pid = selected->pid; devices[1].node = 42; pappl_device_t *device = papplDeviceOpen(uri, "replug-test", error, NULL); assert(device && selected_node == 42); papplDeviceClose(device); inventory(0, false); @@ -171,7 +217,7 @@ int main(void) assert(contexts == 0 && !detached && closes == opens); claim_result = 0; enumeration_error = 1; - assert(pt_usb_count(0x04f9, 0x2041) == -1 && !pt_usb_present(0x04f9, 0x2041)); + assert(pt_usb_count(selected->vid, selected->pid) == -1 && !pt_usb_present(selected->vid, selected->pid)); assert(!papplDeviceOpen(uri, "enumeration-error-test", error, NULL)); assert(contexts == 0); enumeration_error = 0; @@ -189,12 +235,88 @@ int main(void) device = papplDeviceOpen(socket_uri, "socket-guard-test", error, NULL); assert(device); int previous_reads = data_reads, previous_transfers = transfers; uint8_t packet[32]; - assert(pt_usb_present(0x04f9, 0x2041)); + assert(pt_usb_present(selected->vid, selected->pid)); assert(pt_usb_read_status(device, socket_uri, packet, sizeof packet) == -1); assert(pt_usb_read_status(device, NULL, packet, sizeof packet) == -1); assert(data_reads == previous_reads && transfers == previous_transfers); papplDeviceClose(device); close(listener); assert(contexts == 0 && !detached && claims == opens); - puts("USB selection, model support, replug, and socket guard passed"); + /* Both supported models can coexist; absence never opens the other one. */ + inventory(1, false); + num_devices = 2; + devices[1].vid = 0x04f9; + devices[1].pid = selected->pid == 0x2041 ? 0x20af : 0x2041; + devices[1].node = 98; + const pt_dev *other = pt_lookup_dev(devices[1].vid, devices[1].pid); + assert(!pt_usb_list(found, NULL, error, NULL)); + assert(listed == 1 && errors == 1 && strstr(last_error, "attached but unselected")); + assert(strstr(last_error, other->name) && strstr(last_error, "PTOUCH_MODEL=")); + device = papplDeviceOpen(uri, "both-models-test", error, NULL); + assert(device && selected_node == 1); + char actual_id[100], expected_id[100]; + pt_usb_format_id(selected, expected_id, sizeof expected_id); + assert(!strcmp(pt_usb_id(device, actual_id, sizeof actual_id), expected_id)); + papplDeviceClose(device); + devices[0] = devices[1]; num_devices = 1; listed = errors = 0; + assert(!pt_usb_list(found, NULL, error, NULL) && listed == 0 && errors == 1); + int prior_opens = opens, prior_output = output_bytes; + assert(!papplDeviceOpen(uri, "other-only-test", error, NULL)); + smoke_saw_error = false; + assert(run_smoke() == 2); + assert(opens == prior_opens && output_bytes == prior_output); + + inventory(1, false); + /* probe enumerates the selected model but does not open or write it. */ + assert(probe_cb(NULL, 0, NULL, 0, NULL, NULL) == 0); + assert(opens == prior_opens && output_bytes == prior_output); + smoke_saw_error = false; + assert(run_smoke() == 0); + assert(selected_node == 1 && output_bytes == prior_output + 2); + assert(last_output[0] == 0x1b && last_output[1] == 0x40); + assert(contexts == 0 && !detached); + + unsetenv("PTOUCH_DEVICE_URI"); + pappl_system_t *sys = system_cb(0, NULL, NULL); assert(sys); + assert(!strcmp(queue_uri, uri) && !strcmp(queue_id, expected_id)); + papplSystemDelete(sys); + setenv("PTOUCH_DEVICE_URI", "socket://127.0.0.1:9100", 1); + sys = system_cb(0, NULL, NULL); assert(sys); + assert(!strcmp(queue_uri, "socket://127.0.0.1:9100") && !strcmp(queue_id, expected_id)); + papplSystemDelete(sys); + char other_uri[80]; pt_usb_format_uri(other, other_uri, sizeof other_uri); + setenv("PTOUCH_DEVICE_URI", other_uri, 1); + char *argv[] = { "ptouch-app", "smoke", NULL }; + prior_output = output_bytes; prior_opens = opens; + assert(pt_application_main(2, argv) == EXIT_FAILURE); + assert(output_bytes == prior_output && opens == prior_opens); + unsetenv("PTOUCH_DEVICE_URI"); +} + +int main(void) +{ + unsetenv("PTOUCH_MODEL"); + assert(pt_usb_supported_model(NULL)->pid == 0x2041); + assert(pt_usb_supported_model("")->pid == 0x2041); + assert(!pt_usb_model_from_uri("ptouch://Brother/")); + assert(!pt_usb_model_from_uri("ptouch://Brother/PT-P710BT/")); + assert(!pt_usb_model_from_uri("ptouch://Brother/PT-P710BT?x=1")); + assert(!pt_usb_model_from_uri("PT-P710BT")); + assert(!pt_usb_model_from_uri("")); + test_model("PT-2730"); + test_model("PT-P710BT"); + /* Invalid configuration is rejected equally for server and diagnostics, + * before enumeration, opening, or command I/O. */ + int previous_inits = init_calls, previous_opens = opens, previous_output = output_bytes; + const char *bad_models[] = { "PT-P700", "typo" }; + const char *commands[] = { "server", "probe", "smoke" }; + for (size_t i = 0; i < sizeof bad_models / sizeof bad_models[0]; i++) { + setenv("PTOUCH_MODEL", bad_models[i], 1); + for (size_t j = 0; j < sizeof commands / sizeof commands[0]; j++) { + char *args[] = { "ptouch-app", (char *)commands[j], NULL }; + assert(pt_application_main(2, args) == EXIT_FAILURE); + } + } + assert(init_calls == previous_inits && opens == previous_opens && output_bytes == previous_output); + puts("Both USB models, production diagnostics, startup identity, replug, and socket guards passed"); return 0; } diff --git a/tests/test_driver.c b/tests/test_driver.c index 6cc4421..8dad2fb 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -22,6 +22,8 @@ static unsigned char wire[65536], statuses[160]; static size_t wire_used, status_used, status_size, status_chunk = 32; static int fail_opcode = -1, fail_advanced = -1; static bool counting; +static const pt_dev *selected_model; +static char selected_id[128], selected_uri[128]; static struct { size_t rows, ff, eject, modes, cuts, final, infos, inits; } counts; static bool usb_present = true, can_open = true; static ssize_t usb_matches = 1; @@ -30,10 +32,21 @@ static pappl_printer_t *poll_during_job; static bool busy_poll_checked; ssize_t pt_usb_count(uint16_t vid, uint16_t pid) -{ (void)vid; (void)pid; usb_enumerations++; return usb_present ? usb_matches : 0; } +{ assert(vid == selected_model->vid && pid == selected_model->pid); usb_enumerations++; return usb_present ? usb_matches : 0; } bool pt_usb_present(uint16_t vid, uint16_t pid) { return pt_usb_count(vid, pid) == 1; } -bool pt_usb_is_uri(const char *uri) -{ return uri && !strcmp(uri, "ptouch://Brother/PT-2730"); } +const pt_dev *pt_usb_supported_model(const char *name) +{ + if (!name || !*name) name = "PT-2730"; + return !strcmp(name, "PT-2730") || !strcmp(name, "PT-P710BT") ? pt_lookup_name(name) : NULL; +} +const pt_dev *pt_usb_model_from_uri(const char *uri) +{ + if (!uri) return NULL; + if (!strcmp(uri, "ptouch://Brother/PT-2730")) return pt_lookup_name("PT-2730"); + if (!strcmp(uri, "ptouch://Brother/PT-P710BT")) return pt_lookup_name("PT-P710BT"); + return NULL; +} +bool pt_usb_is_uri(const char *uri) { return pt_usb_model_from_uri(uri) != NULL; } ssize_t pt_usb_read_status(pappl_device_t *d, const char *uri, void *buffer, size_t bytes) { (void)d; (void)uri; @@ -69,7 +82,7 @@ static ssize_t dev_write(pappl_device_t *d, const void *buffer, size_t n) else if (p[0] == 12) counts.ff++; else if (p[0] == 26) counts.eject++; else if (p[0] == 27 && p[1] == '@') counts.inits++; - else if (p[0] == 27 && p[1] == 'i' && p[2] == 'c') counts.infos++; + else if (p[0] == 27 && p[1] == 'i' && (p[2] == 'c' || p[2] == 'z')) counts.infos++; else if (p[0] == 27 && p[1] == 'i' && p[2] == 'M') { counts.modes++; counts.cuts += p[3] == 0x40; } else if (p[0] == 27 && p[1] == 'i' && p[2] == 'K' && p[3] == 8) { @@ -85,7 +98,7 @@ static ssize_t dev_write(pappl_device_t *d, const void *buffer, size_t n) } static pappl_preason_t dev_status(pappl_device_t *d) { (void)d; return PAPPL_PREASON_NONE; } static char *dev_id(pappl_device_t *d, char *buffer, size_t n) -{ (void)d; snprintf(buffer, n, "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;"); return buffer; } +{ (void)d; snprintf(buffer, n, "%s", selected_id); return buffer; } static void reset_wire(void) { wire_used = 0; fail_opcode = fail_advanced = -1; } static void ready_status(int error, size_t chunk) @@ -94,6 +107,15 @@ static void ready_status(int error, size_t chunk) /* Printing, then completed-but-still-printing, then receiving-ready. */ for (int i = 0; i < 3; i++) { unsigned char *s = statuses + i * 32; + /* Recorded P710BT snapshot from retained p710bt-rectangles/live.log; + * phase mutations below reproduce its actual printing/completed/ready replies. */ + static const unsigned char p710bt_snapshot[32] = { + 0x80, 0x20, 0x42, 0x30, 0x76, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x00, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + if (selected_model->pid == 0x20af) memcpy(s, p710bt_snapshot, sizeof p710bt_snapshot); s[0] = 0x80; s[1] = 0x20; s[10] = 12; s[18] = i == 1 ? 1 : 6; s[19] = i == 2 ? 0 : 1; @@ -179,21 +201,69 @@ static void end(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) assert(r_endjob(j, o, d)); assert(wire_used == before); papplJobDeletePrintOptions(o); } +/* Length-aware decoding keeps command-looking raster bytes inside their row. */ +static size_t command_size(size_t pos) +{ + assert(pos < wire_used); + unsigned char op = wire[pos]; + bool p710bt = selected_model->pid == 0x20af; + size_t n; + if (op == 0) { + assert(p710bt); n = 100; + assert(pos + n <= wire_used); + for (size_t i = 0; i < n; i++) assert(wire[pos + i] == 0); + } else if (op == 'G') { + assert(pos + 3 <= wire_used); + unsigned length = wire[pos + 1] | (unsigned)wire[pos + 2] << 8; + assert(length == (p710bt ? 17u : 16u)); + n = 3 + length; + if (p710bt) assert(wire[pos + 3] == 15); /* Complete 16-byte literal run. */ + } else if (op == 'M') { + assert(p710bt && wire[pos + 1] == 2); n = 2; + } else if (op == 12 || op == 26) n = 1; + else { + assert(op == 27); + if (wire[pos + 1] == '@') n = 2; + else { + assert(wire[pos + 1] == 'i'); + unsigned char sub = wire[pos + 2]; + if (sub == 'c' || sub == 'z') { + assert(sub == (p710bt ? 'z' : 'c')); + assert(wire[pos + 3] == 0x84 && wire[pos + 4] == 0 && wire[pos + 6] == 0); + n = p710bt ? 13 : 8; + if (p710bt) assert(wire[pos + 11] <= 1 && wire[pos + 12] == 0); + } else if (sub == 'd') { + assert(p710bt && wire[pos + 3] == 14 && wire[pos + 4] == 0); n = 5; + } else if (sub == 'S') n = 3; + else { + assert(sub == 'M' || sub == 'K' || sub == 'R' || sub == 'a' || sub == '!'); + if (sub == 'R' || sub == 'a') assert(sub == (p710bt ? 'a' : 'R') && wire[pos + 3] == 1); + if (sub == '!') assert(p710bt && wire[pos + 3] == 0); + n = 4; + } + } + } + assert(pos + n <= wire_used); + return n; +} + static void expect_wire(int inits, int rows, int ffs, int ejects) { int actual_init = 0, actual_rows = 0, actual_ff = 0, actual_eject = 0; + bool fresh = false; for (size_t pos = 0; pos < wire_used;) { - if (wire[pos] == 0x47) { pos += 3 + wire[pos + 1]; actual_rows++; } - else if (wire[pos] == 0x0c) { pos++; actual_ff++; } - else if (wire[pos] == 0x1a) { pos++; actual_eject++; } - else if (wire[pos] == 0x1b && wire[pos + 1] == 0x40) { pos += 2; actual_init++; } - else { - assert(wire[pos] == 0x1b && wire[pos + 1] == 0x69); - if (wire[pos + 2] == 'M') - assert(wire[pos + 3] == 0x40 || wire[pos + 3] == 0); - pos += wire[pos + 2] == 'c' ? 8 : 4; + size_t n = command_size(pos); + if (wire[pos] == 'G') actual_rows++; + else if (wire[pos] == 12) actual_ff++; + else if (wire[pos] == 26) actual_eject++; + else if (wire[pos] == 27 && wire[pos + 1] == '@') { actual_init++; fresh = true; } + else if (wire[pos] == 27 && wire[pos + 1] == 'i') { + if (wire[pos + 2] == 'M') assert(wire[pos + 3] == 0x40 || wire[pos + 3] == 0); + if (wire[pos + 2] == 'z') { + assert(wire[pos + 11] == (fresh ? 0 : 1)); fresh = false; + } } - assert(pos <= wire_used); + pos += n; } if (actual_init != inits || actual_rows != rows || actual_ff != ffs || actual_eject != ejects) fprintf(stderr, "wire init/rows/FF/EJECT: %d/%d/%d/%d expected %d/%d/%d/%d\n", @@ -207,15 +277,19 @@ static void expect_policy(unsigned labels, const unsigned *lengths, pt_cut_mode { unsigned infos = 0, modes = 0, ff = 0, eject = 0, advanced_final = 0, rows = 0; for (size_t pos = 0; pos < wire_used;) { - if (wire[pos] == 0x47) { rows++; pos += 3 + wire[pos + 1]; } - else if (wire[pos] == 0x0c) { ff++; pos++; } - else if (wire[pos] == 0x1a) { eject++; pos++; } - else if (wire[pos] == 0x1b && wire[pos + 1] == 0x40) pos += 2; - else { - assert(wire[pos] == 0x1b && wire[pos + 1] == 0x69); + size_t n = command_size(pos); + if (wire[pos] == 'G') rows++; + else if (wire[pos] == 12) ff++; + else if (wire[pos] == 26) eject++; + else if (wire[pos] == 27 && wire[pos + 1] == 'i') { unsigned char op = wire[pos + 2], arg = wire[pos + 3]; - if (op == 'c') { + if (op == 'c' || op == 'z') { if (infos) assert(rows == lengths[infos - 1]); + if (op == 'z') { + unsigned height = wire[pos + 7] | (unsigned)wire[pos + 8] << 8 | + (unsigned)wire[pos + 9] << 16 | (unsigned)wire[pos + 10] << 24; + assert(infos < labels && height == lengths[infos]); + } rows = 0; infos++; } else if (op == 'M') { bool cut = modes == 0 || mode == PT_CUT_EACH || mode == PT_CUT_CHAIN; @@ -227,9 +301,8 @@ static void expect_policy(unsigned labels, const unsigned *lengths, pt_cut_mode advanced_final++; } } - pos += op == 'c' ? 8 : 4; } - assert(pos <= wire_used); + pos += n; } assert(infos == labels && modes == labels && rows == lengths[labels - 1]); bool final = mode == PT_CUT_EACH || mode == PT_CUT_END; @@ -562,8 +635,12 @@ static void reject_unsupported_finishing(pappl_printer_t *p) ippDelete(response); } -int main(void) +int main(int argc, char **argv) { + assert(argc <= 2); + selected_model = pt_usb_supported_model(argc == 2 ? argv[1] : NULL); assert(selected_model); + snprintf(selected_id, sizeof selected_id, "MFG:Brother;MDL:%s;CMD:PT-CBP;", selected_model->name); + snprintf(selected_uri, sizeof selected_uri, "ptouch://Brother/%s", selected_model->name); pappl_pr_driver_t driver = {"brother_ptouch", "Test", NULL, NULL}; papplDeviceAddScheme("fixture", PAPPL_DEVTYPE_CUSTOM_LOCAL, NULL, dev_open, dev_close, NULL, dev_write, dev_status, dev_id); @@ -582,9 +659,19 @@ int main(void) unsupported_ids[i], &rejected, &attrs, NULL)); assert(!rejected.rstartjob_cb && !rejected.printfile_cb && wire_used == 0); } + /* Mismatched or malformed USB identities are rejected before commands. */ + const char *bad_uris[] = { + selected_model->pid == 0x20af ? "ptouch://Brother/PT-2730" : "ptouch://Brother/PT-P710BT", + "ptouch://Brother/PT-P710BT/extra", "ptouch://Brother/PT-unknown" + }; + for (size_t i = 0; i < sizeof bad_uris / sizeof bad_uris[0]; i++) { + pappl_pr_driver_data_t rejected = {0}; ipp_t *attrs = NULL; + assert(!pt_driver_cb(sys, "brother_ptouch", bad_uris[i], selected_id, &rejected, &attrs, NULL)); + assert(!rejected.extension && wire_used == 0); + } papplSystemSetPrinterDrivers(sys, 1, &driver, NULL, NULL, fixture_driver, NULL); pappl_printer_t *p = papplPrinterCreate(sys, 0, "test", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "socket://127.0.0.1:9"); assert(p); + selected_id, "socket://127.0.0.1:9"); assert(p); papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); assert(papplSystemAddListeners(sys, "localhost")); pthread_t server; assert(!pthread_create(&server, NULL, run_server, sys)); @@ -598,6 +685,37 @@ int main(void) pt_live_read incompatible = pt_read_live_tape(d, "socket://127.0.0.1:9"); assert(incompatible.n < 0 && !incompatible.parsed); assert(usb_enumerations == enumeration_before && usb_reads == reads_before && wire_used == 0); + if (selected_model->pid == 0x20af) { + /* Golden production-callback setup, then the first page of a held run. + * Heights come from the destination header, independent of job totals. */ + pt_run_set(p, PT_RUN_CLOSED); + for (unsigned continuation = 0; continuation < 2; continuation++) { + pappl_job_t *setup_job = job(p, "none", 1, false); + pappl_pr_options_t *setup_options = begin(setup_job, d); + setup_options->header.cupsHeight = 180 + 36 * continuation; + reset_wire(); assert(r_startpage(setup_job, setup_options, d, 0)); + unsigned char setup[] = { + 27, 'i', 'a', 1, 27, 'i', '!', 0, + 27, 'i', 'z', 0x84, 0, 12, 0, 0, 0, 0, 0, 0, 0, + 27, 'i', 'M', 0x40, 27, 'i', 'K', 0, + 27, 'i', 'd', 14, 0, 'M', 2 + }; + unsigned rows = setup_options->header.cupsHeight; + for (unsigned i = 0; i < 4; i++) setup[15 + i] = (unsigned char)(rows >> (8 * i)); + setup[19] = (unsigned char)continuation; + unsigned char expected[138] = {0}; size_t prefix = continuation ? 0 : 102; + if (!continuation) { expected[100] = 27; expected[101] = '@'; } + memcpy(expected + prefix, setup, sizeof setup); + assert(wire_used == prefix + sizeof setup && !memcmp(wire, expected, wire_used)); + unsigned char pixels[32] = {0}; size_t before = wire_used; + assert(r_writeline(setup_job, setup_options, d, 0, pixels)); + unsigned char row[20] = {'G', 17, 0, 15}; + assert(wire_used == before + sizeof row && !memcmp(wire + before, row, sizeof row)); + assert(r_endpage(setup_job, setup_options, d, 0)); end(setup_job, setup_options, d); + assert(pt_chain_get(p)); + } + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + } setenv("PTOUCH_CUT_MODE", "each", 1); /* Must not override an explicit none. */ setenv("PTOUCH_PRECUT", "0", 1); /* Retired: mandatory precut cannot be disabled. */ const char *choices[] = {NULL, "none", "{finishing-template=none}"}; @@ -677,7 +795,7 @@ int main(void) assert(wire_used == pending_size && pt_run_get(p) == PT_RUN_UNCERTAIN); /* Actual USB status parsing consumes all phases, including fragmented packets. */ pappl_printer_t *usb = papplPrinterCreate(sys, 0, "usb", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "ptouch://Brother/PT-2730"); assert(usb); + selected_id, selected_uri); assert(usb); j = job(usb, "none", 1, false); for (size_t chunk = 7; chunk <= 32; chunk += 25) { ready_status(0, chunk); assert(pt_wait_ready(j, d)); assert(status_used == status_size); @@ -832,7 +950,7 @@ int main(void) /* Cassette detection must survive an open none run without ESC @. */ pappl_printer_t *poll = papplPrinterCreate(sys, 0, "poll", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "ptouch://Brother/PT-2730"); assert(poll); + selected_id, selected_uri); assert(poll); const int widths[] = {12, 24, 0}; for (int i = 0; i < 3; i++) { ready_status(0, 32); statuses[10] = widths[i]; statuses[18] = statuses[19] = 0; @@ -862,7 +980,7 @@ int main(void) for (int i = 0; i < 10; i++) { char name[32]; snprintf(name, sizeof name, "owned-%d", i); pappl_printer_t *owned = papplPrinterCreate(sys, 0, name, "brother_ptouch", - "MFG:Brother;MDL:PT-2730;", "socket://127.0.0.1:9"); + selected_id, "socket://127.0.0.1:9"); assert(owned && pt_state(owned) && pt_state(owned) != pt_state(p)); assert(pt_run_get(owned) == PT_RUN_CLOSED && !pt_media_confirmed(owned)); } @@ -875,7 +993,7 @@ int main(void) assert(!listen(sink_listener, 1)); char socket_uri[128]; snprintf(socket_uri, sizeof socket_uri, "socket://127.0.0.1:%u", ntohs(address.sin_port)); pappl_printer_t *capture = papplPrinterCreate(sys, 0, "capture", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", socket_uri); assert(capture); + selected_id, socket_uri); assert(capture); png_socket_case(capture, NULL, 1, false, 1, 0, 1); png_socket_case(capture, "none", 3, false, 1, 3, 0); png_socket_case(capture, "malformed", 1, false, 0, 0, 0); @@ -963,7 +1081,7 @@ int main(void) raster_socket_case(capture, "trim", false, 1, 1, 1, CUPS_RASTER_WRITE_PWG, true); pt_run_set(capture, PT_RUN_CLOSED); /* Simulate documented explicit recovery. */ pappl_printer_t *counter = papplPrinterCreate(sys, 0, "counter", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;", "fixture://counter"); assert(counter); + selected_id, "fixture://counter"); assert(counter); counting_png_case(counter, "none", 65, PT_CUT_NONE); counting_png_case(counter, "trim", 999, PT_CUT_EACH); close(sink_listener); diff --git a/tests/test_png_geometry.c b/tests/test_png_geometry.c index cae6cdb..16bdc27 100644 --- a/tests/test_png_geometry.c +++ b/tests/test_png_geometry.c @@ -23,8 +23,16 @@ static unsigned fail_row; static unsigned written_rows; static bool truncate_png; static pappl_job_t *last_job; +static const char *selected_model; +static bool packbits; +const pt_dev *pt_usb_supported_model(const char *name) +{ + if (!name || !*name) name = "PT-2730"; + return !strcmp(name, "PT-2730") || !strcmp(name, "PT-P710BT") ? pt_lookup_name(name) : NULL; +} +const pt_dev *pt_usb_model_from_uri(const char *uri) { (void)uri; return NULL; } bool pt_usb_present(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return false; } -int pt_usb_count(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return 0; } +ssize_t pt_usb_count(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return 0; } bool pt_usb_is_uri(const char *uri) { (void)uri; return false; } ssize_t pt_usb_read_status(pappl_device_t *d, const char *uri, void *buffer, size_t bytes) { (void)d; (void)uri; (void)buffer; (void)bytes; assert(!"Unexpected USB read"); return -1; } @@ -40,7 +48,7 @@ static ssize_t device_write(pappl_device_t *d, const void *buffer, size_t n) } static pappl_preason_t device_status(pappl_device_t *d) { (void)d; return PAPPL_PREASON_NONE; } static char *device_id(pappl_device_t *d, char *b, size_t n) -{ (void)d; snprintf(b, n, "MFG:Brother;MDL:PT-2730;"); return b; } +{ (void)d; snprintf(b, n, "MFG:Brother;MDL:%s;", selected_model); return b; } static bool printer_status(pappl_printer_t *p) { (void)p; return true; } static bool driver(pappl_system_t *s, const char *name, const char *uri, const char *id, pappl_pr_driver_data_t *data, ipp_t **attrs, void *cb) @@ -58,22 +66,67 @@ static void operation(ipp_t *request) ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "test"); } +/* Command lengths bound raster payloads, including opcode bytes in PackBits. */ static unsigned rows(unsigned char image[][128], unsigned *finishes) { - unsigned y = 0; *finishes = 0; + unsigned y = 0, page_rows = 0, expected_rows = 0; + bool compressed = false, fresh = false; + *finishes = 0; for (size_t pos = 0; pos < used;) { + size_t n; if (wire[pos] == 'G') { - assert(wire[pos + 1] == 16 && y < 7000); + assert(used - pos >= 3 && wire[pos + 2] == 0 && y < 7000); + n = 3 + wire[pos + 1]; assert(n <= used - pos); + unsigned char head[16]; size_t decoded = 0; + if (compressed) { + for (size_t i = pos + 3; i < pos + n;) { + unsigned control = wire[i++]; + size_t count = control < 128 ? control + 1 : 257 - control; + if (control == 128) continue; + assert(decoded + count <= sizeof head); + if (control < 128) { + assert(count <= pos + n - i); + memcpy(head + decoded, wire + i, count); i += count; + } else { + assert(i < pos + n); memset(head + decoded, wire[i++], count); + } + decoded += count; + } + } else { + assert(!packbits && n == 19); memcpy(head, wire + pos + 3, 16); decoded = 16; + } + assert(decoded == sizeof head); if (image) for (unsigned x = 0; x < 128; x++) - image[y][x] = !!(wire[pos + 3 + 15 - x / 8] & (1u << (x % 8))); - y++; pos += 19; + image[y][x] = !!(head[15 - x / 8] & (1u << (x % 8))); + y++; page_rows++; } else if (wire[pos] == 12 || wire[pos] == 26) { - ++*finishes; pos++; + if (packbits) assert(page_rows == expected_rows); + page_rows = 0; ++*finishes; n = 1; + } else if (wire[pos] == 0) { + assert(packbits); n = 1; /* Fresh-run invalidation. */ + } else if (wire[pos] == 'M') { + assert(packbits && used - pos >= 2 && wire[pos + 1] == 2); + compressed = true; n = 2; } else { - assert(wire[pos] == 27); - if (wire[pos + 1] == '@') pos += 2; - else { assert(wire[pos + 1] == 'i'); pos += wire[pos + 2] == 'c' ? 8 : wire[pos + 2] == 'S' ? 3 : wire[pos + 2] == 'd' ? 5 : 4; } + assert(wire[pos] == 27 && used - pos >= 2); + if (wire[pos + 1] == '@') { n = 2; fresh = true; compressed = false; } + else { + assert(used - pos >= 3 && wire[pos + 1] == 'i'); + unsigned op = wire[pos + 2]; + n = op == 'c' ? 8 : op == 'z' ? 13 : op == 'S' ? 3 : op == 'd' ? 5 : 4; + assert(n <= used - pos); + if (op == 'z') { + assert(packbits && wire[pos + 3] == 0x84); + expected_rows = (unsigned)wire[pos + 7] | (unsigned)wire[pos + 8] << 8 | + (unsigned)wire[pos + 9] << 16 | (unsigned)wire[pos + 10] << 24; + assert(wire[pos + 11] == (fresh ? 0 : 1)); fresh = false; + } else if (op == 'c' || op == 'R') assert(!packbits); + else if (op == 'a') assert(packbits && wire[pos + 3] == 1); + else if (op == '!') assert(packbits && wire[pos + 3] == 0); + else assert(op == 'M' || op == 'K' || op == 'S' || op == 'd'); + } } + assert(n <= used - pos); pos += n; } return y; } @@ -195,17 +248,21 @@ static void run_image(pappl_printer_t *printer, int width, int height, int xppi, assert(actual_rows == expected_rows); if (!success) assert(!finishes); } -int main(void) +static void run_model(const char *model, int index) { + selected_model = model; packbits = !strcmp(model, "PT-P710BT"); + assert(!setenv("PTOUCH_MODEL", model, 1)); + used = fail_row = written_rows = 0; pappl_pr_driver_t drv = {"ptouch", "Test", NULL, NULL}; papplDeviceAddScheme("pngfixture", PAPPL_DEVTYPE_CUSTOM_LOCAL, NULL, device_open, device_close, NULL, device_write, device_status, device_id); - port = 30000 + getpid() % 15000; + port = 30000 + getpid() % 15000 + index; pappl_system_t *sys = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "png-geometry", port, NULL, NULL, "/tmp/ptouch-png-geometry.log", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(sys); papplSystemSetPrinterDrivers(sys, 1, &drv, NULL, NULL, driver, NULL); papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); - pappl_printer_t *p = papplPrinterCreate(sys, 0, "png", "ptouch", "MFG:Brother;MDL:PT-2730;", "pngfixture://capture"); assert(p); + char id[128]; snprintf(id, sizeof id, "MFG:Brother;MDL:%s;", model); + pappl_printer_t *p = papplPrinterCreate(sys, 0, "png", "ptouch", id, "pngfixture://capture"); assert(p); assert(papplSystemAddListeners(sys, "localhost")); pthread_t thread; assert(!pthread_create(&thread, NULL, server, sys)); http = httpConnect2("localhost", port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_NEVER, 1, 3000, NULL); assert(http); @@ -307,6 +364,30 @@ int main(void) run_image(p, 76, 1, 180, 180, pixels, 3, 0, 1, "none", true, 35); run_image(p, 76, 2125, 180, 180, pixels, 3, 0, 1, "none", true, 2125); run_image(p, 76, 2126, 180, 180, pixels, 3, 0, 1, "none", false, 0); + /* Decoded full-width outlines exercise the common window on all five tapes. */ + pappl_pr_driver_data_t ready; papplPrinterGetDriverData(p, &ready); + pappl_media_col_t original_media = ready.media_ready[0]; + const int tapes[] = {6, 9, 12, 18, 24}; + for (unsigned i = 0; i < sizeof tapes / sizeof tapes[0]; i++) { + pt_geometry geometry; assert(pt_tape_geometry(tapes[i], &geometry)); + pappl_media_col_t media = original_media; + media.size_width = tapes[i] * 100; + media.left_margin = geometry.left_margin_centimm; + media.right_margin = geometry.right_margin_centimm; + snprintf(media.size_name, sizeof media.size_name, "custom_%dx100mm_%dx100mm", tapes[i], tapes[i]); + assert(papplPrinterSetReadyMedia(p, 1, &media)); + int width = geometry.imageable_px; + memset(pixels, 255, width * 180); + for (int y = 0; y < 180; y++) for (int x = 0; x < width; x++) + if (!y || y == 179 || !x || x == width - 1) pixels[y * width + x] = 0; + run_image(p, width, 180, 180, 180, pixels, 3, 0, 1, "none", true, 180); + assert(rows(actual, &finishes) == 180); + int start = (128 - width) / 2; + for (int y = 0; y < 180; y++) for (int x = 0; x < 128; x++) + assert(actual[y][x] == (x >= start && x < start + width && + (!y || y == 179 || x == start || x == start + width - 1))); + } + assert(papplPrinterSetReadyMedia(p, 1, &original_media)); truncate_png = true; run_image(p, 76, 180, 180, 180, pixels, 3, 0, 1, "none", false, 0); truncate_png = false; @@ -324,5 +405,10 @@ int main(void) /* Do not add a detached PAPPL client while the system is shutting down. */ pthread_join(thread, NULL); papplSystemDelete(sys); puts("PNG IPP geometry, copies, blank margins, scaling, and complete raster markers passed"); +} +int main(void) +{ + run_model("PT-2730", 0); + run_model("PT-P710BT", 1); return 0; } diff --git a/tests/test_protocol.c b/tests/test_protocol.c index 27d7dc0..330691c 100644 --- a/tests/test_protocol.c +++ b/tests/test_protocol.c @@ -4,7 +4,12 @@ #include "tables.h" int main(void) { - uint8_t o[64]; + uint8_t o[128]; + + memset(o, 0xa5, sizeof o); + assert(pt_cmd_invalidate(o) == 100); + for (unsigned i = 0; i < 100; i++) assert(o[i] == 0); + assert(o[100] == 0xa5); assert(pt_cmd_init(o) == 2 && memcmp(o, "\x1b\x40", 2) == 0); @@ -17,6 +22,21 @@ int main(void) { assert(pt_cmd_printinfo(o, 0x18) == 8 && memcmp(o, "\x1b\x69\x63\x84\x00\x18\x00\x00", 8) == 0); + /* The tested first page and a height using all four little-endian bytes. */ + memset(o, 0xa5, sizeof o); + assert(pt_cmd_printinfo_extended(o, 12, 180, 0) == 13 && + memcmp(o, "\x1b\x69\x7a\x84\x00\x0c\x00\xb4\x00\x00\x00\x00\x00", 13) == 0); + assert(o[13] == 0xa5); + assert(pt_cmd_printinfo_extended(o, 18, 0x12345678u, 1) == 13 && + memcmp(o, "\x1b\x69\x7a\x84\x00\x12\x00\x78\x56\x34\x12\x01\x00", 13) == 0); + assert(pt_cmd_printinfo_extended(o, 9, UINT32_MAX, 0) == 13 && + memcmp(o + 7, "\xff\xff\xff\xff\x00\x00", 6) == 0); + assert(pt_cmd_printinfo_extended(o, 6, 0, 1) == 13 && + memcmp(o + 7, "\x00\x00\x00\x00\x01\x00", 6) == 0); + assert(pt_cmd_compression(o, 2) == 2 && memcmp(o, "\x4d\x02", 2) == 0); + assert(pt_cmd_compression(o, 0) == 2 && memcmp(o, "\x4d\x00", 2) == 0); + assert(pt_cmd_auto_status(o, true) == 4 && memcmp(o, "\x1b\x69\x21\x00", 4) == 0); + assert(pt_cmd_auto_status(o, false) == 4 && memcmp(o, "\x1b\x69\x21\x01", 4) == 0); assert(pt_cmd_setmode(o, 0x40) == 4 && memcmp(o, "\x1b\x69\x4d\x40", 4) == 0); assert(pt_cmd_setadvanced(o, 0x08) == 4 && memcmp(o, "\x1b\x69\x4b\x08", 4) == 0); diff --git a/tests/test_raster.c b/tests/test_raster.c index d42ccd7..c8a6506 100644 --- a/tests/test_raster.c +++ b/tests/test_raster.c @@ -1,6 +1,7 @@ #include #include #include "raster.h" +#include "protocol.h" static int bitset(const uint8_t o[16], int p) { return (o[15 - (p / 8)] >> (p % 8)) & 1; } static void setin(uint8_t *in, int i) { in[i / 8] |= (uint8_t)(1 << (7 - (i % 8))); } /* MSB-first */ @@ -30,5 +31,29 @@ int main(void) { uint8_t ones[16]; memset(ones, 0xFF, 16); assert(memcmp(out, ones, 16) == 0); + /* Decode each single-literal PackBits wire line and check every head pin. + * Widths share one packer; no compression or model path may change pins. */ + const int widths[] = {32, 52, 76, 120, 128}; + for (unsigned w = 0; w < sizeof widths / sizeof widths[0]; w++) { + int width = widths[w], offset = (128 - width) / 2; + for (int pixel = 0; pixel < width; pixel++) { + memset(in, 0, sizeof in); + setin(in, pixel); + pt_pack_line(out, in, width); + uint8_t wire[32], decoded[16]; + size_t size = pt_cmd_sendraster_packbits(wire, out, sizeof out); + assert(size == 20 && wire[0] == 0x47); + unsigned payload = wire[1] | ((unsigned)wire[2] << 8); + assert(payload == size - 3); + /* TIFF PackBits control 0..127 means copy control + 1 bytes. */ + assert(wire[3] <= 127); + unsigned literal = wire[3] + 1; + assert(literal == sizeof decoded && literal + 1 == payload); + memcpy(decoded, wire + 4, literal); + for (int pin = 0; pin < 128; pin++) + assert(bitset(decoded, pin) == (pin == offset + pixel)); + } + } + return 0; } diff --git a/tests/test_tables.c b/tests/test_tables.c index c4c5262..26ef802 100644 --- a/tests/test_tables.c +++ b/tests/test_tables.c @@ -1,11 +1,21 @@ #include #include +#include #include "tables.h" int main(void) { const pt_dev *d = pt_lookup_dev(0x04f9, 0x2041); assert(d && d->max_px == 128 && d->dpi == 180); + assert(pt_lookup_name("PT-2730") == d && d->flags == FLAG_NONE); assert(pt_lookup_dev(0x04f9, 0x9999) == NULL); + assert(pt_lookup_dev(0xffff, 0x20af) == NULL); + assert(pt_lookup_name("PT-unknown") == NULL && pt_lookup_name(NULL) == NULL); + + const pt_dev *p710bt = pt_lookup_dev(0x04f9, 0x20af); + assert(p710bt && strcmp(p710bt->name, "PT-P710BT") == 0); + assert(p710bt->max_px == 128 && p710bt->dpi == 180); + assert(p710bt->flags == (FLAG_RASTER_PACKBITS | FLAG_P700_INIT)); + assert(pt_lookup_name("PT-P710BT") == p710bt); /* per-model flags are carried */ const pt_dev *p700 = pt_lookup_dev(0x04f9, 0x2061);