diff --git a/dispenser/dispenser.go b/dispenser/dispenser.go index c70d162..b03792b 100644 --- a/dispenser/dispenser.go +++ b/dispenser/dispenser.go @@ -171,6 +171,26 @@ func InitializeDispenser() (*serial.Port, error) { return port, nil } +func DispenserSequence(port *serial.Port) (string, error) { + const funcName = "dispenserSequence" + var result string + + // Check dispenser status + status, err := CheckDispenserStatus(port) + if err != nil { + return status, fmt.Errorf("[%s] error checking dispenser status: %v", funcName, err) + } + result += status + + // Send card to encoder position + status, err = CardToEncoderPosition(port) + if err != nil { + return status, fmt.Errorf("[%s] error sending card to encoder position: %v", funcName, err) + } + result += "; " + status + + return result, nil +} // if dispenser is not responding, I should repeat the command func CheckDispenserStatus(port *serial.Port) (string, error) { diff --git a/lockserver/assalockserver.go b/lockserver/assalockserver.go new file mode 100644 index 0000000..c8964ca --- /dev/null +++ b/lockserver/assalockserver.go @@ -0,0 +1,67 @@ +package lockserver + +import ( + "fmt" + "net" + "time" + "strings" + + log "github.com/sirupsen/logrus" +) + +// Build key encoding request command for the Assa Abloy lock server. +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 +} + +// Checks heart beat of the Assa Abloy lock server and perform key encoding +func (lock *AssaLockServer) LockSequence(conn net.Conn) error { + const funcName = "AssaLockServer.LockSequence" + resp, err := sendHeartbeatToServer(conn) + if err != nil { + return fmt.Errorf("[%s] heartbeat failed: %v", funcName, err) + } + log.Infof("Heartbeat response: %s", resp) + + resp, err = requestEncoding(conn, lock.command) + if err != nil { + return fmt.Errorf("[%s] request encoding failed: %v", funcName, err) + } + log.Infof("Encoding response: %s", resp) + return 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) + } + + return parseAssaResponse(raw) +} + +func requestEncoding(conn net.Conn, command string) (string, error) { + raw, err := sendAndReceive(conn, []byte(command)) + if err != nil { + return "", fmt.Errorf("failed to send Encoding request: %v", err) + } + + return parseAssaResponse(raw) +} + +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 +} + diff --git a/lockserver/lockserver.go b/lockserver/lockserver.go deleted file mode 100644 index d66a5e1..0000000 --- a/lockserver/lockserver.go +++ /dev/null @@ -1,250 +0,0 @@ -package lockserver - -import ( - "bufio" - // "io" - "fmt" - "net" - "net/url" - "os" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" -) - -const ( - AssaAbloy = "assaabloy" - Omnitec = "omnitec" -) - -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) - if err != nil { - return nil, fmt.Errorf("[%s] failed to parse LockserverUrl: %v", funcName, err) - } - - // Remove any leading/trailing slashes just in case - address := strings.Trim(parsedUrl.Host, "/") - - // Establish a TCP connection to the Visionline server - conn, err := net.Dial("tcp", address) - if err != nil { - return nil, fmt.Errorf("failed to connect to Visionline server: %v", err) - } - return conn, nil -} - -// 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) - } - - return parseAssaResponse(raw) -} - -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) - } - - 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 - dt := checkOut.Format("15:04") // DT = HH:mm - ga := checkIn.Format("060102") // GA = ddMMyy - gd := checkOut.Format("060102") // GD = ddMMyy - 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 - regs, err := lock.linkStart(conn) - if err != nil { - return fmt.Errorf("[%s] linkStart failed: %v", funcName, err) - } - for _, reg := range regs { - log.Printf("Received: %q", reg) - } - - // 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("060102"), - time.Now().Format("150405"), - ) - command := append([]byte{0x02}, append([]byte(payload), 0x03)...) - - log.Printf("Sending command: %q", command) - _, err := conn.Write(command) - if err != nil { - return nil, fmt.Errorf("failed to send command: %v", err) - } - - conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - reader := bufio.NewReader(conn) - - var registers []string - for { - reg, err := reader.ReadString(0x03) - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - break // timeout -> assume no more registers - } - return nil, fmt.Errorf("error reading register: %v", err) - } - - registers = append(registers, reg) - - // stop when you see the final register type, e.g. "LA|…|" - if strings.HasPrefix(reg, "\x02LA|") { - break - } - } - - return registers, 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/lockserver/lockservercommon.go b/lockserver/lockservercommon.go new file mode 100644 index 0000000..b554bae --- /dev/null +++ b/lockserver/lockservercommon.go @@ -0,0 +1,77 @@ +package lockserver + +import ( + "bufio" + // "io" + "fmt" + "net" + "net/url" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + AssaAbloy = "assaabloy" + Omnitec = "omnitec" +) + +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) + if err != nil { + return nil, fmt.Errorf("[%s] failed to parse LockserverUrl: %v", funcName, err) + } + + // Remove any leading/trailing slashes just in case + address := strings.Trim(parsedUrl.Host, "/") + + // Establish a TCP connection to the Visionline server + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to connect to Visionline server: %v", err) + } + return conn, nil +} + +// sendAndReceive sends a command to the lock server and waits for a response. +func sendAndReceive(conn net.Conn, command []byte) (string, error) { + 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 +} + + + + + diff --git a/lockserver/omnilockserver.go b/lockserver/omnilockserver.go new file mode 100644 index 0000000..ee6545f --- /dev/null +++ b/lockserver/omnilockserver.go @@ -0,0 +1,132 @@ +package lockserver + +import ( + "bufio" + "fmt" + "net" + "os" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +// Build key encoding request command for the Omnitec lock server. +func (lock *OmniLockServer) BuildCommand(encoderAddr, lockId string, checkIn, checkOut time.Time) error { + const funcName = "OmniLockServer.BuildCommand" + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("[%s] failed to get hostname: %v", funcName, err) + } + + // Format lockId as 4-digit zero-padded string + idInt, err := strconv.Atoi(lockId) + if err != nil { + return fmt.Errorf("[%s] failed to convert lockId to integer: %v", funcName, err) + } + formattedLockId := fmt.Sprintf("%04d", idInt) + + // Format date/time parts + dt := checkOut.Format("15:04") // DT = HH:mm + ga := checkIn.Format("060102") // GA = ddMMyy + gd := checkOut.Format("060102") // GD = ddMMyy + 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 +} + +// Starts link to the Omnitec lock server and perform key encoding +func (lock *OmniLockServer) LockSequence(conn net.Conn) error { + const funcName = "OmniLockServer.LockSequence" + // Start the link with the lock server + regs, err := lock.linkStart(conn) + if err != nil { + return fmt.Errorf("[%s] linkStart failed: %v", funcName, err) + } + for _, reg := range regs { + log.Printf("Received: %q", reg) + } + + // Request encoding from the lock server + raw, err := lock.requestEncoding(conn) + if err != nil { + return fmt.Errorf("[%s] request encoding failed: %v", funcName, err) + } + log.Infof("Encoding response: %s", raw) + + return nil +} + +func (lock *OmniLockServer) linkStart(conn net.Conn) ([]string, error) { + payload := fmt.Sprintf("LS|DA%s|TI%s|", + time.Now().Format("060102"), + time.Now().Format("150405"), + ) + command := append([]byte{0x02}, append([]byte(payload), 0x03)...) + + log.Printf("Sending command: %q", command) + _, err := conn.Write(command) + if err != nil { + return nil, fmt.Errorf("failed to send command: %v", err) + } + + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + reader := bufio.NewReader(conn) + + var registers []string + for { + reg, err := reader.ReadString(0x03) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + break // timeout -> assume no more registers + } + return nil, fmt.Errorf("error reading register: %v", err) + } + + registers = append(registers, reg) + + // stop when you see the final register type, e.g. "LA|…|" + if strings.HasPrefix(reg, "\x02LA|") { + break + } + } + + return registers, nil +} + +func (lock *OmniLockServer) requestEncoding(conn net.Conn) (string, error) { + raw, err := sendAndReceive(conn, lock.command) + if err != nil { + return "", fmt.Errorf("failed to send Encoding request: %v", err) + } + + return parseOmniResponse(raw) +} + +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 +} \ No newline at end of file diff --git a/main.go b/main.go index f130851..1422731 100644 --- a/main.go +++ b/main.go @@ -223,35 +223,22 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { return } - // build command - app.lockserver.BuildCommand(app.config.EncoderAddress, doorReq.RoomField, checkIn, checkOut) - // dispenser sequence - if status, err := dispenser.CheckDispenserStatus(app.dispPort); err != nil { + if status, err := dispenser.DispenserSequence(app.dispPort); err != nil { if status != "" { - logging.Error(serviceName, status, "Dispenser error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispenser error: "+err.Error()) + logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0) + writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()) } else { - logging.Error(serviceName, err.Error(), "Dispenser error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, err.Error()+"; check card stock") + logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0) + writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock") } return } else { log.Info(status) } - if status, err := dispenser.CardToEncoderPosition(app.dispPort); err != nil { - if status != "" { - logging.Error(serviceName, status, "Dispenser error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispenser move error: "+err.Error()) - } else { - logging.Error(serviceName, err.Error(), "Dispenser move error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispenser move error: "+err.Error()+"; check card stock") - } - return - } else { - log.Info(status) - } + // build lock server command + app.lockserver.BuildCommand(app.config.EncoderAddress, doorReq.RoomField, checkIn, checkOut) // lock server sequence err = app.lockserver.LockSequence(app.lockConn) @@ -281,17 +268,17 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) { const op = logging.Op("printRoomTicket") - log.Println("printRoomTicket called") var roomDetails printer.RoomDetailsRec // Allow CORS preflight if needed 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") - + if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } + log.Println("printRoomTicket called") if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") return