Compare commits
9 Commits
feature/fl
...
flexradio/
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f2dc76d55 | |||
| 5ced01c010 | |||
| 30688ad644 | |||
| 3e169fe615 | |||
| 21db2addff | |||
| 130efeee83 | |||
| 4eeec6bdf6 | |||
| de3fda2648 | |||
| c6ceeb103b |
@@ -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
|
||||||
|
|||||||
@@ -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,26 +30,49 @@ type Client struct {
|
|||||||
running bool
|
running bool
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
onFrequencyChange func(freqMHz float64)
|
|
||||||
checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving)
|
|
||||||
|
|
||||||
// Reconnection settings
|
|
||||||
reconnectInterval time.Duration
|
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)
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
reconnectInterval: 5 * time.Second, // Reconnect every 5 seconds if disconnected
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -72,14 +96,14 @@ func (c *Client) Connect() error {
|
|||||||
|
|
||||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("FlexRadio: Connection failed: %v", err)
|
|
||||||
return fmt.Errorf("failed to connect: %w", err)
|
return fmt.Errorf("failed to connect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,37 +112,48 @@ func (c *Client) Start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try initial connection
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
log.Printf("FlexRadio: Initial connection failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connected status
|
||||||
|
c.statusMu.Lock()
|
||||||
|
if c.lastStatus != nil {
|
||||||
|
c.lastStatus.Connected = (c.conn != nil)
|
||||||
|
c.lastStatus.RadioOn = false
|
||||||
|
}
|
||||||
|
c.statusMu.Unlock()
|
||||||
|
|
||||||
c.running = true
|
c.running = true
|
||||||
|
|
||||||
// Try initial connection but don't fail if it doesn't work
|
// Start message listener
|
||||||
// The messageLoop will handle reconnection
|
|
||||||
err := c.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("FlexRadio: Initial connection failed, will retry: %v", err)
|
|
||||||
} else {
|
|
||||||
// Update connected status
|
|
||||||
c.statusMu.Lock()
|
|
||||||
if c.lastStatus != nil {
|
|
||||||
c.lastStatus.Connected = true
|
|
||||||
}
|
|
||||||
c.statusMu.Unlock()
|
|
||||||
|
|
||||||
// Subscribe to slice updates for frequency tracking
|
|
||||||
c.subscribeToSlices()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start message listener (handles reconnection)
|
|
||||||
go c.messageLoop()
|
go c.messageLoop()
|
||||||
|
|
||||||
return nil
|
// Start reconnection monitor
|
||||||
}
|
go c.reconnectionMonitor()
|
||||||
|
|
||||||
func (c *Client) subscribeToSlices() {
|
// Start radio status checker
|
||||||
log.Println("FlexRadio: Subscribing to slice updates...")
|
go c.radioStatusChecker()
|
||||||
_, err := c.sendCommand("sub slice all")
|
|
||||||
if err != nil {
|
// Start slice list checker
|
||||||
log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() {
|
func (c *Client) Stop() {
|
||||||
@@ -129,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()
|
||||||
@@ -137,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()
|
||||||
|
|
||||||
@@ -162,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()
|
||||||
@@ -172,80 +226,59 @@ 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() {
|
||||||
log.Println("FlexRadio: Message loop started")
|
log.Println("FlexRadio: Message loop started")
|
||||||
|
|
||||||
reconnectTicker := time.NewTicker(c.reconnectInterval)
|
|
||||||
defer reconnectTicker.Stop()
|
|
||||||
|
|
||||||
for c.running {
|
for c.running {
|
||||||
c.connMu.Lock()
|
|
||||||
isConnected := c.conn != nil && c.reader != nil
|
|
||||||
c.connMu.Unlock()
|
|
||||||
|
|
||||||
if !isConnected {
|
|
||||||
// Update status to disconnected
|
|
||||||
c.statusMu.Lock()
|
|
||||||
if c.lastStatus != nil {
|
|
||||||
c.lastStatus.Connected = false
|
|
||||||
}
|
|
||||||
c.statusMu.Unlock()
|
|
||||||
|
|
||||||
// Wait for reconnect interval
|
|
||||||
select {
|
|
||||||
case <-reconnectTicker.C:
|
|
||||||
log.Println("FlexRadio: Attempting to reconnect...")
|
|
||||||
if err := c.Connect(); err != nil {
|
|
||||||
log.Printf("FlexRadio: Reconnect failed: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successfully reconnected
|
|
||||||
c.statusMu.Lock()
|
|
||||||
if c.lastStatus != nil {
|
|
||||||
c.lastStatus.Connected = true
|
|
||||||
}
|
|
||||||
c.statusMu.Unlock()
|
|
||||||
|
|
||||||
// Re-subscribe to slices after reconnection
|
|
||||||
c.subscribeToSlices()
|
|
||||||
|
|
||||||
case <-c.stopChan:
|
|
||||||
log.Println("FlexRadio: Message loop stopping (stop signal received)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read from connection
|
|
||||||
c.connMu.Lock()
|
c.connMu.Lock()
|
||||||
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)
|
||||||
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()
|
||||||
@@ -254,14 +287,13 @@ 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()
|
||||||
|
|
||||||
log.Println("FlexRadio: Connection lost, will attempt reconnection...")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user