package main import ( "fmt" "time" "sdr-wideband-suite/internal/config" "sdr-wideband-suite/internal/sdr" "sdr-wideband-suite/internal/telemetry" ) func (m *sourceManager) Restart(cfg config.Config) error { start := time.Now() m.mu.Lock() defer m.mu.Unlock() old := m.src _ = old.Stop() next, err := m.newSource(cfg) if err != nil { _ = old.Start() m.src = old if m.telemetry != nil { m.telemetry.IncCounter("source.restart.error", 1, nil) m.telemetry.Event("source_restart_failed", "warn", "source restart failed", nil, map[string]any{"error": err.Error()}) } return err } if err := next.Start(); err != nil { _ = next.Stop() _ = old.Start() m.src = old if m.telemetry != nil { m.telemetry.IncCounter("source.restart.error", 1, nil) m.telemetry.Event("source_restart_failed", "warn", "source restart failed", nil, map[string]any{"error": err.Error()}) } return err } m.src = next if m.telemetry != nil { m.telemetry.IncCounter("source.restart.count", 1, nil) m.telemetry.Observe("source.restart.duration_ms", float64(time.Since(start).Milliseconds()), nil) } return nil } func (m *sourceManager) Stats() sdr.SourceStats { m.mu.RLock() defer m.mu.RUnlock() if sp, ok := m.src.(sdr.StatsProvider); ok { return sp.Stats() } return sdr.SourceStats{} } func (m *sourceManager) Flush() { m.mu.RLock() defer m.mu.RUnlock() if fl, ok := m.src.(sdr.Flushable); ok { fl.Flush() } } func newSourceManager(src sdr.Source, newSource func(cfg config.Config) (sdr.Source, error)) *sourceManager { return newSourceManagerWithTelemetry(src, newSource, nil) } func newSourceManagerWithTelemetry(src sdr.Source, newSource func(cfg config.Config) (sdr.Source, error), coll *telemetry.Collector) *sourceManager { return &sourceManager{src: src, newSource: newSource, telemetry: coll} } func (m *sourceManager) Start() error { m.mu.RLock() defer m.mu.RUnlock() return m.src.Start() } func (m *sourceManager) Stop() error { m.mu.RLock() defer m.mu.RUnlock() return m.src.Stop() } func (m *sourceManager) ReadIQ(n int) ([]complex64, error) { waitStart := time.Now() m.mu.RLock() wait := time.Since(waitStart) defer m.mu.RUnlock() if m.telemetry != nil { m.telemetry.Observe("source.lock_wait_ms", float64(wait.Microseconds())/1000.0, telemetry.TagsFromPairs("lock", "read")) if wait > 2*time.Millisecond { m.telemetry.IncCounter("source.lock_contention.count", 1, telemetry.TagsFromPairs("lock", "read")) } } readStart := time.Now() out, err := m.src.ReadIQ(n) if m.telemetry != nil { tags := telemetry.TagsFromPairs("requested", fmt.Sprintf("%d", n)) m.telemetry.Observe("source.read.duration_ms", float64(time.Since(readStart).Microseconds())/1000.0, tags) m.telemetry.SetGauge("source.read.samples", float64(len(out)), nil) if err != nil { m.telemetry.IncCounter("source.read.error", 1, nil) } } return out, err } func (m *sourceManager) ApplyConfig(cfg config.Config) error { m.mu.Lock() defer m.mu.Unlock() if updatable, ok := m.src.(sdr.ConfigurableSource); ok { if err := updatable.UpdateConfig(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.AGC, cfg.TunerBwKHz); err == nil { return nil } } old := m.src _ = old.Stop() next, err := m.newSource(cfg) if err != nil { _ = old.Start() return err } if err := next.Start(); err != nil { _ = next.Stop() _ = old.Start() return err } m.src = next return nil } func pushDSPUpdate(ch chan dspUpdate, update dspUpdate) { select { case ch <- update: default: select { case <-ch: default: } ch <- update } }