generated from VLADIMIR/template
327 lines
8.5 KiB
Go
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
|
|
}
|
|
}
|