diff --git a/cmd/sdrd/pipeline_decision_test.go b/cmd/sdrd/pipeline_decision_test.go new file mode 100644 index 0000000..30a8719 --- /dev/null +++ b/cmd/sdrd/pipeline_decision_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "testing" + + "sdr-wideband-suite/internal/classifier" + "sdr-wideband-suite/internal/pipeline" +) + +func TestDecisionCanEnableRecorderFlags(t *testing.T) { + rt := &dspRuntime{} + rt.cfg.Recorder.Enabled = false + rt.cfg.Recorder.AutoDecode = false + policy := pipeline.Policy{AutoRecordClasses: []string{"WFM"}, AutoDecodeClasses: []string{"WFM"}} + decision := pipeline.DecideSignalAction(policy, pipeline.Candidate{ID: 1, Hint: "WFM"}, &classifier.Classification{ModType: classifier.ClassWFM}) + if decision.ShouldRecord { + rt.cfg.Recorder.Enabled = true + } + if decision.ShouldAutoDecode { + rt.cfg.Recorder.AutoDecode = true + } + if !rt.cfg.Recorder.Enabled || !rt.cfg.Recorder.AutoDecode { + t.Fatalf("expected recorder flags to be enabled") + } +} diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index 6c65d72..041a3db 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -219,9 +219,11 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extracti policy := pipeline.PolicyFromConfig(rt.cfg) candidates := pipeline.CandidatesFromSignals(art.detected, "surveillance-detector") scheduled := pipeline.ScheduleCandidates(candidates, policy) - selected := make([]detector.Signal, 0, len(scheduled)) + selectedCandidates := make([]pipeline.Candidate, 0, len(scheduled)) + selectedSignals := make([]detector.Signal, 0, len(scheduled)) for _, sc := range scheduled { - selected = append(selected, detector.Signal{ + selectedCandidates = append(selectedCandidates, sc.Candidate) + selectedSignals = append(selectedSignals, detector.Signal{ ID: sc.Candidate.ID, FirstBin: sc.Candidate.FirstBin, LastBin: sc.Candidate.LastBin, @@ -232,14 +234,21 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extracti NoiseDb: sc.Candidate.NoiseDb, }) } - snips, snipRates := extractSignalIQBatch(extractMgr, art.iq, rt.cfg.SampleRate, rt.cfg.CenterHz, selected) - refined := pipeline.RefineCandidates(pipeline.CandidatesFromSignals(selected, "scheduled-candidate"), art.spectrum, rt.cfg.SampleRate, rt.cfg.FFTSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode)) + snips, snipRates := extractSignalIQBatch(extractMgr, art.iq, rt.cfg.SampleRate, rt.cfg.CenterHz, selectedSignals) + refined := pipeline.RefineCandidates(selectedCandidates, art.spectrum, rt.cfg.SampleRate, rt.cfg.FFTSize, snips, snipRates, classifier.ClassifierMode(rt.cfg.ClassifierMode)) signals := make([]detector.Signal, 0, len(refined)) for i, ref := range refined { sig := ref.Signal signals = append(signals, sig) cls := sig.Class snipRate := ref.SnippetRate + decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls) + if decision.ShouldAutoDecode && rec != nil { + rt.cfg.Recorder.AutoDecode = true + } + if decision.ShouldRecord && rec != nil { + rt.cfg.Recorder.Enabled = true + } if cls != nil { pll := classifier.PLLResult{} if i < len(snips) && snips[i] != nil && len(snips[i]) > 256 { diff --git a/internal/pipeline/decisions.go b/internal/pipeline/decisions.go new file mode 100644 index 0000000..c857f65 --- /dev/null +++ b/internal/pipeline/decisions.go @@ -0,0 +1,32 @@ +package pipeline + +import "sdr-wideband-suite/internal/classifier" + +type SignalDecision struct { + Candidate Candidate `json:"candidate"` + Class string `json:"class,omitempty"` + ShouldRecord bool `json:"should_record"` + ShouldAutoDecode bool `json:"should_auto_decode"` + Reason string `json:"reason,omitempty"` +} + +func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Classification) SignalDecision { + decision := SignalDecision{Candidate: candidate} + if cls != nil { + decision.Class = string(cls.ModType) + } + if cls != nil && WantsClass(policy.AutoRecordClasses, string(cls.ModType)) { + decision.ShouldRecord = true + decision.Reason = "matched auto_record_classes" + } + if cls != nil && WantsClass(policy.AutoDecodeClasses, string(cls.ModType)) { + decision.ShouldAutoDecode = true + if decision.Reason == "" { + decision.Reason = "matched auto_decode_classes" + } + } + if decision.Reason == "" && candidate.Hint != "" { + decision.Reason = "policy evaluated candidate hint" + } + return decision +} diff --git a/internal/pipeline/decisions_test.go b/internal/pipeline/decisions_test.go new file mode 100644 index 0000000..c130c1f --- /dev/null +++ b/internal/pipeline/decisions_test.go @@ -0,0 +1,19 @@ +package pipeline + +import ( + "testing" + + "sdr-wideband-suite/internal/classifier" +) + +func TestDecideSignalAction(t *testing.T) { + policy := Policy{AutoRecordClasses: []string{"WFM"}, AutoDecodeClasses: []string{"RDS", "WFM"}} + cls := &classifier.Classification{ModType: classifier.ClassWFM} + decision := DecideSignalAction(policy, Candidate{ID: 1, Hint: "WFM"}, cls) + if !decision.ShouldRecord { + t.Fatalf("expected record decision") + } + if !decision.ShouldAutoDecode { + t.Fatalf("expected auto decode decision") + } +}