package dispenser import ( "fmt" "strings" "time" log "github.com/sirupsen/logrus" "github.com/tarm/serial" ) // Control characters. const ( STX = 0x02 // Start of Text ETX = 0x03 // End of Text ACK = 0x06 // Positive response NAK = 0x15 // Negative response ENQ = 0x05 // Enquiry from host space = 0x00 // Space character baudRate = 9600 // Baud rate for serial communication delay = 500 * time.Millisecond // Delay for processing commands // cache freshness for "continuous status" reads (tune as you wish) defaultStatusTTL = 1500 * time.Millisecond ) var ( SerialPort string Address []byte commandFC7 = []byte{ETX, 0x46, 0x43, 0x37} // "FC7" commandFC0 = []byte{ETX, 0x46, 0x43, 0x30} // "FC0" statusPos0 = map[byte]string{ 0x38: "Keep", 0x34: "Command cannot execute", 0x32: "Preparing card fails", 0x31: "Preparing card", 0x30: "Normal", } statusPos1 = map[byte]string{ 0x38: "Dispensing card", 0x34: "Capturing card", 0x32: "Dispense card error", 0x31: "Capture card error", 0x30: "Normal", } statusPos2 = map[byte]string{ 0x38: "No captured card", 0x34: "Card overlapped", 0x32: "Card jammed", 0x31: "Card pre-empty", 0x30: "Normal", } statusPos3 = map[byte]string{ 0x38: "Card empty", 0x34: "Card ready position", 0x33: "Card at encoder position", 0x32: "Card at hold card position", 0x31: "Card out of card mouth position", 0x30: "Normal", } ) // -------------------- // Status helpers // -------------------- func logStatus(statusBytes []byte) { if len(statusBytes) < 4 { log.Infof("Dispenser status: ", len(statusBytes)) return } posStatus := []struct { pos int value byte mapper map[byte]string }{ {pos: 1, value: statusBytes[0], mapper: statusPos0}, {pos: 2, value: statusBytes[1], mapper: statusPos1}, {pos: 3, value: statusBytes[2], mapper: statusPos2}, {pos: 4, value: statusBytes[3], mapper: statusPos3}, } var result strings.Builder for _, p := range posStatus { statusMsg, exists := p.mapper[p.value] if !exists { statusMsg = fmt.Sprintf("Unknown status 0x%X at position %d", p.value, p.pos) } if p.value != 0x30 { result.WriteString(statusMsg + "; ") } } log.Infof("Dispenser status: %s", result.String()) } func isAtEncoderPosition(statusBytes []byte) bool { return len(statusBytes) >= 4 && statusBytes[3] == 0x33 } func stockTake(statusBytes []byte) string { if len(statusBytes) < 4 { return "" } status := "" if statusBytes[2] != 0x30 { status = statusPos2[statusBytes[2]] } if statusBytes[3] == 0x38 { status = statusPos3[statusBytes[3]] } return status } func isCardWellEmpty(statusBytes []byte) bool { return len(statusBytes) >= 4 && statusBytes[3] == 0x38 } func checkACK(statusResp []byte) error { if len(statusResp) == 3 && statusResp[0] == ACK && len(Address) >= 2 && statusResp[1] == Address[0] && statusResp[2] == Address[1] { return nil } if len(statusResp) > 0 && statusResp[0] == NAK { return fmt.Errorf("negative response from dispenser") } return fmt.Errorf("unexpected response status: % X", statusResp) } // calculateBCC computes BCC as XOR of all bytes from STX to ETX. func calculateBCC(data []byte) byte { var bcc byte for _, b := range data { bcc ^= b } return bcc } func createPacket(address []byte, command []byte) []byte { packet := []byte{STX} packet = append(packet, address...) packet = append(packet, space) packet = append(packet, command...) packet = append(packet, ETX) bcc := calculateBCC(packet) packet = append(packet, bcc) return packet } func buildCheckAP(address []byte) []byte { return createPacket(address, []byte{STX, 0x41, 0x50}) } func sendAndReceive(port *serial.Port, packet []byte, delay time.Duration) ([]byte, error) { _, err := port.Write(packet) if err != nil { return nil, fmt.Errorf("error writing to port: %w", err) } time.Sleep(delay) buf := make([]byte, 128) n, err := port.Read(buf) if err != nil { return nil, fmt.Errorf("error reading from port: %w", err) } return buf[:n], nil } // -------------------- // Serial init (3 attempts) // -------------------- func InitializeDispenser() (*serial.Port, error) { const ( funcName = "InitializeDispenser" maxRetries = 3 retryDelay = 2 * time.Second ) if SerialPort == "" { return nil, fmt.Errorf("%s: SerialPort is empty", funcName) } if len(Address) < 2 { return nil, fmt.Errorf("%s: Address must be at least 2 bytes", funcName) } serialConfig := &serial.Config{ Name: SerialPort, Baud: baudRate, ReadTimeout: 2 * time.Second, } var lastErr error for attempt := 1; attempt <= maxRetries; attempt++ { port, err := serial.OpenPort(serialConfig) if err == nil { log.Infof("%s: dispenser opened on %s (attempt %d/%d)", funcName, SerialPort, attempt, maxRetries) return port, nil } lastErr = err log.Warnf("%s: failed to open dispenser on %s (attempt %d/%d): %v", funcName, SerialPort, attempt, maxRetries, err) if attempt < maxRetries { time.Sleep(retryDelay) } } return nil, fmt.Errorf("%s: failed to open dispenser on %s after %d attempts: %w", funcName, SerialPort, maxRetries, lastErr) } // -------------------- // Internal (port-owner only) operations // -------------------- // checkDispenserStatus talks to the device and returns the 4 status bytes [pos0..pos3]. func checkDispenserStatus(port *serial.Port) ([]byte, error) { checkCmd := buildCheckAP(Address) enq := append([]byte{ENQ}, Address...) statusResp, err := sendAndReceive(port, checkCmd, delay) if err != nil { return nil, fmt.Errorf("error sending check command: %w", err) } if len(statusResp) == 0 { return nil, fmt.Errorf("no response from dispenser") } if err := checkACK(statusResp); err != nil { return nil, err } statusResp, err = sendAndReceive(port, enq, delay) if err != nil { return nil, fmt.Errorf("error sending ENQ: %w", err) } if len(statusResp) < 13 { return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp) } return statusResp[7:11], nil } func cardToEncoderPosition(port *serial.Port) error { enq := append([]byte{ENQ}, Address...) dispenseCmd := createPacket(Address, commandFC7) log.Println("Send card to encoder position") statusResp, err := sendAndReceive(port, dispenseCmd, delay) if err != nil { return fmt.Errorf("error sending card to encoder position: %w", err) } if err := checkACK(statusResp); err != nil { return err } _, err = port.Write(enq) if err != nil { return fmt.Errorf("error sending ENQ to prompt device: %w", err) } return nil } func cardOutOfMouth(port *serial.Port) error { enq := append([]byte{ENQ}, Address...) dispenseCmd := createPacket(Address, commandFC0) log.Println("Send card to out mouth position") statusResp, err := sendAndReceive(port, dispenseCmd, delay) if err != nil { return fmt.Errorf("error sending out of mouth command: %w", err) } if err := checkACK(statusResp); err != nil { return err } _, err = port.Write(enq) if err != nil { return fmt.Errorf("error sending ENQ to prompt device: %w", err) } return nil }