updated workflow for Salto locks
This commit is contained in:
parent
5ce9fdcf0b
commit
fc91f2c0f2
@ -1,113 +1,150 @@
|
|||||||
package lockserver
|
package lockserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// "strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
STX = 0x02 // Start of Text
|
STX = 0x02 // Start of Text
|
||||||
ETX = 0x03 // End of Text
|
ETX = 0x03 // End of Text
|
||||||
ENQ = 0x05 // Enquiry from host
|
ENQ = 0x05 // Enquiry from host
|
||||||
ACK = 0x06 // Positive response
|
ACK = 0x06 // Positive response
|
||||||
NAK = 0x15 // Negative response
|
NAK = 0x15 // Negative response
|
||||||
|
separator = 0xB3 // '│' character in ASCII (179 decimal)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// calculateLRC computes the Longitudinal Redundancy Check over data
|
||||||
func calculateLRC(data []byte) byte {
|
func calculateLRC(data []byte) byte {
|
||||||
lrc := byte(0x00)
|
var lrc byte
|
||||||
for _, b := range data {
|
for _, b := range data {
|
||||||
lrc ^= b
|
lrc ^= b
|
||||||
}
|
}
|
||||||
return lrc
|
return lrc
|
||||||
}
|
}
|
||||||
|
|
||||||
// simple join that avoids importing strings just for this.
|
// BuildCommand assembles the SALTO frame with fields #0–#10, STX/ETX and LRC
|
||||||
func stringJoin(parts []string, sep string) string {
|
func (lock *SaltoLockServer) BuildCommand(req DoorCardRequest, checkIn, checkOut time.Time) error {
|
||||||
if len(parts) == 0 {
|
// helper: hh[mm]DDMMYY
|
||||||
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(doorReq DoorCardRequest, checkIn, checkOut time.Time) error {
|
|
||||||
// format helper: hhmmDDMMYY
|
|
||||||
var command string
|
|
||||||
fmtStamp := func(t time.Time) string {
|
fmtStamp := func(t time.Time) string {
|
||||||
return fmt.Sprintf("%02d%02d%02d%02d%02d",
|
return fmt.Sprintf("%02d%02d%02d%02d%02d",
|
||||||
t.Hour(), t.Minute(), t.Day(), int(t.Month()), t.Year()%100)
|
t.Hour(), t.Minute(), t.Day(), int(t.Month()), t.Year()%100)
|
||||||
}
|
}
|
||||||
|
|
||||||
start := fmtStamp(checkIn)
|
start := fmtStamp(checkIn)
|
||||||
expiry := fmtStamp(checkOut)
|
expiry := fmtStamp(checkOut)
|
||||||
|
|
||||||
switch doorReq.FollowStr {
|
// command type
|
||||||
case "0":
|
cmd := "CN"
|
||||||
command = "CN" // encode keycard
|
if req.FollowStr == "1" {
|
||||||
case "1":
|
cmd = "CC"
|
||||||
command = "CC" // encode keycard copy
|
|
||||||
default:
|
|
||||||
command = "CN"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the 12 fields between STX and ETX
|
// fields 0–10
|
||||||
fields := []string{
|
fields := []string{
|
||||||
command, lock.encoderAddr, "E", doorReq.RoomField, "", "", "", "", "", "", start, expiry,
|
cmd,
|
||||||
|
lock.encoderAddr,
|
||||||
|
"E",
|
||||||
|
req.RoomField,
|
||||||
|
"", // optional field 4
|
||||||
|
"", // 5
|
||||||
|
"", // 6
|
||||||
|
"", // 7
|
||||||
|
"", // 8
|
||||||
|
start,
|
||||||
|
expiry,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := "|" + stringJoin(fields, "|") + "|" // leading/trailing pipes, so the ETX ends the last field
|
// build payload between STX and ETX
|
||||||
|
body := []byte{STX}
|
||||||
|
for _, f := range fields {
|
||||||
|
body = append(body, separator)
|
||||||
|
body = append(body, []byte(f)...)
|
||||||
|
}
|
||||||
|
body = append(body, separator)
|
||||||
|
body = append(body, ETX)
|
||||||
|
|
||||||
// wrap with STX/ETX
|
// append LRC (XOR of everything after STX through ETX)
|
||||||
msg := append([]byte{STX}, []byte(body)...)
|
lrc := calculateLRC(body[1:])
|
||||||
msg = append(msg, ETX)
|
body = append(body, lrc)
|
||||||
|
|
||||||
// compute LRC over everything *after* STX up to and including ETX
|
lock.command = body
|
||||||
lrc := calculateLRC(msg[1:]) // skip STX
|
|
||||||
msg = append(msg, lrc)
|
|
||||||
|
|
||||||
lock.command = msg
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks heart beat of the Assa Abloy lock server and perform key encoding
|
// LockSequence performs the full ENQ/ACK handshake and command exchange
|
||||||
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
|
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
|
||||||
const funcName = "SaltoLockServer.LockSequence"
|
const timeout = 10 * time.Second
|
||||||
|
|
||||||
// Step 1: ENQ to check availability
|
reader := bufio.NewReader(conn)
|
||||||
respStr, err := sendAndReceive(conn, []byte{ENQ})
|
|
||||||
|
// 1. Send ENQ
|
||||||
|
if _, err := conn.Write([]byte{ENQ}); err != nil {
|
||||||
|
return fmt.Errorf("failed to send ENQ: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Expect ACK
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
b, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] failed sending ENQ: %w", funcName, err)
|
return fmt.Errorf("error awaiting ACK to ENQ: %w", err)
|
||||||
}
|
}
|
||||||
resp := []byte(respStr)
|
if b != ACK {
|
||||||
|
return fmt.Errorf("expected ACK after ENQ, got 0x%X", b)
|
||||||
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
|
// 3. Send the command frame
|
||||||
respStr, err = sendAndReceive(conn, lock.command)
|
if _, err := conn.Write(lock.command); err != nil {
|
||||||
|
return fmt.Errorf("failed to send command frame: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Expect ACK to command
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
b, err = reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] failed sending command: %w", funcName, err)
|
return fmt.Errorf("error awaiting ACK to command: %w", err)
|
||||||
}
|
}
|
||||||
resp = []byte(respStr)
|
if b == NAK {
|
||||||
|
return fmt.Errorf("command rejected (NAK)")
|
||||||
if len(resp) == 1 && resp[0] == NAK {
|
} else if b != ACK {
|
||||||
return fmt.Errorf("[%s] command rejected by lock server (NAK)", funcName)
|
return fmt.Errorf("expected ACK to command, got 0x%X", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Encoding response: %q", respStr)
|
// 5. Read response: expect STX
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
b, err = reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response STX: %w", err)
|
||||||
|
}
|
||||||
|
if b != STX {
|
||||||
|
return fmt.Errorf("expected STX at response start, got 0x%X", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Read until ETX
|
||||||
|
var resp []byte
|
||||||
|
for {
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
c, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response body: %w", err)
|
||||||
|
}
|
||||||
|
resp = append(resp, c)
|
||||||
|
if c == ETX {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. (Optional) Read LRC or final CR
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
if lrc, err := reader.ReadByte(); err == nil {
|
||||||
|
resp = append(resp, lrc)
|
||||||
|
} else {
|
||||||
|
log.Warnf("LockSequence: failed to read trailing LRC/CR: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("LockSequence: received response: % X", resp)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
3
main.go
3
main.go
@ -30,7 +30,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.0.5"
|
buildVersion = "1.0.6"
|
||||||
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/"
|
||||||
@ -49,7 +49,6 @@ type configRec struct {
|
|||||||
isPayment bool `yaml:"isPayment"`
|
isPayment bool `yaml:"isPayment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// App holds shared resources.
|
// App holds shared resources.
|
||||||
type App struct {
|
type App struct {
|
||||||
dispPort *serial.Port
|
dispPort *serial.Port
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### 1.0.6 - 25 July 2024
|
||||||
|
updated workflow for Salto locks
|
||||||
|
|
||||||
#### 1.0.5 - 24 July 2024
|
#### 1.0.5 - 24 July 2024
|
||||||
added encoding keycard copy for Salto locks
|
added encoding keycard copy for Salto locks
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user