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.
digital_ticket.ticket_no and its width is exactly 8.GS ! 0x10 for double width and GS ! 0x00 to reset.Files:
internal/modules/printer/service/usb_layout.gointernal/modules/printer/service/usb_layout_test.goInterfaces:
Produces: buildUSBTicketLayout(data usbTicketData) []usbPrintOp and executeUSBLayout(device usbPrintDevice, ops []usbPrintOp) error.
[x] Step 1: Write failing layout tests
Use this fixed input:
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:
[]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::.
Run:
go test ./internal/modules/printer/service -run 'TestBuildUSBTicketLayout|TestExecuteUSBLayout' -count=1
Expected: compilation fails because the USB layout types and functions do not exist.
Create:
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:
var (
usbSizeNormal = []byte{GS, '!', 0x00}
usbDoubleWidth = []byte{GS, '!', 0x10}
usbFontA = []byte{ESC, 'M', 0}
usbFontB = []byte{ESC, 'M', 1}
)
Define:
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.
Run go test ./internal/modules/printer/service -run 'TestBuildUSBTicketLayout|TestExecuteUSBLayout' -count=1.
Expected: PASS.
PrintTicketFiles:
internal/modules/printer/service/service.go:53-109internal/modules/printer/service/usb_layout_test.goInterfaces:
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
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() }
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.
Run:
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.
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.
Run git diff --check and inspect the three printer-service files. Confirm only the USB path changed and all dynamic data sources remain intact.