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|0|antenna name= tx= rx= inband= hotkey= 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 auto=<0|1> source= band= freq= nickname= rxant= txant= inband= tx=<0|1> inhibit= 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 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 }