hardlink/internal/lockserver/dormakabalockserver.go

291 lines
6.6 KiB
Go

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
}