package lockserver import ( "bufio" "fmt" "net" "net/url" "strings" "time" log "github.com/sirupsen/logrus" ) const ( kabaSTX = 0x02 kabaETX = 0x03 kabaACK = 0x06 kabaNAK = 0x15 ) // BuildCommand builds a key encoding request command for the dormakaba/Kaba lock server. // KR|KTD|WS192.168.135.20|KC2|RN41|KO000000|GA241213|TI16:56|GD241214|DT11:00|G#75| func (lock *KabaLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, checkOut time.Time) error { const funcName = "DormakabaLockServer.BuildCommand" room := strings.TrimSpace(doorReq.RoomField) if room == "" { return fmt.Errorf("[%s] roomField is required", funcName) } if checkIn.IsZero() { return fmt.Errorf("[%s] checkin time is required", funcName) } if checkOut.IsZero() { return fmt.Errorf("[%s] checkout time is required", funcName) } ws := dormakabaWorkstationID() ga := checkIn.Format("060102") // yyMMdd, example: 241213 gd := checkOut.Format("060102") // yyMMdd, example: 241214 ti := checkIn.Format("15:04") // HH:mm, example: 16:56 dt := checkOut.Format("15:04") // HH:mm, example: 11:00 payload := fmt.Sprintf( "KR|KTD|WS%s|KC%s|RN%s|KO000000|GA%s|TI%s|GD%s|DT%s|G#75|", ws, lock.encoderAddr, room, ga, ti, gd, dt, ) lock.command = wrapKabaFrame(payload) return nil } // LockSequence starts the link and performs key encoding. func (lock *KabaLockServer) LockSequence() error { const funcName = "KabaLockServer.LockSequence" conn, err := InitializeServerConnection(LockServerURL) if err != nil { return err } defer conn.Close() reader := bufio.NewReader(conn) regs, err := lock.linkStart(conn, reader) if err != nil { return fmt.Errorf("[%s] linkStart failed: %v", funcName, err) } for _, reg := range regs { log.Printf("Received: %q", reg) } raw, err := lock.requestEncoding(conn, reader) if err != nil { return fmt.Errorf("[%s] request encoding failed: %v", funcName, err) } log.Infof("Encoding response: %s", raw) return nil } // linkStart sends the dormakaba/Kaba LS command. // LS|DA241213|TI165607|WS192.168.135.20|PW1234| func (lock *KabaLockServer) linkStart(conn net.Conn, reader *bufio.Reader) ([]string, error) { ws := dormakabaWorkstationID() pw := dormakabaPassword() payload := fmt.Sprintf( "LS|DA%s|TI%s|WS%s|PW%s|", time.Now().Format("060102"), // yyMMdd time.Now().Format("150405"), // HHmmss ws, pw, ) command := wrapKabaFrame(payload) log.Printf("Sending Link Start command: %q", command) if _, err := conn.Write(command); err != nil { return nil, fmt.Errorf("failed to send Link Start command: %v", err) } var registers []string timeout := 10 * time.Second for { conn.SetReadDeadline(time.Now().Add(timeout)) b, err := reader.ReadByte() if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if len(registers) > 0 { return registers, nil } } return nil, fmt.Errorf("error reading Link Start response: %v", err) } switch b { case kabaACK: registers = append(registers, "ACK") continue case kabaNAK: return registers, fmt.Errorf("received NAK after Link Start") case kabaSTX: frame, err := readKabaFrame(conn, reader, b, timeout) if err != nil { return registers, fmt.Errorf("failed to read Link Start frame: %v", err) } frameText := string(frame) registers = append(registers, frameText) clean := cleanKabaFrame(frameText) if strings.HasPrefix(clean, "LA|") { return registers, nil } default: log.Warnf("Ignoring unexpected byte during Link Start: 0x%X", b) } } } func (lock *KabaLockServer) requestEncoding(conn net.Conn, reader *bufio.Reader) (string, error) { log.Printf("Sending Encoding command: %q", lock.command) if _, err := conn.Write(lock.command); err != nil { return "", fmt.Errorf("failed to send Encoding command: %v", err) } deadline := time.Now().Add(60 * time.Second) for { remaining := time.Until(deadline) if remaining <= 0 { return "", fmt.Errorf("timeout waiting for dormakaba encoding response") } conn.SetReadDeadline(time.Now().Add(remaining)) b, err := reader.ReadByte() if err != nil { return "", fmt.Errorf("error reading encoding response: %v", err) } switch b { case kabaACK: log.Debug("Received ACK after Encoding command") continue case kabaNAK: return "", fmt.Errorf("received NAK after Encoding command") case kabaSTX: frame, err := readKabaFrame(conn, reader, b, 60*time.Second) if err != nil { return "", fmt.Errorf("failed to read encoding response frame: %v", err) } raw := string(frame) clean := cleanKabaFrame(raw) log.Printf("Received Encoding frame: %q", clean) if strings.HasPrefix(clean, "KA|") { return parseDormakabaEncodingResponse(clean) } log.Warnf("Ignoring non-KA frame while waiting for encoding result: %q", clean) default: log.Warnf("Ignoring unexpected byte while waiting for encoding response: 0x%X", b) } } } func parseDormakabaEncodingResponse(clean string) (string, error) { if strings.Contains(clean, "|ASOK|") { return "Success: " + clean, nil } if strings.Contains(clean, "|AS") { return "", fmt.Errorf("negative dormakaba response: %s", clean) } return "", fmt.Errorf("unexpected dormakaba response: %s", clean) } func readKabaFrame(conn net.Conn, reader *bufio.Reader, firstByte byte, timeout time.Duration) ([]byte, error) { frame := []byte{firstByte} for { conn.SetReadDeadline(time.Now().Add(timeout)) b, err := reader.ReadByte() if err != nil { return frame, fmt.Errorf("error reading frame body: %w", err) } frame = append(frame, b) if b == kabaETX { return frame, nil } } } func wrapKabaFrame(payload string) []byte { command := make([]byte, 0, len(payload)+2) command = append(command, kabaSTX) command = append(command, []byte(payload)...) command = append(command, kabaETX) return command } func cleanKabaFrame(raw string) string { return strings.Trim(raw, string([]byte{kabaSTX, kabaETX})) } func dormakabaPassword() string { if strings.TrimSpace(Cert) != "" { return strings.TrimSpace(Cert) } return "1234" } func dormakabaWorkstationID() string { parsed, err := url.Parse(LockServerURL) if err == nil && parsed.Host != "" { host := parsed.Host if h, _, splitErr := net.SplitHostPort(host); splitErr == nil { return h } return strings.Trim(host, "/") } raw := strings.TrimSpace(LockServerURL) raw = strings.TrimPrefix(raw, "http://") raw = strings.TrimPrefix(raw, "https://") raw = strings.Trim(raw, "/") if h, _, splitErr := net.SplitHostPort(raw); splitErr == nil { return h } if idx := strings.Index(raw, ":"); idx >= 0 { return raw[:idx] } return raw }