Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
22KB

  1. package config
  2. import (
  3. "math"
  4. "os"
  5. "strings"
  6. "time"
  7. "gopkg.in/yaml.v3"
  8. )
  9. type Band struct {
  10. Name string `yaml:"name" json:"name"`
  11. StartHz float64 `yaml:"start_hz" json:"start_hz"`
  12. EndHz float64 `yaml:"end_hz" json:"end_hz"`
  13. }
  14. type MonitorWindow struct {
  15. Label string `yaml:"label" json:"label"`
  16. StartHz float64 `yaml:"start_hz" json:"start_hz"`
  17. EndHz float64 `yaml:"end_hz" json:"end_hz"`
  18. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  19. SpanHz float64 `yaml:"span_hz" json:"span_hz"`
  20. Priority float64 `yaml:"priority" json:"priority"`
  21. AutoRecord bool `yaml:"auto_record" json:"auto_record"`
  22. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  23. }
  24. type DetectorConfig struct {
  25. ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"`
  26. MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"`
  27. HoldMs int `yaml:"hold_ms" json:"hold_ms"`
  28. EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"`
  29. HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"`
  30. MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"`
  31. GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"`
  32. CFARMode string `yaml:"cfar_mode" json:"cfar_mode"`
  33. CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"`
  34. CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"`
  35. CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"`
  36. CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"`
  37. CFARRank int `yaml:"cfar_rank" json:"cfar_rank"`
  38. CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"`
  39. CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"`
  40. EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"`
  41. MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"`
  42. MergeGapHz float64 `yaml:"merge_gap_hz" json:"merge_gap_hz"`
  43. ClassHistorySize int `yaml:"class_history_size" json:"class_history_size"`
  44. ClassSwitchRatio float64 `yaml:"class_switch_ratio" json:"class_switch_ratio"`
  45. // Deprecated (backward compatibility)
  46. CFAREnabled *bool `yaml:"cfar_enabled,omitempty" json:"cfar_enabled,omitempty"`
  47. }
  48. type RecorderConfig struct {
  49. Enabled bool `yaml:"enabled" json:"enabled"`
  50. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  51. MinDuration string `yaml:"min_duration" json:"min_duration"`
  52. MaxDuration string `yaml:"max_duration" json:"max_duration"`
  53. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  54. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  55. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  56. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  57. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  58. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  59. OutputDir string `yaml:"output_dir" json:"output_dir"`
  60. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  61. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  62. // Audio quality settings (AQ-2, AQ-3, AQ-5)
  63. DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"` // De-emphasis time constant in µs. 50=Europe, 75=US/Japan, 0=disabled. Default: 50
  64. ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"` // FIR tap count for extraction filter. Default: 101, max 301
  65. ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"` // BW multiplier for extraction. Default: 1.2 (20% wider than detected)
  66. }
  67. type DecoderConfig struct {
  68. FT8Cmd string `yaml:"ft8_cmd" json:"ft8_cmd"`
  69. WSPRCmd string `yaml:"wspr_cmd" json:"wspr_cmd"`
  70. DMRCmd string `yaml:"dmr_cmd" json:"dmr_cmd"`
  71. DStarCmd string `yaml:"dstar_cmd" json:"dstar_cmd"`
  72. FSKCmd string `yaml:"fsk_cmd" json:"fsk_cmd"`
  73. PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"`
  74. }
  75. type PipelineGoalConfig struct {
  76. Intent string `yaml:"intent" json:"intent"`
  77. MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"`
  78. MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"`
  79. MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"`
  80. MonitorWindows []MonitorWindow `yaml:"monitor_windows" json:"monitor_windows"`
  81. SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"`
  82. AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"`
  83. AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"`
  84. }
  85. type PipelineConfig struct {
  86. Mode string `yaml:"mode" json:"mode"`
  87. Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
  88. Goals PipelineGoalConfig `yaml:"goals" json:"goals"`
  89. }
  90. type SurveillanceConfig struct {
  91. AnalysisFFTSize int `yaml:"analysis_fft_size" json:"analysis_fft_size"`
  92. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  93. Strategy string `yaml:"strategy" json:"strategy"`
  94. DisplayBins int `yaml:"display_bins" json:"display_bins"`
  95. DisplayFPS int `yaml:"display_fps" json:"display_fps"`
  96. DerivedDetection string `yaml:"derived_detection" json:"derived_detection"`
  97. }
  98. type RefinementConfig struct {
  99. Enabled bool `yaml:"enabled" json:"enabled"`
  100. MaxConcurrent int `yaml:"max_concurrent" json:"max_concurrent"`
  101. DetailFFTSize int `yaml:"detail_fft_size" json:"detail_fft_size"`
  102. MinCandidateSNRDb float64 `yaml:"min_candidate_snr_db" json:"min_candidate_snr_db"`
  103. MinSpanHz float64 `yaml:"min_span_hz" json:"min_span_hz"`
  104. MaxSpanHz float64 `yaml:"max_span_hz" json:"max_span_hz"`
  105. AutoSpan *bool `yaml:"auto_span" json:"auto_span"`
  106. }
  107. type ResourceConfig struct {
  108. PreferGPU bool `yaml:"prefer_gpu" json:"prefer_gpu"`
  109. MaxRefinementJobs int `yaml:"max_refinement_jobs" json:"max_refinement_jobs"`
  110. MaxRecordingStreams int `yaml:"max_recording_streams" json:"max_recording_streams"`
  111. MaxDecodeJobs int `yaml:"max_decode_jobs" json:"max_decode_jobs"`
  112. DecisionHoldMs int `yaml:"decision_hold_ms" json:"decision_hold_ms"`
  113. }
  114. type ProfileConfig struct {
  115. Name string `yaml:"name" json:"name"`
  116. Description string `yaml:"description" json:"description"`
  117. Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"`
  118. Surveillance *SurveillanceConfig `yaml:"surveillance,omitempty" json:"surveillance,omitempty"`
  119. Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"`
  120. Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"`
  121. }
  122. type Config struct {
  123. Bands []Band `yaml:"bands" json:"bands"`
  124. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  125. SampleRate int `yaml:"sample_rate" json:"sample_rate"`
  126. FFTSize int `yaml:"fft_size" json:"fft_size"`
  127. GainDb float64 `yaml:"gain_db" json:"gain_db"`
  128. TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"`
  129. UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"`
  130. ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"`
  131. AGC bool `yaml:"agc" json:"agc"`
  132. DCBlock bool `yaml:"dc_block" json:"dc_block"`
  133. IQBalance bool `yaml:"iq_balance" json:"iq_balance"`
  134. Pipeline PipelineConfig `yaml:"pipeline" json:"pipeline"`
  135. Surveillance SurveillanceConfig `yaml:"surveillance" json:"surveillance"`
  136. Refinement RefinementConfig `yaml:"refinement" json:"refinement"`
  137. Resources ResourceConfig `yaml:"resources" json:"resources"`
  138. Profiles []ProfileConfig `yaml:"profiles" json:"profiles"`
  139. Detector DetectorConfig `yaml:"detector" json:"detector"`
  140. Recorder RecorderConfig `yaml:"recorder" json:"recorder"`
  141. Decoder DecoderConfig `yaml:"decoder" json:"decoder"`
  142. WebAddr string `yaml:"web_addr" json:"web_addr"`
  143. EventPath string `yaml:"event_path" json:"event_path"`
  144. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  145. WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"`
  146. WebRoot string `yaml:"web_root" json:"web_root"`
  147. }
  148. func Default() Config {
  149. return Config{
  150. Bands: []Band{
  151. {Name: "example", StartHz: 99.5e6, EndHz: 100.5e6},
  152. },
  153. CenterHz: 100.0e6,
  154. SampleRate: 2_048_000,
  155. FFTSize: 2048,
  156. GainDb: 30,
  157. TunerBwKHz: 1536,
  158. UseGPUFFT: false,
  159. ClassifierMode: "combined",
  160. AGC: false,
  161. DCBlock: false,
  162. IQBalance: false,
  163. Pipeline: PipelineConfig{
  164. Mode: "legacy",
  165. Goals: PipelineGoalConfig{
  166. Intent: "general-monitoring",
  167. },
  168. },
  169. Surveillance: SurveillanceConfig{
  170. AnalysisFFTSize: 2048,
  171. FrameRate: 15,
  172. Strategy: "single-resolution",
  173. DisplayBins: 2048,
  174. DisplayFPS: 15,
  175. DerivedDetection: "auto",
  176. },
  177. Refinement: RefinementConfig{
  178. Enabled: true,
  179. MaxConcurrent: 8,
  180. DetailFFTSize: 0,
  181. MinCandidateSNRDb: 0,
  182. MinSpanHz: 0,
  183. MaxSpanHz: 0,
  184. AutoSpan: boolPtr(true),
  185. },
  186. Resources: ResourceConfig{
  187. PreferGPU: true,
  188. MaxRefinementJobs: 8,
  189. MaxRecordingStreams: 16,
  190. MaxDecodeJobs: 16,
  191. DecisionHoldMs: 2000,
  192. },
  193. Profiles: []ProfileConfig{
  194. {
  195. Name: "legacy",
  196. Description: "Current single-band pipeline behavior",
  197. Pipeline: &PipelineConfig{Mode: "legacy", Profile: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}},
  198. Surveillance: &SurveillanceConfig{
  199. AnalysisFFTSize: 2048,
  200. FrameRate: 15,
  201. Strategy: "single-resolution",
  202. DisplayBins: 2048,
  203. DisplayFPS: 15,
  204. DerivedDetection: "auto",
  205. },
  206. Refinement: &RefinementConfig{
  207. Enabled: true,
  208. MaxConcurrent: 8,
  209. DetailFFTSize: 0,
  210. MinCandidateSNRDb: 0,
  211. MinSpanHz: 0,
  212. MaxSpanHz: 0,
  213. AutoSpan: boolPtr(true),
  214. },
  215. Resources: &ResourceConfig{
  216. PreferGPU: false,
  217. MaxRefinementJobs: 8,
  218. MaxRecordingStreams: 16,
  219. MaxDecodeJobs: 16,
  220. DecisionHoldMs: 2000,
  221. },
  222. },
  223. {
  224. Name: "wideband-balanced",
  225. Description: "Baseline multi-resolution wideband surveillance",
  226. Pipeline: &PipelineConfig{Mode: "wideband-balanced", Profile: "wideband-balanced", Goals: PipelineGoalConfig{
  227. Intent: "wideband-surveillance",
  228. SignalPriorities: []string{"digital", "wfm"},
  229. }},
  230. Surveillance: &SurveillanceConfig{
  231. AnalysisFFTSize: 4096,
  232. FrameRate: 12,
  233. Strategy: "multi-resolution",
  234. DisplayBins: 2048,
  235. DisplayFPS: 12,
  236. DerivedDetection: "auto",
  237. },
  238. Refinement: &RefinementConfig{
  239. Enabled: true,
  240. MaxConcurrent: 16,
  241. DetailFFTSize: 0,
  242. MinCandidateSNRDb: 0,
  243. MinSpanHz: 4000,
  244. MaxSpanHz: 200000,
  245. AutoSpan: boolPtr(true),
  246. },
  247. Resources: &ResourceConfig{
  248. PreferGPU: true,
  249. MaxRefinementJobs: 16,
  250. MaxRecordingStreams: 16,
  251. MaxDecodeJobs: 12,
  252. DecisionHoldMs: 2000,
  253. },
  254. },
  255. {
  256. Name: "wideband-aggressive",
  257. Description: "Higher surveillance/refinement budgets for dense wideband monitoring",
  258. Pipeline: &PipelineConfig{Mode: "wideband-aggressive", Profile: "wideband-aggressive", Goals: PipelineGoalConfig{
  259. Intent: "high-density-wideband-surveillance",
  260. SignalPriorities: []string{"digital", "wfm", "trunk"},
  261. }},
  262. Surveillance: &SurveillanceConfig{
  263. AnalysisFFTSize: 8192,
  264. FrameRate: 10,
  265. Strategy: "multi-resolution",
  266. DisplayBins: 4096,
  267. DisplayFPS: 10,
  268. DerivedDetection: "auto",
  269. },
  270. Refinement: &RefinementConfig{
  271. Enabled: true,
  272. MaxConcurrent: 32,
  273. DetailFFTSize: 0,
  274. MinCandidateSNRDb: 0,
  275. MinSpanHz: 6000,
  276. MaxSpanHz: 250000,
  277. AutoSpan: boolPtr(true),
  278. },
  279. Resources: &ResourceConfig{
  280. PreferGPU: true,
  281. MaxRefinementJobs: 32,
  282. MaxRecordingStreams: 24,
  283. MaxDecodeJobs: 16,
  284. DecisionHoldMs: 2000,
  285. },
  286. },
  287. {
  288. Name: "archive",
  289. Description: "Record-first monitoring profile",
  290. Pipeline: &PipelineConfig{Mode: "archive", Profile: "archive", Goals: PipelineGoalConfig{
  291. Intent: "archive-and-triage",
  292. SignalPriorities: []string{"wfm", "nfm", "digital"},
  293. }},
  294. Surveillance: &SurveillanceConfig{
  295. AnalysisFFTSize: 4096,
  296. FrameRate: 12,
  297. Strategy: "single-resolution",
  298. DisplayBins: 2048,
  299. DisplayFPS: 12,
  300. DerivedDetection: "auto",
  301. },
  302. Refinement: &RefinementConfig{
  303. Enabled: true,
  304. MaxConcurrent: 12,
  305. DetailFFTSize: 0,
  306. MinCandidateSNRDb: 0,
  307. MinSpanHz: 4000,
  308. MaxSpanHz: 200000,
  309. AutoSpan: boolPtr(true),
  310. },
  311. Resources: &ResourceConfig{
  312. PreferGPU: true,
  313. MaxRefinementJobs: 12,
  314. MaxRecordingStreams: 24,
  315. MaxDecodeJobs: 12,
  316. DecisionHoldMs: 2500,
  317. },
  318. },
  319. {
  320. Name: "digital-hunting",
  321. Description: "Digital-first refinement and decode focus",
  322. Pipeline: &PipelineConfig{Mode: "digital-hunting", Profile: "digital-hunting", Goals: PipelineGoalConfig{
  323. Intent: "digital-surveillance",
  324. SignalPriorities: []string{"ft8", "wspr", "fsk", "psk", "dmr"},
  325. }},
  326. Surveillance: &SurveillanceConfig{
  327. AnalysisFFTSize: 4096,
  328. FrameRate: 12,
  329. Strategy: "multi-resolution",
  330. DisplayBins: 2048,
  331. DisplayFPS: 12,
  332. DerivedDetection: "auto",
  333. },
  334. Refinement: &RefinementConfig{
  335. Enabled: true,
  336. MaxConcurrent: 16,
  337. DetailFFTSize: 0,
  338. MinCandidateSNRDb: 0,
  339. MinSpanHz: 3000,
  340. MaxSpanHz: 120000,
  341. AutoSpan: boolPtr(true),
  342. },
  343. Resources: &ResourceConfig{
  344. PreferGPU: true,
  345. MaxRefinementJobs: 16,
  346. MaxRecordingStreams: 12,
  347. MaxDecodeJobs: 16,
  348. DecisionHoldMs: 2000,
  349. },
  350. },
  351. },
  352. Detector: DetectorConfig{
  353. ThresholdDb: -20,
  354. MinDurationMs: 250,
  355. HoldMs: 500,
  356. EmaAlpha: 0.2,
  357. HysteresisDb: 3,
  358. MinStableFrames: 3,
  359. GapToleranceMs: 500,
  360. CFARMode: "GOSCA",
  361. CFARGuardHz: 500,
  362. CFARTrainHz: 5000,
  363. CFARGuardCells: 3,
  364. CFARTrainCells: 24,
  365. CFARRank: 36,
  366. CFARScaleDb: 6,
  367. CFARWrapAround: true,
  368. EdgeMarginDb: 3.0,
  369. MaxSignalBwHz: 150000,
  370. MergeGapHz: 5000,
  371. ClassHistorySize: 10,
  372. ClassSwitchRatio: 0.6,
  373. },
  374. Recorder: RecorderConfig{
  375. Enabled: false,
  376. MinSNRDb: 10,
  377. MinDuration: "1s",
  378. MaxDuration: "300s",
  379. PrerollMs: 500,
  380. RecordIQ: true,
  381. RecordAudio: false,
  382. AutoDemod: true,
  383. AutoDecode: false,
  384. MaxDiskMB: 0,
  385. OutputDir: "data/recordings",
  386. RingSeconds: 8,
  387. DeemphasisUs: 50,
  388. ExtractionTaps: 101,
  389. ExtractionBwMult: 1.2,
  390. },
  391. Decoder: DecoderConfig{},
  392. WebAddr: ":8080",
  393. EventPath: "data/events.jsonl",
  394. FrameRate: 15,
  395. WaterfallLines: 200,
  396. WebRoot: "web",
  397. }
  398. }
  399. func Load(path string) (Config, error) {
  400. cfg := Default()
  401. if b, err := os.ReadFile(autosavePath(path)); err == nil {
  402. if err := yaml.Unmarshal(b, &cfg); err == nil {
  403. return applyDefaults(cfg), nil
  404. }
  405. }
  406. b, err := os.ReadFile(path)
  407. if err != nil {
  408. return cfg, err
  409. }
  410. if err := yaml.Unmarshal(b, &cfg); err != nil {
  411. return cfg, err
  412. }
  413. return applyDefaults(cfg), nil
  414. }
  415. func applyDefaults(cfg Config) Config {
  416. if cfg.Detector.MinDurationMs <= 0 {
  417. cfg.Detector.MinDurationMs = 250
  418. }
  419. if cfg.Detector.HoldMs <= 0 {
  420. cfg.Detector.HoldMs = 500
  421. }
  422. if cfg.Detector.MinStableFrames <= 0 {
  423. cfg.Detector.MinStableFrames = 3
  424. }
  425. if cfg.Detector.GapToleranceMs <= 0 {
  426. cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs
  427. }
  428. if cfg.Detector.CFARMode == "" {
  429. if cfg.Detector.CFAREnabled != nil {
  430. if *cfg.Detector.CFAREnabled {
  431. cfg.Detector.CFARMode = "OS"
  432. } else {
  433. cfg.Detector.CFARMode = "OFF"
  434. }
  435. } else {
  436. cfg.Detector.CFARMode = "GOSCA"
  437. }
  438. }
  439. if cfg.Detector.CFARGuardHz <= 0 && cfg.Detector.CFARGuardCells > 0 {
  440. cfg.Detector.CFARGuardHz = float64(cfg.Detector.CFARGuardCells) * 62.5
  441. }
  442. if cfg.Detector.CFARTrainHz <= 0 && cfg.Detector.CFARTrainCells > 0 {
  443. cfg.Detector.CFARTrainHz = float64(cfg.Detector.CFARTrainCells) * 62.5
  444. }
  445. if cfg.Detector.CFARGuardHz <= 0 {
  446. cfg.Detector.CFARGuardHz = 500
  447. }
  448. if cfg.Detector.CFARTrainHz <= 0 {
  449. cfg.Detector.CFARTrainHz = 5000
  450. }
  451. if cfg.Detector.CFARGuardCells <= 0 {
  452. cfg.Detector.CFARGuardCells = 3
  453. }
  454. if cfg.Detector.CFARTrainCells <= 0 {
  455. cfg.Detector.CFARTrainCells = 24
  456. }
  457. if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells {
  458. cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells)))
  459. if cfg.Detector.CFARRank <= 0 {
  460. cfg.Detector.CFARRank = 1
  461. }
  462. }
  463. if cfg.Detector.CFARScaleDb <= 0 {
  464. cfg.Detector.CFARScaleDb = 6
  465. }
  466. if cfg.Detector.EdgeMarginDb <= 0 {
  467. cfg.Detector.EdgeMarginDb = 3.0
  468. }
  469. if cfg.Detector.MaxSignalBwHz <= 0 {
  470. cfg.Detector.MaxSignalBwHz = 150000
  471. }
  472. if cfg.Detector.MergeGapHz <= 0 {
  473. cfg.Detector.MergeGapHz = 5000
  474. }
  475. if cfg.Detector.ClassHistorySize <= 0 {
  476. cfg.Detector.ClassHistorySize = 10
  477. }
  478. if cfg.Detector.ClassSwitchRatio <= 0 || cfg.Detector.ClassSwitchRatio > 1 {
  479. cfg.Detector.ClassSwitchRatio = 0.6
  480. }
  481. if cfg.Pipeline.Mode == "" {
  482. cfg.Pipeline.Mode = "legacy"
  483. }
  484. if cfg.Pipeline.Goals.Intent == "" {
  485. cfg.Pipeline.Goals.Intent = "general-monitoring"
  486. }
  487. if cfg.Pipeline.Goals.MonitorSpanHz <= 0 && cfg.Pipeline.Goals.MonitorStartHz != 0 && cfg.Pipeline.Goals.MonitorEndHz != 0 && cfg.Pipeline.Goals.MonitorEndHz > cfg.Pipeline.Goals.MonitorStartHz {
  488. cfg.Pipeline.Goals.MonitorSpanHz = cfg.Pipeline.Goals.MonitorEndHz - cfg.Pipeline.Goals.MonitorStartHz
  489. }
  490. if cfg.Surveillance.AnalysisFFTSize <= 0 {
  491. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  492. }
  493. if cfg.Surveillance.FrameRate <= 0 {
  494. cfg.Surveillance.FrameRate = cfg.FrameRate
  495. }
  496. if cfg.Surveillance.Strategy == "" {
  497. cfg.Surveillance.Strategy = "single-resolution"
  498. }
  499. if cfg.Surveillance.DerivedDetection == "" {
  500. cfg.Surveillance.DerivedDetection = "auto"
  501. }
  502. switch strings.ToLower(strings.TrimSpace(cfg.Surveillance.DerivedDetection)) {
  503. case "auto", "on", "off", "true", "false", "enabled", "disabled", "enable", "disable":
  504. default:
  505. cfg.Surveillance.DerivedDetection = "auto"
  506. }
  507. if cfg.Surveillance.DisplayBins <= 0 {
  508. cfg.Surveillance.DisplayBins = cfg.FFTSize
  509. }
  510. if cfg.Surveillance.DisplayFPS <= 0 {
  511. cfg.Surveillance.DisplayFPS = cfg.FrameRate
  512. }
  513. if !cfg.Refinement.Enabled {
  514. // keep explicit false if user disabled it; enable by default only when unset-like zero config
  515. if cfg.Refinement.MaxConcurrent == 0 && cfg.Refinement.MinCandidateSNRDb == 0 {
  516. cfg.Refinement.Enabled = true
  517. }
  518. }
  519. if cfg.Refinement.MaxConcurrent <= 0 {
  520. cfg.Refinement.MaxConcurrent = 8
  521. }
  522. if cfg.Refinement.DetailFFTSize <= 0 {
  523. cfg.Refinement.DetailFFTSize = cfg.Surveillance.AnalysisFFTSize
  524. }
  525. if cfg.Refinement.DetailFFTSize&(cfg.Refinement.DetailFFTSize-1) != 0 {
  526. cfg.Refinement.DetailFFTSize = cfg.Surveillance.AnalysisFFTSize
  527. }
  528. if cfg.Refinement.MinSpanHz < 0 {
  529. cfg.Refinement.MinSpanHz = 0
  530. }
  531. if cfg.Refinement.MaxSpanHz < 0 {
  532. cfg.Refinement.MaxSpanHz = 0
  533. }
  534. if cfg.Refinement.MaxSpanHz > 0 && cfg.Refinement.MinSpanHz > cfg.Refinement.MaxSpanHz {
  535. cfg.Refinement.MaxSpanHz = cfg.Refinement.MinSpanHz
  536. }
  537. if cfg.Refinement.AutoSpan == nil {
  538. cfg.Refinement.AutoSpan = boolPtr(true)
  539. }
  540. if cfg.Resources.MaxRefinementJobs <= 0 {
  541. cfg.Resources.MaxRefinementJobs = cfg.Refinement.MaxConcurrent
  542. }
  543. if cfg.Resources.MaxRecordingStreams <= 0 {
  544. cfg.Resources.MaxRecordingStreams = 16
  545. }
  546. if cfg.Resources.DecisionHoldMs < 0 {
  547. cfg.Resources.DecisionHoldMs = 0
  548. }
  549. if cfg.Resources.DecisionHoldMs == 0 {
  550. cfg.Resources.DecisionHoldMs = 2000
  551. }
  552. if cfg.FrameRate <= 0 {
  553. cfg.FrameRate = 15
  554. }
  555. if cfg.WaterfallLines <= 0 {
  556. cfg.WaterfallLines = 200
  557. }
  558. if cfg.WebRoot == "" {
  559. cfg.WebRoot = "web"
  560. }
  561. if cfg.WebAddr == "" {
  562. cfg.WebAddr = ":8080"
  563. }
  564. if cfg.EventPath == "" {
  565. cfg.EventPath = "data/events.jsonl"
  566. }
  567. if cfg.SampleRate <= 0 {
  568. cfg.SampleRate = 2_048_000
  569. }
  570. if cfg.ClassifierMode == "" {
  571. cfg.ClassifierMode = "combined"
  572. }
  573. switch cfg.ClassifierMode {
  574. case "rule", "math", "combined":
  575. default:
  576. cfg.ClassifierMode = "combined"
  577. }
  578. if cfg.FFTSize <= 0 {
  579. cfg.FFTSize = 2048
  580. }
  581. if cfg.Surveillance.AnalysisFFTSize > 0 {
  582. cfg.FFTSize = cfg.Surveillance.AnalysisFFTSize
  583. } else {
  584. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  585. }
  586. if cfg.TunerBwKHz <= 0 {
  587. cfg.TunerBwKHz = 1536
  588. }
  589. if cfg.CenterHz == 0 {
  590. cfg.CenterHz = 100.0e6
  591. }
  592. if cfg.Recorder.OutputDir == "" {
  593. cfg.Recorder.OutputDir = "data/recordings"
  594. }
  595. if cfg.Recorder.RingSeconds <= 0 {
  596. cfg.Recorder.RingSeconds = 8
  597. }
  598. if cfg.Recorder.DeemphasisUs == 0 {
  599. cfg.Recorder.DeemphasisUs = 50
  600. }
  601. if cfg.Recorder.ExtractionTaps <= 0 {
  602. cfg.Recorder.ExtractionTaps = 101
  603. }
  604. if cfg.Recorder.ExtractionTaps > 301 {
  605. cfg.Recorder.ExtractionTaps = 301
  606. }
  607. if cfg.Recorder.ExtractionTaps%2 == 0 {
  608. cfg.Recorder.ExtractionTaps++ // must be odd
  609. }
  610. if cfg.Recorder.ExtractionBwMult <= 0 {
  611. cfg.Recorder.ExtractionBwMult = 1.2
  612. }
  613. return cfg
  614. }
  615. func (c Config) FrameInterval() time.Duration {
  616. fps := c.FrameRate
  617. if fps <= 0 {
  618. fps = 15
  619. }
  620. return time.Second / time.Duration(fps)
  621. }