| @@ -106,6 +106,16 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||
| } else { | |||
| displaySignals = rt.det.StableSignals() | |||
| } | |||
| if rec != nil && len(displaySignals) > 0 { | |||
| runtimeInfo := rec.RuntimeInfoBySignalID() | |||
| for i := range displaySignals { | |||
| if info, ok := runtimeInfo[displaySignals[i].ID]; ok { | |||
| displaySignals[i].DemodName = info.DemodName | |||
| displaySignals[i].PlaybackMode = info.PlaybackMode | |||
| displaySignals[i].StereoState = info.StereoState | |||
| } | |||
| } | |||
| } | |||
| state.arbitration = rt.arbitration | |||
| state.presentation = state.surveillance.DisplayLevel | |||
| if phaseSnap != nil { | |||
| @@ -68,16 +68,19 @@ type activeEvent struct { | |||
| } | |||
| type Signal struct { | |||
| ID int64 `json:"id"` | |||
| FirstBin int `json:"first_bin"` | |||
| LastBin int `json:"last_bin"` | |||
| CenterHz float64 `json:"center_hz"` | |||
| BWHz float64 `json:"bw_hz"` | |||
| PeakDb float64 `json:"peak_db"` | |||
| SNRDb float64 `json:"snr_db"` | |||
| NoiseDb float64 `json:"noise_db,omitempty"` | |||
| Class *classifier.Classification `json:"class,omitempty"` | |||
| PLL *classifier.PLLResult `json:"pll,omitempty"` | |||
| ID int64 `json:"id"` | |||
| FirstBin int `json:"first_bin"` | |||
| LastBin int `json:"last_bin"` | |||
| CenterHz float64 `json:"center_hz"` | |||
| BWHz float64 `json:"bw_hz"` | |||
| PeakDb float64 `json:"peak_db"` | |||
| SNRDb float64 `json:"snr_db"` | |||
| NoiseDb float64 `json:"noise_db,omitempty"` | |||
| Class *classifier.Classification `json:"class,omitempty"` | |||
| PLL *classifier.PLLResult `json:"pll,omitempty"` | |||
| DemodName string `json:"demod,omitempty"` | |||
| PlaybackMode string `json:"playback_mode,omitempty"` | |||
| StereoState string `json:"stereo_state,omitempty"` | |||
| } | |||
| func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector { | |||
| @@ -356,6 +356,13 @@ func (m *Manager) StreamerRef() *Streamer { | |||
| return m.streamer | |||
| } | |||
| func (m *Manager) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo { | |||
| if m == nil || m.streamer == nil { | |||
| return nil | |||
| } | |||
| return m.streamer.RuntimeInfoBySignalID() | |||
| } | |||
| // ActiveStreams returns info about currently active streaming sessions. | |||
| func (m *Manager) ActiveStreams() int { | |||
| if m == nil || m.streamer == nil { | |||
| @@ -115,6 +115,14 @@ type audioSub struct { | |||
| ch chan []byte | |||
| } | |||
| type RuntimeSignalInfo struct { | |||
| DemodName string | |||
| PlaybackMode string | |||
| StereoState string | |||
| Channels int | |||
| SampleRate int | |||
| } | |||
| // AudioInfo describes the audio format of a live-listen subscription. | |||
| // Sent to the WebSocket client as the first message. | |||
| type AudioInfo struct { | |||
| @@ -457,6 +465,22 @@ func (st *Streamer) attachPendingListeners(sess *streamSession) { | |||
| } | |||
| // CloseAll finalises all sessions and stops the worker goroutine. | |||
| func (st *Streamer) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo { | |||
| st.mu.RLock() | |||
| defer st.mu.RUnlock() | |||
| out := make(map[int64]RuntimeSignalInfo, len(st.sessions)) | |||
| for _, sess := range st.sessions { | |||
| out[sess.signalID] = RuntimeSignalInfo{ | |||
| DemodName: sess.demodName, | |||
| PlaybackMode: sess.playbackMode, | |||
| StereoState: sess.stereoState, | |||
| Channels: sess.channels, | |||
| SampleRate: sess.sampleRate, | |||
| } | |||
| } | |||
| return out | |||
| } | |||
| func (st *Streamer) CloseAll() { | |||
| close(st.feedCh) | |||
| <-st.done | |||
| @@ -307,6 +307,8 @@ function isListeningSignal(signal) { | |||
| } | |||
| function getSignalPrimaryMode(signal) { | |||
| if (signal?.playback_mode) return signal.playback_mode; | |||
| if (signal?.demod) return signal.demod; | |||
| if (isListeningSignal(signal) && liveListenInfo?.playback_mode && liveListenInfo.playback_mode !== '-') { | |||
| return liveListenInfo.playback_mode; | |||
| } | |||
| @@ -315,10 +317,12 @@ function getSignalPrimaryMode(signal) { | |||
| } | |||
| function getSignalRuntimeSummary(signal) { | |||
| if (!isListeningSignal(signal)) return ''; | |||
| const bits = []; | |||
| if (liveListenInfo?.status && !['Idle', '-'].includes(liveListenInfo.status)) bits.push(liveListenInfo.status); | |||
| if (liveListenInfo?.stereo_state && liveListenInfo.stereo_state !== '-') bits.push(liveListenInfo.stereo_state); | |||
| if (signal?.stereo_state) bits.push(signal.stereo_state); | |||
| if (!bits.length && isListeningSignal(signal)) { | |||
| if (liveListenInfo?.status && !['Idle', '-'].includes(liveListenInfo.status)) bits.push(liveListenInfo.status); | |||
| if (liveListenInfo?.stereo_state && liveListenInfo.stereo_state !== '-') bits.push(liveListenInfo.stereo_state); | |||
| } | |||
| return bits.join(' · '); | |||
| } | |||
| @@ -1511,15 +1515,15 @@ function _createSignalItem(s) { | |||
| btn.dataset.id = s.id || 0; | |||
| const primaryMode = getSignalPrimaryMode(s); | |||
| const mc = modColor(primaryMode); | |||
| const rds = s.class?.pll?.rds_station || ''; | |||
| const dec = decisionIndex.get(String(s.id || 0)); | |||
| const decText = dec?.reason ? `${dec.reason}` : ''; | |||
| const decFlags = dec ? `${dec.record ? 'REC' : ''}${dec.decode ? (dec.record ? '+DEC' : 'DEC') : ''}` : ''; | |||
| const metaBits = []; | |||
| if (decFlags) metaBits.push(decFlags); | |||
| if (decText) metaBits.push(decText); | |||
| if (s.class?.pll?.rds_station) metaBits.push(`RDS ${s.class.pll.rds_station}`); | |||
| btn.title = metaBits.join(' · '); | |||
| btn.innerHTML = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="item-meta item-meta--runtime" data-field="mode" style="color:${mc.label}">${primaryMode}</span></div>`; | |||
| btn.innerHTML = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="item-meta item-meta--runtime" data-field="mode" style="color:${mc.label}">${primaryMode}</span>${rds ? `<span class="item-meta item-meta--rds" data-field="rds">${rds}</span>` : ''}</div>`; | |||
| btn.style.borderLeftColor = mc.label; | |||
| btn.style.borderLeftWidth = '3px'; | |||
| btn.style.borderLeftStyle = 'solid'; | |||