added salto lock server and implemented workflow for Salto
This commit is contained in:
parent
dc91a9ae63
commit
ea6b1225aa
@ -15,6 +15,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
AssaAbloy = "assaabloy"
|
AssaAbloy = "assaabloy"
|
||||||
Omnitec = "omnitec"
|
Omnitec = "omnitec"
|
||||||
|
Salto = "salto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -32,6 +33,11 @@ type (
|
|||||||
encoderAddr string // Encoder unit address
|
encoderAddr string // Encoder unit address
|
||||||
command []byte // Command to be sent to the lock server
|
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 {
|
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}
|
return &AssaLockServer{encoderAddr: encoderAddr}
|
||||||
case Omnitec:
|
case Omnitec:
|
||||||
return &OmniLockServer{encoderAddr: encoderAddr}
|
return &OmniLockServer{encoderAddr: encoderAddr}
|
||||||
|
case Salto:
|
||||||
|
return &SaltoLockServer{encoderAddr: encoderAddr}
|
||||||
default:
|
default:
|
||||||
fatalError(fmt.Errorf("unsupported LockType: %s; must be 'assaabloy' or 'omnitec'", lockType))
|
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
|
return nil // This line will never be reached, but is needed to satisfy the compiler
|
||||||
|
103
lockserver/saltolockserver.go
Normal file
103
lockserver/saltolockserver.go
Normal file
@ -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|<encoder addres>|E|<room>| | | | | |<start>|<expiry>|ETX|<LRC>
|
||||||
|
// where <start> and <expiry> 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
107
main.go
107
main.go
@ -30,7 +30,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.0.0"
|
buildVersion = "1.0.1"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
customLayout = "2006-01-02 15:04:05 -0700"
|
customLayout = "2006-01-02 15:04:05 -0700"
|
||||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||||
@ -46,10 +46,7 @@ type configRec struct {
|
|||||||
DispenserAdrr string `yaml:"dispensAddr"`
|
DispenserAdrr string `yaml:"dispensAddr"`
|
||||||
PrinterName string `yaml:"printerName"`
|
PrinterName string `yaml:"printerName"`
|
||||||
LogDir string `yaml:"logdir"`
|
LogDir string `yaml:"logdir"`
|
||||||
dbport int `yaml:"dbport"` // Port for the database connection
|
isPayment bool `yaml:"isPayment"`
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoorCardRequest is the JSON payload for /issue-door-card.
|
// DoorCardRequest is the JSON payload for /issue-door-card.
|
||||||
@ -124,60 +121,62 @@ func main() {
|
|||||||
// }
|
// }
|
||||||
// defer db.Close()
|
// defer db.Close()
|
||||||
|
|
||||||
startClient := func() (*exec.Cmd, error) {
|
if config.isPayment {
|
||||||
cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe")
|
startClient := func() (*exec.Cmd, error) {
|
||||||
err := cmd.Start()
|
cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe")
|
||||||
if err != nil {
|
err := cmd.Start()
|
||||||
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 err != nil {
|
if err != nil {
|
||||||
log.Errorf("ChipDnaClient exited unexpectedly: %v", err)
|
return nil, fmt.Errorf("failed to start ChipDnaClient: %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")
|
|
||||||
}
|
}
|
||||||
|
log.Infof("ChipDnaClient started with PID %d", cmd.Process.Pid)
|
||||||
|
return cmd, nil
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Handle shutdown signals
|
cmd, err := startClient()
|
||||||
sigs := make(chan os.Signal, 1)
|
if err != nil {
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
fatalError(err)
|
||||||
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)
|
|
||||||
}()
|
// 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
|
// Create App and wire routes
|
||||||
app := newApp(dispHandle, lockConn, config)
|
app := newApp(dispHandle, lockConn, config)
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
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
|
added creditcall payment method
|
||||||
`/starttransaction` - API payment endpoint to start a transaction
|
`/starttransaction` - API payment endpoint to start a transaction
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user