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.

144 lines
4.4KB

  1. package main
  2. import (
  3. "context"
  4. "flag"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "path/filepath"
  10. "runtime/debug"
  11. "sync"
  12. "syscall"
  13. "time"
  14. "sdr-wideband-suite/internal/config"
  15. "sdr-wideband-suite/internal/detector"
  16. fftutil "sdr-wideband-suite/internal/fft"
  17. "sdr-wideband-suite/internal/fft/gpufft"
  18. "sdr-wideband-suite/internal/logging"
  19. "sdr-wideband-suite/internal/mock"
  20. "sdr-wideband-suite/internal/recorder"
  21. "sdr-wideband-suite/internal/runtime"
  22. "sdr-wideband-suite/internal/sdr"
  23. "sdr-wideband-suite/internal/sdrplay"
  24. )
  25. func main() {
  26. // Reduce GC target to limit peak memory. Default GOGC=100 lets heap
  27. // grow to 2× live set before collecting. GOGC=50 triggers GC at 1.5×,
  28. // halving the memory swings at a small CPU cost.
  29. debug.SetGCPercent(50)
  30. // Soft memory limit — GC will be more aggressive near this limit.
  31. // 1 GB is generous for 5 WFM-stereo signals + FFT + recordings.
  32. debug.SetMemoryLimit(1024 * 1024 * 1024)
  33. var cfgPath string
  34. var mockFlag bool
  35. flag.StringVar(&cfgPath, "config", "config.yaml", "path to config YAML")
  36. flag.BoolVar(&mockFlag, "mock", false, "use synthetic IQ source")
  37. flag.Parse()
  38. cfg, err := config.Load(cfgPath)
  39. if err != nil {
  40. log.Fatalf("load config: %v", err)
  41. }
  42. if err := logging.Init(logging.Config(cfg.Logging)); err != nil {
  43. log.Fatalf("logging init: %v", err)
  44. }
  45. defer logging.Close()
  46. cfgManager := runtime.New(cfg)
  47. gpuState := &gpuStatus{Available: gpufft.Available()}
  48. newSource := func(cfg config.Config) (sdr.Source, error) {
  49. if mockFlag {
  50. src := mock.New(cfg.SampleRate)
  51. if updatable, ok := interface{}(src).(sdr.ConfigurableSource); ok {
  52. _ = updatable.UpdateConfig(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.AGC, cfg.TunerBwKHz)
  53. }
  54. return src, nil
  55. }
  56. src, err := sdrplay.New(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.TunerBwKHz)
  57. if err != nil {
  58. return nil, err
  59. }
  60. if updatable, ok := src.(sdr.ConfigurableSource); ok {
  61. _ = updatable.UpdateConfig(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.AGC, cfg.TunerBwKHz)
  62. }
  63. return src, nil
  64. }
  65. src, err := newSource(cfg)
  66. if err != nil {
  67. log.Fatalf("sdrplay init failed: %v (try --mock or build with -tags sdrplay)", err)
  68. }
  69. srcMgr := newSourceManager(src, newSource)
  70. if err := srcMgr.Start(); err != nil {
  71. log.Fatalf("source start: %v", err)
  72. }
  73. defer srcMgr.Stop()
  74. if err := os.MkdirAll(filepath.Dir(cfg.EventPath), 0o755); err != nil {
  75. log.Fatalf("event path: %v", err)
  76. }
  77. eventFile, err := os.OpenFile(cfg.EventPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
  78. if err != nil {
  79. log.Fatalf("open events: %v", err)
  80. }
  81. defer eventFile.Close()
  82. eventMu := &sync.RWMutex{}
  83. det := detector.New(cfg.Detector, cfg.SampleRate, cfg.FFTSize)
  84. window := fftutil.Hann(cfg.FFTSize)
  85. h := newHub()
  86. dspUpdates := make(chan dspUpdate, 1)
  87. ctx, cancel := context.WithCancel(context.Background())
  88. defer cancel()
  89. decodeMap := buildDecoderMap(cfg)
  90. recMgr := recorder.New(cfg.SampleRate, cfg.FFTSize, recorder.Policy{
  91. Enabled: cfg.Recorder.Enabled,
  92. MinSNRDb: cfg.Recorder.MinSNRDb,
  93. MinDuration: mustParseDuration(cfg.Recorder.MinDuration, 1*time.Second),
  94. MaxDuration: mustParseDuration(cfg.Recorder.MaxDuration, 300*time.Second),
  95. PrerollMs: cfg.Recorder.PrerollMs,
  96. RecordIQ: cfg.Recorder.RecordIQ,
  97. RecordAudio: cfg.Recorder.RecordAudio,
  98. AutoDemod: cfg.Recorder.AutoDemod,
  99. AutoDecode: cfg.Recorder.AutoDecode,
  100. MaxDiskMB: cfg.Recorder.MaxDiskMB,
  101. OutputDir: cfg.Recorder.OutputDir,
  102. ClassFilter: cfg.Recorder.ClassFilter,
  103. RingSeconds: cfg.Recorder.RingSeconds,
  104. DeemphasisUs: cfg.Recorder.DeemphasisUs,
  105. ExtractionTaps: cfg.Recorder.ExtractionTaps,
  106. ExtractionBwMult: cfg.Recorder.ExtractionBwMult,
  107. }, cfg.CenterHz, decodeMap)
  108. defer recMgr.Close()
  109. sigSnap := &signalSnapshot{}
  110. extractMgr := &extractionManager{}
  111. defer extractMgr.reset()
  112. phaseSnap := &phaseSnapshot{}
  113. go runDSP(ctx, srcMgr, cfg, det, window, h, eventFile, eventMu, dspUpdates, gpuState, recMgr, sigSnap, extractMgr, phaseSnap)
  114. server := newHTTPServer(cfg.WebAddr, cfg.WebRoot, h, cfgPath, cfgManager, srcMgr, dspUpdates, gpuState, recMgr, sigSnap, eventMu, phaseSnap)
  115. go func() {
  116. log.Printf("web listening on %s", cfg.WebAddr)
  117. if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  118. log.Fatalf("server: %v", err)
  119. }
  120. }()
  121. stop := make(chan os.Signal, 1)
  122. signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
  123. <-stop
  124. shutdownServer(server)
  125. }