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 { 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 }