diff --git a/README.md b/README.md index 2984a8d..967cc82 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ go build -tags sdrplay ./cmd/sdrd - `POST /api/config` - `POST /api/sdr/settings` - `GET /api/gpu` +- `GET /api/pipeline/policy` +- `GET /api/pipeline/recommendations` ### Signals / Events - `GET /api/signals` → current live signals diff --git a/cmd/sdrd/http_handlers.go b/cmd/sdrd/http_handlers.go index 2ed3f12..853a2a7 100644 --- a/cmd/sdrd/http_handlers.go +++ b/cmd/sdrd/http_handlers.go @@ -133,6 +133,21 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime cfg := cfgManager.Snapshot() _ = json.NewEncoder(w).Encode(pipeline.PolicyFromConfig(cfg)) }) + mux.HandleFunc("/api/pipeline/recommendations", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + cfg := cfgManager.Snapshot() + policy := pipeline.PolicyFromConfig(cfg) + 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, + } + _ = json.NewEncoder(w).Encode(recommend) + }) mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") limit := 200 diff --git a/internal/pipeline/goals.go b/internal/pipeline/goals.go new file mode 100644 index 0000000..cca092e --- /dev/null +++ b/internal/pipeline/goals.go @@ -0,0 +1,29 @@ +package pipeline + +import "strings" + +func WantsClass(values []string, class string) bool { + if len(values) == 0 || class == "" { + return false + } + for _, v := range values { + if strings.EqualFold(strings.TrimSpace(v), class) { + return true + } + } + return false +} + +func CandidatePriorityBoost(policy Policy, hint string) float64 { + h := strings.ToLower(strings.TrimSpace(hint)) + for i, want := range policy.SignalPriorities { + w := strings.ToLower(strings.TrimSpace(want)) + if w == "" { + continue + } + if strings.Contains(h, w) || strings.Contains(w, h) { + return float64(len(policy.SignalPriorities)-i) * 3.0 + } + } + return 0 +} diff --git a/internal/pipeline/goals_test.go b/internal/pipeline/goals_test.go new file mode 100644 index 0000000..cff9e90 --- /dev/null +++ b/internal/pipeline/goals_test.go @@ -0,0 +1,19 @@ +package pipeline + +import "testing" + +func TestWantsClass(t *testing.T) { + if !WantsClass([]string{"WFM", "DMR"}, "wfm") { + t.Fatalf("expected case-insensitive match") + } + if WantsClass([]string{"DMR"}, "WFM") { + t.Fatalf("unexpected match") + } +} + +func TestCandidatePriorityBoost(t *testing.T) { + p := Policy{SignalPriorities: []string{"voice", "digital", "cw"}} + if boost := CandidatePriorityBoost(p, "digital-burst"); boost <= 0 { + t.Fatalf("expected positive boost, got %v", boost) + } +} diff --git a/internal/pipeline/scheduler.go b/internal/pipeline/scheduler.go index 57b3ad5..dc7f14e 100644 --- a/internal/pipeline/scheduler.go +++ b/internal/pipeline/scheduler.go @@ -19,7 +19,7 @@ func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandid if c.SNRDb < policy.MinCandidateSNRDb { continue } - priority := c.SNRDb + priority := c.SNRDb + CandidatePriorityBoost(policy, c.Hint) if c.BandwidthHz > 0 { priority += minFloat64(c.BandwidthHz/25000.0, 6) } diff --git a/internal/pipeline/scheduler_test.go b/internal/pipeline/scheduler_test.go index 7dc060a..452293f 100644 --- a/internal/pipeline/scheduler_test.go +++ b/internal/pipeline/scheduler_test.go @@ -21,3 +21,14 @@ func TestScheduleCandidates(t *testing.T) { t.Fatalf("expected next strongest candidate second, got id=%d", got[1].Candidate.ID) } } + +func TestScheduleCandidatesPriorityBoost(t *testing.T) { + policy := Policy{MaxRefinementJobs: 1, MinCandidateSNRDb: 0, SignalPriorities: []string{"digital"}} + got := ScheduleCandidates([]Candidate{ + {ID: 1, SNRDb: 15, Hint: "voice"}, + {ID: 2, SNRDb: 14, Hint: "digital-burst"}, + }, policy) + if len(got) != 1 || got[0].Candidate.ID != 2 { + t.Fatalf("expected priority boost to favor digital candidate, got %+v", got) + } +} diff --git a/internal/pipeline/types.go b/internal/pipeline/types.go index 9bb959b..858d0ab 100644 --- a/internal/pipeline/types.go +++ b/internal/pipeline/types.go @@ -32,6 +32,10 @@ type Refinement struct { func CandidatesFromSignals(signals []detector.Signal, source string) []Candidate { out := make([]Candidate, 0, len(signals)) for _, s := range signals { + hint := "" + if s.Class != nil { + hint = string(s.Class.ModType) + } out = append(out, Candidate{ ID: s.ID, CenterHz: s.CenterHz, @@ -42,6 +46,7 @@ func CandidatesFromSignals(signals []detector.Signal, source string) []Candidate LastBin: s.LastBin, NoiseDb: s.NoiseDb, Source: source, + Hint: hint, }) } return out