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 } }