diff --git a/dispenser/dispenser.go b/dispenser/dispenser.go index 1f43e49..c6019dd 100644 --- a/dispenser/dispenser.go +++ b/dispenser/dispenser.go @@ -3,6 +3,8 @@ package dispenser import ( // "encoding/hex" "fmt" + "strings" + // "log" "time" @@ -69,44 +71,78 @@ var ( } ) -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}, - } +func logStatus(statusBytes []byte) { + // 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") - } + var result strings.Builder + for _, p := range posStatus { + statusMsg, exists := p.mapper[p.value] + if !exists { + statusMsg = fmt.Sprintf("Unknown status 0x%X;", p.value) } - 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 { - 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) - } + return fmt.Errorf("unexpected response status: % X", statusResp) } } @@ -171,47 +207,130 @@ func InitializeDispenser() (*serial.Port, error) { return port, nil } -func DispenserSequence(port *serial.Port) (string, error) { +func DispenserPrepare(port *serial.Port) (string, error) { const funcName = "dispenserSequence" - var result string - + stockStatus := "" // Check dispenser status status, err := CheckDispenserStatus(port) 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 - status, err = CardToEncoderPosition(port) + err = CardToEncoderPosition(port) 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 -func CheckDispenserStatus(port *serial.Port) (string, error) { +func CheckDispenserStatus(port *serial.Port) ([]byte, 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) + return nil, fmt.Errorf("error sending check command: %v", err) } 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 { - return status, err + return nil, err } - result += "; " + status // Send ENQ+ADDR to prompt device to execute the command. statusResp, err = sendAndReceive(port, enq, delay) @@ -219,17 +338,15 @@ func CheckDispenserStatus(port *serial.Port) (string, error) { log.Errorf("error sending ENQ: %v", err) } if len(statusResp) == 0 { - return "", fmt.Errorf("no response from dispenser") + return nil, fmt.Errorf("no response from dispenser") } - status, err = checkStatus(statusResp) - if err != nil { - return status, err + if len(statusResp) < 13 { + return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp) } - result += status - return result, nil + return statusResp[7:11], nil // Return status bytes } -func CardToEncoderPosition(port *serial.Port) (string, error) { +func CardToEncoderPosition(port *serial.Port) error { const funcName = "cartToEncoderPosition" enq := append([]byte{ENQ}, Address...) @@ -238,30 +355,22 @@ func CardToEncoderPosition(port *serial.Port) (string, error) { 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) + return fmt.Errorf("error sending card to encoder position: %v", err) } - _, err = checkStatus(statusResp) + err = checkACK(statusResp) if err != nil { - return "", err + 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) + return fmt.Errorf("error sending ENQ to prompt device: %v", err) } - - time.Sleep(delay) - - //Check card position status - status, err := CheckDispenserStatus(port) - if err != nil { - return "", err - } - return status, nil + return nil } -func CardOutOfMouth(port *serial.Port) (string, error) { +func CardOutOfMouth(port *serial.Port) error { const funcName = "CardOutOfMouth" enq := append([]byte{ENQ}, Address...) @@ -270,25 +379,17 @@ func CardOutOfMouth(port *serial.Port) (string, error) { 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) + return fmt.Errorf("error sending out of mouth command: %v", err) } - _, err = checkStatus(statusResp) + err = checkACK(statusResp) if err != nil { - return "", err + 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) + return fmt.Errorf("error sending ENQ to prompt device: %v", err) } - - time.Sleep(delay) - - //Check card position status - status, err := CheckDispenserStatus(port) - if err != nil { - return "", err - } - return status, nil + return nil } diff --git a/handlers/handlers.go b/handlers/handlers.go index c33db7c..b0be399 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -27,21 +27,23 @@ import ( ) type App struct { - dispPort *serial.Port - lockserver lockserver.LockServer - isPayment bool - db *sql.DB - cfg *config.ConfigRec - dbMu sync.Mutex + dispPort *serial.Port + lockserver lockserver.LockServer + isPayment bool + db *sql.DB + cfg *config.ConfigRec + 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{ isPayment: cfg.IsPayment, dispPort: dispPort, lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError), db: db, cfg: cfg, + cardWellStatus: cardWellStatus, } } @@ -50,6 +52,7 @@ func (app *App) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/printroomticket", app.printRoomTicket) mux.HandleFunc("/takepreauth", app.takePreauthorization) mux.HandleFunc("/takepayment", app.takePayment) + mux.HandleFunc("/dispenserstatus", app.reportDispenserStatus) } 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 } - // dispenser sequence - if status, err := dispenser.DispenserSequence(app.dispPort); err != nil { - if status != "" { - 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") - } + if app.cardWellStatus, err = dispenser.DispenserStart(app.dispPort); err != nil { + logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0) + errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()) return - } else { - log.Info(status) } // build lock server command @@ -299,18 +294,16 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { err = app.lockserver.LockSequence() if err != nil { logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0) + dispenser.DispenserFinal(app.dispPort) errorhandlers.WriteError(w, http.StatusBadGateway, err.Error()) - dispenser.CardOutOfMouth(app.dispPort) return } // 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) errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error()) return - } else { - log.Info(status) } theResponse.Code = http.StatusOK @@ -372,3 +365,12 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) { 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, + }) +} diff --git a/lockserver/saltolockserver.go b/lockserver/saltolockserver.go index 7963558..c367b29 100644 --- a/lockserver/saltolockserver.go +++ b/lockserver/saltolockserver.go @@ -155,7 +155,7 @@ func (lock *SaltoLockServer) LockSequence() error { reader := bufio.NewReader(conn) // 1. Send ENQ - log.Infof("Sending ENQ") + log.Infof("LockSequence: sending ENQ") if _, e := conn.Write([]byte{ENQ}); e != nil { return fmt.Errorf("failed to send ENQ: %w", e) } @@ -166,7 +166,7 @@ func (lock *SaltoLockServer) LockSequence() error { } // 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 { return fmt.Errorf("failed to send command frame: %w", e) } diff --git a/main.go b/main.go index da54d60..7c4b6c9 100644 --- a/main.go +++ b/main.go @@ -18,15 +18,15 @@ import ( "gitea.futuresens.co.uk/futuresens/hardlink/bootstrap" "gitea.futuresens.co.uk/futuresens/hardlink/config" "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/handlers" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/logging" "gitea.futuresens.co.uk/futuresens/hardlink/printer" ) const ( - buildVersion = "1.0.30" + buildVersion = "1.1.0" serviceName = "hardlink" ) @@ -38,6 +38,7 @@ func main() { lockserver.Cert = config.Cert lockserver.LockServerURL = config.LockserverUrl dispHandle := &serial.Port{} + cardWellStatus := "" // Setup logging and get file handle logFile, err := logging.SetupLogging(config.LogDir, serviceName, buildVersion) @@ -56,17 +57,12 @@ func main() { } defer dispHandle.Close() - status, err := dispenser.CheckDispenserStatus(dispHandle) + cardWellStatus, err = dispenser.DispenserPrepare(dispHandle) if err != nil { - if len(status) == 0 { - err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr) - errorhandlers.FatalError(err) - } else { - fmt.Println(status) - fmt.Println(err.Error()) - } + err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr) + errorhandlers.FatalError(err) } - log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status) + fmt.Println(cardWellStatus) } // Test lock-server connection @@ -101,7 +97,7 @@ func main() { } // 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() app.RegisterRoutes(mux) diff --git a/release notes.md b/release notes.md index eac58c5..2090d24 100644 --- a/release notes.md +++ b/release notes.md @@ -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 db connection check before adding a transaction to the database 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 improved logging for preauth releaser