rot finished
This commit is contained in:
437
internal/devices/antennagenius/antennagenius.go
Normal file
437
internal/devices/antennagenius/antennagenius.go
Normal file
@@ -0,0 +1,437 @@
|
||||
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
|
||||
antennas, err := c.getAntennaList()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get antenna list: %w", err)
|
||||
}
|
||||
|
||||
c.antennasMu.Lock()
|
||||
c.antennas = antennas
|
||||
c.antennasMu.Unlock()
|
||||
|
||||
// Subscribe to port updates
|
||||
if err := c.subscribeToPortUpdates(); err != nil {
|
||||
return fmt.Errorf("failed to subscribe: %w", err)
|
||||
}
|
||||
|
||||
// Initialize status
|
||||
c.statusMu.Lock()
|
||||
c.lastStatus = &Status{
|
||||
PortA: &PortStatus{},
|
||||
PortB: &PortStatus{},
|
||||
Antennas: antennas,
|
||||
Connected: true,
|
||||
}
|
||||
c.statusMu.Unlock()
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return c.lastStatus, 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
|
||||
}
|
||||
|
||||
// Reboot reboots the device
|
||||
func (c *Client) Reboot() error {
|
||||
_, err := c.sendCommand("reboot")
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user