5 Commits

Author SHA1 Message Date
0f2dc76d55 up 2026-01-16 01:28:28 +01:00
5ced01c010 update 2026-01-16 01:17:28 +01:00
30688ad644 working reconnect and slices 2026-01-15 22:39:39 +01:00
3e169fe615 update 2026-01-15 22:34:44 +01:00
21db2addff up 2026-01-15 22:19:32 +01:00
4 changed files with 634 additions and 104 deletions

View File

@@ -121,6 +121,9 @@ func (dm *DeviceManager) Initialize() error {
dm.config.Devices.FlexRadio.Port, 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) // Set callback for immediate frequency changes (no waiting for update cycle)
dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) { dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) {
dm.handleFrequencyChange(freqMHz) dm.handleFrequencyChange(freqMHz)
@@ -421,9 +424,7 @@ func (dm *DeviceManager) updateStatus() {
// Check cooldown to prevent rapid fire commands // Check cooldown to prevent rapid fire commands
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime) timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
if timeSinceLastUpdate < dm.freqUpdateCooldown { if timeSinceLastUpdate > dm.freqUpdateCooldown {
log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate)
} else {
log.Printf("Auto-track (%s): Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", radioSource, freqDiff, radioFreqKhz, directionToUse) 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 // Send to Ultrabeam with saved or current direction

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode"
) )
type Client struct { type Client struct {
@@ -17,8 +18,8 @@ type Client struct {
conn net.Conn conn net.Conn
reader *bufio.Reader reader *bufio.Reader
connMu sync.Mutex // For connection management connMu sync.Mutex
writeMu sync.Mutex // For writing to connection (separate from reads) writeMu sync.Mutex
lastStatus *Status lastStatus *Status
statusMu sync.RWMutex statusMu sync.RWMutex
@@ -29,22 +30,49 @@ type Client struct {
running bool running bool
stopChan chan struct{} stopChan chan struct{}
// Callbacks reconnectInterval time.Duration
reconnectAttempts int
maxReconnectDelay time.Duration
radioInfo map[string]string
radioInfoMu sync.RWMutex
lastInfoCheck time.Time
infoCheckTimer *time.Timer
activeSlices []int
activeSlicesMu sync.RWMutex
sliceListTimer *time.Timer
onFrequencyChange func(freqMHz float64) onFrequencyChange func(freqMHz float64)
checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving) checkTransmitAllowed func() bool
} }
func New(host string, port int) *Client { func New(host string, port int) *Client {
return &Client{ return &Client{
host: host, host: host,
port: port, port: port,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
reconnectInterval: 5 * time.Second,
maxReconnectDelay: 60 * time.Second,
radioInfo: make(map[string]string),
activeSlices: []int{},
lastStatus: &Status{ lastStatus: &Status{
Connected: false, Connected: false,
RadioOn: false,
}, },
} }
} }
// SetReconnectInterval sets the reconnection interval
func (c *Client) SetReconnectInterval(interval time.Duration) {
c.reconnectInterval = interval
}
// SetMaxReconnectDelay sets the maximum delay for exponential backoff
func (c *Client) SetMaxReconnectDelay(delay time.Duration) {
c.maxReconnectDelay = delay
}
// SetFrequencyChangeCallback sets the callback function called when frequency changes // SetFrequencyChangeCallback sets the callback function called when frequency changes
func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) { func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) {
c.onFrequencyChange = callback c.onFrequencyChange = callback
@@ -73,8 +101,9 @@ func (c *Client) Connect() error {
c.conn = conn c.conn = conn
c.reader = bufio.NewReader(conn) c.reader = bufio.NewReader(conn)
c.reconnectAttempts = 0
log.Println("FlexRadio: Connected successfully") log.Println("FlexRadio: TCP connection established")
return nil return nil
} }
@@ -83,14 +112,16 @@ func (c *Client) Start() error {
return nil return nil
} }
// Try initial connection
if err := c.Connect(); err != nil { if err := c.Connect(); err != nil {
return err log.Printf("FlexRadio: Initial connection failed: %v", err)
} }
// Update connected status // Update connected status
c.statusMu.Lock() c.statusMu.Lock()
if c.lastStatus != nil { if c.lastStatus != nil {
c.lastStatus.Connected = true c.lastStatus.Connected = (c.conn != nil)
c.lastStatus.RadioOn = false
} }
c.statusMu.Unlock() c.statusMu.Unlock()
@@ -99,11 +130,27 @@ func (c *Client) Start() error {
// Start message listener // Start message listener
go c.messageLoop() go c.messageLoop()
// Subscribe to slice updates for frequency tracking // Start reconnection monitor
log.Println("FlexRadio: Subscribing to slice updates...") go c.reconnectionMonitor()
_, err := c.sendCommand("sub slice all")
if err != nil { // Start radio status checker
log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err) go c.radioStatusChecker()
// Start slice list checker
go c.sliceListChecker()
// Try to get initial radio info and subscribe to slices
if c.conn != nil {
go func() {
time.Sleep(500 * time.Millisecond)
c.SendInfo()
time.Sleep(500 * time.Millisecond)
c.SendSliceList()
time.Sleep(500 * time.Millisecond)
c.SubscribeToSlices()
}()
} }
return nil return nil
@@ -117,6 +164,14 @@ func (c *Client) Stop() {
c.running = false c.running = false
close(c.stopChan) close(c.stopChan)
// Stop timers
if c.infoCheckTimer != nil {
c.infoCheckTimer.Stop()
}
if c.sliceListTimer != nil {
c.sliceListTimer.Stop()
}
c.connMu.Lock() c.connMu.Lock()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
@@ -125,23 +180,34 @@ func (c *Client) Stop() {
} }
c.connMu.Unlock() c.connMu.Unlock()
// Update connected status // Update status
c.statusMu.Lock() c.statusMu.Lock()
if c.lastStatus != nil { if c.lastStatus != nil {
c.lastStatus.Connected = false c.lastStatus.Connected = false
c.lastStatus.RadioOn = false
c.lastStatus.RadioInfo = "Disconnected"
c.lastStatus.ActiveSlices = 0
c.lastStatus.Frequency = 0
c.lastStatus.Mode = ""
c.lastStatus.Tx = false
} }
c.statusMu.Unlock() c.statusMu.Unlock()
} }
func (c *Client) getNextSeq() int { // Helper functions for common commands
c.cmdSeqMu.Lock() func (c *Client) SendInfo() error {
defer c.cmdSeqMu.Unlock() return c.sendCommand("info")
c.cmdSeq++
return c.cmdSeq
} }
func (c *Client) sendCommand(cmd string) (string, error) { func (c *Client) SendSliceList() error {
// Use writeMu instead of connMu to avoid blocking on messageLoop reads return c.sendCommand("slice list")
}
func (c *Client) SubscribeToSlices() error {
return c.sendCommand("sub slice all")
}
func (c *Client) sendCommand(cmd string) error {
c.writeMu.Lock() c.writeMu.Lock()
defer c.writeMu.Unlock() defer c.writeMu.Unlock()
@@ -150,7 +216,7 @@ func (c *Client) sendCommand(cmd string) (string, error) {
c.connMu.Unlock() c.connMu.Unlock()
if conn == nil { if conn == nil {
return "", fmt.Errorf("not connected") return fmt.Errorf("not connected")
} }
seq := c.getNextSeq() seq := c.getNextSeq()
@@ -160,14 +226,35 @@ func (c *Client) sendCommand(cmd string) (string, error) {
_, err := conn.Write([]byte(fullCmd)) _, err := conn.Write([]byte(fullCmd))
if err != nil { if err != nil {
// Mark connection as broken
c.connMu.Lock() c.connMu.Lock()
c.conn = nil if c.conn != nil {
c.reader = nil c.conn.Close()
c.conn = nil
c.reader = nil
}
c.connMu.Unlock() c.connMu.Unlock()
return "", fmt.Errorf("failed to send command: %w", err)
// 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)
} }
return "", nil return nil
}
func (c *Client) getNextSeq() int {
c.cmdSeqMu.Lock()
defer c.cmdSeqMu.Unlock()
c.cmdSeq++
return c.cmdSeq
} }
func (c *Client) messageLoop() { func (c *Client) messageLoop() {
@@ -178,25 +265,20 @@ func (c *Client) messageLoop() {
if c.conn == nil || c.reader == nil { if c.conn == nil || c.reader == nil {
c.connMu.Unlock() c.connMu.Unlock()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if err := c.Connect(); err != nil {
log.Printf("FlexRadio: Reconnect failed: %v", err)
continue
}
continue continue
} }
// Set read deadline to allow periodic checks
c.conn.SetReadDeadline(time.Now().Add(2 * time.Second)) c.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
line, err := c.reader.ReadString('\n') line, err := c.reader.ReadString('\n')
c.connMu.Unlock() c.connMu.Unlock()
if err != nil { if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Timeout is expected, continue
continue continue
} }
log.Printf("FlexRadio: Read error: %v", err) log.Printf("FlexRadio: Read error: %v", err)
c.connMu.Lock() c.connMu.Lock()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
@@ -205,10 +287,11 @@ func (c *Client) messageLoop() {
} }
c.connMu.Unlock() c.connMu.Unlock()
// Update connected status
c.statusMu.Lock() c.statusMu.Lock()
if c.lastStatus != nil { if c.lastStatus != nil {
c.lastStatus.Connected = false c.lastStatus.Connected = false
c.lastStatus.RadioOn = false
c.lastStatus.RadioInfo = "Connection lost"
} }
c.statusMu.Unlock() c.statusMu.Unlock()
continue continue
@@ -225,87 +308,402 @@ func (c *Client) messageLoop() {
log.Println("FlexRadio: Message loop stopped") log.Println("FlexRadio: Message loop stopped")
} }
// Message handling - SIMPLIFIED VERSION
func (c *Client) handleMessage(msg string) { func (c *Client) handleMessage(msg string) {
// Response format: R<seq>|<status>|<data> msg = strings.TrimSpace(msg)
if strings.HasPrefix(msg, "R") { if msg == "" {
c.handleResponse(msg)
return return
} }
// Status format: S<handle>|<key>=<value> ... // DEBUG: Log tous les messages reçus
if strings.HasPrefix(msg, "S") { log.Printf("FlexRadio RAW: %s", msg)
c.handleStatus(msg)
return
}
// Version/handle format: V<version>|H<handle> // Router selon le premier caractère
if strings.HasPrefix(msg, "V") { switch msg[0] {
log.Printf("FlexRadio: Version/Handle received: %s", msg) case 'R': // Réponse à une commande
return c.handleCommandResponse(msg)
} case 'S': // Message de statut
c.handleStatusMessage(msg)
// Message format: M<handle>|<message> case 'V': // Version/Handle
if strings.HasPrefix(msg, "M") { log.Printf("FlexRadio: Version/Handle: %s", msg)
case 'M': // Message général
log.Printf("FlexRadio: Message: %s", msg) log.Printf("FlexRadio: Message: %s", msg)
return default:
log.Printf("FlexRadio: Unknown message type: %s", msg)
} }
} }
func (c *Client) handleResponse(msg string) { func (c *Client) handleCommandResponse(msg string) {
// Format: R<seq>|<status>|<data> // Format: R<seq>|<status>|<data>
// Example: R21|0|000000F4
parts := strings.SplitN(msg, "|", 3) parts := strings.SplitN(msg, "|", 3)
if len(parts) < 2 { if len(parts) < 3 {
log.Printf("FlexRadio: Malformed response: %s", msg)
return return
} }
seqStr := strings.TrimPrefix(parts[0], "R")
status := parts[1] status := parts[1]
data := parts[2]
seq, _ := strconv.Atoi(seqStr)
if status != "0" { if status != "0" {
log.Printf("FlexRadio: Command error: status=%s, message=%s", status, msg) log.Printf("FlexRadio: Command error (seq=%d): %s", seq, msg)
return return
} }
log.Printf("FlexRadio: Command success (seq=%d)", seq)
// Identifier le type de réponse par son contenu
switch {
case strings.Contains(data, "model="):
c.parseInfoResponse(data)
case isSliceListResponse(data):
c.parseSliceListResponse(data)
default:
log.Printf("FlexRadio: Generic response: %s", data)
}
} }
func (c *Client) handleStatus(msg string) { func isSliceListResponse(data string) bool {
// Format: S<handle>|<key>=<value> ... data = strings.TrimSpace(data)
if data == "" {
return true
}
for _, char := range data {
if !unicode.IsDigit(char) && char != ' ' {
return false
}
}
return true
}
func (c *Client) handleStatusMessage(msg string) {
parts := strings.SplitN(msg, "|", 2) parts := strings.SplitN(msg, "|", 2)
if len(parts) < 2 { if len(parts) < 2 {
return return
} }
handle := parts[0][1:]
data := parts[1] data := parts[1]
// Parse key=value pairs
pairs := strings.Fields(data)
statusMap := make(map[string]string) statusMap := make(map[string]string)
pairs := strings.Fields(data)
for _, pair := range pairs { for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2) if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
if len(kv) == 2 {
statusMap[kv[0]] = kv[1] statusMap[kv[0]] = kv[1]
} }
} }
// Check for slice updates (frequency changes) switch {
if strings.Contains(msg, "slice") { case strings.Contains(msg, "interlock"):
if rfFreq, ok := statusMap["RF_frequency"]; ok { c.handleInterlockStatus(handle, statusMap)
freq, err := strconv.ParseFloat(rfFreq, 64)
if err == nil {
c.statusMu.Lock()
oldFreq := c.lastStatus.Frequency
c.lastStatus.Frequency = freq
c.statusMu.Unlock()
// Only log significant frequency changes (> 1 kHz) case strings.Contains(msg, "slice"):
if oldFreq == 0 || (freq-oldFreq)*(freq-oldFreq) > 0.001 { c.handleSliceStatus(handle, statusMap)
log.Printf("FlexRadio: Frequency updated to %.6f MHz", freq)
// Trigger callback for immediate auto-track case strings.Contains(msg, "radio"):
if c.onFrequencyChange != nil { c.handleRadioStatus(handle, statusMap)
go c.onFrequencyChange(freq)
} default:
} log.Printf("FlexRadio: Unknown status (handle=%s): %s", handle, msg)
}
}
func (c *Client) handleInterlockStatus(handle string, statusMap map[string]string) {
c.statusMu.Lock()
defer c.statusMu.Unlock()
if state, ok := statusMap["state"]; ok {
c.lastStatus.Tx = (state == "TRANSMITTING" || state == "TUNE")
log.Printf("FlexRadio: Interlock state=%s, TX=%v", state, c.lastStatus.Tx)
}
}
func (c *Client) handleSliceStatus(handle string, statusMap map[string]string) {
c.statusMu.Lock()
defer c.statusMu.Unlock()
// Quand on reçoit un message de slice, on a au moins une slice active
c.lastStatus.ActiveSlices = 1
if rfFreq, ok := statusMap["RF_frequency"]; ok {
if freq, err := strconv.ParseFloat(rfFreq, 64); err == nil && freq > 0 {
c.lastStatus.Frequency = freq
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq)
if c.onFrequencyChange != nil {
go c.onFrequencyChange(freq)
} }
} else if freq == 0 {
// Fréquence 0 dans le message de slice = slice inactive
c.lastStatus.Frequency = 0
c.lastStatus.RadioInfo = "Slice inactive"
}
}
if mode, ok := statusMap["mode"]; ok {
c.lastStatus.Mode = mode
}
if tx, ok := statusMap["tx"]; ok {
c.lastStatus.Tx = (tx == "1")
}
}
func (c *Client) handleRadioStatus(handle string, statusMap map[string]string) {
if slices, ok := statusMap["slices"]; ok {
if num, err := strconv.Atoi(slices); err == nil {
c.statusMu.Lock()
c.lastStatus.NumSlices = num
c.statusMu.Unlock()
}
}
}
func (c *Client) parseInfoResponse(data string) {
log.Printf("FlexRadio: Parsing info response: %s", data)
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))
}
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])
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
value = value[1 : len(value)-1]
}
c.radioInfo[key] = value
}
}
c.radioInfoMu.Unlock()
c.updateRadioStatus(true, "Radio is on")
go func() {
time.Sleep(300 * time.Millisecond)
c.SendSliceList()
}()
}
func (c *Client) parseSliceListResponse(data string) {
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)
// NE PAS effacer la fréquence ici !
// La fréquence est gérée par handleSliceStatus
// Seulement mettre à jour RadioInfo si vraiment pas de slices
if len(slices) == 0 && c.lastStatus.Frequency == 0 {
c.lastStatus.RadioInfo = "Radio is on without any active slice"
} else if len(slices) == 0 && c.lastStatus.Frequency > 0 {
// Cas spécial : fréquence mais pas de slice dans la liste
// Peut arriver temporairement, garder l'info actuelle
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", c.lastStatus.Frequency)
}
}
c.statusMu.Unlock()
log.Printf("FlexRadio: Active slices updated: %v (total: %d)", slices, len(slices))
}
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
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 isOn && c.lastStatus.Frequency == 0 && c.lastStatus.ActiveSlices == 0 {
c.lastStatus.RadioInfo = "Radio is on without any active slice"
}
}
}
func (c *Client) reconnectionMonitor() {
log.Println("FlexRadio: Reconnection monitor started")
for c.running {
c.connMu.Lock()
connected := (c.conn != nil)
c.connMu.Unlock()
if !connected {
c.reconnectAttempts++
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
go func() {
time.Sleep(500 * time.Millisecond)
c.SendInfo()
}()
}
case <-c.stopChan:
return
}
} else {
select {
case <-time.After(10 * time.Second):
case <-c.stopChan:
return
}
}
}
}
func (c *Client) calculateReconnectDelay() time.Duration {
delay := c.reconnectInterval
if c.reconnectAttempts > 1 {
multiplier := 1 << (c.reconnectAttempts - 1)
delay = c.reconnectInterval * time.Duration(multiplier)
if delay > c.maxReconnectDelay {
delay = c.maxReconnectDelay
}
}
return delay
}
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 {
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)
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = true
c.lastStatus.RadioInfo = "TCP connected, checking radio..."
}
c.statusMu.Unlock()
log.Println("FlexRadio: TCP connection reestablished")
return nil
}
func (c *Client) radioStatusChecker() {
c.infoCheckTimer = time.NewTimer(10 * time.Second)
for c.running {
select {
case <-c.infoCheckTimer.C:
c.SendInfo()
c.infoCheckTimer.Reset(10 * time.Second)
case <-c.stopChan:
return
}
}
}
func (c *Client) sliceListChecker() {
c.sliceListTimer = time.NewTimer(10 * time.Second)
for c.running {
select {
case <-c.sliceListTimer.C:
if c.IsRadioOn() {
c.SendSliceList()
}
c.sliceListTimer.Reset(10 * time.Second)
case <-c.stopChan:
return
} }
} }
} }
@@ -315,14 +713,25 @@ func (c *Client) GetStatus() (*Status, error) {
defer c.statusMu.RUnlock() defer c.statusMu.RUnlock()
if c.lastStatus == nil { if c.lastStatus == nil {
return &Status{Connected: false}, nil return &Status{
Connected: false,
RadioOn: false,
RadioInfo: "Not initialized",
}, nil
} }
// Create a copy
status := *c.lastStatus 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 &status, nil
} }
// 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 false
}
return c.lastStatus.RadioOn
}

