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.

614 lines
21KB

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