Files
ShackMaster/internal/devices/antennagenius/antennagenius.go
2026-01-10 23:33:47 +01:00

479 lines
10 KiB
Go

package antennagenius
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
)
type Client struct {
host string
port int
conn net.Conn
reader *bufio.Reader
connMu sync.Mutex
lastStatus *Status
statusMu sync.RWMutex
antennas []Antenna
antennasMu sync.RWMutex
stopChan chan struct{}
running bool
}
type Status struct {
PortA *PortStatus `json:"port_a"`
PortB *PortStatus `json:"port_b"`
Antennas []Antenna `json:"antennas"`
Connected bool `json:"connected"`
}
type PortStatus struct {
Auto bool `json:"auto"`
Source string `json:"source"`
Band int `json:"band"`
Frequency float64 `json:"frequency"`
Nickname string `json:"nickname"`
RxAnt int `json:"rx_ant"`
TxAnt int `json:"tx_ant"`
InBand int `json:"in_band"`
TX bool `json:"tx"`
Inhibit int `json:"inhibit"`
}
type Antenna struct {
Number int `json:"number"`
Name string `json:"name"`
TX string `json:"tx"`
RX string `json:"rx"`
InBand string `json:"in_band"`
Hotkey int `json:"hotkey"`
}
func New(host string, port int) *Client {
return &Client{
host: host,
port: port,
stopChan: make(chan struct{}),
}
}
func (c *Client) Connect() error {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.conn != nil {
return nil
}
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second)
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
c.conn = conn
c.reader = bufio.NewReader(c.conn)
// Read and discard banner
_, _ = c.reader.ReadString('\n')
return nil
}
func (c *Client) Close() error {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.stopChan != nil {
close(c.stopChan)
}
if c.conn != nil {
return c.conn.Close()
}
return nil
}
func (c *Client) Start() error {
if c.running {
return nil
}
_ = c.Connect()
c.running = true
go c.pollLoop()
return nil
}
func (c *Client) pollLoop() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
initialized := false
for {
select {
case <-ticker.C:
c.connMu.Lock()
if c.conn == nil {
c.connMu.Unlock()
c.statusMu.Lock()
c.lastStatus = &Status{Connected: false}
c.statusMu.Unlock()
if err := c.Connect(); err != nil {
continue
}
initialized = false
c.connMu.Lock()
}
c.connMu.Unlock()
// Initialize: get antenna list and subscribe
if !initialized {
if err := c.initialize(); err != nil {
log.Printf("AntennaGenius init error: %v", err)
c.connMu.Lock()
if c.conn != nil {
c.conn.Close()
c.conn = nil
c.reader = nil
}
c.connMu.Unlock()
continue
}
initialized = true
}
// Read spontaneous messages from subscription
c.connMu.Lock()
if c.conn != nil && c.reader != nil {
c.conn.SetReadDeadline(time.Now().Add(150 * time.Millisecond))
for {
line, err := c.reader.ReadString('\n')
if err != nil {
break
}
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "S") {
c.parsePortStatus(line)
}
}
}
c.connMu.Unlock()
case <-c.stopChan:
return
}
}
}
func (c *Client) initialize() error {
// Get antenna list
log.Println("AntennaGenius: Getting antenna list...")
antennas, err := c.getAntennaList()
if err != nil {
return fmt.Errorf("failed to get antenna list: %w", err)
}
log.Printf("AntennaGenius: Found %d antennas", len(antennas))
for i, ant := range antennas {
log.Printf("AntennaGenius: Antenna %d: number=%d, name=%s", i, ant.Number, ant.Name)
}
c.antennasMu.Lock()
c.antennas = antennas
c.antennasMu.Unlock()
// Initialize status BEFORE subscribing so parsePortStatus can update it
c.statusMu.Lock()
c.lastStatus = &Status{
PortA: &PortStatus{},
PortB: &PortStatus{},
Antennas: antennas,
Connected: true,
}
c.statusMu.Unlock()
log.Println("AntennaGenius: Status initialized, now subscribing to port updates...")
// Subscribe to port updates (this will parse and update port status)
if err := c.subscribeToPortUpdates(); err != nil {
return fmt.Errorf("failed to subscribe: %w", err)
}
// Request initial status for both ports
log.Println("AntennaGenius: Requesting additional port status...")
_, _ = c.sendCommand("port get 1") // Port A
_, _ = c.sendCommand("port get 2") // Port B
c.statusMu.RLock()
log.Printf("AntennaGenius: Initialization complete - PortA.RxAnt=%d, PortB.RxAnt=%d",
c.lastStatus.PortA.RxAnt, c.lastStatus.PortB.RxAnt)
c.statusMu.RUnlock()
return nil
}
func (c *Client) sendCommand(cmd string) (string, error) {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.conn == nil || c.reader == nil {
return "", fmt.Errorf("not connected")
}
// AntennaGenius only accepts C1| for all commands
fullCmd := fmt.Sprintf("C1|%s\n", cmd)
_, err := c.conn.Write([]byte(fullCmd))
if err != nil {
c.conn = nil
c.reader = nil
return "", fmt.Errorf("failed to send command: %w", err)
}
// Read all response lines using shared reader
var response strings.Builder
// Set a read timeout to avoid blocking forever
c.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
defer c.conn.SetReadDeadline(time.Time{})
for {
line, err := c.reader.ReadString('\n')
if err != nil {
if response.Len() > 0 {
// We got some data, return it
break
}
c.conn = nil
c.reader = nil
return "", fmt.Errorf("failed to read response: %w", err)
}
response.WriteString(line)
// Parse spontaneous status updates
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "S0|") {
c.connMu.Unlock()
c.parsePortStatus(trimmedLine)
c.connMu.Lock()
}
// Check if this is the last line (empty line or timeout)
if trimmedLine == "" {
break
}
}
return response.String(), nil
}
func (c *Client) getAntennaList() ([]Antenna, error) {
resp, err := c.sendCommand("antenna list")
if err != nil {
return nil, err
}
var antennas []Antenna
// Response format: R<id>|0|antenna <num> name=<name> tx=<hex> rx=<hex> inband=<hex> hotkey=<num>
lines := strings.Split(resp, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.Contains(line, "antenna ") {
continue
}
antenna := c.parseAntennaLine(line)
// Skip unconfigured antennas (name = Antenna X with space)
if strings.HasPrefix(antenna.Name, "Antenna ") {
continue
}
antennas = append(antennas, antenna)
}
return antennas, nil
}
func (c *Client) parseAntennaLine(line string) Antenna {
antenna := Antenna{}
// Extract antenna number
if idx := strings.Index(line, "antenna "); idx != -1 {
rest := line[idx+8:]
parts := strings.Fields(rest)
if len(parts) > 0 {
antenna.Number, _ = strconv.Atoi(parts[0])
}
}
// Parse key=value pairs
pairs := strings.Fields(line)
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
continue
}
key := kv[0]
value := kv[1]
switch key {
case "name":
// Replace underscores with spaces
antenna.Name = strings.ReplaceAll(value, "_", " ")
case "tx":
antenna.TX = value
case "rx":
antenna.RX = value
case "inband":
antenna.InBand = value
case "hotkey":
antenna.Hotkey, _ = strconv.Atoi(value)
}
}
return antenna
}
func (c *Client) subscribeToPortUpdates() error {
resp, err := c.sendCommand("sub port all")
if err != nil {
log.Printf("AntennaGenius: Failed to subscribe: %v", err)
return err
}
// Parse initial port status from subscription response
// The response may contain S0|port messages with current status
lines := strings.Split(resp, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "S0|port") {
c.parsePortStatus(line)
}
}
log.Println("AntennaGenius: Subscription complete")
return nil
}
func (c *Client) parsePortStatus(line string) {
// Format: S0|port <id> auto=<0|1> source=<src> band=<n> freq=<f> nickname=<name> rxant=<n> txant=<n> inband=<n> tx=<0|1> inhibit=<n>
var portID int
portStatus := &PortStatus{}
// Extract port ID
if idx := strings.Index(line, "port "); idx != -1 {
rest := line[idx+5:]
parts := strings.Fields(rest)
if len(parts) > 0 {
portID, _ = strconv.Atoi(parts[0])
}
}
// Parse key=value pairs
pairs := strings.Fields(line)
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
continue
}
key := kv[0]
value := kv[1]
switch key {
case "auto":
portStatus.Auto = value == "1"
case "source":
portStatus.Source = value
case "band":
portStatus.Band, _ = strconv.Atoi(value)
case "freq":
portStatus.Frequency, _ = strconv.ParseFloat(value, 64)
case "nickname":
portStatus.Nickname = value
case "rxant":
portStatus.RxAnt, _ = strconv.Atoi(value)
case "txant":
portStatus.TxAnt, _ = strconv.Atoi(value)
case "inband":
portStatus.InBand, _ = strconv.Atoi(value)
case "tx":
portStatus.TX = value == "1"
case "inhibit":
portStatus.Inhibit, _ = strconv.Atoi(value)
}
}
// Update status
c.statusMu.Lock()
if c.lastStatus != nil {
if portID == 1 {
c.lastStatus.PortA = portStatus
} else if portID == 2 {
c.lastStatus.PortB = portStatus
}
}
c.statusMu.Unlock()
}
func (c *Client) GetStatus() (*Status, error) {
c.statusMu.RLock()
defer c.statusMu.RUnlock()
if c.lastStatus == nil {
return &Status{Connected: false}, nil
}
// Check if device is actually alive
// If no antennas and all values are default, device is probably off
status := *c.lastStatus
if len(status.Antennas) == 0 || (status.PortA != nil && status.PortA.Source == "" && status.PortB != nil && status.PortB.Source == "") {
status.Connected = false
}
return &status, nil
}
// SetAntenna sets the antenna for a specific port
func (c *Client) SetAntenna(port, antenna int) error {
cmd := fmt.Sprintf("port set %d rxant=%d", port, antenna)
_, err := c.sendCommand(cmd)
return err
}
// DeselectAntenna deselects an antenna from a port (sets rxant=00)
// Command format: "C1|port set <port> rxant=00"
func (c *Client) DeselectAntenna(port, antenna int) error {
cmd := fmt.Sprintf("port set %d rxant=00", port)
log.Printf("AntennaGenius: Sending deselect command: %s", cmd)
resp, err := c.sendCommand(cmd)
if err != nil {
log.Printf("AntennaGenius: Deselect failed: %v", err)
return err
}
log.Printf("AntennaGenius: Deselect response: %s", resp)
return nil
}
// Reboot reboots the device
func (c *Client) Reboot() error {
_, err := c.sendCommand("reboot")
return err
}