fix(cw): первый прогон на железе — частота, мощность, индикация, полоса

Семь дефектов, вскрытых проверкой в эфире. Общий корень у большинства: с
кейером в прошивке FTransmitting не поднимается вовсе — передачей владеет
FPGA, — а половина проекта считала этот флаг признаком эфира.

Обрыв посылки после первого элемента. При break-in железо само поднимает
T/R на каждую посылку, HPS_PTT это отражает, и разбор статуса гонял по
фронту SetMOX(True/False); снятие MOX гасило CWX. Теперь в телеграфе фронт
HPS_PTT не управляет передачей. Без этого и работа манипулятором
пересобирала бы передающий тракт на каждой точке.

Обрыв по «key hit» переведён на фронт лепестка: на плате без ключа входы
могут стоять в единице, и проверка уровня убивала бы передачу сразу.

Посылка уходила по центру дисплея, а не на VFO. StartRunning ставил DUC на
центр DDC; до первой перестройки VFO или до первого MOX там и оставалось.
Без CTUN центр совпадает с VFO, поэтому баг и дожил до телеграфа, где MOX
не бывает. SetRunAndFreq получает TX-частоту, SetSliceTarget толкает кадр,
когда перестраивают слайс-источник. Правило: DUC обязан стоять правильно
всегда, а не только на передаче.

Мощность ниже, чем на TUN: уровень (байт 345) клался в кадр только при
FIsTransmitting. Введён SetCWKeyerArmed — пока кейер вооружён, уровень
лежит в каждом HP-кадре. Там же пересчитывается FDriveLevel, иначе до
первого касания ручки в железо уходил ноль.

Индикация не показывала передачу: события rfTransmitting в телеграфе не
бывает, поэтому блок рендера не выполнялся ни разу. ApplyHPStatus шлёт его
сам на фронтах RadioKeyed («железо в эфире» = передача или замыкание ключа
прошивкой, выдержка 8 точек и не меньше 500 мс). На RadioKeyed переведены
S-метр, красная полоса главного VFO и TX-бейджи флагов вместе с красной
полосой слайса на панадаптерах.

Полоса на спектре рисовалась как SSB: TXSignedEdges обязана отдавать для
CW голосовую боковую (через bp0 идёт тон pitch при настройке), поэтому
экранные кромки развязаны в TXFilterEdgesHz — занимаемая полоса
манипуляции симметрично несущей. Тот же класс, что был у ЧМ.

