hardlink/internal/dispenser/dispenser.go

417 lines
12 KiB
Go

package dispenser
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"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
CardWellEmptyMessage = "Card well is empty"
)
const (
positionPreDispense = 0x01
positionEncoder = 0x02
positionMouth = 0x04
positionEmpty = 0x08
)
var (
ErrCardWellEmpty = errors.New(CardWellEmptyMessage)
SerialPort string
Address []byte
commandFC7 = []byte{ETX, 0x46, 0x43, 0x37} // "FC7"
commandFC0 = []byte{ETX, 0x46, 0x43, 0x30} // "FC0"
commandRS = []byte{0x02, 0x52, 0x53} // Length 2, "RS"
statusPos0 = map[byte]string{
0x38: "Keep",
0x34: "Command cannot execute",
0x32: "Preparing card fails",
0x31: "Preparing card",
0x30: "Normal",
0x36: "Command cannot execute; Preparing card fails",
}
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",
}
)
// --------------------
// Status helpers
// --------------------
// decodePositionStatus decodes the manual's 0x30 + combined sensor/empty flags.
// Sensor 2 (0x02) is the read position; sensor 1 and sensor 2 together give 0x33.
func decodePositionStatus(value byte) (flags byte, valid bool) {
if value&0xF0 != 0x30 {
return 0, false
}
return value & 0x0F, true
}
func statusDescription(statusBytes []byte) string {
if len(statusBytes) < 4 {
return fmt.Sprintf("<invalid len=%d>", len(statusBytes))
}
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},
}
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 + "; ")
}
}
flags, valid := decodePositionStatus(statusBytes[3])
if !valid {
fmt.Fprintf(&result, "Unknown status 0x%X at position 4; ", statusBytes[3])
} else {
for _, position := range []struct {
mask byte
text string
}{
{positionPreDispense, "Card at pre-dispense position"},
{positionEncoder, "Card at encoder position"},
{positionMouth, "Card at mouth position"},
{positionEmpty, "Card empty"},
} {
if flags&position.mask != 0 {
result.WriteString(position.text + "; ")
}
}
}
return result.String()
}
func logStatus(statusBytes []byte) {
log.Infof("Dispenser status: %s", statusDescription(statusBytes))
}
func isAtEncoderPosition(statusBytes []byte) bool {
if len(statusBytes) < 4 {
return false
}
flags, valid := decodePositionStatus(statusBytes[3])
return valid && flags&positionEncoder != 0
}
func validateDispenserStatusData(statusBytes []byte) error {
if len(statusBytes) != 4 {
return fmt.Errorf("malformed dispenser status: got %d bytes, want 4", len(statusBytes))
}
statusMaps := []map[byte]string{statusPos0, statusPos1, statusPos2}
for position, mapper := range statusMaps {
if _, ok := mapper[statusBytes[position]]; !ok {
return fmt.Errorf("unknown dispenser status 0x%X at position %d", statusBytes[position], position+1)
}
}
if _, valid := decodePositionStatus(statusBytes[3]); !valid {
return fmt.Errorf("unknown dispenser status 0x%X at position 4", statusBytes[3])
}
return nil
}
func dispenserStatusError(statusBytes []byte) error {
switch statusBytes[0] {
case 0x34, 0x32, 0x36:
return fmt.Errorf("dispenser error: %s", statusPos0[statusBytes[0]])
}
switch statusBytes[1] {
case 0x32, 0x31:
return fmt.Errorf("dispenser error: %s", statusPos1[statusBytes[1]])
}
switch statusBytes[2] {
case 0x34, 0x32:
return fmt.Errorf("dispenser error: %s", statusPos2[statusBytes[2]])
}
return nil
}
func isPreparationMoving(statusBytes []byte) bool {
return len(statusBytes) == 4 &&
(statusBytes[0] == 0x31 || statusBytes[1] == 0x38)
}
func hasPreparationDiagnostics(statusBytes []byte) bool {
return len(statusBytes) == 4 &&
(statusBytes[0] != 0x30 || statusBytes[1] != 0x30 || statusBytes[2] != 0x30)
}
func stockTake(statusBytes []byte) string {
if len(statusBytes) < 4 {
return ""
}
status := ""
if statusBytes[0] == 0x32 || statusBytes[0] == 0x36 {
status = statusPos0[statusBytes[0]]
}
if statusBytes[2] != 0x30 {
status = statusPos2[statusBytes[2]]
}
if isCardWellEmpty(statusBytes) {
status = "Card empty"
}
return status
}
func isCardWellEmpty(statusBytes []byte) bool {
if len(statusBytes) < 4 {
return false
}
flags, valid := decodePositionStatus(statusBytes[3])
return valid && flags&positionEmpty != 0
}
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}) }
// serialTransport is used only by the serial-port owner.
type serialTransport interface {
io.Reader
io.Writer
}
func writePacket(ctx context.Context, port serialTransport, packet []byte) error {
if err := ctx.Err(); err != nil {
return err
}
n, err := port.Write(packet)
if err != nil {
return fmt.Errorf("write dispenser packet: %w", err)
}
if n != len(packet) {
return fmt.Errorf("write dispenser packet (%d/%d bytes): %w", n, len(packet), io.ErrShortWrite)
}
return ctx.Err()
}
func readExact(ctx context.Context, port serialTransport, data []byte) error {
for len(data) > 0 {
if err := ctx.Err(); err != nil {
return err
}
n, err := port.Read(data)
data = data[n:]
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
if err != nil {
return fmt.Errorf("read dispenser response: %w", err)
}
if n == 0 {
return fmt.Errorf("read dispenser response: %w", io.ErrNoProgress)
}
}
return nil
}
func sendAndReadACK(ctx context.Context, port serialTransport, packet []byte, processingDelay time.Duration) error {
if err := writePacket(ctx, port, packet); err != nil {
return err
}
if err := waitForSequence(ctx, processingDelay); err != nil {
return err
}
response := make([]byte, 3)
if err := readExact(ctx, port, response); err != nil {
return fmt.Errorf("read ACK: %w", err)
}
return checkACK(response)
}
// queryStatus accepts only the fixed RF/AP payload sizes, before reading a body.
func queryStatus(ctx context.Context, port serialTransport, command []byte, statusCount int, processingDelay time.Duration) ([]byte, error) {
if err := sendAndReadACK(ctx, port, createPacket(Address, command), processingDelay); err != nil {
return nil, err
}
if err := writePacket(ctx, port, append([]byte{ENQ}, Address...)); err != nil {
return nil, err
}
if err := waitForSequence(ctx, processingDelay); err != nil {
return nil, err
}
header := make([]byte, 5)
if err := readExact(ctx, port, header); err != nil {
return nil, fmt.Errorf("read status header: %w", err)
}
if header[0] != STX {
return nil, fmt.Errorf("invalid status STX: % X", header)
}
if len(Address) != 2 || header[1] != Address[0] || header[2] != Address[1] {
return nil, fmt.Errorf("unexpected status address: % X", header[1:3])
}
length := int(binary.BigEndian.Uint16(header[3:5]))
if length != statusCount+2 {
return nil, fmt.Errorf("invalid status payload length: got %d, want %d", length, statusCount+2)
}
frame := append(header, make([]byte, length+2)...)
if err := readExact(ctx, port, frame[5:]); err != nil {
return nil, fmt.Errorf("read status body: %w", err)
}
if frame[len(frame)-2] != ETX {
return nil, fmt.Errorf("invalid status ETX: % X", frame)
}
if calculateBCC(frame[:len(frame)-1]) != frame[len(frame)-1] {
return nil, fmt.Errorf("invalid status BCC: % X", frame)
}
if frame[5] != 'S' || frame[6] != 'F' {
return nil, fmt.Errorf("unexpected status response type: % X", frame[5:7])
}
return frame[7 : 7+statusCount], nil
}
// --------------------
// Serial init (3 attempts)
// --------------------
func InitializeDispenser() (*serial.Port, error) {
const (
funcName = "InitializeDispenser"
maxRetries = 3
retryDelay = 4 * 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(ctx context.Context, port serialTransport) ([]byte, error) {
return queryStatus(ctx, port, []byte{0x02, 'A', 'P'}, 4, delay)
}
// dispatchCommand confirms ACK and sends ENQ; it does not wait for movement.
func dispatchCommand(ctx context.Context, port serialTransport, command []byte, processingDelay time.Duration) error {
if err := sendAndReadACK(ctx, port, createPacket(Address, command), processingDelay); err != nil {
return err
}
return writePacket(ctx, port, append([]byte{ENQ}, Address...))
}
func cardToEncoderPosition(ctx context.Context, port serialTransport) error {
log.Println("Send card to encoder position")
return dispatchCommand(ctx, port, commandFC7, delay)
}
func resetDispenser(ctx context.Context, port serialTransport) error {
return dispatchCommand(ctx, port, commandRS, delay)
}
func cardOutOfMouth(ctx context.Context, port serialTransport) error {
log.Println("Send card to out mouth position")
return dispatchCommand(ctx, port, commandFC0, delay)
}