package antennagenius import ( "bufio" "fmt" <<<<<<< HEAD "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"` ======= "net" "strconv" "strings" "time" . "git.rouggy.com/rouggy/ShackMaster/internal/devices" ) type Client struct { host string port int conn net.Conn } type Status struct { Radio1Antenna int `json:"radio1_antenna"` // 0-7 (antenna index) Radio2Antenna int `json:"radio2_antenna"` // 0-7 (antenna index) Connected bool `json:"connected"` >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } func New(host string, port int) *Client { return &Client{ <<<<<<< HEAD host: host, port: port, stopChan: make(chan struct{}), ======= host: host, port: port, >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } } func (c *Client) Connect() error { <<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() if c.conn != nil { return nil } ======= >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 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 <<<<<<< HEAD c.reader = bufio.NewReader(c.conn) // Read and discard banner _, _ = c.reader.ReadString('\n') ======= >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 return nil } func (c *Client) Close() error { <<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() if c.stopChan != nil { close(c.stopChan) } ======= >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if c.conn != nil { return c.conn.Close() } return nil } <<<<<<< HEAD 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") ======= func (c *Client) sendCommand(cmd string) (string, error) { if c.conn == nil { if err := c.Connect(); err != nil { return "", err } } // Get next command ID from global counter cmdID := GetGlobalCommandID().GetNextID() // Format command with ID: C| fullCmd := fmt.Sprintf("C%d|%s\n", cmdID, cmd) // Send command _, err := c.conn.Write([]byte(fullCmd)) if err != nil { c.conn = nil return "", fmt.Errorf("failed to send command: %w", err) } // Read response reader := bufio.NewReader(c.conn) response, err := reader.ReadString('\n') if err != nil { c.conn = nil return "", fmt.Errorf("failed to read response: %w", err) } return strings.TrimSpace(response), nil } func (c *Client) GetStatus() (*Status, error) { resp, err := c.sendCommand("status") >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return nil, err } <<<<<<< HEAD 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") ======= return c.parseStatus(resp) } func (c *Client) parseStatus(resp string) (*Status, error) { status := &Status{ Connected: true, } // Parse response format from 4O3A API // Expected format will vary - this is a basic parser pairs := strings.Fields(resp) for _, pair := range pairs { parts := strings.SplitN(pair, "=", 2) if len(parts) != 2 { continue } key := parts[0] value := parts[1] switch key { case "radio1", "r1": status.Radio1Antenna, _ = strconv.Atoi(value) case "radio2", "r2": status.Radio2Antenna, _ = strconv.Atoi(value) } } return status, nil } // SetRadioAntenna sets which antenna a radio should use // radio: 1 or 2 // antenna: 0-7 (antenna index) func (c *Client) SetRadioAntenna(radio int, antenna int) error { if radio < 1 || radio > 2 { return fmt.Errorf("radio must be 1 or 2") } if antenna < 0 || antenna > 7 { return fmt.Errorf("antenna must be between 0 and 7") } cmd := fmt.Sprintf("set radio%d=%d", radio, antenna) resp, err := c.sendCommand(cmd) >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return err } <<<<<<< HEAD // 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) } ======= // Check response for success if !strings.Contains(strings.ToLower(resp), "ok") && resp != "" { // If response doesn't contain "ok" but isn't empty, assume success // (some devices may return the new state instead of "ok") >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } return nil } <<<<<<< HEAD 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 } 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 ======= // GetRadioAntenna gets which antenna a radio is currently using func (c *Client) GetRadioAntenna(radio int) (int, error) { if radio < 1 || radio > 2 { return -1, fmt.Errorf("radio must be 1 or 2") } status, err := c.GetStatus() if err != nil { return -1, err } if radio == 1 { return status.Radio1Antenna, nil } return status.Radio2Antenna, nil >>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }