release version 1.1.0

This commit is contained in:
yurii 2026-01-29 17:23:00 +00:00
parent 9f0a9c939f
commit 4a255c06ed
5 changed files with 221 additions and 120 deletions

View File

@ -3,6 +3,8 @@ package dispenser
import ( import (
// "encoding/hex" // "encoding/hex"
"fmt" "fmt"
"strings"
// "log" // "log"
"time" "time"
@ -69,44 +71,78 @@ var (
} }
) )
func checkStatus(statusResp []byte) (string, error) { func logStatus(statusBytes []byte) {
if len(statusResp) > 3 { // For each position, get the ASCII character, hex value, and mapped meaning.
statusBytes := statusResp[7:11] // Extract the relevant bytes from the response posStatus := []struct {
// For each position, get the ASCII character, hex value, and mapped meaning. pos int
posStatus := []struct { value byte
pos int mapper map[byte]string
value byte }{
mapper map[byte]string {pos: 1, value: statusBytes[0], mapper: statusPos0},
}{ {pos: 2, value: statusBytes[1], mapper: statusPos1},
{pos: 1, value: statusBytes[0], mapper: statusPos0}, {pos: 3, value: statusBytes[2], mapper: statusPos2},
{pos: 2, value: statusBytes[1], mapper: statusPos1}, {pos: 4, value: statusBytes[3], mapper: statusPos3},
{pos: 3, value: statusBytes[2], mapper: statusPos2}, }
{pos: 4, value: statusBytes[3], mapper: statusPos3},
}
result := "" var result strings.Builder
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 = "Unknown status" statusMsg = fmt.Sprintf("Unknown status 0x%X;", p.value)
}
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 if p.value != 0x30 {
result.WriteString(statusMsg + "; ")
}
}
log.Infof("Dispenser status: %s", result.String())
}
func isAtEncoderPosition(statusBytes []byte) bool {
if statusBytes == nil {
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 {
status := ""
if statusBytes == nil {
return status
}
if statusBytes[2] != 0x30 {
status = statusPos2[statusBytes[2]]
}
if statusBytes[3] == 0x38 { // Card well empty
status = statusPos3[statusBytes[3]]
}
return status
}
func isCardWellEmpty(statusBytes []byte) bool {
if statusBytes == nil {
return false
}
switch statusBytes[3] {
case 0x38: // Card well empty
return true
default:
return false
}
}
func checkACK(statusResp []byte) error {
if len(statusResp) == 3 && statusResp[0] == ACK && statusResp[1] == Address[0] && statusResp[2] == Address[1] {
return nil
} else if len(statusResp) > 0 && statusResp[0] == NAK {
return fmt.Errorf("negative response from dispenser")
} else { } else {
if len(statusResp) == 3 && statusResp[0] == ACK && statusResp[1] == Address[0] && statusResp[2] == Address[1] { return fmt.Errorf("unexpected response status: % X", statusResp)
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)
}
} }
} }
@ -171,47 +207,130 @@ func InitializeDispenser() (*serial.Port, error) {
return port, nil return port, nil
} }
func DispenserSequence(port *serial.Port) (string, error) { func DispenserPrepare(port *serial.Port) (string, error) {
const funcName = "dispenserSequence" const funcName = "dispenserSequence"
var result string stockStatus := ""
// Check dispenser status // Check dispenser status
status, err := CheckDispenserStatus(port) status, err := CheckDispenserStatus(port)
if err != nil { if err != nil {
return status, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err) return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
}
logStatus(status)
stockStatus = stockTake(status)
if isCardWellEmpty(status) {
return stockStatus, nil
} }
result += status
if isAtEncoderPosition(status) {
return stockStatus, nil
}
// Send card to encoder position // Send card to encoder position
status, err = CardToEncoderPosition(port) err = CardToEncoderPosition(port)
if err != nil { if err != nil {
return status, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err) return stockStatus, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
} }
result += "; " + status
return result, nil 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 // if dispenser is not responding, I should repeat the command
func CheckDispenserStatus(port *serial.Port) (string, error) { func CheckDispenserStatus(port *serial.Port) ([]byte, error) {
const funcName = "checkDispenserStatus" const funcName = "checkDispenserStatus"
var result string
checkCmd := buildCheckAP(Address) checkCmd := buildCheckAP(Address)
enq := append([]byte{ENQ}, Address...) enq := append([]byte{ENQ}, Address...)
// Send check command (AP) // Send check command (AP)
statusResp, err := sendAndReceive(port, checkCmd, delay) statusResp, err := sendAndReceive(port, checkCmd, delay)
if err != nil { if err != nil {
return "", fmt.Errorf("error sending check command: %v", err) return nil, fmt.Errorf("error sending check command: %v", err)
} }
if len(statusResp) == 0 { if len(statusResp) == 0 {
return "", fmt.Errorf("no response from dispenser") return nil, fmt.Errorf("no response from dispenser")
} }
status, err := checkStatus(statusResp) err = checkACK(statusResp)
if err != nil { if err != nil {
return status, err return nil, err
} }
result += "; " + status
// Send ENQ+ADDR to prompt device to execute the command. // Send ENQ+ADDR to prompt device to execute the command.
statusResp, err = sendAndReceive(port, enq, delay) statusResp, err = sendAndReceive(port, enq, delay)
@ -219,17 +338,15 @@ func CheckDispenserStatus(port *serial.Port) (string, error) {
log.Errorf("error sending ENQ: %v", err) log.Errorf("error sending ENQ: %v", err)
} }
if len(statusResp) == 0 { if len(statusResp) == 0 {
return "", fmt.Errorf("no response from dispenser") return nil, fmt.Errorf("no response from dispenser")
} }
status, err = checkStatus(statusResp) if len(statusResp) < 13 {
if err != nil { return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
return status, err
} }
result += status return statusResp[7:11], nil // Return status bytes
return result, nil
} }
func CardToEncoderPosition(port *serial.Port) (string, error) { func CardToEncoderPosition(port *serial.Port) error {
const funcName = "cartToEncoderPosition" const funcName = "cartToEncoderPosition"
enq := append([]byte{ENQ}, Address...) enq := append([]byte{ENQ}, Address...)
@ -238,30 +355,22 @@ func CardToEncoderPosition(port *serial.Port) (string, error) {
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: %v", err)
} }
_, err = checkStatus(statusResp) err = checkACK(statusResp)
if err != nil { if err != nil {
return "", err return err
} }
//Send ENQ to prompt device --- //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: %v", err)
} }
return nil
time.Sleep(delay)
//Check card position status
status, err := CheckDispenserStatus(port)
if err != nil {
return "", err
}
return status, nil
} }
func CardOutOfMouth(port *serial.Port) (string, error) { func CardOutOfMouth(port *serial.Port) error {
const funcName = "CardOutOfMouth" const funcName = "CardOutOfMouth"
enq := append([]byte{ENQ}, Address...) enq := append([]byte{ENQ}, Address...)
@ -270,25 +379,17 @@ func CardOutOfMouth(port *serial.Port) (string, error) {
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: %v", err)
} }
_, err = checkStatus(statusResp) err = checkACK(statusResp)
if err != nil { if err != nil {
return "", err return err
} }
//Send ENQ to prompt device --- //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: %v", err)
} }
return nil
time.Sleep(delay)
//Check card position status
status, err := CheckDispenserStatus(port)
if err != nil {
return "", err
}
return status, nil
} }

