291 lines
6.6 KiB
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
|
|
} |