hardlink/dispenser/dispenser.go
2025-05-22 12:08:55 +01:00

271 lines
7.1 KiB
Go

package dispenser
import (
// "encoding/hex"
"fmt"
// "log"
"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
)
// type (
// configRec struct {
// SerialPort string `yaml:"port"`
// Address string `yaml:"addr"`
// }
// )
var (
SerialPort string
Address []byte
commandFC7 = []byte{ETX, 0x46, 0x43, 0x37} // "FC7" command dispense card at read card position
commandFC0 = []byte{ETX, 0x46, 0x43, 0x30} // "FC0" command dispense card out of card mouth command
statusPos0 = map[byte]string{
0x38: "Keep",
0x34: "Command cannot execute",
0x32: "Preparing card fails",
0x31: "Preparing card",
0x30: "Normal", // Default if none of the above
}
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",
}
)
func checkStatus(statusResp []byte) (string, error) {
if len(statusResp) > 3 {
statusBytes := statusResp[7:11] // Extract the relevant bytes from the response
// For each position, get the ASCII character, hex value, and mapped meaning.
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},
}
result := ""
for _, p := range posStatus {
statusMsg, exists := p.mapper[p.value]
if !exists {
statusMsg = "Unknown status"
}
if p.value != 0x30 {
result += fmt.Sprintf("Status: %s; ", statusMsg)
}
if p.pos == 4 && p.value == 0x38 {
return result, fmt.Errorf("Card well empty")
}
}
return result, nil
} else {
if len(statusResp) == 3 && statusResp[0] == ACK && statusResp[1] == Address[0] && statusResp[2] == Address[1] {
return "active;", nil
} else if len(statusResp) > 0 && statusResp[0] == NAK {
return "", fmt.Errorf("negative response from dispenser")
} else {
return "", fmt.Errorf("unexpected response status: % X", statusResp)
}
}
}
// calculateBCC computes the Block Check Character (BCC) as the 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...) // Address bytes
packet = append(packet, space) // Space character
packet = append(packet, command...)
packet = append(packet, ETX)
bcc := calculateBCC(packet)
packet = append(packet, bcc)
return packet
}
func buildCheckRF(address []byte) []byte {
return createPacket(address, []byte{STX, 0x52, 0x46})
}
func buildCheckAP(address []byte) []byte {
return createPacket(address, []byte{STX, 0x41, 0x50})
}
func sendAndReceive(port *serial.Port, packet []byte, delay time.Duration) ([]byte, error) {
n, err := port.Write(packet)
if err != nil {
return nil, fmt.Errorf("error writing to port: %w", err)
}
// log.Printf("TX %d bytes: % X", n, packet[:n])
time.Sleep(delay) // Wait for the dispenser to process the command
buf := make([]byte, 128)
n, err = port.Read(buf)
if err != nil {
return nil, fmt.Errorf("error reading from port: %w", err)
}
resp := buf[:n]
// log.Printf("RX %d bytes: % X", n, buf[:n])
return resp, nil
}
func InitializeDispenser() (*serial.Port, error) {
const funcName = "initializeDispenser"
serialConfig := &serial.Config{
Name: SerialPort,
Baud: baudRate,
ReadTimeout: time.Second * 2,
}
port, err := serial.OpenPort(serialConfig)
if err != nil {
return nil, fmt.Errorf("error opening dispenser COM port: %w", err)
}
return port, nil
}
// if dispenser is not responding, I should repeat the command
func CheckDispenserStatus(port *serial.Port) (string, error) {
const funcName = "checkDispenserStatus"
var result string
checkCmd := buildCheckAP(Address)
enq := append([]byte{ENQ}, Address...)
// Send check command (AP)
statusResp, err := sendAndReceive(port, checkCmd, delay)
if err != nil {
return "", fmt.Errorf("error sending check command: %v", err)
}
if len(statusResp) == 0 {
return "", fmt.Errorf("no response from dispenser")
}
status, err := checkStatus(statusResp)
if err != nil {
return status, err
}
result += "; " + status
// Send ENQ+ADDR to prompt device to execute the command.
statusResp, err = sendAndReceive(port, enq, delay)
if err != nil {
log.Errorf("error sending ENQ: %v", err)
}
if len(statusResp) == 0 {
return "", fmt.Errorf("no response from dispenser")
}
status, err = checkStatus(statusResp)
if err != nil {
return status, err
}
result += status
return result, nil
}
func CardToEncoderPosition(port *serial.Port) (string, error) {
const funcName = "cartToEncoderPosition"
enq := append([]byte{ENQ}, Address...)
//Send Dispense card to encoder position (FC7) ---
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: %v", err)
}
_, err = checkStatus(statusResp)
if err != nil {
return "", err
}
//Send ENQ to prompt device ---
_, err = port.Write(enq)
if err != nil {
return "", fmt.Errorf("error sending ENQ to prompt device: %v", err)
}
//Check card position status
status, err := CheckDispenserStatus(port)
if err != nil {
return "", err
}
return status, nil
}
func CardOutOfMouth(port *serial.Port) (string, error) {
const funcName = "CardOutOfMouth"
enq := append([]byte{ENQ}, Address...)
// Send card out of card mouth (FC0) ---
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: %v", err)
}
_, err = checkStatus(statusResp)
if err != nil {
return "", err
}
//Send ENQ to prompt device ---
_, err = port.Write(enq)
if err != nil {
return "", fmt.Errorf("error sending ENQ to prompt device: %v", err)
}
//Check card position status
status, err := CheckDispenserStatus(port)
if err != nil {
return "", err
}
return status, nil
}