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:
+121
@@ -28,6 +28,7 @@ const
|
||||
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
|
||||
CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
|
||||
CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
|
||||
CW_MSG_COUNT = 8; // ячеек памяти телеграфных сообщений (F1..F8)
|
||||
TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство
|
||||
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
||||
// (split-LO транспондер; настраивается на
|
||||
@@ -306,6 +307,39 @@ type
|
||||
// (Orion MkII → 5.0, иначе выкл.), 0 = выкл.
|
||||
end;
|
||||
|
||||
// ---- CW ------------------------------------------------------------------
|
||||
// Телеграф в openHPSDR P2 делает ПРОШИВКА: точки/тире формирует кейер в FPGA,
|
||||
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
|
||||
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
|
||||
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
|
||||
// Раскладка байта 5 — HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs)
|
||||
// и pihpsdr (new_protocol.c:1353).
|
||||
TCWSettings = record
|
||||
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
|
||||
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
||||
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
||||
FWKeyer: Boolean; // кейер в прошивке (иначе телеграф выключен)
|
||||
KeyerMode: Integer; // 0=Straight, 1=Iambic A, 2=Iambic B
|
||||
ReverseKeys: Boolean; // поменять точку и тире местами
|
||||
StrictSpacing: Boolean; // строгие межзнаковые интервалы
|
||||
Speed: Integer; // WPM, 5..60
|
||||
Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
|
||||
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
|
||||
HangTimeMS: Integer; // сколько держать TX после последнего элемента
|
||||
RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс)
|
||||
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
|
||||
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
|
||||
SidetoneHWLevel: Integer; // 0..127
|
||||
// Программный сайдтон — свой генератор в аудиотракте PC. Нужен тем, кто
|
||||
// слушает через звуковую карту: аппаратный тон туда физически не попадает
|
||||
// (он подмешивается в наушники трансивера, а не в IQ-поток).
|
||||
// ★Точен там, где момент нажатия знает САМ PC: программная передача текста
|
||||
// (CWX) и прямой ключ. При иамбике таймингом владеет FPGA, и PC видит лишь
|
||||
// состояние лепестков манипулятора — там верен только аппаратный тон.
|
||||
SidetoneSW: Boolean;
|
||||
SidetoneSWLevel: Integer; // 0..100 % от полной шкалы аудио
|
||||
end;
|
||||
|
||||
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
|
||||
// Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию
|
||||
// микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку,
|
||||
@@ -646,6 +680,10 @@ type
|
||||
// в любом случае T заполняется (дефолтами или сохранёнными значениями).
|
||||
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
|
||||
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings);
|
||||
// CW settings per-device (JSON-секция "cw" под MAC).
|
||||
class procedure DefaultCW(out C: TCWSettings);
|
||||
function LoadCW(const MAC: array of Byte; out C: TCWSettings): Boolean;
|
||||
procedure SaveCW(const MAC: array of Byte; const C: TCWSettings);
|
||||
class function MacToStr(const MAC: array of Byte): string;
|
||||
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||||
@@ -1733,6 +1771,89 @@ begin
|
||||
T.PSOutlierSigma := JD(O,'ps_outlier_sigma', T.PSOutlierSigma);
|
||||
end;
|
||||
|
||||
class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
|
||||
begin
|
||||
FillChar(C, SizeOf(C), 0);
|
||||
C.Pitch := 600; // Thetis cw_pitch default
|
||||
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
|
||||
C.KeyerMode := 2; // Iambic B
|
||||
C.ReverseKeys := False;
|
||||
C.StrictSpacing := False;
|
||||
C.Speed := 20;
|
||||
C.Weight := 50;
|
||||
C.BreakIn := True;
|
||||
C.HangTimeMS := 300;
|
||||
C.RampMS := 9; // Thetis SetCWEdgeLength(9)
|
||||
C.RFDelayMS := 0;
|
||||
C.SidetoneHW := True;
|
||||
C.SidetoneHWLevel := 50;
|
||||
C.SidetoneSW := False;
|
||||
C.SidetoneSWLevel := 30;
|
||||
// Заводские заготовки: типовой вызов, ответ и стандартные концовки.
|
||||
C.Messages[0] := 'CQ CQ DE ';
|
||||
C.Messages[1] := 'DE ';
|
||||
C.Messages[2] := 'UR RST 599 599 ';
|
||||
C.Messages[3] := 'TU 73 GL';
|
||||
C.Messages[4] := '?';
|
||||
C.Messages[5] := 'AGN';
|
||||
C.Messages[6] := 'QRZ?';
|
||||
C.Messages[7] := 'TEST';
|
||||
end;
|
||||
|
||||
function TSettingsManager.LoadCW(const MAC: array of Byte;
|
||||
out C: TCWSettings): Boolean;
|
||||
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
|
||||
begin
|
||||
DefaultCW(C);
|
||||
MacStr := MacToStr(MAC);
|
||||
Result := FRoot.Find(MacStr) <> nil;
|
||||
if not Result then Exit;
|
||||
DevObj := GetDevObj(MacStr);
|
||||
if DevObj.Find('cw') = nil then Exit; // нет секции — остаются дефолты
|
||||
O := EnsureObj(DevObj, 'cw');
|
||||
C.Pitch := EnsureRange(JI(O,'pitch', C.Pitch), 200, 1200);
|
||||
C.FWKeyer := JB(O,'fw_keyer', C.FWKeyer);
|
||||
C.KeyerMode := EnsureRange(JI(O,'keyer_mode', C.KeyerMode), 0, 2);
|
||||
C.ReverseKeys := JB(O,'reverse_keys', C.ReverseKeys);
|
||||
C.StrictSpacing := JB(O,'strict_spacing',C.StrictSpacing);
|
||||
C.Speed := EnsureRange(JI(O,'speed', C.Speed), 5, 60);
|
||||
C.Weight := EnsureRange(JI(O,'weight', C.Weight), 33, 66);
|
||||
C.BreakIn := JB(O,'break_in', C.BreakIn);
|
||||
C.HangTimeMS := EnsureRange(JI(O,'hang_time_ms', C.HangTimeMS), 0, 2000);
|
||||
C.RampMS := EnsureRange(JI(O,'ramp_ms', C.RampMS), 0, 20);
|
||||
C.RFDelayMS := EnsureRange(JI(O,'rf_delay_ms', C.RFDelayMS), 0, 255);
|
||||
C.SidetoneHW := JB(O,'sidetone_hw', C.SidetoneHW);
|
||||
C.SidetoneHWLevel := EnsureRange(JI(O,'sidetone_hw_level', C.SidetoneHWLevel), 0, 127);
|
||||
C.SidetoneSW := JB(O,'sidetone_sw', C.SidetoneSW);
|
||||
C.SidetoneSWLevel := EnsureRange(JI(O,'sidetone_sw_level', C.SidetoneSWLevel), 0, 100);
|
||||
for i := 0 to CW_MSG_COUNT - 1 do
|
||||
C.Messages[i] := Copy(JS(O, 'msg_' + IntToStr(i), C.Messages[i]), 1, 63);
|
||||
end;
|
||||
|
||||
procedure TSettingsManager.SaveCW(const MAC: array of Byte;
|
||||
const C: TCWSettings);
|
||||
var O: TJSONObject; i: Integer;
|
||||
begin
|
||||
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'cw');
|
||||
JW(O,'pitch', C.Pitch);
|
||||
JW(O,'fw_keyer', C.FWKeyer);
|
||||
JW(O,'keyer_mode', C.KeyerMode);
|
||||
JW(O,'reverse_keys', C.ReverseKeys);
|
||||
JW(O,'strict_spacing', C.StrictSpacing);
|
||||
JW(O,'speed', C.Speed);
|
||||
JW(O,'weight', C.Weight);
|
||||
JW(O,'break_in', C.BreakIn);
|
||||
JW(O,'hang_time_ms', C.HangTimeMS);
|
||||
JW(O,'ramp_ms', C.RampMS);
|
||||
JW(O,'rf_delay_ms', C.RFDelayMS);
|
||||
JW(O,'sidetone_hw', C.SidetoneHW);
|
||||
JW(O,'sidetone_hw_level', C.SidetoneHWLevel);
|
||||
JW(O,'sidetone_sw', C.SidetoneSW);
|
||||
JW(O,'sidetone_sw_level', C.SidetoneSWLevel);
|
||||
for i := 0 to CW_MSG_COUNT - 1 do
|
||||
JWS(O, 'msg_' + IntToStr(i), C.Messages[i]);
|
||||
end;
|
||||
|
||||
procedure TSettingsManager.SaveTX(const MAC: array of Byte;
|
||||
const T: TTXSettings);
|
||||
var O: TJSONObject; i: Integer;
|
||||
|
||||
Reference in New Issue
Block a user