updated Salto key encoding workflow
This commit is contained in:
parent
c8f6c57983
commit
b4d16f9021
@ -75,120 +75,145 @@ func (lock *SaltoLockServer) BuildCommand(req DoorCardRequest, checkIn, checkOut
|
|||||||
return nil
|
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
|
// LockSequence performs the full ENQ/ACK handshake and command exchange
|
||||||
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
|
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
|
||||||
log.Infof("Sending command: %q", string(lock.command))
|
|
||||||
|
|
||||||
const timeout = 10 * time.Second
|
const timeout = 10 * time.Second
|
||||||
var (
|
var (
|
||||||
err error
|
resp []byte
|
||||||
resp []byte
|
reader = bufio.NewReader(conn)
|
||||||
|
drained = 0 // count of stale frames consumed across waits
|
||||||
)
|
)
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
// 1. Send ENQ
|
// 1. Send ENQ
|
||||||
|
log.Infof("Sending ENQ")
|
||||||
if _, e := conn.Write([]byte{ENQ}); e != nil {
|
if _, e := conn.Write([]byte{ENQ}); e != nil {
|
||||||
return fmt.Errorf("failed to send ENQ: %w", e)
|
return fmt.Errorf("failed to send ENQ: %w", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Expect ACK
|
// 2. Expect ACK (but drain any queued STX frames first)
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
if e := lock.waitForAck(conn, reader, timeout, &drained); e != nil {
|
||||||
if b, e := reader.ReadByte(); e != nil {
|
|
||||||
return fmt.Errorf("error awaiting ACK to ENQ: %w", e)
|
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
|
// 3. Send command frame
|
||||||
|
log.Infof("Sending encoding command: %q", string(lock.command))
|
||||||
if _, e := conn.Write(lock.command); e != nil {
|
if _, e := conn.Write(lock.command); e != nil {
|
||||||
return fmt.Errorf("failed to send command frame: %w", e)
|
return fmt.Errorf("failed to send command frame: %w", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Expect ACK to command
|
// 4. Expect ACK to command (again drain any queued frames that might precede it)
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
if e := lock.waitForAck(conn, reader, timeout, &drained); e != nil {
|
||||||
if b, e := reader.ReadByte(); e != nil {
|
|
||||||
return fmt.Errorf("error awaiting ACK to command: %w", e)
|
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
|
// 5. Now read the *next* STX frame which should be the response to our command.
|
||||||
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 {
|
for {
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
conn.SetReadDeadline(time.Now().Add(20 * time.Second))
|
||||||
c, e := reader.ReadByte()
|
b, e := reader.ReadByte()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
if err == nil {
|
return fmt.Errorf("error reading response start: %w", e)
|
||||||
err = fmt.Errorf("error reading response body: %w", e)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
resp = append(resp, c)
|
if b != STX {
|
||||||
if c == ETX {
|
// If anything else arrives, it might be another control byte or noise;
|
||||||
break
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Optional: read trailing byte (LRC or CR)
|
// parse the command code within response frame
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
if len(resp) >= 4 {
|
||||||
if lrc, e := reader.ReadByte(); e == nil {
|
// The command bytes are usually at indices 2 and 3 if separator is at index 1
|
||||||
resp = append(resp, lrc)
|
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 {
|
} else {
|
||||||
log.Warnf("LockSequence: failed to read trailing LRC/CR: %v", e)
|
log.Warnf("LockSequence: response too short: %q", string(resp))
|
||||||
|
return fmt.Errorf("response too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("LockSequence: received response: %q", string(resp))
|
log.Infof("LockSequence: received response: %q", string(resp))
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
2
main.go
2
main.go
@ -30,7 +30,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.0.10"
|
buildVersion = "1.0.11"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
customLayout = "2006-01-02 15:04:05 -0700"
|
customLayout = "2006-01-02 15:04:05 -0700"
|
||||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### 1.0.11 - 11 August 2024
|
||||||
|
updated Salto key encoding workflow
|
||||||
|
|
||||||
#### 1.0.10 - 08 August 2024
|
#### 1.0.10 - 08 August 2024
|
||||||
updated logging for TLJ locks
|
updated logging for TLJ locks
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user