Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

142 Zeilen
4.1KB

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