View File

@@ -2,13 +2,17 @@ package flexradio
// Status represents the FlexRadio status // Status represents the FlexRadio status
type Status struct { type Status struct {
Connected bool `json:"connected"` Connected bool `json:"connected"`
InterlockID string `json:"interlock_id"` Frequency float64 `json:"frequency"`
InterlockState string `json:"interlock_state"` Mode string `json:"mode"`
Frequency float64 `json:"frequency"` // MHz Tx bool `json:"tx"`
Model string `json:"model"` RadioOn bool `json:"radio_on"` // Radio is powered on and responding
Serial string `json:"serial"` RadioInfo string `json:"radio_info"` // Additional info about radio state
Version string `json:"version"` 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 // InterlockState represents possible interlock states

View File

@@ -9,11 +9,16 @@
export let gustWarningThreshold = 50; // km/h export let gustWarningThreshold = 50; // km/h
export let graylineWindow = 30; // minutes avant/après sunrise/sunset export let graylineWindow = 30; // minutes avant/après sunrise/sunset
// FlexRadio frequency and mode // FlexRadio status
$: frequency = flexradio?.frequency || 0; $: frequency = flexradio?.frequency || 0;
$: mode = flexradio?.mode || ''; $: mode = flexradio?.mode || '';
$: txEnabled = flexradio?.tx || false; $: txEnabled = flexradio?.tx || false;
$: connected = flexradio?.connected || 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 // Grayline calculation
let sunrise = null; let sunrise = null;
@@ -55,6 +60,16 @@
updateTimeToNextEvent(); updateTimeToNextEvent();
} }
$: console.log('FlexRadio status:', {
connected,
radioOn,
frequency,
activeSlices,
radioInfo,
callsign,
model
});
// Simplified sun calculation (based on NOAA algorithm) // Simplified sun calculation (based on NOAA algorithm)
function getSunTimes(date, lat, lon) { function getSunTimes(date, lat, lon) {
const rad = Math.PI / 180; const rad = Math.PI / 180;
@@ -228,16 +243,24 @@
$: currentBand = getBand(frequency); $: currentBand = getBand(frequency);
$: bandColor = getBandColor(currentBand); $: 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;
</script> </script>
<div class="status-banner" class:has-warning={hasAnyWarning}> <div class="status-banner" class:has-warning={hasAnyWarning}>
<!-- FlexRadio Section --> <!-- FlexRadio Section -->
<div class="flex-section"> <div class="flex-section">
<div class="flex-icon" class:connected={connected} class:disconnected={!connected}> <div class="flex-icon" class:connected={connected} class:disconnected={!connected}>
📻 📻
</div> </div>
{#if connected && frequency > 0} {#if showFrequency}
<!-- Radio is on and has active slice with frequency -->
<div class="frequency-display"> <div class="frequency-display">
<span class="frequency" style="--band-color: {bandColor}"> <span class="frequency" style="--band-color: {bandColor}">
{formatFrequency(frequency)} {formatFrequency(frequency)}
@@ -256,17 +279,58 @@
{mode} {mode}
</span> </span>
{/if} {/if}
<!-- AFFICHAGE TX - SEULEMENT SI TX EST VRAI -->
{#if txEnabled} {#if txEnabled}
<span class="tx-indicator"> <span class="tx-indicator">
TX TX
</span> </span>
{/if} {/if}
{:else if showRadioOnWithSliceNoFreq}
<!-- Radio is on with slice but frequency is 0 (maybe slice just created) -->
<div class="radio-status">
<span class="radio-on-indicator"></span>
<span class="radio-status-text">Slice active, waiting for frequency...</span>
{#if model}
<span class="model-badge">{model}</span>
{/if}
{#if callsign}
<span class="callsign-badge">{callsign}</span>
{/if}
</div>
{:else if showRadioOnNoSlice}
<!-- Radio is on but no active slice -->
<div class="radio-status">
<span class="radio-on-indicator"></span>
<span class="radio-status-text">{radioInfo || 'Radio is on'}</span>
{#if model}
<span class="model-badge">{model}</span>
{/if}
{#if callsign}
<span class="callsign-badge">{callsign}</span>
{/if}
</div>
{:else if showConnectedNoRadio}
<!-- TCP connected but radio not responding -->
<div class="radio-status">
<span class="radio-off-indicator"></span>
<span class="radio-status-text">TCP connected, radio off</span>
</div>
{:else if showNotConnected}
<!-- Not connected at all -->
<span class="no-signal">FlexRadio not connected</span>
{:else} {:else}
<span class="no-signal">FlexRadio non connecté</span> <!-- Default/unknown state -->
<span class="no-signal">Checking FlexRadio...</span>
{/if} {/if}
</div> </div>
<!-- Separator --> <!-- Separator -->
<div class="separator"></div> <div class="separator"></div>
@@ -304,7 +368,7 @@
{#if isGrayline} {#if isGrayline}
<span class="grayline-badge" class:sunrise={graylineType === 'sunrise'} class:sunset={graylineType === 'sunset'}> <span class="grayline-badge" class:sunrise={graylineType === 'sunrise'} class:sunset={graylineType === 'sunset'}>
✨ GRAYLINE ✨ Grayline
</span> </span>
{:else if timeToNextEvent} {:else if timeToNextEvent}
<span class="next-event"> <span class="next-event">
@@ -312,7 +376,7 @@
</span> </span>
{/if} {/if}
{:else} {:else}
<span class="no-location">📍 Position non configurée</span> <span class="no-location">📍 Position not set</span>
{/if} {/if}
</div> </div>
@@ -342,7 +406,7 @@
{#if !hasAnyWarning} {#if !hasAnyWarning}
<div class="status-ok"> <div class="status-ok">
<span class="ok-icon"></span> <span class="ok-icon"></span>
<span class="ok-text">Météo OK</span> <span class="ok-text">Weather OK</span>
</div> </div>
{/if} {/if}
</div> </div>
@@ -444,6 +508,58 @@
50% { opacity: 0.6; } 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 { .no-signal {
color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 0.4);
font-size: 14px; font-size: 14px;