| @@ -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") | |||
| } | |||
| } | |||
| @@ -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 { | |||
| @@ -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 | |||
| } | |||
| @@ -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") | |||
| } | |||
| } | |||