|
- package platform
-
- import (
- "context"
- "fmt"
- "sync"
- "sync/atomic"
- "time"
-
- "github.com/jan/fm-rds-tx/internal/output"
- )
-
- // -----------------------------------------------------------------------
- // Device capabilities and runtime stats
- // -----------------------------------------------------------------------
-
- // DeviceCaps describes what a hardware device supports.
- type DeviceCaps struct {
- MinSampleRate float64
- MaxSampleRate float64
- SupportedSampleRates []float64
- HasGain bool
- GainMinDB float64
- GainMaxDB float64
- Channels []int
- }
-
- // RuntimeStats exposes live telemetry from the backend.
- type RuntimeStats struct {
- TXEnabled bool `json:"txEnabled"`
- StreamActive bool `json:"streamActive"`
- FramesWritten uint64 `json:"framesWritten"`
- SamplesWritten uint64 `json:"samplesWritten"`
- Underruns uint64 `json:"underruns"`
- Overruns uint64 `json:"overruns"`
- LastError string `json:"lastError,omitempty"`
- LastErrorAt string `json:"lastErrorAt,omitempty"`
- EffectiveRate float64 `json:"effectiveSampleRateHz"`
- }
-
- // -----------------------------------------------------------------------
- // SoapyConfig
- // -----------------------------------------------------------------------
-
- type SoapyConfig struct {
- output.BackendConfig
- Driver string
- Device string
- CenterFreqHz float64
- GainDB float64
- Channels []int
- DeviceArgs map[string]string
- Simulated bool
- SimulationPath string
- }
-
- // -----------------------------------------------------------------------
- // SoapyDriver interface — extended for real HW
- // -----------------------------------------------------------------------
-
- type SoapyDriver interface {
- Name() string
- Configure(ctx context.Context, cfg SoapyConfig) error
- Capabilities(ctx context.Context) (DeviceCaps, error)
- Start(ctx context.Context) error
- Write(ctx context.Context, frame *output.CompositeFrame) (int, error)
- Stop(ctx context.Context) error
- Flush(ctx context.Context) error
- Close(ctx context.Context) error
- Stats() RuntimeStats
- // Tune changes the TX center frequency while streaming. Thread-safe.
- Tune(ctx context.Context, freqHz float64) error
- }
-
- // -----------------------------------------------------------------------
- // SoapyBackend wraps driver and exposes output.Backend
- // -----------------------------------------------------------------------
-
- type SoapyBackend struct {
- mu sync.Mutex
- driver SoapyDriver
- cfg SoapyConfig
- info output.BackendInfo
- }
-
- func NewSoapyBackend(cfg SoapyConfig, driver SoapyDriver) *SoapyBackend {
- if driver == nil {
- driver = NewSimulatedDriver(nil)
- }
- info := output.BackendInfo{
- Name: fmt.Sprintf("soapy/%s", cfg.Driver),
- Description: "SoapySDR-friendly backend",
- Capabilities: output.BackendCapabilities{
- SupportsComposite: true,
- FixedRate: cfg.SampleRateHz > 0,
- MaxSamplesPerWrite: 8192,
- },
- }
- return &SoapyBackend{driver: driver, cfg: cfg, info: info}
- }
-
- func (sb *SoapyBackend) Configure(ctx context.Context, cfg output.BackendConfig) error {
- sb.mu.Lock()
- sb.cfg.BackendConfig = cfg
- sb.mu.Unlock()
- return sb.driver.Configure(ctx, sb.cfg)
- }
-
- func (sb *SoapyBackend) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
- return sb.driver.Write(ctx, frame)
- }
-
- func (sb *SoapyBackend) Flush(ctx context.Context) error {
- return sb.driver.Flush(ctx)
- }
-
- func (sb *SoapyBackend) Close(ctx context.Context) error {
- return sb.driver.Close(ctx)
- }
-
- func (sb *SoapyBackend) Info() output.BackendInfo {
- sb.mu.Lock()
- defer sb.mu.Unlock()
- return sb.info
- }
-
- func (sb *SoapyBackend) Driver() SoapyDriver {
- return sb.driver
- }
-
- // -----------------------------------------------------------------------
- // SimulatedDriver — implements full SoapyDriver interface
- // -----------------------------------------------------------------------
-
- type SimulatedDriver struct {
- mu sync.Mutex
- fallback output.Backend
- cfg SoapyConfig
- started bool
- framesWritten atomic.Uint64
- samplesWritten atomic.Uint64
- lastError string
- lastErrorAt string
- }
-
- func NewSimulatedDriver(writer output.Backend) *SimulatedDriver {
- if writer == nil {
- writer = output.NewDummyBackend("simulated-soapy")
- }
- return &SimulatedDriver{fallback: writer}
- }
-
- func (sd *SimulatedDriver) Name() string {
- return sd.fallback.Info().Name
- }
-
- func (sd *SimulatedDriver) Configure(ctx context.Context, cfg SoapyConfig) error {
- sd.mu.Lock()
- sd.cfg = cfg
- sd.mu.Unlock()
- return sd.fallback.Configure(ctx, cfg.BackendConfig)
- }
-
- func (sd *SimulatedDriver) Capabilities(_ context.Context) (DeviceCaps, error) {
- return DeviceCaps{
- MinSampleRate: 48000,
- MaxSampleRate: 2400000,
- HasGain: true,
- GainMinDB: 0,
- GainMaxDB: 47,
- Channels: []int{0},
- }, nil
- }
-
- func (sd *SimulatedDriver) Start(_ context.Context) error {
- sd.mu.Lock()
- defer sd.mu.Unlock()
- sd.started = true
- return nil
- }
-
- func (sd *SimulatedDriver) Write(ctx context.Context, frame *output.CompositeFrame) (int, error) {
- n, err := sd.fallback.Write(ctx, frame)
- if err != nil {
- sd.mu.Lock()
- sd.lastError = err.Error()
- sd.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
- sd.mu.Unlock()
- }
- if n > 0 {
- sd.framesWritten.Add(1)
- sd.samplesWritten.Add(uint64(n))
- }
- return n, err
- }
-
- func (sd *SimulatedDriver) Stop(_ context.Context) error {
- sd.mu.Lock()
- defer sd.mu.Unlock()
- sd.started = false
- return nil
- }
-
- func (sd *SimulatedDriver) Flush(ctx context.Context) error {
- return sd.fallback.Flush(ctx)
- }
-
- func (sd *SimulatedDriver) Close(ctx context.Context) error {
- return sd.fallback.Close(ctx)
- }
-
- func (sd *SimulatedDriver) Stats() RuntimeStats {
- sd.mu.Lock()
- defer sd.mu.Unlock()
- return RuntimeStats{
- TXEnabled: sd.started,
- StreamActive: sd.started,
- FramesWritten: sd.framesWritten.Load(),
- SamplesWritten: sd.samplesWritten.Load(),
- LastError: sd.lastError,
- LastErrorAt: sd.lastErrorAt,
- EffectiveRate: sd.cfg.SampleRateHz,
- }
- }
-
- func (sd *SimulatedDriver) Tune(_ context.Context, freqHz float64) error {
- sd.mu.Lock()
- sd.cfg.CenterFreqHz = freqHz
- sd.mu.Unlock()
- return nil
- }
|