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.

230 Zeilen
6.5KB

  1. package main
  2. import (
  3. "context"
  4. "encoding/json"
  5. "log"
  6. "math"
  7. "os"
  8. "runtime/debug"
  9. "strings"
  10. "sync"
  11. "time"
  12. "sdr-visual-suite/internal/classifier"
  13. "sdr-visual-suite/internal/config"
  14. "sdr-visual-suite/internal/detector"
  15. "sdr-visual-suite/internal/dsp"
  16. fftutil "sdr-visual-suite/internal/fft"
  17. "sdr-visual-suite/internal/fft/gpufft"
  18. "sdr-visual-suite/internal/recorder"
  19. )
  20. func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *detector.Detector, window []float64, h *hub, eventFile *os.File, eventMu *sync.RWMutex, updates <-chan dspUpdate, gpuState *gpuStatus, rec *recorder.Manager, sigSnap *signalSnapshot) {
  21. defer func() {
  22. if r := recover(); r != nil {
  23. log.Printf("FATAL: runDSP goroutine panic: %v\n%s", r, debug.Stack())
  24. }
  25. }()
  26. ticker := time.NewTicker(cfg.FrameInterval())
  27. defer ticker.Stop()
  28. logTicker := time.NewTicker(5 * time.Second)
  29. defer logTicker.Stop()
  30. enc := json.NewEncoder(eventFile)
  31. dcBlocker := dsp.NewDCBlocker(0.995)
  32. dcEnabled := cfg.DCBlock
  33. iqEnabled := cfg.IQBalance
  34. plan := fftutil.NewCmplxPlan(cfg.FFTSize)
  35. useGPU := cfg.UseGPUFFT
  36. var gpuEngine *gpufft.Engine
  37. if useGPU && gpuState != nil {
  38. snap := gpuState.snapshot()
  39. if snap.Available {
  40. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  41. gpuEngine = eng
  42. gpuState.set(true, nil)
  43. } else {
  44. gpuState.set(false, err)
  45. useGPU = false
  46. }
  47. } else {
  48. gpuState.set(false, nil)
  49. useGPU = false
  50. }
  51. } else if gpuState != nil {
  52. gpuState.set(false, nil)
  53. }
  54. gotSamples := false
  55. for {
  56. select {
  57. case <-ctx.Done():
  58. return
  59. case <-logTicker.C:
  60. st := srcMgr.Stats()
  61. log.Printf("stats: buf=%d drop=%d reset=%d last=%dms", st.BufferSamples, st.Dropped, st.Resets, st.LastSampleAgoMs)
  62. case upd := <-updates:
  63. prevFFT := cfg.FFTSize
  64. prevUseGPU := useGPU
  65. cfg = upd.cfg
  66. if rec != nil {
  67. rec.Update(cfg.SampleRate, cfg.FFTSize, recorder.Policy{
  68. Enabled: cfg.Recorder.Enabled,
  69. MinSNRDb: cfg.Recorder.MinSNRDb,
  70. MinDuration: mustParseDuration(cfg.Recorder.MinDuration, 1*time.Second),
  71. MaxDuration: mustParseDuration(cfg.Recorder.MaxDuration, 300*time.Second),
  72. PrerollMs: cfg.Recorder.PrerollMs,
  73. RecordIQ: cfg.Recorder.RecordIQ,
  74. RecordAudio: cfg.Recorder.RecordAudio,
  75. AutoDemod: cfg.Recorder.AutoDemod,
  76. AutoDecode: cfg.Recorder.AutoDecode,
  77. MaxDiskMB: cfg.Recorder.MaxDiskMB,
  78. OutputDir: cfg.Recorder.OutputDir,
  79. ClassFilter: cfg.Recorder.ClassFilter,
  80. RingSeconds: cfg.Recorder.RingSeconds,
  81. }, cfg.CenterHz, buildDecoderMap(cfg))
  82. }
  83. if upd.det != nil {
  84. det = upd.det
  85. }
  86. if upd.window != nil {
  87. window = upd.window
  88. plan = fftutil.NewCmplxPlan(cfg.FFTSize)
  89. }
  90. dcEnabled = upd.dcBlock
  91. iqEnabled = upd.iqBalance
  92. if cfg.FFTSize != prevFFT || cfg.UseGPUFFT != prevUseGPU {
  93. srcMgr.Flush()
  94. gotSamples = false
  95. if gpuEngine != nil {
  96. gpuEngine.Close()
  97. gpuEngine = nil
  98. }
  99. useGPU = cfg.UseGPUFFT
  100. if useGPU && gpuState != nil {
  101. snap := gpuState.snapshot()
  102. if snap.Available {
  103. if eng, err := gpufft.New(cfg.FFTSize); err == nil {
  104. gpuEngine = eng
  105. gpuState.set(true, nil)
  106. } else {
  107. gpuState.set(false, err)
  108. useGPU = false
  109. }
  110. } else {
  111. gpuState.set(false, nil)
  112. useGPU = false
  113. }
  114. } else if gpuState != nil {
  115. gpuState.set(false, nil)
  116. }
  117. }
  118. dcBlocker.Reset()
  119. ticker.Reset(cfg.FrameInterval())
  120. case <-ticker.C:
  121. iq, err := srcMgr.ReadIQ(cfg.FFTSize)
  122. if err != nil {
  123. log.Printf("read IQ: %v", err)
  124. if strings.Contains(err.Error(), "timeout") {
  125. if err := srcMgr.Restart(cfg); err != nil {
  126. log.Printf("restart failed: %v", err)
  127. }
  128. }
  129. continue
  130. }
  131. if rec != nil {
  132. rec.Ingest(time.Now(), iq)
  133. }
  134. if !gotSamples {
  135. log.Printf("received IQ samples")
  136. gotSamples = true
  137. }
  138. if dcEnabled {
  139. dcBlocker.Apply(iq)
  140. }
  141. if iqEnabled {
  142. dsp.IQBalance(iq)
  143. }
  144. var spectrum []float64
  145. if useGPU && gpuEngine != nil {
  146. if len(window) == len(iq) {
  147. for i := 0; i < len(iq); i++ {
  148. v := iq[i]
  149. w := float32(window[i])
  150. iq[i] = complex(real(v)*w, imag(v)*w)
  151. }
  152. }
  153. out, err := gpuEngine.Exec(iq)
  154. if err != nil {
  155. if gpuState != nil {
  156. gpuState.set(false, err)
  157. }
  158. useGPU = false
  159. spectrum = fftutil.SpectrumWithPlan(iq, nil, plan)
  160. } else {
  161. spectrum = fftutil.SpectrumFromFFT(out)
  162. }
  163. } else {
  164. spectrum = fftutil.SpectrumWithPlan(iq, window, plan)
  165. }
  166. for i := range spectrum {
  167. if math.IsNaN(spectrum[i]) || math.IsInf(spectrum[i], 0) {
  168. spectrum[i] = -200
  169. }
  170. }
  171. now := time.Now()
  172. finished, signals := det.Process(now, spectrum, cfg.CenterHz)
  173. thresholds := det.LastThresholds()
  174. noiseFloor := det.LastNoiseFloor()
  175. if len(iq) > 0 {
  176. snips := extractSignalIQBatch(iq, cfg.SampleRate, cfg.CenterHz, signals)
  177. for i := range signals {
  178. var snip []complex64
  179. if i < len(snips) {
  180. snip = snips[i]
  181. }
  182. cls := classifier.Classify(classifier.SignalInput{FirstBin: signals[i].FirstBin, LastBin: signals[i].LastBin, SNRDb: signals[i].SNRDb}, spectrum, cfg.SampleRate, cfg.FFTSize, snip)
  183. signals[i].Class = cls
  184. }
  185. det.UpdateClasses(signals)
  186. }
  187. if sigSnap != nil {
  188. sigSnap.set(signals)
  189. }
  190. eventMu.Lock()
  191. for _, ev := range finished {
  192. _ = enc.Encode(ev)
  193. }
  194. eventMu.Unlock()
  195. if rec != nil && len(finished) > 0 {
  196. evCopy := make([]detector.Event, len(finished))
  197. copy(evCopy, finished)
  198. rec.OnEvents(evCopy)
  199. }
  200. var debugInfo *SpectrumDebug
  201. if len(thresholds) > 0 || len(signals) > 0 || noiseFloor != 0 {
  202. scoreDebug := make([]map[string]any, 0, len(signals))
  203. for _, s := range signals {
  204. if s.Class == nil || len(s.Class.Scores) == 0 {
  205. scoreDebug = append(scoreDebug, map[string]any{"center_hz": s.CenterHz, "class": nil})
  206. continue
  207. }
  208. scores := make(map[string]float64, len(s.Class.Scores))
  209. for k, v := range s.Class.Scores {
  210. scores[string(k)] = v
  211. }
  212. scoreDebug = append(scoreDebug, map[string]any{
  213. "center_hz": s.CenterHz,
  214. "mod_type": s.Class.ModType,
  215. "confidence": s.Class.Confidence,
  216. "second_best": s.Class.SecondBest,
  217. "scores": scores,
  218. })
  219. }
  220. debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug}
  221. }
  222. h.broadcast(SpectrumFrame{Timestamp: now.UnixMilli(), CenterHz: cfg.CenterHz, SampleHz: cfg.SampleRate, FFTSize: cfg.FFTSize, Spectrum: spectrum, Signals: signals, Debug: debugInfo})
  223. }
  224. }
  225. }