diff --git a/cmd/sdrd/pipeline_runtime.go b/cmd/sdrd/pipeline_runtime.go index ae9cd6e..961c02e 100644 --- a/cmd/sdrd/pipeline_runtime.go +++ b/cmd/sdrd/pipeline_runtime.go @@ -238,9 +238,16 @@ func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult) pip } windows := make([]pipeline.RefinementWindow, 0, len(scheduled)) for _, sc := range scheduled { + span := sc.Candidate.BandwidthHz + if policy.RefinementMinSpanHz > 0 && span < policy.RefinementMinSpanHz { + span = policy.RefinementMinSpanHz + } + if policy.RefinementMaxSpanHz > 0 && span > policy.RefinementMaxSpanHz { + span = policy.RefinementMaxSpanHz + } windows = append(windows, pipeline.RefinementWindow{ CenterHz: sc.Candidate.CenterHz, - SpanHz: sc.Candidate.BandwidthHz, + SpanHz: span, Source: "candidate", }) } diff --git a/internal/config/config.go b/internal/config/config.go index 9af7a1b..dc14fb7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,25 +15,25 @@ type Band struct { } type DetectorConfig struct { - ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` - MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` - HoldMs int `yaml:"hold_ms" json:"hold_ms"` - EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"` - HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` - MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"` - GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"` - CFARMode string `yaml:"cfar_mode" json:"cfar_mode"` - CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"` - CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"` - CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"` - CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"` - CFARRank int `yaml:"cfar_rank" json:"cfar_rank"` - CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"` - CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"` - EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"` - MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"` - MergeGapHz float64 `yaml:"merge_gap_hz" json:"merge_gap_hz"` - ClassHistorySize int `yaml:"class_history_size" json:"class_history_size"` + ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` + MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` + HoldMs int `yaml:"hold_ms" json:"hold_ms"` + EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"` + HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` + MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"` + GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"` + CFARMode string `yaml:"cfar_mode" json:"cfar_mode"` + CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"` + CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"` + CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"` + CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"` + CFARRank int `yaml:"cfar_rank" json:"cfar_rank"` + CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"` + CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"` + EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"` + MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"` + MergeGapHz float64 `yaml:"merge_gap_hz" json:"merge_gap_hz"` + ClassHistorySize int `yaml:"class_history_size" json:"class_history_size"` ClassSwitchRatio float64 `yaml:"class_switch_ratio" json:"class_switch_ratio"` // Deprecated (backward compatibility) @@ -56,7 +56,7 @@ type RecorderConfig struct { RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"` // Audio quality settings (AQ-2, AQ-3, AQ-5) - DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"` // De-emphasis time constant in µs. 50=Europe, 75=US/Japan, 0=disabled. Default: 50 + DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"` // De-emphasis time constant in µs. 50=Europe, 75=US/Japan, 0=disabled. Default: 50 ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"` // FIR tap count for extraction filter. Default: 101, max 301 ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"` // BW multiplier for extraction. Default: 1.2 (20% wider than detected) } @@ -71,13 +71,13 @@ type DecoderConfig struct { } type PipelineGoalConfig struct { - Intent string `yaml:"intent" json:"intent"` - MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"` - MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"` - MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"` - SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"` - AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"` - AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"` + Intent string `yaml:"intent" json:"intent"` + MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"` + MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"` + MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"` + SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"` + AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"` + AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"` } type PipelineConfig struct { @@ -94,51 +94,53 @@ type SurveillanceConfig struct { } type RefinementConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` - MaxConcurrent int `yaml:"max_concurrent" json:"max_concurrent"` - MinCandidateSNRDb float64 `yaml:"min_candidate_snr_db" json:"min_candidate_snr_db"` + Enabled bool `yaml:"enabled" json:"enabled"` + MaxConcurrent int `yaml:"max_concurrent" json:"max_concurrent"` + MinCandidateSNRDb float64 `yaml:"min_candidate_snr_db" json:"min_candidate_snr_db"` + MinSpanHz float64 `yaml:"min_span_hz" json:"min_span_hz"` + MaxSpanHz float64 `yaml:"max_span_hz" json:"max_span_hz"` } type ResourceConfig struct { - PreferGPU bool `yaml:"prefer_gpu" json:"prefer_gpu"` - MaxRefinementJobs int `yaml:"max_refinement_jobs" json:"max_refinement_jobs"` - MaxRecordingStreams int `yaml:"max_recording_streams" json:"max_recording_streams"` + PreferGPU bool `yaml:"prefer_gpu" json:"prefer_gpu"` + MaxRefinementJobs int `yaml:"max_refinement_jobs" json:"max_refinement_jobs"` + MaxRecordingStreams int `yaml:"max_recording_streams" json:"max_recording_streams"` } type ProfileConfig struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` - Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"` Surveillance *SurveillanceConfig `yaml:"surveillance,omitempty" json:"surveillance,omitempty"` - Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"` - Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"` + Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"` + Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"` } type Config struct { - Bands []Band `yaml:"bands" json:"bands"` - CenterHz float64 `yaml:"center_hz" json:"center_hz"` - SampleRate int `yaml:"sample_rate" json:"sample_rate"` - FFTSize int `yaml:"fft_size" json:"fft_size"` - GainDb float64 `yaml:"gain_db" json:"gain_db"` - TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"` - UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"` - ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"` - AGC bool `yaml:"agc" json:"agc"` - DCBlock bool `yaml:"dc_block" json:"dc_block"` - IQBalance bool `yaml:"iq_balance" json:"iq_balance"` - Pipeline PipelineConfig `yaml:"pipeline" json:"pipeline"` - Surveillance SurveillanceConfig `yaml:"surveillance" json:"surveillance"` - Refinement RefinementConfig `yaml:"refinement" json:"refinement"` - Resources ResourceConfig `yaml:"resources" json:"resources"` - Profiles []ProfileConfig `yaml:"profiles" json:"profiles"` - Detector DetectorConfig `yaml:"detector" json:"detector"` - Recorder RecorderConfig `yaml:"recorder" json:"recorder"` - Decoder DecoderConfig `yaml:"decoder" json:"decoder"` - WebAddr string `yaml:"web_addr" json:"web_addr"` - EventPath string `yaml:"event_path" json:"event_path"` - FrameRate int `yaml:"frame_rate" json:"frame_rate"` - WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"` - WebRoot string `yaml:"web_root" json:"web_root"` + Bands []Band `yaml:"bands" json:"bands"` + CenterHz float64 `yaml:"center_hz" json:"center_hz"` + SampleRate int `yaml:"sample_rate" json:"sample_rate"` + FFTSize int `yaml:"fft_size" json:"fft_size"` + GainDb float64 `yaml:"gain_db" json:"gain_db"` + TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"` + UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"` + ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"` + AGC bool `yaml:"agc" json:"agc"` + DCBlock bool `yaml:"dc_block" json:"dc_block"` + IQBalance bool `yaml:"iq_balance" json:"iq_balance"` + Pipeline PipelineConfig `yaml:"pipeline" json:"pipeline"` + Surveillance SurveillanceConfig `yaml:"surveillance" json:"surveillance"` + Refinement RefinementConfig `yaml:"refinement" json:"refinement"` + Resources ResourceConfig `yaml:"resources" json:"resources"` + Profiles []ProfileConfig `yaml:"profiles" json:"profiles"` + Detector DetectorConfig `yaml:"detector" json:"detector"` + Recorder RecorderConfig `yaml:"recorder" json:"recorder"` + Decoder DecoderConfig `yaml:"decoder" json:"decoder"` + WebAddr string `yaml:"web_addr" json:"web_addr"` + EventPath string `yaml:"event_path" json:"event_path"` + FrameRate int `yaml:"frame_rate" json:"frame_rate"` + WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"` + WebRoot string `yaml:"web_root" json:"web_root"` } func Default() Config { @@ -146,16 +148,16 @@ func Default() Config { Bands: []Band{ {Name: "example", StartHz: 99.5e6, EndHz: 100.5e6}, }, - CenterHz: 100.0e6, - SampleRate: 2_048_000, - FFTSize: 2048, - GainDb: 30, - TunerBwKHz: 1536, - UseGPUFFT: false, + CenterHz: 100.0e6, + SampleRate: 2_048_000, + FFTSize: 2048, + GainDb: 30, + TunerBwKHz: 1536, + UseGPUFFT: false, ClassifierMode: "combined", - AGC: false, - DCBlock: false, - IQBalance: false, + AGC: false, + DCBlock: false, + IQBalance: false, Pipeline: PipelineConfig{ Mode: "legacy", Goals: PipelineGoalConfig{ @@ -173,6 +175,8 @@ func Default() Config { Enabled: true, MaxConcurrent: 8, MinCandidateSNRDb: 0, + MinSpanHz: 0, + MaxSpanHz: 0, }, Resources: ResourceConfig{ PreferGPU: true, @@ -186,39 +190,39 @@ func Default() Config { {Name: "archive", Description: "Record-first monitoring profile", Pipeline: &PipelineConfig{Mode: "archive", Goals: PipelineGoalConfig{Intent: "archive-and-triage"}}}, }, Detector: DetectorConfig{ - ThresholdDb: -20, - MinDurationMs: 250, - HoldMs: 500, - EmaAlpha: 0.2, - HysteresisDb: 3, - MinStableFrames: 3, - GapToleranceMs: 500, - CFARMode: "GOSCA", - CFARGuardHz: 500, - CFARTrainHz: 5000, - CFARGuardCells: 3, - CFARTrainCells: 24, - CFARRank: 36, - CFARScaleDb: 6, - CFARWrapAround: true, - EdgeMarginDb: 3.0, - MaxSignalBwHz: 150000, - MergeGapHz: 5000, + ThresholdDb: -20, + MinDurationMs: 250, + HoldMs: 500, + EmaAlpha: 0.2, + HysteresisDb: 3, + MinStableFrames: 3, + GapToleranceMs: 500, + CFARMode: "GOSCA", + CFARGuardHz: 500, + CFARTrainHz: 5000, + CFARGuardCells: 3, + CFARTrainCells: 24, + CFARRank: 36, + CFARScaleDb: 6, + CFARWrapAround: true, + EdgeMarginDb: 3.0, + MaxSignalBwHz: 150000, + MergeGapHz: 5000, ClassHistorySize: 10, ClassSwitchRatio: 0.6, }, Recorder: RecorderConfig{ - Enabled: false, - MinSNRDb: 10, - MinDuration: "1s", - MaxDuration: "300s", - PrerollMs: 500, - RecordIQ: true, - RecordAudio: false, - AutoDemod: true, - AutoDecode: false, - MaxDiskMB: 0, - OutputDir: "data/recordings", + Enabled: false, + MinSNRDb: 10, + MinDuration: "1s", + MaxDuration: "300s", + PrerollMs: 500, + RecordIQ: true, + RecordAudio: false, + AutoDemod: true, + AutoDecode: false, + MaxDiskMB: 0, + OutputDir: "data/recordings", RingSeconds: 8, DeemphasisUs: 50, ExtractionTaps: 101, @@ -349,6 +353,12 @@ func applyDefaults(cfg Config) Config { if cfg.Refinement.MaxConcurrent <= 0 { cfg.Refinement.MaxConcurrent = 8 } + if cfg.Refinement.MinSpanHz < 0 { + cfg.Refinement.MinSpanHz = 0 + } + if cfg.Refinement.MaxSpanHz < 0 { + cfg.Refinement.MaxSpanHz = 0 + } if cfg.Resources.MaxRefinementJobs <= 0 { cfg.Resources.MaxRefinementJobs = cfg.Refinement.MaxConcurrent } diff --git a/internal/pipeline/policy.go b/internal/pipeline/policy.go index e1c2f6a..dc16400 100644 --- a/internal/pipeline/policy.go +++ b/internal/pipeline/policy.go @@ -18,6 +18,8 @@ type Policy struct { RefinementEnabled bool `json:"refinement_enabled"` MaxRefinementJobs int `json:"max_refinement_jobs"` MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` + RefinementMinSpanHz float64 `json:"refinement_min_span_hz"` + RefinementMaxSpanHz float64 `json:"refinement_max_span_hz"` PreferGPU bool `json:"prefer_gpu"` } @@ -38,6 +40,8 @@ func PolicyFromConfig(cfg config.Config) Policy { RefinementEnabled: cfg.Refinement.Enabled, MaxRefinementJobs: cfg.Resources.MaxRefinementJobs, MinCandidateSNRDb: cfg.Refinement.MinCandidateSNRDb, + RefinementMinSpanHz: cfg.Refinement.MinSpanHz, + RefinementMaxSpanHz: cfg.Refinement.MaxSpanHz, PreferGPU: cfg.Resources.PreferGPU, } }