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.

242 lines
5.7KB

  1. package main
  2. import (
  3. "encoding/binary"
  4. "encoding/json"
  5. "log"
  6. "math"
  7. "time"
  8. "sdr-visual-suite/internal/detector"
  9. )
  10. func (s *signalSnapshot) set(sig []detector.Signal) {
  11. s.mu.Lock()
  12. defer s.mu.Unlock()
  13. s.signals = append([]detector.Signal(nil), sig...)
  14. }
  15. func (s *signalSnapshot) get() []detector.Signal {
  16. s.mu.RLock()
  17. defer s.mu.RUnlock()
  18. return append([]detector.Signal(nil), s.signals...)
  19. }
  20. func (g *gpuStatus) set(active bool, err error) {
  21. g.mu.Lock()
  22. defer g.mu.Unlock()
  23. g.Active = active
  24. if err != nil {
  25. g.Error = err.Error()
  26. } else {
  27. g.Error = ""
  28. }
  29. }
  30. func (g *gpuStatus) snapshot() gpuStatus {
  31. g.mu.RLock()
  32. defer g.mu.RUnlock()
  33. return gpuStatus{Available: g.Available, Active: g.Active, Error: g.Error}
  34. }
  35. func newHub() *hub {
  36. return &hub{clients: map[*client]struct{}{}, lastLogTs: time.Now()}
  37. }
  38. func (h *hub) add(c *client) {
  39. h.mu.Lock()
  40. defer h.mu.Unlock()
  41. h.clients[c] = struct{}{}
  42. log.Printf("ws connected (%d clients)", len(h.clients))
  43. }
  44. func (h *hub) remove(c *client) {
  45. c.closeOnce.Do(func() { close(c.done) })
  46. h.mu.Lock()
  47. defer h.mu.Unlock()
  48. delete(h.clients, c)
  49. log.Printf("ws disconnected (%d clients)", len(h.clients))
  50. }
  51. func (h *hub) broadcast(frame SpectrumFrame) {
  52. // Pre-encode JSON for legacy clients (only if needed)
  53. var jsonBytes []byte
  54. // Pre-encode binary for binary clients at various decimation levels
  55. // We cache per unique maxBins value to avoid re-encoding
  56. type binCacheEntry struct {
  57. bins int
  58. data []byte
  59. }
  60. var binCache []binCacheEntry
  61. h.mu.Lock()
  62. clients := make([]*client, 0, len(h.clients))
  63. for c := range h.clients {
  64. clients = append(clients, c)
  65. }
  66. h.mu.Unlock()
  67. for _, c := range clients {
  68. // Frame rate limiting
  69. if c.targetFps > 0 && c.frameSkip > 1 {
  70. c.frameN++
  71. if c.frameN%c.frameSkip != 0 {
  72. continue
  73. }
  74. }
  75. if c.binary {
  76. // Find or create cached binary encoding for this bin count
  77. bins := c.maxBins
  78. if bins <= 0 || bins >= len(frame.Spectrum) {
  79. bins = len(frame.Spectrum)
  80. }
  81. var encoded []byte
  82. for _, entry := range binCache {
  83. if entry.bins == bins {
  84. encoded = entry.data
  85. break
  86. }
  87. }
  88. if encoded == nil {
  89. encoded = encodeBinaryFrame(frame, bins)
  90. binCache = append(binCache, binCacheEntry{bins: bins, data: encoded})
  91. }
  92. select {
  93. case c.send <- encoded:
  94. default:
  95. h.remove(c)
  96. }
  97. } else {
  98. // JSON path (legacy)
  99. if jsonBytes == nil {
  100. var err error
  101. jsonBytes, err = json.Marshal(frame)
  102. if err != nil {
  103. log.Printf("marshal frame: %v", err)
  104. return
  105. }
  106. }
  107. select {
  108. case c.send <- jsonBytes:
  109. default:
  110. h.remove(c)
  111. }
  112. }
  113. }
  114. h.frameCnt++
  115. if time.Since(h.lastLogTs) > 2*time.Second {
  116. h.lastLogTs = time.Now()
  117. log.Printf("broadcast frames=%d clients=%d", h.frameCnt, len(clients))
  118. }
  119. }
  120. // ---------------------------------------------------------------------------
  121. // Binary spectrum protocol v4
  122. // ---------------------------------------------------------------------------
  123. //
  124. // Hybrid approach: spectrum data as compact binary, signals + debug as JSON.
  125. //
  126. // Layout (32-byte header):
  127. // [0:1] magic: 0x53 0x50 ("SP")
  128. // [2:3] version: uint16 LE = 4
  129. // [4:11] timestamp: int64 LE (Unix millis)
  130. // [12:19] center_hz: float64 LE
  131. // [20:23] bin_count: uint32 LE (supports FFT up to 4 billion)
  132. // [24:27] sample_rate_hz: uint32 LE (Hz, max ~4.29 GHz)
  133. // [28:31] json_offset: uint32 LE (byte offset where JSON starts)
  134. //
  135. // [32 .. 32+bins*2-1] spectrum: int16 LE, dB × 100
  136. // [json_offset ..] JSON: {"signals":[...],"debug":{...}}
  137. const binaryHeaderSize = 32
  138. func encodeBinaryFrame(frame SpectrumFrame, targetBins int) []byte {
  139. spectrum := frame.Spectrum
  140. srcBins := len(spectrum)
  141. if targetBins <= 0 || targetBins > srcBins {
  142. targetBins = srcBins
  143. }
  144. var decimated []float64
  145. if targetBins < srcBins && targetBins > 0 {
  146. decimated = decimateSpectrum(spectrum, targetBins)
  147. } else {
  148. decimated = spectrum
  149. targetBins = srcBins
  150. }
  151. // JSON-encode signals + debug (full fidelity)
  152. jsonPart, _ := json.Marshal(struct {
  153. Signals []detector.Signal `json:"signals"`
  154. Debug *SpectrumDebug `json:"debug,omitempty"`
  155. }{
  156. Signals: frame.Signals,
  157. Debug: frame.Debug,
  158. })
  159. specBytes := targetBins * 2
  160. jsonOffset := uint32(binaryHeaderSize + specBytes)
  161. totalSize := int(jsonOffset) + len(jsonPart)
  162. buf := make([]byte, totalSize)
  163. // Header
  164. buf[0] = 0x53 // 'S'
  165. buf[1] = 0x50 // 'P'
  166. binary.LittleEndian.PutUint16(buf[2:4], 4) // version 4
  167. binary.LittleEndian.PutUint64(buf[4:12], uint64(frame.Timestamp))
  168. binary.LittleEndian.PutUint64(buf[12:20], math.Float64bits(frame.CenterHz))
  169. binary.LittleEndian.PutUint32(buf[20:24], uint32(targetBins))
  170. binary.LittleEndian.PutUint32(buf[24:28], uint32(frame.SampleHz))
  171. binary.LittleEndian.PutUint32(buf[28:32], jsonOffset)
  172. // Spectrum (int16, dB × 100)
  173. off := binaryHeaderSize
  174. for i := 0; i < targetBins; i++ {
  175. v := decimated[i] * 100
  176. if v > 32767 {
  177. v = 32767
  178. } else if v < -32767 {
  179. v = -32767
  180. }
  181. binary.LittleEndian.PutUint16(buf[off:off+2], uint16(int16(v)))
  182. off += 2
  183. }
  184. // JSON signals + debug
  185. copy(buf[jsonOffset:], jsonPart)
  186. return buf
  187. }
  188. // decimateSpectrum reduces bins via peak-hold within each group.
  189. func decimateSpectrum(spectrum []float64, targetBins int) []float64 {
  190. src := len(spectrum)
  191. out := make([]float64, targetBins)
  192. ratio := float64(src) / float64(targetBins)
  193. for i := 0; i < targetBins; i++ {
  194. lo := int(float64(i) * ratio)
  195. hi := int(float64(i+1) * ratio)
  196. if hi > src {
  197. hi = src
  198. }
  199. if lo >= hi {
  200. if lo < src {
  201. out[i] = spectrum[lo]
  202. }
  203. continue
  204. }
  205. peak := spectrum[lo]
  206. for j := lo + 1; j < hi; j++ {
  207. if spectrum[j] > peak {
  208. peak = spectrum[j]
  209. }
  210. }
  211. out[i] = peak
  212. }
  213. return out
  214. }