Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

348 рядки
13KB

  1. package main
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "log"
  7. "os"
  8. "runtime/debug"
  9. "strings"
  10. "sync"
  11. "time"
  12. "sdr-wideband-suite/internal/config"
  13. "sdr-wideband-suite/internal/detector"
  14. "sdr-wideband-suite/internal/dsp"
  15. "sdr-wideband-suite/internal/logging"
  16. "sdr-wideband-suite/internal/pipeline"
  17. "sdr-wideband-suite/internal/recorder"
  18. "sdr-wideband-suite/internal/telemetry"
  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, extractMgr *extractionManager, phaseSnap *phaseSnapshot, coll *telemetry.Collector) {
  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. rt := newDSPRuntime(cfg, det, window, gpuState, coll)
  27. ticker := time.NewTicker(cfg.FrameInterval())
  28. defer ticker.Stop()
  29. logTicker := time.NewTicker(5 * time.Second)
  30. defer logTicker.Stop()
  31. enc := json.NewEncoder(eventFile)
  32. dcBlocker := dsp.NewDCBlocker(0.995)
  33. state := &phaseState{}
  34. var frameID uint64
  35. prevDisplayed := map[int64]detector.Signal{}
  36. lastSourceDrops := uint64(0)
  37. lastSourceResets := uint64(0)
  38. for {
  39. select {
  40. case <-ctx.Done():
  41. return
  42. case <-logTicker.C:
  43. st := srcMgr.Stats()
  44. log.Printf("stats: buf=%d drop=%d reset=%d last=%dms", st.BufferSamples, st.Dropped, st.Resets, st.LastSampleAgoMs)
  45. if coll != nil {
  46. coll.SetGauge("source.buffer_samples", float64(st.BufferSamples), nil)
  47. coll.SetGauge("source.last_sample_ago_ms", float64(st.LastSampleAgoMs), nil)
  48. if st.Dropped > lastSourceDrops {
  49. coll.IncCounter("source.drop.count", float64(st.Dropped-lastSourceDrops), nil)
  50. }
  51. if st.Resets > lastSourceResets {
  52. coll.IncCounter("source.reset.count", float64(st.Resets-lastSourceResets), nil)
  53. coll.Event("source_reset", "warn", "source reset observed", nil, map[string]any{"resets": st.Resets})
  54. }
  55. lastSourceDrops = st.Dropped
  56. lastSourceResets = st.Resets
  57. }
  58. case upd := <-updates:
  59. rt.applyUpdate(upd, srcMgr, rec, gpuState)
  60. dcBlocker.Reset()
  61. ticker.Reset(rt.cfg.FrameInterval())
  62. if coll != nil {
  63. coll.IncCounter("dsp.update.apply", 1, nil)
  64. }
  65. case <-ticker.C:
  66. frameStart := time.Now()
  67. frameID++
  68. art, err := rt.captureSpectrum(srcMgr, rec, dcBlocker, gpuState)
  69. if err != nil {
  70. log.Printf("read IQ: %v", err)
  71. if strings.Contains(err.Error(), "timeout") {
  72. if err := srcMgr.Restart(rt.cfg); err != nil {
  73. log.Printf("restart failed: %v", err)
  74. }
  75. }
  76. continue
  77. }
  78. if !rt.gotSamples {
  79. log.Printf("received IQ samples")
  80. rt.gotSamples = true
  81. }
  82. logging.Debug("trace", "capture_done", "trace", frameID, "allIQ", len(art.allIQ), "detailIQ", len(art.detailIQ))
  83. if coll != nil {
  84. coll.Observe("stage.capture.duration_ms", float64(time.Since(frameStart).Microseconds())/1000.0, telemetry.TagsFromPairs("frame_id", fmt.Sprintf("%d", frameID)))
  85. }
  86. survStart := time.Now()
  87. state.surveillance = rt.buildSurveillanceResult(art)
  88. if coll != nil {
  89. coll.Observe("stage.surveillance.duration_ms", float64(time.Since(survStart).Microseconds())/1000.0, telemetry.TagsFromPairs("frame_id", fmt.Sprintf("%d", frameID)))
  90. }
  91. refineStart := time.Now()
  92. state.refinement = rt.runRefinement(art, state.surveillance, extractMgr, rec)
  93. if coll != nil {
  94. coll.Observe("stage.refinement.duration_ms", float64(time.Since(refineStart).Microseconds())/1000.0, telemetry.TagsFromPairs("frame_id", fmt.Sprintf("%d", frameID)))
  95. }
  96. finished := state.surveillance.Finished
  97. thresholds := state.surveillance.Thresholds
  98. noiseFloor := state.surveillance.NoiseFloor
  99. var displaySignals []detector.Signal
  100. if len(art.detailIQ) > 0 {
  101. displaySignals = state.refinement.Result.Signals
  102. stableSignals := rt.det.StableSignals()
  103. streamSignals := displaySignals
  104. if len(stableSignals) > 0 {
  105. streamSignals = stableSignals
  106. }
  107. if rec != nil && len(art.allIQ) > 0 {
  108. if art.streamDropped {
  109. rt.streamOverlap = &streamIQOverlap{}
  110. for k := range rt.streamPhaseState {
  111. rt.streamPhaseState[k].phase = 0
  112. }
  113. resetStreamingOracleRunner()
  114. rec.ResetStreams()
  115. logging.Warn("gap", "iq_dropped", "msg", "buffer bloat caused extraction drop; overlap reset")
  116. if coll != nil {
  117. coll.IncCounter("capture.stream_reset", 1, nil)
  118. coll.Event("iq_dropped", "warn", "stream overlap reset after dropped IQ", nil, map[string]any{"frame_id": frameID})
  119. }
  120. }
  121. if rt.cfg.Recorder.DebugLiveAudio {
  122. log.Printf("LIVEAUDIO DSP: detailIQ=%d displaySignals=%d streamSignals=%d stableSignals=%d allIQ=%d", len(art.detailIQ), len(displaySignals), len(streamSignals), len(stableSignals), len(art.allIQ))
  123. }
  124. aqCfg := extractionConfig{firTaps: rt.cfg.Recorder.ExtractionTaps, bwMult: rt.cfg.Recorder.ExtractionBwMult}
  125. extractStart := time.Now()
  126. streamSnips, streamRates := extractForStreaming(extractMgr, art.allIQ, rt.cfg.SampleRate, rt.cfg.CenterHz, streamSignals, rt.streamPhaseState, rt.streamOverlap, aqCfg, rt.telemetry)
  127. if coll != nil {
  128. coll.Observe("stage.extract_stream.duration_ms", float64(time.Since(extractStart).Microseconds())/1000.0, telemetry.TagsFromPairs("frame_id", fmt.Sprintf("%d", frameID)))
  129. coll.SetGauge("stage.extract_stream.signals", float64(len(streamSignals)), nil)
  130. if coll.ShouldSampleHeavy() {
  131. for i := range streamSnips {
  132. if i >= len(streamSignals) {
  133. break
  134. }
  135. tags := telemetry.TagsFromPairs(
  136. "signal_id", fmt.Sprintf("%d", streamSignals[i].ID),
  137. "stage", "extract_stream",
  138. )
  139. coll.SetGauge("iq.stage.extract.length", float64(len(streamSnips[i])), tags)
  140. if len(streamSnips[i]) > 0 {
  141. observeIQStats(coll, "extract_stream", streamSnips[i], tags)
  142. }
  143. }
  144. }
  145. }
  146. nonEmpty := 0
  147. minLen := 0
  148. maxLen := 0
  149. for i := range streamSnips {
  150. l := len(streamSnips[i])
  151. if l == 0 {
  152. continue
  153. }
  154. nonEmpty++
  155. if minLen == 0 || l < minLen {
  156. minLen = l
  157. }
  158. if l > maxLen {
  159. maxLen = l
  160. }
  161. }
  162. logging.Debug("trace", "extract_stats", "trace", frameID, "signals", len(streamSignals), "nonempty", nonEmpty, "minLen", minLen, "maxLen", maxLen)
  163. items := make([]recorder.StreamFeedItem, 0, len(streamSignals))
  164. for j, ds := range streamSignals {
  165. className := "<nil>"
  166. if ds.Class != nil {
  167. className = string(ds.Class.ModType)
  168. }
  169. snipLen := 0
  170. if j < len(streamSnips) {
  171. snipLen = len(streamSnips[j])
  172. }
  173. if rt.cfg.Recorder.DebugLiveAudio {
  174. log.Printf("LIVEAUDIO DSP: streamSignal idx=%d id=%d center=%.3fMHz bw=%.0f class=%s snip=%d", j, ds.ID, ds.CenterHz/1e6, ds.BWHz, className, snipLen)
  175. }
  176. if ds.ID == 0 || ds.Class == nil {
  177. continue
  178. }
  179. if j >= len(streamSnips) || len(streamSnips[j]) == 0 {
  180. logging.Warn("gap", "snippet_empty", "signal", ds.ID)
  181. continue
  182. }
  183. snipRate := rt.cfg.SampleRate
  184. if j < len(streamRates) && streamRates[j] > 0 {
  185. snipRate = streamRates[j]
  186. }
  187. items = append(items, recorder.StreamFeedItem{Signal: ds, Snippet: streamSnips[j], SnipRate: snipRate})
  188. }
  189. if rt.cfg.Recorder.DebugLiveAudio {
  190. log.Printf("LIVEAUDIO DSP: feedItems=%d", len(items))
  191. }
  192. if len(items) > 0 {
  193. feedStart := time.Now()
  194. rec.FeedSnippets(items, frameID)
  195. if coll != nil {
  196. coll.Observe("stage.feed_enqueue.duration_ms", float64(time.Since(feedStart).Microseconds())/1000.0, telemetry.TagsFromPairs("frame_id", fmt.Sprintf("%d", frameID)))
  197. coll.SetGauge("stage.feed.items", float64(len(items)), nil)
  198. }
  199. logging.Debug("trace", "feed", "trace", frameID, "items", len(items), "signals", len(streamSignals), "allIQ", len(art.allIQ))
  200. } else {
  201. logging.Warn("gap", "feed_empty", "signals", len(streamSignals), "trace", frameID)
  202. if coll != nil {
  203. coll.IncCounter("stage.feed.empty", 1, nil)
  204. }
  205. }
  206. }
  207. rt.maintenance(displaySignals, rec)
  208. } else {
  209. displaySignals = rt.det.StableSignals()
  210. }
  211. if rec != nil && len(displaySignals) > 0 {
  212. runtimeInfo := rec.RuntimeInfoBySignalID()
  213. for i := range displaySignals {
  214. if info, ok := runtimeInfo[displaySignals[i].ID]; ok {
  215. displaySignals[i].DemodName = info.DemodName
  216. displaySignals[i].PlaybackMode = info.PlaybackMode
  217. displaySignals[i].StereoState = info.StereoState
  218. }
  219. }
  220. }
  221. state.arbitration = rt.arbitration
  222. state.presentation = state.surveillance.DisplayLevel
  223. if phaseSnap != nil {
  224. phaseSnap.Set(*state)
  225. }
  226. if sigSnap != nil {
  227. sigSnap.set(displaySignals)
  228. }
  229. if coll != nil {
  230. coll.SetGauge("signals.display.count", float64(len(displaySignals)), nil)
  231. current := make(map[int64]detector.Signal, len(displaySignals))
  232. for _, s := range displaySignals {
  233. current[s.ID] = s
  234. if _, ok := prevDisplayed[s.ID]; !ok {
  235. coll.Event("signal_create", "info", "signal entered display set", telemetry.TagsFromPairs("signal_id", fmt.Sprintf("%d", s.ID)), map[string]any{
  236. "center_hz": s.CenterHz,
  237. "bw_hz": s.BWHz,
  238. })
  239. }
  240. }
  241. for id, prev := range prevDisplayed {
  242. if _, ok := current[id]; !ok {
  243. coll.Event("signal_remove", "info", "signal left display set", telemetry.TagsFromPairs("signal_id", fmt.Sprintf("%d", id)), map[string]any{
  244. "center_hz": prev.CenterHz,
  245. })
  246. }
  247. }
  248. prevDisplayed = current
  249. }
  250. eventMu.Lock()
  251. for _, ev := range finished {
  252. _ = enc.Encode(ev)
  253. }
  254. eventMu.Unlock()
  255. if rec != nil && len(finished) > 0 {
  256. evCopy := make([]detector.Event, len(finished))
  257. copy(evCopy, finished)
  258. rec.OnEvents(evCopy)
  259. }
  260. var debugInfo *SpectrumDebug
  261. plan := state.refinement.Input.Plan
  262. windowSummary := buildWindowSummary(plan, state.refinement.Input.Windows, state.surveillance.Candidates, state.refinement.Input.WorkItems, state.refinement.Result.Decisions)
  263. var windowStats *RefinementWindowStats
  264. var monitorSummary []pipeline.MonitorWindowStats
  265. if windowSummary != nil {
  266. windowStats = windowSummary.Refinement
  267. monitorSummary = windowSummary.MonitorWindows
  268. }
  269. hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0
  270. hasWindows := windowStats != nil && windowStats.Count > 0
  271. if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan || hasWindows {
  272. scoreDebug := make([]map[string]any, 0, len(displaySignals))
  273. for _, s := range displaySignals {
  274. if s.Class == nil || len(s.Class.Scores) == 0 {
  275. scoreDebug = append(scoreDebug, map[string]any{"center_hz": s.CenterHz, "class": nil})
  276. continue
  277. }
  278. scores := make(map[string]float64, len(s.Class.Scores))
  279. for k, v := range s.Class.Scores {
  280. scores[string(k)] = v
  281. }
  282. scoreDebug = append(scoreDebug, map[string]any{
  283. "center_hz": s.CenterHz,
  284. "mod_type": s.Class.ModType,
  285. "confidence": s.Class.Confidence,
  286. "second_best": s.Class.SecondBest,
  287. "scores": scores,
  288. })
  289. }
  290. debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug}
  291. candidateSources := buildCandidateSourceSummary(state.surveillance.Candidates)
  292. candidateEvidence := buildCandidateEvidenceSummary(state.surveillance.Candidates)
  293. candidateEvidenceStates := buildCandidateEvidenceStateSummary(state.surveillance.Candidates)
  294. candidateWindows := buildCandidateWindowSummary(state.surveillance.Candidates, plan.MonitorWindows)
  295. if len(candidateSources) > 0 {
  296. debugInfo.CandidateSources = candidateSources
  297. }
  298. if len(candidateEvidence) > 0 {
  299. debugInfo.CandidateEvidence = candidateEvidence
  300. }
  301. if candidateEvidenceStates != nil {
  302. debugInfo.CandidateEvidenceStates = candidateEvidenceStates
  303. }
  304. if len(candidateWindows) > 0 {
  305. debugInfo.CandidateWindows = candidateWindows
  306. }
  307. if len(monitorSummary) > 0 {
  308. debugInfo.MonitorWindowStats = monitorSummary
  309. }
  310. if windowSummary != nil {
  311. debugInfo.WindowSummary = windowSummary
  312. }
  313. if hasPlan {
  314. debugInfo.RefinementPlan = &plan
  315. }
  316. if hasWindows {
  317. debugInfo.Windows = windowStats
  318. }
  319. refinementDebug := &RefinementDebug{}
  320. if hasPlan {
  321. refinementDebug.Plan = &plan
  322. refinementDebug.Request = &state.refinement.Input.Request
  323. refinementDebug.WorkItems = state.refinement.Input.WorkItems
  324. }
  325. if hasWindows {
  326. refinementDebug.Windows = windowStats
  327. }
  328. if len(monitorSummary) > 0 {
  329. refinementDebug.MonitorWindowStats = monitorSummary
  330. }
  331. if windowSummary != nil {
  332. refinementDebug.WindowSummary = windowSummary
  333. }
  334. refinementDebug.Arbitration = buildArbitrationSnapshot(state.refinement, state.arbitration)
  335. debugInfo.Refinement = refinementDebug
  336. }
  337. h.broadcast(SpectrumFrame{Timestamp: art.now.UnixMilli(), CenterHz: rt.cfg.CenterHz, SampleHz: rt.cfg.SampleRate, FFTSize: rt.cfg.FFTSize, Spectrum: art.surveillanceSpectrum, Signals: displaySignals, Debug: debugInfo})
  338. if coll != nil {
  339. coll.Observe("dsp.frame.duration_ms", float64(time.Since(frameStart).Microseconds())/1000.0, nil)
  340. }
  341. }
  342. }
  343. }