View File

@ -27,21 +27,23 @@ import (
) )
type App struct { type App struct {
dispPort *serial.Port dispPort *serial.Port
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
cardWellStatus string
} }
func NewApp(dispPort *serial.Port, lockType, encoderAddress string, db *sql.DB, cfg *config.ConfigRec) *App { func NewApp(dispPort *serial.Port, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
return &App{ return &App{
isPayment: cfg.IsPayment, isPayment: cfg.IsPayment,
dispPort: dispPort, dispPort: dispPort,
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError), lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
db: db, db: db,
cfg: cfg, cfg: cfg,
cardWellStatus: cardWellStatus,
} }
} }
@ -50,6 +52,7 @@ func (app *App) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/printroomticket", app.printRoomTicket) mux.HandleFunc("/printroomticket", app.printRoomTicket)
mux.HandleFunc("/takepreauth", app.takePreauthorization) mux.HandleFunc("/takepreauth", app.takePreauthorization)
mux.HandleFunc("/takepayment", app.takePayment) mux.HandleFunc("/takepayment", app.takePayment)
mux.HandleFunc("/dispenserstatus", app.reportDispenserStatus)
} }
func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) { func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
@ -278,18 +281,10 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
return return
} }
// dispenser sequence if app.cardWellStatus, err = dispenser.DispenserStart(app.dispPort); err != nil {
if status, err := dispenser.DispenserSequence(app.dispPort); err != nil { logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
if status != "" { errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
} else {
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock")
}
return return
} else {
log.Info(status)
} }
// build lock server command // build lock server command
@ -299,18 +294,16 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
err = app.lockserver.LockSequence() err = app.lockserver.LockSequence()
if 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)
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error()) errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
dispenser.CardOutOfMouth(app.dispPort)
return return
} }
// final dispenser steps // final dispenser steps
if status, err := dispenser.CardOutOfMouth(app.dispPort); err != nil { if app.cardWellStatus, err = dispenser.DispenserFinal(app.dispPort); err != nil {
logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0) logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error()) errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error())
return return
} else {
log.Info(status)
} }
theResponse.Code = http.StatusOK theResponse.Code = http.StatusOK
@ -372,3 +365,12 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
Message: "Print job sent successfully", Message: "Print job sent successfully",
}) })
} }
func (app *App) reportDispenserStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(cmstypes.StatusRec{
Code: http.StatusOK,
Message: app.cardWellStatus,
})
}

