diff --git a/cmd/sdrd/http_handlers.go b/cmd/sdrd/http_handlers.go index 19a1338..2ed3f12 100644 --- a/cmd/sdrd/http_handlers.go +++ b/cmd/sdrd/http_handlers.go @@ -39,6 +39,12 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime http.Error(w, err.Error(), http.StatusBadRequest) return } + if update.Pipeline != nil && update.Pipeline.Profile != nil { + if prof, ok := pipeline.ResolveProfile(next, *update.Pipeline.Profile); ok { + pipeline.MergeProfile(&next, prof) + cfgManager.Replace(next) + } + } sourceChanged := prev.CenterHz != next.CenterHz || prev.SampleRate != next.SampleRate || prev.GainDb != next.GainDb || prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz if sourceChanged { if err := srcMgr.ApplyConfig(next); err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e7b28e1..0218fe1 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -45,3 +45,20 @@ func TestLoadConfig(t *testing.T) { t.Fatalf("event path default not applied") } } + +func TestProfileDefaultsPresent(t *testing.T) { + cfg := Default() + if len(cfg.Profiles) < 2 { + t.Fatalf("expected built-in profiles") + } + found := false + for _, p := range cfg.Profiles { + if p.Name == "wideband-balanced" { + found = true + break + } + } + if !found { + t.Fatalf("missing wideband-balanced profile") + } +} diff --git a/internal/pipeline/profile.go b/internal/pipeline/profile.go new file mode 100644 index 0000000..e4e9ce0 --- /dev/null +++ b/internal/pipeline/profile.go @@ -0,0 +1,36 @@ +package pipeline + +import "sdr-wideband-suite/internal/config" + +func ResolveProfile(cfg config.Config, name string) (config.ProfileConfig, bool) { + for _, p := range cfg.Profiles { + if p.Name == name { + return p, true + } + } + return config.ProfileConfig{}, false +} + +func MergeProfile(cfg *config.Config, profile config.ProfileConfig) { + if cfg == nil { + return + } + if profile.Pipeline != nil { + cfg.Pipeline = *profile.Pipeline + } + if profile.Surveillance != nil { + cfg.Surveillance = *profile.Surveillance + } + if profile.Refinement != nil { + cfg.Refinement = *profile.Refinement + } + if profile.Resources != nil { + cfg.Resources = *profile.Resources + } + if cfg.Surveillance.AnalysisFFTSize > 0 { + cfg.FFTSize = cfg.Surveillance.AnalysisFFTSize + } + if cfg.Surveillance.FrameRate > 0 { + cfg.FrameRate = cfg.Surveillance.FrameRate + } +} diff --git a/internal/pipeline/profile_test.go b/internal/pipeline/profile_test.go new file mode 100644 index 0000000..f121ad9 --- /dev/null +++ b/internal/pipeline/profile_test.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "testing" + + "sdr-wideband-suite/internal/config" +) + +func TestResolveAndMergeProfile(t *testing.T) { + cfg := config.Default() + cfg.Profiles = append(cfg.Profiles, config.ProfileConfig{ + Name: "custom-test", + Description: "test profile", + Pipeline: &config.PipelineConfig{Mode: "custom", Goals: config.PipelineGoalConfig{Intent: "custom-intent", MonitorSpanHz: 12.5e6}}, + Surveillance: &config.SurveillanceConfig{AnalysisFFTSize: 16384, FrameRate: 8, Strategy: "single-resolution"}, + Refinement: &config.RefinementConfig{Enabled: true, MaxConcurrent: 20, MinCandidateSNRDb: 4}, + Resources: &config.ResourceConfig{PreferGPU: true, MaxRefinementJobs: 20, MaxRecordingStreams: 32}, + }) + p, ok := ResolveProfile(cfg, "custom-test") + if !ok { + t.Fatalf("expected profile") + } + MergeProfile(&cfg, p) + if cfg.Pipeline.Mode != "custom" || cfg.Pipeline.Goals.Intent != "custom-intent" { + t.Fatalf("pipeline not merged: %+v", cfg.Pipeline) + } + if cfg.FFTSize != 16384 || cfg.FrameRate != 8 { + t.Fatalf("surveillance not merged into legacy fields: fft=%d fps=%d", cfg.FFTSize, cfg.FrameRate) + } + if cfg.Resources.MaxRefinementJobs != 20 { + t.Fatalf("resources not merged: %+v", cfg.Resources) + } +} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 8cb5f2e..8af9d9e 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -10,7 +10,8 @@ import ( ) type PipelineUpdate struct { - Mode *string `json:"mode"` + Mode *string `json:"mode"` + Profile *string `json:"profile"` } type SurveillanceUpdate struct { diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 2bc5171..4660d22 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -23,6 +23,7 @@ func TestApplyConfigUpdate(t *testing.T) { cfarScale := 5.5 mode := "wideband-balanced" + profile := "wideband-balanced" survFPS := 12 maxRefJobs := 24 updated, err := mgr.ApplyConfig(ConfigUpdate{ @@ -30,7 +31,7 @@ func TestApplyConfigUpdate(t *testing.T) { SampleRate: &sampleRate, FFTSize: &fftSize, TunerBwKHz: &bw, - Pipeline: &PipelineUpdate{Mode: &mode}, + Pipeline: &PipelineUpdate{Mode: &mode, Profile: &profile}, Surveillance: &SurveillanceUpdate{FrameRate: &survFPS}, Resources: &ResourcesUpdate{MaxRefinementJobs: &maxRefJobs}, Detector: &DetectorUpdate{