| @@ -56,6 +56,12 @@ Edit `config.yaml` (autosave goes to `config.autosave.yaml`). | |||
| ### New phase-1 pipeline fields | |||
| - `pipeline.mode` — operating mode label (`legacy`, `wideband-balanced`, ...) | |||
| - `pipeline.goals.*` — declarative target/intent layer for future autonomous operation | |||
| - `intent` | |||
| - `monitor_start_hz` / `monitor_end_hz` / `monitor_span_hz` | |||
| - `signal_priorities` | |||
| - `auto_record_classes` | |||
| - `auto_decode_classes` | |||
| - `surveillance.analysis_fft_size` — analysis FFT size used by the surveillance layer | |||
| - `surveillance.frame_rate` — surveillance cadence target | |||
| - `surveillance.strategy` — currently `single-resolution`, reserved for future multi-resolution modes | |||
| @@ -73,6 +79,9 @@ In phase 1, the engine stays backward compatible, but the config model now refle | |||
| - local refinement | |||
| - resource policy | |||
| - presentation | |||
| - operator goals / future autonomous intent | |||
| The long-term target is that you describe *what the system should do* (for example broad-span monitoring intent, preferred signal families, auto-record/decode priorities), while the engine decides *how* to allocate surveillance, refinement and decoding budgets. | |||
| **CFAR modes:** `OFF`, `CA`, `OS`, `GOSCA`, `CASO` | |||
| @@ -13,6 +13,14 @@ dc_block: false | |||
| iq_balance: false | |||
| pipeline: | |||
| mode: wideband-balanced | |||
| goals: | |||
| intent: wideband-surveillance | |||
| monitor_start_hz: 7.0e6 | |||
| monitor_end_hz: 7.2e6 | |||
| monitor_span_hz: 200000 | |||
| signal_priorities: ["hf-voice", "digital", "cw"] | |||
| auto_record_classes: [] | |||
| auto_decode_classes: [] | |||
| surveillance: | |||
| analysis_fft_size: 2048 | |||
| frame_rate: 15 | |||
| @@ -70,8 +70,19 @@ type DecoderConfig struct { | |||
| PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"` | |||
| } | |||
| 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"` | |||
| } | |||
| type PipelineConfig struct { | |||
| Mode string `yaml:"mode" json:"mode"` | |||
| Mode string `yaml:"mode" json:"mode"` | |||
| Goals PipelineGoalConfig `yaml:"goals" json:"goals"` | |||
| } | |||
| type SurveillanceConfig struct { | |||
| @@ -93,8 +104,12 @@ type ResourceConfig struct { | |||
| } | |||
| type ProfileConfig struct { | |||
| Name string `yaml:"name" json:"name"` | |||
| Description string `yaml:"description" json:"description"` | |||
| 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"` | |||
| } | |||
| type Config struct { | |||
| @@ -141,6 +156,9 @@ func Default() Config { | |||
| IQBalance: false, | |||
| Pipeline: PipelineConfig{ | |||
| Mode: "legacy", | |||
| Goals: PipelineGoalConfig{ | |||
| Intent: "general-monitoring", | |||
| }, | |||
| }, | |||
| Surveillance: SurveillanceConfig{ | |||
| AnalysisFFTSize: 2048, | |||
| @@ -158,8 +176,10 @@ func Default() Config { | |||
| MaxRecordingStreams: 16, | |||
| }, | |||
| Profiles: []ProfileConfig{ | |||
| {Name: "legacy", Description: "Current single-band pipeline behavior"}, | |||
| {Name: "wideband-balanced", Description: "Prepared profile for scalable wideband surveillance"}, | |||
| {Name: "legacy", Description: "Current single-band pipeline behavior", Pipeline: &PipelineConfig{Mode: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}}}, | |||
| {Name: "wideband-balanced", Description: "Prepared baseline for scalable wideband surveillance", Pipeline: &PipelineConfig{Mode: "wideband-balanced", Goals: PipelineGoalConfig{Intent: "wideband-surveillance"}}}, | |||
| {Name: "wideband-aggressive", Description: "Higher surveillance/refinement budgets for future broad-span monitoring", Pipeline: &PipelineConfig{Mode: "wideband-aggressive", Goals: PipelineGoalConfig{Intent: "high-density-wideband-surveillance"}}}, | |||
| {Name: "archive", Description: "Record-first monitoring profile", Pipeline: &PipelineConfig{Mode: "archive", Goals: PipelineGoalConfig{Intent: "archive-and-triage"}}}, | |||
| }, | |||
| Detector: DetectorConfig{ | |||
| ThresholdDb: -20, | |||
| @@ -295,6 +315,12 @@ func applyDefaults(cfg Config) Config { | |||
| if cfg.Pipeline.Mode == "" { | |||
| cfg.Pipeline.Mode = "legacy" | |||
| } | |||
| if cfg.Pipeline.Goals.Intent == "" { | |||
| cfg.Pipeline.Goals.Intent = "general-monitoring" | |||
| } | |||
| if cfg.Pipeline.Goals.MonitorSpanHz <= 0 && cfg.Pipeline.Goals.MonitorStartHz != 0 && cfg.Pipeline.Goals.MonitorEndHz != 0 && cfg.Pipeline.Goals.MonitorEndHz > cfg.Pipeline.Goals.MonitorStartHz { | |||
| cfg.Pipeline.Goals.MonitorSpanHz = cfg.Pipeline.Goals.MonitorEndHz - cfg.Pipeline.Goals.MonitorStartHz | |||
| } | |||
| if cfg.Surveillance.AnalysisFFTSize <= 0 { | |||
| cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize | |||
| } | |||
| @@ -3,18 +3,32 @@ package pipeline | |||
| import "sdr-wideband-suite/internal/config" | |||
| type Policy struct { | |||
| Mode string `json:"mode"` | |||
| SurveillanceFFTSize int `json:"surveillance_fft_size"` | |||
| SurveillanceFPS int `json:"surveillance_fps"` | |||
| RefinementEnabled bool `json:"refinement_enabled"` | |||
| MaxRefinementJobs int `json:"max_refinement_jobs"` | |||
| MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` | |||
| PreferGPU bool `json:"prefer_gpu"` | |||
| Mode string `json:"mode"` | |||
| Intent string `json:"intent"` | |||
| MonitorStartHz float64 `json:"monitor_start_hz,omitempty"` | |||
| MonitorEndHz float64 `json:"monitor_end_hz,omitempty"` | |||
| MonitorSpanHz float64 `json:"monitor_span_hz,omitempty"` | |||
| SignalPriorities []string `json:"signal_priorities,omitempty"` | |||
| AutoRecordClasses []string `json:"auto_record_classes,omitempty"` | |||
| AutoDecodeClasses []string `json:"auto_decode_classes,omitempty"` | |||
| SurveillanceFFTSize int `json:"surveillance_fft_size"` | |||
| SurveillanceFPS int `json:"surveillance_fps"` | |||
| RefinementEnabled bool `json:"refinement_enabled"` | |||
| MaxRefinementJobs int `json:"max_refinement_jobs"` | |||
| MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` | |||
| PreferGPU bool `json:"prefer_gpu"` | |||
| } | |||
| func PolicyFromConfig(cfg config.Config) Policy { | |||
| return Policy{ | |||
| Mode: cfg.Pipeline.Mode, | |||
| Intent: cfg.Pipeline.Goals.Intent, | |||
| MonitorStartHz: cfg.Pipeline.Goals.MonitorStartHz, | |||
| MonitorEndHz: cfg.Pipeline.Goals.MonitorEndHz, | |||
| MonitorSpanHz: cfg.Pipeline.Goals.MonitorSpanHz, | |||
| SignalPriorities: append([]string(nil), cfg.Pipeline.Goals.SignalPriorities...), | |||
| AutoRecordClasses: append([]string(nil), cfg.Pipeline.Goals.AutoRecordClasses...), | |||
| AutoDecodeClasses: append([]string(nil), cfg.Pipeline.Goals.AutoDecodeClasses...), | |||
| SurveillanceFFTSize: cfg.Surveillance.AnalysisFFTSize, | |||
| SurveillanceFPS: cfg.Surveillance.FrameRate, | |||
| RefinementEnabled: cfg.Refinement.Enabled, | |||
| @@ -31,6 +45,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||
| switch name { | |||
| case "legacy": | |||
| cfg.Pipeline.Mode = "legacy" | |||
| cfg.Pipeline.Goals.Intent = "general-monitoring" | |||
| cfg.Surveillance.Strategy = "single-resolution" | |||
| cfg.Refinement.Enabled = true | |||
| if cfg.Resources.MaxRefinementJobs <= 0 { | |||
| @@ -38,6 +53,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||
| } | |||
| case "wideband-balanced": | |||
| cfg.Pipeline.Mode = "wideband-balanced" | |||
| cfg.Pipeline.Goals.Intent = "wideband-surveillance" | |||
| cfg.Surveillance.Strategy = "single-resolution" | |||
| if cfg.Surveillance.AnalysisFFTSize < 4096 { | |||
| cfg.Surveillance.AnalysisFFTSize = 4096 | |||
| @@ -58,6 +74,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||
| cfg.Resources.PreferGPU = true | |||
| case "wideband-aggressive": | |||
| cfg.Pipeline.Mode = "wideband-aggressive" | |||
| cfg.Pipeline.Goals.Intent = "high-density-wideband-surveillance" | |||
| cfg.Surveillance.Strategy = "single-resolution" | |||
| if cfg.Surveillance.AnalysisFFTSize < 8192 { | |||
| cfg.Surveillance.AnalysisFFTSize = 8192 | |||
| @@ -78,6 +95,7 @@ func ApplyNamedProfile(cfg *config.Config, name string) { | |||
| cfg.Resources.PreferGPU = true | |||
| case "archive": | |||
| cfg.Pipeline.Mode = "archive" | |||
| cfg.Pipeline.Goals.Intent = "archive-and-triage" | |||
| cfg.Refinement.Enabled = true | |||
| if cfg.Refinement.MaxConcurrent < 12 { | |||
| cfg.Refinement.MaxConcurrent = 12 | |||
| @@ -12,6 +12,9 @@ func TestApplyNamedProfile(t *testing.T) { | |||
| if cfg.Pipeline.Mode != "wideband-balanced" { | |||
| t.Fatalf("mode not applied: %s", cfg.Pipeline.Mode) | |||
| } | |||
| if cfg.Pipeline.Goals.Intent != "wideband-surveillance" { | |||
| t.Fatalf("intent not applied: %s", cfg.Pipeline.Goals.Intent) | |||
| } | |||
| if cfg.Surveillance.AnalysisFFTSize < 4096 { | |||
| t.Fatalf("analysis fft too small: %d", cfg.Surveillance.AnalysisFFTSize) | |||
| } | |||
| @@ -26,6 +29,11 @@ func TestApplyNamedProfile(t *testing.T) { | |||
| func TestPolicyFromConfig(t *testing.T) { | |||
| cfg := config.Default() | |||
| cfg.Pipeline.Mode = "archive" | |||
| cfg.Pipeline.Goals.Intent = "archive-and-triage" | |||
| cfg.Pipeline.Goals.MonitorStartHz = 88e6 | |||
| cfg.Pipeline.Goals.MonitorEndHz = 108e6 | |||
| cfg.Pipeline.Goals.MonitorSpanHz = 20e6 | |||
| cfg.Pipeline.Goals.SignalPriorities = []string{"broadcast-fm", "rds"} | |||
| cfg.Surveillance.AnalysisFFTSize = 8192 | |||
| cfg.Surveillance.FrameRate = 9 | |||
| cfg.Refinement.Enabled = true | |||
| @@ -33,9 +41,12 @@ func TestPolicyFromConfig(t *testing.T) { | |||
| cfg.Refinement.MinCandidateSNRDb = 2.5 | |||
| cfg.Resources.PreferGPU = true | |||
| p := PolicyFromConfig(cfg) | |||
| if p.Mode != "archive" || p.SurveillanceFFTSize != 8192 || p.SurveillanceFPS != 9 { | |||
| if p.Mode != "archive" || p.Intent != "archive-and-triage" || p.SurveillanceFFTSize != 8192 || p.SurveillanceFPS != 9 { | |||
| t.Fatalf("unexpected policy: %+v", p) | |||
| } | |||
| if p.MonitorSpanHz != 20e6 || len(p.SignalPriorities) != 2 { | |||
| t.Fatalf("unexpected policy goals: %+v", p) | |||
| } | |||
| if !p.RefinementEnabled || p.MaxRefinementJobs != 5 || p.MinCandidateSNRDb != 2.5 || !p.PreferGPU { | |||
| t.Fatalf("unexpected policy details: %+v", p) | |||
| } | |||