From 21db2addff0694c30e0880d6aaa0af1cd73e2992 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 15 Jan 2026 22:19:32 +0100 Subject: [PATCH] up --- internal/api/device_manager.go | 7 +- internal/devices/flexradio/flexradio.go | 662 ++++++++++++++++++++++-- internal/devices/flexradio/types.go | 18 +- web/src/components/StatusBanner.svelte | 131 ++++- 4 files changed, 758 insertions(+), 60 deletions(-) diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index dbde011..8e2cd17 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -121,6 +121,9 @@ func (dm *DeviceManager) Initialize() error { dm.config.Devices.FlexRadio.Port, ) + dm.flexRadio.SetReconnectInterval(3 * time.Second) // Retry every 3 seconds + dm.flexRadio.SetMaxReconnectDelay(30 * time.Second) // Max 30 second delay + // Set callback for immediate frequency changes (no waiting for update cycle) dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) { dm.handleFrequencyChange(freqMHz) @@ -421,9 +424,7 @@ func (dm *DeviceManager) updateStatus() { // Check cooldown to prevent rapid fire commands timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime) - if timeSinceLastUpdate < dm.freqUpdateCooldown { - log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate) - } else { + if timeSinceLastUpdate > dm.freqUpdateCooldown { log.Printf("Auto-track (%s): Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", radioSource, freqDiff, radioFreqKhz, directionToUse) // Send to Ultrabeam with saved or current direction diff --git a/internal/devices/flexradio/flexradio.go b/internal/devices/flexradio/flexradio.go index 7e1fafe..3ff67f9 100644 --- a/internal/devices/flexradio/flexradio.go +++ b/internal/devices/flexradio/flexradio.go @@ -29,22 +29,52 @@ type Client struct { running bool stopChan chan struct{} + // Reconnection settings + reconnectInterval time.Duration + reconnectAttempts int // Track attempts for logging + maxReconnectDelay time.Duration + + // Radio info from "info" command + radioInfo map[string]string + radioInfoMu sync.RWMutex + lastInfoCheck time.Time + infoCheckTimer *time.Timer + // Callbacks onFrequencyChange func(freqMHz float64) checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving) + + activeSlices []int // Liste des slices actives [0, 1, 2, 3] + activeSlicesMu sync.RWMutex + sliceListTimer *time.Timer // Timer pour vérifier périodiquement les slices } func New(host string, port int) *Client { return &Client{ - host: host, - port: port, - stopChan: make(chan struct{}), + host: host, + port: port, + stopChan: make(chan struct{}), + reconnectInterval: 5 * time.Second, + maxReconnectDelay: 60 * time.Second, + radioInfo: make(map[string]string), + activeSlices: []int{}, // Initialiser vide lastStatus: &Status{ Connected: false, + RadioOn: false, }, } } +// SetReconnectInterval sets the reconnection interval (default 5 seconds) +func (c *Client) SetReconnectInterval(interval time.Duration) { + c.reconnectInterval = interval +} + +// SetMaxReconnectDelay sets the maximum delay for exponential backoff (default 60 seconds) +func (c *Client) SetMaxReconnectDelay(delay time.Duration) { + c.maxReconnectDelay = delay +} + // SetFrequencyChangeCallback sets the callback function called when frequency changes func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) { c.onFrequencyChange = callback @@ -73,8 +103,9 @@ func (c *Client) Connect() error { c.conn = conn c.reader = bufio.NewReader(conn) + c.reconnectAttempts = 0 // Reset attempts on successful connection - log.Println("FlexRadio: Connected successfully") + log.Println("FlexRadio: TCP connection established") return nil } @@ -83,14 +114,17 @@ func (c *Client) Start() error { return nil } + // Try initial connection if err := c.Connect(); err != nil { - return err + log.Printf("FlexRadio: Initial connection failed: %v", err) + // Don't return error, let reconnection handle it } // Update connected status c.statusMu.Lock() if c.lastStatus != nil { - c.lastStatus.Connected = true + c.lastStatus.Connected = (c.conn != nil) + c.lastStatus.RadioOn = false // Will be updated by checkRadioStatus } c.statusMu.Unlock() @@ -99,16 +133,72 @@ func (c *Client) Start() error { // Start message listener go c.messageLoop() - // Subscribe to slice updates for frequency tracking - log.Println("FlexRadio: Subscribing to slice updates...") - _, err := c.sendCommand("sub slice all") - if err != nil { - log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err) + // Start reconnection monitor + go c.reconnectionMonitor() + + // Start radio status checker (checks if radio is actually on) + go c.radioStatusChecker() + + // Try to get initial radio info and subscribe to slices + if c.conn != nil { + go func() { + // Petite pause pour laisser la connexion s'établir + time.Sleep(500 * time.Millisecond) + + // D'abord vérifier le statut de la radio + c.checkRadioStatus() + + // Puis s'abonner aux updates des slices + time.Sleep(500 * time.Millisecond) + log.Println("FlexRadio: Subscribing to slice updates...") + + c.writeMu.Lock() + defer c.writeMu.Unlock() + + c.connMu.Lock() + conn := c.conn + c.connMu.Unlock() + + if conn != nil { + seq := c.getNextSeq() + subscribeCmd := fmt.Sprintf("C%d|sub slice all\n", seq) + + conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Write([]byte(subscribeCmd)) + conn.SetWriteDeadline(time.Time{}) + + if err != nil { + log.Printf("FlexRadio: Failed to subscribe to slices: %v", err) + } else { + log.Println("FlexRadio: Successfully subscribed to slice updates") + } + } + }() } + go c.sliceListChecker() + return nil } +// sliceListChecker vérifie périodiquement la liste des slices +func (c *Client) sliceListChecker() { + // Vérifier toutes les 10 secondes + c.sliceListTimer = time.NewTimer(10 * time.Second) + + for c.running { + select { + case <-c.sliceListTimer.C: + if c.IsRadioOn() { + c.getActiveSlices() + } + c.sliceListTimer.Reset(10 * time.Second) + case <-c.stopChan: + return + } + } +} + func (c *Client) Stop() { if !c.running { return @@ -117,6 +207,11 @@ func (c *Client) Stop() { c.running = false close(c.stopChan) + // Stop info check timer + if c.infoCheckTimer != nil { + c.infoCheckTimer.Stop() + } + c.connMu.Lock() if c.conn != nil { c.conn.Close() @@ -125,14 +220,220 @@ func (c *Client) Stop() { } c.connMu.Unlock() - // Update connected status + // Update status c.statusMu.Lock() if c.lastStatus != nil { c.lastStatus.Connected = false + c.lastStatus.RadioOn = false + c.lastStatus.RadioInfo = "Disconnected" } c.statusMu.Unlock() } +// radioStatusChecker periodically checks if the radio is actually powered on +func (c *Client) radioStatusChecker() { + // Check every 10 seconds + c.infoCheckTimer = time.NewTimer(10 * time.Second) + + for c.running { + select { + case <-c.infoCheckTimer.C: + c.checkRadioStatus() + c.infoCheckTimer.Reset(10 * time.Second) + case <-c.stopChan: + return + } + } +} + +// checkRadioStatus sends "info" command to check if radio is actually powered on +func (c *Client) checkRadioStatus() { + c.writeMu.Lock() + defer c.writeMu.Unlock() + + c.connMu.Lock() + conn := c.conn + c.connMu.Unlock() + + if conn == nil { + c.updateRadioStatus(false, "No TCP connection") + return + } + + seq := c.getNextSeq() + infoCmd := fmt.Sprintf("C%d|info\n", seq) + + log.Printf("FlexRadio: Checking radio status with 'info' command...") + + // Set timeout for the check + conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Write([]byte(infoCmd)) + conn.SetWriteDeadline(time.Time{}) // Clear deadline + + if err != nil { + log.Printf("FlexRadio: Failed to send info command: %v", err) + c.updateRadioStatus(false, "Failed to send info command") + return + } + + c.lastInfoCheck = time.Now() + log.Println("FlexRadio: Info command sent, waiting for response...") +} + +// updateRadioStatus updates the radio status based on info command response +func (c *Client) updateRadioStatus(isOn bool, info string) { + c.statusMu.Lock() + defer c.statusMu.Unlock() + + if c.lastStatus != nil { + c.lastStatus.RadioOn = isOn + c.lastStatus.RadioInfo = info + + // Update callsign and model from radioInfo if available + c.radioInfoMu.RLock() + if callsign, ok := c.radioInfo["callsign"]; ok { + c.lastStatus.Callsign = callsign + } + if model, ok := c.radioInfo["model"]; ok { + c.lastStatus.Model = model + } + if softwareVer, ok := c.radioInfo["software_ver"]; ok { + c.lastStatus.SoftwareVer = softwareVer + } + if numSlicesStr, ok := c.radioInfo["num_slice"]; ok { + if numSlices, err := strconv.Atoi(numSlicesStr); err == nil { + c.lastStatus.NumSlices = numSlices + } + } + c.radioInfoMu.RUnlock() + + // If radio is on but no frequency, update info message + if isOn && c.lastStatus.Frequency == 0 { + c.lastStatus.RadioInfo = "Radio is on without any active slice" + } + } +} + +// reconnectionMonitor handles automatic reconnection attempts +func (c *Client) reconnectionMonitor() { + defer func() { + if r := recover(); r != nil { + log.Printf("FlexRadio: Recovered from panic in reconnectionMonitor: %v", r) + } + }() + log.Println("FlexRadio: Reconnection monitor started") + + for c.running { + c.connMu.Lock() + connected := (c.conn != nil) + c.connMu.Unlock() + + if !connected { + c.reconnectAttempts++ + + // Calculate delay with exponential backoff + delay := c.calculateReconnectDelay() + + log.Printf("FlexRadio: Attempting to reconnect in %v (attempt %d)...", delay, c.reconnectAttempts) + + select { + case <-time.After(delay): + if err := c.reconnect(); err != nil { + log.Printf("FlexRadio: Reconnection attempt %d failed: %v", c.reconnectAttempts, err) + } else { + log.Printf("FlexRadio: Reconnected successfully on attempt %d", c.reconnectAttempts) + c.reconnectAttempts = 0 // Reset on success + + // Check radio status after reconnection + go func() { + time.Sleep(500 * time.Millisecond) + c.checkRadioStatus() + }() + } + case <-c.stopChan: + return + } + } else { + // If connected, wait a bit before checking again + select { + case <-time.After(10 * time.Second): + // Just check connection status periodically + case <-c.stopChan: + return + } + } + } +} + +// calculateReconnectDelay calculates delay with exponential backoff +func (c *Client) calculateReconnectDelay() time.Duration { + // Start with base interval + delay := c.reconnectInterval + + // Apply exponential backoff: 5s, 10s, 20s, 40s, etc. + if c.reconnectAttempts > 1 { + multiplier := 1 << (c.reconnectAttempts - 1) // 2^(attempts-1) + delay = c.reconnectInterval * time.Duration(multiplier) + + // Cap at maximum delay + if delay > c.maxReconnectDelay { + delay = c.maxReconnectDelay + } + } + + return delay +} + +// reconnect attempts to establish a new connection +func (c *Client) reconnect() error { + c.connMu.Lock() + defer c.connMu.Unlock() + + // Close existing connection if any + if c.conn != nil { + c.conn.Close() + c.conn = nil + c.reader = nil + } + + addr := fmt.Sprintf("%s:%d", c.host, c.port) + log.Printf("FlexRadio: Reconnecting to %s...", addr) + + conn, err := net.DialTimeout("tcp", addr, 5*time.Second) + if err != nil { + // Update status + c.statusMu.Lock() + if c.lastStatus != nil { + c.lastStatus.Connected = false + c.lastStatus.RadioOn = false + c.lastStatus.RadioInfo = "Disconnected" + } + c.statusMu.Unlock() + return fmt.Errorf("reconnect failed: %w", err) + } + + c.conn = conn + c.reader = bufio.NewReader(conn) + + // Update TCP connection status + c.statusMu.Lock() + if c.lastStatus != nil { + c.lastStatus.Connected = true + c.lastStatus.RadioInfo = "TCP connected, checking radio..." + } + c.statusMu.Unlock() + + go func() { + time.Sleep(1 * time.Second) + c.checkRadioStatus() // Envoie la commande info + time.Sleep(500 * time.Millisecond) + c.getActiveSlices() // Demande la liste des slices + }() + + log.Println("FlexRadio: TCP connection reestablished") + return nil +} + func (c *Client) getNextSeq() int { c.cmdSeqMu.Lock() defer c.cmdSeqMu.Unlock() @@ -153,6 +454,19 @@ func (c *Client) sendCommand(cmd string) (string, error) { return "", fmt.Errorf("not connected") } + // Vérifier si la connexion est encore valide + // en essayant de lire l'adresse distante + if conn.RemoteAddr() == nil { + c.connMu.Lock() + if c.conn != nil { + c.conn.Close() + c.conn = nil + c.reader = nil + } + c.connMu.Unlock() + return "", fmt.Errorf("connection closed") + } + seq := c.getNextSeq() fullCmd := fmt.Sprintf("C%d|%s\n", seq, cmd) @@ -160,10 +474,24 @@ func (c *Client) sendCommand(cmd string) (string, error) { _, err := conn.Write([]byte(fullCmd)) if err != nil { + // Mark connection as broken c.connMu.Lock() - c.conn = nil - c.reader = nil + if c.conn != nil { + c.conn.Close() + c.conn = nil + c.reader = nil + } c.connMu.Unlock() + + // Update status + c.statusMu.Lock() + if c.lastStatus != nil { + c.lastStatus.Connected = false + c.lastStatus.RadioOn = false + c.lastStatus.RadioInfo = "Connection lost" + } + c.statusMu.Unlock() + return "", fmt.Errorf("failed to send command: %w", err) } @@ -171,17 +499,19 @@ func (c *Client) sendCommand(cmd string) (string, error) { } func (c *Client) messageLoop() { + defer func() { + if r := recover(); r != nil { + log.Printf("FlexRadio: Recovered from panic in messageLoop: %v", r) + } + }() log.Println("FlexRadio: Message loop started") for c.running { c.connMu.Lock() if c.conn == nil || c.reader == nil { c.connMu.Unlock() + // Connection is broken, wait for reconnection time.Sleep(1 * time.Second) - if err := c.Connect(); err != nil { - log.Printf("FlexRadio: Reconnect failed: %v", err) - continue - } continue } @@ -196,7 +526,10 @@ func (c *Client) messageLoop() { // Timeout is expected, continue continue } + log.Printf("FlexRadio: Read error: %v", err) + + // Mark connection as broken c.connMu.Lock() if c.conn != nil { c.conn.Close() @@ -205,10 +538,12 @@ func (c *Client) messageLoop() { } c.connMu.Unlock() - // Update connected status + // Update status c.statusMu.Lock() if c.lastStatus != nil { c.lastStatus.Connected = false + c.lastStatus.RadioOn = false + c.lastStatus.RadioInfo = "Connection lost" } c.statusMu.Unlock() continue @@ -219,12 +554,80 @@ func (c *Client) messageLoop() { continue } + // DEBUG: Log tous les messages reçus + log.Printf("FlexRadio RAW: %s", line) + c.handleMessage(line) } log.Println("FlexRadio: Message loop stopped") } +func (c *Client) getActiveSlices() error { + c.writeMu.Lock() + defer c.writeMu.Unlock() + + c.connMu.Lock() + conn := c.conn + c.connMu.Unlock() + + if conn == nil { + return fmt.Errorf("not connected") + } + + seq := c.getNextSeq() + sliceCmd := fmt.Sprintf("C%d|slice list\n", seq) + + log.Printf("FlexRadio: Requesting slice list...") + + conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Write([]byte(sliceCmd)) + conn.SetWriteDeadline(time.Time{}) + + if err != nil { + log.Printf("FlexRadio: Failed to send slice list command: %v", err) + return err + } + + log.Printf("FlexRadio: Slice list command sent (seq=%d)", seq) + return nil +} + +// parseSliceListResponse parse la réponse de "slice list" +func (c *Client) parseSliceListResponse(data string) { + // Format: "0" ou "0 1" ou "0 1 2" ou "0 1 2 3" + slices := []int{} + + if strings.TrimSpace(data) != "" { + parts := strings.Fields(data) + for _, part := range parts { + if sliceNum, err := strconv.Atoi(part); err == nil { + slices = append(slices, sliceNum) + } + } + } + + c.activeSlicesMu.Lock() + c.activeSlices = slices + c.activeSlicesMu.Unlock() + + c.statusMu.Lock() + if c.lastStatus != nil { + c.lastStatus.ActiveSlices = len(slices) + + // Si plus de slices actives, réinitialiser fréquence et mode + if len(slices) == 0 { + c.lastStatus.Frequency = 0 + c.lastStatus.Mode = "" + c.lastStatus.RadioInfo = "Radio is on without any active slice" + } + // Note: La fréquence/mode seront mis à jour par les messages de statut des slices + } + c.statusMu.Unlock() + + log.Printf("FlexRadio: Active slices updated: %v (total: %d)", slices, len(slices)) +} + func (c *Client) handleMessage(msg string) { // Response format: R|| if strings.HasPrefix(msg, "R") { @@ -255,15 +658,171 @@ func (c *Client) handleResponse(msg string) { // Format: R|| // Example: R21|0|000000F4 parts := strings.SplitN(msg, "|", 3) - if len(parts) < 2 { + if len(parts) < 3 { return } + seqStr := strings.TrimPrefix(parts[0], "R") status := parts[1] + data := parts[2] + + // Log the sequence for debugging + seq, err := strconv.Atoi(seqStr) + if err == nil { + log.Printf("FlexRadio: Response for sequence %d, status=%s", seq, status) + } + if status != "0" { log.Printf("FlexRadio: Command error: status=%s, message=%s", status, msg) return } + + // Check if this is an info response + if strings.Contains(data, "model=") { + log.Printf("FlexRadio: Received info response for sequence %d", seq) + c.parseInfoResponse(data) + return + } + + // Check if this is a slice list response + // La réponse est juste une liste de nombres: "0" ou "0 1" etc. + // On vérifie si c'est une réponse numérique simple + if isSliceListResponse(data) { + log.Printf("FlexRadio: Received slice list response: %s", data) + c.parseSliceListResponse(data) + return + } +} + +// isSliceListResponse vérifie si la réponse est une liste de slices +func isSliceListResponse(data string) bool { + data = strings.TrimSpace(data) + if data == "" { + return true // Liste vide + } + + // Vérifie si c'est une liste de nombres séparés par des espaces + parts := strings.Fields(data) + for _, part := range parts { + if _, err := strconv.Atoi(part); err != nil { + return false // Pas un nombre + } + } + return true +} + +func (c *Client) parseInfoResponse(data string) { + // Parse key=value pairs from info response + // Example: model="FLEX-8600",chassis_serial="2725-1213-8600-3867",name="F4BPO-8600",callsign="F4BPO",... + + log.Printf("FlexRadio: Parsing info response: %s", data) + + // Split by comma, but handle quoted values + pairs := []string{} + current := "" + inQuotes := false + + for _, char := range data { + if char == '"' { + inQuotes = !inQuotes + } + if char == ',' && !inQuotes { + pairs = append(pairs, strings.TrimSpace(current)) + current = "" + } else { + current += string(char) + } + } + if current != "" { + pairs = append(pairs, strings.TrimSpace(current)) + } + + // Parse each pair + c.radioInfoMu.Lock() + c.radioInfo = make(map[string]string) + + for _, pair := range pairs { + kv := strings.SplitN(pair, "=", 2) + if len(kv) == 2 { + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + + // Remove quotes if present + if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { + value = value[1 : len(value)-1] + } + + c.radioInfo[key] = value + log.Printf("FlexRadio Info: %s = %s", key, value) + } + } + + c.radioInfoMu.Unlock() + + // Update radio status - radio is definitely on if we got info response + c.updateRadioStatus(true, "Radio is on") + + // NE PAS remettre ActiveSlices à 0 ici ! + // La réponse info ne contient pas l'état des slices actives + // On garde la valeur actuelle de ActiveSlices + + c.statusMu.Lock() + if c.lastStatus != nil { + // Update callsign and model + if callsign, ok := c.radioInfo["callsign"]; ok { + c.lastStatus.Callsign = callsign + } + if model, ok := c.radioInfo["model"]; ok { + c.lastStatus.Model = model + } + if softwareVer, ok := c.radioInfo["software_ver"]; ok { + c.lastStatus.SoftwareVer = softwareVer + } + if numSlicesStr, ok := c.radioInfo["num_slice"]; ok { + if numSlices, err := strconv.Atoi(numSlicesStr); err == nil { + c.lastStatus.NumSlices = numSlices + } + } + + // NE PAS modifier ActiveSlices - garder la valeur actuelle + // c.lastStatus.ActiveSlices reste inchangé + + // Mettre à jour RadioInfo en fonction de l'état actuel + if c.lastStatus.ActiveSlices == 0 { + c.lastStatus.RadioInfo = "Radio is on without any active slice" + } else if c.lastStatus.Frequency > 0 { + c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", c.lastStatus.Frequency) + } else { + c.lastStatus.RadioInfo = "Radio is on with active slice(s)" + } + } + c.statusMu.Unlock() + + go func() { + time.Sleep(500 * time.Millisecond) // Petite pause + c.getActiveSlices() + }() + + log.Printf("FlexRadio: Radio is powered on and responding (total slices: %d, active slices: %d)", + c.lastStatus.NumSlices, c.lastStatus.ActiveSlices) +} + +func (c *Client) GetStatus() (*Status, error) { + c.statusMu.RLock() + defer c.statusMu.RUnlock() + + if c.lastStatus == nil { + return &Status{ + Connected: false, + RadioOn: false, + RadioInfo: "Not initialized", + }, nil + } + + // Create a copy + status := *c.lastStatus + + return &status, nil } func (c *Client) handleStatus(msg string) { @@ -288,41 +847,66 @@ func (c *Client) handleStatus(msg string) { // Check for slice updates (frequency changes) if strings.Contains(msg, "slice") { + log.Printf("FlexRadio: Slice status update received") + + c.statusMu.Lock() + defer c.statusMu.Unlock() + + // Update frequency if present if rfFreq, ok := statusMap["RF_frequency"]; ok { freq, err := strconv.ParseFloat(rfFreq, 64) - if err == nil { - c.statusMu.Lock() + if err == nil && freq > 0 { oldFreq := c.lastStatus.Frequency c.lastStatus.Frequency = freq - c.statusMu.Unlock() + c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq) - // Only log significant frequency changes (> 1 kHz) - if oldFreq == 0 || (freq-oldFreq)*(freq-oldFreq) > 0.001 { - log.Printf("FlexRadio: Frequency updated to %.6f MHz", freq) + log.Printf("FlexRadio: Frequency updated from %.6f to %.6f MHz", oldFreq, freq) - // Trigger callback for immediate auto-track - if c.onFrequencyChange != nil { - go c.onFrequencyChange(freq) - } + // Trigger callback for immediate auto-track + if c.onFrequencyChange != nil { + go c.onFrequencyChange(freq) } } } + + // Update mode if present + if mode, ok := statusMap["mode"]; ok { + c.lastStatus.Mode = mode + log.Printf("FlexRadio: Mode updated to %s", mode) + } + + // Update TX status if present + if tx, ok := statusMap["tx"]; ok { + c.lastStatus.Tx = (tx == "1") + log.Printf("FlexRadio: TX status updated to %v", c.lastStatus.Tx) + } + } + + // Check for interlock updates (TX state) + if strings.Contains(msg, "interlock") { + log.Printf("FlexRadio: Interlock status update received") + + c.statusMu.Lock() + defer c.statusMu.Unlock() + + // Update TX status based on interlock state + if state, ok := statusMap["state"]; ok { + // Les états possibles: RECEIVE, TRANSMIT, TUNE, etc. + // RECEIVE = réception, TRANSMIT = émission + c.lastStatus.Tx = (state == "TRANSMIT" || state == "TUNE") + log.Printf("FlexRadio: Interlock state: %s, TX=%v", state, c.lastStatus.Tx) + } } } -func (c *Client) GetStatus() (*Status, error) { +// IsRadioOn returns true if radio is powered on and responding +func (c *Client) IsRadioOn() bool { c.statusMu.RLock() defer c.statusMu.RUnlock() if c.lastStatus == nil { - return &Status{Connected: false}, nil + return false } - // Create a copy - status := *c.lastStatus - - // DON'T lock connMu here - it causes 4-second blocking! - // The messageLoop updates Connected status, and we trust the cached value - - return &status, nil + return c.lastStatus.RadioOn } diff --git a/internal/devices/flexradio/types.go b/internal/devices/flexradio/types.go index fcee5da..8af11d4 100644 --- a/internal/devices/flexradio/types.go +++ b/internal/devices/flexradio/types.go @@ -2,13 +2,17 @@ package flexradio // Status represents the FlexRadio status type Status struct { - Connected bool `json:"connected"` - InterlockID string `json:"interlock_id"` - InterlockState string `json:"interlock_state"` - Frequency float64 `json:"frequency"` // MHz - Model string `json:"model"` - Serial string `json:"serial"` - Version string `json:"version"` + Connected bool `json:"connected"` + Frequency float64 `json:"frequency"` + Mode string `json:"mode"` + Tx bool `json:"tx"` + RadioOn bool `json:"radio_on"` // Radio is powered on and responding + RadioInfo string `json:"radio_info"` // Additional info about radio state + Callsign string `json:"callsign"` // From info command + Model string `json:"model"` // From info command + SoftwareVer string `json:"software_ver"` // From info command + NumSlices int `json:"num_slices"` // From info command + ActiveSlices int `json:"active_slices"` // Count of active slices } // InterlockState represents possible interlock states diff --git a/web/src/components/StatusBanner.svelte b/web/src/components/StatusBanner.svelte index 065d082..67989c3 100644 --- a/web/src/components/StatusBanner.svelte +++ b/web/src/components/StatusBanner.svelte @@ -9,11 +9,16 @@ export let gustWarningThreshold = 50; // km/h export let graylineWindow = 30; // minutes avant/après sunrise/sunset - // FlexRadio frequency and mode + // FlexRadio status $: frequency = flexradio?.frequency || 0; $: mode = flexradio?.mode || ''; $: txEnabled = flexradio?.tx || false; $: connected = flexradio?.connected || false; + $: radioOn = flexradio?.radio_on || false; + $: radioInfo = flexradio?.radio_info || ''; + $: callsign = flexradio?.callsign || ''; + $: model = flexradio?.model || ''; + $: activeSlices = flexradio?.active_slices || 0; // Grayline calculation let sunrise = null; @@ -55,6 +60,16 @@ updateTimeToNextEvent(); } + $: console.log('FlexRadio status:', { + connected, + radioOn, + frequency, + activeSlices, + radioInfo, + callsign, + model + }); + // Simplified sun calculation (based on NOAA algorithm) function getSunTimes(date, lat, lon) { const rad = Math.PI / 180; @@ -228,6 +243,13 @@ $: currentBand = getBand(frequency); $: bandColor = getBandColor(currentBand); + + // Determine what to show for FlexRadio - MODIFIÉ + $: showFrequency = radioOn && frequency > 0; + $: showRadioOnNoSlice = radioOn && frequency === 0 && activeSlices === 0; + $: showRadioOnWithSliceNoFreq = radioOn && frequency === 0 && activeSlices > 0; + $: showNotConnected = !connected; + $: showConnectedNoRadio = connected && !radioOn;
@@ -237,7 +259,8 @@ 📻
- {#if connected && frequency > 0} + {#if showFrequency} +
{formatFrequency(frequency)} @@ -256,17 +279,51 @@ {mode} {/if} + + {:else if showRadioOnWithSliceNoFreq} + +
+ + Slice active, waiting for frequency... + {#if model} + {model} + {/if} + {#if callsign} + {callsign} + {/if} +
- {#if txEnabled} - - TX - - {/if} - {:else} + {:else if showRadioOnNoSlice} + +
+ + {radioInfo || 'Radio is on'} + {#if model} + {model} + {/if} + {#if callsign} + {callsign} + {/if} +
+ + {:else if showConnectedNoRadio} + +
+ + TCP connected, radio off +
+ + {:else if showNotConnected} + FlexRadio non connecté + + {:else} + + Checking FlexRadio... {/if}
+
@@ -304,7 +361,7 @@ {#if isGrayline} - ✨ GRAYLINE + ✨ Grayline {:else if timeToNextEvent} @@ -312,7 +369,7 @@ {/if} {:else} - 📍 Position non configurée + 📍 Position not set {/if} @@ -342,7 +399,7 @@ {#if !hasAnyWarning}
- Météo OK + Weather OK
{/if} @@ -444,6 +501,58 @@ 50% { opacity: 0.6; } } + .slice-waiting { + color: #fbbf24; /* Jaune pour "en attente" */ + animation: pulse 1.5s infinite; +} + + /* Radio status indicators */ + .radio-status { + display: flex; + align-items: center; + gap: 8px; + } + + .radio-on-indicator { + color: #22c55e; + font-size: 16px; + animation: pulse 2s infinite; + } + + .radio-off-indicator { + color: #ef4444; + font-size: 16px; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + .radio-status-text { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + } + + .model-badge { + padding: 3px 8px; + background: rgba(79, 195, 247, 0.2); + border: 1px solid rgba(79, 195, 247, 0.4); + border-radius: 4px; + font-size: 12px; + color: #4fc3f7; + } + + .callsign-badge { + padding: 3px 8px; + background: rgba(34, 197, 94, 0.2); + border: 1px solid rgba(34, 197, 94, 0.4); + border-radius: 4px; + font-size: 12px; + font-weight: 600; + color: #22c55e; + } + .no-signal { color: rgba(255, 255, 255, 0.4); font-size: 14px;