| @@ -106,6 +106,16 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| } else { | } else { | ||||
| displaySignals = rt.det.StableSignals() | 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.arbitration = rt.arbitration | ||||
| state.presentation = state.surveillance.DisplayLevel | state.presentation = state.surveillance.DisplayLevel | ||||
| if phaseSnap != nil { | if phaseSnap != nil { | ||||
| @@ -68,16 +68,19 @@ type activeEvent struct { | |||||
| } | } | ||||
| type Signal 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 { | func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector { | ||||
| @@ -356,6 +356,13 @@ func (m *Manager) StreamerRef() *Streamer { | |||||
| return m.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. | // ActiveStreams returns info about currently active streaming sessions. | ||||
| func (m *Manager) ActiveStreams() int { | func (m *Manager) ActiveStreams() int { | ||||
| if m == nil || m.streamer == nil { | if m == nil || m.streamer == nil { | ||||
| @@ -115,6 +115,14 @@ type audioSub struct { | |||||
| ch chan []byte | 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. | // AudioInfo describes the audio format of a live-listen subscription. | ||||
| // Sent to the WebSocket client as the first message. | // Sent to the WebSocket client as the first message. | ||||
| type AudioInfo struct { | type AudioInfo struct { | ||||
| @@ -457,6 +465,22 @@ func (st *Streamer) attachPendingListeners(sess *streamSession) { | |||||
| } | } | ||||
| // CloseAll finalises all sessions and stops the worker goroutine. | // 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() { | func (st *Streamer) CloseAll() { | ||||
| close(st.feedCh) | close(st.feedCh) | ||||
| <-st.done | <-st.done | ||||
| @@ -307,6 +307,8 @@ function isListeningSignal(signal) { | |||||
| } | } | ||||
| function getSignalPrimaryMode(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 !== '-') { | if (isListeningSignal(signal) && liveListenInfo?.playback_mode && liveListenInfo.playback_mode !== '-') { | ||||
| return liveListenInfo.playback_mode; | return liveListenInfo.playback_mode; | ||||
| } | } | ||||
| @@ -315,10 +317,12 @@ function getSignalPrimaryMode(signal) { | |||||
| } | } | ||||
| function getSignalRuntimeSummary(signal) { | function getSignalRuntimeSummary(signal) { | ||||
| if (!isListeningSignal(signal)) return ''; | |||||
| const bits = []; | 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(' · '); | return bits.join(' · '); | ||||
| } | } | ||||
| @@ -1511,15 +1515,15 @@ function _createSignalItem(s) { | |||||
| btn.dataset.id = s.id || 0; | btn.dataset.id = s.id || 0; | ||||
| const primaryMode = getSignalPrimaryMode(s); | const primaryMode = getSignalPrimaryMode(s); | ||||
| const mc = modColor(primaryMode); | const mc = modColor(primaryMode); | ||||
| const rds = s.class?.pll?.rds_station || ''; | |||||
| const dec = decisionIndex.get(String(s.id || 0)); | const dec = decisionIndex.get(String(s.id || 0)); | ||||
| const decText = dec?.reason ? `${dec.reason}` : ''; | const decText = dec?.reason ? `${dec.reason}` : ''; | ||||
| const decFlags = dec ? `${dec.record ? 'REC' : ''}${dec.decode ? (dec.record ? '+DEC' : 'DEC') : ''}` : ''; | const decFlags = dec ? `${dec.record ? 'REC' : ''}${dec.decode ? (dec.record ? '+DEC' : 'DEC') : ''}` : ''; | ||||
| const metaBits = []; | const metaBits = []; | ||||
| if (decFlags) metaBits.push(decFlags); | if (decFlags) metaBits.push(decFlags); | ||||
| if (decText) metaBits.push(decText); | if (decText) metaBits.push(decText); | ||||
| if (s.class?.pll?.rds_station) metaBits.push(`RDS ${s.class.pll.rds_station}`); | |||||
| btn.title = metaBits.join(' · '); | 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.borderLeftColor = mc.label; | ||||
| btn.style.borderLeftWidth = '3px'; | btn.style.borderLeftWidth = '3px'; | ||||
| btn.style.borderLeftStyle = 'solid'; | btn.style.borderLeftStyle = 'solid'; | ||||