feat(04-03): PrinterDriver interface, MockDriver, PrtQutieDriver stub
- PrinterDriver interface: Connect/Print(bitmap,w,h)/Disconnect - ImageToRawBitmap(): 1-bit packed row-major converter from image.Image - MockDriver: saves PNG to SaveDir (/tmp default) for visual inspection - PrtQutieDriver: stub returns ErrNoDevice — safe before hardware arrives - ErrNoDevice, ErrNotConnected, ErrEmptyBitmap sentinel errors
This commit is contained in:
parent
b223b54a87
commit
1156eff896
3 changed files with 185 additions and 0 deletions
49
internal/printer/driver.go
Normal file
49
internal/printer/driver.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrinterDriver is the interface all label printer implementations must satisfy.
|
||||||
|
// The PRT Qutie driver, mock driver, and any future printer use this interface.
|
||||||
|
type PrinterDriver interface {
|
||||||
|
// Connect opens the connection to the printer. Must be called before Print.
|
||||||
|
Connect() error
|
||||||
|
// Print sends a rendered label bitmap to the printer.
|
||||||
|
// bitmap is raw 1-bit packed row-major data from ImageToRawBitmap.
|
||||||
|
Print(bitmap []byte, width, height int) error
|
||||||
|
// Disconnect closes the connection.
|
||||||
|
Disconnect() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoDevice is returned when no printer matching the VID/PID is found.
|
||||||
|
ErrNoDevice = errors.New("printer: no device found matching VID/PID")
|
||||||
|
// ErrNotConnected is returned when Print is called before Connect.
|
||||||
|
ErrNotConnected = errors.New("printer: not connected")
|
||||||
|
// ErrEmptyBitmap is returned when an empty or nil bitmap is passed to Print.
|
||||||
|
ErrEmptyBitmap = errors.New("printer: bitmap is empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageToRawBitmap converts an image.Image to 1-bit packed row-major bitmap.
|
||||||
|
// White pixels → 0, dark pixels → 1. Returns bytes, width, and height.
|
||||||
|
func ImageToRawBitmap(img image.Image) ([]byte, int, int) {
|
||||||
|
b := img.Bounds()
|
||||||
|
w, h := b.Max.X-b.Min.X, b.Max.Y-b.Min.Y
|
||||||
|
rowBytes := int(math.Ceil(float64(w) / 8.0))
|
||||||
|
out := make([]byte, rowBytes*h)
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
r, g, bv, _ := img.At(b.Min.X+x, b.Min.Y+y).RGBA()
|
||||||
|
// Luminance: pixel is dark if average < 50% of max (0xffff).
|
||||||
|
lum := (r + g + bv) / 3
|
||||||
|
if lum < 0x7fff {
|
||||||
|
byteIdx := y*rowBytes + x/8
|
||||||
|
out[byteIdx] |= 1 << (7 - uint(x%8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, w, h
|
||||||
|
}
|
||||||
72
internal/printer/mock_driver.go
Normal file
72
internal/printer/mock_driver.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockDriver implements PrinterDriver for testing and development.
|
||||||
|
// Print() saves a PNG to SaveDir (default /tmp) and logs to stdout.
|
||||||
|
// No hardware required — use this as the default driver until PRT Qutie arrives.
|
||||||
|
type MockDriver struct {
|
||||||
|
connected bool
|
||||||
|
// SaveDir is the directory where label PNGs are written.
|
||||||
|
// Defaults to /tmp. Override in tests to use t.TempDir().
|
||||||
|
SaveDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockDriver creates a MockDriver that saves PNGs to /tmp.
|
||||||
|
func NewMockDriver() *MockDriver {
|
||||||
|
return &MockDriver{SaveDir: "/tmp"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect marks the driver as connected.
|
||||||
|
func (m *MockDriver) Connect() error {
|
||||||
|
m.connected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect marks the driver as disconnected.
|
||||||
|
func (m *MockDriver) Disconnect() error {
|
||||||
|
m.connected = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print reconstructs the label image from the 1-bit bitmap and saves it as a PNG.
|
||||||
|
// Returns ErrEmptyBitmap if bitmap is nil or empty.
|
||||||
|
func (m *MockDriver) Print(bitmap []byte, width, height int) error {
|
||||||
|
if len(bitmap) == 0 {
|
||||||
|
return ErrEmptyBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct image from 1-bit packed bitmap for visual inspection.
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
rowBytes := (width + 7) / 8
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
byteIdx := y*rowBytes + x/8
|
||||||
|
bit := (bitmap[byteIdx] >> (7 - uint(x%8))) & 1
|
||||||
|
if bit == 1 {
|
||||||
|
img.SetGray(x, y, color.Gray{Y: 0})
|
||||||
|
} else {
|
||||||
|
img.SetGray(x, y, color.Gray{Y: 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s/hwlab-label-%d.png", m.SaveDir, time.Now().UnixMilli())
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("printer: mock create file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := png.Encode(f, img); err != nil {
|
||||||
|
return fmt.Errorf("printer: mock encode PNG: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[MockDriver] label saved → %s (%dx%d)\n", path, width, height)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
internal/printer/prt_qutie.go
Normal file
64
internal/printer/prt_qutie.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.bug.st/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrtQutieDriver implements PrinterDriver for the PRT Qutie thermal printer.
|
||||||
|
//
|
||||||
|
// Protocol: UNKNOWN — reverse engineering required after hardware arrival 2026-04-13.
|
||||||
|
//
|
||||||
|
// TODO(hardware): Replace the stub Print() with real ESC/POS or PRT Qutie commands
|
||||||
|
// after capturing USB traffic with Wireshark on hardware arrival day.
|
||||||
|
// Reference: https://atctwo.net/posts/2024/07/16/thermal-printer.html
|
||||||
|
type PrtQutieDriver struct {
|
||||||
|
portPath string // resolved at Connect() time
|
||||||
|
port serial.Port
|
||||||
|
baudRate int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrtQutieDriver creates a stub driver. portPath is resolved at Connect() via
|
||||||
|
// USB Manager VID/PID enumeration. baudRate 0 defaults to 9600.
|
||||||
|
func NewPrtQutieDriver(baudRate int) *PrtQutieDriver {
|
||||||
|
if baudRate == 0 {
|
||||||
|
baudRate = 9600
|
||||||
|
}
|
||||||
|
return &PrtQutieDriver{baudRate: baudRate}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect attempts to find and open the PRT Qutie serial port.
|
||||||
|
// Currently returns ErrNoDevice — real port resolution requires hardware.
|
||||||
|
//
|
||||||
|
// TODO(hardware): Resolve portPath via serial.GetPortsList() + system_profiler VID/PID
|
||||||
|
// cross-reference, then open the port with serial.Open().
|
||||||
|
func (d *PrtQutieDriver) Connect() error {
|
||||||
|
// Stub: return ErrNoDevice so callers can detect missing hardware gracefully.
|
||||||
|
// Real implementation: enumerate ports, match VID:PID 0525:a4a7, open port.
|
||||||
|
return ErrNoDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print sends label bitmap to the printer.
|
||||||
|
// Returns ErrNotConnected if Connect() has not been successfully called.
|
||||||
|
//
|
||||||
|
// TODO(hardware): Implement PRT Qutie print protocol — likely a binary
|
||||||
|
// command sequence (ESC/POS or proprietary). Capture with Wireshark.
|
||||||
|
func (d *PrtQutieDriver) Print(bitmap []byte, width, height int) error {
|
||||||
|
if d.port == nil {
|
||||||
|
return ErrNotConnected
|
||||||
|
}
|
||||||
|
// TODO(hardware): implement PRT Qutie print protocol
|
||||||
|
_ = bitmap
|
||||||
|
_ = width
|
||||||
|
_ = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect closes the serial port if open.
|
||||||
|
func (d *PrtQutieDriver) Disconnect() error {
|
||||||
|
if d.port != nil {
|
||||||
|
err := d.port.Close()
|
||||||
|
d.port = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue