|
- package main
-
- import (
- "encoding/binary"
- "encoding/json"
- "log"
- "math"
- "time"
-
- "sdr-wideband-suite/internal/detector"
- )
-
- func (s *signalSnapshot) set(sig []detector.Signal) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.signals = append([]detector.Signal(nil), sig...)
- }
-
- func (s *signalSnapshot) get() []detector.Signal {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return append([]detector.Signal(nil), s.signals...)
- }
-
- func (g *gpuStatus) set(active bool, err error) {
- g.mu.Lock()
- defer g.mu.Unlock()
- g.Active = active
- if err != nil {
- g.Error = err.Error()
- } else {
- g.Error = ""
- }
- }
-
- func (g *gpuStatus) snapshot() gpuStatus {
- g.mu.RLock()
- defer g.mu.RUnlock()
- return gpuStatus{Available: g.Available, Active: g.Active, Error: g.Error}
- }
-
- func newHub() *hub {
- return &hub{clients: map[*client]struct{}{}, lastLogTs: time.Now()}
- }
-
- func (h *hub) add(c *client) {
- h.mu.Lock()
- defer h.mu.Unlock()
- h.clients[c] = struct{}{}
- log.Printf("ws connected (%d clients)", len(h.clients))
- }
-
- func (h *hub) remove(c *client) {
- c.closeOnce.Do(func() { close(c.done) })
- h.mu.Lock()
- defer h.mu.Unlock()
- delete(h.clients, c)
- log.Printf("ws disconnected (%d clients)", len(h.clients))
- }
-
- func (h *hub) broadcast(frame SpectrumFrame) {
- // Pre-encode JSON for legacy clients (only if needed)
- var jsonBytes []byte
- // Pre-encode binary for binary clients at various decimation levels
- // We cache per unique maxBins value to avoid re-encoding
- type binCacheEntry struct {
- bins int
- data []byte
- }
- var binCache []binCacheEntry
-
- h.mu.Lock()
- clients := make([]*client, 0, len(h.clients))
- for c := range h.clients {
- clients = append(clients, c)
- }
- h.mu.Unlock()
-
- for _, c := range clients {
- // Frame rate limiting
- if c.targetFps > 0 && c.frameSkip > 1 {
- c.frameN++
- if c.frameN%c.frameSkip != 0 {
- continue
- }
- }
-
- if c.binary {
- // Find or create cached binary encoding for this bin count
- bins := c.maxBins
- if bins <= 0 || bins >= len(frame.Spectrum) {
- bins = len(frame.Spectrum)
- }
- var encoded []byte
- for _, entry := range binCache {
- if entry.bins == bins {
- encoded = entry.data
- break
- }
- }
- if encoded == nil {
- encoded = encodeBinaryFrame(frame, bins)
- binCache = append(binCache, binCacheEntry{bins: bins, data: encoded})
- }
- select {
- case c.send <- encoded:
- default:
- h.remove(c)
- }
- } else {
- // JSON path (legacy)
- if jsonBytes == nil {
- var err error
- jsonBytes, err = json.Marshal(frame)
- if err != nil {
- log.Printf("marshal frame: %v", err)
- return
- }
- }
- select {
- case c.send <- jsonBytes:
- default:
- h.remove(c)
- }
- }
- }
-
- h.frameCnt++
- if time.Since(h.lastLogTs) > 2*time.Second {
- h.lastLogTs = time.Now()
- log.Printf("broadcast frames=%d clients=%d", h.frameCnt, len(clients))
- }
- }
-
- // ---------------------------------------------------------------------------
- // Binary spectrum protocol v4
- // ---------------------------------------------------------------------------
- //
- // Hybrid approach: spectrum data as compact binary, signals + debug as JSON.
- //
- // Layout (32-byte header):
- // [0:1] magic: 0x53 0x50 ("SP")
- // [2:3] version: uint16 LE = 4
- // [4:11] timestamp: int64 LE (Unix millis)
- // [12:19] center_hz: float64 LE
- // [20:23] bin_count: uint32 LE (supports FFT up to 4 billion)
- // [24:27] sample_rate_hz: uint32 LE (Hz, max ~4.29 GHz)
- // [28:31] json_offset: uint32 LE (byte offset where JSON starts)
- //
- // [32 .. 32+bins*2-1] spectrum: int16 LE, dB × 100
- // [json_offset ..] JSON: {"signals":[...],"debug":{...}}
-
- const binaryHeaderSize = 32
-
- func encodeBinaryFrame(frame SpectrumFrame, targetBins int) []byte {
- spectrum := frame.Spectrum
- srcBins := len(spectrum)
- if targetBins <= 0 || targetBins > srcBins {
- targetBins = srcBins
- }
-
- var decimated []float64
- if targetBins < srcBins && targetBins > 0 {
- decimated = decimateSpectrum(spectrum, targetBins)
- } else {
- decimated = spectrum
- targetBins = srcBins
- }
-
- // JSON-encode signals + debug (full fidelity)
- jsonPart, _ := json.Marshal(struct {
- Signals []detector.Signal `json:"signals"`
- Debug *SpectrumDebug `json:"debug,omitempty"`
- }{
- Signals: frame.Signals,
- Debug: frame.Debug,
- })
-
- specBytes := targetBins * 2
- jsonOffset := uint32(binaryHeaderSize + specBytes)
- totalSize := int(jsonOffset) + len(jsonPart)
- buf := make([]byte, totalSize)
-
- // Header
- buf[0] = 0x53 // 'S'
- buf[1] = 0x50 // 'P'
- binary.LittleEndian.PutUint16(buf[2:4], 4) // version 4
- binary.LittleEndian.PutUint64(buf[4:12], uint64(frame.Timestamp))
- binary.LittleEndian.PutUint64(buf[12:20], math.Float64bits(frame.CenterHz))
- binary.LittleEndian.PutUint32(buf[20:24], uint32(targetBins))
- binary.LittleEndian.PutUint32(buf[24:28], uint32(frame.SampleHz))
- binary.LittleEndian.PutUint32(buf[28:32], jsonOffset)
-
- // Spectrum (int16, dB × 100)
- off := binaryHeaderSize
- for i := 0; i < targetBins; i++ {
- v := decimated[i] * 100
- if v > 32767 {
- v = 32767
- } else if v < -32767 {
- v = -32767
- }
- binary.LittleEndian.PutUint16(buf[off:off+2], uint16(int16(v)))
- off += 2
- }
-
- // JSON signals + debug
- copy(buf[jsonOffset:], jsonPart)
-
- return buf
- }
-
- // decimateSpectrum reduces bins via peak-hold within each group.
- func decimateSpectrum(spectrum []float64, targetBins int) []float64 {
- src := len(spectrum)
- out := make([]float64, targetBins)
- ratio := float64(src) / float64(targetBins)
- for i := 0; i < targetBins; i++ {
- lo := int(float64(i) * ratio)
- hi := int(float64(i+1) * ratio)
- if hi > src {
- hi = src
- }
- if lo >= hi {
- if lo < src {
- out[i] = spectrum[lo]
- }
- continue
- }
- peak := spectrum[lo]
- for j := lo + 1; j < hi; j++ {
- if spectrum[j] > peak {
- peak = spectrum[j]
- }
- }
- out[i] = peak
- }
- return out
- }
-
|