# USB Ticket Photo Layout Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make USB/CSN thermal-printer output match the approved reference-photo layout while leaving the serial path unchanged. **Architecture:** Add a pure USB layout builder that returns ordered write, feed, QR-code, and cut operations. Execute those operations through a small CSN device interface so layout ordering and short-write failures can be tested without printer hardware; `PrintTicket` retains database lookup and port lifecycle ownership. **Tech Stack:** Go 1.25, standard library testing, ESC/POS raw commands, existing `CsnPrinterLibs.dll` wrapper. ## Global Constraints - Only the USB/CSN path changes; the serial path remains behaviorally unchanged. - The QR payload remains `digital_ticket.ticket_no` and its width is exactly `8`. - Title and vehicle use `GS ! 0x10` for double width and `GS ! 0x00` to reset. - Empty channel and vehicle values omit their complete lines. - No new dependency is added. --- ### Task 1: Build and test the USB photo layout **Files:** - Create: `internal/modules/printer/service/usb_layout.go` - Create: `internal/modules/printer/service/usb_layout_test.go` **Interfaces:** - Consumes: parking-lot name, channel, plate, formatted entry time, slip number, and ticket number. - Produces: `buildUSBTicketLayout(data usbTicketData) []usbPrintOp` and `executeUSBLayout(device usbPrintDevice, ops []usbPrintOp) error`. - [x] **Step 1: Write failing layout tests** Use this fixed input: ```go data := usbTicketData{ lotName: "KTI AIRPORT", channelCode: "IN11", plateNo: "2BC1875", entryTime: "30-07-2026 11:14:59", slipNo: "54600", ticketNo: "0123456789abcdef0123456789abcdef", } ``` Assert this exact ordered text: ```go []string{ "Parking Ticket\n", "KTI AIRPORT\n", strings.Repeat("-", 42) + "\n", "\n", "PARK AT YOUR OWN RISK\n", "KTI AIRPORT A-General Car\n", "In-Time::30-07-2026 11:14:59\n", "InGate::IN11\n", "Slip No::54600\n", "Veh No::2BC1875\n", "SCAN AND PAY WITH NEW\n", "THE CANADIA BANK APP\n", } ``` Also assert title and vehicle are bracketed by `GS ! 0x10` and `GS ! 0x00`, only one divider exists, and the QR operation uses the ticket number with width `8`. A second case with empty channel and plate must omit `InGate::` and `Veh No::`. - [x] **Step 2: Run tests and verify RED** Run: ```powershell go test ./internal/modules/printer/service -run 'TestBuildUSBTicketLayout|TestExecuteUSBLayout' -count=1 ``` Expected: compilation fails because the USB layout types and functions do not exist. - [x] **Step 3: Implement the operation model and exact layout** Create: ```go type usbPrintOpKind uint8 const ( usbOpWrite usbPrintOpKind = iota; usbOpFeed; usbOpQRCode; usbOpCut ) type usbPrintOp struct { kind usbPrintOpKind data []byte count int qrData string qrWidth int } type usbTicketData struct { lotName, channelCode, plateNo string entryTime, slipNo, ticketNo string } ``` Generate this sequence: initialize; centered Font A double-width bold title and reset; centered Font B lot subtitle; one 42-character divider; one blank line; centered risk text; left-aligned combined lot/type line; Font A time, optional gate, and slip; optional double-width bold vehicle and reset; centered one-line feed, QR width 8, one-line feed; Font B corrected payment lines; three-line feed; full cut. Use exact size and font commands: ```go var ( usbSizeNormal = []byte{GS, '!', 0x00} usbDoubleWidth = []byte{GS, '!', 0x10} usbFontA = []byte{ESC, 'M', 0} usbFontB = []byte{ESC, 'M', 1} ) ``` - [x] **Step 4: Implement and test the device executor** Define: ```go type usbPrintDevice interface { Write([]byte) int FeedLines(int) QRCode(string, int) bool FullCut() } ``` Stop on any write count different from the requested length and return `fmt.Errorf("USB打印短写: 写入%d/%d字节", n, len(op.data))`; return `fmt.Errorf("USB二维码打印失败")` when `QRCode` returns false. The CSN adapter must call `Pos_EscQrcode`, because the vendor SDK limits `Pos_Qrcode` width to 1..6 while `Pos_EscQrcode` supports width 8. Use a fake device to prove later operations do not run after either failure. - [x] **Step 5: Run focused tests and verify GREEN** Run `go test ./internal/modules/printer/service -run 'TestBuildUSBTicketLayout|TestExecuteUSBLayout' -count=1`. Expected: PASS. ### Task 2: Integrate the layout into `PrintTicket` **Files:** - Modify: `internal/modules/printer/service/service.go:53-109` - Test: `internal/modules/printer/service/usb_layout_test.go` **Interfaces:** - Consumes: the Task 1 builder and executor. - Produces: unchanged public `PrintTicket(printerID uint, lotName, channelCode, plateNo string, ticketID uint, ticketNo string) error` behavior with the new USB layout. - [x] **Step 1: Add the CSN adapter** ```go type csnUSBDevice struct{} func (csnUSBDevice) Write(data []byte) int { return Csn_WriteData(data) } func (csnUSBDevice) FeedLines(n int) { csn_FeedLines(n) } func (csnUSBDevice) QRCode(data string, width int) bool { return csn_QRCode(data, width) } func (csnUSBDevice) FullCut() { csn_FullCut() } ``` - [x] **Step 2: Replace only the USB inline layout** Build `usbTicketData` from the existing dynamic values, execute it with `csnUSBDevice{}`, return an execution error before updating status, and retain the current `online` update after success. Do not modify the serial branch. - [x] **Step 3: Format and run package tests** Run: ```powershell gofmt -w internal/modules/printer/service/usb_layout.go internal/modules/printer/service/usb_layout_test.go internal/modules/printer/service/service.go go test ./internal/modules/printer/service -count=1 ``` Expected: PASS. - [x] **Step 4: Run repository-level verification** Run `go test ./internal/modules/printer/... -count=1`, then `go test ./... -count=1`. Expected: printer tests pass. Report any unrelated repository failure with its package and error output. - [x] **Step 5: Inspect the final diff** Run `git diff --check` and inspect the three printer-service files. Confirm only the USB path changed and all dynamic data sources remain intact.