F1..F8 молчали, пока не откроешь окно памяти: обработчик упирался в
проверку лениво создаваемой формы. Текст берётся из настроек.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 13:14:38 +03:00
co-authored by Claude Opus 5
parent 53329e82e8
commit 0a1a1ee3af
6 changed files with 159 additions and 16 deletions
+93 -5
View File
@@ -442,6 +442,14 @@ type
// разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone —
// толерантная гонка на одном булеве, как FHPp*.
FCWKeyDown: Boolean;
// Прошлое состояние лепестков — обрыв программной передачи по фронту.
FCWPrevDot: Boolean;
FCWPrevDash: Boolean;
// Момент последнего замыкания ключа прошивкой — только для ИНДИКАЦИИ
// (см. RadioKeyed): без выдержки метр моргал бы RX/TX на каждой точке.
FCWKeyedAt: QWord;
// Предыдущее значение RadioKeyed — событие рендера шлём только на фронте.
FCWKeyedShown: Boolean;
// Последний отправленный байт 5 — чтобы не гнать DUC-пакет на каждую
// смену режима, а только когда телеграфная конфигурация реально поменялась.
FLastCWOptions: Byte;
@@ -945,6 +953,10 @@ type
// ---- Телеграф ----------------------------------------------------------
// Настройки применяются целиком: pitch уезжает в движок (там он двигает
// гетеродин и полосу), остальное — в DUC Specific прошивке.
// «Железо сейчас в эфире» — ДЛЯ ИНДИКАЦИИ (S-метр, лампочки), не для тракта.
// В телеграфе FTransmitting не поднимается: передачей владеет кейер прошивки,
// и единственный признак эфира — HPS_PTT в статусе.
function RadioKeyed: Boolean;
function CWSettings: TCWSettings;
procedure SetCWSettings(const C: TCWSettings);
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
@@ -1495,11 +1507,16 @@ begin
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload;
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
// Любое касание обрывает программную передачу — как в Thetis/pihpsdr
// («key hit» останавливает CAT CW), иначе оператор не может вклиниться.
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
KeyDash := (Status.StatusBits and HPS_DASH) <> 0;
if (KeyDot or KeyDash) and CWXBusy then CWXAbort;
if ((KeyDot and not FCWPrevDot) or (KeyDash and not FCWPrevDash))
and CWXBusy then CWXAbort;
FCWPrevDot := KeyDot;
FCWPrevDash := KeyDash;
// Программный сайдтон от лепестков верен ТОЛЬКО у прямого ключа: при иамбике
// элементы считает FPGA, а PC видит лишь замыкание контакта.
if (FCWSettings.KeyerMode = 0) and (ActiveTXMode in [MODE_CWL, MODE_CWU])
@@ -1553,6 +1570,32 @@ begin
Changed(rfADCOverload);
end;
// ★Телеграф с кейером в прошивке: HPS_PTT здесь — ЭХО собственной манипуляции
// (при break-in железо само поднимает T/R на каждую посылку), а не действие
// оператора. Гонять по нему SetMOX нельзя: на каждой точке пересобирался бы
// передающий тракт, а программная передача обрывалась бы после ПЕРВОЙ посылки
// (снятие MOX гасит CWX). Флаг держим актуальным — им выбирается источник
// модуляции, — но передачей по нему не управляем.
if CWTXActive then
begin
FHWPTTActive := FHPpHWPTT;
if FHPpHWPTT then FCWKeyedAt := GetTickCount64;
// UI про телеграфную передачу иначе не узнает вовсе: SetMOX здесь не
// вызывается, а значит и rfTransmitting никто не шлёт. Фронт RadioKeyed
// (с выдержкой) — единственный сигнал «покрасить бейджи и полосы».
if FCWKeyedShown <> RadioKeyed then
begin
FCWKeyedShown := RadioKeyed;
Changed(rfTransmitting);
end;
Exit;
end;
// Ушли из телеграфа/сняли вооружение — вернуть индикацию в исходное.
if FCWKeyedShown then
begin
FCWKeyedShown := False;
Changed(rfTransmitting);
end;
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
// его включил именно HWPTT.
@@ -2291,6 +2334,10 @@ begin
FSlices[idx].TargetHz := TargetHz;
if Assigned(FDSPEngine) then
FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
// Слайс — источник передачи ⇒ он же задаёт частоту DUC. Раньше её дошлёт бы
// первый MOX, но в телеграфе ключ замыкает прошивка без всякого MOX: DUC
// обязан стоять правильно ВСЕГДА, а не только на передаче.
if FTxSliceId = Id then PushNetworkState;
end;
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
@@ -5628,6 +5675,23 @@ end;
// Телеграф
// ===========================================================================
function TRadioController.RadioKeyed: Boolean;
// Выдержка после последней посылки: точка на 20 WPM длится 60 мс, а индикаторы
// перерисовываются 10 раз в секунду — без неё бейджи и метр мигали бы внутри
// одного знака. Держим 8 точек, но не меньше 500 мс: на медленной скорости
// межсловный интервал (7 точек) сам по себе длиннее полусекунды, и фиксированная
// выдержка гасила бы индикацию между словами.
var Hold: QWord;
begin
Result := FTransmitting or FTuning;
if Result then Exit;
if not CWTXActive then Exit(False);
if FCWKeyedAt = 0 then Exit(False);
Hold := 8 * (1200 div EnsureRange(FCWSettings.Speed, 5, 60));
if Hold < 500 then Hold := 500;
Result := (GetTickCount64 - FCWKeyedAt) < Hold;
end;
function TRadioController.CWSettings: TCWSettings;
begin
Result := FCWSettings;
@@ -5645,10 +5709,14 @@ begin
FCWSettings.SidetoneHWLevel := EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127);
FCWSettings.SidetoneSWLevel := EnsureRange(FCWSettings.SidetoneSWLevel, 0, 100);
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
FDSPEngine.SetCWSpeed(FCWSettings.Speed); // ширина полосы CW на экране
end;
if Assigned(FCWSender) then
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
SendDUCSpecificFromSettings;
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
// если крутят pitch прямо на настройке — обновляем живьём.
if FTuning and FWDSPReady and Assigned(FDSPEngine)
@@ -5667,6 +5735,9 @@ procedure TRadioController.SyncCWKeyer;
// иначе прошивка поднимет PTT на замыкание ключа посреди SSB.
begin
if not (ActiveTXMode in [MODE_CWL, MODE_CWU]) then CWXAbort;
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче.
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings;
end;
@@ -6224,8 +6295,17 @@ begin
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
// Уровень пересчитываем здесь же: с вооружённым кейером он уедет в железо
// сразу, до всякой передачи, а после конструктора FDriveLevel ещё нулевой.
FDriveLevel := CalcDriveByte;
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel, False, True, True, FTuning, FTwoTone);
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
// ★DUC — на TX-ЧАСТОТУ, а не на центр дисплея. Раньше здесь стоял центр, и до
// первой перестройки VFO (или до первого MOX, который частоту дошлёт сам)
// передатчик стоял не там, где маркер: при CTUN центр и VFO — разные частоты.
// С кейером в прошивке это стало слышно в эфире: железо может замкнуть ключ
// сразу после старта, никакого MOX перед этим не будет.
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq),
XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
FRXLastPktTime := 0;
@@ -6251,8 +6331,12 @@ begin
// Тон телеграфа в движок: на момент ConnectDevice канал WDSP мог быть ещё
// не открыт, а здесь он гарантированно жив.
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
FDSPEngine.SetCWSpeed(FCWSettings.Speed);
end;
SendDUCSpecificFromSettings;
SyncCWKeyer; // вооружение кейера + уровень мощности в HP-кадре
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
ApplyModeFilter;
@@ -6280,7 +6364,8 @@ begin
for p := 1 to MAX_PANS - 1 do
RemovePanDDC(p);
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True, FTuning, FTwoTone);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq),
XvtrTranslateTX(ActiveTXFreqHz), 0);
FRunning := False;
FTransmitting := False;
// Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить
@@ -6465,7 +6550,10 @@ begin
FSettings.LoadTX(FDevMAC, FTXSettings);
FSettings.LoadCW(FDevMAC, FCWSettings);
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
FDSPEngine.SetCWSpeed(FCWSettings.Speed);
end;
FSettings.LoadAlex(FDevMAC, FAlexSettings);
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
FNetwork.SetAlexConfig(FAlexSettings);