Просмотр исходного кода

Expose surveillance level summaries in API and UI

master
Jan Svabenik 8 часов назад
Родитель
Сommit
9e5df37fac
3 измененных файлов: 186 добавлений и 21 удалений
  1. +40
    -18
      cmd/sdrd/http_handlers.go
  2. +135
    -0
      cmd/sdrd/level_summary.go
  3. +11
    -3
      web/app.js

+ 40
- 18
cmd/sdrd/http_handlers.go Просмотреть файл

@@ -165,6 +165,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
snap := phaseSnap.Snapshot()
windowStats := buildWindowStats(snap.refinement.Input.Windows)
arbitration := buildArbitrationSnapshot(snap.refinement, snap.arbitration)
levelSet := snap.surveillance.LevelSet
spectraBins := map[string]int{}
for _, spec := range snap.surveillance.Spectra {
if len(spec.Spectrum) == 0 {
@@ -172,25 +173,46 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
}
spectraBins[spec.Level.Name] = len(spec.Spectrum)
}
levelSummaries := buildSurveillanceLevelSummaries(levelSet, snap.surveillance.Spectra)
candidateSources := buildCandidateSourceSummary(snap.surveillance.Candidates)
candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates)
out := map[string]any{
"plan": snap.refinement.Input.Plan,
"windows": snap.refinement.Input.Windows,
"window_stats": windowStats,
"request": snap.refinement.Input.Request,
"context": snap.refinement.Input.Context,
"detail_level": snap.refinement.Input.Detail,
"arbitration": arbitration,
"work_items": snap.refinement.Input.WorkItems,
"candidates": len(snap.refinement.Input.Candidates),
"scheduled": len(snap.refinement.Input.Scheduled),
"signals": len(snap.refinement.Result.Signals),
"decisions": len(snap.refinement.Result.Decisions),
"surveillance_level": snap.surveillance.Level,
"surveillance_levels": snap.surveillance.Levels,
"surveillance_spectra_bins": spectraBins,
"display_level": snap.surveillance.DisplayLevel,
"refinement_level": snap.refinement.Input.Level,
"presentation_level": snap.presentation,
"plan": snap.refinement.Input.Plan,
"windows": snap.refinement.Input.Windows,
"window_stats": windowStats,
"request": snap.refinement.Input.Request,
"context": snap.refinement.Input.Context,
"detail_level": snap.refinement.Input.Detail,
"arbitration": arbitration,
"work_items": snap.refinement.Input.WorkItems,
"candidates": len(snap.refinement.Input.Candidates),
"scheduled": len(snap.refinement.Input.Scheduled),
"signals": len(snap.refinement.Result.Signals),
"decisions": len(snap.refinement.Result.Decisions),
"surveillance_level": snap.surveillance.Level,
"surveillance_levels": snap.surveillance.Levels,
"surveillance_level_set": levelSet,
"surveillance_active_levels": func() []pipeline.AnalysisLevel {
if len(levelSet.All) > 0 {
return levelSet.All
}
active := make([]pipeline.AnalysisLevel, 0, len(snap.surveillance.Levels)+1)
if snap.surveillance.Level.Name != "" {
active = append(active, snap.surveillance.Level)
}
active = append(active, snap.surveillance.Levels...)
if snap.surveillance.DisplayLevel.Name != "" {
active = append(active, snap.surveillance.DisplayLevel)
}
return active
}(),
"surveillance_level_summary": levelSummaries,
"surveillance_spectra_bins": spectraBins,
"candidate_sources": candidateSources,
"candidate_evidence": candidateEvidence,
"display_level": snap.surveillance.DisplayLevel,
"refinement_level": snap.refinement.Input.Level,
"presentation_level": snap.presentation,
}
_ = json.NewEncoder(w).Encode(out)
})


+ 135
- 0
cmd/sdrd/level_summary.go Просмотреть файл

@@ -0,0 +1,135 @@
package main

import (
"sort"

"sdr-wideband-suite/internal/pipeline"
)

type SurveillanceLevelSummary struct {
Name string `json:"name"`
Role string `json:"role,omitempty"`
Truth string `json:"truth,omitempty"`
SampleRate int `json:"sample_rate,omitempty"`
FFTSize int `json:"fft_size,omitempty"`
BinHz float64 `json:"bin_hz,omitempty"`
Decimation int `json:"decimation,omitempty"`
SpanHz float64 `json:"span_hz,omitempty"`
CenterHz float64 `json:"center_hz,omitempty"`
Source string `json:"source,omitempty"`
SpectrumBins int `json:"spectrum_bins,omitempty"`
}

type CandidateEvidenceSummary struct {
Level string `json:"level"`
Provenance string `json:"provenance,omitempty"`
Count int `json:"count"`
}

