Compare commits
2 Commits
1.1.0
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| aeea755045 | |||
| ebe50b17a9 |
@ -1,11 +1,8 @@
|
|||||||
package dispenser
|
package dispenser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
// "log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -22,29 +19,25 @@ const (
|
|||||||
space = 0x00 // Space character
|
space = 0x00 // Space character
|
||||||
baudRate = 9600 // Baud rate for serial communication
|
baudRate = 9600 // Baud rate for serial communication
|
||||||
delay = 500 * time.Millisecond // Delay for processing commands
|
delay = 500 * time.Millisecond // Delay for processing commands
|
||||||
)
|
|
||||||
|
|
||||||
// type (
|
// cache freshness for "continuous status" reads (tune as you wish)
|
||||||
// configRec struct {
|
defaultStatusTTL = 1500 * time.Millisecond
|
||||||
// SerialPort string `yaml:"port"`
|
)
|
||||||
// Address string `yaml:"addr"`
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
SerialPort string
|
SerialPort string
|
||||||
Address []byte
|
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
|
commandFC7 = []byte{ETX, 0x46, 0x43, 0x37} // "FC7"
|
||||||
|
commandFC0 = []byte{ETX, 0x46, 0x43, 0x30} // "FC0"
|
||||||
|
|
||||||
statusPos0 = map[byte]string{
|
statusPos0 = map[byte]string{
|
||||||
0x38: "Keep",
|
0x38: "Keep",
|
||||||
0x34: "Command cannot execute",
|
0x34: "Command cannot execute",
|
||||||
0x32: "Preparing card fails",
|
0x32: "Preparing card fails",
|
||||||
0x31: "Preparing card",
|
0x31: "Preparing card",
|
||||||
0x30: "Normal", // Default if none of the above
|
0x30: "Normal",
|
||||||
}
|
}
|
||||||
|
|
||||||
statusPos1 = map[byte]string{
|
statusPos1 = map[byte]string{
|
||||||
0x38: "Dispensing card",
|
0x38: "Dispensing card",
|
||||||
0x34: "Capturing card",
|
0x34: "Capturing card",
|
||||||
@ -52,7 +45,6 @@ var (
|
|||||||
0x31: "Capture card error",
|
0x31: "Capture card error",
|
||||||
0x30: "Normal",
|
0x30: "Normal",
|
||||||
}
|
}
|
||||||
|
|
||||||
statusPos2 = map[byte]string{
|
statusPos2 = map[byte]string{
|
||||||
0x38: "No captured card",
|
0x38: "No captured card",
|
||||||
0x34: "Card overlapped",
|
0x34: "Card overlapped",
|
||||||
@ -60,7 +52,6 @@ var (
|
|||||||
0x31: "Card pre-empty",
|
0x31: "Card pre-empty",
|
||||||
0x30: "Normal",
|
0x30: "Normal",
|
||||||
}
|
}
|
||||||
|
|
||||||
statusPos3 = map[byte]string{
|
statusPos3 = map[byte]string{
|
||||||
0x38: "Card empty",
|
0x38: "Card empty",
|
||||||
0x34: "Card ready position",
|
0x34: "Card ready position",
|
||||||
@ -71,8 +62,16 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Status helpers
|
||||||
|
// --------------------
|
||||||
|
|
||||||
func logStatus(statusBytes []byte) {
|
func logStatus(statusBytes []byte) {
|
||||||
// For each position, get the ASCII character, hex value, and mapped meaning.
|
if len(statusBytes) < 4 {
|
||||||
|
log.Infof("Dispenser status: <invalid len=%d>", len(statusBytes))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
posStatus := []struct {
|
posStatus := []struct {
|
||||||
pos int
|
pos int
|
||||||
value byte
|
value byte
|
||||||
@ -88,65 +87,52 @@ func logStatus(statusBytes []byte) {
|
|||||||
for _, p := range posStatus {
|
for _, p := range posStatus {
|
||||||
statusMsg, exists := p.mapper[p.value]
|
statusMsg, exists := p.mapper[p.value]
|
||||||
if !exists {
|
if !exists {
|
||||||
statusMsg = fmt.Sprintf("Unknown status 0x%X;", p.value)
|
statusMsg = fmt.Sprintf("Unknown status 0x%X at position %d", p.value, p.pos)
|
||||||
}
|
}
|
||||||
if p.value != 0x30 {
|
if p.value != 0x30 {
|
||||||
result.WriteString(statusMsg + "; ")
|
result.WriteString(statusMsg + "; ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Dispenser status: %s", result.String())
|
log.Infof("Dispenser status: %s", result.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAtEncoderPosition(statusBytes []byte) bool {
|
func isAtEncoderPosition(statusBytes []byte) bool {
|
||||||
if statusBytes == nil {
|
return len(statusBytes) >= 4 && statusBytes[3] == 0x33
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch statusBytes[3] {
|
|
||||||
case 0x33: // Card at encoder position
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false // Not at encoder position
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stockTake(statusBytes []byte) string {
|
func stockTake(statusBytes []byte) string {
|
||||||
status := ""
|
if len(statusBytes) < 4 {
|
||||||
if statusBytes == nil {
|
return ""
|
||||||
return status
|
|
||||||
}
|
}
|
||||||
|
status := ""
|
||||||
if statusBytes[2] != 0x30 {
|
if statusBytes[2] != 0x30 {
|
||||||
status = statusPos2[statusBytes[2]]
|
status = statusPos2[statusBytes[2]]
|
||||||
}
|
}
|
||||||
if statusBytes[3] == 0x38 { // Card well empty
|
if statusBytes[3] == 0x38 {
|
||||||
status = statusPos3[statusBytes[3]]
|
status = statusPos3[statusBytes[3]]
|
||||||
}
|
}
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCardWellEmpty(statusBytes []byte) bool {
|
func isCardWellEmpty(statusBytes []byte) bool {
|
||||||
if statusBytes == nil {
|
return len(statusBytes) >= 4 && statusBytes[3] == 0x38
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch statusBytes[3] {
|
|
||||||
case 0x38: // Card well empty
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkACK(statusResp []byte) error {
|
func checkACK(statusResp []byte) error {
|
||||||
if len(statusResp) == 3 && statusResp[0] == ACK && statusResp[1] == Address[0] && statusResp[2] == Address[1] {
|
if len(statusResp) == 3 &&
|
||||||
|
statusResp[0] == ACK &&
|
||||||
|
len(Address) >= 2 &&
|
||||||
|
statusResp[1] == Address[0] &&
|
||||||
|
statusResp[2] == Address[1] {
|
||||||
return nil
|
return 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)
|
|
||||||
}
|
}
|
||||||
|
if len(statusResp) > 0 && statusResp[0] == NAK {
|
||||||
|
return fmt.Errorf("negative response from dispenser")
|
||||||
|
}
|
||||||
|
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.
|
// calculateBCC computes BCC as XOR of all bytes from STX to ETX.
|
||||||
func calculateBCC(data []byte) byte {
|
func calculateBCC(data []byte) byte {
|
||||||
var bcc byte
|
var bcc byte
|
||||||
for _, b := range data {
|
for _, b := range data {
|
||||||
@ -157,8 +143,8 @@ func calculateBCC(data []byte) byte {
|
|||||||
|
|
||||||
func createPacket(address []byte, command []byte) []byte {
|
func createPacket(address []byte, command []byte) []byte {
|
||||||
packet := []byte{STX}
|
packet := []byte{STX}
|
||||||
packet = append(packet, address...) // Address bytes
|
packet = append(packet, address...)
|
||||||
packet = append(packet, space) // Space character
|
packet = append(packet, space)
|
||||||
packet = append(packet, command...)
|
packet = append(packet, command...)
|
||||||
packet = append(packet, ETX)
|
packet = append(packet, ETX)
|
||||||
bcc := calculateBCC(packet)
|
bcc := calculateBCC(packet)
|
||||||
@ -166,230 +152,134 @@ func createPacket(address []byte, command []byte) []byte {
|
|||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCheckRF(address []byte) []byte {
|
func buildCheckAP(address []byte) []byte { return createPacket(address, []byte{STX, 0x41, 0x50}) }
|
||||||
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) {
|
func sendAndReceive(port *serial.Port, packet []byte, delay time.Duration) ([]byte, error) {
|
||||||
n, err := port.Write(packet)
|
_, err := port.Write(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error writing to port: %w", err)
|
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
|
time.Sleep(delay)
|
||||||
|
|
||||||
buf := make([]byte, 128)
|
buf := make([]byte, 128)
|
||||||
n, err = port.Read(buf)
|
n, err := port.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading from port: %w", err)
|
return nil, fmt.Errorf("error reading from port: %w", err)
|
||||||
}
|
}
|
||||||
resp := buf[:n]
|
return buf[:n], nil
|
||||||
// log.Printf("RX %d bytes: % X", n, buf[:n])
|
|
||||||
return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Serial init (3 attempts)
|
||||||
|
// --------------------
|
||||||
|
|
||||||
func InitializeDispenser() (*serial.Port, error) {
|
func InitializeDispenser() (*serial.Port, error) {
|
||||||
const funcName = "initializeDispenser"
|
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{
|
serialConfig := &serial.Config{
|
||||||
Name: SerialPort,
|
Name: SerialPort,
|
||||||
Baud: baudRate,
|
Baud: baudRate,
|
||||||
ReadTimeout: time.Second * 2,
|
ReadTimeout: 2 * time.Second,
|
||||||
}
|
}
|
||||||
port, err := serial.OpenPort(serialConfig)
|
|
||||||
if err != nil {
|
var lastErr error
|
||||||
return nil, fmt.Errorf("error opening dispenser COM port: %w", err)
|
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 port, nil
|
|
||||||
|
return nil, fmt.Errorf("%s: failed to open dispenser on %s after %d attempts: %w", funcName, SerialPort, maxRetries, lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DispenserPrepare(port *serial.Port) (string, error) {
|
// --------------------
|
||||||
const funcName = "dispenserSequence"
|
// Internal (port-owner only) operations
|
||||||
stockStatus := ""
|
// --------------------
|
||||||
// Check dispenser status
|
|
||||||
status, err := CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
if isCardWellEmpty(status) {
|
|
||||||
return stockStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if isAtEncoderPosition(status) {
|
// checkDispenserStatus talks to the device and returns the 4 status bytes [pos0..pos3].
|
||||||
return stockStatus, nil
|
func checkDispenserStatus(port *serial.Port) ([]byte, error) {
|
||||||
}
|
|
||||||
// Send card to encoder position
|
|
||||||
err = CardToEncoderPosition(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
|
||||||
// Check dispenser status
|
|
||||||
status, err = CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
|
|
||||||
return stockStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DispenserStart(port *serial.Port) (string, error) {
|
|
||||||
const funcName = "dispenserSequence"
|
|
||||||
stockStatus := ""
|
|
||||||
// Check dispenser status
|
|
||||||
status, err := CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
if isCardWellEmpty(status) {
|
|
||||||
return stockStatus, fmt.Errorf(stockStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isAtEncoderPosition(status) {
|
|
||||||
return stockStatus, nil
|
|
||||||
}
|
|
||||||
// Send card to encoder position
|
|
||||||
err = CardToEncoderPosition(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
|
||||||
// Check dispenser status
|
|
||||||
status, err = CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
|
|
||||||
return stockStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DispenserFinal(port *serial.Port) (string, error) {
|
|
||||||
const funcName = "dispenserSequence"
|
|
||||||
stockStatus := ""
|
|
||||||
|
|
||||||
err := CardOutOfMouth(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error sending card to out mouth position: %v", funcName, err)
|
|
||||||
}
|
|
||||||
time.Sleep(delay)
|
|
||||||
// Check dispenser status
|
|
||||||
status, err := CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
|
||||||
// Send card to encoder position
|
|
||||||
err = CardToEncoderPosition(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
|
||||||
// Check dispenser status
|
|
||||||
status, err = CheckDispenserStatus(port)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
|
|
||||||
return stockStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if dispenser is not responding, I should repeat the command
|
|
||||||
func CheckDispenserStatus(port *serial.Port) ([]byte, error) {
|
|
||||||
const funcName = "checkDispenserStatus"
|
|
||||||
checkCmd := buildCheckAP(Address)
|
checkCmd := buildCheckAP(Address)
|
||||||
enq := append([]byte{ENQ}, Address...)
|
enq := append([]byte{ENQ}, Address...)
|
||||||
|
|
||||||
// Send check command (AP)
|
|
||||||
statusResp, err := sendAndReceive(port, checkCmd, delay)
|
statusResp, err := sendAndReceive(port, checkCmd, delay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error sending check command: %v", err)
|
return nil, fmt.Errorf("error sending check command: %w", err)
|
||||||
}
|
}
|
||||||
if len(statusResp) == 0 {
|
if len(statusResp) == 0 {
|
||||||
return nil, fmt.Errorf("no response from dispenser")
|
return nil, fmt.Errorf("no response from dispenser")
|
||||||
}
|
}
|
||||||
err = checkACK(statusResp)
|
if err := checkACK(statusResp); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ENQ+ADDR to prompt device to execute the command.
|
|
||||||
statusResp, err = sendAndReceive(port, enq, delay)
|
statusResp, err = sendAndReceive(port, enq, delay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error sending ENQ: %v", err)
|
return nil, fmt.Errorf("error sending ENQ: %w", err)
|
||||||
}
|
|
||||||
if len(statusResp) == 0 {
|
|
||||||
return nil, fmt.Errorf("no response from dispenser")
|
|
||||||
}
|
}
|
||||||
if len(statusResp) < 13 {
|
if len(statusResp) < 13 {
|
||||||
return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
|
return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
|
||||||
}
|
}
|
||||||
return statusResp[7:11], nil // Return status bytes
|
return statusResp[7:11], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CardToEncoderPosition(port *serial.Port) error {
|
func cardToEncoderPosition(port *serial.Port) error {
|
||||||
const funcName = "cartToEncoderPosition"
|
|
||||||
enq := append([]byte{ENQ}, Address...)
|
enq := append([]byte{ENQ}, Address...)
|
||||||
|
|
||||||
//Send Dispense card to encoder position (FC7) ---
|
|
||||||
dispenseCmd := createPacket(Address, commandFC7)
|
dispenseCmd := createPacket(Address, commandFC7)
|
||||||
log.Println("Send card to encoder position")
|
log.Println("Send card to encoder position")
|
||||||
|
|
||||||
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error sending card to encoder position: %v", err)
|
return fmt.Errorf("error sending card to encoder position: %w", err)
|
||||||
}
|
}
|
||||||
err = checkACK(statusResp)
|
if err := checkACK(statusResp); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//Send ENQ to prompt device ---
|
|
||||||
_, err = port.Write(enq)
|
_, err = port.Write(enq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error sending ENQ to prompt device: %v", err)
|
return fmt.Errorf("error sending ENQ to prompt device: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CardOutOfMouth(port *serial.Port) error {
|
func cardOutOfMouth(port *serial.Port) error {
|
||||||
const funcName = "CardOutOfMouth"
|
|
||||||
enq := append([]byte{ENQ}, Address...)
|
enq := append([]byte{ENQ}, Address...)
|
||||||
|
|
||||||
// Send card out of card mouth (FC0) ---
|
|
||||||
dispenseCmd := createPacket(Address, commandFC0)
|
dispenseCmd := createPacket(Address, commandFC0)
|
||||||
log.Println("Send card to out mouth position")
|
log.Println("Send card to out mouth position")
|
||||||
|
|
||||||
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error sending out of mouth command: %v", err)
|
return fmt.Errorf("error sending out of mouth command: %w", err)
|
||||||
}
|
}
|
||||||
err = checkACK(statusResp)
|
if err := checkACK(statusResp); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//Send ENQ to prompt device ---
|
|
||||||
_, err = port.Write(enq)
|
_, err = port.Write(enq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error sending ENQ to prompt device: %v", err)
|
return fmt.Errorf("error sending ENQ to prompt device: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
341
dispenser/dispenserclient.go
Normal file
341
dispenser/dispenserclient.go
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
// --------------------
|
||||||
|
// Queue-based client (single owner of port)
|
||||||
|
// --------------------
|
||||||
|
package dispenser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/tarm/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cmdType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
cmdStatus cmdType = iota
|
||||||
|
cmdToEncoder
|
||||||
|
cmdOutOfMouth
|
||||||
|
)
|
||||||
|
|
||||||
|
type cmdReq struct {
|
||||||
|
typ cmdType
|
||||||
|
ctx context.Context
|
||||||
|
respCh chan cmdResp
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmdResp struct {
|
||||||
|
status []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
port *serial.Port
|
||||||
|
|
||||||
|
reqCh chan cmdReq
|
||||||
|
done chan struct{}
|
||||||
|
|
||||||
|
// status cache
|
||||||
|
mu sync.RWMutex
|
||||||
|
lastStatus []byte
|
||||||
|
lastStatusT time.Time
|
||||||
|
statusTTL time.Duration
|
||||||
|
|
||||||
|
// published "stock/cardwell" cache + callback
|
||||||
|
lastStockMu sync.RWMutex
|
||||||
|
lastStock string
|
||||||
|
onStock func(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient starts the worker that owns the serial port.
|
||||||
|
func NewClient(port *serial.Port, queueSize int) *Client {
|
||||||
|
if queueSize <= 0 {
|
||||||
|
queueSize = 16
|
||||||
|
}
|
||||||
|
c := &Client{
|
||||||
|
port: port,
|
||||||
|
reqCh: make(chan cmdReq, queueSize),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
statusTTL: defaultStatusTTL,
|
||||||
|
}
|
||||||
|
go c.loop()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(c.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: tune cache TTL (how "fresh" cached status must be)
|
||||||
|
func (c *Client) SetStatusTTL(d time.Duration) {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.statusTTL = d
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnStockUpdate registers a callback called whenever polling (or status reads) produce a stock status string.
|
||||||
|
func (c *Client) OnStockUpdate(fn func(string)) {
|
||||||
|
c.lastStockMu.Lock()
|
||||||
|
c.onStock = fn
|
||||||
|
c.lastStockMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastStock returns the most recently computed stock/card-well status string.
|
||||||
|
func (c *Client) LastStock() string {
|
||||||
|
c.lastStockMu.RLock()
|
||||||
|
defer c.lastStockMu.RUnlock()
|
||||||
|
return c.lastStock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setStock(statusBytes []byte) {
|
||||||
|
stock := stockTake(statusBytes)
|
||||||
|
|
||||||
|
c.lastStockMu.Lock()
|
||||||
|
c.lastStock = stock
|
||||||
|
fn := c.onStock
|
||||||
|
c.lastStockMu.Unlock()
|
||||||
|
|
||||||
|
// call outside lock
|
||||||
|
if fn != nil {
|
||||||
|
fn(stock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartPolling performs a periodic status refresh.
|
||||||
|
// It will NOT interrupt commands: it enqueues only when queue is idle.
|
||||||
|
func (c *Client) StartPolling(interval time.Duration) {
|
||||||
|
if interval <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
// enqueue only if idle to avoid delaying real commands
|
||||||
|
if len(c.reqCh) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
_, err := c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("dispenser polling: %v", err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) loop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
case req := <-c.reqCh:
|
||||||
|
c.handle(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handle(req cmdReq) {
|
||||||
|
select {
|
||||||
|
case <-req.ctx.Done():
|
||||||
|
req.respCh <- cmdResp{err: req.ctx.Err()}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.typ {
|
||||||
|
case cmdStatus:
|
||||||
|
st, err := checkDispenserStatus(c.port)
|
||||||
|
if err == nil && len(st) == 4 {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.lastStatus = append([]byte(nil), st...)
|
||||||
|
c.lastStatusT = time.Now()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// publish stock/cardwell
|
||||||
|
c.setStock(st)
|
||||||
|
}
|
||||||
|
req.respCh <- cmdResp{status: st, err: err}
|
||||||
|
|
||||||
|
case cmdToEncoder:
|
||||||
|
err := cardToEncoderPosition(c.port)
|
||||||
|
req.respCh <- cmdResp{err: err}
|
||||||
|
|
||||||
|
case cmdOutOfMouth:
|
||||||
|
err := cardOutOfMouth(c.port)
|
||||||
|
req.respCh <- cmdResp{err: err}
|
||||||
|
|
||||||
|
default:
|
||||||
|
req.respCh <- cmdResp{err: fmt.Errorf("unknown command")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(ctx context.Context, typ cmdType) ([]byte, error) {
|
||||||
|
rch := make(chan cmdResp, 1)
|
||||||
|
req := cmdReq{typ: typ, ctx: ctx, respCh: rch}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case c.reqCh <- req:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case r := <-rch:
|
||||||
|
return r.status, r.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckStatus returns cached status if fresh, otherwise enqueues a device status read.
|
||||||
|
func (c *Client) CheckStatus(ctx context.Context) ([]byte, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
ttl := c.statusTTL
|
||||||
|
st := append([]byte(nil), c.lastStatus...)
|
||||||
|
ts := c.lastStatusT
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(st) == 4 && time.Since(ts) <= ttl {
|
||||||
|
// even when returning cached, keep stock in sync
|
||||||
|
c.setStock(st)
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
return c.do(ctx, cmdStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ToEncoder(ctx context.Context) error {
|
||||||
|
_, err := c.do(ctx, cmdToEncoder)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) OutOfMouth(ctx context.Context) error {
|
||||||
|
_, err := c.do(ctx, cmdOutOfMouth)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Public sequences updated to use Client (queue)
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
// DispenserPrepare: check status; if empty => ok; else ensure at encoder.
|
||||||
|
func (c *Client) DispenserPrepare(ctx context.Context) (string, error) {
|
||||||
|
const funcName = "DispenserPrepare"
|
||||||
|
stockStatus := ""
|
||||||
|
|
||||||
|
status, err := c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
if isCardWellEmpty(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
if isAtEncoderPosition(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ToEncoder(ctx); err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
status, err = c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] re-check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DispenserStart(ctx context.Context) (string, error) {
|
||||||
|
const funcName = "DispenserStart"
|
||||||
|
stockStatus := ""
|
||||||
|
|
||||||
|
status, err := c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
if isCardWellEmpty(status) {
|
||||||
|
return stockStatus, fmt.Errorf(stockStatus)
|
||||||
|
}
|
||||||
|
if isAtEncoderPosition(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ToEncoder(ctx); err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
status, err = c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] re-check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DispenserFinal(ctx context.Context) (string, error) {
|
||||||
|
const funcName = "DispenserFinal"
|
||||||
|
stockStatus := ""
|
||||||
|
|
||||||
|
if err := c.OutOfMouth(ctx); err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] out of mouth: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
status, err := c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
if err := c.ToEncoder(ctx); err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
status, err = c.CheckStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stockStatus, fmt.Errorf("[%s] re-check status: %w", funcName, err)
|
||||||
|
}
|
||||||
|
logStatus(status)
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
@ -12,8 +12,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tarm/serial"
|
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||||
@ -27,24 +25,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
dispPort *serial.Port
|
disp *dispenser.Client
|
||||||
lockserver lockserver.LockServer
|
lockserver lockserver.LockServer
|
||||||
isPayment bool
|
isPayment bool
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
cfg *config.ConfigRec
|
cfg *config.ConfigRec
|
||||||
dbMu sync.Mutex
|
dbMu sync.Mutex
|
||||||
|
cardWellMu sync.RWMutex
|
||||||
cardWellStatus string
|
cardWellStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(dispPort *serial.Port, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
func NewApp(disp *dispenser.Client, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
||||||
return &App{
|
app := &App{
|
||||||
isPayment: cfg.IsPayment,
|
isPayment: cfg.IsPayment,
|
||||||
dispPort: dispPort,
|
disp: disp,
|
||||||
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
|
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
|
||||||
db: db,
|
db: db,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cardWellStatus: cardWellStatus,
|
|
||||||
}
|
}
|
||||||
|
app.SetCardWellStatus(cardWellStatus)
|
||||||
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) RegisterRoutes(mux *http.ServeMux) {
|
func (app *App) RegisterRoutes(mux *http.ServeMux) {
|
||||||
@ -281,37 +281,51 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.cardWellStatus, err = dispenser.DispenserStart(app.dispPort); err != nil {
|
// Ensure dispenser ready (card at encoder) BEFORE we attempt encoding.
|
||||||
|
// With queued dispenser ops, this will not clash with polling.
|
||||||
|
status, err := app.disp.DispenserStart(r.Context())
|
||||||
|
app.SetCardWellStatus(status)
|
||||||
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always attempt to finalize after we have moved a card / started an issuance flow.
|
||||||
|
// This guarantees we eject and prepare the next card even on lock failures.
|
||||||
|
finalize := func() {
|
||||||
|
if app.disp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status, ferr := app.disp.DispenserFinal(ctx)
|
||||||
|
if ferr != nil {
|
||||||
|
logging.Error(serviceName, ferr.Error(), "Dispenser final error", string(op), "", "", 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.SetCardWellStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
// build lock server command
|
// build lock server command
|
||||||
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
||||||
|
|
||||||
// lock server sequence
|
// lock server sequence
|
||||||
err = app.lockserver.LockSequence()
|
if err := app.lockserver.LockSequence(); err != nil {
|
||||||
if err != nil {
|
|
||||||
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
||||||
dispenser.DispenserFinal(app.dispPort)
|
finalize()
|
||||||
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
|
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// final dispenser steps
|
// final dispenser steps
|
||||||
if app.cardWellStatus, err = dispenser.DispenserFinal(app.dispPort); err != nil {
|
finalize()
|
||||||
logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0)
|
|
||||||
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
theResponse.Code = http.StatusOK
|
theResponse.Code = http.StatusOK
|
||||||
theResponse.Message = "Card issued successfully"
|
theResponse.Message = "Card issued successfully"
|
||||||
// success! return 200 and any data you like
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(theResponse)
|
_ = json.NewEncoder(w).Encode(theResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -369,8 +383,20 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (app *App) reportDispenserStatus(w http.ResponseWriter, r *http.Request) {
|
func (app *App) reportDispenserStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(cmstypes.StatusRec{
|
_ = json.NewEncoder(w).Encode(cmstypes.StatusRec{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
Message: app.cardWellStatus,
|
Message: app.CardWellStatus(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) SetCardWellStatus(s string) {
|
||||||
|
app.cardWellMu.Lock()
|
||||||
|
app.cardWellStatus = s
|
||||||
|
app.cardWellMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) CardWellStatus() string {
|
||||||
|
app.cardWellMu.RLock()
|
||||||
|
defer app.cardWellMu.RUnlock()
|
||||||
|
return app.cardWellStatus
|
||||||
|
}
|
||||||
|
|||||||
81
main.go
81
main.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -26,68 +27,82 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.1.0"
|
buildVersion = "1.1.2"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
|
pollingFrequency = 8 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load config
|
// Load config
|
||||||
config := config.ReadHardlinkConfig()
|
cfg := config.ReadHardlinkConfig()
|
||||||
printer.Layout = readTicketLayout()
|
printer.Layout = readTicketLayout()
|
||||||
printer.PrinterName = config.PrinterName
|
printer.PrinterName = cfg.PrinterName
|
||||||
lockserver.Cert = config.Cert
|
lockserver.Cert = cfg.Cert
|
||||||
lockserver.LockServerURL = config.LockserverUrl
|
lockserver.LockServerURL = cfg.LockserverUrl
|
||||||
dispHandle := &serial.Port{}
|
|
||||||
cardWellStatus := ""
|
var (
|
||||||
|
dispPort *serial.Port
|
||||||
|
disp *dispenser.Client
|
||||||
|
cardWellStatus string
|
||||||
|
)
|
||||||
|
|
||||||
// Setup logging and get file handle
|
// Setup logging and get file handle
|
||||||
logFile, err := logging.SetupLogging(config.LogDir, serviceName, buildVersion)
|
logFile, err := logging.SetupLogging(cfg.LogDir, serviceName, buildVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to set up logging: %v\n", err)
|
log.Printf("Failed to set up logging: %v\n", err)
|
||||||
}
|
}
|
||||||
defer logFile.Close()
|
defer logFile.Close()
|
||||||
|
|
||||||
// Initialize dispenser
|
// Initialize dispenser
|
||||||
if !config.TestMode {
|
if !cfg.TestMode {
|
||||||
dispenser.SerialPort = config.DispenserPort
|
dispenser.SerialPort = cfg.DispenserPort
|
||||||
dispenser.Address = []byte(config.DispenserAdrr)
|
dispenser.Address = []byte(cfg.DispenserAdrr)
|
||||||
dispHandle, err = dispenser.InitializeDispenser()
|
|
||||||
|
dispPort, err = dispenser.InitializeDispenser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorhandlers.FatalError(err)
|
errorhandlers.FatalError(err)
|
||||||
}
|
}
|
||||||
defer dispHandle.Close()
|
defer dispPort.Close()
|
||||||
|
|
||||||
cardWellStatus, err = dispenser.DispenserPrepare(dispHandle)
|
// Start queued dispenser client (single goroutine owns the serial port)
|
||||||
|
disp = dispenser.NewClient(dispPort, 32)
|
||||||
|
defer disp.Close()
|
||||||
|
|
||||||
|
// Prepare dispenser (ensures card at encoder position unless empty)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cardWellStatus, err = disp.DispenserPrepare(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr)
|
err = fmt.Errorf("%s; wrong dispenser address: %s", err, cfg.DispenserAdrr)
|
||||||
errorhandlers.FatalError(err)
|
errorhandlers.FatalError(err)
|
||||||
}
|
}
|
||||||
fmt.Println(cardWellStatus)
|
fmt.Println(cardWellStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test lock-server connection
|
// Test lock-server connection
|
||||||
switch strings.ToLower(config.LockType) {
|
switch strings.ToLower(cfg.LockType) {
|
||||||
case lockserver.TLJ:
|
case lockserver.TLJ:
|
||||||
|
// TLJ uses HTTP - skip TCP probe here (as you did before)
|
||||||
default:
|
default:
|
||||||
lockConn, err := lockserver.InitializeServerConnection(config.LockserverUrl)
|
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Connected to the lock server successfuly at %s\n", config.LockserverUrl)
|
fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverUrl)
|
||||||
log.Infof("Connected to the lock server successfuly at %s", config.LockserverUrl)
|
log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverUrl)
|
||||||
lockConn.Close()
|
lockConn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
database, err := bootstrap.OpenDB(&config)
|
database, err := bootstrap.OpenDB(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("DB init failed: %v", err)
|
log.Warnf("DB init failed: %v", err)
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
if config.IsPayment {
|
if cfg.IsPayment {
|
||||||
fmt.Println("Payment processing is enabled")
|
fmt.Println("Payment processing is enabled")
|
||||||
log.Info("Payment processing is enabled")
|
log.Info("Payment processing is enabled")
|
||||||
startChipDnaClient()
|
startChipDnaClient()
|
||||||
@ -97,14 +112,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create App and wire routes
|
// Create App and wire routes
|
||||||
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, cardWellStatus, database, &config)
|
// NOTE: change handlers.NewApp signature to accept *dispenser.Client instead of *serial.Port
|
||||||
|
app := handlers.NewApp(disp, cfg.LockType, cfg.EncoderAddress, cardWellStatus, database, &cfg)
|
||||||
|
|
||||||
|
// Update cardWellStatus when dispenser status changes
|
||||||
|
if !cfg.TestMode {
|
||||||
|
// Set initial cardWellStatus
|
||||||
|
app.SetCardWellStatus(cardWellStatus)
|
||||||
|
|
||||||
|
// Set up callback to update cardWellStatus when dispenser status changes
|
||||||
|
disp.OnStockUpdate(func(stock string) {
|
||||||
|
app.SetCardWellStatus(stock)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start polling for dispenser status every 10 seconds
|
||||||
|
disp.StartPolling(pollingFrequency)
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
app.RegisterRoutes(mux)
|
app.RegisterRoutes(mux)
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%d", config.Port)
|
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||||
log.Infof("Starting HTTP server on http://localhost%s", addr)
|
log.Infof("Starting HTTP server on http://localhost%s", addr)
|
||||||
fmt.Printf("Starting HTTP server on http://localhost%s", addr)
|
fmt.Printf("Starting HTTP server on http://localhost%s", addr)
|
||||||
|
|
||||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||||
errorhandlers.FatalError(err)
|
errorhandlers.FatalError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### 1.1.2 - 02 February 2026
|
||||||
|
added logging for unknown dispenser status positions
|
||||||
|
|
||||||
|
#### 1.1.1 - 02 February 2026
|
||||||
|
added contionuous polling of the dispenser status every 8 seconds to update the card well status
|
||||||
|
|
||||||
#### 1.1.0 - 26 January 2026
|
#### 1.1.0 - 26 January 2026
|
||||||
divided `/starttransaction` endpoint into two separate endpoints:
|
divided `/starttransaction` endpoint into two separate endpoints:
|
||||||
`/takepreauth` to request preauthorization payment
|
`/takepreauth` to request preauthorization payment
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user