hardlink/lockserver/saltolockserver.go

220 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package lockserver
import (
"bufio"
"fmt"
"net"
"time"
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
separator = 0xB3 // '│' character in ASCII (179 decimal)
)
// calculateLRC computes the Longitudinal Redundancy Check over data
func calculateLRC(data []byte) byte {
var lrc byte
for _, b := range data {
lrc ^= b
}
return lrc
}
// 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)
// command type
cmd := "CN"
if req.FollowStr == "1" {
cmd = "CC"
}
// fields 010
fields := []string{
cmd,
lock.encoderAddr,
"E",
req.RoomField,
"", // optional field 4
"", // 5
"", // 6
"", // 7
"", // 8
start,
expiry,
}
// 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)
// append LRC (XOR of everything after STX through ETX)
lrc := calculateLRC(body[1:])
body = append(body, lrc)
lock.command = body
return nil
}
// readFrame consumes a full frame starting with firstByte (expected STX).
// It reads up to ETX and then attempts to read a trailing LRC (or CR). timeout controls read deadlines.
func (lock *SaltoLockServer) readFrame(conn net.Conn, reader *bufio.Reader, firstByte byte, timeout time.Duration) ([]byte, error) {
frame := []byte{firstByte}
for {
conn.SetReadDeadline(time.Now().Add(timeout))
b, e := reader.ReadByte()
if e != nil {
return frame, fmt.Errorf("error reading frame body: %w", e)
}
frame = append(frame, b)
if b == ETX {
break
}
}
// read trailing LRC (or CR) if present (non-blocking w/ timeout)
conn.SetReadDeadline(time.Now().Add(timeout))
if lrc, e := reader.ReadByte(); e == nil {
frame = append(frame, lrc)
} else {
// Not fatal: some devices might omit LRC/CR — just log
log.Warnf("readFrame: no trailing LRC/CR: %v", e)
}
return frame, nil
}
// waitForAck waits for ACK or NAK. If STX frames are encountered they are drained
// using readFrame. drainedCount (if non-nil) accumulates the number of drained frames.
func (lock *SaltoLockServer) waitForAck(conn net.Conn, reader *bufio.Reader, timeout time.Duration, drainedCount *int) error {
deadline := time.Now().Add(timeout)
for {
conn.SetReadDeadline(time.Now().Add(time.Until(deadline)))
b, e := reader.ReadByte()
if e != nil {
return fmt.Errorf("error waiting for ACK/NAK: %w", e)
}
switch b {
case ACK:
return nil
case NAK:
return fmt.Errorf("received NAK")
case STX:
// stale or queued full response: consume it and continue waiting
frame, fe := lock.readFrame(conn, reader, b, timeout)
if fe != nil {
// if we can't consume frame, consider it an error
return fmt.Errorf("failed to consume queued STX frame: %w", fe)
}
if drainedCount != nil {
*drainedCount++
log.Infof("Drained queued frame #%d (while waiting for ACK): %q", *drainedCount, string(frame))
} else {
log.Infof("Drained queued frame (while waiting for ACK): %q", string(frame))
}
// loop to keep waiting for ACK
default:
// Unexpected byte while waiting for ACK. Log and continue reading.
log.Warnf("waitForAck: unexpected byte 0x%X while waiting for ACK; ignoring", b)
// keep looping until timeout or ACK/NAK
}
}
}
// LockSequence performs the full ENQ/ACK handshake and command exchange
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
const timeout = 10 * time.Second
var (
resp []byte
reader = bufio.NewReader(conn)
drained = 0 // count of stale frames consumed across waits
)
// 1. Send ENQ
log.Infof("Sending ENQ")
if _, e := conn.Write([]byte{ENQ}); e != nil {
return fmt.Errorf("failed to send ENQ: %w", e)
}
// 2. Expect ACK (but drain any queued STX frames first)
if e := lock.waitForAck(conn, reader, timeout, &drained); e != nil {
return fmt.Errorf("error awaiting ACK to ENQ: %w", e)
}
// 3. Send command frame
log.Infof("Sending encoding command: %q", string(lock.command))
if _, e := conn.Write(lock.command); e != nil {
return fmt.Errorf("failed to send command frame: %w", e)
}
// 4. Expect ACK to command (again drain any queued frames that might precede it)
if e := lock.waitForAck(conn, reader, timeout, &drained); e != nil {
return fmt.Errorf("error awaiting ACK to command: %w", e)
}
// 5. Now read the *next* STX frame which should be the response to our command.
for {
conn.SetReadDeadline(time.Now().Add(20 * time.Second))
b, e := reader.ReadByte()
if e != nil {
return fmt.Errorf("error reading response start: %w", e)
}
if b != STX {
// If anything else arrives, it might be another control byte or noise;
// log and keep consuming until we find STX.
log.Warnf("expected STX to start response but got 0x%X; ignoring", b)
continue
}
// consume full response frame
frame, fe := lock.readFrame(conn, reader, b, timeout)
if fe != nil {
return fmt.Errorf("error reading response frame: %w", fe)
}
resp = append(resp, frame...)
break
}
// parse the command code within response frame
if len(resp) >= 4 {
// The command bytes are usually at indices 2 and 3 if separator is at index 1
sepIndex := 1
b1 := resp[sepIndex+1]
b2 := resp[sepIndex+2]
switch {
case b1 == 'C' && b2 == 'N':
log.Infof("LockSequence: command response is CN (normal)")
case b1 == 'C' && b2 == 'C':
log.Infof("LockSequence: command response is CC (follow-up)")
case b1 == 'T' && b2 == 'D':
log.Warnf("LockSequence: command response is TD (room does not exist)")
return fmt.Errorf("lock response indicates room does not exist")
default:
log.Warnf("LockSequence: unexpected command response %q", string(resp))
return fmt.Errorf("error encoding keycard, unexpected response")
}
} else {
log.Warnf("LockSequence: response too short: %q", string(resp))
return fmt.Errorf("response too short")
}
log.Infof("LockSequence: received response: %q", string(resp))
return nil
}