114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
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(doorReq DoorCardRequest, checkIn, checkOut time.Time) error {
|
||
// format helper: hhmmDDMMYY
|
||
var command string
|
||
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"
|
||
}
|
||
|
||
// the 12 fields between STX and ETX
|
||
fields := []string{
|
||
command, lock.encoderAddr, "E", doorReq.RoomField, "", "", "", "", "", "", 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
|
||
}
|
||
|
||
|