feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста

CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг
нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в
эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда
был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали.

Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются
относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают
три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых
вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица
фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска
на спектре и маркер VFO верны без правок в UI, а слайсы получают свой
сдвиг по своему режиму.

Правила эфира:
- голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только
  PTT; несущую даёт кейер, бит CWX или тон TUN;
- байт 5 собирается из настроек и перепосылается на каждой смене режима и
  TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на
  замыкание ключа посреди SSB;
- TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO;
- приёмник на передаче в CW не глушится, иначе умолкает программный
  сайдтон и эфир между посылками.

Программная передача текста (CWMorse): поток с абсолютными дедлайнами
дёргает бит CWX в High Priority, элементы рисует прошивка. Касание
манипулятора или снятие MOX обрывают передачу. Память сообщений — окно
CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY.

Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS.

Программный сайдтон точен для передачи текста и прямого ключа; при иамбике
таймингом владеет FPGA, поэтому там верен только аппаратный тон.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 12:20:56 +03:00
co-authored by Claude Opus 5
parent 0ec120c2d5
commit 53329e82e8
12 changed files with 1421 additions and 46 deletions
+54 -12
View File
@@ -112,6 +112,14 @@ type
GetFilterHigh: TCATGetIntFunc;
SetFilterLow: TCATSetIntProc;
SetFilterHigh: TCATSetIntProc;
// Телеграф (KS/KY/ZZKS/ZZKY/ZZKM). Скорость — WPM 5..60; SendCWText ставит
// текст в очередь программной передачи, CWTextBusy = «очередь занята».
GetCWSpeed: TCATGetIntFunc;
SetCWSpeed: TCATSetIntProc;
GetCWKeyerMode: TCATGetIntFunc; // 0=прямой ключ, 1=iambic A, 2=iambic B
SetCWKeyerMode: TCATSetIntProc;
SendCWText: procedure(const S: string) of object;
CWTextBusy: TCATGetBoolFunc;
// Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
@@ -136,6 +144,7 @@ type
function SafeGetVfoA: Double;
function SafeGetVfoB: Double;
function SafeGetMode: Integer;
function SafeGetCWSpeed: Integer;
function SafeGetVolume: Integer;
function SafeGetDriveLevel: Integer;
function SafeGetFilterIdx: Integer;
@@ -812,21 +821,43 @@ begin
Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000';
end;
function TCATEngine.SafeGetCWSpeed: Integer;
begin
if Assigned(FCtx.GetCWSpeed) then Result := FCtx.GetCWSpeed else Result := 20;
end;
function TCATEngine.CmdKS(const s: string): string;
// KS: CW keying speed (3 digits, WPM) — STUB: no CW keyer
// KS: CW keying speed (3 digits, WPM).
var v: Integer;
begin
if Length(s) = 3 then
Result := '' // accepted but ignored
begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWSpeed) then FCtx.SetCWSpeed(v);
Result := '';
end
else if Length(s) = 0 then
Result := '025' // default 25 WPM
Result := Format('%.3d', [SafeGetCWSpeed])
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdKY(const s: string): string;
// KY: send CW text — STUB: no CW keyer implemented
// KY: передать текст телеграфом. Kenwood-семантика: первый символ — пробел-
// заполнитель признака «не занято», далее сам текст; пустой аргумент = опрос
// занятости ('1' — очередь занята, логгер ждёт).
var Txt: string;
begin
Result := '0'; // queue not full
if Length(s) = 0 then
begin
if Assigned(FCtx.CWTextBusy) and FCtx.CWTextBusy then Result := '1'
else Result := '0';
Exit;
end;
Txt := s;
if (Length(Txt) > 0) and (Txt[1] = ' ') then Delete(Txt, 1, 1);
if Assigned(FCtx.SendCWText) then FCtx.SendCWText(Txt);
Result := '';
end;
function TCATEngine.CmdMD(const s: string): string;
@@ -1754,23 +1785,34 @@ begin
end;
function TCATEngine.ZZKM(const s: string): string;
// ZZKM: CW key mode — STUB: no CW keyer
// ZZKM: режим кейера (0=прямой ключ, 1=iambic A, 2=iambic B).
var v: Integer;
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
if Length(s) = 1 then
begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if (v < 0) or (v > 2) then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWKeyerMode) then FCtx.SetCWKeyerMode(v);
Result := '';
end
else if Length(s) = 0 then
begin
if Assigned(FCtx.GetCWKeyerMode) then Result := IntToStr(FCtx.GetCWKeyerMode)
else Result := '0';
end
else Result := CAT_ERROR;
end;
function TCATEngine.ZZKS(const s: string): string;
// ZZKS: CW speed (WPM) — STUB: no CW keyer
// ZZKS: скорость кейера, WPM.
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025'
else Result := CAT_ERROR;
Result := CmdKS(s);
end;
function TCATEngine.ZZKY(const s: string): string;
// ZZKY: CW text to send — STUB: no CW keyer implemented
// ZZKY: передать текст телеграфом (то же, что KY).
begin
Result := '0'; // buffer not full
Result := CmdKY(s);
end;
function TCATEngine.ZZMA(const s: string): string;