From fdf698528295270971e05fcdc4ec116553903ad9 Mon Sep 17 00:00:00 2001 From: yurii Date: Mon, 9 Jun 2025 13:14:35 +0100 Subject: [PATCH] added lockserver interface and implemented workflow for Omnitec --- lockserver/lockserver.go | 232 ++++++++++++++++++++++++++++++--------- main.go | 60 +++++----- release notes.md | 7 ++ 3 files changed, 217 insertions(+), 82 deletions(-) diff --git a/lockserver/lockserver.go b/lockserver/lockserver.go index 4c2d83f..a57f605 100644 --- a/lockserver/lockserver.go +++ b/lockserver/lockserver.go @@ -2,22 +2,39 @@ package lockserver import ( "bufio" + // "io" "fmt" "net" "net/url" - - // "os" + "os" + "strconv" "strings" "time" log "github.com/sirupsen/logrus" ) -var ( - LockserverUrl string +const ( + AssaAbloy = "assaabloy" + Omnitec = "omnitec" ) -func InitializeServerConnection() (net.Conn, error) { +type ( + LockServer interface { + LockSequence(conn net.Conn) error + BuildCommand(encoderAddr, lockId string, checkIn, checkOut time.Time) error + } + + AssaLockServer struct { + command string + } + + OmniLockServer struct { + command []byte // Command to be sent to the lock server + } +) + +func InitializeServerConnection(LockserverUrl string) (net.Conn, error) { const funcName = "InitializeServerConnection" // Parse the URL to extract host and port parsedUrl, err := url.Parse(LockserverUrl) @@ -36,63 +53,170 @@ func InitializeServerConnection() (net.Conn, error) { return conn, nil } -func SendHeartbeatToServer(conn net.Conn) (string, error) { - const funcName = "SendHeartbeatToServer" - // Write the check command to the server - heartbeat := "CCC;EAHEARTBEAT;AM1;\r\n" - log.Printf("Sending headrbeat: %q", heartbeat) - _, err := conn.Write([]byte(heartbeat)) +// sendAndReceive sends a command to the lock server and waits for a response. +func sendAndReceive(conn net.Conn, command []byte) (string, error) { + const funcName = "SendAndReceive" + // Write the command to the connection + log.Printf("Sending command: %q", command) + _, err := conn.Write(command) + if err != nil { + return "", fmt.Errorf("failed to send command: %v", err) + } + + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + + buf := make([]byte, 128) + reader := bufio.NewReader(conn) + n, err := reader.Read(buf) + if err != nil { + return "", fmt.Errorf("error reading response: %v", err) + } + response := buf[:n] + return string(response), nil +} + +func parseAssaResponse(raw string) (string, error) { + clean := strings.ReplaceAll(raw, "\r\n", "") + idx := strings.Index(clean, "RC") + code := clean[idx+2 : idx+3] // Extract the response code + if code != "0" { + return "", fmt.Errorf("negative response code: %s", clean) + } + return "Success: " + clean, nil +} + +func parseOmniResponse(raw string) (string, error) { + clean := strings.Trim(raw, "\x02\x03") + idx := strings.Index(clean, "AS") + code := clean[idx+2 : idx+4] // Extract the response code + code = strings.ToLower(code) // Convert to lowercase for consistency + if code != "ok" { + return "", fmt.Errorf("negative response code: %s", clean) + } + return "Success: " + clean, nil +} + +func sendHeartbeatToServer(conn net.Conn) (string, error) { + const heartbeatRegister = "CCC;EAHEARTBEAT;AM1;\r\n" + + raw, err := sendAndReceive(conn, []byte(heartbeatRegister)) if err != nil { return "", fmt.Errorf("failed to send Heartbeat command: %v", err) } - // set a read timeout to avoid indefinite blocking - conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - - // Read the response from the server. Visionline returns the response as ASCII text terminated by CRLF. - reader := bufio.NewReader(conn) - response, err := reader.ReadString('\n') - if err != nil { - return "", fmt.Errorf("error reading response: %v", err) - } - - substr := "RC" - response = strings.ReplaceAll(response, "\r\n", "") // Remove CRLF from the response - num := strings.Index(response, substr) // Find the index of the response code - responseCode := response[num+2 : len(response)-1] // Extract the result code from the response - if responseCode != "0" { - return "", fmt.Errorf("negative Heartbeat response code: %s", responseCode) - } - - return "Success: " + response, nil + return parseAssaResponse(raw) } -func RequestEncoding(conn net.Conn, command string) (string, error) { - const funcName = "RequestEncoding" - // Write the command to the connection - log.Printf("Sending Encoding request: %q", command) - _, err := conn.Write([]byte(command)) +func requestEncoding(conn net.Conn, command string) (string, error) { + // 1) Send and read raw response + raw, err := sendAndReceive(conn, []byte(command)) if err != nil { return "", fmt.Errorf("failed to send Encoding request: %v", err) } - // Optional: set a read timeout to avoid indefinite blocking - conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - - // Read the response from the server. Visionline returns the response as ASCII text terminated by CRLF. - reader := bufio.NewReader(conn) - response, err := reader.ReadString('\n') - if err != nil { - return "", fmt.Errorf("error reading response: %v", err) - } - - substr := "RC" - response = strings.ReplaceAll(response, "\r\n", "") // Remove CRLF from the response - num := strings.Index(response, substr) // Find the index of the response code - responseCode := response[num+2 : len(response)-1] // Extract the result code from the response - if responseCode != "0" { - return "", fmt.Errorf("negative lock server response code: %s", responseCode) - } - - return "Success: " + response, nil + return parseAssaResponse(raw) +} + +func (lock *AssaLockServer) LockSequence(conn net.Conn) error { + resp, err := sendHeartbeatToServer(conn) + if err != nil { + return fmt.Errorf("lock server heartbeat failed: %v", err) + } + log.Infof("Heartbeat response: %s", resp) + + resp, err = requestEncoding(conn, lock.command) + if err != nil { + return fmt.Errorf("lock server request encoding failed: %v", err) + } + log.Infof("Encoding response: %s", resp) + return nil +} + +func (lock *AssaLockServer) BuildCommand(encoderAddr, lockId string, checkIn, checkOut time.Time) error { + ci := checkIn.Format("200601021504") + co := checkOut.Format("200601021504") + + lock.command = fmt.Sprintf("CCA;EA%s;GR%s;CO%s;CI%s;AM1;\r\n", encoderAddr, lockId, co, ci) + return nil +} + +func (lock *OmniLockServer) BuildCommand(encoderAddr, lockId string, checkIn, checkOut time.Time) error { + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("could not get hostname: %v", err) + } + + // Format lockId as 4-digit zero-padded string + idInt, err := strconv.Atoi(lockId) + if err != nil { + return fmt.Errorf("invalid lockId %q: %v", lockId, err) + } + formattedLockId := fmt.Sprintf("%04d", idInt) + + // Format date/time parts + ga := checkIn.Format("020106") // GA = ddMMyy + gd := checkOut.Format("020106") // GD = ddMMyy + dt := checkIn.Format("15:04") // DT = HH:mm + ti := checkIn.Format("150405") // TI = HHmmss + + // Construct payload + payload := fmt.Sprintf( + "KR|KC%s|KTD|RN%s|%s|DT%s|G#75|GA%s|GD%s|KO0000|DA%s|TI%s|", + encoderAddr, + formattedLockId, + hostname, + dt, + ga, + gd, + ga, + ti, + ) + + // Assign to command field with STX and ETX + lock.command = append([]byte{0x02}, append([]byte(payload), 0x03)...) + + return nil +} + +func (lock *OmniLockServer) LockSequence(conn net.Conn) error { + const funcName = "OmniLockServer.LockSequence" + // Start the link with the lock server + raw, err := lock.linkStart(conn) + if err != nil { + return fmt.Errorf("[%s] linkStart failed: %v", funcName, err) + } + log.Infof("Link start response: %s", raw) + + // Request encoding from the lock server + raw, err = lock.requestEncoding(conn) + if err != nil { + return fmt.Errorf("[%s] requestEncoding failed: %v", funcName, err) + } + log.Infof("Encoding response: %s", raw) + + return nil +} + +func (lock *OmniLockServer) linkStart(conn net.Conn) (string, error) { + const funcName = "OmniLockServer.linkStart" + // Send the link start command + payload := fmt.Sprintf("LS|DA%s|TI%s|", time.Now().Format("150405"), time.Now().Format("150405")) + command := append([]byte{0x02}, append([]byte(payload), 0x03)...) + + raw, err := sendAndReceive(conn, command) + if err != nil { + return "", fmt.Errorf("failed to send Link Start command: %v", err) + } + return raw, nil +} + +func (lock *OmniLockServer) requestEncoding(conn net.Conn) (string, error) { + const funcName = "OmniLockServer.requestEncoding" + // Send the encoding request command + raw, err := sendAndReceive(conn, lock.command) + if err != nil { + return "", fmt.Errorf("failed to send Encoding request: %v", err) + } + + return parseOmniResponse(raw) } diff --git a/main.go b/main.go index 94794e5..adf1b47 100644 --- a/main.go +++ b/main.go @@ -24,8 +24,8 @@ import ( ) const ( - buildVersion = "0.9.0" - serviceName = "accesspoint" + buildVersion = "0.9.1" + serviceName = "hardlink" customLayout = "2006-01-02 15:04:05 -0700" ) @@ -33,6 +33,7 @@ const ( type configRec struct { Port int `yaml:"port"` LockserverUrl string `yaml:"lockservUrl"` + LockType string `yaml:"lockType"` EncoderAddress string `yaml:"encoderAddr"` DispenserPort string `yaml:"dispensPort"` DispenserAdrr string `yaml:"dispensAddr"` @@ -50,9 +51,10 @@ type DoorCardRequest struct { // App holds shared resources. type App struct { - dispPort *serial.Port - lockConn net.Conn - config configRec + dispPort *serial.Port + lockConn net.Conn + config configRec + lockserver lockserver.LockServer } func main() { @@ -91,20 +93,28 @@ func main() { log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status) // Initialize lock-server connection once - lockserver.LockserverUrl = config.LockserverUrl - lockConn, err := lockserver.InitializeServerConnection() + lockConn, err := lockserver.InitializeServerConnection(config.LockserverUrl) if err != nil { fatalError(err) } defer lockConn.Close() - log.Infof("Connected to lock server at %s", config.LockserverUrl) // Create App and wire routes app := &App{ dispPort: dispHandle, - lockConn: lockConn, - config: config, + lockConn: lockConn, + config: config, + } + + switch config.LockType { + case lockserver.AssaAbloy: + app.lockserver = &lockserver.AssaLockServer{} + case lockserver.Omnitec: + app.lockserver = &lockserver.OmniLockServer{} + default: + err = fmt.Errorf("unsupported LockType: %s; must be 'assaabloy' or 'omnitec'", config.LockType) + fatalError(err) } mux := http.NewServeMux() @@ -171,7 +181,6 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { theResponse cmstypes.StatusRec ) - log.Println("issueDoorCard called") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") @@ -182,6 +191,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { return } + log.Println("issueDoorCard called") if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") return @@ -214,11 +224,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { } // build command - ci := checkIn.Format("200601021504") - co := checkOut.Format("200601021504") - cmd := fmt.Sprintf("CCA;EA%s;GR%s;CO%s;CI%s;AM1;\r\n", - app.config.EncoderAddress, doorReq.RoomField, co, ci, - ) + app.lockserver.BuildCommand(app.config.EncoderAddress, doorReq.RoomField, checkIn, checkOut) // dispenser sequence if status, err := dispenser.CheckDispenserStatus(app.dispPort); err != nil { @@ -248,22 +254,13 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { } // lock server sequence - resp, err := lockserver.SendHeartbeatToServer(app.lockConn) + err = app.lockserver.LockSequence(app.lockConn) if err != nil { - logging.Error(serviceName, err.Error(), "Lock server heartbeat error", string(op), "", "", 0) - writeError(w, http.StatusBadGateway, "Lock server heartbeat failed: "+err.Error()) + logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0) + writeError(w, http.StatusBadGateway, err.Error()) dispenser.CardOutOfMouth(app.dispPort) return } - log.Infof("Heartbeat response: %s", resp) - resp, err = lockserver.RequestEncoding(app.lockConn, cmd) - if err != nil { - logging.Error(serviceName, err.Error(), "Lock server encoding error", string(op), "", "", 0) - writeError(w, http.StatusBadGateway, "Lock server encoding failed: "+err.Error()) - dispenser.CardOutOfMouth(app.dispPort) - return - } - log.Infof("Lock server response: %s", resp) // final dispenser steps if status, err := dispenser.CardOutOfMouth(app.dispPort); err != nil { @@ -351,6 +348,13 @@ func readConfig() configRec { if cfg.Port == 0 { cfg.Port = defaultPort } + + if cfg.LockType == "" { + err = fmt.Errorf("LockType is required in %s", configName) + fatalError(err) + } + cfg.LockType = strings.ToLower(cfg.LockType) + if cfg.LogDir == "" { cfg.LogDir = "./logs" + sep } else if !strings.HasSuffix(cfg.LogDir, sep) { diff --git a/release notes.md b/release notes.md index 1e7eea6..0919f17 100644 --- a/release notes.md +++ b/release notes.md @@ -1,3 +1,10 @@ +## Release notes + +builtVersion is a const in main.go + +#### 0.9.1 - 22 May 2024 +added lockserver interface and implemented workflow for Omnitec + #### 0.9.0 - 22 May 2024 The new API has two new endpoints: - `/issuedoorcard` - encoding the door card for the room.