tidy up the app

This commit is contained in:
yurii 2025-06-11 11:31:28 +01:00
parent 140c9a44cc
commit d9871e6a1c
6 changed files with 305 additions and 272 deletions

View File

@ -171,6 +171,26 @@ func InitializeDispenser() (*serial.Port, error) {
return port, nil 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 // if dispenser is not responding, I should repeat the command
func CheckDispenserStatus(port *serial.Port) (string, error) { func CheckDispenserStatus(port *serial.Port) (string, error) {

View File

@ -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
}

View File

@ -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|…|<ETX>"
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)
}

View File

@ -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
}

View File

@ -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|…|<ETX>"
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
}

31
main.go
View File

@ -223,35 +223,22 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
return return
} }
// build command
app.lockserver.BuildCommand(app.config.EncoderAddress, doorReq.RoomField, checkIn, checkOut)
// dispenser sequence // dispenser sequence
if status, err := dispenser.CheckDispenserStatus(app.dispPort); err != nil { if status, err := dispenser.DispenserSequence(app.dispPort); err != nil {
if status != "" { if status != "" {
logging.Error(serviceName, status, "Dispenser error", string(op), "", "", 0) logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0)
writeError(w, http.StatusServiceUnavailable, "Dispenser error: "+err.Error()) writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
} else { } else {
logging.Error(serviceName, err.Error(), "Dispenser error", string(op), "", "", 0) logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
writeError(w, http.StatusServiceUnavailable, err.Error()+"; check card stock") writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock")
} }
return return
} else { } else {
log.Info(status) log.Info(status)
} }
if status, err := dispenser.CardToEncoderPosition(app.dispPort); err != nil { // build lock server command
if status != "" { app.lockserver.BuildCommand(app.config.EncoderAddress, doorReq.RoomField, checkIn, checkOut)
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)
}
// lock server sequence // lock server sequence
err = app.lockserver.LockSequence(app.lockConn) 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) { func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
const op = logging.Op("printRoomTicket") const op = logging.Op("printRoomTicket")
log.Println("printRoomTicket called")
var roomDetails printer.RoomDetailsRec var roomDetails printer.RoomDetailsRec
// Allow CORS preflight if needed // Allow CORS preflight if needed
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions { if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
return return
} }
log.Println("printRoomTicket called")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
return return