hardlink/lockserver/saltolockserver.go

151 lines
3.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
}
// LockSequence performs the full ENQ/ACK handshake and command exchange
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
const timeout = 10 * time.Second
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("error awaiting ACK to ENQ: %w", err)
}
if b != ACK {
return fmt.Errorf("expected ACK after ENQ, got 0x%X", b)
}
// 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("error awaiting ACK to command: %w", err)
}
if b == NAK {
return fmt.Errorf("command rejected (NAK)")
} else if b != ACK {
return fmt.Errorf("expected ACK to command, got 0x%X", b)
}
// 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
}