func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra []pipeline.SurveillanceLevelSpectrum) map[string]SurveillanceLevelSummary {
if set.Primary.Name == "" && len(set.Derived) == 0 && set.Presentation.Name == "" && len(set.All) == 0 {
return nil
}
bins := map[string]int{}
for _, spec := range spectra {
if spec.Level.Name == "" || len(spec.Spectrum) == 0 {
continue
}
bins[spec.Level.Name] = len(spec.Spectrum)
}
levels := set.All
if len(levels) == 0 {
if set.Primary.Name != "" {
levels = append(levels, set.Primary)
}
if len(set.Derived) > 0 {
levels = append(levels, set.Derived...)
}
if set.Presentation.Name != "" {
levels = append(levels, set.Presentation)
}
}
out := make(map[string]SurveillanceLevelSummary, len(levels))
for _, level := range levels {
name := level.Name
if name == "" {
continue
}
binHz := level.BinHz
if binHz == 0 && level.SampleRate > 0 && level.FFTSize > 0 {
binHz = float64(level.SampleRate) / float64(level.FFTSize)
}
out[name] = SurveillanceLevelSummary{
Name: name,
Role: level.Role,
Truth: level.Truth,
SampleRate: level.SampleRate,
FFTSize: level.FFTSize,
BinHz: binHz,
Decimation: level.Decimation,
SpanHz: level.SpanHz,
CenterHz: level.CenterHz,
Source: level.Source,
SpectrumBins: bins[name],
}
}
if len(out) == 0 {
return nil
}
return out
}

func buildCandidateSourceSummary(candidates []pipeline.Candidate) map[string]int {
if len(candidates) == 0 {
return nil
}
out := map[string]int{}
for _, cand := range candidates {
if cand.Source == "" {
continue
}
out[cand.Source]++
}
if len(out) == 0 {
return nil
}
return out
}

func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateEvidenceSummary {
if len(candidates) == 0 {
return nil
}
type key struct {
level string
provenance string
}
counts := map[key]int{}
for _, cand := range candidates {
for _, ev := range cand.Evidence {
name := ev.Level.Name
if name == "" {
name = "unknown"
}
k := key{level: name, provenance: ev.Provenance}
counts[k]++
}
}
if len(counts) == 0 {
return nil
}
out := make([]CandidateEvidenceSummary, 0, len(counts))
for k, v := range counts {
out = append(out, CandidateEvidenceSummary{Level: k.level, Provenance: k.provenance, Count: v})
}
sort.Slice(out, func(i, j int) bool {
if out[i].Count == out[j].Count {
if out[i].Level == out[j].Level {
return out[i].Provenance < out[j].Provenance
}
return out[i].Level < out[j].Level
}
return out[i].Count > out[j].Count
})
return out
}

+ 11
- 3
web/app.js Просмотреть файл

@@ -771,7 +771,11 @@ function formatLevelSummary(level) {
const name = level.name || 'level';
const fft = level.fft_size ? `${level.fft_size} bins` : 'bins n/a';
const span = level.span_hz ? fmtHz(level.span_hz) : 'span n/a';
return `${name} · ${fft} · ${span}`;
const binHz = level.bin_hz || ((level.sample_rate && level.fft_size) ? (level.sample_rate / level.fft_size) : 0);
const binText = binHz ? `${binHz.toFixed(1)} Hz/bin` : 'bin n/a';
const decim = level.decimation && level.decimation > 1 ? `decim ${level.decimation}` : '';
const source = level.source ? `src ${level.source}` : '';
return [name, fft, span, binText, decim, source].filter(Boolean).join(' · ');
}

function queueConfigUpdate(partial) {
@@ -879,8 +883,12 @@ function updateHeroMetrics() {
if (healthRefineWindows) {
const stats = refinementInfo.window_stats || null;
if (stats && stats.count) {
const levels = refinementInfo.surveillance_level ? ` · ${formatLevelSummary(refinementInfo.surveillance_level)}` : '';
healthRefineWindows.textContent = `${fmtHz(stats.min_span_hz || 0)}–${fmtHz(stats.max_span_hz || 0)}${levels}`;
const levelSet = refinementInfo.surveillance_level_set || {};
const primary = levelSet.primary || refinementInfo.surveillance_level;
const presentation = levelSet.presentation || refinementInfo.display_level || null;
const primaryText = primary ? ` · primary ${formatLevelSummary(primary)}` : '';
const presentationText = presentation ? ` · display ${formatLevelSummary(presentation)}` : '';
healthRefineWindows.textContent = `${fmtHz(stats.min_span_hz || 0)}–${fmtHz(stats.max_span_hz || 0)}${primaryText}${presentationText}`;
} else {
const windows = refinementInfo.windows || [];
if (!Array.isArray(windows) || windows.length === 0) {


Загрузка…
Отмена
Сохранить