mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
298 lines
6.2 KiB
Go
298 lines
6.2 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
//go:build !rns_slim
|
|
|
|
package interfaces
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/common"
|
|
"quad4/reticulum-go/pkg/debug"
|
|
"quad4/reticulum-go/pkg/sdr"
|
|
)
|
|
|
|
const (
|
|
sdrDefaultBitrate = 1200
|
|
sdrRXQueue = 16
|
|
sdrTXQueue = 16
|
|
)
|
|
|
|
func init() {
|
|
registerBuiltinFromConfig("SDRInterface", newSDRFromConfig)
|
|
}
|
|
|
|
func newSDRFromConfig(name string, cfg *common.InterfaceConfig, _ *FromConfigContext) (Interface, error) {
|
|
return NewSDRInterface(name, cfg.Enabled, SDROptionsFromConfig(cfg))
|
|
}
|
|
|
|
// SDROptions configures SDRInterface.
|
|
type SDROptions struct {
|
|
Device string
|
|
Serial string
|
|
Address string
|
|
Frequency int64
|
|
SampleRate int
|
|
Bandwidth int
|
|
RXGain float64
|
|
TXGain float64
|
|
Modem string
|
|
Bitrate int64
|
|
MTU int
|
|
// DeviceOverride injects a device (tests).
|
|
DeviceOverride sdr.Device
|
|
}
|
|
|
|
// SDROptionsFromConfig maps InterfaceConfig into SDROptions.
|
|
func SDROptionsFromConfig(cfg *common.InterfaceConfig) SDROptions {
|
|
if cfg == nil {
|
|
return SDROptions{}
|
|
}
|
|
addr := cfg.Address
|
|
if addr == "" && cfg.TargetHost != "" {
|
|
if cfg.TargetPort != 0 {
|
|
addr = net.JoinHostPort(cfg.TargetHost, fmt.Sprintf("%d", cfg.TargetPort))
|
|
} else {
|
|
addr = cfg.TargetHost
|
|
}
|
|
}
|
|
return SDROptions{
|
|
Device: cfg.Device,
|
|
Serial: cfg.SerialNum,
|
|
Address: addr,
|
|
Frequency: cfg.FrequencyHz,
|
|
SampleRate: cfg.SampleRate,
|
|
Bandwidth: cfg.Bandwidth,
|
|
RXGain: cfg.RXGain,
|
|
TXGain: cfg.TXGain,
|
|
Modem: cfg.Modem,
|
|
Bitrate: cfg.Bitrate,
|
|
MTU: cfg.MTU,
|
|
}
|
|
}
|
|
|
|
// SDRInterface sends RNS packets over an SDR device using a Go burst modem.
|
|
//
|
|
// Lab and testing use. Prefer mock or sim for development. Live TX is not
|
|
// authorized by this package. Operators must follow local radio rules. The
|
|
// burst modem is not air-compatible with Modem73 or RNode.
|
|
type SDRInterface struct {
|
|
BaseInterface
|
|
|
|
opts SDROptions
|
|
dev sdr.Device
|
|
modem *sdr.BurstModem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
rxIQ chan []sdr.Complex64
|
|
txIQ chan []sdr.Complex64
|
|
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// NewSDRInterface constructs an SDRInterface. Call Start to open the device.
|
|
// See SDRInterface for the lab and testing disclaimer.
|
|
func NewSDRInterface(name string, enabled bool, opts SDROptions) (*SDRInterface, error) {
|
|
if opts.Device == "" {
|
|
opts.Device = "mock"
|
|
}
|
|
if opts.Modem == "" {
|
|
opts.Modem = "burst"
|
|
}
|
|
if opts.Modem != "burst" {
|
|
return nil, fmt.Errorf("sdr modem %q not supported", opts.Modem)
|
|
}
|
|
if opts.Bitrate <= 0 {
|
|
opts.Bitrate = sdrDefaultBitrate
|
|
}
|
|
si := &SDRInterface{
|
|
BaseInterface: NewBaseInterface(name, common.IFTypeSDR, enabled),
|
|
opts: opts,
|
|
modem: sdr.NewBurstModem(),
|
|
rxIQ: make(chan []sdr.Complex64, sdrRXQueue),
|
|
txIQ: make(chan []sdr.Complex64, sdrTXQueue),
|
|
}
|
|
if opts.MTU > 0 {
|
|
si.MTU = opts.MTU
|
|
}
|
|
si.Bitrate = opts.Bitrate
|
|
return si, nil
|
|
}
|
|
|
|
func (si *SDRInterface) String() string {
|
|
return fmt.Sprintf("SDRInterface[%s/%s]", si.Name, si.opts.Device)
|
|
}
|
|
|
|
func (si *SDRInterface) Start() error {
|
|
si.Mutex.RLock()
|
|
enabled := si.Enabled
|
|
started := si.cancel != nil
|
|
si.Mutex.RUnlock()
|
|
if !enabled || started {
|
|
return nil
|
|
}
|
|
si.Mutex.Lock()
|
|
if si.cancel != nil {
|
|
si.Mutex.Unlock()
|
|
return nil
|
|
}
|
|
si.ctx, si.cancel = context.WithCancel(context.Background())
|
|
si.Mutex.Unlock()
|
|
|
|
dev := si.opts.DeviceOverride
|
|
if dev == nil {
|
|
var err error
|
|
dev, err = sdr.Open(sdr.Config{
|
|
Device: si.opts.Device,
|
|
Serial: si.opts.Serial,
|
|
Address: si.opts.Address,
|
|
Frequency: si.opts.Frequency,
|
|
SampleRate: si.opts.SampleRate,
|
|
Bandwidth: si.opts.Bandwidth,
|
|
RXGain: si.opts.RXGain,
|
|
TXGain: si.opts.TXGain,
|
|
})
|
|
if err != nil {
|
|
si.cancel()
|
|
si.cancel = nil
|
|
return err
|
|
}
|
|
}
|
|
if err := dev.Open(si.ctx); err != nil {
|
|
si.cancel()
|
|
si.cancel = nil
|
|
return err
|
|
}
|
|
_ = dev.Tune(si.opts.Frequency)
|
|
_ = dev.SetSampleRate(si.opts.SampleRate)
|
|
_ = dev.SetBandwidth(si.opts.Bandwidth)
|
|
_ = dev.SetRXGain(si.opts.RXGain)
|
|
_ = dev.SetTXGain(si.opts.TXGain)
|
|
|
|
si.dev = dev
|
|
caps := dev.Caps()
|
|
if caps.RX {
|
|
if err := dev.StartRX(si.ctx, si.rxIQ); err != nil {
|
|
_ = dev.Close()
|
|
si.cancel()
|
|
si.cancel = nil
|
|
return err
|
|
}
|
|
}
|
|
if caps.TX {
|
|
if err := dev.StartTX(si.ctx, si.txIQ); err != nil {
|
|
_ = dev.Close()
|
|
si.cancel()
|
|
si.cancel = nil
|
|
return err
|
|
}
|
|
}
|
|
|
|
si.Mutex.Lock()
|
|
si.Online = true
|
|
si.Detached = false
|
|
si.Mutex.Unlock()
|
|
|
|
si.wg.Go(func() {
|
|
si.rxLoop()
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (si *SDRInterface) Stop() error {
|
|
si.Detach()
|
|
return nil
|
|
}
|
|
|
|
func (si *SDRInterface) Detach() {
|
|
si.Mutex.Lock()
|
|
if si.Detached {
|
|
si.Mutex.Unlock()
|
|
return
|
|
}
|
|
si.Detached = true
|
|
si.Online = false
|
|
cancel := si.cancel
|
|
si.cancel = nil
|
|
dev := si.dev
|
|
si.Mutex.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
if dev != nil {
|
|
_ = dev.Close()
|
|
}
|
|
si.wg.Wait()
|
|
}
|
|
|
|
func (si *SDRInterface) rxLoop() {
|
|
for {
|
|
select {
|
|
case <-si.ctx.Done():
|
|
return
|
|
case block, ok := <-si.rxIQ:
|
|
if !ok {
|
|
return
|
|
}
|
|
if len(block) == 0 {
|
|
continue
|
|
}
|
|
payload, ok := si.modem.Decode(block)
|
|
if !ok {
|
|
continue
|
|
}
|
|
si.ProcessIncoming(payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (si *SDRInterface) ProcessOutgoing(data []byte) error {
|
|
if si.ReceiveOnly {
|
|
return errors.New("interface is receive-only")
|
|
}
|
|
if !si.IsOnline() {
|
|
return errors.New("sdr interface offline")
|
|
}
|
|
samples, err := si.modem.Encode(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case si.txIQ <- samples:
|
|
si.Mutex.Lock()
|
|
si.TxBytes += uint64(len(data))
|
|
si.TxPackets++
|
|
si.Mutex.Unlock()
|
|
return nil
|
|
case <-si.ctx.Done():
|
|
return context.Canceled
|
|
default:
|
|
debug.Log(debug.DebugError, "sdr tx queue full dropping packet", "name", si.Name)
|
|
return errors.New("sdr tx queue full")
|
|
}
|
|
}
|
|
|
|
func (si *SDRInterface) Send(data []byte, _ string) error {
|
|
masked, err := common.ApplyIFACOutbound(si, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return si.ProcessOutgoing(masked)
|
|
}
|
|
|
|
func (si *SDRInterface) GetConn() net.Conn { return nil }
|
|
|
|
func (si *SDRInterface) SendPathRequest([]byte) error { return nil }
|
|
|
|
func (si *SDRInterface) SendLinkPacket([]byte, []byte, time.Time) error { return nil }
|
|
|
|
func (si *SDRInterface) GetBandwidthAvailable() bool {
|
|
return si.IsOnline() && !si.ReceiveOnly
|
|
}
|