View File

@ -155,7 +155,7 @@ func (lock *SaltoLockServer) LockSequence() error {
reader := bufio.NewReader(conn) reader := bufio.NewReader(conn)
// 1. Send ENQ // 1. Send ENQ
log.Infof("Sending ENQ") log.Infof("LockSequence: sending ENQ")
if _, e := conn.Write([]byte{ENQ}); e != nil { if _, e := conn.Write([]byte{ENQ}); e != nil {
return fmt.Errorf("failed to send ENQ: %w", e) return fmt.Errorf("failed to send ENQ: %w", e)
} }
@ -166,7 +166,7 @@ func (lock *SaltoLockServer) LockSequence() error {
} }
// 3. Send command frame // 3. Send command frame
log.Infof("Sending encoding command: %q", string(lock.command)) log.Infof("LockSequence: sending encoding command: %q", string(lock.command))
if _, e := conn.Write(lock.command); e != nil { if _, e := conn.Write(lock.command); e != nil {
return fmt.Errorf("failed to send command frame: %w", e) return fmt.Errorf("failed to send command frame: %w", e)
} }

20
main.go
View File

@ -18,15 +18,15 @@ import (
"gitea.futuresens.co.uk/futuresens/hardlink/bootstrap" "gitea.futuresens.co.uk/futuresens/hardlink/bootstrap"
"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"
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers" "gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
"gitea.futuresens.co.uk/futuresens/hardlink/logging" "gitea.futuresens.co.uk/futuresens/hardlink/logging"
"gitea.futuresens.co.uk/futuresens/hardlink/printer" "gitea.futuresens.co.uk/futuresens/hardlink/printer"
) )
const ( const (
buildVersion = "1.0.30" buildVersion = "1.1.0"
serviceName = "hardlink" serviceName = "hardlink"
) )
@ -38,6 +38,7 @@ func main() {
lockserver.Cert = config.Cert lockserver.Cert = config.Cert
lockserver.LockServerURL = config.LockserverUrl lockserver.LockServerURL = config.LockserverUrl
dispHandle := &serial.Port{} dispHandle := &serial.Port{}
cardWellStatus := ""
// Setup logging and get file handle // Setup logging and get file handle
logFile, err := logging.SetupLogging(config.LogDir, serviceName, buildVersion) logFile, err := logging.SetupLogging(config.LogDir, serviceName, buildVersion)
@ -56,17 +57,12 @@ func main() {
} }
defer dispHandle.Close() defer dispHandle.Close()
status, err := dispenser.CheckDispenserStatus(dispHandle) cardWellStatus, err = dispenser.DispenserPrepare(dispHandle)
if err != nil { if err != nil {
if len(status) == 0 { err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr)
err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr) errorhandlers.FatalError(err)
errorhandlers.FatalError(err)
} else {
fmt.Println(status)
fmt.Println(err.Error())
}
} }
log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status) fmt.Println(cardWellStatus)
} }
// Test lock-server connection // Test lock-server connection
@ -101,7 +97,7 @@ func main() {
} }
// Create App and wire routes // Create App and wire routes
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, database, &config) app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, cardWellStatus, database, &config)
mux := http.NewServeMux() mux := http.NewServeMux()
app.RegisterRoutes(mux) app.RegisterRoutes(mux)

View File

@ -9,6 +9,8 @@ divided `/starttransaction` endpoint into two separate endpoints:
added preauth releaser functionality to release preauthorization payments after a defined time period added preauth releaser functionality to release preauthorization payments after a defined time period
added db connection check before adding a transaction to the database added db connection check before adding a transaction to the database
and reconnection functionality if the connection to the database is lost and reconnection functionality if the connection to the database is lost
added `/dispenserstatus` endpoint
key card always stays at encoder position
#### 1.0.30 - 09 January 2026 #### 1.0.30 - 09 January 2026
improved logging for preauth releaser improved logging for preauth releaser