updated workflow for Salto locks

This commit is contained in:
yurii 2025-07-25 14:32:30 +01:00
parent 5ce9fdcf0b
commit fc91f2c0f2
3 changed files with 105 additions and 66 deletions

View File

@ -1,113 +1,150 @@
package lockserver
import (
"bufio"
"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
STX = 0x02 // Start of Text
ETX = 0x03 // End of Text
ENQ = 0x05 // Enquiry from host
ACK = 0x06 // Positive 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 {
lrc := byte(0x00)
var lrc byte
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 cardissuance 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
// BuildCommand assembles the SALTO frame with fields #0#10, STX/ETX and LRC
func (lock *SaltoLockServer) BuildCommand(req DoorCardRequest, checkIn, checkOut time.Time) error {
// helper: hh[mm]DDMMYY
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)
switch doorReq.FollowStr {
case "0":
command = "CN" // encode keycard
case "1":
command = "CC" // encode keycard copy
default:
command = "CN"
// command type
cmd := "CN"
if req.FollowStr == "1" {
cmd = "CC"
}
// the 12 fields between STX and ETX
// fields 010
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
msg := append([]byte{STX}, []byte(body)...)
msg = append(msg, ETX)
// append LRC (XOR of everything after STX through ETX)
lrc := calculateLRC(body[1:])
body = append(body, lrc)
// compute LRC over everything *after* STX up to and including ETX
lrc := calculateLRC(msg[1:]) // skip STX
msg = append(msg, lrc)
lock.command = msg
lock.command = body
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 {
const funcName = "SaltoLockServer.LockSequence"
const timeout = 10 * time.Second
// Step 1: ENQ to check availability
respStr, err := sendAndReceive(conn, []byte{ENQ})
reader := bufio.NewReader(conn)
// 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 {
return fmt.Errorf("[%s] failed sending ENQ: %w", funcName, err)
return fmt.Errorf("error awaiting ACK to ENQ: %w", 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)
if b != ACK {
return fmt.Errorf("expected ACK after ENQ, got 0x%X", b)
}
// Step 2: Send actual command
respStr, err = sendAndReceive(conn, lock.command)
// 3. Send the command frame
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 {
return fmt.Errorf("[%s] failed sending command: %w", funcName, err)
return fmt.Errorf("error awaiting ACK to command: %w", err)
}
resp = []byte(respStr)
if len(resp) == 1 && resp[0] == NAK {
return fmt.Errorf("[%s] command rejected by lock server (NAK)", funcName)
if b == NAK {
return fmt.Errorf("command rejected (NAK)")
} else if b != ACK {
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
}

View File

@ -30,7 +30,7 @@ import (
)
const (
buildVersion = "1.0.5"
buildVersion = "1.0.6"
serviceName = "hardlink"
customLayout = "2006-01-02 15:04:05 -0700"
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
@ -49,7 +49,6 @@ type configRec struct {
isPayment bool `yaml:"isPayment"`
}
// App holds shared resources.
type App struct {
dispPort *serial.Port

View File

@ -2,6 +2,9 @@
builtVersion is a const in main.go
#### 1.0.6 - 25 July 2024
updated workflow for Salto locks
#### 1.0.5 - 24 July 2024
added encoding keycard copy for Salto locks