diff --git a/lockserver/lockservercommon.go b/lockserver/lockservercommon.go index 994a4f6..c81ff08 100644 --- a/lockserver/lockservercommon.go +++ b/lockserver/lockservercommon.go @@ -15,6 +15,7 @@ import ( const ( AssaAbloy = "assaabloy" Omnitec = "omnitec" + Salto = "salto" ) type ( @@ -32,6 +33,11 @@ type ( encoderAddr string // Encoder unit address command []byte // Command to be sent to the lock server } + + SaltoLockServer struct { + encoderAddr string + command []byte + } ) func NewLockServer(lockType, encoderAddr string, fatalError func(error)) LockServer { @@ -40,6 +46,8 @@ func NewLockServer(lockType, encoderAddr string, fatalError func(error)) LockSer return &AssaLockServer{encoderAddr: encoderAddr} case Omnitec: return &OmniLockServer{encoderAddr: encoderAddr} + case Salto: + return &SaltoLockServer{encoderAddr: encoderAddr} default: fatalError(fmt.Errorf("unsupported LockType: %s; must be 'assaabloy' or 'omnitec'", lockType)) return nil // This line will never be reached, but is needed to satisfy the compiler diff --git a/lockserver/saltolockserver.go b/lockserver/saltolockserver.go new file mode 100644 index 0000000..5348365 --- /dev/null +++ b/lockserver/saltolockserver.go @@ -0,0 +1,103 @@ +package lockserver + +import ( + "fmt" + "net" + "time" + + // "strings" + + log "github.com/sirupsen/logrus" +) + +const ( + STX = 0x02 // Start of Text + ETX = 0x03 // End of Text + ENQ = 0x05 // Enquiry from host + ACK = 0x06 // Positive response + NAK = 0x15 // Negative response +) + +func calculateLRC(data []byte) byte { + lrc := byte(0x00) + for _, b := range data { + lrc ^= b + } + return lrc +} + +// simple join that avoids importing strings just for this. +func stringJoin(parts []string, sep string) string { + if len(parts) == 0 { + return "" + } + out := parts[0] + for _, p := range parts[1:] { + out += sep + p + } + return out +} + +// BuildCommand builds a Salto card‑issuance command in the form: +// STX|CN||E|| | | | | |||ETX| +// where and are hhmmDDMMYY, and LRC is the XOR of all bytes +func (lock *SaltoLockServer) BuildCommand(lockId string, checkIn, checkOut time.Time) error { + // format helper: hhmmDDMMYY + fmtStamp := func(t time.Time) string { + return fmt.Sprintf("%02d%02d%02d%02d%02d", + t.Hour(), t.Minute(), t.Day(), int(t.Month()), t.Year()%100) + } + + start := fmtStamp(checkIn) + expiry := fmtStamp(checkOut) + + // the 12 fields between STX and ETX + fields := []string{ + "CN", lock.encoderAddr, "E", lockId, "", "", "", "", "", "", start, expiry, + } + + body := "|" + stringJoin(fields, "|") + "|" // leading/trailing pipes, so the ETX ends the last field + + // wrap with STX/ETX + msg := append([]byte{STX}, []byte(body)...) + msg = append(msg, ETX) + + // compute LRC over everything *after* STX up to and including ETX + lrc := calculateLRC(msg[1:]) // skip STX + msg = append(msg, lrc) + + lock.command = msg + return nil +} + +// Checks heart beat of the Assa Abloy lock server and perform key encoding +func (lock *SaltoLockServer) LockSequence(conn net.Conn) error { + const funcName = "SaltoLockServer.LockSequence" + + // Step 1: ENQ to check availability + respStr, err := sendAndReceive(conn, []byte{ENQ}) + if err != nil { + return fmt.Errorf("[%s] failed sending ENQ: %w", funcName, err) + } + resp := []byte(respStr) + + if len(resp) != 1 || resp[0] != ACK { + return fmt.Errorf("[%s] expected ACK (0x06) after ENQ, got: %q (hex: % X)", funcName, respStr, resp) + } + + // Step 2: Send actual command + respStr, err = sendAndReceive(conn, lock.command) + if err != nil { + return fmt.Errorf("[%s] failed sending command: %w", funcName, err) + } + resp = []byte(respStr) + + if len(resp) == 1 && resp[0] == NAK { + return fmt.Errorf("[%s] command rejected by lock server (NAK)", funcName) + } + + log.Infof("Encoding response: %q", respStr) + return nil +} + + diff --git a/main.go b/main.go index 55668e1..3704485 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ import ( ) const ( - buildVersion = "1.0.0" + buildVersion = "1.0.1" serviceName = "hardlink" customLayout = "2006-01-02 15:04:05 -0700" transactionUrl = "http://127.0.0.1:18181/start-transaction/" @@ -46,10 +46,7 @@ type configRec struct { DispenserAdrr string `yaml:"dispensAddr"` PrinterName string `yaml:"printerName"` LogDir string `yaml:"logdir"` - dbport int `yaml:"dbport"` // Port for the database connection - dbname string `yaml:"dbname"` // Database name for the connection - dbuser string `yaml:"dbuser"` // User for the database connection - dbpassword string `yaml:"dbpassword"` // Password for the database connection + isPayment bool `yaml:"isPayment"` } // DoorCardRequest is the JSON payload for /issue-door-card. @@ -124,60 +121,62 @@ func main() { // } // defer db.Close() - startClient := func() (*exec.Cmd, error) { - cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe") - err := cmd.Start() - if err != nil { - return nil, fmt.Errorf("Failed to start ChipDnaClient: %v", err) - } - log.Infof("ChipDnaClient started with PID %d", cmd.Process.Pid) - return cmd, nil - } - - cmd, err := startClient() - if err != nil { - fatalError(err) - } - - // Restart loop - go func() { - for { - err := cmd.Wait() + if config.isPayment { + startClient := func() (*exec.Cmd, error) { + cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe") + err := cmd.Start() if err != nil { - log.Errorf("ChipDnaClient exited unexpectedly: %v", err) - time.Sleep(2 * time.Second) - cmd, err = startClient() - if err != nil { - log.Errorf("Restart failed: %v", err) - return - } - log.Info("ChipDnaClient restarted successfully") + return nil, fmt.Errorf("failed to start ChipDnaClient: %v", err) } + log.Infof("ChipDnaClient started with PID %d", cmd.Process.Pid) + return cmd, nil } - }() - // Handle shutdown signals - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sigs - log.Info("Shutting down...") - if cmd.Process != nil { - log.Info("Sending SIGTERM to ChipDnaClient...") - _ = cmd.Process.Signal(syscall.SIGTERM) - // wait up to 5s for graceful shutdown - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() - select { - case <-time.After(5 * time.Second): - log.Warn("ChipDnaClient did not exit in time, killing...") - _ = cmd.Process.Kill() - case err := <-done: - log.Infof("ChipDnaClient exited cleanly: %v", err) - } + cmd, err := startClient() + if err != nil { + fatalError(err) } - os.Exit(0) - }() + + // Restart loop + go func() { + for { + err := cmd.Wait() + if err != nil { + log.Errorf("ChipDnaClient exited unexpectedly: %v", err) + time.Sleep(2 * time.Second) + cmd, err = startClient() + if err != nil { + log.Errorf("Restart failed: %v", err) + return + } + log.Info("ChipDnaClient restarted successfully") + } + } + }() + + // Handle shutdown signals + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + log.Info("Shutting down...") + if cmd.Process != nil { + log.Info("Sending SIGTERM to ChipDnaClient...") + _ = cmd.Process.Signal(syscall.SIGTERM) + // wait up to 5s for graceful shutdown + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + select { + case <-time.After(5 * time.Second): + log.Warn("ChipDnaClient did not exit in time, killing...") + _ = cmd.Process.Kill() + case err := <-done: + log.Infof("ChipDnaClient exited cleanly: %v", err) + } + } + os.Exit(0) + }() + } // Create App and wire routes app := newApp(dispHandle, lockConn, config) diff --git a/release notes.md b/release notes.md index c2a5902..107cfad 100644 --- a/release notes.md +++ b/release notes.md @@ -2,7 +2,11 @@ builtVersion is a const in main.go -#### 1.0.0 - 30 Jun 2024 +#### 1.0.1 - 22 July 2024 +added salto lock server and implemented workflow for Salto + + +#### 1.0.0 - 30 June 2024 added creditcall payment method `/starttransaction` - API payment endpoint to start a transaction