package lockserver import ( "fmt" "net" "time" // "strings" 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 ) func calculateLRC(data []byte) byte { lrc := byte(0x00) for _, b := range data { lrc ^= b } return lrc } // simple join that avoids importing strings just for this. func stringJoin(parts []string, sep string) string { if len(parts) == 0 { return "" } out := parts[0] for _, p := range parts[1:] { out += sep + p } return out } // BuildCommand builds a Salto card‑issuance command in the form: // STX|CN||E|| | | | | |||ETX| // where and are hhmmDDMMYY, and LRC is the XOR of all bytes func (lock *SaltoLockServer) BuildCommand(lockId string, checkIn, checkOut time.Time) error { // format helper: hhmmDDMMYY 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) // the 12 fields between STX and ETX fields := []string{ "CN", lock.encoderAddr, "E", lockId, "", "", "", "", "", "", start, expiry, } body := "|" + stringJoin(fields, "|") + "|" // leading/trailing pipes, so the ETX ends the last field // wrap with STX/ETX msg := append([]byte{STX}, []byte(body)...) msg = append(msg, ETX) // compute LRC over everything *after* STX up to and including ETX lrc := calculateLRC(msg[1:]) // skip STX msg = append(msg, lrc) lock.command = msg return nil } // Checks heart beat of the Assa Abloy lock server and perform key encoding func (lock *SaltoLockServer) LockSequence(conn net.Conn) error { const funcName = "SaltoLockServer.LockSequence" // Step 1: ENQ to check availability respStr, err := sendAndReceive(conn, []byte{ENQ}) if err != nil { return fmt.Errorf("[%s] failed sending ENQ: %w", funcName, err) } resp := []byte(respStr) if len(resp) != 1 || resp[0] != ACK { return fmt.Errorf("[%s] expected ACK (0x06) after ENQ, got: %q (hex: % X)", funcName, respStr, resp) } // Step 2: Send actual command respStr, err = sendAndReceive(conn, lock.command) if err != nil { return fmt.Errorf("[%s] failed sending command: %w", funcName, err) } resp = []byte(respStr) if len(resp) == 1 && resp[0] == NAK { return fmt.Errorf("[%s] command rejected by lock server (NAK)", funcName) } log.Infof("Encoding response: %q", respStr) return nil }