Compare commits
No commits in common. "4a255c06ed72ad16d1a049066ec67b9e18f2e40b" and "7f6262b470ac1a87c7eef52b24d437580dd12c5b" have entirely different histories.
4a255c06ed
...
7f6262b470
@ -3,8 +3,6 @@ package dispenser
|
|||||||
import (
|
import (
|
||||||
// "encoding/hex"
|
// "encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
// "log"
|
// "log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -71,7 +69,9 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func logStatus(statusBytes []byte) {
|
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.
|
// For each position, get the ASCII character, hex value, and mapped meaning.
|
||||||
posStatus := []struct {
|
posStatus := []struct {
|
||||||
pos int
|
pos int
|
||||||
@ -84,65 +84,29 @@ func logStatus(statusBytes []byte) {
|
|||||||
{pos: 4, value: statusBytes[3], mapper: statusPos3},
|
{pos: 4, value: statusBytes[3], mapper: statusPos3},
|
||||||
}
|
}
|
||||||
|
|
||||||
var result strings.Builder
|
result := ""
|
||||||
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 = "Unknown status"
|
||||||
}
|
}
|
||||||
if p.value != 0x30 {
|
if p.value != 0x30 {
|
||||||
result.WriteString(statusMsg + "; ")
|
result += fmt.Sprintf("Status: %s; ", statusMsg)
|
||||||
|
}
|
||||||
|
if p.pos == 4 && p.value == 0x38 {
|
||||||
|
return result, fmt.Errorf("Card well empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
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 {
|
||||||
return fmt.Errorf("unexpected response status: % X", statusResp)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,130 +171,47 @@ func InitializeDispenser() (*serial.Port, error) {
|
|||||||
return port, nil
|
return port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DispenserPrepare(port *serial.Port) (string, error) {
|
func DispenserSequence(port *serial.Port) (string, error) {
|
||||||
const funcName = "dispenserSequence"
|
const funcName = "dispenserSequence"
|
||||||
stockStatus := ""
|
var result string
|
||||||
|
|
||||||
// Check dispenser status
|
// Check dispenser status
|
||||||
status, err := CheckDispenserStatus(port)
|
status, err := CheckDispenserStatus(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stockStatus, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err)
|
return status, 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
|
||||||
err = CardToEncoderPosition(port)
|
status, err = CardToEncoderPosition(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stockStatus, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
|
return status, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err)
|
||||||
}
|
}
|
||||||
|
result += "; " + status
|
||||||
|
|
||||||
time.Sleep(delay)
|
return result, nil
|
||||||
// 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) ([]byte, error) {
|
func CheckDispenserStatus(port *serial.Port) (string, 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 nil, fmt.Errorf("error sending check command: %v", err)
|
return "", fmt.Errorf("error sending check command: %v", err)
|
||||||
}
|
}
|
||||||
if len(statusResp) == 0 {
|
if len(statusResp) == 0 {
|
||||||
return nil, fmt.Errorf("no response from dispenser")
|
return "", fmt.Errorf("no response from dispenser")
|
||||||
}
|
}
|
||||||
err = checkACK(statusResp)
|
status, err := checkStatus(statusResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return status, 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)
|
||||||
@ -338,15 +219,17 @@ func CheckDispenserStatus(port *serial.Port) ([]byte, error) {
|
|||||||
log.Errorf("error sending ENQ: %v", err)
|
log.Errorf("error sending ENQ: %v", err)
|
||||||
}
|
}
|
||||||
if len(statusResp) == 0 {
|
if len(statusResp) == 0 {
|
||||||
return nil, fmt.Errorf("no response from dispenser")
|
return "", fmt.Errorf("no response from dispenser")
|
||||||
}
|
}
|
||||||
if len(statusResp) < 13 {
|
status, err = checkStatus(statusResp)
|
||||||
return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
|
if err != nil {
|
||||||
|
return status, err
|
||||||
}
|
}
|
||||||
return statusResp[7:11], nil // Return status bytes
|
result += status
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CardToEncoderPosition(port *serial.Port) error {
|
func CardToEncoderPosition(port *serial.Port) (string, error) {
|
||||||
const funcName = "cartToEncoderPosition"
|
const funcName = "cartToEncoderPosition"
|
||||||
enq := append([]byte{ENQ}, Address...)
|
enq := append([]byte{ENQ}, Address...)
|
||||||
|
|
||||||
@ -355,22 +238,30 @@ func CardToEncoderPosition(port *serial.Port) 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 = checkACK(statusResp)
|
_, err = checkStatus(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) error {
|
func CardOutOfMouth(port *serial.Port) (string, error) {
|
||||||
const funcName = "CardOutOfMouth"
|
const funcName = "CardOutOfMouth"
|
||||||
enq := append([]byte{ENQ}, Address...)
|
enq := append([]byte{ENQ}, Address...)
|
||||||
|
|
||||||
@ -379,17 +270,25 @@ func CardOutOfMouth(port *serial.Port) 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 = checkACK(statusResp)
|
_, err = checkStatus(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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type preauthSpoolRecord struct {
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
CheckoutDate string `json:"checkoutDate"` // keep as received
|
|
||||||
Fields map[string]string `json:"fields"` // ChipDNA result.Fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *App) getDB(ctx context.Context) (*sql.DB, error) {
|
func (app *App) getDB(ctx context.Context) (*sql.DB, error) {
|
||||||
app.dbMu.Lock()
|
app.dbMu.Lock()
|
||||||
defer app.dbMu.Unlock()
|
defer app.dbMu.Unlock()
|
||||||
@ -52,6 +40,7 @@ func (app *App) getDB(ctx context.Context) (*sql.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional ping (InitMSSQL already pings, but this keeps semantics explicit)
|
||||||
pingCtx, cancel2 := context.WithTimeout(dialCtx, 1*time.Second)
|
pingCtx, cancel2 := context.WithTimeout(dialCtx, 1*time.Second)
|
||||||
defer cancel2()
|
defer cancel2()
|
||||||
|
|
||||||
@ -63,144 +52,3 @@ func (app *App) getDB(ctx context.Context) (*sql.DB, error) {
|
|||||||
app.db = dbConn
|
app.db = dbConn
|
||||||
return app.db, nil
|
return app.db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) spoolPath() string {
|
|
||||||
// keep it near logs; adjust if you prefer a dedicated dir
|
|
||||||
// ensure LogDir ends with separator in your config loader
|
|
||||||
return filepath.Join(app.cfg.LogDir, "preauth_spool.ndjson")
|
|
||||||
}
|
|
||||||
|
|
||||||
// persistPreauth tries DB first; if DB is down or insert fails, it spools to file.
|
|
||||||
// It never returns an error to the caller (so your HTTP flow stays simple),
|
|
||||||
// but it logs failures.
|
|
||||||
func (app *App) persistPreauth(ctx context.Context, fields map[string]string, checkoutDate string) {
|
|
||||||
// First, try DB (with your reconnect logic inside getDB)
|
|
||||||
dbConn, err := app.getDB(ctx)
|
|
||||||
if err == nil && dbConn != nil {
|
|
||||||
if err := db.InsertPreauth(ctx, dbConn, fields, checkoutDate); err == nil {
|
|
||||||
// opportunistic drain once DB is alive
|
|
||||||
go app.drainPreauthSpool(context.Background())
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
log.WithError(err).Warn("DB insert failed; will spool preauth")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithError(err).Warn("DB unavailable; will spool preauth")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: spool to file
|
|
||||||
rec := preauthSpoolRecord{
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
CheckoutDate: checkoutDate,
|
|
||||||
Fields: fields,
|
|
||||||
}
|
|
||||||
if spErr := app.spoolPreauth(rec); spErr != nil {
|
|
||||||
log.WithError(spErr).Error("failed to spool preauth")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append one line JSON (NDJSON)
|
|
||||||
func (app *App) spoolPreauth(rec preauthSpoolRecord) error {
|
|
||||||
p := app.spoolPath()
|
|
||||||
|
|
||||||
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open spool file: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
b, err := json.Marshal(rec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshal spool record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := f.Write(append(b, '\n')); err != nil {
|
|
||||||
return fmt.Errorf("write spool record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Sync() // ensure it's on disk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drain spool into DB.
|
|
||||||
// Strategy: read all lines, insert each; keep failures in a temp file; then replace original.
|
|
||||||
func (app *App) drainPreauthSpool(ctx context.Context) {
|
|
||||||
dbConn, err := app.getDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return // still down, nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
spool := app.spoolPath()
|
|
||||||
in, err := os.Open(spool)
|
|
||||||
if err != nil {
|
|
||||||
// no spool is fine
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
tmp := spool + ".tmp"
|
|
||||||
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("drain spool: open tmp failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
sc := bufio.NewScanner(in)
|
|
||||||
// allow long lines if receipts ever sneak in (shouldn't, but safe)
|
|
||||||
buf := make([]byte, 0, 64*1024)
|
|
||||||
sc.Buffer(buf, 2*1024*1024)
|
|
||||||
|
|
||||||
var (
|
|
||||||
okCount int
|
|
||||||
failCount int
|
|
||||||
)
|
|
||||||
|
|
||||||
for sc.Scan() {
|
|
||||||
line := sc.Bytes()
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var rec preauthSpoolRecord
|
|
||||||
if err := json.Unmarshal(line, &rec); err != nil {
|
|
||||||
// malformed line: keep it so we don't lose evidence
|
|
||||||
_, _ = out.Write(append(line, '\n'))
|
|
||||||
failCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt insert
|
|
||||||
if err := db.InsertPreauth(ctx, dbConn, rec.Fields, rec.CheckoutDate); err != nil {
|
|
||||||
// DB still flaky or data issue: keep it for later retry
|
|
||||||
_, _ = out.Write(append(line, '\n'))
|
|
||||||
failCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
okCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sc.Err(); err != nil {
|
|
||||||
log.WithError(err).Warn("drain spool: scanner error")
|
|
||||||
// best effort; do not replace spool
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = out.Sync()
|
|
||||||
|
|
||||||
// Replace original spool with temp (atomic on Windows is best-effort; still OK here)
|
|
||||||
_ = in.Close()
|
|
||||||
_ = out.Close()
|
|
||||||
|
|
||||||
if err := os.Rename(tmp, spool); err != nil {
|
|
||||||
log.WithError(err).Warn("drain spool: rename failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if okCount > 0 || failCount > 0 {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"inserted": okCount,
|
|
||||||
"remaining": failCount,
|
|
||||||
}).Info("preauth spool drained")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@ -16,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"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/db"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||||
@ -33,17 +33,15 @@ type App struct {
|
|||||||
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, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
func NewApp(dispPort *serial.Port, lockType, encoderAddress 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +50,6 @@ 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) {
|
||||||
@ -140,7 +137,14 @@ func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
|||||||
theResponse.Status = result.Status
|
theResponse.Status = result.Status
|
||||||
theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields)
|
theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields)
|
||||||
if save {
|
if save {
|
||||||
go app.persistPreauth(context.Background(), result.Fields, theRequest.CheckoutDate)
|
dbConn, err := app.getDB(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warn("DB unavailable; preauth not stored")
|
||||||
|
} else {
|
||||||
|
if err := db.InsertPreauth(r.Context(), dbConn, result.Fields, theRequest.CheckoutDate); err != nil {
|
||||||
|
log.WithError(err).Warn("Failed to store preauth in DB")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeTransactionResult(w, http.StatusOK, theResponse)
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||||
@ -281,10 +285,18 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.cardWellStatus, err = dispenser.DispenserStart(app.dispPort); err != nil {
|
// dispenser sequence
|
||||||
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
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())
|
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
|
||||||
@ -294,16 +306,18 @@ 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 app.cardWellStatus, err = dispenser.DispenserFinal(app.dispPort); err != nil {
|
if status, err := dispenser.CardOutOfMouth(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
|
||||||
@ -365,12 +379,3 @@ 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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("LockSequence: sending ENQ")
|
log.Infof("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("LockSequence: sending encoding command: %q", string(lock.command))
|
log.Infof("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)
|
||||||
}
|
}
|
||||||
|
|||||||
16
main.go
16
main.go
@ -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/errorhandlers"
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"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.1.0"
|
buildVersion = "1.0.30"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ 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)
|
||||||
@ -57,12 +56,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer dispHandle.Close()
|
defer dispHandle.Close()
|
||||||
|
|
||||||
cardWellStatus, err = dispenser.DispenserPrepare(dispHandle)
|
status, err := dispenser.CheckDispenserStatus(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())
|
||||||
}
|
}
|
||||||
fmt.Println(cardWellStatus)
|
}
|
||||||
|
log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test lock-server connection
|
// Test lock-server connection
|
||||||
@ -97,7 +101,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create App and wire routes
|
// Create App and wire routes
|
||||||
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, cardWellStatus, database, &config)
|
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, database, &config)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
app.RegisterRoutes(mux)
|
app.RegisterRoutes(mux)
|
||||||
|
|||||||
@ -2,16 +2,6 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
#### 1.1.0 - 26 January 2026
|
|
||||||
divided `/starttransaction` endpoint into two separate endpoints:
|
|
||||||
`/takepreauth` to request preauthorization payment
|
|
||||||
`/takepayment` to request taking payment
|
|
||||||
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
|
#### 1.0.30 - 09 January 2026
|
||||||
improved logging for preauth releaser
|
improved logging for preauth releaser
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user