Compare commits
No commits in common. "ffd814e076d0b3f1bc078e5b65108593a8fc2d9a" and "af151fd3898c3344f23d820727e799a91f15d715" have entirely different histories.
ffd814e076
...
af151fd389
@ -5,7 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/db"
|
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OpenDB(cfg *config.ConfigRec) (*sql.DB, error) {
|
func OpenDB(cfg *config.ConfigRec) (*sql.DB, error) {
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// Package config handles reading and parsing configuration from config.yml.
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,15 +5,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigRec holds values from config.yml.
|
// configRec holds values from config.yml.
|
||||||
type ConfigRec struct {
|
type ConfigRec struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
LockserverURL string `yaml:"lockservUrl"`
|
LockserverUrl string `yaml:"lockservUrl"`
|
||||||
LockType string `yaml:"lockType"`
|
LockType string `yaml:"lockType"`
|
||||||
EncoderAddress string `yaml:"encoderAddr"`
|
EncoderAddress string `yaml:"encoderAddr"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
@ -33,7 +32,7 @@ type ConfigRec struct {
|
|||||||
SendErrorEmails []string `yaml:"senderroremails"`
|
SendErrorEmails []string `yaml:"senderroremails"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadHardlinkConfig reads config.yml and applies defaults.
|
// ReadConfig reads config.yml and applies defaults.
|
||||||
func ReadHardlinkConfig() ConfigRec {
|
func ReadHardlinkConfig() ConfigRec {
|
||||||
var cfg ConfigRec
|
var cfg ConfigRec
|
||||||
const configName = "config.yml"
|
const configName = "config.yml"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import (
|
|||||||
mssqldb "github.com/denisenkom/go-mssqldb" // for error inspection
|
mssqldb "github.com/denisenkom/go-mssqldb" // for error inspection
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitMSSQL opens and pings the SQL Server instance (keeps your original behaviour)
|
// InitMSSQL opens and pings the SQL Server instance (keeps your original behaviour)
|
||||||
@ -1,4 +1,6 @@
|
|||||||
// Package dispenser provides a queue-based client (single owner of port).
|
// --------------------
|
||||||
|
// Queue-based client (single owner of port)
|
||||||
|
// --------------------
|
||||||
package dispenser
|
package dispenser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -72,7 +74,7 @@ func (c *Client) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStatusTTL sets the duration for which cached status is considered fresh.
|
// Optional: tune cache TTL (how "fresh" cached status must be)
|
||||||
func (c *Client) SetStatusTTL(d time.Duration) {
|
func (c *Client) SetStatusTTL(d time.Duration) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.statusTTL = d
|
c.statusTTL = d
|
||||||
@ -231,7 +233,7 @@ func (c *Client) OutOfMouth(ctx context.Context) error {
|
|||||||
// Public sequences updated to use Client (queue)
|
// Public sequences updated to use Client (queue)
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
// DispenserPrepare checks status; if empty => ok; else ensure at encoder.
|
// DispenserPrepare: check status; if empty => ok; else ensure at encoder.
|
||||||
func (c *Client) DispenserPrepare(ctx context.Context) (string, error) {
|
func (c *Client) DispenserPrepare(ctx context.Context) (string, error) {
|
||||||
const funcName = "DispenserPrepare"
|
const funcName = "DispenserPrepare"
|
||||||
stockStatus := ""
|
stockStatus := ""
|
||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/db"
|
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,13 +13,13 @@ import (
|
|||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/dispenser"
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/lockserver"
|
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
|
"gitea.futuresens.co.uk/futuresens/hardlink/mail"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/payment"
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/printer"
|
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/payment"
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -9,11 +9,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/lockserver"
|
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
|
"gitea.futuresens.co.uk/futuresens/hardlink/mail"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/payment"
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -6,10 +6,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/bootstrap"
|
"gitea.futuresens.co.uk/futuresens/hardlink/bootstrap"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/logging"
|
"gitea.futuresens.co.uk/futuresens/hardlink/logging"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/payment"
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,291 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -19,7 +19,7 @@ func (lock *AssaLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LockSequence checks heartbeat of the Assa Abloy lock server and performs key encoding
|
// Checks heart beat of the Assa Abloy lock server and perform key encoding
|
||||||
func (lock *AssaLockServer) LockSequence() error {
|
func (lock *AssaLockServer) LockSequence() error {
|
||||||
const funcName = "AssaLockServer.LockSequence"
|
const funcName = "AssaLockServer.LockSequence"
|
||||||
|
|
||||||
@ -25,11 +25,10 @@ const (
|
|||||||
Omnitec = "omnitec"
|
Omnitec = "omnitec"
|
||||||
Salto = "salto"
|
Salto = "salto"
|
||||||
TLJ = "tlj"
|
TLJ = "tlj"
|
||||||
Dormakaba = "kaba"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Cert string
|
Cert string
|
||||||
LockServerURL string
|
LockServerURL string
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,11 +38,6 @@ type (
|
|||||||
LockSequence() error
|
LockSequence() error
|
||||||
}
|
}
|
||||||
|
|
||||||
KabaLockServer struct {
|
|
||||||
encoderAddr string
|
|
||||||
command []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
AssaLockServer struct {
|
AssaLockServer struct {
|
||||||
encoderAddr string
|
encoderAddr string
|
||||||
command string
|
command string
|
||||||
@ -75,24 +69,22 @@ func NewLockServer(lockType, encoderAddr string, fatalError func(error)) LockSer
|
|||||||
return &SaltoLockServer{encoderAddr: encoderAddr}
|
return &SaltoLockServer{encoderAddr: encoderAddr}
|
||||||
case TLJ:
|
case TLJ:
|
||||||
return &TLJLockServer{encoderAddr: encoderAddr}
|
return &TLJLockServer{encoderAddr: encoderAddr}
|
||||||
case Dormakaba:
|
|
||||||
return &KabaLockServer{encoderAddr: encoderAddr}
|
|
||||||
default:
|
default:
|
||||||
fatalError(fmt.Errorf("unsupported LockType: %s; must be 'assaabloy' or 'omnitec'", lockType))
|
fatalError(fmt.Errorf("unsupported LockType: %s; must be 'assaabloy' or 'omnitec'", lockType))
|
||||||
return nil // This line will never be reached, but is needed to satisfy the compiler
|
return nil // This line will never be reached, but is needed to satisfy the compiler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeServerConnection(LockserverURL string) (net.Conn, error) {
|
func InitializeServerConnection(LockserverUrl string) (net.Conn, error) {
|
||||||
const funcName = "InitializeServerConnection"
|
const funcName = "InitializeServerConnection"
|
||||||
// Parse the URL to extract host and port
|
// Parse the URL to extract host and port
|
||||||
parsedURL, err := url.Parse(LockserverURL)
|
parsedUrl, err := url.Parse(LockserverUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("[%s] failed to parse LockserverURL: %v", funcName, err)
|
return nil, fmt.Errorf("[%s] failed to parse LockserverUrl: %v", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any leading/trailing slashes just in case
|
// Remove any leading/trailing slashes just in case
|
||||||
address := strings.Trim(parsedURL.Host, "/")
|
address := strings.Trim(parsedUrl.Host, "/")
|
||||||
|
|
||||||
// Establish a TCP connection to the Visionline server
|
// Establish a TCP connection to the Visionline server
|
||||||
conn, err := net.Dial("tcp", address)
|
conn, err := net.Dial("tcp", address)
|
||||||
@ -12,7 +12,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildCommand builds key encoding request command for the Omnitec lock server.
|
// Build key encoding request command for the Omnitec lock server.
|
||||||
func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, checkOut time.Time) error {
|
func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, checkOut time.Time) error {
|
||||||
const funcName = "OmniLockServer.BuildCommand"
|
const funcName = "OmniLockServer.BuildCommand"
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
@ -25,7 +25,7 @@ func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] failed to convert lockId to integer: %v", funcName, err)
|
return fmt.Errorf("[%s] failed to convert lockId to integer: %v", funcName, err)
|
||||||
}
|
}
|
||||||
formattedLockID := fmt.Sprintf("%04d", idInt)
|
formattedLockId := fmt.Sprintf("%04d", idInt)
|
||||||
|
|
||||||
// Format date/time parts
|
// Format date/time parts
|
||||||
dt := checkOut.Format("15:04") // DT = HH:mm
|
dt := checkOut.Format("15:04") // DT = HH:mm
|
||||||
@ -37,7 +37,7 @@ func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
|||||||
payload := fmt.Sprintf(
|
payload := fmt.Sprintf(
|
||||||
"KR|KC%s|KTD|RN%s|%s|DT%s|G#75|GA%s|GD%s|KO0000|DA%s|TI%s|",
|
"KR|KC%s|KTD|RN%s|%s|DT%s|G#75|GA%s|GD%s|KO0000|DA%s|TI%s|",
|
||||||
lock.encoderAddr,
|
lock.encoderAddr,
|
||||||
formattedLockID,
|
formattedLockId,
|
||||||
hostname,
|
hostname,
|
||||||
dt,
|
dt,
|
||||||
ga,
|
ga,
|
||||||
@ -52,7 +52,7 @@ func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LockSequence starts link to the Omnitec lock server and perform key encoding
|
// Starts link to the Omnitec lock server and perform key encoding
|
||||||
func (lock *OmniLockServer) LockSequence() error {
|
func (lock *OmniLockServer) LockSequence() error {
|
||||||
const funcName = "OmniLockServer.LockSequence"
|
const funcName = "OmniLockServer.LockSequence"
|
||||||
|
|
||||||
@ -16,20 +16,20 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/bootstrap"
|
"gitea.futuresens.co.uk/futuresens/hardlink/bootstrap"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/dispenser"
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/handlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/lockserver"
|
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/logging"
|
"gitea.futuresens.co.uk/futuresens/hardlink/logging"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
|
"gitea.futuresens.co.uk/futuresens/hardlink/mail"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/payment"
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/printer"
|
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.2.9"
|
buildVersion = "1.2.7"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
pollingFrequency = 8 * time.Second
|
pollingFrequency = 8 * time.Second
|
||||||
)
|
)
|
||||||
@ -40,7 +40,7 @@ func main() {
|
|||||||
printer.Layout = readTicketLayout()
|
printer.Layout = readTicketLayout()
|
||||||
printer.PrinterName = cfg.PrinterName
|
printer.PrinterName = cfg.PrinterName
|
||||||
lockserver.Cert = cfg.Cert
|
lockserver.Cert = cfg.Cert
|
||||||
lockserver.LockServerURL = cfg.LockserverURL
|
lockserver.LockServerURL = cfg.LockserverUrl
|
||||||
mail.SendErrorEmails = cfg.SendErrorEmails
|
mail.SendErrorEmails = cfg.SendErrorEmails
|
||||||
|
|
||||||
// Root context for background goroutines
|
// Root context for background goroutines
|
||||||
@ -94,14 +94,14 @@ func main() {
|
|||||||
case lockserver.TLJ:
|
case lockserver.TLJ:
|
||||||
// TLJ uses HTTP - skip TCP probe here
|
// TLJ uses HTTP - skip TCP probe here
|
||||||
default:
|
default:
|
||||||
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverURL)
|
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Lock Server Connection Error", err.Error())
|
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Lock Server Connection Error", err.Error())
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverURL)
|
fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverUrl)
|
||||||
log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverURL)
|
log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverUrl)
|
||||||
lockConn.Close()
|
lockConn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
_ "github.com/denisenkom/go-mssqldb"
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -11,8 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/db"
|
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
#### 1.2.9 - 02 June 2026
|
|
||||||
added Dormakaba lock server integration
|
|
||||||
|
|
||||||
#### 1.2.8 - 14 May 2026
|
|
||||||
Updated hardlink source layout to use cmd/hardlink for the main application entry point and internal/ for application packages. Runtime files and preauth-release layout remain unchanged. No functional changes.
|
|
||||||
|
|
||||||
#### 1.2.7 - 13 May 2026
|
#### 1.2.7 - 13 May 2026
|
||||||
retrieve CardType from the ChepDNA response
|
retrieve CardType from the ChepDNA response
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user