Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

408 linhas
10KB

  1. package recorder
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. "sdr-wideband-suite/internal/demod/gpudemod"
  12. "sdr-wideband-suite/internal/detector"
  13. "sdr-wideband-suite/internal/telemetry"
  14. )
  15. type Policy struct {
  16. Enabled bool `yaml:"enabled" json:"enabled"`
  17. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  18. MinDuration time.Duration `yaml:"min_duration" json:"min_duration"`
  19. MaxDuration time.Duration `yaml:"max_duration" json:"max_duration"`
  20. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  21. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  22. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  23. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  24. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  25. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  26. OutputDir string `yaml:"output_dir" json:"output_dir"`
  27. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  28. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  29. // Audio quality (AQ-2, AQ-3, AQ-5)
  30. DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"`
  31. ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"`
  32. ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"`
  33. DebugLiveAudio bool `yaml:"debug_live_audio" json:"debug_live_audio"`
  34. }
  35. type Manager struct {
  36. mu sync.RWMutex
  37. policy Policy
  38. ring *Ring
  39. sampleRate int
  40. blockSize int
  41. centerHz float64
  42. decodeCommands map[string]string
  43. queue chan detector.Event
  44. gpuDemod *gpudemod.Engine
  45. closed bool
  46. closeOnce sync.Once
  47. workerWG sync.WaitGroup
  48. // Streaming recorder
  49. streamer *Streamer
  50. streamedIDs map[int64]bool // signal IDs that were streamed (skip retroactive recording)
  51. streamedMu sync.Mutex
  52. telemetry *telemetry.Collector
  53. }
  54. func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string, coll *telemetry.Collector) *Manager {
  55. if policy.OutputDir == "" {
  56. policy.OutputDir = "data/recordings"
  57. }
  58. if policy.RingSeconds <= 0 {
  59. policy.RingSeconds = 8
  60. }
  61. m := &Manager{
  62. policy: policy,
  63. ring: NewRing(sampleRate, blockSize, policy.RingSeconds),
  64. sampleRate: sampleRate,
  65. blockSize: blockSize,
  66. centerHz: centerHz,
  67. decodeCommands: decodeCommands,
  68. queue: make(chan detector.Event, 64),
  69. streamer: newStreamer(policy, centerHz, coll),
  70. streamedIDs: make(map[int64]bool),
  71. telemetry: coll,
  72. }
  73. m.initGPUDemod(sampleRate, blockSize)
  74. m.workerWG.Add(1)
  75. go m.worker()
  76. return m
  77. }
  78. func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
  79. m.mu.Lock()
  80. defer m.mu.Unlock()
  81. m.policy = policy
  82. m.centerHz = centerHz
  83. m.decodeCommands = decodeCommands
  84. // Only reset ring and GPU engine if sample parameters actually changed
  85. needRingReset := m.sampleRate != sampleRate || m.blockSize != blockSize
  86. m.sampleRate = sampleRate
  87. m.blockSize = blockSize
  88. if needRingReset {
  89. m.initGPUDemodLocked(sampleRate, blockSize)
  90. if m.ring == nil {
  91. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  92. } else {
  93. m.ring.Reset(sampleRate, blockSize, policy.RingSeconds)
  94. }
  95. } else if m.ring == nil {
  96. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  97. }
  98. if m.streamer != nil {
  99. m.streamer.updatePolicy(policy, centerHz)
  100. }
  101. if m.telemetry != nil {
  102. m.telemetry.Event("recorder_update", "info", "recorder policy updated", nil, map[string]any{
  103. "sample_rate": sampleRate,
  104. "block_size": blockSize,
  105. "enabled": policy.Enabled,
  106. })
  107. }
  108. }
  109. func (m *Manager) Ingest(t0 time.Time, samples []complex64) {
  110. if m == nil {
  111. return
  112. }
  113. m.mu.RLock()
  114. ring := m.ring
  115. m.mu.RUnlock()
  116. if ring == nil {
  117. return
  118. }
  119. ring.Push(t0, samples)
  120. if m.telemetry != nil {
  121. m.telemetry.SetGauge("recorder.ring.push_samples", float64(len(samples)), nil)
  122. }
  123. }
  124. func (m *Manager) OnEvents(events []detector.Event) {
  125. if m == nil || len(events) == 0 {
  126. return
  127. }
  128. m.mu.RLock()
  129. enabled := m.policy.Enabled
  130. closed := m.closed
  131. m.mu.RUnlock()
  132. if !enabled || closed {
  133. return
  134. }
  135. for _, ev := range events {
  136. select {
  137. case m.queue <- ev:
  138. default:
  139. // drop if queue full
  140. if m.telemetry != nil {
  141. m.telemetry.IncCounter("recorder.event_queue.drop", 1, nil)
  142. }
  143. }
  144. }
  145. if m.telemetry != nil {
  146. m.telemetry.SetGauge("recorder.event_queue.len", float64(len(m.queue)), nil)
  147. }
  148. }
  149. func (m *Manager) worker() {
  150. defer m.workerWG.Done()
  151. for ev := range m.queue {
  152. _ = m.recordEvent(ev)
  153. }
  154. }
  155. func (m *Manager) initGPUDemod(sampleRate int, blockSize int) {
  156. m.mu.Lock()
  157. defer m.mu.Unlock()
  158. m.initGPUDemodLocked(sampleRate, blockSize)
  159. }
  160. func (m *Manager) gpuEngine() *gpudemod.Engine {
  161. m.mu.RLock()
  162. defer m.mu.RUnlock()
  163. return m.gpuDemod
  164. }
  165. func (m *Manager) initGPUDemodLocked(sampleRate int, blockSize int) {
  166. if m.gpuDemod != nil {
  167. m.gpuDemod.Close()
  168. m.gpuDemod = nil
  169. }
  170. if !gpudemod.Available() {
  171. return
  172. }
  173. eng, err := gpudemod.New(blockSize, sampleRate)
  174. if err != nil {
  175. return
  176. }
  177. m.gpuDemod = eng
  178. }
  179. func (m *Manager) Close() {
  180. if m == nil {
  181. return
  182. }
  183. m.closeOnce.Do(func() {
  184. // Close all active streaming sessions first
  185. if m.streamer != nil {
  186. m.streamer.CloseAll()
  187. }
  188. m.mu.Lock()
  189. m.closed = true
  190. if m.queue != nil {
  191. close(m.queue)
  192. }
  193. gpu := m.gpuDemod
  194. m.gpuDemod = nil
  195. m.mu.Unlock()
  196. m.workerWG.Wait()
  197. if gpu != nil {
  198. gpu.Close()
  199. }
  200. })
  201. }
  202. func (m *Manager) recordEvent(ev detector.Event) error {
  203. // Skip events that were already recorded via streaming
  204. m.streamedMu.Lock()
  205. wasStreamed := m.streamedIDs[ev.ID]
  206. delete(m.streamedIDs, ev.ID) // clean up — event is finished
  207. m.streamedMu.Unlock()
  208. if wasStreamed {
  209. log.Printf("STREAM: skipping retroactive recording for signal %d (already streamed)", ev.ID)
  210. return nil
  211. }
  212. m.mu.RLock()
  213. policy := m.policy
  214. ring := m.ring
  215. sampleRate := m.sampleRate
  216. centerHz := m.centerHz
  217. m.mu.RUnlock()
  218. if !policy.Enabled {
  219. return nil
  220. }
  221. if ev.SNRDb < policy.MinSNRDb {
  222. return nil
  223. }
  224. dur := ev.End.Sub(ev.Start)
  225. if policy.MinDuration > 0 && dur < policy.MinDuration {
  226. return nil
  227. }
  228. if policy.MaxDuration > 0 && dur > policy.MaxDuration {
  229. return nil
  230. }
  231. if len(policy.ClassFilter) > 0 && ev.Class != nil {
  232. match := false
  233. for _, c := range policy.ClassFilter {
  234. if strings.EqualFold(c, string(ev.Class.ModType)) {
  235. match = true
  236. break
  237. }
  238. }
  239. if !match {
  240. return nil
  241. }
  242. }
  243. if !policy.RecordIQ && !policy.RecordAudio {
  244. return nil
  245. }
  246. start := ev.Start.Add(-time.Duration(policy.PrerollMs) * time.Millisecond)
  247. end := ev.End
  248. if start.After(end) {
  249. return errors.New("invalid event window")
  250. }
  251. if ring == nil {
  252. return errors.New("no ring buffer")
  253. }
  254. segment := ring.Slice(start, end)
  255. if len(segment) == 0 {
  256. return errors.New("no iq in ring")
  257. }
  258. dir := filepath.Join(policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID))
  259. if err := os.MkdirAll(dir, 0o755); err != nil {
  260. return err
  261. }
  262. files := map[string]any{}
  263. var iqPath string
  264. if policy.RecordIQ {
  265. iqPath = filepath.Join(dir, "signal.cf32")
  266. if err := writeCF32(iqPath, segment); err != nil {
  267. return err
  268. }
  269. files["iq"] = "signal.cf32"
  270. files["iq_format"] = "cf32"
  271. files["iq_sample_rate"] = sampleRate
  272. }
  273. if policy.RecordAudio && policy.AutoDemod && ev.Class != nil {
  274. if err := m.demodAndWrite(dir, ev, segment, files); err != nil {
  275. return err
  276. }
  277. }
  278. if policy.AutoDecode && iqPath != "" && ev.Class != nil {
  279. m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, sampleRate, files, dir)
  280. }
  281. _ = centerHz
  282. return writeMeta(dir, ev, sampleRate, files)
  283. }
  284. // SliceRecent returns the most recent `seconds` of raw IQ from the ring buffer.
  285. // Returns the IQ samples, sample rate, and center frequency.
  286. func (m *Manager) SliceRecent(seconds float64) ([]complex64, int, float64) {
  287. if m == nil {
  288. return nil, 0, 0
  289. }
  290. m.mu.RLock()
  291. ring := m.ring
  292. sr := m.sampleRate
  293. center := m.centerHz
  294. m.mu.RUnlock()
  295. if ring == nil || sr <= 0 {
  296. return nil, 0, 0
  297. }
  298. end := time.Now()
  299. start := end.Add(-time.Duration(seconds * float64(time.Second)))
  300. iq := ring.Slice(start, end)
  301. return iq, sr, center
  302. }
  303. // FeedSnippets is called once per DSP frame with pre-extracted IQ snippets
  304. // (GPU-accelerated FreqShift+FIR+Decimate). The Streamer handles demod with
  305. // persistent state (overlap-save, stereo decode, de-emphasis) asynchronously.
  306. func (m *Manager) FeedSnippets(items []StreamFeedItem, traceID uint64) {
  307. if m == nil || m.streamer == nil || len(items) == 0 {
  308. return
  309. }
  310. m.mu.RLock()
  311. closed := m.closed
  312. m.mu.RUnlock()
  313. if closed {
  314. return
  315. }
  316. // Mark all signal IDs so recordEvent skips them
  317. m.streamedMu.Lock()
  318. for _, item := range items {
  319. if item.Signal.ID != 0 {
  320. m.streamedIDs[item.Signal.ID] = true
  321. }
  322. }
  323. m.streamedMu.Unlock()
  324. // Convert to internal type
  325. internal := make([]streamFeedItem, len(items))
  326. for i, item := range items {
  327. internal[i] = streamFeedItem{
  328. signal: item.Signal,
  329. snippet: item.Snippet,
  330. snipRate: item.SnipRate,
  331. }
  332. }
  333. m.streamer.FeedSnippets(internal, traceID)
  334. }
  335. // StreamFeedItem is the public type for passing extracted snippets from DSP loop.
  336. type StreamFeedItem struct {
  337. Signal detector.Signal
  338. Snippet []complex64
  339. SnipRate int
  340. }
  341. // Streamer returns the underlying Streamer for live-listen subscriptions.
  342. func (m *Manager) StreamerRef() *Streamer {
  343. if m == nil {
  344. return nil
  345. }
  346. return m.streamer
  347. }
  348. func (m *Manager) ResetStreams() {
  349. if m == nil || m.streamer == nil {
  350. return
  351. }
  352. m.streamer.ResetStreams()
  353. }
  354. func (m *Manager) RuntimeInfoBySignalID() map[int64]RuntimeSignalInfo {
  355. if m == nil || m.streamer == nil {
  356. return nil
  357. }
  358. return m.streamer.RuntimeInfoBySignalID()
  359. }
  360. // ActiveStreams returns info about currently active streaming sessions.
  361. func (m *Manager) ActiveStreams() int {
  362. if m == nil || m.streamer == nil {
  363. return 0
  364. }
  365. return m.streamer.ActiveSessions()
  366. }
  367. // HasListeners returns true if any live-listen subscribers are active or pending.
  368. func (m *Manager) HasListeners() bool {
  369. if m == nil || m.streamer == nil {
  370. return false
  371. }
  372. return m.streamer.HasListeners()
  373. }