| @@ -106,8 +106,10 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| } | } | ||||
| var debugInfo *SpectrumDebug | var debugInfo *SpectrumDebug | ||||
| plan := state.refinementInput.Plan | plan := state.refinementInput.Plan | ||||
| windowStats := buildWindowStats(state.refinementInput.Windows) | |||||
| hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0 | hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0 | ||||
| if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan { | |||||
| hasWindows := windowStats != nil && windowStats.Count > 0 | |||||
| if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan || hasWindows { | |||||
| scoreDebug := make([]map[string]any, 0, len(displaySignals)) | scoreDebug := make([]map[string]any, 0, len(displaySignals)) | ||||
| for _, s := range displaySignals { | for _, s := range displaySignals { | ||||
| if s.Class == nil || len(s.Class.Scores) == 0 { | if s.Class == nil || len(s.Class.Scores) == 0 { | ||||
| @@ -130,6 +132,9 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| if hasPlan { | if hasPlan { | ||||
| debugInfo.RefinementPlan = &plan | debugInfo.RefinementPlan = &plan | ||||
| } | } | ||||
| if hasWindows { | |||||
| debugInfo.Windows = windowStats | |||||
| } | |||||
| } | } | ||||
| h.broadcast(SpectrumFrame{Timestamp: art.now.UnixMilli(), CenterHz: rt.cfg.CenterHz, SampleHz: rt.cfg.SampleRate, FFTSize: rt.cfg.FFTSize, Spectrum: art.spectrum, Signals: displaySignals, Debug: debugInfo}) | h.broadcast(SpectrumFrame{Timestamp: art.now.UnixMilli(), CenterHz: rt.cfg.CenterHz, SampleHz: rt.cfg.SampleRate, FFTSize: rt.cfg.FFTSize, Spectrum: art.spectrum, Signals: displaySignals, Debug: debugInfo}) | ||||
| } | } | ||||
| @@ -15,8 +15,8 @@ import ( | |||||
| "sdr-wideband-suite/internal/config" | "sdr-wideband-suite/internal/config" | ||||
| "sdr-wideband-suite/internal/detector" | "sdr-wideband-suite/internal/detector" | ||||
| "sdr-wideband-suite/internal/events" | "sdr-wideband-suite/internal/events" | ||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| fftutil "sdr-wideband-suite/internal/fft" | fftutil "sdr-wideband-suite/internal/fft" | ||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| "sdr-wideband-suite/internal/recorder" | "sdr-wideband-suite/internal/recorder" | ||||
| "sdr-wideband-suite/internal/runtime" | "sdr-wideband-suite/internal/runtime" | ||||
| ) | ) | ||||
| @@ -138,13 +138,16 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||||
| cfg := cfgManager.Snapshot() | cfg := cfgManager.Snapshot() | ||||
| policy := pipeline.PolicyFromConfig(cfg) | policy := pipeline.PolicyFromConfig(cfg) | ||||
| recommend := map[string]any{ | recommend := map[string]any{ | ||||
| "mode": policy.Mode, | |||||
| "intent": policy.Intent, | |||||
| "monitor_span_hz": policy.MonitorSpanHz, | |||||
| "signal_priorities": policy.SignalPriorities, | |||||
| "auto_record_classes": policy.AutoRecordClasses, | |||||
| "auto_decode_classes": policy.AutoDecodeClasses, | |||||
| "refinement_jobs": policy.MaxRefinementJobs, | |||||
| "mode": policy.Mode, | |||||
| "intent": policy.Intent, | |||||
| "monitor_span_hz": policy.MonitorSpanHz, | |||||
| "signal_priorities": policy.SignalPriorities, | |||||
| "auto_record_classes": policy.AutoRecordClasses, | |||||
| "auto_decode_classes": policy.AutoDecodeClasses, | |||||
| "refinement_jobs": policy.MaxRefinementJobs, | |||||
| "refinement_auto_span": policy.RefinementAutoSpan, | |||||
| "refinement_min_span_hz": policy.RefinementMinSpanHz, | |||||
| "refinement_max_span_hz": policy.RefinementMaxSpanHz, | |||||
| } | } | ||||
| _ = json.NewEncoder(w).Encode(recommend) | _ = json.NewEncoder(w).Encode(recommend) | ||||
| }) | }) | ||||
| @@ -18,6 +18,15 @@ type SpectrumDebug struct { | |||||
| NoiseFloor float64 `json:"noise_floor,omitempty"` | NoiseFloor float64 `json:"noise_floor,omitempty"` | ||||
| Scores []map[string]any `json:"scores,omitempty"` | Scores []map[string]any `json:"scores,omitempty"` | ||||
| RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | ||||
| Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` | |||||
| } | |||||
| type RefinementWindowStats struct { | |||||
| Count int `json:"count"` | |||||
| MinSpan float64 `json:"min_span_hz"` | |||||
| MaxSpan float64 `json:"max_span_hz"` | |||||
| AvgSpan float64 `json:"avg_span_hz"` | |||||
| Sources map[string]int `json:"sources,omitempty"` | |||||
| } | } | ||||
| type SpectrumFrame struct { | type SpectrumFrame struct { | ||||
| @@ -0,0 +1,37 @@ | |||||
| package main | |||||
| import "sdr-wideband-suite/internal/pipeline" | |||||
| func buildWindowStats(windows []pipeline.RefinementWindow) *RefinementWindowStats { | |||||
| if len(windows) == 0 { | |||||
| return nil | |||||
| } | |||||
| stats := &RefinementWindowStats{Sources: map[string]int{}} | |||||
| minSpan := 0.0 | |||||
| maxSpan := 0.0 | |||||
| sum := 0.0 | |||||
| for i, w := range windows { | |||||
| span := w.SpanHz | |||||
| if span <= 0 { | |||||
| continue | |||||
| } | |||||
| if i == 0 || span < minSpan { | |||||
| minSpan = span | |||||
| } | |||||
| if i == 0 || span > maxSpan { | |||||
| maxSpan = span | |||||
| } | |||||
| sum += span | |||||
| stats.Count++ | |||||
| if w.Source != "" { | |||||
| stats.Sources[w.Source]++ | |||||
| } | |||||
| } | |||||
| if stats.Count == 0 { | |||||
| return nil | |||||
| } | |||||
| stats.MinSpan = minSpan | |||||
| stats.MaxSpan = maxSpan | |||||
| stats.AvgSpan = sum / float64(stats.Count) | |||||
| return stats | |||||
| } | |||||