Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

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