mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(cw): программный кейер — телеграф на Pluto/LibreSDR
У 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>
This commit is contained in:
+994
@@ -0,0 +1,994 @@
|
|||||||
|
unit CWKeyer;
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Локальный (программный) телеграфный генератор + вход манипулятора.
|
||||||
|
//
|
||||||
|
// ЗАЧЕМ. У openHPSDR P2 манипуляцию делает кейер в FPGA: PC отдаёт ему
|
||||||
|
// скорость/вес (DUC Specific байты 5..13), а точки/тире формирует прошивка —
|
||||||
|
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
|
||||||
|
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
|
||||||
|
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
|
||||||
|
// Эталон — pihpsdr: src/iambic.c (автомат) + cw_shape_buffer в transmitter.c
|
||||||
|
// (огибающая прямо в TX-буфере, мимо WDSP).
|
||||||
|
//
|
||||||
|
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
|
||||||
|
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
|
||||||
|
// Поэтому генератор отдаёт готовые IQ-пары прямо в бэкенд: несущая = комплексная
|
||||||
|
// экспонента на OffsetHz с косинусной («приподнятый косинус») огибающей.
|
||||||
|
//
|
||||||
|
// ТАЙМИНГИ ЖИВУТ В СЭМПЛАХ, а не в миллисекундах. Точка на 40 WPM — 30 мс;
|
||||||
|
// отмеряй её Sleep-ами, и планировщик ОС растянул бы её на единицы мс, отчего
|
||||||
|
// знак «плывёт». В сэмплах длительность точна по определению — ошибиться может
|
||||||
|
// только ТЕМП ВЫДАЧИ блоков, а его сглаживает FIFO бэкенда (держим в нём запас
|
||||||
|
// CW_PREFILL_MS).
|
||||||
|
//
|
||||||
|
// СЕССИЯ. Между посылками несущей нет, но реле T/R, LO передатчика и аттенюатор
|
||||||
|
// дёргать на каждую точку нельзя. Поэтому генератор работает сессиями: первое
|
||||||
|
// замыкание поднимает передачу (OnSession(True)), дальше идут элементы, и через
|
||||||
|
// hang после последнего передача снимается. Внутри сессии манипуляция — чисто
|
||||||
|
// цифровая, в огибающей.
|
||||||
|
//
|
||||||
|
// САЙДТОН. Огибающая пишется в кольцо на аудио-rate; аудиотракт её вычитывает
|
||||||
|
// и умножает на свой синус (PullSidetone). Так тон точен ДЛЯ ЛЮБОГО источника —
|
||||||
|
// и для текста, и для иамбика, — в отличие от пути с кейером в прошивке, где
|
||||||
|
// PC знает лишь состояние лепестков.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, SyncObjs, Math, Serial, CWMorse;
|
||||||
|
|
||||||
|
const
|
||||||
|
CW_SIDETONE_RATE = 48000; // rate огибающей сайдтона (аудиотракт PC)
|
||||||
|
CW_IQ_BLOCK = 240; // пар в блоке = ровно DUC IQ пакет openHPSDR
|
||||||
|
// Запас в FIFO бэкенда, он же латентность РЧ. Нижняя планка задана ЖЕЛЕЗОМ:
|
||||||
|
// TX-поток Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576 ksps) и
|
||||||
|
// добивает нулями всё, чего в FIFO не хватило — то есть при меньшем запасе
|
||||||
|
// посылка получила бы дыры. Сайдтон этой латентности не наследует: его отставание
|
||||||
|
// отдельно ограничено CW_ST_MAX_LAG_MS.
|
||||||
|
CW_PREFILL_MS = 60;
|
||||||
|
CW_ST_MAX_LAG_MS = 20; // потолок отставания сайдтона от манипуляции
|
||||||
|
|
||||||
|
type
|
||||||
|
// Режим манипулятора. Прямой ключ = уровень на линии, иамбик = автомат по
|
||||||
|
// двум лепесткам (A — без памяти, B — с памятью задетого элемента).
|
||||||
|
TCWLocalMode = (clmStraight, clmIambicA, clmIambicB);
|
||||||
|
|
||||||
|
// Линии модемного разъёма, которыми читается манипулятор.
|
||||||
|
TCWKeyLine = (cklCTS, cklDSR, cklCD, cklRI);
|
||||||
|
|
||||||
|
// IQ-пары (interleaved I,Q) в конвенции WDSP: РЧ = гетеродин + f.
|
||||||
|
TCWIQEvent = procedure(const Buf: array of Double; Count: Integer) of object;
|
||||||
|
// Начало/конец сессии передачи: PTT, реле T/R, антенна, аттенюатор.
|
||||||
|
TCWSessionEvent = procedure(Active: Boolean) of object;
|
||||||
|
// «Ключ живой» — дёргается на каждом выданном блоке (индикация/выдержка).
|
||||||
|
TCWTickEvent = procedure of object;
|
||||||
|
|
||||||
|
TCWLocalConfig = record
|
||||||
|
Rate: Integer; // sample rate потока IQ (TX rate движка)
|
||||||
|
OffsetHz: Double; // сдвиг несущей от гетеродина (уход от утечки LO)
|
||||||
|
Amplitude: Double; // 0..1 — цифровой уровень несущей
|
||||||
|
WPM: Integer;
|
||||||
|
Weight: Integer; // 33..66 (50 = точка равна паузе)
|
||||||
|
RampMS: Integer; // форма фронта посылки
|
||||||
|
Mode: TCWLocalMode;
|
||||||
|
Reverse: Boolean; // поменять лепестки местами
|
||||||
|
Strict: Boolean; // строгие интервалы (см. RunSession)
|
||||||
|
BreakIn: Boolean; // сессия сама поднимает передачу
|
||||||
|
HangMS: Integer; // сколько держать передачу после последнего элемента
|
||||||
|
RFDelayMS: Integer; // пауза после подъёма передачи до первой посылки
|
||||||
|
Sidetone: Boolean; // писать огибающую в кольцо сайдтона
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ Генератор. Один поток: решает, что передавать, и сам же рисует IQ. }
|
||||||
|
TCWLocalKeyer = class(TThread)
|
||||||
|
private
|
||||||
|
FLock: TCriticalSection;
|
||||||
|
FSTLock: TCriticalSection;
|
||||||
|
FWake: TEvent;
|
||||||
|
// ---- вход (под FLock) --------------------------------------------------
|
||||||
|
FCfg: TCWLocalConfig;
|
||||||
|
FArmed: Boolean;
|
||||||
|
FPending: string; // очередь текста
|
||||||
|
FAbortReq: Boolean;
|
||||||
|
FBusyText: Boolean;
|
||||||
|
FDotIn: Boolean; // лепесток «точка» (сырой, до Reverse)
|
||||||
|
FDashIn: Boolean;
|
||||||
|
FStraightIn:Boolean; // прямой ключ отдельным источником
|
||||||
|
FTouched: Boolean; // касание манипулятора — обрывает текст
|
||||||
|
// ---- рабочая копия (только поток генератора) ---------------------------
|
||||||
|
FRate: Integer;
|
||||||
|
FAmp: Double;
|
||||||
|
FDit: Int64; // длительности в сэмплах
|
||||||
|
FMark: Int64;
|
||||||
|
FGap: Int64;
|
||||||
|
FRamp: Int64;
|
||||||
|
FHang: Int64;
|
||||||
|
FRFDelay: Integer;
|
||||||
|
FMode: TCWLocalMode;
|
||||||
|
FReverse: Boolean;
|
||||||
|
FStrict: Boolean;
|
||||||
|
FSidetoneOn:Boolean;
|
||||||
|
FCosStep: Double; // поворот фазора на сэмпл
|
||||||
|
FSinStep: Double;
|
||||||
|
FPhI: Double; // текущий фазор
|
||||||
|
FPhQ: Double;
|
||||||
|
FPhCnt: Integer; // счётчик до перенормировки
|
||||||
|
FEnvPos: Int64; // позиция в рампе 0..FRamp
|
||||||
|
FBuf: array of Double;
|
||||||
|
FBufCnt: Integer;
|
||||||
|
FEmitted: Int64; // сэмплов с начала сессии (пейсинг)
|
||||||
|
FT0: QWord;
|
||||||
|
FSTAcc: Integer; // делитель rate → CW_SIDETONE_RATE
|
||||||
|
FSTRing: array of Single;
|
||||||
|
FSTHead: Integer;
|
||||||
|
FSTTail: Integer;
|
||||||
|
// ---- разбор текста (только поток) --------------------------------------
|
||||||
|
FText: string;
|
||||||
|
FTextPos: Integer;
|
||||||
|
FCode: string; // код текущего знака ('.' и '-')
|
||||||
|
FCodePos: Integer;
|
||||||
|
FLeadGap: Int64; // пауза ПЕРЕД следующим элементом (межсловная)
|
||||||
|
FTailGap: Int64; // добор паузы ПОСЛЕ элемента (межзнаковая)
|
||||||
|
// ---- иамбик (только поток) ---------------------------------------------
|
||||||
|
FLastDot: Boolean;
|
||||||
|
FDotMem: Boolean;
|
||||||
|
FDashMem: Boolean;
|
||||||
|
FSeenDot: Boolean; // лепесток задет во время элемента (режим B)
|
||||||
|
FSeenDash: Boolean;
|
||||||
|
// ---- события -----------------------------------------------------------
|
||||||
|
FOnIQ: TCWIQEvent;
|
||||||
|
FOnSession: TCWSessionEvent;
|
||||||
|
FOnTick: TCWTickEvent;
|
||||||
|
|
||||||
|
function Snapshot: Boolean; // конфиг → рабочие поля; False = не вооружён
|
||||||
|
function MsToSamples(Ms: Integer): Int64;
|
||||||
|
procedure Flush;
|
||||||
|
procedure Pace;
|
||||||
|
procedure Emit(KeyOn: Boolean; N: Int64; Watch: Boolean);
|
||||||
|
procedure PushSidetone(E: Double);
|
||||||
|
procedure ReadPaddles(out Dot, Dash: Boolean);
|
||||||
|
function WorkPending: Boolean;
|
||||||
|
function TakeTextElement(out IsDot: Boolean): Boolean;
|
||||||
|
function TakeIambicElement(out IsDot: Boolean): Boolean;
|
||||||
|
procedure ResetText;
|
||||||
|
procedure RunSession;
|
||||||
|
protected
|
||||||
|
procedure Execute; override;
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
|
||||||
|
procedure Configure(const C: TCWLocalConfig);
|
||||||
|
// Вооружение: снятие мгновенно обрывает передачу и отпускает ключ.
|
||||||
|
procedure SetArmed(Value: Boolean);
|
||||||
|
function Armed: Boolean;
|
||||||
|
|
||||||
|
procedure SendText(const S: string);
|
||||||
|
procedure AbortText;
|
||||||
|
function Busy: Boolean;
|
||||||
|
|
||||||
|
// Вход манипулятора (поток опроса порта / GUI). Состояния лепестков сырые,
|
||||||
|
// Reverse применяет сам генератор.
|
||||||
|
procedure Paddle(Dot, Dash: Boolean);
|
||||||
|
procedure StraightKey(Down: Boolean);
|
||||||
|
|
||||||
|
// Аудиотракт: вычитать до N отсчётов огибающей (0..1). Возвращает сколько
|
||||||
|
// реально отдано — остальное вызывающий доигрывает спадом.
|
||||||
|
function PullSidetone(var Buf: array of Single; N: Integer): Integer;
|
||||||
|
|
||||||
|
property OnIQ: TCWIQEvent read FOnIQ write FOnIQ;
|
||||||
|
property OnSession: TCWSessionEvent read FOnSession write FOnSession;
|
||||||
|
property OnTick: TCWTickEvent read FOnTick write FOnTick;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TCWKeyPortConfig = record
|
||||||
|
Enabled: Boolean;
|
||||||
|
Port: string;
|
||||||
|
DotLine: TCWKeyLine;
|
||||||
|
DashLine: TCWKeyLine;
|
||||||
|
Invert: Boolean; // ключ замыкает линию в «0» (инверсный интерфейс)
|
||||||
|
PowerDTR: Boolean; // поднять DTR (питание/общий провод ключа)
|
||||||
|
PowerRTS: Boolean;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
|
||||||
|
|
||||||
|
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
|
||||||
|
CTS/DSR/DCD/RI читаются как лепестки. Так же делают Thetis (cwkeyer.cs),
|
||||||
|
hamlib и fldigi — это единственный вход ключа, доступный на любом железе. }
|
||||||
|
TCWKeyPort = class(TThread)
|
||||||
|
private
|
||||||
|
FLock: TCriticalSection;
|
||||||
|
FCfg: TCWKeyPortConfig;
|
||||||
|
FDirty: Boolean; // конфиг сменился — переоткрыть
|
||||||
|
FHandle: TSerialHandle;
|
||||||
|
FLastErr: string;
|
||||||
|
FOnKey: TCWKeyPortEvent;
|
||||||
|
FPrevDot: Boolean;
|
||||||
|
FPrevDash: Boolean;
|
||||||
|
function ReadLine(L: TCWKeyLine): Boolean;
|
||||||
|
procedure ClosePort;
|
||||||
|
function OpenPort(const C: TCWKeyPortConfig): Boolean;
|
||||||
|
procedure SetErr(const S: string);
|
||||||
|
procedure Release;
|
||||||
|
protected
|
||||||
|
procedure Execute; override;
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
procedure Configure(const C: TCWKeyPortConfig);
|
||||||
|
function LastError: string;
|
||||||
|
property OnKey: TCWKeyPortEvent read FOnKey write FOnKey;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
DIT_MS_AT_1WPM = 1200; // стандарт PARIS
|
||||||
|
ST_RING_MS = 250; // глубина кольца сайдтона
|
||||||
|
|
||||||
|
{ ── TCWLocalKeyer ─────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
|
constructor TCWLocalKeyer.Create;
|
||||||
|
begin
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
FSTLock := TCriticalSection.Create;
|
||||||
|
FWake := TEvent.Create(nil, False, False, '');
|
||||||
|
FCfg.Rate := 192000;
|
||||||
|
FCfg.Amplitude := 0.99;
|
||||||
|
FCfg.WPM := 20;
|
||||||
|
FCfg.Weight := 50;
|
||||||
|
FCfg.RampMS := 9;
|
||||||
|
FCfg.Mode := clmIambicB;
|
||||||
|
FCfg.BreakIn := True;
|
||||||
|
FCfg.HangMS := 300;
|
||||||
|
FRate := FCfg.Rate;
|
||||||
|
FCodePos := 1;
|
||||||
|
SetLength(FBuf, CW_IQ_BLOCK * 2);
|
||||||
|
SetLength(FSTRing, (CW_SIDETONE_RATE * ST_RING_MS) div 1000);
|
||||||
|
FreeOnTerminate := False;
|
||||||
|
inherited Create(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TCWLocalKeyer.Destroy;
|
||||||
|
begin
|
||||||
|
Terminate;
|
||||||
|
SetArmed(False);
|
||||||
|
FWake.SetEvent;
|
||||||
|
WaitFor;
|
||||||
|
FWake.Free;
|
||||||
|
FSTLock.Free;
|
||||||
|
FLock.Free;
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Configure(const C: TCWLocalConfig);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FCfg := C;
|
||||||
|
if FCfg.Rate < 8000 then FCfg.Rate := 8000;
|
||||||
|
if FCfg.WPM < 5 then FCfg.WPM := 5;
|
||||||
|
if FCfg.WPM > 60 then FCfg.WPM := 60;
|
||||||
|
if FCfg.Weight < 33 then FCfg.Weight := 33;
|
||||||
|
if FCfg.Weight > 66 then FCfg.Weight := 66;
|
||||||
|
if FCfg.RampMS < 0 then FCfg.RampMS := 0;
|
||||||
|
if FCfg.RampMS > 20 then FCfg.RampMS := 20;
|
||||||
|
if FCfg.HangMS < 0 then FCfg.HangMS := 0;
|
||||||
|
if FCfg.RFDelayMS < 0 then FCfg.RFDelayMS := 0;
|
||||||
|
if FCfg.Amplitude < 0 then FCfg.Amplitude := 0;
|
||||||
|
if FCfg.Amplitude > 1 then FCfg.Amplitude := 1;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.SetArmed(Value: Boolean);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if FArmed = Value then Exit;
|
||||||
|
FArmed := Value;
|
||||||
|
if not Value then
|
||||||
|
begin
|
||||||
|
// Разоружение — это «передавать нельзя» (ушли из CW, зашли в RX-only слот
|
||||||
|
// трансвертера, встали на бэнд с DoNotTx). Всё бросаем немедленно.
|
||||||
|
FPending := '';
|
||||||
|
FAbortReq := True;
|
||||||
|
FDotIn := False;
|
||||||
|
FDashIn := False;
|
||||||
|
FStraightIn := False;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.Armed: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FArmed;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.SendText(const S: string);
|
||||||
|
begin
|
||||||
|
if S = '' then Exit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if not FArmed then Exit;
|
||||||
|
FAbortReq := False;
|
||||||
|
FTouched := False;
|
||||||
|
FPending := FPending + S;
|
||||||
|
FBusyText := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.AbortText;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FPending := '';
|
||||||
|
FAbortReq := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.Busy: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FBusyText or (FPending <> '');
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Paddle(Dot, Dash: Boolean);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if (Dot and not FDotIn) or (Dash and not FDashIn) then FTouched := True;
|
||||||
|
FDotIn := Dot;
|
||||||
|
FDashIn := Dash;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Dot or Dash then FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.StraightKey(Down: Boolean);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if Down and not FStraightIn then FTouched := True;
|
||||||
|
FStraightIn := Down;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Down then FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.PullSidetone(var Buf: array of Single; N: Integer): Integer;
|
||||||
|
var
|
||||||
|
Avail, MaxLag: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if N <= 0 then Exit;
|
||||||
|
FSTLock.Enter;
|
||||||
|
try
|
||||||
|
// Генератор идёт впереди эфира на CW_PREFILL_MS, и весь этот запас оседал бы
|
||||||
|
// в кольце — сайдтон отставал бы от руки на всю латентность РЧ. Лишнее
|
||||||
|
// пролистываем: в начале сессии это тишина (окно RF delay), а дальше темпы
|
||||||
|
// равны и листать уже нечего.
|
||||||
|
Avail := FSTHead - FSTTail;
|
||||||
|
if Avail < 0 then Inc(Avail, Length(FSTRing));
|
||||||
|
MaxLag := N + (CW_SIDETONE_RATE * CW_ST_MAX_LAG_MS) div 1000;
|
||||||
|
if Avail > MaxLag then
|
||||||
|
FSTTail := (FSTTail + (Avail - MaxLag)) mod Length(FSTRing);
|
||||||
|
while (Result < N) and (FSTTail <> FSTHead) do
|
||||||
|
begin
|
||||||
|
Buf[Result] := FSTRing[FSTTail];
|
||||||
|
FSTTail := (FSTTail + 1) mod Length(FSTRing);
|
||||||
|
Inc(Result);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
FSTLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.PushSidetone(E: Double);
|
||||||
|
var
|
||||||
|
NextH: Integer;
|
||||||
|
begin
|
||||||
|
FSTLock.Enter;
|
||||||
|
try
|
||||||
|
NextH := (FSTHead + 1) mod Length(FSTRing);
|
||||||
|
if NextH = FSTTail then
|
||||||
|
// Переполнение: аудиотракт молчит или отстал — держим ХВОСТ, иначе
|
||||||
|
// сайдтон уезжал бы по времени от манипуляции.
|
||||||
|
FSTTail := (FSTTail + 1) mod Length(FSTRing);
|
||||||
|
FSTRing[FSTHead] := E;
|
||||||
|
FSTHead := NextH;
|
||||||
|
finally
|
||||||
|
FSTLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.MsToSamples(Ms: Integer): Int64;
|
||||||
|
begin
|
||||||
|
if Ms <= 0 then Exit(0);
|
||||||
|
Result := (Int64(FRate) * Ms) div 1000;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.Snapshot: Boolean;
|
||||||
|
// Копия конфига в рабочие поля. Зовётся на каждом элементе — скорость/вес можно
|
||||||
|
// крутить прямо во время передачи.
|
||||||
|
var
|
||||||
|
C: TCWLocalConfig;
|
||||||
|
Th: Double;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
C := FCfg;
|
||||||
|
Result := FArmed;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FRate := C.Rate;
|
||||||
|
FAmp := C.Amplitude;
|
||||||
|
FMode := C.Mode;
|
||||||
|
FReverse := C.Reverse;
|
||||||
|
FStrict := C.Strict;
|
||||||
|
FSidetoneOn := C.Sidetone;
|
||||||
|
FRFDelay := C.RFDelayMS;
|
||||||
|
FDit := (Int64(FRate) * DIT_MS_AT_1WPM) div (Int64(C.WPM) * 1000);
|
||||||
|
// Вес: 50 = точка равна паузе. Выше — посылки длиннее за счёт пауз ВНУТРИ
|
||||||
|
// знака; межзнаковые интервалы остаются стандартными (иначе плывёт темп).
|
||||||
|
FMark := (FDit * C.Weight) div 50;
|
||||||
|
FGap := (FDit * (100 - C.Weight)) div 50;
|
||||||
|
if FMark < 1 then FMark := 1;
|
||||||
|
if FGap < 1 then FGap := 1;
|
||||||
|
FRamp := (Int64(FRate) * C.RampMS) div 1000;
|
||||||
|
if FRamp * 2 > FMark then FRamp := FMark div 2; // рампа не длиннее посылки
|
||||||
|
if FRamp < 0 then FRamp := 0;
|
||||||
|
FHang := MsToSamples(C.HangMS);
|
||||||
|
Th := 2.0 * Pi * C.OffsetHz / FRate;
|
||||||
|
FCosStep := Cos(Th);
|
||||||
|
FSinStep := Sin(Th);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Flush;
|
||||||
|
begin
|
||||||
|
if FBufCnt <= 0 then Exit;
|
||||||
|
if Assigned(FOnIQ) then FOnIQ(FBuf, FBufCnt);
|
||||||
|
FBufCnt := 0;
|
||||||
|
if Assigned(FOnTick) then FOnTick;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Pace;
|
||||||
|
// Держим в FIFO бэкенда запас CW_PREFILL_MS: продюсер идёт по часам PC, а
|
||||||
|
// потребитель — по радиоклоку. Больше запас — больше латентность РЧ и сайдтона,
|
||||||
|
// меньше — риск подсоса нулей посреди посылки.
|
||||||
|
var
|
||||||
|
Target, Elapsed: Int64;
|
||||||
|
begin
|
||||||
|
Target := (FEmitted * 1000) div FRate - CW_PREFILL_MS;
|
||||||
|
if Target <= 0 then Exit;
|
||||||
|
repeat
|
||||||
|
if Terminated then Exit;
|
||||||
|
Elapsed := Int64(GetTickCount64 - FT0);
|
||||||
|
if Elapsed >= Target then Exit;
|
||||||
|
Sleep(1);
|
||||||
|
until False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Emit(KeyOn: Boolean; N: Int64; Watch: Boolean);
|
||||||
|
// Рисует N сэмплов при заданном состоянии ключа. Огибающая живёт МЕЖДУ
|
||||||
|
// вызовами (FEnvPos): посылка = Emit(True, mark) поднимает фронт в первые FRamp
|
||||||
|
// сэмплов, пауза = Emit(False, gap) роняет его в свои первые FRamp. Так точка
|
||||||
|
// длится ровно mark по уровню 50%, а щелчка нет ни на одном фронте.
|
||||||
|
var
|
||||||
|
i: Int64;
|
||||||
|
E, A, NI, NQ, Nrm: Double;
|
||||||
|
Dot, Dash: Boolean;
|
||||||
|
begin
|
||||||
|
i := 0;
|
||||||
|
while i < N do
|
||||||
|
begin
|
||||||
|
if Terminated then Exit;
|
||||||
|
if KeyOn then
|
||||||
|
begin
|
||||||
|
if FEnvPos < FRamp then Inc(FEnvPos) else FEnvPos := FRamp;
|
||||||
|
end
|
||||||
|
else if FEnvPos > 0 then
|
||||||
|
Dec(FEnvPos);
|
||||||
|
|
||||||
|
if FRamp > 0 then
|
||||||
|
E := 0.5 * (1.0 - Cos(Pi * FEnvPos / FRamp))
|
||||||
|
else if KeyOn then
|
||||||
|
E := 1.0
|
||||||
|
else
|
||||||
|
E := 0.0;
|
||||||
|
|
||||||
|
A := FAmp * E;
|
||||||
|
if (FSinStep <> 0.0) or (FCosStep <> 1.0) then
|
||||||
|
begin
|
||||||
|
// Поворот фазора вместо Sin/Cos на каждый сэмпл (192к вызовов в секунду).
|
||||||
|
NI := FPhI * FCosStep - FPhQ * FSinStep;
|
||||||
|
NQ := FPhI * FSinStep + FPhQ * FCosStep;
|
||||||
|
FPhI := NI; FPhQ := NQ;
|
||||||
|
Inc(FPhCnt);
|
||||||
|
if FPhCnt >= 1024 then
|
||||||
|
begin
|
||||||
|
FPhCnt := 0;
|
||||||
|
Nrm := Sqrt(FPhI * FPhI + FPhQ * FPhQ);
|
||||||
|
if Nrm > 0 then begin FPhI := FPhI / Nrm; FPhQ := FPhQ / Nrm; end
|
||||||
|
else begin FPhI := 1.0; FPhQ := 0.0; end;
|
||||||
|
end;
|
||||||
|
FBuf[FBufCnt * 2] := A * FPhI;
|
||||||
|
FBuf[FBufCnt * 2 + 1] := A * FPhQ;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
FBuf[FBufCnt * 2] := A; // нулевой сдвиг — несущая прямо на DUC
|
||||||
|
FBuf[FBufCnt * 2 + 1] := 0.0;
|
||||||
|
end;
|
||||||
|
Inc(FBufCnt);
|
||||||
|
|
||||||
|
if FSidetoneOn then
|
||||||
|
begin
|
||||||
|
Inc(FSTAcc, CW_SIDETONE_RATE);
|
||||||
|
if FSTAcc >= FRate then
|
||||||
|
begin
|
||||||
|
Dec(FSTAcc, FRate);
|
||||||
|
PushSidetone(E);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FBufCnt >= CW_IQ_BLOCK then
|
||||||
|
begin
|
||||||
|
Flush;
|
||||||
|
Pace;
|
||||||
|
// Лепестки во время элемента — память режима B (см. RunSession).
|
||||||
|
if Watch then
|
||||||
|
begin
|
||||||
|
ReadPaddles(Dot, Dash);
|
||||||
|
if Dot then FSeenDot := True;
|
||||||
|
if Dash then FSeenDash := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Inc(i);
|
||||||
|
Inc(FEmitted);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.ReadPaddles(out Dot, Dash: Boolean);
|
||||||
|
var
|
||||||
|
D, H: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
D := FDotIn;
|
||||||
|
H := FDashIn;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if FReverse then begin Dot := H; Dash := D; end
|
||||||
|
else begin Dot := D; Dash := H; end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.WorkPending: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FArmed and ((FPending <> '') or FDotIn or FDashIn or FStraightIn);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.ResetText;
|
||||||
|
begin
|
||||||
|
FText := '';
|
||||||
|
FTextPos := 0;
|
||||||
|
FCode := '';
|
||||||
|
FCodePos := 1;
|
||||||
|
FLeadGap := 0;
|
||||||
|
FTailGap := 0;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FBusyText := False;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.TakeTextElement(out IsDot: Boolean): Boolean;
|
||||||
|
// Очередной элемент передаваемого текста. Раскладка пауз та же, что у кейера
|
||||||
|
// прошивки: 1 точка между элементами знака, 3 между знаками, 7 между словами.
|
||||||
|
// Межсловная пауза копится в FLeadGap (отдаётся ПЕРЕД элементом), межзнаковая —
|
||||||
|
// в FTailGap (после).
|
||||||
|
var
|
||||||
|
Ch: Char;
|
||||||
|
Add: string;
|
||||||
|
Drop: Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
IsDot := True;
|
||||||
|
FTailGap := 0;
|
||||||
|
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Drop := FAbortReq or FTouched;
|
||||||
|
if Drop then
|
||||||
|
begin
|
||||||
|
FPending := '';
|
||||||
|
FAbortReq := False;
|
||||||
|
FTouched := False;
|
||||||
|
end;
|
||||||
|
Add := FPending;
|
||||||
|
FPending := '';
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Drop then
|
||||||
|
begin
|
||||||
|
ResetText;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if Add <> '' then FText := FText + Add;
|
||||||
|
|
||||||
|
// Ищем следующий элемент, пропуская пробелы и знаки не из таблицы.
|
||||||
|
while FCodePos > Length(FCode) do
|
||||||
|
begin
|
||||||
|
if FTextPos >= Length(FText) then
|
||||||
|
begin
|
||||||
|
ResetText;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
Inc(FTextPos);
|
||||||
|
Ch := FText[FTextPos];
|
||||||
|
if Ch = ' ' then
|
||||||
|
begin
|
||||||
|
// 3 точки уже отданы после прошлого знака — добираем до 7.
|
||||||
|
Inc(FLeadGap, 4 * FDit);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
FCode := MorseCode(Ch);
|
||||||
|
FCodePos := 1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
IsDot := FCode[FCodePos] = '.';
|
||||||
|
Result := True;
|
||||||
|
Inc(FCodePos);
|
||||||
|
// Последний элемент знака — добираем межзнаковый интервал до 3 точек.
|
||||||
|
if FCodePos > Length(FCode) then FTailGap := 2 * FDit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FBusyText := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
|
||||||
|
// Автомат иамбика (логика pihpsdr src/iambic.c в терминах элементов):
|
||||||
|
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
|
||||||
|
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
|
||||||
|
// следующим; режим A памяти не имеет (в этом и вся разница);
|
||||||
|
// * strict spacing — лепестки во время межэлементной паузы не запоминаются:
|
||||||
|
// ритм задаёт кейер, а не рука.
|
||||||
|
var
|
||||||
|
Dot, Dash: Boolean;
|
||||||
|
begin
|
||||||
|
ReadPaddles(Dot, Dash);
|
||||||
|
Result := True;
|
||||||
|
if Dot and Dash then IsDot := not FLastDot
|
||||||
|
else if Dot then IsDot := True
|
||||||
|
else if Dash then IsDot := False
|
||||||
|
else if FDotMem then IsDot := True
|
||||||
|
else if FDashMem then IsDot := False
|
||||||
|
else Result := False;
|
||||||
|
if Result then
|
||||||
|
begin
|
||||||
|
FDotMem := False;
|
||||||
|
FDashMem := False;
|
||||||
|
FLastDot := IsDot;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.RunSession;
|
||||||
|
// Одна сессия передачи: подъём T/R → элементы → hang → снятие T/R.
|
||||||
|
var
|
||||||
|
Idle: Int64;
|
||||||
|
Chunk: Int64;
|
||||||
|
IsDot: Boolean;
|
||||||
|
Level: Boolean;
|
||||||
|
Got: Boolean;
|
||||||
|
Dot, Dash: Boolean;
|
||||||
|
Br: Boolean;
|
||||||
|
begin
|
||||||
|
if not Snapshot then Exit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Br := FCfg.BreakIn;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FEnvPos := 0; FEmitted := 0; FBufCnt := 0; FSTAcc := 0;
|
||||||
|
FPhI := 1.0; FPhQ := 0.0; FPhCnt := 0;
|
||||||
|
FDotMem := False; FDashMem := False; FSeenDot := False; FSeenDash := False;
|
||||||
|
FSTLock.Enter;
|
||||||
|
try
|
||||||
|
FSTHead := 0; FSTTail := 0; // старую огибающую в кольце не доигрываем
|
||||||
|
finally
|
||||||
|
FSTLock.Leave;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if Br and Assigned(FOnSession) then FOnSession(True);
|
||||||
|
FT0 := GetTickCount64;
|
||||||
|
try
|
||||||
|
// Окно на реле T/R, LO передатчика и аттенюатор: без него первая точка
|
||||||
|
// ушла бы в ещё приёмную обвязку.
|
||||||
|
Emit(False, MsToSamples(FRFDelay), False);
|
||||||
|
Idle := 0;
|
||||||
|
while not Terminated do
|
||||||
|
begin
|
||||||
|
if not Snapshot then Break; // разоружили посреди передачи
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Level := FStraightIn;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if FMode = clmStraight then
|
||||||
|
begin
|
||||||
|
ReadPaddles(Dot, Dash);
|
||||||
|
Level := Level or Dot or Dash; // прямой ключ на любом лепестке
|
||||||
|
end;
|
||||||
|
// Прямой ключ — следуем за уровнем: длительность задаёт рука, наше дело
|
||||||
|
// форма фронта. Квант 2 мс = задержка реакции на отпускание.
|
||||||
|
if Level then
|
||||||
|
begin
|
||||||
|
Emit(True, MsToSamples(2), False);
|
||||||
|
Idle := 0;
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Текст имеет приоритет над манипулятором; касание лепестка его обрывает
|
||||||
|
// (это ловит сам TakeTextElement по FTouched).
|
||||||
|
Got := TakeTextElement(IsDot);
|
||||||
|
if (not Got) and (FMode <> clmStraight) then Got := TakeIambicElement(IsDot);
|
||||||
|
if Got then
|
||||||
|
begin
|
||||||
|
if FLeadGap > 0 then
|
||||||
|
begin
|
||||||
|
Emit(False, FLeadGap, not FStrict);
|
||||||
|
FLeadGap := 0;
|
||||||
|
end;
|
||||||
|
FSeenDot := False; FSeenDash := False;
|
||||||
|
if IsDot then Emit(True, FMark, True)
|
||||||
|
else Emit(True, 3 * FMark, True);
|
||||||
|
Emit(False, FGap, not FStrict);
|
||||||
|
if FTailGap > 0 then
|
||||||
|
begin
|
||||||
|
Emit(False, FTailGap, not FStrict);
|
||||||
|
FTailGap := 0;
|
||||||
|
end;
|
||||||
|
if FMode = clmIambicB then
|
||||||
|
begin
|
||||||
|
if IsDot and FSeenDash then FDashMem := True;
|
||||||
|
if (not IsDot) and FSeenDot then FDotMem := True;
|
||||||
|
end;
|
||||||
|
Idle := 0;
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Передавать нечего: держим передачу hang, потом закрываем сессию.
|
||||||
|
Chunk := MsToSamples(4);
|
||||||
|
if Chunk < 1 then Chunk := 1;
|
||||||
|
Emit(False, Chunk, False);
|
||||||
|
Inc(Idle, Chunk);
|
||||||
|
if Idle >= FHang then Break;
|
||||||
|
end;
|
||||||
|
// Добить спад огибающей — сессия не должна обрываться щелчком.
|
||||||
|
if FEnvPos > 0 then Emit(False, FRamp + 1, False);
|
||||||
|
// ★Хвост тишины на всю глубину предзаполнения: конец сессии сбрасывает
|
||||||
|
// очередь IQ в бэкенде, и без него в мусор ушёл бы ЕЩЁ НЕ СЫГРАННЫЙ спад
|
||||||
|
// последней посылки (то есть щелчок в эфир). При hang > 0 хвост и так
|
||||||
|
// тишина, но полагаться на настройку тут нельзя.
|
||||||
|
Emit(False, MsToSamples(CW_PREFILL_MS + 5), False);
|
||||||
|
Flush;
|
||||||
|
finally
|
||||||
|
if Br and Assigned(FOnSession) then FOnSession(False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWLocalKeyer.Execute;
|
||||||
|
begin
|
||||||
|
while not Terminated do
|
||||||
|
begin
|
||||||
|
if not WorkPending then
|
||||||
|
begin
|
||||||
|
FWake.WaitFor(100);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
RunSession;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ── TCWKeyPort ────────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
|
constructor TCWKeyPort.Create;
|
||||||
|
begin
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
FHandle := 0;
|
||||||
|
FreeOnTerminate := False;
|
||||||
|
inherited Create(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TCWKeyPort.Destroy;
|
||||||
|
begin
|
||||||
|
Terminate;
|
||||||
|
WaitFor;
|
||||||
|
ClosePort;
|
||||||
|
FLock.Free;
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWKeyPort.Configure(const C: TCWKeyPortConfig);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if (FCfg.Enabled = C.Enabled) and (FCfg.Port = C.Port)
|
||||||
|
and (FCfg.DotLine = C.DotLine) and (FCfg.DashLine = C.DashLine)
|
||||||
|
and (FCfg.Invert = C.Invert) and (FCfg.PowerDTR = C.PowerDTR)
|
||||||
|
and (FCfg.PowerRTS = C.PowerRTS) then Exit;
|
||||||
|
FCfg := C;
|
||||||
|
FDirty := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWKeyPort.LastError: string;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FLastErr;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWKeyPort.SetErr(const S: string);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FLastErr := S;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWKeyPort.Release;
|
||||||
|
// Отпустить ключ: порт закрылся/выключили — генератор не должен остаться с
|
||||||
|
// «зажатым» лепестком, иначе несущая повиснет в эфире.
|
||||||
|
begin
|
||||||
|
if (FPrevDot or FPrevDash) and Assigned(FOnKey) then FOnKey(False, False);
|
||||||
|
FPrevDot := False;
|
||||||
|
FPrevDash := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWKeyPort.ClosePort;
|
||||||
|
begin
|
||||||
|
if FHandle > 0 then
|
||||||
|
begin
|
||||||
|
SerSetDTR(FHandle, False);
|
||||||
|
SerSetRTS(FHandle, False);
|
||||||
|
SerClose(FHandle);
|
||||||
|
end;
|
||||||
|
FHandle := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWKeyPort.OpenPort(const C: TCWKeyPortConfig): Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if C.Port = '' then Exit;
|
||||||
|
FHandle := SerOpen(C.Port);
|
||||||
|
if FHandle <= 0 then
|
||||||
|
begin
|
||||||
|
FHandle := 0;
|
||||||
|
SetErr('cannot open ' + C.Port);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Скорость/формат манипулятору безразличны (данные не идут), но порт должен
|
||||||
|
// быть настроен — иначе часть драйверов не отдаёт модемные линии.
|
||||||
|
SerSetParams(FHandle, 9600, 8, NoneParity, 1, []);
|
||||||
|
SerSetDTR(FHandle, C.PowerDTR);
|
||||||
|
SerSetRTS(FHandle, C.PowerRTS);
|
||||||
|
SetErr('');
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWKeyPort.ReadLine(L: TCWKeyLine): Boolean;
|
||||||
|
begin
|
||||||
|
case L of
|
||||||
|
cklDSR: Result := SerGetDSR(FHandle);
|
||||||
|
cklCD: Result := SerGetCD(FHandle);
|
||||||
|
cklRI: Result := SerGetRI(FHandle);
|
||||||
|
else
|
||||||
|
Result := SerGetCTS(FHandle);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWKeyPort.Execute;
|
||||||
|
var
|
||||||
|
C: TCWKeyPortConfig;
|
||||||
|
Dot, Dash: Boolean;
|
||||||
|
Retry: QWord;
|
||||||
|
NeedReopen: Boolean;
|
||||||
|
begin
|
||||||
|
Retry := 0;
|
||||||
|
while not Terminated do
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
C := FCfg;
|
||||||
|
NeedReopen := FDirty;
|
||||||
|
FDirty := False;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if NeedReopen then
|
||||||
|
begin
|
||||||
|
Release;
|
||||||
|
ClosePort;
|
||||||
|
Retry := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not C.Enabled then
|
||||||
|
begin
|
||||||
|
if FHandle > 0 then begin Release; ClosePort; end;
|
||||||
|
Sleep(100);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if FHandle <= 0 then
|
||||||
|
begin
|
||||||
|
// Переоткрытие раз в 2 с: адаптер могли воткнуть уже после старта.
|
||||||
|
if GetTickCount64 < Retry then begin Sleep(100); Continue; end;
|
||||||
|
Retry := GetTickCount64 + 2000;
|
||||||
|
if not OpenPort(C) then begin Sleep(100); Continue; end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Dot := ReadLine(C.DotLine);
|
||||||
|
Dash := ReadLine(C.DashLine);
|
||||||
|
if C.Invert then begin Dot := not Dot; Dash := not Dash; end;
|
||||||
|
if (Dot <> FPrevDot) or (Dash <> FPrevDash) then
|
||||||
|
begin
|
||||||
|
FPrevDot := Dot;
|
||||||
|
FPrevDash := Dash;
|
||||||
|
if Assigned(FOnKey) then FOnKey(Dot, Dash);
|
||||||
|
end;
|
||||||
|
Sleep(1);
|
||||||
|
end;
|
||||||
|
Release;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -731,6 +731,8 @@ begin
|
|||||||
Result.NumADCs := 1;
|
Result.NumADCs := 1;
|
||||||
// PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2.
|
// PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2.
|
||||||
Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10];
|
Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10];
|
||||||
|
// Кейер в прошивке есть у всех плат P2 (DUC Specific байты 5..13).
|
||||||
|
Result.HasCWKeyer := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
constructor THPSDRNetwork.Create;
|
constructor THPSDRNetwork.Create;
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ var
|
|||||||
out val: clonglong): cint; cdecl;
|
out val: clonglong): cint; cdecl;
|
||||||
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
||||||
val: clonglong): cint; cdecl;
|
val: clonglong): cint; cdecl;
|
||||||
|
// 'calib_mode' — режим калибровок AD936x (строка): 'auto' = драйвер сам
|
||||||
|
// переигрывает TX quad (утечка LO + квадратура) на каждой смене TX LO.
|
||||||
|
iio_device_attr_write: function(dev: Piio_device; attr: PAnsiChar;
|
||||||
|
src: PAnsiChar): ptrint; cdecl;
|
||||||
|
|
||||||
// ---- Debug-атрибуты устройства (debugfs) ----
|
// ---- Debug-атрибуты устройства (debugfs) ----
|
||||||
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
|
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
|
||||||
@@ -195,6 +199,7 @@ begin
|
|||||||
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
|
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
|
||||||
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
|
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
|
||||||
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
|
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
|
||||||
|
iio_device_attr_write := Sym('iio_device_attr_write');
|
||||||
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
|
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
|
||||||
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
|
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
|
||||||
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
|
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
|
||||||
|
|||||||
+12
-6
@@ -3688,6 +3688,9 @@ begin
|
|||||||
FController.ServiceBeaconLock;
|
FController.ServiceBeaconLock;
|
||||||
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
|
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
|
||||||
FController.ServicePlutoTelemetry;
|
FController.ServicePlutoTelemetry;
|
||||||
|
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет — гнать индикацию
|
||||||
|
// и отпускать реле T/R после выдержки больше некому).
|
||||||
|
FController.ServiceCWKeyed;
|
||||||
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
|
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
|
||||||
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
|
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
|
||||||
FController.PureSignalTick;
|
FController.PureSignalTick;
|
||||||
@@ -4601,7 +4604,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -4623,7 +4626,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -5069,7 +5072,7 @@ begin
|
|||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
||||||
XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True,
|
FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True,
|
||||||
FController.FTuning, FController.FTwoTone);
|
FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -8040,7 +8043,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||||||
@@ -8075,7 +8078,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||||||
@@ -8121,7 +8124,7 @@ begin
|
|||||||
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -9137,6 +9140,9 @@ begin
|
|||||||
PushTXProfilesToSettings;
|
PushTXProfilesToSettings;
|
||||||
SF.LoadTXSettings(FController.FTXSettings,
|
SF.LoadTXSettings(FController.FTXSettings,
|
||||||
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
||||||
|
// Телеграф: есть ли кейер в железе. У AD936x его нет — там телеграф может
|
||||||
|
// быть только программным, и настройки прошивки в группе гасятся.
|
||||||
|
SF.SetCWBackend(FController.BackendCaps.HasCWKeyer);
|
||||||
SF.LoadCWSettings(FController.CWSettings);
|
SF.LoadCWSettings(FController.CWSettings);
|
||||||
SF.LoadAlexSettings(FController.FAlexSettings,
|
SF.LoadAlexSettings(FController.FAlexSettings,
|
||||||
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ begin
|
|||||||
Result.HasWideband := False;
|
Result.HasWideband := False;
|
||||||
Result.HasDitherRandom := False;
|
Result.HasDitherRandom := False;
|
||||||
Result.HasHWMic := False;
|
Result.HasHWMic := False;
|
||||||
|
Result.HasCWKeyer := False; // голый AD936x — телеграф формирует PC
|
||||||
Result.HasPLLStatus := False;
|
Result.HasPLLStatus := False;
|
||||||
Result.HasHWGain := True; // manual gain / hw-AGC
|
Result.HasHWGain := True; // manual gain / hw-AGC
|
||||||
Result.HasRFBandwidth := True;
|
Result.HasRFBandwidth := True;
|
||||||
@@ -634,6 +635,15 @@ begin
|
|||||||
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
|
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
|
||||||
|
|
||||||
FConnected := True;
|
FConnected := True;
|
||||||
|
// ★Калибровки — в автомат. AD936x нулит утечку гетеродина и квадратурный
|
||||||
|
// разбаланс передатчика калибровкой TX quad, и в режиме 'auto' драйвер сам
|
||||||
|
// прогоняет её на каждой смене TX LO. Найдено на живом Pluto: там стояло
|
||||||
|
// 'manual_tx_quad', то есть калибровка НЕ переигрывалась вовсе — утечка LO
|
||||||
|
// (в телеграфе она видна отдельной несущей в стороне от рабочей частоты)
|
||||||
|
// оставалась такой, какой её оставила прошлая программа. Состояние живёт в
|
||||||
|
// чипе между сессиями, поэтому выставляем явно.
|
||||||
|
if Assigned(iio_device_attr_write) then
|
||||||
|
iio_device_attr_write(FPhy, 'calib_mode', 'auto');
|
||||||
// Приводим железо в известное состояние: валидный rate + вход + усиление.
|
// Приводим железо в известное состояние: валидный rate + вход + усиление.
|
||||||
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
||||||
ApplySampleRate(FSampleRate);
|
ApplySampleRate(FSampleRate);
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ type
|
|||||||
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
|
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
|
||||||
// Достоверно после Connect.
|
// Достоверно после Connect.
|
||||||
HasPureSignal: Boolean;
|
HasPureSignal: Boolean;
|
||||||
|
// Телеграфный кейер в железе: точки/тире формирует прошивка по параметрам из
|
||||||
|
// DUC Specific (openHPSDR P2), а манипулятор воткнут в сам трансивер. False
|
||||||
|
// (голый AD936x) означает, что телеграф обязан формировать локальный
|
||||||
|
// генератор — см. CWKeyer.pas.
|
||||||
|
HasCWKeyer: Boolean;
|
||||||
MinSampleRate: Integer;
|
MinSampleRate: Integer;
|
||||||
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
|
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
|
||||||
MinFreqHz: Double;
|
MinFreqHz: Double;
|
||||||
|
|||||||
+374
-46
@@ -50,7 +50,7 @@ uses
|
|||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer;
|
||||||
|
|
||||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||||
@@ -457,6 +457,19 @@ type
|
|||||||
FCWSTEnv: Double; // огибающая 0..1, сглаживает щелчки (DSP-поток)
|
FCWSTEnv: Double; // огибающая 0..1, сглаживает щелчки (DSP-поток)
|
||||||
FCWSTBufL: array of Single; // буферы подмешивания (только DSP-поток)
|
FCWSTBufL: array of Single; // буферы подмешивания (только DSP-поток)
|
||||||
FCWSTBufR: array of Single;
|
FCWSTBufR: array of Single;
|
||||||
|
FCWSTEnvBuf: array of Single; // огибающая от локального кейера (DSP-поток)
|
||||||
|
// ---- Локальный кейер (CWKeyer.pas) --------------------------------------
|
||||||
|
// Генератор несущей с огибающей прямо в поток IQ. Обязателен там, где кейера
|
||||||
|
// в железе нет (Pluto/LibreSDR), и опционален у openHPSDR. Оба объекта
|
||||||
|
// создаются лениво — кто телеграфом не пользуется, лишних нитей не получает.
|
||||||
|
FCWLocal: TCWLocalKeyer;
|
||||||
|
FCWKeyPort: TCWKeyPort;
|
||||||
|
// Сессия локального кейера подняла передачу сама (break-in): по этому флагу
|
||||||
|
// её снимаем и не путаем с ручным MOX.
|
||||||
|
FCWLocalTX: Boolean;
|
||||||
|
// Локальный кейер вооружён — считается в SyncCWKeyer, читается пейсингом
|
||||||
|
// частоты DUC (CWCarrierOffsetHz) и аттенюатором Pluto.
|
||||||
|
FCWLocalArmed: Boolean;
|
||||||
FAlexSettings: TAlexSettings;
|
FAlexSettings: TAlexSettings;
|
||||||
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
|
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
|
||||||
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
||||||
@@ -578,6 +591,11 @@ type
|
|||||||
// uplink-LO не подвержен дрейфу LNB).
|
// uplink-LO не подвержен дрейфу LNB).
|
||||||
function XvtrTranslateTX(VisibleHz: Double): Double;
|
function XvtrTranslateTX(VisibleHz: Double): Double;
|
||||||
function ActiveTXFreqHz: Double;
|
function ActiveTXFreqHz: Double;
|
||||||
|
// TXTuneFreqHz — ЕДИНАЯ дверь «какую частоту передачи класть в железо»:
|
||||||
|
// ActiveTXFreqHz через трансвертер, минус сдвиг несущей локального кейера
|
||||||
|
// (CWCarrierOffsetHz). Прямых XvtrTranslateTX(ActiveTXFreqHz) в пушах
|
||||||
|
// состояния быть не должно — иначе телеграф уедет от VFO ровно на сдвиг.
|
||||||
|
function TXTuneFreqHz: Double;
|
||||||
// Режим передающего тракта: слайс-источник TX (FTxSliceId) либо главный FMode.
|
// Режим передающего тракта: слайс-источник TX (FTxSliceId) либо главный FMode.
|
||||||
function ActiveTXMode: Integer;
|
function ActiveTXMode: Integer;
|
||||||
// Диапазон для калибровки драйва/антенны TX (банд TX-слайса при кросс-банде).
|
// Диапазон для калибровки драйва/антенны TX (банд TX-слайса при кросс-банде).
|
||||||
@@ -616,10 +634,22 @@ type
|
|||||||
// (FCurrentBand при входе в слот не меняется).
|
// (FCurrentBand при входе в слот не меняется).
|
||||||
function XvtrSlotActive: Boolean;
|
function XvtrSlotActive: Boolean;
|
||||||
function TXProhibited: Boolean; // DoNotTx-бэнд / RX-only XVTR
|
function TXProhibited: Boolean; // DoNotTx-бэнд / RX-only XVTR
|
||||||
function CWTXActive: Boolean; // передачей владеет FPGA-кейер
|
function CWTXActive: Boolean; // манипуляцией владеет не MOX
|
||||||
|
function CWLocalSource: Boolean; // манипуляцию рисует PC
|
||||||
|
function CWFWSource: Boolean; // манипуляцию рисует прошивка
|
||||||
function CWOptionsByte: Byte; // DUC Specific байт 5
|
function CWOptionsByte: Byte; // DUC Specific байт 5
|
||||||
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
||||||
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
||||||
|
// Сдвиг несущей манипуляции от гетеродина (уход от утечки LO у zero-IF).
|
||||||
|
// На него же уезжает DUC/LO, чтобы РЧ встала ровно на VFO — см. TXTuneFreqHz.
|
||||||
|
function CWCarrierOffsetHz: Double;
|
||||||
|
// ---- Локальный кейер ----------------------------------------------------
|
||||||
|
procedure EnsureCWLocal; // ленивое создание генератора
|
||||||
|
procedure PushCWLocalConfig; // настройки → генератор/порт
|
||||||
|
procedure CWLocalIQ(const Buf: array of Double; Count: Integer);
|
||||||
|
procedure CWLocalSession(Active: Boolean); // T/R под сессию манипуляции
|
||||||
|
procedure CWLocalTick; // «ключ живой» (индикация)
|
||||||
|
procedure CWKeyPortEvent(Dot, Dash: Boolean); // лепестки с COM-порта
|
||||||
// Подмешать программный сайдтон в блок RX-аудио (DSP-поток). Возвращает
|
// Подмешать программный сайдтон в блок RX-аудио (DSP-поток). Возвращает
|
||||||
// False, если подмешивать нечего — тогда вызывающий отдаёт исходный блок.
|
// False, если подмешивать нечего — тогда вызывающий отдаёт исходный блок.
|
||||||
function MixSidetone(const Left, Right: array of Single;
|
function MixSidetone(const Left, Right: array of Single;
|
||||||
@@ -964,6 +994,9 @@ type
|
|||||||
// В телеграфе FTransmitting не поднимается: передачей владеет кейер прошивки,
|
// В телеграфе FTransmitting не поднимается: передачей владеет кейер прошивки,
|
||||||
// и единственный признак эфира — HPS_PTT в статусе.
|
// и единственный признак эфира — HPS_PTT в статусе.
|
||||||
function RadioKeyed: Boolean;
|
function RadioKeyed: Boolean;
|
||||||
|
// Фронты RadioKeyed → индикация. Зовётся из разбора HP-статуса (openHPSDR) и
|
||||||
|
// с UI-таймера: у Pluto статуса нет вовсе, а телеграф там локальный.
|
||||||
|
procedure ServiceCWKeyed;
|
||||||
function CWSettings: TCWSettings;
|
function CWSettings: TCWSettings;
|
||||||
procedure SetCWSettings(const C: TCWSettings);
|
procedure SetCWSettings(const C: TCWSettings);
|
||||||
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
||||||
@@ -1298,7 +1331,15 @@ end;
|
|||||||
|
|
||||||
destructor TRadioController.Destroy;
|
destructor TRadioController.Destroy;
|
||||||
begin
|
begin
|
||||||
// Поток передачи телеграфа гасим ПЕРВЫМ: его колбэк ключа лезет в бэкенд.
|
// Потоки телеграфа гасим ПЕРВЫМИ: их колбэки лезут в бэкенд и в движок.
|
||||||
|
// Порядок — порт ключа → генератор: иначе лепесток успел бы разбудить уже
|
||||||
|
// разрушаемый генератор.
|
||||||
|
if Assigned(FCWKeyPort) then FreeAndNil(FCWKeyPort);
|
||||||
|
if Assigned(FCWLocal) then
|
||||||
|
begin
|
||||||
|
FCWLocal.SetArmed(False);
|
||||||
|
FreeAndNil(FCWLocal);
|
||||||
|
end;
|
||||||
if Assigned(FCWSender) then
|
if Assigned(FCWSender) then
|
||||||
begin
|
begin
|
||||||
FCWSender.AbortSending;
|
FCWSender.AbortSending;
|
||||||
@@ -1334,7 +1375,7 @@ begin
|
|||||||
// Состояние передачи для ЖЕЛЕЗА — RadioKeyed, а не FTransmitting: в
|
// Состояние передачи для ЖЕЛЕЗА — RadioKeyed, а не FTransmitting: в
|
||||||
// телеграфе ключ замыкает прошивка, и реле T/R с антенной и OC обязаны
|
// телеграфе ключ замыкает прошивка, и реле T/R с антенной и OC обязаны
|
||||||
// ехать за ней. PTT в кадре этим не поднимается (он живёт в SendPTT).
|
// ехать за ней. PTT в кадре этим не поднимается (он живёт в SendPTT).
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||||||
FDriveLevel, RadioKeyed, True, True, FTuning, FTwoTone);
|
FDriveLevel, RadioKeyed, True, True, FTuning, FTwoTone);
|
||||||
FNetwork.SendFullHP;
|
FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -1604,28 +1645,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
FHWPTTActive := FHPpHWPTT;
|
FHWPTTActive := FHPpHWPTT;
|
||||||
if FHPpHWPTT then FCWKeyedAt := GetTickCount64;
|
if FHPpHWPTT then FCWKeyedAt := GetTickCount64;
|
||||||
// UI про телеграфную передачу иначе не узнает вовсе: SetMOX здесь не
|
ServiceCWKeyed;
|
||||||
// вызывается, а значит и rfTransmitting никто не шлёт. Фронт RadioKeyed
|
|
||||||
// (с выдержкой) — единственный сигнал «покрасить бейджи и полосы».
|
|
||||||
if FCWKeyedShown <> RadioKeyed then
|
|
||||||
begin
|
|
||||||
FCWKeyedShown := RadioKeyed;
|
|
||||||
// ★Железная обвязка обязана идти за фактическим ключом: реле T/R (Alex
|
|
||||||
// bit 27), TX-антенна, bypass/Ext1 и выходы OC (ими ключуется усилитель)
|
|
||||||
// считаются в бэкенде от состояния передачи. Без этого пути прошивка
|
|
||||||
// гнала бы несущую, пока Alex стоит в ПРИЁМНОЙ конфигурации.
|
|
||||||
PushNetworkState;
|
|
||||||
Changed(rfTransmitting);
|
|
||||||
end;
|
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Ушли из телеграфа/сняли вооружение — вернуть индикацию в исходное.
|
ServiceCWKeyed; // ушли из телеграфа — вернуть индикацию в исходное
|
||||||
if FCWKeyedShown then
|
|
||||||
begin
|
|
||||||
FCWKeyedShown := False;
|
|
||||||
PushNetworkState;
|
|
||||||
Changed(rfTransmitting);
|
|
||||||
end;
|
|
||||||
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
|
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
|
||||||
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
|
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
|
||||||
// его включил именно HWPTT.
|
// его включил именно HWPTT.
|
||||||
@@ -3080,6 +3103,26 @@ begin
|
|||||||
Result := Result + FDSPEngine.CWLOOffset(ActiveTXMode);
|
Result := Result + FDSPEngine.CWLOOffset(ActiveTXMode);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRadioController.TXTuneFreqHz: Double;
|
||||||
|
// Единая дверь «какую частоту передачи класть в железо». Кроме трансвертера
|
||||||
|
// учитывает сдвиг несущей локального кейера: манипуляция идёт не на нуле
|
||||||
|
// бейсбенда, а на CWCarrierOffsetHz, значит гетеродин обязан уехать НАВСТРЕЧУ,
|
||||||
|
// иначе телеграф уйдёт в эфир мимо VFO ровно на этот сдвиг.
|
||||||
|
// ★Сдвиг применяется, пока кейер ВООРУЖЁН, а не только на передаче: железо
|
||||||
|
// может замкнуть ключ в любой момент (тот же урок, что был с частотой DUC).
|
||||||
|
//
|
||||||
|
// ★★ЗНАК — ЗАМЕРЕН НА ЭФИРЕ (Pluto, 2026-08-10), а не выведен: положительная
|
||||||
|
// частота бейсбенда в нашей конвенции выходит в эфир НИЖЕ гетеродина
|
||||||
|
// (РЧ = LO + CW_TX_BB_SIGN·f). Первая версия считала наоборот, и несущая вставала
|
||||||
|
// на 2×сдвига НИЖЕ маркера. Отсюда гетеродин уезжает ВВЕРХ на величину сдвига.
|
||||||
|
// Если когда-нибудь понадобится ненулевой сдвиг у openHPSDR — проверить там
|
||||||
|
// отдельно: тракт другой (цифровой DUC вместо zero-IF), и знак может отличаться.
|
||||||
|
const
|
||||||
|
CW_TX_BB_SIGN = -1;
|
||||||
|
begin
|
||||||
|
Result := XvtrTranslateTX(ActiveTXFreqHz) - CW_TX_BB_SIGN * CWCarrierOffsetHz;
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.ActiveTXMode: Integer;
|
function TRadioController.ActiveTXMode: Integer;
|
||||||
var S: TCtrlSlice;
|
var S: TCtrlSlice;
|
||||||
begin
|
begin
|
||||||
@@ -4498,8 +4541,13 @@ const
|
|||||||
begin
|
begin
|
||||||
if (FNetwork = nil) or not IsPluto then Exit;
|
if (FNetwork = nil) or not IsPluto then Exit;
|
||||||
// На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл).
|
// На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл).
|
||||||
if FTransmitting or FTuning then FNetwork.SetTxAtten(PlutoTxAttForDrive)
|
// ★В телеграфе FTransmitting не поднимается вовсе (передачей владеет кейер),
|
||||||
else FNetwork.SetTxAtten(TX_OFF);
|
// поэтому спрашиваем и сессию локального генератора — иначе манипуляция шла бы
|
||||||
|
// в закрытый аттенюатор, то есть в никуда.
|
||||||
|
if FTransmitting or FTuning or FCWLocalTX then
|
||||||
|
FNetwork.SetTxAtten(PlutoTxAttForDrive)
|
||||||
|
else
|
||||||
|
FNetwork.SetTxAtten(TX_OFF);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetPlutoTxMaxAtt(Db: Double);
|
procedure TRadioController.SetPlutoTxMaxAtt(Db: Double);
|
||||||
@@ -5134,15 +5182,64 @@ begin
|
|||||||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly);
|
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.CWTXActive: Boolean;
|
function TRadioController.CWFWSource: Boolean;
|
||||||
// «Телеграфом управляет железо»: режим ИСТОЧНИКА передачи — CW и кейер в
|
// Манипуляцию рисует ПРОШИВКА (кейер в FPGA, openHPSDR P2). Путь по умолчанию:
|
||||||
// прошивке. В этом состоянии голосовой TXA не запускается вовсе (эталон
|
// тайминги в железе, джиттер PC не влияет вовсе.
|
||||||
// Thetis, console.cs:29570), а точки/тире формирует FPGA.
|
|
||||||
begin
|
begin
|
||||||
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FCWSettings.FWKeyer
|
Result := BackendCaps.HasCWKeyer and FCWSettings.FWKeyer
|
||||||
|
and (not FCWSettings.LocalKeyer);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.CWLocalSource: Boolean;
|
||||||
|
// Манипуляцию рисует САМ EWSDR (CWKeyer.pas). Выбирается списком в настройках, а
|
||||||
|
// на железе без кейера в прошивке (Pluto/LibreSDR — голый AD936x) выбор
|
||||||
|
// «прошивка» вырождается сюда: другого пути там просто нет.
|
||||||
|
// ★Оба источника False = кейер выключен намеренно (третий пункт списка): тогда
|
||||||
|
// телеграф идёт только через MOX, и ключевать некому.
|
||||||
|
begin
|
||||||
|
Result := FCWSettings.LocalKeyer
|
||||||
|
or ((not BackendCaps.HasCWKeyer) and FCWSettings.FWKeyer);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.CWTXActive: Boolean;
|
||||||
|
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
|
||||||
|
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
|
||||||
|
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570),
|
||||||
|
// а несущую даёт кейер / бит CWX / тон TUN.
|
||||||
|
begin
|
||||||
|
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||||||
|
and (CWFWSource or CWLocalSource)
|
||||||
and (not TXProhibited); // где передавать нельзя — кейер разоружён
|
and (not TXProhibited); // где передавать нельзя — кейер разоружён
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRadioController.CWCarrierOffsetHz: Double;
|
||||||
|
// Сдвиг несущей манипуляции от гетеродина — и в генераторе, и в частоте DUC/LO.
|
||||||
|
// ★У zero-IF (AD936x) ноль бейсбенда = частота гетеродина, и ТАМ ЖЕ сидит его
|
||||||
|
// утечка. Держи несущую на нуле — между посылками в эфир пойдёт слабый backwave
|
||||||
|
// РОВНО на рабочей частоте (самое неприятное, что бывает в телеграфе). Поэтому
|
||||||
|
// у Pluto манипуляция идёт на сдвиге, а LO уезжает навстречу (знак — в
|
||||||
|
// TXTuneFreqHz, он замерен на эфире). У openHPSDR DUC цифровой, утекать
|
||||||
|
// нечему — сдвиг 0.
|
||||||
|
const
|
||||||
|
CW_AUTO_OFFSET_HZ = 12000;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if not FCWLocalArmed then Exit;
|
||||||
|
// TUN/2TON идут через TXA (несущую даёт SSB-модулятор из тона), их частота
|
||||||
|
// уже посчитана в ActiveTXFreqHz — сдвигать нечего.
|
||||||
|
if FTuning or FTwoTone then Exit;
|
||||||
|
// −1 = «без сдвига» ЯВНО (несущая прямо на гетеродине). Нужно не только для
|
||||||
|
// отладки: сдвиг разносит TX LO и RX LO на единицы-десятки килогерц, а два
|
||||||
|
// fractional-N синтезатора в одном кристалле на близких частотах тянут друг
|
||||||
|
// друга и дают симметричные боковые вокруг несущей. Что хуже — утечка LO на
|
||||||
|
// рабочей частоте или эти боковые, решает оператор на своём железе.
|
||||||
|
if FCWSettings.CarrierOffsetHz < 0 then Exit;
|
||||||
|
if FCWSettings.CarrierOffsetHz > 0 then
|
||||||
|
Result := FCWSettings.CarrierOffsetHz
|
||||||
|
else if not BackendCaps.HasCWKeyer then
|
||||||
|
Result := CW_AUTO_OFFSET_HZ;
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.CWOptionsByte: Byte;
|
function TRadioController.CWOptionsByte: Byte;
|
||||||
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
||||||
// объяснение, почему iambic-бит выставляется и для Mode B).
|
// объяснение, почему iambic-бит выставляется и для Mode B).
|
||||||
@@ -5150,7 +5247,10 @@ function TRadioController.CWOptionsByte: Byte;
|
|||||||
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
if not CWTXActive then Exit;
|
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
|
||||||
|
// взял на себя локальный генератор: иначе прошивка ключевала бы ПАРАЛЛЕЛЬНО
|
||||||
|
// с ним (и поднимала бы PTT на замыкание своего разъёма).
|
||||||
|
if not (CWTXActive and CWFWSource) then Exit;
|
||||||
Result := Result or CW_MODE;
|
Result := Result or CW_MODE;
|
||||||
if FCWSettings.ReverseKeys then Result := Result or CW_REVERSE_KEYS;
|
if FCWSettings.ReverseKeys then Result := Result or CW_REVERSE_KEYS;
|
||||||
case FCWSettings.KeyerMode of
|
case FCWSettings.KeyerMode of
|
||||||
@@ -5573,7 +5673,7 @@ begin
|
|||||||
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.ClearDUCIQQueue;
|
FNetwork.ClearDUCIQQueue;
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||||||
FDriveLevel, True, True, True, FTuning, FTwoTone);
|
FDriveLevel, True, True, True, FTuning, FTwoTone);
|
||||||
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
||||||
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
||||||
@@ -5666,13 +5766,16 @@ begin
|
|||||||
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
||||||
// PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC).
|
// PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC).
|
||||||
FNetwork.SetPureSignalTX(False);
|
FNetwork.SetPureSignalTX(False);
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||||||
FDriveLevel, False, True, True, FTuning, FTwoTone);
|
FDriveLevel, False, True, True, FTuning, FTwoTone);
|
||||||
FNetwork.ClearDUCIQQueue;
|
FNetwork.ClearDUCIQQueue;
|
||||||
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
|
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
|
||||||
end;
|
end;
|
||||||
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
|
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
|
||||||
ApplyActiveVolume; // T/R: переключаем громкость RX ↔ TX-монитор (если monitoring)
|
ApplyActiveVolume; // T/R: переключаем громкость RX ↔ TX-монитор (если monitoring)
|
||||||
|
// При выключенном break-in локальный кейер вооружён только вместе с MOX
|
||||||
|
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
|
||||||
|
SyncCWKeyer;
|
||||||
Changed(rfTransmitting);
|
Changed(rfTransmitting);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -5712,7 +5815,7 @@ begin
|
|||||||
ApplyWDSPDrive(FDrivePercent);
|
ApplyWDSPDrive(FDrivePercent);
|
||||||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||||||
FDriveLevel, FTransmitting, True, True, FTuning, FTwoTone);
|
FDriveLevel, FTransmitting, True, True, FTuning, FTwoTone);
|
||||||
FNetwork.SendFullHP;
|
FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -5747,6 +5850,30 @@ begin
|
|||||||
Result := (GetTickCount64 - FCWKeyedAt) < Hold;
|
Result := (GetTickCount64 - FCWKeyedAt) < Hold;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ServiceCWKeyed;
|
||||||
|
// UI про телеграфную передачу иначе не узнает вовсе: SetMOX в телеграфе не
|
||||||
|
// вызывается, а значит и rfTransmitting никто не шлёт. Фронт RadioKeyed (с
|
||||||
|
// выдержкой) — единственный сигнал «покрасить бейджи и полосы».
|
||||||
|
// Зовётся из разбора HP-статуса (openHPSDR, быстро) и с UI-таймера — у Pluto
|
||||||
|
// статуса нет вообще, а телеграф там локальный.
|
||||||
|
begin
|
||||||
|
// Вооружение кейера обязано быть САМОВОССТАНАВЛИВАЮЩИМСЯ: у openHPSDR это
|
||||||
|
// делает разбор HP-статуса, а у Pluto статуса нет — значит здесь. Разоружить
|
||||||
|
// может что угодно (бэнд, слот трансвертера, выход из TUN), перечислять такие
|
||||||
|
// точки поимённо — гарантия однажды пропустить очередную.
|
||||||
|
SyncCWKeyer;
|
||||||
|
if FCWKeyedShown = RadioKeyed then Exit;
|
||||||
|
FCWKeyedShown := RadioKeyed;
|
||||||
|
// ★Железная обвязка обязана идти за фактическим ключом: реле T/R (Alex
|
||||||
|
// bit 27), TX-антенна, bypass/Ext1 и выходы OC (ими ключуется усилитель)
|
||||||
|
// считаются в бэкенде от состояния передачи. Без этого пути прошивка гнала бы
|
||||||
|
// несущую, пока Alex стоит в ПРИЁМНОЙ конфигурации. У локального кейера то же
|
||||||
|
// делает сессия (CWLocalSession) — синхронно, до первой посылки; здесь остаётся
|
||||||
|
// хвост выдержки, когда реле пора отпустить.
|
||||||
|
PushNetworkState;
|
||||||
|
Changed(rfTransmitting);
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.CWSettings: TCWSettings;
|
function TRadioController.CWSettings: TCWSettings;
|
||||||
begin
|
begin
|
||||||
Result := FCWSettings;
|
Result := FCWSettings;
|
||||||
@@ -5771,7 +5898,10 @@ begin
|
|||||||
if Assigned(FCWSender) then
|
if Assigned(FCWSender) then
|
||||||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||||||
SendDUCSpecificFromSettings;
|
SendDUCSpecificFromSettings;
|
||||||
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
|
// Локальный кейер: параметры + вооружение (галку «кейер в программе», как и
|
||||||
|
// запрет передачи, разбирает SyncCWKeyer — одна дверь на всё).
|
||||||
|
PushCWLocalConfig;
|
||||||
|
SyncCWKeyer;
|
||||||
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
|
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
|
||||||
// если крутят pitch прямо на настройке — обновляем живьём.
|
// если крутят pitch прямо на настройке — обновляем живьём.
|
||||||
if FTuning and FWDSPReady and Assigned(FDSPEngine)
|
if FTuning and FWDSPReady and Assigned(FDSPEngine)
|
||||||
@@ -5788,6 +5918,8 @@ procedure TRadioController.SyncCWKeyer;
|
|||||||
// Байт 5 зависит от режима ИСТОЧНИКА передачи, поэтому каждая смена режима или
|
// Байт 5 зависит от режима ИСТОЧНИКА передачи, поэтому каждая смена режима или
|
||||||
// TX-слайса обязана его перепослать: вне телеграфа он должен стать нулевым,
|
// TX-слайса обязана его перепослать: вне телеграфа он должен стать нулевым,
|
||||||
// иначе прошивка поднимет PTT на замыкание ключа посреди SSB.
|
// иначе прошивка поднимет PTT на замыкание ключа посреди SSB.
|
||||||
|
var
|
||||||
|
WasArmed: Boolean;
|
||||||
begin
|
begin
|
||||||
// Дёшево и идемпотентно: пара сравнений плюс ранний выход внутри
|
// Дёшево и идемпотентно: пара сравнений плюс ранний выход внутри
|
||||||
// SetCWKeyerArmed. Поэтому зовётся не только на смене режима/источника, но и
|
// SetCWKeyerArmed. Поэтому зовётся не только на смене режима/источника, но и
|
||||||
@@ -5798,11 +5930,167 @@ begin
|
|||||||
if (not (ActiveTXMode in [MODE_CWL, MODE_CWU]))
|
if (not (ActiveTXMode in [MODE_CWL, MODE_CWU]))
|
||||||
and Assigned(FCWSender) and FCWSender.Busy then CWXAbort;
|
and Assigned(FCWSender) and FCWSender.Busy then CWXAbort;
|
||||||
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
|
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
|
||||||
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче.
|
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче. Локальный
|
||||||
|
// генератор — та же история: FTransmitting в телеграфе не поднимается, а
|
||||||
|
// несущая идёт.
|
||||||
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
|
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
|
||||||
|
|
||||||
|
// ---- Локальный кейер ----------------------------------------------------
|
||||||
|
// Вооружение самовосстанавливающееся, как и у прошивки: SyncCWKeyer зовётся
|
||||||
|
// из разбора статуса и со всех смен режима/источника/бэнда.
|
||||||
|
WasArmed := FCWLocalArmed;
|
||||||
|
// Без break-in передачей владеет оператор: ключ имеет право звучать только
|
||||||
|
// когда MOX уже поднят — иначе несущая ушла бы в приёмную обвязку.
|
||||||
|
// ★TUN и 2TON исключены намеренно: там несущую даёт TXA, и в DUC-очередь уже
|
||||||
|
// пишет поток WDSP. Двух продюсеров одной очереди быть не должно.
|
||||||
|
FCWLocalArmed := CWTXActive and CWLocalSource
|
||||||
|
and FDevConnected and FRunning
|
||||||
|
and (FCWSettings.BreakIn or FTransmitting)
|
||||||
|
and (not FTuning) and (not FTwoTone);
|
||||||
|
if FCWLocalArmed and (not Assigned(FCWLocal)) then EnsureCWLocal;
|
||||||
|
if Assigned(FCWLocal) then
|
||||||
|
begin
|
||||||
|
if FCWLocalArmed then PushCWLocalConfig;
|
||||||
|
FCWLocal.SetArmed(FCWLocalArmed);
|
||||||
|
end;
|
||||||
|
if Assigned(FCWKeyPort) then
|
||||||
|
if not FCWLocalArmed then CWKeyPortEvent(False, False);
|
||||||
|
// Частота DUC/LO зависит от вооружения (сдвиг несущей) — на фронте
|
||||||
|
// перекладываем железо, иначе манипуляция уехала бы от VFO на сдвиг.
|
||||||
|
if WasArmed <> FCWLocalArmed then PushNetworkState;
|
||||||
|
|
||||||
if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings;
|
if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.EnsureCWLocal;
|
||||||
|
// Ленивое создание: у тех, кто телеграфом не пользуется, лишних нитей нет.
|
||||||
|
begin
|
||||||
|
if not Assigned(FCWLocal) then
|
||||||
|
begin
|
||||||
|
FCWLocal := TCWLocalKeyer.Create;
|
||||||
|
FCWLocal.OnIQ := CWLocalIQ;
|
||||||
|
FCWLocal.OnSession := CWLocalSession;
|
||||||
|
FCWLocal.OnTick := CWLocalTick;
|
||||||
|
end;
|
||||||
|
if not Assigned(FCWKeyPort) then
|
||||||
|
begin
|
||||||
|
FCWKeyPort := TCWKeyPort.Create;
|
||||||
|
FCWKeyPort.OnKey := CWKeyPortEvent;
|
||||||
|
end;
|
||||||
|
PushCWLocalConfig;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.PushCWLocalConfig;
|
||||||
|
// Настройки → генератор и порт манипулятора. Дёшево и идемпотентно: зовётся из
|
||||||
|
// SyncCWKeyer (то есть с каждой смены режима/бэнда) и из SetCWSettings.
|
||||||
|
var
|
||||||
|
C: TCWLocalConfig;
|
||||||
|
P: TCWKeyPortConfig;
|
||||||
|
begin
|
||||||
|
if not Assigned(FCWLocal) then Exit;
|
||||||
|
FillChar(C, SizeOf(C), 0);
|
||||||
|
if FWDSPReady and Assigned(FDSPEngine) then C.Rate := FDSPEngine.TXSampleRate
|
||||||
|
else C.Rate := TX_SAMPLE_RATE;
|
||||||
|
C.OffsetHz := CWCarrierOffsetHz;
|
||||||
|
// Уровень несущей. У Pluto мощность задаёт аттенюатор, цифровой уровень
|
||||||
|
// держим на максимуме; у openHPSDR цифровой drive работает вместе с
|
||||||
|
// байтом 345 — так же, как WDSP-драйв голосового тракта (ApplyWDSPDrive).
|
||||||
|
if IsPluto then C.Amplitude := 0.99
|
||||||
|
else C.Amplitude := 0.99 * EnsureRange(FDrivePercent, 0, 100) / 100.0;
|
||||||
|
C.WPM := FCWSettings.Speed;
|
||||||
|
C.Weight := FCWSettings.Weight;
|
||||||
|
C.RampMS := FCWSettings.RampMS;
|
||||||
|
case FCWSettings.KeyerMode of
|
||||||
|
1: C.Mode := clmIambicA;
|
||||||
|
2: C.Mode := clmIambicB;
|
||||||
|
else C.Mode := clmStraight;
|
||||||
|
end;
|
||||||
|
C.Reverse := FCWSettings.ReverseKeys;
|
||||||
|
C.Strict := FCWSettings.StrictSpacing;
|
||||||
|
C.BreakIn := FCWSettings.BreakIn;
|
||||||
|
C.HangMS := FCWSettings.HangTimeMS;
|
||||||
|
// Реле T/R, LO передатчика (Pluto гасит его на приёме) и аттенюатор успевают
|
||||||
|
// встать за это окно. Ноль у прошивки означает «железо знает само», у нас
|
||||||
|
// ждать некому — держим минимум.
|
||||||
|
C.RFDelayMS := Max(FCWSettings.RFDelayMS, 10);
|
||||||
|
C.Sidetone := FCWSettings.SidetoneSW and (FCWSettings.SidetoneSWLevel > 0);
|
||||||
|
FCWLocal.Configure(C);
|
||||||
|
|
||||||
|
if Assigned(FCWKeyPort) then
|
||||||
|
begin
|
||||||
|
FillChar(P, SizeOf(P), 0);
|
||||||
|
P.Enabled := FCWSettings.KeyPortEnabled and (FCWSettings.KeyPort <> '');
|
||||||
|
P.Port := FCWSettings.KeyPort;
|
||||||
|
P.DotLine := TCWKeyLine(EnsureRange(FCWSettings.KeyDotLine, 0, 3));
|
||||||
|
P.DashLine := TCWKeyLine(EnsureRange(FCWSettings.KeyDashLine, 0, 3));
|
||||||
|
P.Invert := FCWSettings.KeyInvert;
|
||||||
|
P.PowerDTR := FCWSettings.KeyPowerDTR;
|
||||||
|
P.PowerRTS := FCWSettings.KeyPowerRTS;
|
||||||
|
FCWKeyPort.Configure(P);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWLocalIQ(const Buf: array of Double; Count: Integer);
|
||||||
|
// Поток генератора. Тот же маршрут, что у WDSP TXA (OnTXIQ): пары копятся в
|
||||||
|
// DUC-пакеты и уходят в бэкенд. Голосовой тракт в телеграфе стоит, так что
|
||||||
|
// продюсер тут ровно один.
|
||||||
|
begin
|
||||||
|
OnTXIQ(Buf, Count);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWLocalSession(Active: Boolean);
|
||||||
|
// Поток генератора. Сессия = одна передача (не одна посылка!): поднимаем PTT,
|
||||||
|
// реле T/R с антенной и OC, аттенюатор Pluto — и держим до конца hang.
|
||||||
|
// ★FTransmitting НЕ трогаем: в телеграфе передающий тракт WDSP не запускается,
|
||||||
|
// а вся индикация идёт через RadioKeyed (см. ApplyHPStatus/ServiceCWKeyed).
|
||||||
|
begin
|
||||||
|
if Active then FCWKeyedAt := GetTickCount64;
|
||||||
|
FCWLocalTX := Active;
|
||||||
|
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
|
begin
|
||||||
|
if Active then
|
||||||
|
begin
|
||||||
|
PushNetworkState; // RadioKeyed=True: Alex bit27, TX-антенна, OC
|
||||||
|
FNetwork.SendPTT(True); // у Pluto здесь же поднимается LO передатчика
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// ★Если оператор ДЕРЖИТ MOX (ручной T/R поверх break-in), снимать передачу
|
||||||
|
// не наше дело — иначе конец посылки выбрасывал бы его из эфира.
|
||||||
|
if not FTransmitting then
|
||||||
|
begin
|
||||||
|
FNetwork.SendPTT(False);
|
||||||
|
FNetwork.ClearDUCIQQueue;
|
||||||
|
end;
|
||||||
|
FCWKeyedAt := GetTickCount64; // выдержка RadioKeyed идёт от КОНЦА сессии
|
||||||
|
PushNetworkState;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
ApplyPlutoTxAtten;
|
||||||
|
// ★Индикацию отсюда НЕ дёргаем: Invoke у GUI — это TThread.Synchronize, то
|
||||||
|
// есть блокировка потока генератора до кадра UI. Мало того что она вклинилась
|
||||||
|
// бы в путь к эфиру, так ещё и давала бы классический клинч на выходе из
|
||||||
|
// программы (UI ждёт WaitFor генератора, генератор — UI). Фронты снимет
|
||||||
|
// ServiceCWKeyed с таймера хозяина: для бейджа задержка в кадр незаметна,
|
||||||
|
// а выдержка RadioKeyed всё равно не меньше 500 мс.
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWLocalTick;
|
||||||
|
// Поток генератора, на каждом блоке (1.25 мс): держим «железо в эфире» живым,
|
||||||
|
// иначе выдержка RadioKeyed истекла бы посреди длинного сообщения.
|
||||||
|
begin
|
||||||
|
FCWKeyedAt := GetTickCount64;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWKeyPortEvent(Dot, Dash: Boolean);
|
||||||
|
// Поток опроса порта. Касание манипулятора обрывает программную передачу —
|
||||||
|
// оператор всегда может вклиниться (то же правило, что и у кейера прошивки).
|
||||||
|
begin
|
||||||
|
if not Assigned(FCWLocal) then Exit;
|
||||||
|
if (Dot or Dash) and FCWLocal.Busy then FCWLocal.AbortText;
|
||||||
|
FCWLocal.Paddle(Dot, Dash);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.CWXKeyEvent(Down: Boolean);
|
procedure TRadioController.CWXKeyEvent(Down: Boolean);
|
||||||
// Поток передачи, НЕ GUI. Обе операции потокобезопасны: SetCWXKey шлёт HP-пакет
|
// Поток передачи, НЕ GUI. Обе операции потокобезопасны: SetCWXKey шлёт HP-пакет
|
||||||
// под своим локом бэкенда, FCWKeyDown читает DSP-поток.
|
// под своим локом бэкенда, FCWKeyDown читает DSP-поток.
|
||||||
@@ -5814,7 +6102,8 @@ end;
|
|||||||
|
|
||||||
function TRadioController.CWXBusy: Boolean;
|
function TRadioController.CWXBusy: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := Assigned(FCWSender) and FCWSender.Busy;
|
Result := (Assigned(FCWSender) and FCWSender.Busy)
|
||||||
|
or (Assigned(FCWLocal) and FCWLocal.Busy);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.CWXSend(const Text: string);
|
procedure TRadioController.CWXSend(const Text: string);
|
||||||
@@ -5826,6 +6115,15 @@ begin
|
|||||||
// слот трансвертера, бэнд с DoNotTx) — там кейер разоружён, и дёргать бит
|
// слот трансвертера, бэнд с DoNotTx) — там кейер разоружён, и дёргать бит
|
||||||
// CWX незачем.
|
// CWX незачем.
|
||||||
if not CWTXActive then Exit;
|
if not CWTXActive then Exit;
|
||||||
|
// Локальный генератор сам держит тайминги В СЭМПЛАХ — текст уходит ему как
|
||||||
|
// есть; поток TCWSender (бит CWX прошивки) при этом не нужен вовсе.
|
||||||
|
if CWLocalSource then
|
||||||
|
begin
|
||||||
|
EnsureCWLocal;
|
||||||
|
SyncCWKeyer; // вооружиться, если пришли сюда первым делом
|
||||||
|
FCWLocal.SendText(Text);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
if not Assigned(FCWSender) then
|
if not Assigned(FCWSender) then
|
||||||
FCWSender := TCWSender.Create(Self.CWXKeyEvent);
|
FCWSender := TCWSender.Create(Self.CWXKeyEvent);
|
||||||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||||||
@@ -5835,6 +6133,7 @@ end;
|
|||||||
procedure TRadioController.CWXAbort;
|
procedure TRadioController.CWXAbort;
|
||||||
begin
|
begin
|
||||||
if Assigned(FCWSender) then FCWSender.AbortSending;
|
if Assigned(FCWSender) then FCWSender.AbortSending;
|
||||||
|
if Assigned(FCWLocal) then FCWLocal.AbortText;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.MixSidetone(const Left, Right: array of Single;
|
function TRadioController.MixSidetone(const Left, Right: array of Single;
|
||||||
@@ -5846,15 +6145,29 @@ const
|
|||||||
RAMP_MS = 5;
|
RAMP_MS = 5;
|
||||||
AUDIO_SR = 48000;
|
AUDIO_SR = 48000;
|
||||||
var
|
var
|
||||||
i: Integer;
|
i, Got: Integer;
|
||||||
Amp, Step, Rate, S: Double;
|
Amp, Step, Rate, S: Double;
|
||||||
|
Local: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
if not FCWSettings.SidetoneSW then Exit;
|
if not FCWSettings.SidetoneSW then Exit;
|
||||||
if FCWSettings.SidetoneSWLevel <= 0 then Exit;
|
if FCWSettings.SidetoneSWLevel <= 0 then Exit;
|
||||||
// Ключ отпущен и огибающая уже сошлась в ноль — блок трогать незачем.
|
|
||||||
if (not FCWKeyDown) and (FCWSTEnv <= 0.0) then Exit;
|
|
||||||
if Count <= 0 then Exit;
|
if Count <= 0 then Exit;
|
||||||
|
// ★Локальный кейер даёт ГОТОВУЮ огибающую — ту же самую, что ушла в эфир, с
|
||||||
|
// точностью до сэмпла. Это снимает старую границу «программный сайдтон верен
|
||||||
|
// только для текста и прямого ключа»: при иамбике в прошивке PC видел лишь
|
||||||
|
// состояние лепестков, а здесь тайминг его собственный.
|
||||||
|
Got := 0;
|
||||||
|
Local := FCWLocalArmed and Assigned(FCWLocal);
|
||||||
|
if Local then
|
||||||
|
begin
|
||||||
|
if Length(FCWSTEnvBuf) < Count then SetLength(FCWSTEnvBuf, Count);
|
||||||
|
Got := FCWLocal.PullSidetone(FCWSTEnvBuf, Count);
|
||||||
|
// Кольцо пусто и хвост уже сошёлся в ноль — блок трогать незачем.
|
||||||
|
if (Got <= 0) and (FCWSTEnv <= 0.0) then Exit;
|
||||||
|
end
|
||||||
|
else if (not FCWKeyDown) and (FCWSTEnv <= 0.0) then
|
||||||
|
Exit;
|
||||||
if Length(FCWSTBufL) < Count then
|
if Length(FCWSTBufL) < Count then
|
||||||
begin
|
begin
|
||||||
SetLength(FCWSTBufL, Count);
|
SetLength(FCWSTBufL, Count);
|
||||||
@@ -5865,7 +6178,18 @@ begin
|
|||||||
Rate := 1000.0 / (RAMP_MS * AUDIO_SR);
|
Rate := 1000.0 / (RAMP_MS * AUDIO_SR);
|
||||||
for i := 0 to Count - 1 do
|
for i := 0 to Count - 1 do
|
||||||
begin
|
begin
|
||||||
if FCWKeyDown then
|
if Local then
|
||||||
|
begin
|
||||||
|
// Дальше конца кольца доигрываем спадом: генератор либо отстал на
|
||||||
|
// блок, либо сессия кончилась.
|
||||||
|
if i < Got then FCWSTEnv := FCWSTEnvBuf[i]
|
||||||
|
else if FCWSTEnv > 0.0 then
|
||||||
|
begin
|
||||||
|
FCWSTEnv := FCWSTEnv - Rate;
|
||||||
|
if FCWSTEnv < 0.0 then FCWSTEnv := 0.0;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else if FCWKeyDown then
|
||||||
begin
|
begin
|
||||||
FCWSTEnv := FCWSTEnv + Rate;
|
FCWSTEnv := FCWSTEnv + Rate;
|
||||||
if FCWSTEnv > 1.0 then FCWSTEnv := 1.0;
|
if FCWSTEnv > 1.0 then FCWSTEnv := 1.0;
|
||||||
@@ -6359,14 +6683,14 @@ begin
|
|||||||
// Уровень пересчитываем здесь же: с вооружённым кейером он уедет в железо
|
// Уровень пересчитываем здесь же: с вооружённым кейером он уедет в железо
|
||||||
// сразу, до всякой передачи, а после конструктора FDriveLevel ещё нулевой.
|
// сразу, до всякой передачи, а после конструктора FDriveLevel ещё нулевой.
|
||||||
FDriveLevel := CalcDriveByte;
|
FDriveLevel := CalcDriveByte;
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel, False, True, True, FTuning, FTwoTone);
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel, False, True, True, FTuning, FTwoTone);
|
||||||
// ★DUC — на TX-ЧАСТОТУ, а не на центр дисплея. Раньше здесь стоял центр, и до
|
// ★DUC — на TX-ЧАСТОТУ, а не на центр дисплея. Раньше здесь стоял центр, и до
|
||||||
// первой перестройки VFO (или до первого MOX, который частоту дошлёт сам)
|
// первой перестройки VFO (или до первого MOX, который частоту дошлёт сам)
|
||||||
// передатчик стоял не там, где маркер: при CTUN центр и VFO — разные частоты.
|
// передатчик стоял не там, где маркер: при CTUN центр и VFO — разные частоты.
|
||||||
// С кейером в прошивке это стало слышно в эфире: железо может замкнуть ключ
|
// С кейером в прошивке это стало слышно в эфире: железо может замкнуть ключ
|
||||||
// сразу после старта, никакого MOX перед этим не будет.
|
// сразу после старта, никакого MOX перед этим не будет.
|
||||||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq),
|
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq),
|
||||||
XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel);
|
TXTuneFreqHz, FDriveLevel);
|
||||||
FRunning := True;
|
FRunning := True;
|
||||||
FRXStartTime := GetTickCount64;
|
FRXStartTime := GetTickCount64;
|
||||||
FRXLastPktTime := 0;
|
FRXLastPktTime := 0;
|
||||||
@@ -6424,11 +6748,15 @@ begin
|
|||||||
// (3.3 UI пересоздаст по персисту при следующем старте).
|
// (3.3 UI пересоздаст по персисту при следующем старте).
|
||||||
for p := 1 to MAX_PANS - 1 do
|
for p := 1 to MAX_PANS - 1 do
|
||||||
RemovePanDDC(p);
|
RemovePanDDC(p);
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True, FTuning, FTwoTone);
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, 0, False, True, True, FTuning, FTwoTone);
|
||||||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq),
|
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq),
|
||||||
XvtrTranslateTX(ActiveTXFreqHz), 0);
|
TXTuneFreqHz, 0);
|
||||||
FRunning := False;
|
FRunning := False;
|
||||||
FTransmitting := False;
|
FTransmitting := False;
|
||||||
|
// ★Локальный кейер обязан разоружиться ЗДЕСЬ: таймер хозяина, который его
|
||||||
|
// пересинхронизирует, на остановленном радио уже не работает — иначе
|
||||||
|
// манипулятор продолжал бы гонять IQ в закрытую сессию.
|
||||||
|
SyncCWKeyer;
|
||||||
// Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить
|
// Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить
|
||||||
// остановку (иначе при следующем старте уйдёт в эфир старый тон/голос),
|
// остановку (иначе при следующем старте уйдёт в эфир старый тон/голос),
|
||||||
// а TUN-состояние (PostGen-тон) — вернуться в выключенное.
|
// а TUN-состояние (PostGen-тон) — вернуться в выключенное.
|
||||||
|
|||||||
+51
-1
@@ -318,7 +318,10 @@ type
|
|||||||
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
|
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
|
||||||
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
||||||
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
||||||
FWKeyer: Boolean; // кейер в прошивке (иначе телеграф выключен)
|
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
|
||||||
|
// ключа нет). ВМЕСТЕ с LocalKeyer образует одну развилку «кто рисует точки»,
|
||||||
|
// в UI это один список: прошивка / программа / выкл.
|
||||||
|
FWKeyer: Boolean;
|
||||||
KeyerMode: Integer; // 0=Straight, 1=Iambic A, 2=Iambic B
|
KeyerMode: Integer; // 0=Straight, 1=Iambic A, 2=Iambic B
|
||||||
ReverseKeys: Boolean; // поменять точку и тире местами
|
ReverseKeys: Boolean; // поменять точку и тире местами
|
||||||
StrictSpacing: Boolean; // строгие межзнаковые интервалы
|
StrictSpacing: Boolean; // строгие межзнаковые интервалы
|
||||||
@@ -338,6 +341,26 @@ type
|
|||||||
// состояние лепестков манипулятора — там верен только аппаратный тон.
|
// состояние лепестков манипулятора — там верен только аппаратный тон.
|
||||||
SidetoneSW: Boolean;
|
SidetoneSW: Boolean;
|
||||||
SidetoneSWLevel: Integer; // 0..100 % от полной шкалы аудио
|
SidetoneSWLevel: Integer; // 0..100 % от полной шкалы аудио
|
||||||
|
// ---- Локальный (программный) кейер --------------------------------------
|
||||||
|
// Манипуляцию формирует САМ EWSDR: генератор рисует несущую с огибающей
|
||||||
|
// прямо в поток IQ (CWKeyer.pas). Для Pluto/LibreSDR это единственный
|
||||||
|
// возможный путь — кейера в железе там нет, и выбор «прошивка» вырождается
|
||||||
|
// туда сам; для openHPSDR это опция, по умолчанию выключенная (тайминги в
|
||||||
|
// FPGA точнее любой программы).
|
||||||
|
LocalKeyer: Boolean;
|
||||||
|
// Сдвиг несущей манипуляции от гетеродина. 0 = авто: у openHPSDR 0 (DUC —
|
||||||
|
// цифровой, утекать нечему), у zero-IF AD936x — CW_AUTO_OFFSET_HZ, иначе
|
||||||
|
// утечка LO сидит РОВНО на рабочей частоте и слышна между посылками.
|
||||||
|
CarrierOffsetHz: Integer;
|
||||||
|
// Манипулятор на модемных линиях COM/USB-serial (DTR/RTS питают ключ,
|
||||||
|
// CTS/DSR/DCD/RI читаются как лепестки).
|
||||||
|
KeyPortEnabled: Boolean;
|
||||||
|
KeyPort: string[63];
|
||||||
|
KeyDotLine: Integer; // 0=CTS, 1=DSR, 2=DCD, 3=RI
|
||||||
|
KeyDashLine: Integer;
|
||||||
|
KeyInvert: Boolean;
|
||||||
|
KeyPowerDTR: Boolean;
|
||||||
|
KeyPowerRTS: Boolean;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
|
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
|
||||||
@@ -1789,6 +1812,15 @@ begin
|
|||||||
C.SidetoneHWLevel := 50;
|
C.SidetoneHWLevel := 50;
|
||||||
C.SidetoneSW := False;
|
C.SidetoneSW := False;
|
||||||
C.SidetoneSWLevel := 30;
|
C.SidetoneSWLevel := 30;
|
||||||
|
C.LocalKeyer := False; // у openHPSDR тайминги в FPGA; Pluto включит сам
|
||||||
|
C.CarrierOffsetHz := 0; // 0 = авто (по типу бэкенда)
|
||||||
|
C.KeyPortEnabled := False;
|
||||||
|
C.KeyPort := '';
|
||||||
|
C.KeyDotLine := 0; // CTS
|
||||||
|
C.KeyDashLine := 1; // DSR
|
||||||
|
C.KeyInvert := False;
|
||||||
|
C.KeyPowerDTR := True; // типовая распайка: ключ питается от DTR/RTS
|
||||||
|
C.KeyPowerRTS := True;
|
||||||
// Заводские заготовки: типовой вызов, ответ и стандартные концовки.
|
// Заводские заготовки: типовой вызов, ответ и стандартные концовки.
|
||||||
C.Messages[0] := 'CQ CQ DE ';
|
C.Messages[0] := 'CQ CQ DE ';
|
||||||
C.Messages[1] := 'DE ';
|
C.Messages[1] := 'DE ';
|
||||||
@@ -1826,6 +1858,15 @@ begin
|
|||||||
C.SidetoneHWLevel := EnsureRange(JI(O,'sidetone_hw_level', C.SidetoneHWLevel), 0, 127);
|
C.SidetoneHWLevel := EnsureRange(JI(O,'sidetone_hw_level', C.SidetoneHWLevel), 0, 127);
|
||||||
C.SidetoneSW := JB(O,'sidetone_sw', C.SidetoneSW);
|
C.SidetoneSW := JB(O,'sidetone_sw', C.SidetoneSW);
|
||||||
C.SidetoneSWLevel := EnsureRange(JI(O,'sidetone_sw_level', C.SidetoneSWLevel), 0, 100);
|
C.SidetoneSWLevel := EnsureRange(JI(O,'sidetone_sw_level', C.SidetoneSWLevel), 0, 100);
|
||||||
|
C.LocalKeyer := JB(O,'local_keyer', C.LocalKeyer);
|
||||||
|
C.CarrierOffsetHz := EnsureRange(JI(O,'carrier_offset_hz', C.CarrierOffsetHz), -1, 48000);
|
||||||
|
C.KeyPortEnabled := JB(O,'key_port_on', C.KeyPortEnabled);
|
||||||
|
C.KeyPort := Copy(JS(O,'key_port', C.KeyPort), 1, 63);
|
||||||
|
C.KeyDotLine := EnsureRange(JI(O,'key_dot_line', C.KeyDotLine), 0, 3);
|
||||||
|
C.KeyDashLine := EnsureRange(JI(O,'key_dash_line', C.KeyDashLine), 0, 3);
|
||||||
|
C.KeyInvert := JB(O,'key_invert', C.KeyInvert);
|
||||||
|
C.KeyPowerDTR := JB(O,'key_power_dtr', C.KeyPowerDTR);
|
||||||
|
C.KeyPowerRTS := JB(O,'key_power_rts', C.KeyPowerRTS);
|
||||||
for i := 0 to CW_MSG_COUNT - 1 do
|
for i := 0 to CW_MSG_COUNT - 1 do
|
||||||
C.Messages[i] := Copy(JS(O, 'msg_' + IntToStr(i), C.Messages[i]), 1, 63);
|
C.Messages[i] := Copy(JS(O, 'msg_' + IntToStr(i), C.Messages[i]), 1, 63);
|
||||||
end;
|
end;
|
||||||
@@ -1850,6 +1891,15 @@ begin
|
|||||||
JW(O,'sidetone_hw_level', C.SidetoneHWLevel);
|
JW(O,'sidetone_hw_level', C.SidetoneHWLevel);
|
||||||
JW(O,'sidetone_sw', C.SidetoneSW);
|
JW(O,'sidetone_sw', C.SidetoneSW);
|
||||||
JW(O,'sidetone_sw_level', C.SidetoneSWLevel);
|
JW(O,'sidetone_sw_level', C.SidetoneSWLevel);
|
||||||
|
JW(O,'local_keyer', C.LocalKeyer);
|
||||||
|
JW(O,'carrier_offset_hz', C.CarrierOffsetHz);
|
||||||
|
JW(O,'key_port_on', C.KeyPortEnabled);
|
||||||
|
JWS(O,'key_port', C.KeyPort);
|
||||||
|
JW(O,'key_dot_line', C.KeyDotLine);
|
||||||
|
JW(O,'key_dash_line', C.KeyDashLine);
|
||||||
|
JW(O,'key_invert', C.KeyInvert);
|
||||||
|
JW(O,'key_power_dtr', C.KeyPowerDTR);
|
||||||
|
JW(O,'key_power_rts', C.KeyPowerRTS);
|
||||||
for i := 0 to CW_MSG_COUNT - 1 do
|
for i := 0 to CW_MSG_COUNT - 1 do
|
||||||
JWS(O, 'msg_' + IntToStr(i), C.Messages[i]);
|
JWS(O, 'msg_' + IntToStr(i), C.Messages[i]);
|
||||||
end;
|
end;
|
||||||
|
|||||||
+122
-11
@@ -289,7 +289,9 @@ type
|
|||||||
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
||||||
// CW (телеграф) — свойство устройства, профилем не переключается
|
// CW (телеграф) — свойство устройства, профилем не переключается
|
||||||
FCW: TCWSettings;
|
FCW: TCWSettings;
|
||||||
FChkCWFWKeyer: TFlatCheckBox;
|
// Кто рисует точки: прошивка / программа / никто. Одна развилка — один
|
||||||
|
// список (две галки на неё разъезжались во взаимоисключающих состояниях).
|
||||||
|
FCmbCWKeyerSrc: TFlatComboBox;
|
||||||
FEdCWPitch: TFlatSpinEdit;
|
FEdCWPitch: TFlatSpinEdit;
|
||||||
FEdCWSpeed: TFlatSpinEdit;
|
FEdCWSpeed: TFlatSpinEdit;
|
||||||
FEdCWWeight: TFlatSpinEdit;
|
FEdCWWeight: TFlatSpinEdit;
|
||||||
@@ -304,6 +306,16 @@ type
|
|||||||
FEdCWSidetoneHWLv: TFlatSpinEdit;
|
FEdCWSidetoneHWLv: TFlatSpinEdit;
|
||||||
FChkCWSidetoneSW: TFlatCheckBox;
|
FChkCWSidetoneSW: TFlatCheckBox;
|
||||||
FEdCWSidetoneSWLv: TFlatSpinEdit;
|
FEdCWSidetoneSWLv: TFlatSpinEdit;
|
||||||
|
// Локальный кейер: манипуляцию рисует сам EWSDR (обязателен на AD936x)
|
||||||
|
FEdCWOffset: TFlatSpinEdit;
|
||||||
|
FChkCWKeyPort: TFlatCheckBox;
|
||||||
|
FEdCWKeyPort: TFlatEdit;
|
||||||
|
FCmbCWDotLine: TFlatComboBox;
|
||||||
|
FCmbCWDashLine: TFlatComboBox;
|
||||||
|
FChkCWKeyInvert: TFlatCheckBox;
|
||||||
|
FChkCWKeyDTR: TFlatCheckBox;
|
||||||
|
FChkCWKeyRTS: TFlatCheckBox;
|
||||||
|
FCWHasFWKeyer: Boolean; // у железа есть кейер в прошивке
|
||||||
// TX Display (отдельный analyzer)
|
// TX Display (отдельный analyzer)
|
||||||
FCmbTXFFT: TFlatComboBox;
|
FCmbTXFFT: TFlatComboBox;
|
||||||
FCmbTXWindow: TFlatComboBox;
|
FCmbTXWindow: TFlatComboBox;
|
||||||
@@ -658,6 +670,8 @@ type
|
|||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
||||||
procedure LoadCWSettings(const C: TCWSettings);
|
procedure LoadCWSettings(const C: TCWSettings);
|
||||||
|
// Есть ли у активного железа кейер в прошивке (openHPSDR — да, AD936x — нет).
|
||||||
|
procedure SetCWBackend(HasFWKeyer: Boolean);
|
||||||
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
||||||
// главном окне, здесь показываем его значение как часть профиля).
|
// главном окне, здесь показываем его значение как часть профиля).
|
||||||
procedure LoadTXProfiles(const Names: array of string;
|
procedure LoadTXProfiles(const Names: array of string;
|
||||||
@@ -2042,9 +2056,12 @@ begin
|
|||||||
|
|
||||||
// ---- CW: телеграфом в openHPSDR P2 управляет прошивка -------------------
|
// ---- CW: телеграфом в openHPSDR P2 управляет прошивка -------------------
|
||||||
Inc(Y, 190 + 12);
|
Inc(Y, 190 + 12);
|
||||||
Grp := MakeGroupPanel(FTXSubPage[2], 'CW (telegraphy)', MARGIN, Y, GRP_W, 320);
|
Grp := MakeGroupPanel(FTXSubPage[2], 'CW (telegraphy)', MARGIN, Y, GRP_W, 460);
|
||||||
|
|
||||||
FChkCWFWKeyer := MkChkCW(Grp, PAD, R1 - 6, 250, 'Keyer in radio firmware');
|
MakeLbl(Grp, 'Keyer', PAD, R1 - 1, 50);
|
||||||
|
FCmbCWKeyerSrc := MkCmbCW(Grp, PAD + 54, R1 - 6, 194);
|
||||||
|
FCmbCWKeyerSrc.Items.AddStrings(['Radio firmware','Software (PC)','Off (MOX only)']);
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||||
MkBtn(Grp, PAD + 260, R1 - 6, 110, 'Messages...', BtnCWMessagesClick);
|
MkBtn(Grp, PAD + 260, R1 - 6, 110, 'Messages...', BtnCWMessagesClick);
|
||||||
MakeLbl(Grp, 'Pitch (Hz)', PAD + 400, R1 - 1, 90);
|
MakeLbl(Grp, 'Pitch (Hz)', PAD + 400, R1 - 1, 90);
|
||||||
FEdCWPitch := MkSpinCW(Grp, PAD + 500, R1 - 6, 200, 1200, 600);
|
FEdCWPitch := MkSpinCW(Grp, PAD + 500, R1 - 6, 200, 1200, 600);
|
||||||
@@ -2076,13 +2093,46 @@ begin
|
|||||||
MakeLbl(Grp, 'Level', PAD + 568, R1 + 4 * STEP + 5, 44);
|
MakeLbl(Grp, 'Level', PAD + 568, R1 + 4 * STEP + 5, 44);
|
||||||
FEdCWSidetoneSWLv := MkSpinCW(Grp, PAD + 616, R1 + 4 * STEP, 0, 100, 30);
|
FEdCWSidetoneSWLv := MkSpinCW(Grp, PAD + 616, R1 + 4 * STEP, 0, 100, 30);
|
||||||
|
|
||||||
TxNote(Grp, 'Dot and dash timing is produced by the radio, so PC load never '
|
// ---- Программный кейер: манипуляцию рисует сам EWSDR --------------------
|
||||||
+ 'affects it. The voice transmit chain is not started in CW at all. '
|
MakeLbl(Grp, 'Carrier offset (Hz)', PAD, R1 + 5 * STEP + 5, LW);
|
||||||
+ 'The radio sidetone is heard in the radio headphones; the PC sidetone '
|
FEdCWOffset := MkSpinCW(Grp, CX, R1 + 5 * STEP, -1, 48000, 0);
|
||||||
+ 'is exact for keyboard sending and a straight key, but not for a paddle '
|
MakeLbl(Grp, '0 = auto, -1 = none (software keyer only)', PAD + 320, R1 + 5 * STEP + 5, 260);
|
||||||
+ '— with iambic the firmware owns the timing.', R1 + 5 * STEP - 6, 60);
|
|
||||||
|
|
||||||
Inc(Y, 320 + 12);
|
FChkCWKeyPort := MkChkCW(Grp, PAD, R1 + 6 * STEP, 250, 'Paddle on serial port');
|
||||||
|
FEdCWKeyPort := TFlatEdit.Create(Self);
|
||||||
|
FEdCWKeyPort.Parent := Grp;
|
||||||
|
FEdCWKeyPort.SetBounds(DpiScale(PAD + 260), DpiScale(R1 + 6 * STEP),
|
||||||
|
DpiScale(220), DpiScale(BTN_H));
|
||||||
|
FEdCWKeyPort.Color := CLR_INPUT;
|
||||||
|
FEdCWKeyPort.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
FEdCWKeyPort.Font.Size := 9;
|
||||||
|
FEdCWKeyPort.OnExit := OnCWAnyChange;
|
||||||
|
MakeLbl(Grp, '/dev/ttyUSB0, COM3', PAD + 492, R1 + 6 * STEP + 5, 200);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Dot line', PAD, R1 + 7 * STEP + 5, 70);
|
||||||
|
FCmbCWDotLine := MkCmbCW(Grp, PAD + 74, R1 + 7 * STEP, 90);
|
||||||
|
FCmbCWDotLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||||
|
FCmbCWDotLine.ItemIndex := 0;
|
||||||
|
MakeLbl(Grp, 'Dash line', PAD + 176, R1 + 7 * STEP + 5, 74);
|
||||||
|
FCmbCWDashLine := MkCmbCW(Grp, PAD + 254, R1 + 7 * STEP, 90);
|
||||||
|
FCmbCWDashLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||||
|
FCmbCWDashLine.ItemIndex := 1;
|
||||||
|
FChkCWKeyInvert := MkChkCW(Grp, PAD + 360, R1 + 7 * STEP, 90, 'Invert');
|
||||||
|
FChkCWKeyDTR := MkChkCW(Grp, PAD + 456, R1 + 7 * STEP, 100, 'DTR power');
|
||||||
|
FChkCWKeyRTS := MkChkCW(Grp, PAD + 562, R1 + 7 * STEP, 100, 'RTS power');
|
||||||
|
|
||||||
|
TxNote(Grp, 'Firmware keyer: the radio produces dot and dash timing, so PC '
|
||||||
|
+ 'load never affects it — the default where the hardware has one, and the '
|
||||||
|
+ 'paddle plugs into the radio. Software keyer: EWSDR draws the keyed '
|
||||||
|
+ 'carrier into the IQ stream itself (the only option on Pluto/LibreSDR) '
|
||||||
|
+ 'and the paddle comes in over a serial port; element lengths are counted '
|
||||||
|
+ 'in samples, so only the paddle input jitters, never the rhythm. Carrier '
|
||||||
|
+ 'offset 0 means auto: zero on openHPSDR, 12 kHz on zero-IF hardware, '
|
||||||
|
+ 'where LO leakage would otherwise sit right on the working frequency '
|
||||||
|
+ 'between elements. The voice transmit chain is not started in CW at all.',
|
||||||
|
R1 + 8 * STEP - 6, 92);
|
||||||
|
|
||||||
|
Inc(Y, 460 + 12);
|
||||||
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
|
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
|
||||||
|
|
||||||
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
|
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
|
||||||
@@ -2271,8 +2321,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.CollectCWFromUI;
|
procedure TSettingsForm.CollectCWFromUI;
|
||||||
|
var Src: Integer;
|
||||||
begin
|
begin
|
||||||
if FChkCWFWKeyer <> nil then FCW.FWKeyer := FChkCWFWKeyer.Checked;
|
// Список «кто рисует точки» → пара флагов. На железе без кейера в прошивке
|
||||||
|
// первого пункта в списке нет вовсе, поэтому индексы сдвинуты на один.
|
||||||
|
if FCmbCWKeyerSrc <> nil then
|
||||||
|
begin
|
||||||
|
Src := FCmbCWKeyerSrc.ItemIndex;
|
||||||
|
if Src < 0 then Src := 0;
|
||||||
|
if not FCWHasFWKeyer then Inc(Src);
|
||||||
|
case Src of
|
||||||
|
0: begin FCW.FWKeyer := True; FCW.LocalKeyer := False; end; // прошивка
|
||||||
|
1: begin FCW.FWKeyer := True; FCW.LocalKeyer := True; end; // программа
|
||||||
|
else begin FCW.FWKeyer := False; FCW.LocalKeyer := False; end; // выключен
|
||||||
|
end;
|
||||||
|
end;
|
||||||
if FEdCWPitch <> nil then FCW.Pitch := FEdCWPitch.Value;
|
if FEdCWPitch <> nil then FCW.Pitch := FEdCWPitch.Value;
|
||||||
if FCmbCWKeyerMode <> nil then FCW.KeyerMode := EnsureRange(FCmbCWKeyerMode.ItemIndex, 0, 2);
|
if FCmbCWKeyerMode <> nil then FCW.KeyerMode := EnsureRange(FCmbCWKeyerMode.ItemIndex, 0, 2);
|
||||||
if FChkCWReverse <> nil then FCW.ReverseKeys := FChkCWReverse.Checked;
|
if FChkCWReverse <> nil then FCW.ReverseKeys := FChkCWReverse.Checked;
|
||||||
@@ -2287,6 +2350,36 @@ begin
|
|||||||
if FEdCWSidetoneHWLv <> nil then FCW.SidetoneHWLevel := FEdCWSidetoneHWLv.Value;
|
if FEdCWSidetoneHWLv <> nil then FCW.SidetoneHWLevel := FEdCWSidetoneHWLv.Value;
|
||||||
if FChkCWSidetoneSW <> nil then FCW.SidetoneSW := FChkCWSidetoneSW.Checked;
|
if FChkCWSidetoneSW <> nil then FCW.SidetoneSW := FChkCWSidetoneSW.Checked;
|
||||||
if FEdCWSidetoneSWLv <> nil then FCW.SidetoneSWLevel := FEdCWSidetoneSWLv.Value;
|
if FEdCWSidetoneSWLv <> nil then FCW.SidetoneSWLevel := FEdCWSidetoneSWLv.Value;
|
||||||
|
if FEdCWOffset <> nil then FCW.CarrierOffsetHz := FEdCWOffset.Value;
|
||||||
|
if FChkCWKeyPort <> nil then FCW.KeyPortEnabled := FChkCWKeyPort.Checked;
|
||||||
|
if FEdCWKeyPort <> nil then FCW.KeyPort := Copy(FEdCWKeyPort.Text, 1, 63);
|
||||||
|
if FCmbCWDotLine <> nil then FCW.KeyDotLine := EnsureRange(FCmbCWDotLine.ItemIndex, 0, 3);
|
||||||
|
if FCmbCWDashLine <> nil then FCW.KeyDashLine := EnsureRange(FCmbCWDashLine.ItemIndex, 0, 3);
|
||||||
|
if FChkCWKeyInvert <> nil then FCW.KeyInvert := FChkCWKeyInvert.Checked;
|
||||||
|
if FChkCWKeyDTR <> nil then FCW.KeyPowerDTR := FChkCWKeyDTR.Checked;
|
||||||
|
if FChkCWKeyRTS <> nil then FCW.KeyPowerRTS := FChkCWKeyRTS.Checked;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.SetCWBackend(HasFWKeyer: Boolean);
|
||||||
|
// Гейт по железу: у AD936x кейера в прошивке нет вовсе, значит нет ни этого
|
||||||
|
// пункта в списке, ни аппаратного сайдтона (он живёт в наушниках самого
|
||||||
|
// трансивера) — телеграф там может быть только программным.
|
||||||
|
// ★Обязан отработать ДО LoadCWSettings: тот считает индекс списка по FCWHasFWKeyer.
|
||||||
|
begin
|
||||||
|
FCWHasFWKeyer := HasFWKeyer;
|
||||||
|
if FChkCWSidetoneHW <> nil then FChkCWSidetoneHW.Enabled := HasFWKeyer;
|
||||||
|
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Enabled := HasFWKeyer;
|
||||||
|
if FCmbCWKeyerSrc = nil then Exit;
|
||||||
|
FLoading := True; // правка Items дёргает OnChange — настройки трогать нельзя
|
||||||
|
try
|
||||||
|
FCmbCWKeyerSrc.Items.Clear;
|
||||||
|
if HasFWKeyer then FCmbCWKeyerSrc.Items.Add('Radio firmware');
|
||||||
|
FCmbCWKeyerSrc.Items.Add('Software (PC)');
|
||||||
|
FCmbCWKeyerSrc.Items.Add('Off (MOX only)');
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.FireCWChange;
|
procedure TSettingsForm.FireCWChange;
|
||||||
@@ -2307,11 +2400,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.LoadCWSettings(const C: TCWSettings);
|
procedure TSettingsForm.LoadCWSettings(const C: TCWSettings);
|
||||||
|
var Src: Integer;
|
||||||
begin
|
begin
|
||||||
FLoading := True;
|
FLoading := True;
|
||||||
try
|
try
|
||||||
FCW := C;
|
FCW := C;
|
||||||
if FChkCWFWKeyer <> nil then FChkCWFWKeyer.Checked := C.FWKeyer;
|
if FCmbCWKeyerSrc <> nil then
|
||||||
|
begin
|
||||||
|
// Пара флагов → пункт списка. «Прошивка» на железе без кейера означает
|
||||||
|
// программный кейер: другого пути там нет, и врать в списке незачем.
|
||||||
|
if (not C.FWKeyer) and (not C.LocalKeyer) then Src := 2
|
||||||
|
else if C.LocalKeyer or (not FCWHasFWKeyer) then Src := 1
|
||||||
|
else Src := 0;
|
||||||
|
if not FCWHasFWKeyer then Dec(Src);
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := EnsureRange(Src, 0, FCmbCWKeyerSrc.Items.Count - 1);
|
||||||
|
end;
|
||||||
if FEdCWPitch <> nil then FEdCWPitch.Value := EnsureRange(C.Pitch, 200, 1200);
|
if FEdCWPitch <> nil then FEdCWPitch.Value := EnsureRange(C.Pitch, 200, 1200);
|
||||||
if FCmbCWKeyerMode <> nil then FCmbCWKeyerMode.ItemIndex := EnsureRange(C.KeyerMode, 0, 2);
|
if FCmbCWKeyerMode <> nil then FCmbCWKeyerMode.ItemIndex := EnsureRange(C.KeyerMode, 0, 2);
|
||||||
if FChkCWReverse <> nil then FChkCWReverse.Checked := C.ReverseKeys;
|
if FChkCWReverse <> nil then FChkCWReverse.Checked := C.ReverseKeys;
|
||||||
@@ -2326,6 +2429,14 @@ begin
|
|||||||
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Value := EnsureRange(C.SidetoneHWLevel, 0, 127);
|
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Value := EnsureRange(C.SidetoneHWLevel, 0, 127);
|
||||||
if FChkCWSidetoneSW <> nil then FChkCWSidetoneSW.Checked := C.SidetoneSW;
|
if FChkCWSidetoneSW <> nil then FChkCWSidetoneSW.Checked := C.SidetoneSW;
|
||||||
if FEdCWSidetoneSWLv <> nil then FEdCWSidetoneSWLv.Value := EnsureRange(C.SidetoneSWLevel, 0, 100);
|
if FEdCWSidetoneSWLv <> nil then FEdCWSidetoneSWLv.Value := EnsureRange(C.SidetoneSWLevel, 0, 100);
|
||||||
|
if FEdCWOffset <> nil then FEdCWOffset.Value := EnsureRange(C.CarrierOffsetHz, -1, 48000);
|
||||||
|
if FChkCWKeyPort <> nil then FChkCWKeyPort.Checked := C.KeyPortEnabled;
|
||||||
|
if FEdCWKeyPort <> nil then FEdCWKeyPort.Text := C.KeyPort;
|
||||||
|
if FCmbCWDotLine <> nil then FCmbCWDotLine.ItemIndex := EnsureRange(C.KeyDotLine, 0, 3);
|
||||||
|
if FCmbCWDashLine <> nil then FCmbCWDashLine.ItemIndex := EnsureRange(C.KeyDashLine, 0, 3);
|
||||||
|
if FChkCWKeyInvert <> nil then FChkCWKeyInvert.Checked := C.KeyInvert;
|
||||||
|
if FChkCWKeyDTR <> nil then FChkCWKeyDTR.Checked := C.KeyPowerDTR;
|
||||||
|
if FChkCWKeyRTS <> nil then FChkCWKeyRTS.Checked := C.KeyPowerRTS;
|
||||||
finally
|
finally
|
||||||
FLoading := False;
|
FLoading := False;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -312,6 +312,10 @@
|
|||||||
<Filename Value="CWMorse.pas"/>
|
<Filename Value="CWMorse.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
</Unit>
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="CWKeyer.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
<Unit>
|
<Unit>
|
||||||
<Filename Value="CWMessagesForm.pas"/>
|
<Filename Value="CWMessagesForm.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
|||||||
@@ -467,6 +467,9 @@ begin
|
|||||||
begin
|
begin
|
||||||
if FCtrl.FRunning and FCtrl.FWDSPReady then
|
if FCtrl.FRunning and FCtrl.FWDSPReady then
|
||||||
FCtrl.ServiceBeaconLock;
|
FCtrl.ServiceBeaconLock;
|
||||||
|
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и
|
||||||
|
// отпустить реле T/R после выдержки больше некому).
|
||||||
|
FCtrl.ServiceCWKeyed;
|
||||||
lastBeacon := nowMs;
|
lastBeacon := nowMs;
|
||||||
end;
|
end;
|
||||||
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
||||||
|
|||||||
Reference in New Issue
Block a user