| @@ -29,6 +29,7 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| defer logTicker.Stop() | defer logTicker.Stop() | ||||
| enc := json.NewEncoder(eventFile) | enc := json.NewEncoder(eventFile) | ||||
| dcBlocker := dsp.NewDCBlocker(0.995) | dcBlocker := dsp.NewDCBlocker(0.995) | ||||
| state := &phaseState{} | |||||
| for { | for { | ||||
| select { | select { | ||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| @@ -55,12 +56,14 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| log.Printf("received IQ samples") | log.Printf("received IQ samples") | ||||
| rt.gotSamples = true | rt.gotSamples = true | ||||
| } | } | ||||
| finished := art.finished | |||||
| thresholds := art.thresholds | |||||
| noiseFloor := art.noiseFloor | |||||
| state.surveillance = rt.buildSurveillanceResult(art) | |||||
| finished := state.surveillance.Finished | |||||
| thresholds := state.surveillance.Thresholds | |||||
| noiseFloor := state.surveillance.NoiseFloor | |||||
| var displaySignals []detector.Signal | var displaySignals []detector.Signal | ||||
| if len(art.iq) > 0 { | if len(art.iq) > 0 { | ||||
| displaySignals = rt.refineSignals(art, extractMgr, rec) | |||||
| state.refinement = rt.refineSignals(art, extractMgr, rec) | |||||
| displaySignals = state.refinement.Signals | |||||
| if rec != nil && len(displaySignals) > 0 && len(art.allIQ) > 0 { | if rec != nil && len(displaySignals) > 0 && len(art.allIQ) > 0 { | ||||
| aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult} | aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult} | ||||
| streamSnips, streamRates := extractForStreaming(extractMgr, art.allIQ, rt.cfg.SampleRate, rt.cfg.CenterHz, displaySignals, rt.streamPhaseState, rt.streamOverlap, aqCfg) | streamSnips, streamRates := extractForStreaming(extractMgr, art.allIQ, rt.cfg.SampleRate, rt.cfg.CenterHz, displaySignals, rt.streamPhaseState, rt.streamOverlap, aqCfg) | ||||
| @@ -0,0 +1,8 @@ | |||||
| package main | |||||
| import "sdr-wideband-suite/internal/pipeline" | |||||
| type phaseState struct { | |||||
| surveillance pipeline.SurveillanceResult | |||||
| refinement pipeline.RefinementResult | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| package main | |||||
| import ( | |||||
| "testing" | |||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| ) | |||||
| func TestPhaseStateCarriesPhaseResults(t *testing.T) { | |||||
| ps := &phaseState{ | |||||
| surveillance: pipeline.SurveillanceResult{NoiseFloor: -90}, | |||||
| refinement: pipeline.RefinementResult{Decisions: []pipeline.SignalDecision{{ShouldRecord: true}}}, | |||||
| } | |||||
| if ps.surveillance.NoiseFloor != -90 { | |||||
| t.Fatalf("unexpected surveillance state: %+v", ps.surveillance) | |||||
| } | |||||
| if len(ps.refinement.Decisions) != 1 || !ps.refinement.Decisions[0].ShouldRecord { | |||||
| t.Fatalf("unexpected refinement state: %+v", ps.refinement) | |||||
| } | |||||
| } | |||||
| @@ -212,9 +212,22 @@ func (rt *dspRuntime) captureSpectrum(srcMgr *sourceManager, rec *recorder.Manag | |||||
| }, nil | }, nil | ||||
| } | } | ||||
| func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extractionManager, rec *recorder.Manager) []detector.Signal { | |||||
| func (rt *dspRuntime) buildSurveillanceResult(art *spectrumArtifacts) pipeline.SurveillanceResult { | |||||
| if art == nil { | |||||
| return pipeline.SurveillanceResult{} | |||||
| } | |||||
| return pipeline.SurveillanceResult{ | |||||
| Candidates: pipeline.CandidatesFromSignals(art.detected, "surveillance-detector"), | |||||
| Finished: art.finished, | |||||
| Signals: art.detected, | |||||
| NoiseFloor: art.noiseFloor, | |||||
| Thresholds: art.thresholds, | |||||
| } | |||||
| } | |||||
| func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extractionManager, rec *recorder.Manager) pipeline.RefinementResult { | |||||
| if art == nil || len(art.iq) == 0 { | if art == nil || len(art.iq) == 0 { | ||||
| return nil | |||||
| return pipeline.RefinementResult{} | |||||
| } | } | ||||
| policy := pipeline.PolicyFromConfig(rt.cfg) | policy := pipeline.PolicyFromConfig(rt.cfg) | ||||
| candidates := pipeline.CandidatesFromSignals(art.detected, "surveillance-detector") | candidates := pipeline.CandidatesFromSignals(art.detected, "surveillance-detector") | ||||
| @@ -237,12 +250,14 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extracti | |||||
| snips, snipRates := extractSignalIQBatch(extractMgr, art.iq, rt.cfg.SampleRate, rt.cfg.CenterHz, selectedSignals) | 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)) | 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)) | signals := make([]detector.Signal, 0, len(refined)) | ||||
| decisions := make([]pipeline.SignalDecision, 0, len(refined)) | |||||
| for i, ref := range refined { | for i, ref := range refined { | ||||
| sig := ref.Signal | sig := ref.Signal | ||||
| signals = append(signals, sig) | signals = append(signals, sig) | ||||
| cls := sig.Class | cls := sig.Class | ||||
| snipRate := ref.SnippetRate | snipRate := ref.SnippetRate | ||||
| decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls) | decision := pipeline.DecideSignalAction(policy, ref.Candidate, cls) | ||||
| decisions = append(decisions, decision) | |||||
| if decision.ShouldAutoDecode && rec != nil { | if decision.ShouldAutoDecode && rec != nil { | ||||
| rt.cfg.Recorder.AutoDecode = true | rt.cfg.Recorder.AutoDecode = true | ||||
| } | } | ||||
| @@ -265,7 +280,7 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, extractMgr *extracti | |||||
| } | } | ||||
| } | } | ||||
| rt.det.UpdateClasses(signals) | rt.det.UpdateClasses(signals) | ||||
| return signals | |||||
| return pipeline.RefinementResult{Signals: signals, Decisions: decisions} | |||||
| } | } | ||||
| func (rt *dspRuntime) updateRDS(now time.Time, rec *recorder.Manager, sig *detector.Signal, cls *classifier.Classification) { | func (rt *dspRuntime) updateRDS(now time.Time, rec *recorder.Manager, sig *detector.Signal, cls *classifier.Classification) { | ||||
| @@ -0,0 +1,16 @@ | |||||
| package pipeline | |||||
| import "sdr-wideband-suite/internal/detector" | |||||
| type SurveillanceResult struct { | |||||
| Candidates []Candidate `json:"candidates"` | |||||
| Finished []detector.Event `json:"finished"` | |||||
| Signals []detector.Signal `json:"signals"` | |||||
| NoiseFloor float64 `json:"noise_floor"` | |||||
| Thresholds []float64 `json:"thresholds,omitempty"` | |||||
| } | |||||
| type RefinementResult struct { | |||||
| Signals []detector.Signal `json:"signals"` | |||||
| Decisions []SignalDecision `json:"decisions,omitempty"` | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package pipeline | |||||
| import ( | |||||
| "testing" | |||||
| "sdr-wideband-suite/internal/detector" | |||||
| ) | |||||
| func TestRefinementResultCarriesDecisions(t *testing.T) { | |||||
| res := RefinementResult{ | |||||
| Signals: []detector.Signal{{ID: 1}}, | |||||
| Decisions: []SignalDecision{{ShouldRecord: true}}, | |||||
| } | |||||
| if len(res.Signals) != 1 || len(res.Decisions) != 1 { | |||||
| t.Fatalf("unexpected refinement result: %+v", res) | |||||
| } | |||||
| } | |||||