mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+194
-1
@@ -50,7 +50,8 @@ uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer;
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer,
|
||||
CWDecoder;
|
||||
|
||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||
@@ -103,6 +104,11 @@ type
|
||||
// Сырьё спектра/водопада наружу (фронтенды рисуют). Вызывается из DSP-потока.
|
||||
TPixelDataEvent = procedure(const Pixels: array of Single; Count: Integer) of object;
|
||||
|
||||
// Лента телеграфа для окна-терминала: прибавка текста и чья она — принятая
|
||||
// декодером или эхо собственной передачи (окно красит их по-разному).
|
||||
// Зовётся из потока хозяина (ServiceCWDecoder), не из DSP.
|
||||
TCWLogEvent = procedure(const S: string; IsTX: Boolean) of object;
|
||||
|
||||
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
|
||||
TRadioSnapshot = record
|
||||
VfoA, VfoB, CenterFreq, SpanHz: Double;
|
||||
@@ -470,6 +476,15 @@ type
|
||||
// Локальный кейер вооружён — считается в SyncCWKeyer, читается пейсингом
|
||||
// частоты DUC (CWCarrierOffsetHz) и аттенюатором Pluto.
|
||||
FCWLocalArmed: Boolean;
|
||||
// ---- Декодер приёма (CWDecoder.pas) --------------------------------------
|
||||
// Создаётся лениво вместе с окном-терминалом. Кормится из OnDemodAudioReady
|
||||
// (до громкости и мьюта, без сайдтона), разбирается на таймере хозяина.
|
||||
FCWDec: TCWDecoder;
|
||||
// Лента расшифровки: последние знаки приёма (для полосы под спектром) и
|
||||
// счётчик, по которому потребители понимают, что появилось новое.
|
||||
FCWLogText: string;
|
||||
FCWLogSeq: Int64;
|
||||
FOnCWLog: TCWLogEvent;
|
||||
FAlexSettings: TAlexSettings;
|
||||
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
|
||||
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
||||
@@ -1006,6 +1021,27 @@ type
|
||||
procedure CWXSend(const Text: string);
|
||||
procedure CWXAbort;
|
||||
function CWXBusy: Boolean;
|
||||
// ---- Окно-терминал -----------------------------------------------------
|
||||
// Стереть последний НЕ ушедший в эфир знак (Backspace при наборе).
|
||||
function CWXBackspace: Boolean;
|
||||
// Что ещё стоит в очереди передачи (строка набора показывает именно это).
|
||||
function CWXPendingText: string;
|
||||
// Ключ с клавиатуры: при иамбике — лепестки, при прямом ключе — уровень.
|
||||
// На кейере прошивки лепестков нет (их читает FPGA со своего разъёма),
|
||||
// поэтому там клавиатура работает через бит CWX, то есть прямым ключом.
|
||||
procedure CWKeyboardKey(Dot, Dash: Boolean);
|
||||
// Разбор принимаемого телеграфа: включение и обслуживание (таймер хозяина).
|
||||
procedure SetCWDecoder(On_: Boolean);
|
||||
function CWDecoderOn: Boolean;
|
||||
procedure ServiceCWDecoder;
|
||||
// Последние знаки ленты (для полосы под спектром) и счётчик изменений.
|
||||
function CWLogTail(MaxChars: Integer): string;
|
||||
property CWLogSeq: Int64 read FCWLogSeq;
|
||||
// Оценка скорости корреспондента и расстройка тона (индикация в терминале).
|
||||
function CWDecSpeedWPM: Integer;
|
||||
function CWDecToneOffsetHz: Integer;
|
||||
function CWDecSignal: Boolean;
|
||||
property OnCWLog: TCWLogEvent read FOnCWLog write FOnCWLog;
|
||||
|
||||
// ---- PureSignal ----
|
||||
// Доступность PS у активного бэкенда/платы (Caps.HasPureSignal).
|
||||
@@ -1345,7 +1381,11 @@ begin
|
||||
FCWSender.AbortSending;
|
||||
FreeAndNil(FCWSender);
|
||||
end;
|
||||
if Assigned(FCWDec) then FCWDec.Enabled := False; // кольцо больше не кормим
|
||||
FreeEngines;
|
||||
// Декодер — ПОСЛЕ движка: его кольцо наполняет DSP-поток, и освобождать
|
||||
// объект, пока поток жив, нельзя.
|
||||
if Assigned(FCWDec) then FreeAndNil(FCWDec);
|
||||
FreeAndNil(FDeviceStore);
|
||||
FreeAndNil(FSettings);
|
||||
inherited Destroy;
|
||||
@@ -1728,6 +1768,14 @@ begin
|
||||
end
|
||||
else if Assigned(FDMRDec) and FDMRDec.Enabled then
|
||||
FDMRDec.SetEnabled(False);
|
||||
// ★Телеграфный декодер кормим отсюда же: здесь аудио идёт ДО громкости и
|
||||
// мьюта и БЕЗ программного сайдтона (он подмешивается позже, в OnAudioReady),
|
||||
// зато уже после узкого CW-фильтра — лучшего предетектора не найти.
|
||||
// На своей передаче молчим: в телеграфе приёмник намеренно жив, и собственный
|
||||
// сигнал (особенно в полном дуплексе Pluto) забил бы ленту своей же работой.
|
||||
if Assigned(FCWDec) and FCWDec.Enabled
|
||||
and (FMode in [MODE_CWL, MODE_CWU]) and (not RadioKeyed) then
|
||||
FCWDec.FeedAudio(Left, Count);
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
|
||||
@@ -5902,6 +5950,19 @@ begin
|
||||
// запрет передачи, разбирает SyncCWKeyer — одна дверь на всё).
|
||||
PushCWLocalConfig;
|
||||
SyncCWKeyer;
|
||||
// Декодер слушает на том же pitch и стартует адаптацию со своей скорости.
|
||||
// Создаём лениво прямо здесь: настройки — единственная дверь, через которую
|
||||
// он включается (галка в настройках, кнопка в терминале, загрузка профиля).
|
||||
if FCWSettings.Decoder and (not Assigned(FCWDec)) then
|
||||
begin
|
||||
FCWDec := TCWDecoder.Create;
|
||||
FCWDec.SetSpeedHint(FCWSettings.Speed);
|
||||
end;
|
||||
if Assigned(FCWDec) then
|
||||
begin
|
||||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||||
FCWDec.Enabled := FCWSettings.Decoder;
|
||||
end;
|
||||
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
|
||||
// если крутят pitch прямо на настройке — обновляем живьём.
|
||||
if FTuning and FWDSPReady and Assigned(FDSPEngine)
|
||||
@@ -6136,6 +6197,126 @@ begin
|
||||
if Assigned(FCWLocal) then FCWLocal.AbortText;
|
||||
end;
|
||||
|
||||
function TRadioController.CWXBackspace: Boolean;
|
||||
begin
|
||||
if CWLocalSource then
|
||||
Result := Assigned(FCWLocal) and FCWLocal.Backspace
|
||||
else
|
||||
Result := Assigned(FCWSender) and FCWSender.Backspace;
|
||||
end;
|
||||
|
||||
function TRadioController.CWXPendingText: string;
|
||||
begin
|
||||
if CWLocalSource then
|
||||
begin
|
||||
if Assigned(FCWLocal) then Result := FCWLocal.PendingText else Result := '';
|
||||
end
|
||||
else
|
||||
begin
|
||||
if Assigned(FCWSender) then Result := FCWSender.PendingText else Result := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
|
||||
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
|
||||
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
|
||||
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
|
||||
// ключ (это же ограничение у Thetis).
|
||||
begin
|
||||
if not CWTXActive then
|
||||
begin
|
||||
// Разоружены (не CW, запрет передачи) — ключ обязан быть отпущен, иначе
|
||||
// несущая повиснет при выходе из режима с зажатой клавишей.
|
||||
if Assigned(FCWLocal) then FCWLocal.Paddle(False, False);
|
||||
CWXKeyEvent(False);
|
||||
Exit;
|
||||
end;
|
||||
if CWLocalSource then
|
||||
begin
|
||||
EnsureCWLocal;
|
||||
SyncCWKeyer;
|
||||
FCWLocal.Paddle(Dot, Dash);
|
||||
end
|
||||
else
|
||||
CWXKeyEvent(Dot or Dash);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetCWDecoder(On_: Boolean);
|
||||
begin
|
||||
FCWSettings.Decoder := On_;
|
||||
if On_ then
|
||||
begin
|
||||
if not Assigned(FCWDec) then FCWDec := TCWDecoder.Create;
|
||||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||||
FCWDec.SetSpeedHint(FCWSettings.Speed); // старт адаптации со своей скорости
|
||||
FCWDec.Reset;
|
||||
FCWDec.Enabled := True;
|
||||
end
|
||||
else if Assigned(FCWDec) then
|
||||
FCWDec.Enabled := False;
|
||||
if FDevConnected and Assigned(FSettings) then
|
||||
begin
|
||||
FSettings.SaveCW(FDevMAC, FCWSettings);
|
||||
FSettings.Save;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.CWDecoderOn: Boolean;
|
||||
begin
|
||||
Result := Assigned(FCWDec) and FCWDec.Enabled;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ServiceCWDecoder;
|
||||
// Поток хозяина (таймер GUI / цикл демона): разбор накопленного аудио и сборка
|
||||
// ленты. Здесь же эхо собственной передачи — чтобы в окне была цельная лента
|
||||
// связи, а не только принятое.
|
||||
var
|
||||
S: string;
|
||||
|
||||
procedure Append(const Txt: string; IsTX: Boolean);
|
||||
begin
|
||||
if Txt = '' then Exit;
|
||||
FCWLogText := FCWLogText + Txt;
|
||||
if Length(FCWLogText) > 8192 then
|
||||
FCWLogText := Copy(FCWLogText, Length(FCWLogText) - 8191, 8192);
|
||||
Inc(FCWLogSeq, Length(Txt));
|
||||
if Assigned(FOnCWLog) then FOnCWLog(Txt, IsTX);
|
||||
end;
|
||||
|
||||
begin
|
||||
// Эхо передачи забираем всегда: терминал показывает свою работу и при
|
||||
// выключенном декодере.
|
||||
if Assigned(FCWLocal) then Append(FCWLocal.TakeSentText, True);
|
||||
if Assigned(FCWSender) then Append(FCWSender.TakeSentText, True);
|
||||
if not Assigned(FCWDec) then Exit;
|
||||
if not FCWDec.Enabled then Exit;
|
||||
FCWDec.Process;
|
||||
S := FCWDec.TakeText;
|
||||
Append(S, False);
|
||||
end;
|
||||
|
||||
function TRadioController.CWLogTail(MaxChars: Integer): string;
|
||||
begin
|
||||
if MaxChars <= 0 then Exit('');
|
||||
if Length(FCWLogText) <= MaxChars then Result := FCWLogText
|
||||
else Result := Copy(FCWLogText, Length(FCWLogText) - MaxChars + 1, MaxChars);
|
||||
end;
|
||||
|
||||
function TRadioController.CWDecSpeedWPM: Integer;
|
||||
begin
|
||||
if Assigned(FCWDec) then Result := FCWDec.SpeedWPM else Result := 0;
|
||||
end;
|
||||
|
||||
function TRadioController.CWDecToneOffsetHz: Integer;
|
||||
begin
|
||||
if Assigned(FCWDec) then Result := FCWDec.ToneOffsetHz else Result := 0;
|
||||
end;
|
||||
|
||||
function TRadioController.CWDecSignal: Boolean;
|
||||
begin
|
||||
Result := Assigned(FCWDec) and FCWDec.SignalPresent;
|
||||
end;
|
||||
|
||||
function TRadioController.MixSidetone(const Left, Right: array of Single;
|
||||
Count: Integer): Boolean;
|
||||
// DSP-поток. Генератор синуса на pitch с трапецеидальной огибающей — без неё
|
||||
@@ -6943,6 +7124,18 @@ begin
|
||||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||||
FDSPEngine.SetCWSpeed(FCWSettings.Speed);
|
||||
end;
|
||||
// Декодер приёма — по настройке этого устройства (создаётся лениво).
|
||||
if FCWSettings.Decoder and (not Assigned(FCWDec)) then
|
||||
begin
|
||||
FCWDec := TCWDecoder.Create;
|
||||
FCWDec.SetSpeedHint(FCWSettings.Speed);
|
||||
end;
|
||||
if Assigned(FCWDec) then
|
||||
begin
|
||||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||||
FCWDec.Reset;
|
||||
FCWDec.Enabled := FCWSettings.Decoder;
|
||||
end;
|
||||
FSettings.LoadAlex(FDevMAC, FAlexSettings);
|
||||
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
|
||||
FNetwork.SetAlexConfig(FAlexSettings);
|
||||
|
||||
Reference in New Issue
Block a user