This commit is contained in:
2026-02-28 10:52:04 +01:00
parent 0f2dc76d55
commit 238716fdae
5 changed files with 209 additions and 113 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -3
View File
@@ -7,11 +7,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-BqlArLJ0.js"></script> <script type="module" crossorigin src="/assets/index-Drom3Zfz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bl7hatTL.css"> <link rel="stylesheet" crossorigin href="/assets/index-pnuRxXpy.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
+173 -88
View File
@@ -45,6 +45,10 @@ type Client struct {
onFrequencyChange func(freqMHz float64) onFrequencyChange func(freqMHz float64)
checkTransmitAllowed func() bool checkTransmitAllowed func() bool
// Track current slice frequency
currentFreq float64
currentFreqMu sync.RWMutex
} }
func New(host string, port int) *Client { func New(host string, port int) *Client {
@@ -57,9 +61,13 @@ func New(host string, port int) *Client {
radioInfo: make(map[string]string), radioInfo: make(map[string]string),
activeSlices: []int{}, activeSlices: []int{},
lastStatus: &Status{ lastStatus: &Status{
Connected: false, Connected: false,
RadioOn: false, RadioOn: false,
Tx: false, // Initialisé à false
ActiveSlices: 0,
Frequency: 0,
}, },
currentFreq: 0,
} }
} }
@@ -318,21 +326,108 @@ func (c *Client) handleMessage(msg string) {
// DEBUG: Log tous les messages reçus // DEBUG: Log tous les messages reçus
log.Printf("FlexRadio RAW: %s", msg) log.Printf("FlexRadio RAW: %s", msg)
// Router selon le premier caractère // Vérifier le type de message
switch msg[0] { if len(msg) < 2 {
case 'R': // Réponse à une commande return
}
// Messages commençant par R (réponses)
if msg[0] == 'R' {
c.handleCommandResponse(msg) c.handleCommandResponse(msg)
case 'S': // Message de statut return
c.handleStatusMessage(msg) }
case 'V': // Version/Handle
// Messages commençant par S (statut)
if msg[0] == 'S' {
// Enlever le préfixe S
msg = msg[1:]
// Séparer handle et données
parts := strings.SplitN(msg, "|", 2)
if len(parts) < 2 {
return
}
handle := parts[0]
data := parts[1]
// Parser les paires clé=valeur
statusMap := make(map[string]string)
pairs := strings.Fields(data)
for _, pair := range pairs {
if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
statusMap[kv[0]] = kv[1]
}
}
// Identifier le type de message
if strings.Contains(data, "interlock") {
c.handleInterlockStatus(handle, statusMap)
} else if strings.Contains(data, "slice") {
c.handleSliceStatus(handle, statusMap)
} else if strings.Contains(data, "radio") {
c.handleRadioStatus(handle, statusMap)
} else {
// Vérifier si c'est une mise à jour de fréquence
if freqStr, ok := statusMap["RF_frequency"]; ok {
c.handleFrequencyUpdate(handle, freqStr, statusMap)
} else {
log.Printf("FlexRadio: Message inconnu (handle=%s): %s", handle, data)
}
}
return
}
// Autres types de messages
switch msg[0] {
case 'V':
log.Printf("FlexRadio: Version/Handle: %s", msg) log.Printf("FlexRadio: Version/Handle: %s", msg)
case 'M': // Message général case 'M':
log.Printf("FlexRadio: Message: %s", msg) log.Printf("FlexRadio: Message: %s", msg)
default: default:
log.Printf("FlexRadio: Unknown message type: %s", msg) log.Printf("FlexRadio: Type de message inconnu: %s", msg)
} }
} }
func (c *Client) handleSliceStatus(handle string, statusMap map[string]string) {
c.statusMu.Lock()
defer c.statusMu.Unlock()
if c.lastStatus == nil {
return
}
// Mettre à jour le nombre de slices actives
c.lastStatus.ActiveSlices = 1
// Mettre à jour la fréquence
if rfFreq, ok := statusMap["RF_frequency"]; ok {
if freq, err := strconv.ParseFloat(rfFreq, 64); err == nil && freq > 0 {
oldFreq := c.lastStatus.Frequency
c.lastStatus.Frequency = freq
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq)
// Déclencher le callback si la fréquence a changé
if oldFreq != freq && c.onFrequencyChange != nil {
go c.onFrequencyChange(freq)
}
} else if freq == 0 {
// Fréquence 0 = slice inactive
c.lastStatus.Frequency = 0
c.lastStatus.RadioInfo = "Slice inactive"
}
}
// Mettre à jour le mode
if mode, ok := statusMap["mode"]; ok {
c.lastStatus.Mode = mode
}
// NE PAS utiliser tx du slice pour l'état TX réel
// tx=1 dans le slice signifie seulement "capable de TX", pas "en train de TX"
// L'état TX réel vient de l'interlock
}
func (c *Client) handleCommandResponse(msg string) { func (c *Client) handleCommandResponse(msg string) {
// Format: R<seq>|<status>|<data> // Format: R<seq>|<status>|<data>
parts := strings.SplitN(msg, "|", 3) parts := strings.SplitN(msg, "|", 3)
@@ -381,39 +476,6 @@ func isSliceListResponse(data string) bool {
return true return true
} }
func (c *Client) handleStatusMessage(msg string) {
parts := strings.SplitN(msg, "|", 2)
if len(parts) < 2 {
return
}
handle := parts[0][1:]
data := parts[1]
statusMap := make(map[string]string)
pairs := strings.Fields(data)
for _, pair := range pairs {
if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
statusMap[kv[0]] = kv[1]
}
}
switch {
case strings.Contains(msg, "interlock"):
c.handleInterlockStatus(handle, statusMap)
case strings.Contains(msg, "slice"):
c.handleSliceStatus(handle, statusMap)
case strings.Contains(msg, "radio"):
c.handleRadioStatus(handle, statusMap)
default:
log.Printf("FlexRadio: Unknown status (handle=%s): %s", handle, msg)
}
}
func (c *Client) handleInterlockStatus(handle string, statusMap map[string]string) { func (c *Client) handleInterlockStatus(handle string, statusMap map[string]string) {
c.statusMu.Lock() c.statusMu.Lock()
defer c.statusMu.Unlock() defer c.statusMu.Unlock()
@@ -424,45 +486,57 @@ func (c *Client) handleInterlockStatus(handle string, statusMap map[string]strin
} }
} }
func (c *Client) handleSliceStatus(handle string, statusMap map[string]string) { func (c *Client) handleRadioStatus(handle string, statusMap map[string]string) {
c.statusMu.Lock() c.statusMu.Lock()
defer c.statusMu.Unlock() defer c.statusMu.Unlock()
// Quand on reçoit un message de slice, on a au moins une slice active if c.lastStatus == nil {
c.lastStatus.ActiveSlices = 1 return
}
if rfFreq, ok := statusMap["RF_frequency"]; ok { // Mettre à jour les informations radio
if freq, err := strconv.ParseFloat(rfFreq, 64); err == nil && freq > 0 { c.lastStatus.RadioOn = true
c.lastStatus.Frequency = freq c.lastStatus.Connected = true
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq)
if c.onFrequencyChange != nil { // Mettre à jour le nombre de slices
go c.onFrequencyChange(freq) if slices, ok := statusMap["slices"]; ok {
} if num, err := strconv.Atoi(slices); err == nil {
} else if freq == 0 { c.lastStatus.NumSlices = num
// 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 { // Mettre à jour le callsign
c.lastStatus.Mode = mode if callsign, ok := statusMap["callsign"]; ok {
c.lastStatus.Callsign = callsign
} }
if tx, ok := statusMap["tx"]; ok { // Mettre à jour les autres infos
c.lastStatus.Tx = (tx == "1") if nickname, ok := statusMap["nickname"]; ok {
c.lastStatus.RadioInfo = fmt.Sprintf("Radio: %s", nickname)
} }
} }
func (c *Client) handleRadioStatus(handle string, statusMap map[string]string) { func (c *Client) handleFrequencyUpdate(handle string, freqStr string, statusMap map[string]string) {
if slices, ok := statusMap["slices"]; ok { c.statusMu.Lock()
if num, err := strconv.Atoi(slices); err == nil { defer c.statusMu.Unlock()
c.statusMu.Lock()
c.lastStatus.NumSlices = num if c.lastStatus == nil {
c.statusMu.Unlock() return
}
// Parser la fréquence
if freq, err := strconv.ParseFloat(freqStr, 64); err == nil && freq > 0 {
oldFreq := c.lastStatus.Frequency
c.lastStatus.Frequency = freq
c.lastStatus.RadioInfo = fmt.Sprintf("Active on %.3f MHz", freq)
// Déclencher le callback si la fréquence a changé
if oldFreq != freq && c.onFrequencyChange != nil {
go c.onFrequencyChange(freq)
} }
} }
log.Printf("FlexRadio: Frequency update: %s MHz", freqStr)
} }
func (c *Client) parseInfoResponse(data string) { func (c *Client) parseInfoResponse(data string) {
@@ -506,11 +580,18 @@ func (c *Client) parseInfoResponse(data string) {
c.radioInfoMu.Unlock() c.radioInfoMu.Unlock()
// Mettre à jour le statut
c.updateRadioStatus(true, "Radio is on") c.updateRadioStatus(true, "Radio is on")
go func() { go func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
c.SendSliceList() c.SendSliceList()
// S'abonner aux mises à jour
time.Sleep(200 * time.Millisecond)
c.sendCommand("sub slice all")
time.Sleep(100 * time.Millisecond)
c.sendCommand("sub interlock 0")
}() }()
} }
@@ -555,31 +636,33 @@ func (c *Client) updateRadioStatus(isOn bool, info string) {
c.statusMu.Lock() c.statusMu.Lock()
defer c.statusMu.Unlock() defer c.statusMu.Unlock()
if c.lastStatus != nil { if c.lastStatus == nil {
c.lastStatus.RadioOn = isOn return
c.lastStatus.RadioInfo = info }
c.radioInfoMu.RLock() c.lastStatus.RadioOn = isOn
if callsign, ok := c.radioInfo["callsign"]; ok { c.lastStatus.RadioInfo = info
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.radioInfoMu.RLock()
c.lastStatus.RadioInfo = "Radio is on without any active slice" 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() { func (c *Client) reconnectionMonitor() {
@@ -716,10 +799,12 @@ func (c *Client) GetStatus() (*Status, error) {
return &Status{ return &Status{
Connected: false, Connected: false,
RadioOn: false, RadioOn: false,
Tx: false,
RadioInfo: "Not initialized", RadioInfo: "Not initialized",
}, nil }, nil
} }
// Créer une copie
status := *c.lastStatus status := *c.lastStatus
return &status, nil return &status, nil
} }
+9 -9
View File
@@ -3,16 +3,16 @@ 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"`
Frequency float64 `json:"frequency"` RadioOn bool `json:"radio_on"`
RadioInfo string `json:"radio_info"`
Frequency float64 `json:"frequency"` // Primary frequency in MHz
Mode string `json:"mode"` Mode string `json:"mode"`
Tx bool `json:"tx"` Tx bool `json:"tx"` // Actually transmitting
RadioOn bool `json:"radio_on"` // Radio is powered on and responding ActiveSlices int `json:"active_slices"`
RadioInfo string `json:"radio_info"` // Additional info about radio state NumSlices int `json:"num_slices"`
Callsign string `json:"callsign"` // From info command Callsign string `json:"callsign"`
Model string `json:"model"` // From info command Model string `json:"model"`
SoftwareVer string `json:"software_ver"` // From info command SoftwareVer string `json:"software_ver"`
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