mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
feat(cw): программный кейер — телеграф на Pluto/LibreSDR
У openHPSDR точки и тире формирует кейер в FPGA, и это остаётся путём по умолчанию: тайминги в железе, джиттер PC не влияет вовсе. У AD936x такого кейера нет вовсе, поэтому телеграфа на Pluto не было в принципе — голосовой TXA в CW не запускается, а несущую давать некому. CWKeyer.pas: TCWLocalKeyer рисует манипулированную несущую прямо в поток IQ, мимо WDSP (эталон pihpsdr cw_shape_buffer: в WDSP режим TXA_CWL — ветка SSB, да и PostGen не умеет огибающей). Длительности живут В СЭМПЛАХ, а не в миллисекундах: планировщик ОС влияет только на темп выдачи блоков, а его сглаживает FIFO бэкенда. Умеет текст, иамбик A/B и прямой ключ; TCWKeyPort читает манипулятор с модемных линий COM/USB-serial (DTR/RTS питают ключ, CTS/DSR/DCD/RI — лепестки). Оффлайн-прогон: PARIS занимает ровно 43 точки, все посылки и паузы 1/3/7 dit с точностью до сэмпла. Сессия = одна передача, а не одна посылка: PTT, реле T/R, антенна, OC и аттенюатор Pluto поднимаются один раз и держатся до конца hang — внутри манипуляция чисто цифровая. Предзаполнение FIFO задано железом: TX-поток Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576k) и добивает нулями, чего не хватило. Сайдтон эту латентность не наследует — кольцо огибающей листается, если отставание больше 20 мс. Сдвиг несущей: у zero-IF ноль бейсбенда совпадает с частотой гетеродина, и там же его утечка — манипуляция на нуле дала бы backwave ровно на рабочей частоте. Поэтому несущая идёт на сдвиге, а гетеродин уезжает навстречу через новую единую дверь TXTuneFreqHz (все пуши TX-частоты переведены на неё). Знак (CW_TX_BB_SIGN) замерен на эфире, а не выведен. Развилка источника в UI — один список Keyer: firmware / software (PC) / off; хранится прежней парой флагов, и на железе без кейера выбор «прошивка» вырождается в программный, поэтому Pluto работает из коробки. Новое поле caps HasCWKeyer (раньше это неявно жило в HasHWMic). Попутно: у AD9361 calib_mode стоял manual_tx_quad, то есть драйвер не переигрывал калибровку TX quad (она нулит утечку гетеродина) на сменах TX LO — выставляем auto при подключении. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+122
-11
@@ -289,7 +289,9 @@ type
|
||||
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
||||
// CW (телеграф) — свойство устройства, профилем не переключается
|
||||
FCW: TCWSettings;
|
||||
FChkCWFWKeyer: TFlatCheckBox;
|
||||
// Кто рисует точки: прошивка / программа / никто. Одна развилка — один
|
||||
// список (две галки на неё разъезжались во взаимоисключающих состояниях).
|
||||
FCmbCWKeyerSrc: TFlatComboBox;
|
||||
FEdCWPitch: TFlatSpinEdit;
|
||||
FEdCWSpeed: TFlatSpinEdit;
|
||||
FEdCWWeight: TFlatSpinEdit;
|
||||
@@ -304,6 +306,16 @@ type
|
||||
FEdCWSidetoneHWLv: TFlatSpinEdit;
|
||||
FChkCWSidetoneSW: TFlatCheckBox;
|
||||
FEdCWSidetoneSWLv: TFlatSpinEdit;
|
||||
// Локальный кейер: манипуляцию рисует сам EWSDR (обязателен на AD936x)
|
||||
FEdCWOffset: TFlatSpinEdit;
|
||||
FChkCWKeyPort: TFlatCheckBox;
|
||||
FEdCWKeyPort: TFlatEdit;
|
||||
FCmbCWDotLine: TFlatComboBox;
|
||||
FCmbCWDashLine: TFlatComboBox;
|
||||
FChkCWKeyInvert: TFlatCheckBox;
|
||||
FChkCWKeyDTR: TFlatCheckBox;
|
||||
FChkCWKeyRTS: TFlatCheckBox;
|
||||
FCWHasFWKeyer: Boolean; // у железа есть кейер в прошивке
|
||||
// TX Display (отдельный analyzer)
|
||||
FCmbTXFFT: TFlatComboBox;
|
||||
FCmbTXWindow: TFlatComboBox;
|
||||
@@ -658,6 +670,8 @@ type
|
||||
TcpEnabled: Boolean; TcpPort: Integer);
|
||||
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
||||
procedure LoadCWSettings(const C: TCWSettings);
|
||||
// Есть ли у активного железа кейер в прошивке (openHPSDR — да, AD936x — нет).
|
||||
procedure SetCWBackend(HasFWKeyer: Boolean);
|
||||
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
||||
// главном окне, здесь показываем его значение как часть профиля).
|
||||
procedure LoadTXProfiles(const Names: array of string;
|
||||
@@ -2042,9 +2056,12 @@ begin
|
||||
|
||||
// ---- CW: телеграфом в openHPSDR P2 управляет прошивка -------------------
|
||||
Inc(Y, 190 + 12);
|
||||
Grp := MakeGroupPanel(FTXSubPage[2], 'CW (telegraphy)', MARGIN, Y, GRP_W, 320);
|
||||
Grp := MakeGroupPanel(FTXSubPage[2], 'CW (telegraphy)', MARGIN, Y, GRP_W, 460);
|
||||
|
||||
FChkCWFWKeyer := MkChkCW(Grp, PAD, R1 - 6, 250, 'Keyer in radio firmware');
|
||||
MakeLbl(Grp, 'Keyer', PAD, R1 - 1, 50);
|
||||
FCmbCWKeyerSrc := MkCmbCW(Grp, PAD + 54, R1 - 6, 194);
|
||||
FCmbCWKeyerSrc.Items.AddStrings(['Radio firmware','Software (PC)','Off (MOX only)']);
|
||||
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||
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);
|
||||
@@ -2076,13 +2093,46 @@ begin
|
||||
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);
|
||||
// ---- Программный кейер: манипуляцию рисует сам EWSDR --------------------
|
||||
MakeLbl(Grp, 'Carrier offset (Hz)', PAD, R1 + 5 * STEP + 5, LW);
|
||||
FEdCWOffset := MkSpinCW(Grp, CX, R1 + 5 * STEP, -1, 48000, 0);
|
||||
MakeLbl(Grp, '0 = auto, -1 = none (software keyer only)', PAD + 320, R1 + 5 * STEP + 5, 260);
|
||||
|
||||
Inc(Y, 320 + 12);
|
||||
FChkCWKeyPort := MkChkCW(Grp, PAD, R1 + 6 * STEP, 250, 'Paddle on serial port');
|
||||
FEdCWKeyPort := TFlatEdit.Create(Self);
|
||||
FEdCWKeyPort.Parent := Grp;
|
||||
FEdCWKeyPort.SetBounds(DpiScale(PAD + 260), DpiScale(R1 + 6 * STEP),
|
||||
DpiScale(220), DpiScale(BTN_H));
|
||||
FEdCWKeyPort.Color := CLR_INPUT;
|
||||
FEdCWKeyPort.Font.Color := CLR_INPUT_TEXT;
|
||||
FEdCWKeyPort.Font.Size := 9;
|
||||
FEdCWKeyPort.OnExit := OnCWAnyChange;
|
||||
MakeLbl(Grp, '/dev/ttyUSB0, COM3', PAD + 492, R1 + 6 * STEP + 5, 200);
|
||||
|
||||
MakeLbl(Grp, 'Dot line', PAD, R1 + 7 * STEP + 5, 70);
|
||||
FCmbCWDotLine := MkCmbCW(Grp, PAD + 74, R1 + 7 * STEP, 90);
|
||||
FCmbCWDotLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||
FCmbCWDotLine.ItemIndex := 0;
|
||||
MakeLbl(Grp, 'Dash line', PAD + 176, R1 + 7 * STEP + 5, 74);
|
||||
FCmbCWDashLine := MkCmbCW(Grp, PAD + 254, R1 + 7 * STEP, 90);
|
||||
FCmbCWDashLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||
FCmbCWDashLine.ItemIndex := 1;
|
||||
FChkCWKeyInvert := MkChkCW(Grp, PAD + 360, R1 + 7 * STEP, 90, 'Invert');
|
||||
FChkCWKeyDTR := MkChkCW(Grp, PAD + 456, R1 + 7 * STEP, 100, 'DTR power');
|
||||
FChkCWKeyRTS := MkChkCW(Grp, PAD + 562, R1 + 7 * STEP, 100, 'RTS power');
|
||||
|
||||
TxNote(Grp, 'Firmware keyer: the radio produces dot and dash timing, so PC '
|
||||
+ 'load never affects it — the default where the hardware has one, and the '
|
||||
+ 'paddle plugs into the radio. Software keyer: EWSDR draws the keyed '
|
||||
+ 'carrier into the IQ stream itself (the only option on Pluto/LibreSDR) '
|
||||
+ 'and the paddle comes in over a serial port; element lengths are counted '
|
||||
+ 'in samples, so only the paddle input jitters, never the rhythm. Carrier '
|
||||
+ 'offset 0 means auto: zero on openHPSDR, 12 kHz on zero-IF hardware, '
|
||||
+ 'where LO leakage would otherwise sit right on the working frequency '
|
||||
+ 'between elements. The voice transmit chain is not started in CW at all.',
|
||||
R1 + 8 * STEP - 6, 92);
|
||||
|
||||
Inc(Y, 460 + 12);
|
||||
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
|
||||
|
||||
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
|
||||
@@ -2271,8 +2321,21 @@ begin
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.CollectCWFromUI;
|
||||
var Src: Integer;
|
||||
begin
|
||||
if FChkCWFWKeyer <> nil then FCW.FWKeyer := FChkCWFWKeyer.Checked;
|
||||
// Список «кто рисует точки» → пара флагов. На железе без кейера в прошивке
|
||||
// первого пункта в списке нет вовсе, поэтому индексы сдвинуты на один.
|
||||
if FCmbCWKeyerSrc <> nil then
|
||||
begin
|
||||
Src := FCmbCWKeyerSrc.ItemIndex;
|
||||
if Src < 0 then Src := 0;
|
||||
if not FCWHasFWKeyer then Inc(Src);
|
||||
case Src of
|
||||
0: begin FCW.FWKeyer := True; FCW.LocalKeyer := False; end; // прошивка
|
||||
1: begin FCW.FWKeyer := True; FCW.LocalKeyer := True; end; // программа
|
||||
else begin FCW.FWKeyer := False; FCW.LocalKeyer := False; end; // выключен
|
||||
end;
|
||||
end;
|
||||
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;
|
||||
@@ -2287,6 +2350,36 @@ begin
|
||||
if FEdCWSidetoneHWLv <> nil then FCW.SidetoneHWLevel := FEdCWSidetoneHWLv.Value;
|
||||
if FChkCWSidetoneSW <> nil then FCW.SidetoneSW := FChkCWSidetoneSW.Checked;
|
||||
if FEdCWSidetoneSWLv <> nil then FCW.SidetoneSWLevel := FEdCWSidetoneSWLv.Value;
|
||||
if FEdCWOffset <> nil then FCW.CarrierOffsetHz := FEdCWOffset.Value;
|
||||
if FChkCWKeyPort <> nil then FCW.KeyPortEnabled := FChkCWKeyPort.Checked;
|
||||
if FEdCWKeyPort <> nil then FCW.KeyPort := Copy(FEdCWKeyPort.Text, 1, 63);
|
||||
if FCmbCWDotLine <> nil then FCW.KeyDotLine := EnsureRange(FCmbCWDotLine.ItemIndex, 0, 3);
|
||||
if FCmbCWDashLine <> nil then FCW.KeyDashLine := EnsureRange(FCmbCWDashLine.ItemIndex, 0, 3);
|
||||
if FChkCWKeyInvert <> nil then FCW.KeyInvert := FChkCWKeyInvert.Checked;
|
||||
if FChkCWKeyDTR <> nil then FCW.KeyPowerDTR := FChkCWKeyDTR.Checked;
|
||||
if FChkCWKeyRTS <> nil then FCW.KeyPowerRTS := FChkCWKeyRTS.Checked;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.SetCWBackend(HasFWKeyer: Boolean);
|
||||
// Гейт по железу: у AD936x кейера в прошивке нет вовсе, значит нет ни этого
|
||||
// пункта в списке, ни аппаратного сайдтона (он живёт в наушниках самого
|
||||
// трансивера) — телеграф там может быть только программным.
|
||||
// ★Обязан отработать ДО LoadCWSettings: тот считает индекс списка по FCWHasFWKeyer.
|
||||
begin
|
||||
FCWHasFWKeyer := HasFWKeyer;
|
||||
if FChkCWSidetoneHW <> nil then FChkCWSidetoneHW.Enabled := HasFWKeyer;
|
||||
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Enabled := HasFWKeyer;
|
||||
if FCmbCWKeyerSrc = nil then Exit;
|
||||
FLoading := True; // правка Items дёргает OnChange — настройки трогать нельзя
|
||||
try
|
||||
FCmbCWKeyerSrc.Items.Clear;
|
||||
if HasFWKeyer then FCmbCWKeyerSrc.Items.Add('Radio firmware');
|
||||
FCmbCWKeyerSrc.Items.Add('Software (PC)');
|
||||
FCmbCWKeyerSrc.Items.Add('Off (MOX only)');
|
||||
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||
finally
|
||||
FLoading := False;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.FireCWChange;
|
||||
@@ -2307,11 +2400,21 @@ begin
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.LoadCWSettings(const C: TCWSettings);
|
||||
var Src: Integer;
|
||||
begin
|
||||
FLoading := True;
|
||||
try
|
||||
FCW := C;
|
||||
if FChkCWFWKeyer <> nil then FChkCWFWKeyer.Checked := C.FWKeyer;
|
||||
if FCmbCWKeyerSrc <> nil then
|
||||
begin
|
||||
// Пара флагов → пункт списка. «Прошивка» на железе без кейера означает
|
||||
// программный кейер: другого пути там нет, и врать в списке незачем.
|
||||
if (not C.FWKeyer) and (not C.LocalKeyer) then Src := 2
|
||||
else if C.LocalKeyer or (not FCWHasFWKeyer) then Src := 1
|
||||
else Src := 0;
|
||||
if not FCWHasFWKeyer then Dec(Src);
|
||||
FCmbCWKeyerSrc.ItemIndex := EnsureRange(Src, 0, FCmbCWKeyerSrc.Items.Count - 1);
|
||||
end;
|
||||
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;
|
||||
@@ -2326,6 +2429,14 @@ begin
|
||||
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);
|
||||
if FEdCWOffset <> nil then FEdCWOffset.Value := EnsureRange(C.CarrierOffsetHz, -1, 48000);
|
||||
if FChkCWKeyPort <> nil then FChkCWKeyPort.Checked := C.KeyPortEnabled;
|
||||
if FEdCWKeyPort <> nil then FEdCWKeyPort.Text := C.KeyPort;
|
||||
if FCmbCWDotLine <> nil then FCmbCWDotLine.ItemIndex := EnsureRange(C.KeyDotLine, 0, 3);
|
||||
if FCmbCWDashLine <> nil then FCmbCWDashLine.ItemIndex := EnsureRange(C.KeyDashLine, 0, 3);
|
||||
if FChkCWKeyInvert <> nil then FChkCWKeyInvert.Checked := C.KeyInvert;
|
||||
if FChkCWKeyDTR <> nil then FChkCWKeyDTR.Checked := C.KeyPowerDTR;
|
||||
if FChkCWKeyRTS <> nil then FChkCWKeyRTS.Checked := C.KeyPowerRTS;
|
||||
finally
|
||||
FLoading := False;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user