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:
+125
-1
@@ -43,7 +43,7 @@ uses
|
||||
WinFirewall,
|
||||
BoardUtils, WisdomBuilder, UISync,
|
||||
FlatEdit, FMRepeater,
|
||||
ChannelStore, ChannelsForm, CWMessagesForm, BeaconScopeForm, DMRDecoder,
|
||||
ChannelStore, ChannelsForm, CWMessagesForm, CWTerminalForm, BeaconScopeForm, DMRDecoder,
|
||||
PowerInhibit,
|
||||
DeviceStore,
|
||||
RadioBackend, PlutoBackend,
|
||||
@@ -368,6 +368,7 @@ type
|
||||
FTXProfileDropDown: TFlatDropDown;
|
||||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||
FCWMsgForm: TObject; // TCWMessagesForm (память телеграфа)
|
||||
FCWTermForm: TObject; // TCWTerminalForm (лента + набор с клавиатуры)
|
||||
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
||||
|
||||
// ---- Right panel ----
|
||||
@@ -473,6 +474,8 @@ type
|
||||
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||||
procedure BtnBandClick(Sender: TObject);
|
||||
procedure BtnModeClick(Sender: TObject);
|
||||
procedure BtnModeMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure BtnFilterClick(Sender: TObject);
|
||||
procedure BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
@@ -766,6 +769,14 @@ type
|
||||
procedure OnTXSettingsChange(const T: TTXSettings);
|
||||
procedure OnCWSettingsChange(const C: TCWSettings);
|
||||
procedure ShowCWMessages;
|
||||
procedure ShowCWTerminal;
|
||||
procedure ServiceCWTerminal; // таймер: разбор + обновление окна
|
||||
procedure OnCWLogAppend(const S: string; IsTX: Boolean);
|
||||
procedure OnCWTermBackspace(Sender: TObject);
|
||||
procedure OnCWTermDecoder(Value: Boolean);
|
||||
procedure OnCWTermKbdKey(Dot, Dash: Boolean);
|
||||
procedure OnCWTermSpeed(WPM: Integer);
|
||||
procedure OnCWTermMacro(Sender: TObject);
|
||||
procedure OnCWMessageSend(const Text: string);
|
||||
procedure OnCWMessageAbort(Sender: TObject);
|
||||
procedure FormKeyDownCW(Sender: TObject; var Key: Word; Shift: TShiftState);
|
||||
@@ -2423,6 +2434,7 @@ begin
|
||||
LeftPanelButtonWidth(LEFT_W, 4, i mod 4),
|
||||
BTN_SM, BtnModeClick);
|
||||
B.Tag := i;
|
||||
B.OnMouseDown := BtnModeMouseDown; // ПКМ по CWL/CWU → терминал
|
||||
BtnMode[i] := B;
|
||||
StyleButton(B, i = FController.FMode);
|
||||
end;
|
||||
@@ -3414,6 +3426,7 @@ begin
|
||||
if FDeviceDialog <> nil then FDeviceDialog.SetTheme(T);
|
||||
if FChannelsForm <> nil then TChannelsForm(FChannelsForm).ApplyTheme(T);
|
||||
if FCWMsgForm <> nil then TCWMessagesForm(FCWMsgForm).ApplyTheme(T);
|
||||
if FCWTermForm <> nil then TCWTerminalForm(FCWTermForm).ApplyTheme(T);
|
||||
if FBeaconScopeForm <> nil then FBeaconScopeForm.ApplyTheme(T);
|
||||
FController.FSettings.SaveTheme(V);
|
||||
FController.FSettings.Save;
|
||||
@@ -3691,6 +3704,8 @@ begin
|
||||
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет — гнать индикацию
|
||||
// и отпускать реле T/R после выдержки больше некому).
|
||||
FController.ServiceCWKeyed;
|
||||
// Телеграф: разбор принятого в текст + окно-терминал + лента под спектром.
|
||||
ServiceCWTerminal;
|
||||
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
|
||||
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
|
||||
FController.PureSignalTick;
|
||||
@@ -5584,6 +5599,17 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnModeMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по CWL/CWU открывает телеграфный терминал — по той же идиоме, что ПКМ по
|
||||
// кнопке фильтра открывает редактор фильтров. ЛКМ не трогаем: переключение
|
||||
// режима не должно тащить за собой окно.
|
||||
begin
|
||||
if Button <> mbRight then Exit;
|
||||
if not ((Sender as TFlatButton).Tag in [MODE_CWL, MODE_CWU]) then Exit;
|
||||
ShowCWTerminal;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
||||
@@ -8169,6 +8195,96 @@ begin
|
||||
TCWMessagesForm(FCWMsgForm).Show;
|
||||
end;
|
||||
|
||||
procedure TMainForm.ShowCWTerminal;
|
||||
begin
|
||||
if FCWTermForm = nil then
|
||||
begin
|
||||
FCWTermForm := TCWTerminalForm.Create(Self);
|
||||
TCWTerminalForm(FCWTermForm).OnSend := OnCWMessageSend;
|
||||
TCWTerminalForm(FCWTermForm).OnAbort := OnCWMessageAbort;
|
||||
TCWTerminalForm(FCWTermForm).OnBackspace := OnCWTermBackspace;
|
||||
TCWTerminalForm(FCWTermForm).OnDecoder := OnCWTermDecoder;
|
||||
TCWTerminalForm(FCWTermForm).OnKbdKey := OnCWTermKbdKey;
|
||||
TCWTerminalForm(FCWTermForm).OnSpeed := OnCWTermSpeed;
|
||||
TCWTerminalForm(FCWTermForm).OnMacro := OnCWTermMacro;
|
||||
// Лента собирается в контроллере (там же эхо своей передачи) и приходит
|
||||
// сюда прибавками — окно только рисует.
|
||||
FController.OnCWLog := OnCWLogAppend;
|
||||
end;
|
||||
TCWTerminalForm(FCWTermForm).LoadFrom(FController.CWSettings);
|
||||
TCWTerminalForm(FCWTermForm).ApplyTheme(CurrentAppTheme);
|
||||
TCWTerminalForm(FCWTermForm).Show;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWLogAppend(const S: string; IsTX: Boolean);
|
||||
begin
|
||||
if FCWTermForm <> nil then TCWTerminalForm(FCWTermForm).AppendLog(S, IsTX);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWTermBackspace(Sender: TObject);
|
||||
begin
|
||||
FController.CWXBackspace;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWTermDecoder(Value: Boolean);
|
||||
begin
|
||||
FController.SetCWDecoder(Value);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWTermKbdKey(Dot, Dash: Boolean);
|
||||
begin
|
||||
FController.CWKeyboardKey(Dot, Dash);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWTermSpeed(WPM: Integer);
|
||||
var C: TCWSettings;
|
||||
begin
|
||||
C := FController.CWSettings;
|
||||
if C.Speed = WPM then Exit;
|
||||
C.Speed := WPM;
|
||||
FController.SetCWSettings(C);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWTermMacro(Sender: TObject);
|
||||
var Slot: Integer;
|
||||
begin
|
||||
Slot := TComponent(Sender).Tag;
|
||||
if (Slot < 0) or (Slot >= CW_MSG_COUNT) then Exit;
|
||||
if FController.CWSettings.Messages[Slot] = '' then Exit;
|
||||
FController.CWXSend(FController.CWSettings.Messages[Slot]);
|
||||
end;
|
||||
|
||||
procedure TMainForm.ServiceCWTerminal;
|
||||
// Таймер: разбор принятого аудио + обновление окна и ленты под спектром.
|
||||
// Разбор идёт ВСЕГДА, когда декодер включён, даже если окно закрыто: лента под
|
||||
// спектром живёт сама по себе.
|
||||
var
|
||||
St: string;
|
||||
begin
|
||||
FController.ServiceCWDecoder;
|
||||
if FCWTermForm <> nil then
|
||||
begin
|
||||
TCWTerminalForm(FCWTermForm).SetPending(FController.CWXPendingText);
|
||||
if FController.CWDecoderOn then
|
||||
begin
|
||||
St := Format('RX %d WPM', [FController.CWDecSpeedWPM]);
|
||||
if FController.CWDecToneOffsetHz <> 0 then
|
||||
St := St + Format(', tone %+d Hz', [FController.CWDecToneOffsetHz]);
|
||||
if not FController.CWDecSignal then St := St + ', no signal';
|
||||
end
|
||||
else
|
||||
St := 'decoder off';
|
||||
// Набор молча уходит в никуда, если телеграф не вооружён (не CW, запрет
|
||||
// передачи на бэнде или в слоте трансвертера) — говорим об этом прямо.
|
||||
if not FController.CWTXActive then St := St + ' | TX not armed';
|
||||
TCWTerminalForm(FCWTermForm).SetStatus(St);
|
||||
end;
|
||||
if FPans[0] <> nil then
|
||||
if FPans[0].SetCWStrip(FController.CWSettings.DecoderStrip and FController.CWDecoderOn,
|
||||
FController.CWLogTail(300)) then
|
||||
ResizeSpectrumPanels; // строка появилась/пропала — переложить пан
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCWMessageSend(const Text: string);
|
||||
begin
|
||||
FController.CWXSend(Text);
|
||||
@@ -8188,6 +8304,13 @@ procedure TMainForm.FormKeyDownCW(Sender: TObject; var Key: Word;
|
||||
var Slot: Integer;
|
||||
begin
|
||||
if Shift <> [] then Exit;
|
||||
// F9 — окно телеграфного терминала (лента приёма + набор с клавиатуры).
|
||||
if Key = VK_F9 then
|
||||
begin
|
||||
Key := 0;
|
||||
ShowCWTerminal;
|
||||
Exit;
|
||||
end;
|
||||
if (Key < VK_F1) or (Key > VK_F8) then Exit;
|
||||
if not (FController.ActiveTXMode in [MODE_CWL, MODE_CWU]) then Exit;
|
||||
Slot := Key - VK_F1;
|
||||
@@ -9124,6 +9247,7 @@ begin
|
||||
SF.OnTXChange := OnTXSettingsChange;
|
||||
SF.OnCWChange := OnCWSettingsChange;
|
||||
SF.OnCWMessagesOpen := ShowCWMessages;
|
||||
SF.OnCWTerminalOpen := ShowCWTerminal;
|
||||
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
|
||||
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
|
||||
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
|
||||
|
||||
Reference in New Issue
Block a user