286 lines
7.1 KiB
Go
286 lines
7.1 KiB
Go
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: <invalid len=%d>", 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
|
|
}
|