hardlink/lockserver/saltolockserver.go

172 lines
4.0 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 {
log.Infof("Sending command: %q", string(lock.command))
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 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. Expect STX
conn.SetReadDeadline(time.Now().Add(timeout))
stx, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("error reading STX: %w", err)
}
if stx != STX {
return fmt.Errorf("expected STX, got 0x%X", stx)
}
conn.SetReadDeadline(time.Now().Add(timeout))
_ , err = reader.ReadByte()
if err != nil {
return fmt.Errorf("error reading separator after STX: %w", err)
}
// 6. Read next two bytes and check for TD error
conn.SetReadDeadline(time.Now().Add(timeout))
b1, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("error reading first response byte: %w", err)
}
b2, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("error reading second response byte: %w", err)
}
if b1 == 'T' && b2 == 'D' {
return fmt.Errorf("lock response indicates room does not exist (TD)")
}
// 7. Continue reading until ETX
resp := []byte{b1, b2}
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
}
}
// 8. Optional: read LRC or trailing byte
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: %q", string(resp))
return nil
}