Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

236 řádky
6.2KB

  1. package main
  2. import (
  3. "encoding/json"
  4. "log"
  5. "net/http"
  6. "strconv"
  7. "time"
  8. "github.com/gorilla/websocket"
  9. "sdr-wideband-suite/internal/logging"
  10. "sdr-wideband-suite/internal/recorder"
  11. )
  12. func registerWSHandlers(mux *http.ServeMux, h *hub, recMgr *recorder.Manager) {
  13. upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
  14. origin := r.Header.Get("Origin")
  15. if origin == "" || origin == "null" {
  16. return true
  17. }
  18. return true
  19. }}
  20. mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
  21. conn, err := upgrader.Upgrade(w, r, nil)
  22. if err != nil {
  23. log.Printf("ws upgrade failed: %v (origin: %s)", err, r.Header.Get("Origin"))
  24. return
  25. }
  26. // Parse query params for remote clients: ?binary=1&bins=2048&fps=5
  27. q := r.URL.Query()
  28. c := &client{conn: conn, send: make(chan []byte, 64), done: make(chan struct{})}
  29. if q.Get("binary") == "1" || q.Get("binary") == "true" {
  30. c.binary = true
  31. }
  32. if v, err := strconv.Atoi(q.Get("bins")); err == nil && v > 0 {
  33. c.maxBins = v
  34. }
  35. if v, err := strconv.Atoi(q.Get("fps")); err == nil && v > 0 {
  36. c.targetFps = v
  37. // frameSkip: if server runs at ~15fps and client wants 5fps → skip 3
  38. c.frameSkip = 15 / v
  39. if c.frameSkip < 1 {
  40. c.frameSkip = 1
  41. }
  42. }
  43. h.add(c)
  44. defer func() {
  45. h.remove(c)
  46. _ = conn.Close()
  47. }()
  48. conn.SetReadDeadline(time.Now().Add(60 * time.Second))
  49. conn.SetPongHandler(func(string) error {
  50. conn.SetReadDeadline(time.Now().Add(60 * time.Second))
  51. return nil
  52. })
  53. go func() {
  54. ping := time.NewTicker(30 * time.Second)
  55. defer ping.Stop()
  56. for {
  57. select {
  58. case msg, ok := <-c.send:
  59. if !ok {
  60. return
  61. }
  62. // Binary frames can be large (130KB+) — need more time
  63. deadline := 500 * time.Millisecond
  64. if !c.binary {
  65. deadline = 200 * time.Millisecond
  66. }
  67. _ = conn.SetWriteDeadline(time.Now().Add(deadline))
  68. msgType := websocket.TextMessage
  69. if c.binary {
  70. msgType = websocket.BinaryMessage
  71. }
  72. if err := conn.WriteMessage(msgType, msg); err != nil {
  73. return
  74. }
  75. case <-ping.C:
  76. _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
  77. if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
  78. return
  79. }
  80. case <-c.done:
  81. return
  82. }
  83. }
  84. }()
  85. // Read loop: handle config messages from client + keep-alive
  86. for {
  87. _, msg, err := conn.ReadMessage()
  88. if err != nil {
  89. return
  90. }
  91. // Try to parse as client config update
  92. var cfg struct {
  93. Binary *bool `json:"binary,omitempty"`
  94. Bins *int `json:"bins,omitempty"`
  95. FPS *int `json:"fps,omitempty"`
  96. }
  97. if json.Unmarshal(msg, &cfg) == nil {
  98. if cfg.Binary != nil {
  99. c.binary = *cfg.Binary
  100. }
  101. if cfg.Bins != nil && *cfg.Bins > 0 {
  102. c.maxBins = *cfg.Bins
  103. }
  104. if cfg.FPS != nil && *cfg.FPS > 0 {
  105. c.targetFps = *cfg.FPS
  106. c.frameSkip = 15 / *cfg.FPS
  107. if c.frameSkip < 1 {
  108. c.frameSkip = 1
  109. }
  110. }
  111. }
  112. }
  113. })
  114. // /ws/audio — WebSocket endpoint for continuous live-listen audio streaming.
  115. // Client connects with query params: freq, bw, mode
  116. // Server sends binary frames of PCM s16le audio at 48kHz.
  117. mux.HandleFunc("/ws/audio", func(w http.ResponseWriter, r *http.Request) {
  118. q := r.URL.Query()
  119. freq, _ := strconv.ParseFloat(q.Get("freq"), 64)
  120. bw, _ := strconv.ParseFloat(q.Get("bw"), 64)
  121. mode := q.Get("mode")
  122. if freq <= 0 {
  123. http.Error(w, "freq required", http.StatusBadRequest)
  124. return
  125. }
  126. if bw <= 0 {
  127. bw = 12000
  128. }
  129. streamer := recMgr.StreamerRef()
  130. if streamer == nil {
  131. http.Error(w, "streamer not available", http.StatusServiceUnavailable)
  132. return
  133. }
  134. // LL-3: Subscribe BEFORE upgrading WebSocket.
  135. // SubscribeAudio now returns AudioInfo and never immediately closes
  136. // the channel — it queues pending listeners instead.
  137. subID, ch, audioInfo, err := streamer.SubscribeAudio(freq, bw, mode)
  138. if err != nil {
  139. http.Error(w, err.Error(), http.StatusServiceUnavailable)
  140. return
  141. }
  142. conn, err := upgrader.Upgrade(w, r, nil)
  143. if err != nil {
  144. streamer.UnsubscribeAudio(subID)
  145. log.Printf("ws/audio upgrade failed: %v", err)
  146. return
  147. }
  148. defer func() {
  149. streamer.UnsubscribeAudio(subID)
  150. _ = conn.Close()
  151. }()
  152. log.Printf("ws/audio: client connected freq=%.1fMHz mode=%s", freq/1e6, mode)
  153. logging.Info("ws", "audio_connect", "freq_mhz", freq/1e6, "mode", mode)
  154. // LL-2: Send actual audio info (channels, sample rate from session)
  155. info := map[string]any{
  156. "type": "audio_info",
  157. "sample_rate": audioInfo.SampleRate,
  158. "channels": audioInfo.Channels,
  159. "format": audioInfo.Format,
  160. "demod": audioInfo.DemodName,
  161. "playback_mode": audioInfo.PlaybackMode,
  162. "stereo_state": audioInfo.StereoState,
  163. "freq": freq,
  164. "mode": mode,
  165. }
  166. if infoBytes, err := json.Marshal(info); err == nil {
  167. _ = conn.WriteMessage(websocket.TextMessage, infoBytes)
  168. }
  169. // Read goroutine (to detect disconnect)
  170. done := make(chan struct{})
  171. go func() {
  172. defer close(done)
  173. for {
  174. _, _, err := conn.ReadMessage()
  175. if err != nil {
  176. return
  177. }
  178. }
  179. }()
  180. ping := time.NewTicker(30 * time.Second)
  181. defer ping.Stop()
  182. for {
  183. select {
  184. case data, ok := <-ch:
  185. if !ok {
  186. log.Printf("ws/audio: stream ended freq=%.1fMHz", freq/1e6)
  187. return
  188. }
  189. if len(data) == 0 {
  190. continue
  191. }
  192. _ = conn.SetWriteDeadline(time.Now().Add(500 * time.Millisecond))
  193. // Tag protocol: first byte is message type
  194. // 0x00 = AudioInfo JSON (send as TextMessage, strip tag)
  195. // 0x01 = PCM audio (send as BinaryMessage, strip tag)
  196. tag := data[0]
  197. payload := data[1:]
  198. msgType := websocket.BinaryMessage
  199. if tag == 0x00 {
  200. msgType = websocket.TextMessage
  201. }
  202. if err := conn.WriteMessage(msgType, payload); err != nil {
  203. log.Printf("ws/audio: write error: %v", err)
  204. logging.Warn("ws", "audio_write_error", "err", err.Error())
  205. return
  206. }
  207. case <-ping.C:
  208. _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
  209. if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
  210. return
  211. }
  212. case <-done:
  213. log.Printf("ws/audio: client disconnected freq=%.1fMHz", freq/1e6)
  214. logging.Info("ws", "audio_disconnect", "freq_mhz", freq/1e6)
  215. return
  216. }
  217. }
  218. })
  219. }