feat: Themes added, 4 themes available (3 dark, 1 light)

This commit is contained in:
2026-07-06 09:08:47 +02:00
parent fafa0c22ab
commit 06183bd5d4
43 changed files with 982 additions and 457 deletions
+62 -21
View File
@@ -16,6 +16,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"hamlog/internal/applog"
@@ -56,6 +57,11 @@ type Client struct {
mu sync.Mutex // guards conn + writes
conn net.Conn
// Auth handshake state (touched only on the runLoop/readLoop goroutine).
awaitingAuth bool
authTries int
ready atomic.Bool // init commands sent → keepalive may run
statusMu sync.RWMutex
status Status
antennas map[int]string // index → name (rebuilt into status.Antennas)
@@ -152,25 +158,11 @@ func (c *Client) runLoop() {
c.setStatus(func(s *Status) { s.Connected = true; s.LastError = ""; s.Host = c.host })
applog.Printf("antgenius: TCP connected %s → %s", conn.LocalAddr(), conn.RemoteAddr())
// When reached remotely the device announces "V… AG AUTH" and refuses every
// command (R1|FF) until the client logs in. Sent FIRST so it's authenticated
// before the subscribe/get commands (the device processes the stream in
// order). On the LAN there's no AUTH and the password is blank → skipped.
if c.password != "" {
if err := c.send("login " + c.password); err != nil {
applog.Printf("antgenius: login send failed: %v", err)
} else {
applog.Printf("antgenius: sent login (password %d chars)", len(c.password))
}
}
// Subscribe to live updates and pull the initial state. Command set and
// order mirror a known-working Node-RED v4 client (WA9WUD).
for _, cmd := range []string{"antenna list", "sub port all", "port get 1", "port get 2"} {
if err := c.send(cmd); err != nil {
applog.Printf("antgenius: init send %q failed: %v", cmd, err)
}
}
// Auth + init are driven by the banner (handleLine): on "V… AG AUTH" with a
// password we send "auth code=<pw>" and only send the subscribe/get commands
// once it's accepted (retrying on FF); without AUTH they go out immediately.
c.awaitingAuth, c.authTries = false, 0
c.ready.Store(false)
done := make(chan struct{})
go c.keepalive(conn, done)
@@ -207,12 +199,27 @@ func (c *Client) keepalive(conn net.Conn, done chan struct{}) {
case <-c.stop:
return
case <-t.C:
if !c.ready.Load() {
continue // not authenticated / subscribed yet
}
_ = c.send("port get 1")
_ = c.send("port get 2")
}
}
}
// sendInit subscribes to live updates and pulls the initial state. Called once
// the link is ready (authenticated, or immediately when no AUTH is required).
// Command set/order mirror a known-working Node-RED v4 client (WA9WUD).
func (c *Client) sendInit() {
c.ready.Store(true)
for _, cmd := range []string{"antenna list", "sub port all", "port get 1", "port get 2"} {
if err := c.send(cmd); err != nil {
applog.Printf("antgenius: init send %q failed: %v", cmd, err)
}
}
}
func (c *Client) readLoop(conn net.Conn) error {
r := bufio.NewReader(conn)
var sb strings.Builder
@@ -267,16 +274,31 @@ func (c *Client) handleLine(line string) {
if line == "" {
return
}
// Banner like "V4.1.16 AG" — just confirms the link is up.
// Banner: "V4.1.16 AG" (LAN) or "V4.1.16 AG AUTH" (remote). It drives what we
// send next — authenticate first when AUTH is announced, else go straight to
// the subscribe/get commands.
if line[0] == 'V' && strings.Contains(line, "AG") {
c.setStatus(func(s *Status) { s.Connected = true; s.LastError = "" })
if strings.Contains(line, "AUTH") && c.password != "" {
c.awaitingAuth, c.authTries = true, 1
applog.Printf("antgenius: AUTH required — authenticating")
_ = c.send("auth code=" + c.password)
} else {
if strings.Contains(line, "AUTH") {
applog.Printf("antgenius: device requires AUTH but no remote password set (Settings → Antenna Genius)")
}
c.sendInit()
}
return
}
// R<seq>|<hex>|<message> or S<seq>|<message>
var msg string
var hexCode, msg string
switch {
case strings.HasPrefix(line, "R"):
p := strings.SplitN(line, "|", 3)
if len(p) >= 2 {
hexCode = p[1]
}
if len(p) == 3 {
msg = p[2]
}
@@ -287,6 +309,25 @@ func (c *Client) handleLine(line string) {
}
}
msg = strings.TrimSpace(msg)
// Auth result: the reply to "auth code=" carries an empty message; hex "0" =
// accepted. The device rejects the FIRST attempt (R|FF) and accepts a retry,
// so resend a few times before giving up.
if c.awaitingAuth && strings.HasPrefix(line, "R") && msg == "" {
switch {
case hexCode == "0":
c.awaitingAuth = false
applog.Printf("antgenius: authenticated")
c.sendInit()
case c.authTries < 4:
c.authTries++
applog.Printf("antgenius: auth rejected (R|%s|) — retry %d", hexCode, c.authTries)
_ = c.send("auth code=" + c.password)
default:
c.awaitingAuth = false
applog.Printf("antgenius: auth failed after %d tries (R|%s|) — check the remote password", c.authTries, hexCode)
}
return
}
switch {
case strings.HasPrefix(msg, "antenna "):
c.parseAntenna(msg)