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 0–10 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 }