feat(cw): терминал телеграфа — набор с клавиатуры и декодер приёма

Окно (ПКМ по CWL/CWU, F9, кнопка в настройках CW): одна лента, где принятое
декодером и СВОЯ передача идут вперемешку — своё акцентным цветом. Разделять их
нельзя: в QSK они перемежаются посреди фразы. Под лентой строка набора.

Печать уходит в эфир ПОСИМВОЛЬНО, а не по Enter: строка показывает ровно то,
что ещё не передано (очередь генератора), знаки уходят из неё по мере отправки,
Backspace стирает с хвоста очереди — то, что уже звучит, вернуть нельзя.
Ctrl работает манипулятором (левый точка, правый тире; у кейера прошивки
лепестков нет — там это прямой ключ через бит CWX), по умолчанию выключено,
чтобы Ctrl+C не уводил в эфир. Отпускание ключа ловится и на потере фокуса:
иначе уход из окна с зажатым Ctrl оставил бы несущую в эфире навсегда.

CWDecoder.pas: аудио → текст. Отвод берётся до громкости и мьюта — там нет
программного сайдтона, зато уже отработал узкий CW-фильтр, лучшего предетектора
не найти. Гёрцель гребёнкой из пяти бинов вокруг pitch (заодно показывает
расстройку) → огибающая → адаптивный порог с гистерезисом → длительности →
адаптивная точка → обратная таблица Морзе. Длительности живут в шагах анализа,
а не в показаниях часов: разбор идёт пачками с таймера, и привязка ко времени
вызова ломала бы тайминг на любой загрузке.

Вылезло на тестах и учтено: пик обязан клампиться не ниже пола шума (иначе на
старте порог уходит НИЖЕ шума и первым «знаком» читается собственный шум);
порог дребезга берётся от текущей точки, фиксированный либо пропускает щелчки
на медленной передаче, либо ест посылки на быстрой; длина посылки меряется за
вычетом подтверждения дребезга, иначе скорость занижалась на 15%; расстройка
запоминается только на полной амплитуде посылки, иначе индикатор пляшет.
Граница честная: при вдвое неверной подсказке скорости теряется первое слово —
пока не услышана настоящая точка, длина элемента неизвестна.

Лента расшифровки продублирована строкой под спектром (пан 0), эхо передачи
и очередь набора появились у обоих отправителей — и у локального генератора,
и у кейера прошивки.

★TFlatEdit получил публичный CaretPos: он вставляет знак сам в UTF8KeyPress и
гасит клавишу, поэтому OnKeyPress контрола не вызывается вовсе — из-за этого
набранное «исчезало», а в эфир не уходило. Ввод перенесён на уровень формы.

Проверено оффлайн: 12/20/40 WPM, расстройка, шум, слабый сигнал, цифры и знаки;
плюс сквозной прогон против шести станций CW-стенда в hpsdrsim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 21:28:48 +03:00
co-authored by Claude Opus 5
parent 25fbf10212
commit b02b68aa6a
11 changed files with 1755 additions and 20 deletions
+76 -1
View File
@@ -99,6 +99,11 @@ type
FDashIn: Boolean;
FStraightIn:Boolean; // прямой ключ отдельным источником
FTouched: Boolean; // касание манипулятора — обрывает текст
// Эхо передачи для окна-терминала: знаки, которые УЖЕ ушли в эфир, и хвост,
// который ещё стоит в очереди. Ведём под FLock, потому что читает их UI.
FSent: string;
FUnsent: string;
FTrimTail: Boolean; // Backspace съел знак из уже разобранного текста
// ---- рабочая копия (только поток генератора) ---------------------------
FRate: Integer;
FAmp: Double;
@@ -170,6 +175,12 @@ type
procedure SendText(const S: string);
procedure AbortText;
function Busy: Boolean;
// Терминал: знаки, ушедшие в эфир с прошлого опроса (эхо своей передачи).
function TakeSentText: string;
// Терминал: что ещё стоит в очереди (набранное вперёд).
function PendingText: string;
// Стереть последний НЕ ушедший знак (Backspace в окне набора).
function Backspace: Boolean;
// Вход манипулятора (поток опроса порта / GUI). Состояния лепестков сырые,
// Reverse применяет сам генератор.
@@ -357,6 +368,52 @@ begin
end;
end;
function TCWLocalKeyer.TakeSentText: string;
begin
FLock.Enter;
try
Result := FSent;
FSent := '';
finally
FLock.Leave;
end;
end;
function TCWLocalKeyer.PendingText: string;
begin
FLock.Enter;
try
Result := FUnsent + FPending;
finally
FLock.Leave;
end;
end;
function TCWLocalKeyer.Backspace: Boolean;
// Стираем с ХВОСТА очереди: то, что уже звучит в эфире, вернуть нельзя, а
// набранное вперёд — можно и нужно (иначе опечатка обязательно уедет).
begin
FLock.Enter;
try
Result := False;
if FPending <> '' then
begin
SetLength(FPending, Length(FPending) - 1);
Result := True;
end
else if FUnsent <> '' then
begin
// Хвост уже разобранного текста: помечаем к отсечению — генератор увидит
// укороченный FUnsent на следующем знаке (см. TakeTextElement).
SetLength(FUnsent, Length(FUnsent) - 1);
FTrimTail := True;
Result := True;
end;
finally
FLock.Leave;
end;
end;
procedure TCWLocalKeyer.Paddle(Dot, Dash: Boolean);
begin
FLock.Enter;
@@ -612,6 +669,8 @@ begin
FLock.Enter;
try
FBusyText := False;
FUnsent := '';
FTrimTail := False;
finally
FLock.Leave;
end;
@@ -625,7 +684,8 @@ function TCWLocalKeyer.TakeTextElement(out IsDot: Boolean): Boolean;
var
Ch: Char;
Add: string;
Drop: Boolean;
Drop, Trim: Boolean;
KeepLen: Integer;
begin
Result := False;
IsDot := True;
@@ -642,6 +702,9 @@ begin
end;
Add := FPending;
FPending := '';
Trim := FTrimTail;
FTrimTail := False;
KeepLen := FTextPos + Length(FUnsent);
finally
FLock.Leave;
end;
@@ -650,6 +713,8 @@ begin
ResetText;
Exit;
end;
// Backspace дотянулся до уже разобранного текста — отсекаем хвост.
if Trim and (Length(FText) > KeepLen) then SetLength(FText, KeepLen);
if Add <> '' then FText := FText + Add;
// Ищем следующий элемент, пропуская пробелы и знаки не из таблицы.
@@ -662,6 +727,16 @@ begin
end;
Inc(FTextPos);
Ch := FText[FTextPos];
// Эхо: знак пошёл в эфир — отдаём его окну-терминалу, а хвост очереди
// обновляем, чтобы строка набора показывала только ненабранное.
FLock.Enter;
try
FSent := FSent + Ch;
FUnsent := Copy(FText, FTextPos + 1, MaxInt);
if Length(FSent) > 4096 then FSent := Copy(FSent, Length(FSent) - 4095, 4096);
finally
FLock.Leave;
end;
if Ch = ' ' then
begin
// 3 точки уже отданы после прошлого знака — добираем до 7.