mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
У openHPSDR точки и тире формирует кейер в FPGA, и это остаётся путём по умолчанию: тайминги в железе, джиттер PC не влияет вовсе. У AD936x такого кейера нет вовсе, поэтому телеграфа на Pluto не было в принципе — голосовой TXA в CW не запускается, а несущую давать некому. CWKeyer.pas: TCWLocalKeyer рисует манипулированную несущую прямо в поток IQ, мимо WDSP (эталон pihpsdr cw_shape_buffer: в WDSP режим TXA_CWL — ветка SSB, да и PostGen не умеет огибающей). Длительности живут В СЭМПЛАХ, а не в миллисекундах: планировщик ОС влияет только на темп выдачи блоков, а его сглаживает FIFO бэкенда. Умеет текст, иамбик A/B и прямой ключ; TCWKeyPort читает манипулятор с модемных линий COM/USB-serial (DTR/RTS питают ключ, CTS/DSR/DCD/RI — лепестки). Оффлайн-прогон: PARIS занимает ровно 43 точки, все посылки и паузы 1/3/7 dit с точностью до сэмпла. Сессия = одна передача, а не одна посылка: PTT, реле T/R, антенна, OC и аттенюатор Pluto поднимаются один раз и держатся до конца hang — внутри манипуляция чисто цифровая. Предзаполнение FIFO задано железом: TX-поток Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576k) и добивает нулями, чего не хватило. Сайдтон эту латентность не наследует — кольцо огибающей листается, если отставание больше 20 мс. Сдвиг несущей: у zero-IF ноль бейсбенда совпадает с частотой гетеродина, и там же его утечка — манипуляция на нуле дала бы backwave ровно на рабочей частоте. Поэтому несущая идёт на сдвиге, а гетеродин уезжает навстречу через новую единую дверь TXTuneFreqHz (все пуши TX-частоты переведены на неё). Знак (CW_TX_BB_SIGN) замерен на эфире, а не выведен. Развилка источника в UI — один список Keyer: firmware / software (PC) / off; хранится прежней парой флагов, и на железе без кейера выбор «прошивка» вырождается в программный, поэтому Pluto работает из коробки. Новое поле caps HasCWKeyer (раньше это неявно жило в HasHWMic). Попутно: у AD9361 calib_mode стоял manual_tx_quad, то есть драйвер не переигрывал калибровку TX quad (она нулит утечку гетеродина) на сменах TX LO — выставляем auto при подключении. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
380 lines
18 KiB
ObjectPascal
380 lines
18 KiB
ObjectPascal
unit RadioBackend;
|
||
|
||
{
|
||
Абстракция бэкенда железа. Базовый класс TRadioBackend, который реализуют
|
||
конкретные транспорты:
|
||
- THPSDRNetwork (openHPSDR Ethernet protocol) — HPSDRNetwork.pas
|
||
- TPlutoBackend (libiio / AD936x) — PlutoBackend.pas (позже)
|
||
|
||
TRadioController держит FNetwork: TRadioBackend и работает только через этот
|
||
интерфейс. Возможности бэкенда описывает TBackendCaps (контроллер гейтит по
|
||
ним UI и поведение).
|
||
|
||
Здесь же — общая запись устройства (TRadioDevice) и типы колбэков данных,
|
||
чтобы оба бэкенда и контроллер ссылались на один источник правды.
|
||
TDDCIQPacket/THighPriorityStatus/TMicDataPacket берём из HPSDRProtocol —
|
||
это сетевой формат HPSDR, а Pluto-бэкенд синтезирует те же структуры, поэтому
|
||
WDSP-вход остаётся неизменным.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, HPSDRProtocol, Settings;
|
||
|
||
type
|
||
TBackendKind = (bkHPSDR, bkPluto);
|
||
TBackendRateArray = array of Integer; // пресеты sample-rate (общий тип UI)
|
||
|
||
// Описание возможностей бэкенда. Контроллер гейтит UI/поведение по этим полям.
|
||
TBackendCaps = record
|
||
Kind: TBackendKind;
|
||
HasTX: Boolean; // есть передатчик
|
||
HasPA: Boolean; // fwd power / SWR / supply (HP status)
|
||
HasAlex: Boolean; // Alex фильтры / антенный коммутатор
|
||
HasWideband: Boolean; // wideband ADC дисплей
|
||
HasDitherRandom: Boolean; // dither / random на ADC
|
||
HasHWMic: Boolean; // mic over network / HW keyer / sidetone
|
||
HasPLLStatus: Boolean; // PLL lock в статусе
|
||
HasHWGain: Boolean; // manual gain / hw-AGC (Pluto)
|
||
HasRFBandwidth: Boolean; // настройка аналоговой RF-полосы (Pluto)
|
||
HasFullDuplex: Boolean; // одновременный RX+TX (Pluto / QO-100)
|
||
// Панадаптеры (этап 3): сколько всего панов может показать бэкенд
|
||
// (1 = только главный; HPSDR P2 = главный + доп. DDC). Паны N>0 живут
|
||
// на своих аппаратных DDC (SetPanDDC*).
|
||
MaxPans: Integer;
|
||
// False = частоты панов связаны с паном 0 (Pluto+ 2r2t: общий RX LO).
|
||
IndependentPanFreq: Boolean;
|
||
// Число АЦП (SCU): >1 → у панов есть селектор ADC-источника (A1/A2, 3.4).
|
||
// Достоверно после Connect (у HPSDR зависит от BoardType).
|
||
NumADCs: Integer;
|
||
// Переключаемые антенные разъёмы AD936x (RX1/RX2, TX1/TX2) — у плат с двумя
|
||
// парами SMA (LibreSDR/AntSDR/Pluto+). Гейтит блок «Antenna» в настройках;
|
||
// у openHPSDR антеннами рулит Alex, поэтому False. Достоверно после Connect.
|
||
HasAntSwitch: Boolean;
|
||
// PureSignal-предыскажение: openHPSDR P2 платы со свободными DDC0/1 под
|
||
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
|
||
// Достоверно после Connect.
|
||
HasPureSignal: Boolean;
|
||
// Телеграфный кейер в железе: точки/тире формирует прошивка по параметрам из
|
||
// DUC Specific (openHPSDR P2), а манипулятор воткнут в сам трансивер. False
|
||
// (голый AD936x) означает, что телеграф обязан формировать локальный
|
||
// генератор — см. CWKeyer.pas.
|
||
HasCWKeyer: Boolean;
|
||
MinSampleRate: Integer;
|
||
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
|
||
MinFreqHz: Double;
|
||
MaxFreqHz: Double;
|
||
end;
|
||
|
||
// Общая запись найденного/подключённого устройства. Поля HPSDR (MAC/BoardType/
|
||
// NumDDCs…) и Pluto (URI/Serial) сосуществуют; актуальность по Kind.
|
||
TRadioDevice = record
|
||
Kind: TBackendKind;
|
||
IPAddress: string;
|
||
Port: Word;
|
||
MAC: array[0..5] of Byte; // HPSDR; для Pluto — synthetic из Serial
|
||
BoardType: Byte;
|
||
ProtocolVersion: Byte;
|
||
FirmwareVersion: Byte;
|
||
NumDDCs: Byte;
|
||
FreqOrPhase: Byte;
|
||
EndianModes: Byte;
|
||
URI: string; // Pluto: 'usb:1.5.5' / 'ip:192.168.2.1'
|
||
Serial: string; // Pluto serial (hw_serial)
|
||
Model: string; // Pluto hw_model (для отображения)
|
||
InUse: Boolean;
|
||
Valid: Boolean;
|
||
end;
|
||
PRadioDevice = ^TRadioDevice;
|
||
TRadioDeviceArray = array of TRadioDevice;
|
||
|
||
const
|
||
// Признак «значение телеметрии недоступно» (ReadTelemetry). Заведомо вне
|
||
// физического диапазона температур/RSSI.
|
||
TELEMETRY_NONE = -1.0E9;
|
||
|
||
type
|
||
|
||
// Колбэки данных (вызываются из рабочих потоков бэкенда).
|
||
TOnDeviceFound = procedure(const Dev: TRadioDevice) of object;
|
||
TOnDDCIQPacket = procedure(DDCIndex: Integer;
|
||
const Data: TDDCIQPacket) of object;
|
||
TOnMicPacket = procedure(const Data: TMicDataPacket) of object;
|
||
TOnHPStatus = procedure(const Status: THighPriorityStatus) of object;
|
||
TOnWidebandFrame = procedure(ADCIndex: Integer;
|
||
const Samples: array of SmallInt;
|
||
Count: Integer) of object;
|
||
|
||
{ TRadioBackend — абстрактный бэкенд железа }
|
||
TRadioBackend = class
|
||
protected
|
||
// Колбэки — общее хранилище для всех бэкендов (свойства ниже).
|
||
FOnDeviceFound: TOnDeviceFound;
|
||
FOnDDCIQ: TOnDDCIQPacket;
|
||
FOnMic: TOnMicPacket;
|
||
FOnHPStatus: TOnHPStatus;
|
||
FOnWideband: TOnWidebandFrame;
|
||
|
||
function GetConnected: Boolean; virtual; abstract;
|
||
function GetRunning: Boolean; virtual; abstract;
|
||
function GetDevice: TRadioDevice; virtual; abstract;
|
||
function GetLastError: string; virtual; abstract;
|
||
function GetDirectIP: string; virtual;
|
||
procedure SetDirectIP(const V: string); virtual;
|
||
public
|
||
// ---- Возможности ----
|
||
function Caps: TBackendCaps; virtual; abstract;
|
||
|
||
// ---- Жизненный цикл / discovery ----
|
||
function Discover(TimeoutMs: Integer = 3000): TRadioDeviceArray; virtual; abstract;
|
||
function Connect(const Dev: TRadioDevice): Boolean; virtual; abstract;
|
||
procedure Disconnect; virtual; abstract;
|
||
|
||
// ---- Конфигурация потока RX ----
|
||
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
|
||
DitherEnabled: Boolean = True;
|
||
RandomEnabled: Boolean = True); virtual; abstract;
|
||
|
||
// ---- Панадаптеры на доп. аппаратных DDC (этап 3, только HPSDR) ----
|
||
// SetPanDDC включает/выключает и конфигурирует DDC с hw-индексом DDCIdx
|
||
// (rate/ADC → DDC Specific, частота → High Priority). SetPanDDCFreq —
|
||
// только ретюн (дёшево, без реконфига DDC). База — no-op (Caps.MaxPans=1).
|
||
procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double;
|
||
RateKHz: Word; ADCSource: Byte); virtual;
|
||
procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); virtual;
|
||
// Антенные разъёмы AD936x: 0 = RX1/TX1, 1 = RX2/TX2 (см. Caps.HasAntSwitch).
|
||
// У бэкендов без переключателя — no-op.
|
||
procedure SetAntPorts(RxPort, TxPort: Byte); virtual;
|
||
|
||
// ---- PureSignal (openHPSDR P2, платы с DDC0/1 под feedback) ----
|
||
// SetPureSignal — фича вооружена (кнопка PS в UI). SetPureSignalTX — фаза
|
||
// PTT: на True бэкенд включает feedback-DDC0 (+sync DDC1 = TX DAC) на 192к
|
||
// и ставит их частоты = TX; на False возвращает RX-конфигурацию.
|
||
// База — no-op (Pluto и др. без PS-тракта).
|
||
procedure SetPureSignal(Enabled: Boolean); virtual;
|
||
procedure SetPureSignalTX(Active: Boolean); virtual;
|
||
|
||
// ---- Частота / drive / TX-состояние ----
|
||
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
|
||
DriveLevel: Byte = 100); virtual; abstract;
|
||
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
|
||
Transmitting, PAEnabled, AlexEnabled: Boolean;
|
||
Tuning: Boolean = False; TwoTone: Boolean = False); virtual; abstract;
|
||
procedure SendFullHP; virtual; abstract;
|
||
procedure SendPTT(Active: Boolean); virtual; abstract;
|
||
|
||
// ---- TX IQ ----
|
||
procedure SendDUCIQ(const IData, QData: array of Integer); virtual;
|
||
procedure ClearDUCIQQueue; virtual;
|
||
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
|
||
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
|
||
// первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto
|
||
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
|
||
procedure PrimeDUCIQ(Pairs: Integer); virtual;
|
||
|
||
// ---- Опциональные (HPSDR-специфичные): no-op по умолчанию ----
|
||
procedure SendGeneralPacket(const Pkt: TGeneralPacket); virtual;
|
||
procedure ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
|
||
SamplesPerPacket: Integer = 512; SampleBits: Integer = 16;
|
||
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); virtual;
|
||
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); virtual;
|
||
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual;
|
||
procedure SendHighPriority(const Pkt: THighPriorityPacket); virtual;
|
||
// Бит CWX (High Priority байт 5 bit0): «ключ нажат» для программной
|
||
// передачи телеграфа — несущую по нему формирует прошивка. Пакет уходит
|
||
// немедленно, каждый фронт (тайминги держит PC). Pluto — no-op.
|
||
procedure SetCWXKey(Down: Boolean); virtual;
|
||
// Кейер в прошивке вооружён: железо может замкнуть ключ САМО, без нашего
|
||
// PTT. Пока флаг взведён, уровень мощности обязан лежать в каждом HP-кадре
|
||
// (обычно он шлётся только на передаче — а «передачи» с точки зрения софта
|
||
// тут не происходит вовсе). Pluto — no-op.
|
||
procedure SetCWKeyerArmed(Armed: Boolean); virtual;
|
||
procedure SetStepAtten(dB: Byte); virtual;
|
||
// RX-усиление (Pluto/AD936x). ModeIdx: 0=manual, 1=fast_attack, 2=slow_attack,
|
||
// 3=hybrid. GainDb применяется только в manual. HPSDR — no-op.
|
||
procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); virtual;
|
||
// TX-аттенюация выходного тракта (Pluto/AD936x): hardwaregain на voltage0
|
||
// (output), дБ в диапазоне ~ -89.75..0 (0 = макс. мощность). Контроллер ставит
|
||
// максимум на приёме (≈выкл) и значение от drive на передаче. HPSDR — no-op
|
||
// (мощность там через DriveLevel/DUC). Db < 0.
|
||
procedure SetTxAtten(Db: Double); virtual;
|
||
procedure SetAlexConfig(const A: TAlexSettings); virtual;
|
||
// OC Control (Open Collector выходы Penny/Alex) — no-op по умолчанию
|
||
// (Pluto/AD936x не имеет такого выхода).
|
||
procedure SetOCConfig(const O: TOCSettings); virtual;
|
||
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte;
|
||
ASlot: Integer = -1); virtual;
|
||
procedure SendDDCAudio(const LeftRight: array of SmallInt); virtual;
|
||
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer); virtual;
|
||
procedure SetSpeakerAudio(AEnabled: Boolean); virtual;
|
||
|
||
// Аппаратная телеметрия, не приходящая в потоке (опрашивается контроллером).
|
||
// Pluto/AD936x: температура трансивера (TempC) и Zynq SoC/FPGA (SocTempC) в °C,
|
||
// RSSI приёмника (дБ). HPSDR — нет такой (использует HP-status), возвращает False.
|
||
// Недоступные значения = TELEMETRY_NONE.
|
||
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; virtual;
|
||
// Текущее усиление приёмного тракта (дБ), если оно есть в железе и может
|
||
// меняться само: AD936x hardwaregain (в режимах AGC железо крутит его сам).
|
||
// Нужно для абсолютной шкалы уровней — контроллер вычитает его из показаний.
|
||
// HPSDR: усиления нет (есть аттенюатор), возвращает TELEMETRY_NONE.
|
||
function RxGainNowDb: Double; virtual;
|
||
// Частотная калибровка опорника, ppm: «опора выше номинала на N ppm».
|
||
// HPSDR — масштаб частотного слова, AD936x — xo_correction в ad9361-phy.
|
||
procedure SetFreqCalPPM(P: Double); virtual;
|
||
|
||
// ---- Свойства состояния (полиморфные через виртуальные геттеры) ----
|
||
property Connected: Boolean read GetConnected;
|
||
property Running: Boolean read GetRunning;
|
||
property Device: TRadioDevice read GetDevice;
|
||
property LastError: string read GetLastError;
|
||
property DirectIP: string read GetDirectIP write SetDirectIP;
|
||
|
||
// ---- Колбэки данных ----
|
||
property OnDeviceFound: TOnDeviceFound read FOnDeviceFound write FOnDeviceFound;
|
||
property OnDDCIQ: TOnDDCIQPacket read FOnDDCIQ write FOnDDCIQ;
|
||
property OnMicPacket: TOnMicPacket read FOnMic write FOnMic;
|
||
property OnHPStatus: TOnHPStatus read FOnHPStatus write FOnHPStatus;
|
||
property OnWideband: TOnWidebandFrame read FOnWideband write FOnWideband;
|
||
end;
|
||
|
||
implementation
|
||
|
||
{ TRadioBackend — пустые реализации опциональных методов (no-op).
|
||
Конкретные бэкенды переопределяют то, что поддерживают. }
|
||
|
||
function TRadioBackend.GetDirectIP: string;
|
||
begin
|
||
Result := '';
|
||
end;
|
||
|
||
procedure TRadioBackend.SetDirectIP(const V: string);
|
||
begin
|
||
// no-op по умолчанию (unicast discovery — фича HPSDR)
|
||
end;
|
||
|
||
procedure TRadioBackend.SendDUCIQ(const IData, QData: array of Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.ClearDUCIQQueue;
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.PrimeDUCIQ(Pairs: Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendGeneralPacket(const Pkt: TGeneralPacket);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
|
||
SamplesPerPacket: Integer; SampleBits: Integer;
|
||
UpdateRateMS: Integer; PacketsPerFrame: Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendDDCSpecific(const Pkt: TDDCSpecificPacket);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendDUCSpecific(const Pkt: TDUCSpecificPacket);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendHighPriority(const Pkt: THighPriorityPacket);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetCWXKey(Down: Boolean);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetCWKeyerArmed(Armed: Boolean);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetStepAtten(dB: Byte);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetRxGain(ModeIdx: Integer; GainDb: Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetAlexConfig(const A: TAlexSettings);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte;
|
||
ASlot: Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetOCConfig(const O: TOCSettings);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendDDCAudio(const LeftRight: array of SmallInt);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SendSpeakerAudio(const Left, Right: array of Single; Count: Integer);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetSpeakerAudio(AEnabled: Boolean);
|
||
begin
|
||
end;
|
||
|
||
function TRadioBackend.ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean;
|
||
begin
|
||
TempC := TELEMETRY_NONE;
|
||
SocTempC := TELEMETRY_NONE;
|
||
RSSIdB := TELEMETRY_NONE;
|
||
Result := False;
|
||
end;
|
||
|
||
function TRadioBackend.RxGainNowDb: Double;
|
||
begin
|
||
Result := TELEMETRY_NONE;
|
||
end;
|
||
|
||
procedure TRadioBackend.SetFreqCalPPM(P: Double);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetTxAtten(Db: Double);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetPanDDC(DDCIdx: Integer; Enabled: Boolean;
|
||
FreqHz: Double; RateKHz: Word; ADCSource: Byte);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double);
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetAntPorts(RxPort, TxPort: Byte);
|
||
// Бэкенды без переключаемых разъёмов (openHPSDR — там Alex) ничего не делают.
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetPureSignal(Enabled: Boolean);
|
||
// PureSignal есть только у openHPSDR P2 (THPSDRNetwork переопределяет).
|
||
begin
|
||
end;
|
||
|
||
procedure TRadioBackend.SetPureSignalTX(Active: Boolean);
|
||
begin
|
||
end;
|
||
|
||
end.
|