Files
evening_detective_server/internal/services/team_service/mxw01.go
T
2026-10-13 13:50:13 +07:00

327 lines
8.5 KiB
Go

package team_service
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"sync"
"time"
)
type attConnection interface {
Write(context.Context, []byte) error
Read(context.Context) ([]byte, error)
Close() error
}
type mxw01Printer struct {
address string
mutex sync.Mutex
}
// NewMXW01Printer creates the Linux printer used by the venue hardware process.
func NewMXW01Printer(address string) IReceiptPrinter {
return &mxw01Printer{address: address}
}
func (p *mxw01Printer) Print(ctx context.Context, data []byte) error {
if err := validateReceiptRaster(data); err != nil {
return err
}
if p.address == "" {
return ErrPrinterNotConfigured
}
if !p.mutex.TryLock() {
return ErrPrinterBusy
}
defer p.mutex.Unlock()
connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
conn, err := dialMXW01(connectCtx, p.address)
cancel()
if err != nil {
if errors.Is(err, ErrPrinterNotConfigured) {
return ErrPrinterNotConfigured
}
if ctx.Err() != nil {
return ctx.Err()
}
return fmt.Errorf("connect MXW01: %w: %w", ErrPrinterUnavailable, err)
}
defer conn.Close()
return printMXW01(ctx, conn, data)
}
func printMXW01(ctx context.Context, conn attConnection, data []byte) error {
if err := validateReceiptRaster(data); err != nil {
return err
}
response, err := attRequest(ctx, conn, []byte{0x02, 185, 0}, 0x03)
if err != nil {
return err
}
if len(response) != 3 || binary.LittleEndian.Uint16(response[1:]) < 51 {
return ErrPrinterProtocol
}
handles, err := discoverMXW01(ctx, conn)
if err != nil {
return err
}
if _, err := attRequest(ctx, conn, []byte{0x12, byte(handles.ccc), byte(handles.ccc >> 8), 1, 0}, 0x13); err != nil {
return err
}
send := func(command byte, payload []byte) error {
packet := append([]byte{0x52, byte(handles.control), byte(handles.control >> 8)}, mxw01Command(command, payload)...)
return conn.Write(ctx, packet)
}
if err := send(0xa2, []byte{0x5d}); err != nil {
return err
}
if err := receiptPause(ctx, 100*time.Millisecond); err != nil {
return err
}
if err := send(0xa1, []byte{0}); err != nil {
return err
}
status, err := mxw01Response(ctx, conn, handles.notify, 0xa1, 7*time.Second)
if err != nil {
return err
}
if err := checkMXW01Status(status); err != nil {
return err
}
rows := len(data) / receiptRowBytes
if err := send(0xa9, []byte{byte(rows), byte(rows >> 8), 0x30, 0}); err != nil {
return err
}
accepted, err := mxw01Response(ctx, conn, handles.notify, 0xa9, 7*time.Second)
if err != nil {
return err
}
if len(accepted) < 1 || accepted[0] != 0 {
return ErrPrinterNotReady
}
for offset := 0; offset < len(data); offset += receiptRowBytes {
packet := append([]byte{0x52, byte(handles.data), byte(handles.data >> 8)}, data[offset:offset+receiptRowBytes]...)
if err := conn.Write(ctx, packet); err != nil {
return err
}
if err := receiptPause(ctx, 15*time.Millisecond); err != nil {
return err
}
}
if err := send(0xad, []byte{0}); err != nil {
return err
}
completed, err := mxw01Response(ctx, conn, handles.notify, 0xaa, 15*time.Second+time.Duration(rows)*time.Second/15)
if err != nil {
return err
}
crc := mxw01CRC(data)
if len(completed) != 3 || completed[0] != 0 || completed[1] != crc || completed[2] != crc {
return ErrPrinterIncomplete
}
return nil
}
type mxw01Handles struct{ control, notify, data, ccc uint16 }
func discoverMXW01(ctx context.Context, conn attConnection) (mxw01Handles, error) {
var handles mxw01Handles
var notifyEnd uint16 = 0xffff
for start := uint16(1); start != 0; {
request := []byte{0x08, byte(start), byte(start >> 8), 0xff, 0xff, 0x03, 0x28}
response, err := attRequest(ctx, conn, request, 0x09)
if err != nil {
return handles, err
}
if len(response) == 5 && response[0] == 0x01 && response[1] == 0x08 && response[4] == 0x0a {
break
}
if len(response) < 2 || (response[1] != 7 && response[1] != 21) || (len(response)-2)%int(response[1]) != 0 || len(response) == 2 {
return handles, ErrPrinterProtocol
}
length := int(response[1])
for offset := 2; offset < len(response); offset += length {
entry := response[offset : offset+length]
declaration := binary.LittleEndian.Uint16(entry)
if declaration < start {
return handles, ErrPrinterProtocol
}
if handles.notify != 0 && declaration > handles.notify && notifyEnd == 0xffff {
notifyEnd = declaration - 1
}
handle := binary.LittleEndian.Uint16(entry[3:])
if handle <= declaration {
return handles, ErrPrinterProtocol
}
switch bluetoothUUID16(entry[5:]) {
case 0xae01:
if entry[2]&0x04 == 0 {
return handles, ErrPrinterProtocol
}
handles.control = handle
case 0xae02:
if entry[2]&0x10 == 0 {
return handles, ErrPrinterProtocol
}
handles.notify = handle
case 0xae03:
if entry[2]&0x04 == 0 {
return handles, ErrPrinterProtocol
}
handles.data = handle
}
start = declaration + 1
}
}
if handles.control == 0 || handles.notify == 0 || handles.data == 0 || handles.notify == 0xffff {
return handles, ErrPrinterProtocol
}
start := handles.notify + 1
response, err := attRequest(ctx, conn, []byte{0x04, byte(start), byte(start >> 8), byte(notifyEnd), byte(notifyEnd >> 8)}, 0x05)
if err != nil {
return handles, err
}
if len(response) < 2 || response[0] != 0x05 || response[1] != 1 || (len(response)-2)%4 != 0 {
return handles, ErrPrinterProtocol
}
for offset := 2; offset < len(response); offset += 4 {
if binary.LittleEndian.Uint16(response[offset+2:]) == 0x2902 {
handles.ccc = binary.LittleEndian.Uint16(response[offset:])
if handles.ccc < start || handles.ccc > notifyEnd {
return handles, ErrPrinterProtocol
}
return handles, nil
}
}
return handles, ErrPrinterProtocol
}
func bluetoothUUID16(data []byte) uint16 {
if len(data) == 2 {
return binary.LittleEndian.Uint16(data)
}
base := []byte{0xfb, 0x34, 0x9b, 0x5f, 0x80, 0, 0, 0x80, 0, 0x10, 0, 0}
if len(data) == 16 && bytes.Equal(data[:12], base) && data[14] == 0 && data[15] == 0 {
return binary.LittleEndian.Uint16(data[12:])
}
return 0
}
func attRequest(ctx context.Context, conn attConnection, request []byte, expected byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
defer cancel()
if err := conn.Write(ctx, request); err != nil {
return nil, err
}
for {
response, err := conn.Read(ctx)
if err != nil {
return nil, err
}
if len(response) == 0 {
return nil, ErrPrinterProtocol
}
if response[0] == 0x01 {
if expected == 0x09 && len(response) == 5 && response[1] == request[0] && response[4] == 0x0a {
return response, nil
}
return nil, ErrPrinterProtocol
}
if response[0] == expected {
return response, nil
}
if response[0] != 0x1b {
return nil, ErrPrinterProtocol
}
}
}
func mxw01Response(ctx context.Context, conn attConnection, handle uint16, command byte, timeout time.Duration) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
message, err := conn.Read(ctx)
if err != nil {
return nil, err
}
if len(message) < 3 || message[0] != 0x1b {
return nil, ErrPrinterProtocol
}
if binary.LittleEndian.Uint16(message[1:]) != handle {
continue
}
data := message[3:]
if len(data) < 6 || data[0] != 0x22 || data[1] != 0x21 {
return nil, ErrPrinterProtocol
}
length := int(binary.LittleEndian.Uint16(data[4:]))
if len(data) < length+6 {
return nil, ErrPrinterProtocol
}
// Real firmware adds one non-CRC tail byte; only the declared payload is parsed.
if data[2] == command {
return data[6 : 6+length], nil
}
}
}
func checkMXW01Status(data []byte) error {
if len(data) < 10 {
return ErrPrinterProtocol
}
if data[9] == 7 {
return ErrPrinterNoPaper
}
if data[6] != 0 {
switch data[7] {
case 1, 9:
return ErrPrinterNoPaper
case 4:
return ErrPrinterOverheated
case 8:
return ErrPrinterLowBattery
default:
return ErrPrinterNotReady
}
}
if data[0] != 0 {
return ErrPrinterBusy
}
return nil
}
func mxw01CRC(data []byte) byte {
var crc byte
for _, b := range data {
crc ^= b
for range 8 {
if crc&0x80 != 0 {
crc = (crc << 1) ^ 0x07
} else {
crc <<= 1
}
}
}
return crc
}
func mxw01Command(command byte, payload []byte) []byte {
data := []byte{0x22, 0x21, command, 0, byte(len(payload)), byte(len(payload) >> 8)}
data = append(data, payload...)
return append(data, mxw01CRC(payload), 0xff)
}
func receiptPause(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}