mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
@@ -59,6 +59,8 @@ type
|
||||
TOnCalibrationChange = procedure(const C: TCalibration) of object;
|
||||
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
|
||||
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
|
||||
TOnCWSettingsChange = procedure(const C: TCWSettings) of object;
|
||||
TOnCWMessagesOpen = procedure of object;
|
||||
// TX-профили: выбор/переименование/удаление по индексу, создание — по имени.
|
||||
TOnTXProfileIdx = procedure(Idx: Integer) of object;
|
||||
TOnTXProfileName = procedure(const AName: string) of object;
|
||||
@@ -285,6 +287,23 @@ type
|
||||
// CTCSS
|
||||
FChkTXCTCSSOn: TFlatCheckBox;
|
||||
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
||||
// CW (телеграф) — свойство устройства, профилем не переключается
|
||||
FCW: TCWSettings;
|
||||
FChkCWFWKeyer: TFlatCheckBox;
|
||||
FEdCWPitch: TFlatSpinEdit;
|
||||
FEdCWSpeed: TFlatSpinEdit;
|
||||
FEdCWWeight: TFlatSpinEdit;
|
||||
FCmbCWKeyerMode: TFlatComboBox;
|
||||
FChkCWReverse: TFlatCheckBox;
|
||||
FChkCWStrict: TFlatCheckBox;
|
||||
FChkCWBreakIn: TFlatCheckBox;
|
||||
FEdCWHang: TFlatSpinEdit;
|
||||
FEdCWRamp: TFlatSpinEdit;
|
||||
FEdCWRFDelay: TFlatSpinEdit;
|
||||
FChkCWSidetoneHW: TFlatCheckBox;
|
||||
FEdCWSidetoneHWLv: TFlatSpinEdit;
|
||||
FChkCWSidetoneSW: TFlatCheckBox;
|
||||
FEdCWSidetoneSWLv: TFlatSpinEdit;
|
||||
// TX Display (отдельный analyzer)
|
||||
FCmbTXFFT: TFlatComboBox;
|
||||
FCmbTXWindow: TFlatComboBox;
|
||||
@@ -446,6 +465,8 @@ type
|
||||
FOnWfAGCNFChange: TOnWfAGCNFChange;
|
||||
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
|
||||
FOnTXChange: TOnTXSettingsChange;
|
||||
FOnCWChange: TOnCWSettingsChange;
|
||||
FOnCWMessagesOpen: TOnCWMessagesOpen;
|
||||
FOnTXProfileSelect: TOnTXProfileIdx;
|
||||
FOnTXProfileAdd: TOnTXProfileName;
|
||||
FOnTXProfileRename: TOnTXProfileIdxName;
|
||||
@@ -500,6 +521,10 @@ type
|
||||
procedure CollectTXFromUI;
|
||||
procedure FireTXChange;
|
||||
procedure OnTXAnyChange(Sender: TObject);
|
||||
procedure CollectCWFromUI;
|
||||
procedure FireCWChange;
|
||||
procedure OnCWAnyChange(Sender: TObject);
|
||||
procedure BtnCWMessagesClick(Sender: TObject);
|
||||
procedure OnMicJackChange(Sender: TObject);
|
||||
procedure UpdateMicJackVisibility;
|
||||
// Подвкладки и полоса профиля на странице Transmit
|
||||
@@ -632,6 +657,7 @@ type
|
||||
const SerAndromeda: array of Boolean;
|
||||
TcpEnabled: Boolean; TcpPort: Integer);
|
||||
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
||||
procedure LoadCWSettings(const C: TCWSettings);
|
||||
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
||||
// главном окне, здесь показываем его значение как часть профиля).
|
||||
procedure LoadTXProfiles(const Names: array of string;
|
||||
@@ -678,6 +704,8 @@ type
|
||||
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
|
||||
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
|
||||
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
|
||||
property OnCWChange: TOnCWSettingsChange read FOnCWChange write FOnCWChange;
|
||||
property OnCWMessagesOpen: TOnCWMessagesOpen read FOnCWMessagesOpen write FOnCWMessagesOpen;
|
||||
property OnTXProfileSelect: TOnTXProfileIdx read FOnTXProfileSelect write FOnTXProfileSelect;
|
||||
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
|
||||
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
|
||||
@@ -1795,6 +1823,27 @@ var
|
||||
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
|
||||
end;
|
||||
|
||||
// Телеграф едет отдельной записью (TCWSettings) и своим событием — иначе
|
||||
// правка скорости кейера дёргала бы пересборку всей TX-цепи WDSP.
|
||||
function MkSpinCW(AParent: TWinControl; AX, AY: Integer;
|
||||
AMin, AMax, AVal: Integer): TFlatSpinEdit;
|
||||
begin
|
||||
Result := MkSpin(AParent, AX, AY, AMin, AMax, AVal);
|
||||
Result.OnChange := OnCWAnyChange;
|
||||
end;
|
||||
|
||||
function MkChkCW(AParent: TWinControl; AX, AY, AW: Integer;
|
||||
const ACap: string): TFlatCheckBox;
|
||||
begin
|
||||
Result := MkChk(AParent, AX, AY, AW, ACap);
|
||||
Result.OnChange := OnCWAnyChange;
|
||||
end;
|
||||
|
||||
function MkCmbCW(AParent: TWinControl; AX, AY, AW: Integer): TFlatComboBox;
|
||||
begin
|
||||
Result := MakeCombo(AParent, AX, AY, AW, OnCWAnyChange);
|
||||
end;
|
||||
|
||||
procedure TxNote(AParent: TWinControl; const AText: string;
|
||||
ATop, AHeight: Integer);
|
||||
var
|
||||
@@ -1991,7 +2040,49 @@ begin
|
||||
TxNote(Grp, 'Skirt steepness and path latency, not timbre — hence outside '
|
||||
+ 'the profile.', R1 + 3 * STEP - 2, 22);
|
||||
|
||||
// ---- CW: телеграфом в openHPSDR P2 управляет прошивка -------------------
|
||||
Inc(Y, 190 + 12);
|
||||
Grp := MakeGroupPanel(FTXSubPage[2], 'CW (telegraphy)', MARGIN, Y, GRP_W, 320);
|
||||
|
||||
FChkCWFWKeyer := MkChkCW(Grp, PAD, R1 - 6, 250, 'Keyer in radio firmware');
|
||||
MkBtn(Grp, PAD + 260, R1 - 6, 110, 'Messages...', BtnCWMessagesClick);
|
||||
MakeLbl(Grp, 'Pitch (Hz)', PAD + 400, R1 - 1, 90);
|
||||
FEdCWPitch := MkSpinCW(Grp, PAD + 500, R1 - 6, 200, 1200, 600);
|
||||
|
||||
MakeLbl(Grp, 'Keyer mode', PAD, R1 + STEP + 5, LW);
|
||||
FCmbCWKeyerMode := MkCmbCW(Grp, CX, R1 + STEP, 200);
|
||||
FCmbCWKeyerMode.Items.AddStrings(['Straight key','Iambic A','Iambic B']);
|
||||
FCmbCWKeyerMode.ItemIndex := 2;
|
||||
FChkCWReverse := MkChkCW(Grp, PAD + 400, R1 + STEP, 150, 'Reverse paddles');
|
||||
FChkCWStrict := MkChkCW(Grp, PAD + 560, R1 + STEP, 160, 'Strict spacing');
|
||||
|
||||
MakeLbl(Grp, 'Speed (WPM)', PAD, R1 + 2 * STEP + 5, LW);
|
||||
FEdCWSpeed := MkSpinCW(Grp, CX, R1 + 2 * STEP, 5, 60, 20);
|
||||
MakeLbl(Grp, 'Weight', PAD + 320, R1 + 2 * STEP + 5, 70);
|
||||
FEdCWWeight := MkSpinCW(Grp, PAD + 400, R1 + 2 * STEP, 33, 66, 50);
|
||||
MakeLbl(Grp, 'Edge (ms)', PAD + 520, R1 + 2 * STEP + 5, 80);
|
||||
FEdCWRamp := MkSpinCW(Grp, PAD + 604, R1 + 2 * STEP, 0, 20, 9);
|
||||
|
||||
FChkCWBreakIn := MkChkCW(Grp, PAD, R1 + 3 * STEP, 150, 'Break-in');
|
||||
MakeLbl(Grp, 'Hang (ms)', PAD + 320, R1 + 3 * STEP + 5, 80);
|
||||
FEdCWHang := MkSpinCW(Grp, PAD + 400, R1 + 3 * STEP, 0, 2000, 300);
|
||||
MakeLbl(Grp, 'RF delay', PAD + 520, R1 + 3 * STEP + 5, 80);
|
||||
FEdCWRFDelay := MkSpinCW(Grp, PAD + 604, R1 + 3 * STEP, 0, 255, 0);
|
||||
|
||||
FChkCWSidetoneHW := MkChkCW(Grp, PAD, R1 + 4 * STEP, 190, 'Sidetone in radio');
|
||||
MakeLbl(Grp, 'Level', PAD + 196, R1 + 4 * STEP + 5, 44);
|
||||
FEdCWSidetoneHWLv := MkSpinCW(Grp, PAD + 244, R1 + 4 * STEP, 0, 127, 50);
|
||||
FChkCWSidetoneSW := MkChkCW(Grp, PAD + 362, R1 + 4 * STEP, 200, 'Sidetone in PC audio');
|
||||
MakeLbl(Grp, 'Level', PAD + 568, R1 + 4 * STEP + 5, 44);
|
||||
FEdCWSidetoneSWLv := MkSpinCW(Grp, PAD + 616, R1 + 4 * STEP, 0, 100, 30);
|
||||
|
||||
TxNote(Grp, 'Dot and dash timing is produced by the radio, so PC load never '
|
||||
+ 'affects it. The voice transmit chain is not started in CW at all. '
|
||||
+ 'The radio sidetone is heard in the radio headphones; the PC sidetone '
|
||||
+ 'is exact for keyboard sending and a straight key, but not for a paddle '
|
||||
+ '— with iambic the firmware owns the timing.', R1 + 5 * STEP - 6, 60);
|
||||
|
||||
Inc(Y, 320 + 12);
|
||||
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
|
||||
|
||||
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
|
||||
@@ -2179,6 +2270,67 @@ begin
|
||||
FireTXChange;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.CollectCWFromUI;
|
||||
begin
|
||||
if FChkCWFWKeyer <> nil then FCW.FWKeyer := FChkCWFWKeyer.Checked;
|
||||
if FEdCWPitch <> nil then FCW.Pitch := FEdCWPitch.Value;
|
||||
if FCmbCWKeyerMode <> nil then FCW.KeyerMode := EnsureRange(FCmbCWKeyerMode.ItemIndex, 0, 2);
|
||||
if FChkCWReverse <> nil then FCW.ReverseKeys := FChkCWReverse.Checked;
|
||||
if FChkCWStrict <> nil then FCW.StrictSpacing := FChkCWStrict.Checked;
|
||||
if FEdCWSpeed <> nil then FCW.Speed := FEdCWSpeed.Value;
|
||||
if FEdCWWeight <> nil then FCW.Weight := FEdCWWeight.Value;
|
||||
if FChkCWBreakIn <> nil then FCW.BreakIn := FChkCWBreakIn.Checked;
|
||||
if FEdCWHang <> nil then FCW.HangTimeMS := FEdCWHang.Value;
|
||||
if FEdCWRamp <> nil then FCW.RampMS := FEdCWRamp.Value;
|
||||
if FEdCWRFDelay <> nil then FCW.RFDelayMS := FEdCWRFDelay.Value;
|
||||
if FChkCWSidetoneHW <> nil then FCW.SidetoneHW := FChkCWSidetoneHW.Checked;
|
||||
if FEdCWSidetoneHWLv <> nil then FCW.SidetoneHWLevel := FEdCWSidetoneHWLv.Value;
|
||||
if FChkCWSidetoneSW <> nil then FCW.SidetoneSW := FChkCWSidetoneSW.Checked;
|
||||
if FEdCWSidetoneSWLv <> nil then FCW.SidetoneSWLevel := FEdCWSidetoneSWLv.Value;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireCWChange;
|
||||
begin
|
||||
if FLoading then Exit;
|
||||
CollectCWFromUI;
|
||||
if Assigned(FOnCWChange) then FOnCWChange(FCW);
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.OnCWAnyChange(Sender: TObject);
|
||||
begin
|
||||
FireCWChange;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.BtnCWMessagesClick(Sender: TObject);
|
||||
begin
|
||||
if Assigned(FOnCWMessagesOpen) then FOnCWMessagesOpen;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.LoadCWSettings(const C: TCWSettings);
|
||||
begin
|
||||
FLoading := True;
|
||||
try
|
||||
FCW := C;
|
||||
if FChkCWFWKeyer <> nil then FChkCWFWKeyer.Checked := C.FWKeyer;
|
||||
if FEdCWPitch <> nil then FEdCWPitch.Value := EnsureRange(C.Pitch, 200, 1200);
|
||||
if FCmbCWKeyerMode <> nil then FCmbCWKeyerMode.ItemIndex := EnsureRange(C.KeyerMode, 0, 2);
|
||||
if FChkCWReverse <> nil then FChkCWReverse.Checked := C.ReverseKeys;
|
||||
if FChkCWStrict <> nil then FChkCWStrict.Checked := C.StrictSpacing;
|
||||
if FEdCWSpeed <> nil then FEdCWSpeed.Value := EnsureRange(C.Speed, 5, 60);
|
||||
if FEdCWWeight <> nil then FEdCWWeight.Value := EnsureRange(C.Weight, 33, 66);
|
||||
if FChkCWBreakIn <> nil then FChkCWBreakIn.Checked := C.BreakIn;
|
||||
if FEdCWHang <> nil then FEdCWHang.Value := EnsureRange(C.HangTimeMS, 0, 2000);
|
||||
if FEdCWRamp <> nil then FEdCWRamp.Value := EnsureRange(C.RampMS, 0, 20);
|
||||
if FEdCWRFDelay <> nil then FEdCWRFDelay.Value := EnsureRange(C.RFDelayMS, 0, 255);
|
||||
if FChkCWSidetoneHW <> nil then FChkCWSidetoneHW.Checked := C.SidetoneHW;
|
||||
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Value := EnsureRange(C.SidetoneHWLevel, 0, 127);
|
||||
if FChkCWSidetoneSW <> nil then FChkCWSidetoneSW.Checked := C.SidetoneSW;
|
||||
if FEdCWSidetoneSWLv <> nil then FEdCWSidetoneSWLv.Value := EnsureRange(C.SidetoneSWLevel, 0, 100);
|
||||
finally
|
||||
FLoading := False;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.UpdateMicJackVisibility;
|
||||
var LineIn: Boolean;
|
||||
begin
|
||||
|
||||
Reference in New Issue
Block a user