hardlink/lockserver/saltolockserver.go

195 lines
4.7 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
var (
err error
resp []byte
)
reader := bufio.NewReader(conn)
// 1. Send ENQ
if _, e := conn.Write([]byte{ENQ}); e != nil {
return fmt.Errorf("failed to send ENQ: %w", e)
}
// 2. Expect ACK
conn.SetReadDeadline(time.Now().Add(timeout))
if b, e := reader.ReadByte(); e != nil {
return fmt.Errorf("error awaiting ACK to ENQ: %w", e)
} else if b != ACK {
return fmt.Errorf("expected ACK after ENQ, got 0x%X", b)
}
// 3. Send command frame
if _, e := conn.Write(lock.command); e != nil {
return fmt.Errorf("failed to send command frame: %w", e)
}
// 4. Expect ACK to command
conn.SetReadDeadline(time.Now().Add(timeout))
if b, e := reader.ReadByte(); e != nil {
return fmt.Errorf("error awaiting ACK to command: %w", e)
} else 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, e := reader.ReadByte()
if e != nil {
return fmt.Errorf("error reading STX: %w", e)
}
resp = append(resp, stx)
if stx != STX {
err = fmt.Errorf("expected STX, got 0x%X", stx)
}
// 6. Read separator after STX
conn.SetReadDeadline(time.Now().Add(timeout))
if sep, e := reader.ReadByte(); e == nil {
resp = append(resp, sep)
} else if err == nil {
err = fmt.Errorf("error reading separator after STX: %w", e)
}
// 7. Read command code (e.g., CN, TD)
conn.SetReadDeadline(time.Now().Add(timeout))
b1, e1 := reader.ReadByte()
if e1 == nil {
resp = append(resp, b1)
} else if err == nil {
err = fmt.Errorf("error reading first response byte: %w", e1)
}
b2, e2 := reader.ReadByte()
if e2 == nil {
resp = append(resp, b2)
} else if err == nil {
err = fmt.Errorf("error reading second response byte: %w", e2)
}
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)")
if err == nil {
err = fmt.Errorf("lock response indicates room does not exist")
}
default:
log.Warnf("LockSequence: unexpected command response %q", string(resp))
if err == nil {
err = fmt.Errorf("error encoding keycard, unexpected response")
}
}
// 8. Read rest of message until ETX
for {
conn.SetReadDeadline(time.Now().Add(timeout))
c, e := reader.ReadByte()
if e != nil {
if err == nil {
err = fmt.Errorf("error reading response body: %w", e)
}
break
}
resp = append(resp, c)
if c == ETX {
break
}
}
// 9. Optional: read trailing byte (LRC or CR)
conn.SetReadDeadline(time.Now().Add(timeout))
if lrc, e := reader.ReadByte(); e == nil {
resp = append(resp, lrc)
} else {
log.Warnf("LockSequence: failed to read trailing LRC/CR: %v", e)
}
log.Infof("LockSequence: received response: %q", string(resp))
return err
}