Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

739 wiersze
25KB

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