From 53329e82e877d54879024d0d6bdb1c778ef26f3d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 10 Aug 2026 12:20:56 +0300 Subject: [PATCH] =?UTF-8?q?feat(cw):=20=D1=82=D0=B5=D0=BB=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=84=20=E2=80=94=20FPGA-=D0=BA=D0=B5=D0=B9=D0=B5?= =?UTF-8?q?=D1=80,=20pitch,=20=D1=81=D0=B0=D0=B9=D0=B4=D1=82=D0=BE=D0=BD,?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B0=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CATAdapter.pas | 58 ++++++++ CATEngine.pas | 66 +++++++-- CWMessagesForm.pas | 231 ++++++++++++++++++++++++++++++ CWMorse.pas | 297 +++++++++++++++++++++++++++++++++++++++ HPSDRNetwork.pas | 14 ++ MainForm.pas | 67 ++++++++- RadioBackend.pas | 8 ++ RadioController.pas | 333 +++++++++++++++++++++++++++++++++++++++++--- Settings.pas | 121 ++++++++++++++++ SettingsForm.pas | 152 ++++++++++++++++++++ WDSPEngine.pas | 112 +++++++++++++-- ewsdr.lpi | 8 ++ 12 files changed, 1421 insertions(+), 46 deletions(-) create mode 100644 CWMessagesForm.pas create mode 100644 CWMorse.pas diff --git a/CATAdapter.pas b/CATAdapter.pas index 1882c42..7d5658f 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -38,6 +38,7 @@ type FSyncFreq: Double; FSyncInt: Integer; FSyncBool: Boolean; + FSyncStr: string; // ── Геттеры (CAT-поток, read-only) ── function GetVfoA: Double; @@ -86,6 +87,15 @@ type procedure SetSNB(V: Boolean); procedure SetANF(V: Boolean); procedure SetTransmitting(V: Boolean); + function GetCWSpeed: Integer; + procedure SetCWSpeed(V: Integer); + function GetCWKeyerMode: Integer; + procedure SetCWKeyerMode(V: Integer); + procedure SendCWText(const S: string); + function GetCWTextBusy: Boolean; + procedure SyncCWSpeed; + procedure SyncCWKeyerMode; + procedure SyncCWText; procedure DoBandUp; procedure DoBandDown; procedure DoTuneUp; @@ -217,6 +227,13 @@ begin Ctx.SetAGCTop := @SetAGCTop; Ctx.GetCTun := @GetCTun; Ctx.SetCTun := @SetCTun; + // Телеграф: скорость/режим кейера и программная передача текста (KY/ZZKY). + Ctx.GetCWSpeed := @GetCWSpeed; + Ctx.SetCWSpeed := @SetCWSpeed; + Ctx.GetCWKeyerMode := @GetCWKeyerMode; + Ctx.SetCWKeyerMode := @SetCWKeyerMode; + Ctx.SendCWText := @SendCWText; + Ctx.CWTextBusy := @GetCWTextBusy; Ctx.OnPanelVfoStep := @PanelVfoStep; Ctx.OnPanelEncoder := @PanelEncoder; Ctx.OnPanelButton := @PanelButton; @@ -418,6 +435,47 @@ begin FController.SetANF(FSyncBool); end; procedure TCATAdapter.SetTransmitting(V: Boolean); begin FSyncBool := V; FController.Invoke(@SyncMOX); end; +// ---- Телеграф (KS/KY/ZZKM/ZZKS/ZZKY) -------------------------------------- +// Правки настроек и запуск передачи маршалим в поток контроллера, как всё +// остальное в адаптере: SetCWSettings трогает движок и пишет конфиг. + +function TCATAdapter.GetCWSpeed: Integer; +begin Result := FController.CWSettings.Speed; end; + +procedure TCATAdapter.SetCWSpeed(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncCWSpeed); end; + +procedure TCATAdapter.SyncCWSpeed; +var C: TCWSettings; +begin + C := FController.CWSettings; + C.Speed := FSyncInt; + FController.SetCWSettings(C); +end; + +function TCATAdapter.GetCWKeyerMode: Integer; +begin Result := FController.CWSettings.KeyerMode; end; + +procedure TCATAdapter.SetCWKeyerMode(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncCWKeyerMode); end; + +procedure TCATAdapter.SyncCWKeyerMode; +var C: TCWSettings; +begin + C := FController.CWSettings; + C.KeyerMode := FSyncInt; + FController.SetCWSettings(C); +end; + +procedure TCATAdapter.SendCWText(const S: string); +begin FSyncStr := S; FController.Invoke(@SyncCWText); end; + +procedure TCATAdapter.SyncCWText; +begin FController.CWXSend(FSyncStr); end; + +function TCATAdapter.GetCWTextBusy: Boolean; +begin Result := FController.CWXBusy; end; + procedure TCATAdapter.SyncMOX; begin FController.SetMOX(FSyncBool); end; diff --git a/CATEngine.pas b/CATEngine.pas index 7993e69..b2f2a0b 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -112,6 +112,14 @@ type GetFilterHigh: TCATGetIntFunc; SetFilterLow: TCATSetIntProc; SetFilterHigh: TCATSetIntProc; + // Телеграф (KS/KY/ZZKS/ZZKY/ZZKM). Скорость — WPM 5..60; SendCWText ставит + // текст в очередь программной передачи, CWTextBusy = «очередь занята». + GetCWSpeed: TCATGetIntFunc; + SetCWSpeed: TCATSetIntProc; + GetCWKeyerMode: TCATGetIntFunc; // 0=прямой ключ, 1=iambic A, 2=iambic B + SetCWKeyerMode: TCATSetIntProc; + SendCWText: procedure(const S: string) of object; + CWTextBusy: TCATGetBoolFunc; // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS) OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг @@ -136,6 +144,7 @@ type function SafeGetVfoA: Double; function SafeGetVfoB: Double; function SafeGetMode: Integer; + function SafeGetCWSpeed: Integer; function SafeGetVolume: Integer; function SafeGetDriveLevel: Integer; function SafeGetFilterIdx: Integer; @@ -812,21 +821,43 @@ begin Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000'; end; +function TCATEngine.SafeGetCWSpeed: Integer; +begin + if Assigned(FCtx.GetCWSpeed) then Result := FCtx.GetCWSpeed else Result := 20; +end; + function TCATEngine.CmdKS(const s: string): string; -// KS: CW keying speed (3 digits, WPM) — STUB: no CW keyer +// KS: CW keying speed (3 digits, WPM). +var v: Integer; begin if Length(s) = 3 then - Result := '' // accepted but ignored + begin + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetCWSpeed) then FCtx.SetCWSpeed(v); + Result := ''; + end else if Length(s) = 0 then - Result := '025' // default 25 WPM + Result := Format('%.3d', [SafeGetCWSpeed]) else Result := CAT_ERROR; end; function TCATEngine.CmdKY(const s: string): string; -// KY: send CW text — STUB: no CW keyer implemented +// KY: передать текст телеграфом. Kenwood-семантика: первый символ — пробел- +// заполнитель признака «не занято», далее сам текст; пустой аргумент = опрос +// занятости ('1' — очередь занята, логгер ждёт). +var Txt: string; begin - Result := '0'; // queue not full + if Length(s) = 0 then + begin + if Assigned(FCtx.CWTextBusy) and FCtx.CWTextBusy then Result := '1' + else Result := '0'; + Exit; + end; + Txt := s; + if (Length(Txt) > 0) and (Txt[1] = ' ') then Delete(Txt, 1, 1); + if Assigned(FCtx.SendCWText) then FCtx.SendCWText(Txt); + Result := ''; end; function TCATEngine.CmdMD(const s: string): string; @@ -1754,23 +1785,34 @@ begin end; function TCATEngine.ZZKM(const s: string): string; -// ZZKM: CW key mode — STUB: no CW keyer +// ZZKM: режим кейера (0=прямой ключ, 1=iambic A, 2=iambic B). +var v: Integer; begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + if Length(s) = 1 then + begin + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if (v < 0) or (v > 2) then Exit(CAT_ERROR); + if Assigned(FCtx.SetCWKeyerMode) then FCtx.SetCWKeyerMode(v); + Result := ''; + end + else if Length(s) = 0 then + begin + if Assigned(FCtx.GetCWKeyerMode) then Result := IntToStr(FCtx.GetCWKeyerMode) + else Result := '0'; + end else Result := CAT_ERROR; end; function TCATEngine.ZZKS(const s: string): string; -// ZZKS: CW speed (WPM) — STUB: no CW keyer +// ZZKS: скорость кейера, WPM. begin - if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025' - else Result := CAT_ERROR; + Result := CmdKS(s); end; function TCATEngine.ZZKY(const s: string): string; -// ZZKY: CW text to send — STUB: no CW keyer implemented +// ZZKY: передать текст телеграфом (то же, что KY). begin - Result := '0'; // buffer not full + Result := CmdKY(s); end; function TCATEngine.ZZMA(const s: string): string; diff --git a/CWMessagesForm.pas b/CWMessagesForm.pas new file mode 100644 index 0000000..0485687 --- /dev/null +++ b/CWMessagesForm.pas @@ -0,0 +1,231 @@ +unit CWMessagesForm; + +{ TCWMessagesForm — память телеграфных сообщений (CW_MSG_COUNT ячеек). + + Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки + «отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой + панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV), + так что окно висит рядом — ровно как окно CWX у Thetis. Те же ячейки + привязаны к F1..F8 в главном окне. + + Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить» + нет), через тот же OnChanged, что и вкладка настроек. } + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Math, + Forms, Controls, Graphics, StdCtrls, ExtCtrls, + FlatButton, FlatEdit, AppTheme, Settings, DpiUtils; + +type + TCWSendEvent = procedure(const Text: string) of object; + TCWMsgsChanged = procedure(const C: TCWSettings) of object; + + TCWMessagesForm = class(TForm) + private + FCW: TCWSettings; + FTheme: TAppTheme; + FUpdating: Boolean; + FLbls: array[0..CW_MSG_COUNT-1] of TLabel; + FEdits: array[0..CW_MSG_COUNT-1] of TFlatEdit; + FSendBtns: array[0..CW_MSG_COUNT-1] of TFlatButton; + FBtnStop: TFlatButton; + FBtnClose: TFlatButton; + FHint: TLabel; + FOnSend: TCWSendEvent; + FOnAbort: TNotifyEvent; + FOnChanged: TCWMsgsChanged; + procedure BuildUI; + procedure EditChange(Sender: TObject); + procedure SendClick(Sender: TObject); + procedure StopClick(Sender: TObject); + procedure CloseClick(Sender: TObject); + procedure StyleBtn(B: TFlatButton); + public + constructor Create(AOwner: TComponent); override; + procedure LoadFrom(const C: TCWSettings); + procedure ApplyTheme(const T: TAppTheme); + // Отправить ячейку по индексу (F1..F8 из главного окна). + procedure SendSlot(Idx: Integer); + property OnSend: TCWSendEvent read FOnSend write FOnSend; + property OnAbort: TNotifyEvent read FOnAbort write FOnAbort; + property OnChanged: TCWMsgsChanged read FOnChanged write FOnChanged; + end; + +implementation + +const +{$IFDEF WINDOWS} + UI_FONT = 'Segoe UI'; +{$ELSE} + UI_FONT = 'Sans'; +{$ENDIF} + FORM_W = 470; + PAD = 14; + ROW_H = 26; + ROW_GAP = 6; + LBL_W = 30; + BTN_W = 66; + +constructor TCWMessagesForm.Create(AOwner: TComponent); +begin + inherited CreateNew(AOwner); + FTheme := DarkTheme; + FUpdating := False; + TSettingsManager.DefaultCW(FCW); + + Scaled := False; + Caption := 'CW Messages'; + BorderStyle := bsSingle; + if AOwner is TCustomForm then Position := poOwnerFormCenter + else Position := poScreenCenter; + Width := DpiScale(FORM_W); + // Ряды + строка кнопок + подсказка в ДВЕ строки под ними (в один ряд с + // кнопками она налезала на Close). + Height := DpiScale(2 * PAD + CW_MSG_COUNT * (ROW_H + ROW_GAP) + + ROW_H + 8 + 34); + BuildUI; + ApplyTheme(DarkTheme); +end; + +procedure TCWMessagesForm.BuildUI; +var + i, Y: Integer; +begin + Y := PAD; + for i := 0 to CW_MSG_COUNT - 1 do + begin + FLbls[i] := TLabel.Create(Self); + FLbls[i].Parent := Self; + FLbls[i].Caption := 'F' + IntToStr(i + 1); + FLbls[i].Font.Name := UI_FONT; + FLbls[i].Font.Size := 9; + FLbls[i].SetBounds(DpiScale(PAD), DpiScale(Y + 5), + DpiScale(LBL_W), DpiScale(18)); + + FEdits[i] := TFlatEdit.Create(Self); + FEdits[i].Parent := Self; + FEdits[i].Font.Name := UI_FONT; + FEdits[i].Font.Size := 9; + FEdits[i].MaxLength := 63; + FEdits[i].Tag := i; + FEdits[i].OnChange := @EditChange; + FEdits[i].SetBounds(DpiScale(PAD + LBL_W), DpiScale(Y), + DpiScale(FORM_W - 2 * PAD - LBL_W - BTN_W - 8), + DpiScale(ROW_H - 2)); + + FSendBtns[i] := TFlatButton.Create(Self); + FSendBtns[i].Parent := Self; + FSendBtns[i].Caption := 'Send'; + FSendBtns[i].Tag := i; + FSendBtns[i].OnClick := @SendClick; + FSendBtns[i].SetBounds(DpiScale(FORM_W - PAD - BTN_W), DpiScale(Y), + DpiScale(BTN_W), DpiScale(ROW_H - 2)); + Inc(Y, ROW_H + ROW_GAP); + end; + + FBtnStop := TFlatButton.Create(Self); + FBtnStop.Parent := Self; + FBtnStop.Caption := 'Stop'; + FBtnStop.OnClick := @StopClick; + FBtnStop.SetBounds(DpiScale(PAD), DpiScale(Y), DpiScale(BTN_W), DpiScale(ROW_H)); + + FBtnClose := TFlatButton.Create(Self); + FBtnClose.Parent := Self; + FBtnClose.Caption := 'Close'; + FBtnClose.OnClick := @CloseClick; + FBtnClose.SetBounds(DpiScale(FORM_W - PAD - BTN_W), DpiScale(Y), + DpiScale(BTN_W), DpiScale(ROW_H)); + + FHint := TLabel.Create(Self); + FHint.Parent := Self; + FHint.Font.Name := UI_FONT; + FHint.Font.Size := 8; + FHint.AutoSize := False; + FHint.WordWrap := True; + FHint.Caption := 'F1..F8 in the main window send the same slots. ' + + 'Touching the paddle or dropping MOX stops sending.'; + FHint.SetBounds(DpiScale(PAD), DpiScale(Y + ROW_H + 8), + DpiScale(FORM_W - 2 * PAD), DpiScale(34)); +end; + +procedure TCWMessagesForm.StyleBtn(B: TFlatButton); +begin + if B = nil then Exit; + B.ClrNorm := FTheme.BtnNorm; + B.ClrBorder := FTheme.BtnBorderNorm; + B.ClrHot := FTheme.BtnHot; + B.ClrActive := FTheme.BtnActive; + B.ClrText := FTheme.BtnText; + B.ClrTextAct := FTheme.BtnTextActive; + B.Font.Name := UI_FONT; + B.Font.Size := 9; + B.Invalidate; +end; + +procedure TCWMessagesForm.ApplyTheme(const T: TAppTheme); +var i: Integer; +begin + FTheme := T; + Color := T.BG; + for i := 0 to CW_MSG_COUNT - 1 do + begin + FLbls[i].Font.Color := T.TextDim; + FEdits[i].SetAppTheme(T); + StyleBtn(FSendBtns[i]); + end; + StyleBtn(FBtnStop); + StyleBtn(FBtnClose); + FHint.Font.Color := T.TextDim; + Invalidate; +end; + +procedure TCWMessagesForm.LoadFrom(const C: TCWSettings); +var i: Integer; +begin + FUpdating := True; + try + FCW := C; + for i := 0 to CW_MSG_COUNT - 1 do + FEdits[i].Text := C.Messages[i]; + finally + FUpdating := False; + end; +end; + +procedure TCWMessagesForm.EditChange(Sender: TObject); +var i: Integer; +begin + if FUpdating then Exit; + i := TFlatEdit(Sender).Tag; + if (i < 0) or (i >= CW_MSG_COUNT) then Exit; + FCW.Messages[i] := Copy(TFlatEdit(Sender).Text, 1, 63); + if Assigned(FOnChanged) then FOnChanged(FCW); +end; + +procedure TCWMessagesForm.SendSlot(Idx: Integer); +begin + if (Idx < 0) or (Idx >= CW_MSG_COUNT) then Exit; + if FCW.Messages[Idx] = '' then Exit; + if Assigned(FOnSend) then FOnSend(FCW.Messages[Idx]); +end; + +procedure TCWMessagesForm.SendClick(Sender: TObject); +begin + SendSlot(TFlatButton(Sender).Tag); +end; + +procedure TCWMessagesForm.StopClick(Sender: TObject); +begin + if Assigned(FOnAbort) then FOnAbort(Self); +end; + +procedure TCWMessagesForm.CloseClick(Sender: TObject); +begin + Hide; +end; + +end. diff --git a/CWMorse.pas b/CWMorse.pas new file mode 100644 index 0000000..81b6f6f --- /dev/null +++ b/CWMorse.pas @@ -0,0 +1,297 @@ +unit CWMorse; + +{$mode objfpc}{$H+} + +// --------------------------------------------------------------------------- +// Программная передача телеграфа: текст → точки/тире → ключ. +// +// Кто держит тайминги. У openHPSDR P2 есть ДВА пути в эфир: +// * манипулятор в разъёме трансивера — элементы формирует кейер в FPGA, PC +// только отдаёт ему скорость/вес (DUC Specific байт 5..10). Джиттера нет; +// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по +// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА +// нажать и отпустить, решает PC. Именно этот путь используется для +// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs). +// +// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и +// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка +// каждого сна накапливалась бы и знак «плыл» бы по длине. +// --------------------------------------------------------------------------- + +interface + +uses + Classes, SysUtils, SyncObjs; + +type + // Вызывается из потока передачи на каждом фронте ключа. + TCWKeyEvent = procedure(Down: Boolean) of object; + + TCWSender = class(TThread) + private + FKeyEvent: TCWKeyEvent; + FLock: TCriticalSection; + FWake: TEvent; + FPending: string; // очередь текста (под FLock) + FAbortReq: Boolean; // «бросить и отпустить ключ» (под FLock) + FBusy: Boolean; // идёт передача (под FLock) + FWPM: Integer; // под FLock + FWeight: Integer; // 33..66, 50 = симметрично (под FLock) + function TakeText: string; + function Aborted: Boolean; + procedure Key(Down: Boolean); + // Выдержать Ms миллисекунд, просыпаясь дробно ради отзывчивости на abort. + // Возвращает False, если передачу оборвали. + function Hold(Deadline: QWord): Boolean; + procedure SendChar(Ch: Char; var Deadline: QWord); + protected + procedure Execute; override; + public + constructor Create(AKeyEvent: TCWKeyEvent); + destructor Destroy; override; + procedure Enqueue(const S: string); + procedure AbortSending; + procedure SetSpeed(WPM, Weight: Integer); + function Busy: Boolean; + end; + +// Код знака: строка из '.' и '-'. Пустая — знака нет в таблице (пропускаем, +// иначе опечатка в макросе превратилась бы в мусор в эфире). +function MorseCode(Ch: Char): string; + +implementation + +const + // Точка при 1 WPM = 1200 мс (стандарт PARIS). + DIT_MS_AT_1WPM = 1200; + +function MorseCode(Ch: Char): string; +begin + case UpCase(Ch) of + 'A': Result := '.-'; 'B': Result := '-...'; 'C': Result := '-.-.'; + 'D': Result := '-..'; 'E': Result := '.'; 'F': Result := '..-.'; + 'G': Result := '--.'; 'H': Result := '....'; 'I': Result := '..'; + 'J': Result := '.---'; 'K': Result := '-.-'; 'L': Result := '.-..'; + 'M': Result := '--'; 'N': Result := '-.'; 'O': Result := '---'; + 'P': Result := '.--.'; 'Q': Result := '--.-'; 'R': Result := '.-.'; + 'S': Result := '...'; 'T': Result := '-'; 'U': Result := '..-'; + 'V': Result := '...-'; 'W': Result := '.--'; 'X': Result := '-..-'; + 'Y': Result := '-.--'; 'Z': Result := '--..'; + '0': Result := '-----'; '1': Result := '.----'; '2': Result := '..---'; + '3': Result := '...--'; '4': Result := '....-'; '5': Result := '.....'; + '6': Result := '-....'; '7': Result := '--...'; '8': Result := '---..'; + '9': Result := '----.'; + '.': Result := '.-.-.-'; ',': Result := '--..--'; '?': Result := '..--..'; + '/': Result := '-..-.'; '=': Result := '-...-'; '+': Result := '.-.-.'; + '-': Result := '-....-'; '(': Result := '-.--.'; ')': Result := '-.--.-'; + ':': Result := '---...'; '''':Result := '.----.'; '"': Result := '.-..-.'; + '@': Result := '.--.-.'; '!': Result := '-.-.--'; ';': Result := '-.-.-.'; + '$': Result := '...-..-'; + else + Result := ''; + end; +end; + +constructor TCWSender.Create(AKeyEvent: TCWKeyEvent); +begin + FKeyEvent := AKeyEvent; + FLock := TCriticalSection.Create; + FWake := TEvent.Create(nil, False, False, ''); + FWPM := 20; + FWeight := 50; + FreeOnTerminate := False; + inherited Create(False); +end; + +destructor TCWSender.Destroy; +begin + Terminate; + FWake.SetEvent; + WaitFor; + FWake.Free; + FLock.Free; + inherited Destroy; +end; + +procedure TCWSender.Enqueue(const S: string); +begin + if S = '' then Exit; + FLock.Enter; + try + FAbortReq := False; + FPending := FPending + S; + finally + FLock.Leave; + end; + FWake.SetEvent; +end; + +procedure TCWSender.AbortSending; +begin + FLock.Enter; + try + FPending := ''; + FAbortReq := True; + finally + FLock.Leave; + end; + FWake.SetEvent; +end; + +procedure TCWSender.SetSpeed(WPM, Weight: Integer); +begin + if WPM < 5 then WPM := 5; + if WPM > 60 then WPM := 60; + if Weight < 33 then Weight := 33; + if Weight > 66 then Weight := 66; + FLock.Enter; + try + FWPM := WPM; + FWeight := Weight; + finally + FLock.Leave; + end; +end; + +function TCWSender.Busy: Boolean; +begin + FLock.Enter; + try + Result := FBusy or (FPending <> ''); + finally + FLock.Leave; + end; +end; + +function TCWSender.TakeText: string; +begin + FLock.Enter; + try + Result := FPending; + FPending := ''; + finally + FLock.Leave; + end; +end; + +function TCWSender.Aborted: Boolean; +begin + FLock.Enter; + try + Result := FAbortReq; + finally + FLock.Leave; + end; +end; + +procedure TCWSender.Key(Down: Boolean); +begin + if Assigned(FKeyEvent) then FKeyEvent(Down); +end; + +function TCWSender.Hold(Deadline: QWord): Boolean; +var Now_: QWord; Slice: Integer; +begin + Result := True; + repeat + if Terminated or Aborted then Exit(False); + Now_ := GetTickCount64; + if Now_ >= Deadline then Exit(True); + // Дробим сон: обрыв передачи не должен ждать конца длинного тире. + Slice := Integer(Deadline - Now_); + if Slice > 5 then Slice := 5; + Sleep(Slice); + until False; +end; + +procedure TCWSender.SendChar(Ch: Char; var Deadline: QWord); +// Deadline — момент, к которому знак должен закончиться; ведём его вперёд, +// чтобы длительности не «уползали» на ошибке каждого Sleep. +var + Code: string; + i, Dit, Mark, Gap, WPM, Weight: Integer; +begin + FLock.Enter; + try + WPM := FWPM; + Weight := FWeight; + finally + FLock.Leave; + end; + Dit := DIT_MS_AT_1WPM div WPM; + // Вес: 50 = точка равна паузе. Выше — посылки длиннее за счёт пауз внутри + // знака (межзнаковые интервалы остаются стандартными, иначе «плывёт» темп). + Mark := (Dit * Weight) div 50; + Gap := (Dit * (100 - Weight)) div 50; + if Mark < 1 then Mark := 1; + if Gap < 1 then Gap := 1; + + if Ch = ' ' then + begin + // Межсловный интервал = 7 точек; 3 из них уже отданы после прошлого знака. + Inc(Deadline, 4 * Dit); + Hold(Deadline); + Exit; + end; + + Code := MorseCode(Ch); + if Code = '' then Exit; + for i := 1 to Length(Code) do + begin + Key(True); + if Code[i] = '-' then Inc(Deadline, 3 * Mark) else Inc(Deadline, Mark); + if not Hold(Deadline) then begin Key(False); Exit; end; + Key(False); + Inc(Deadline, Gap); // пауза между элементами знака + if not Hold(Deadline) then Exit; + end; + Inc(Deadline, 2 * Dit); // добор до 3 точек между знаками + Hold(Deadline); +end; + +procedure TCWSender.Execute; +var + Text: string; + i: Integer; + Deadline: QWord; +begin + while not Terminated do + begin + FWake.WaitFor(200); + if Terminated then Break; + Text := TakeText; + if Text = '' then Continue; + + FLock.Enter; + try + FBusy := True; + FAbortReq := False; + finally + FLock.Leave; + end; + try + Deadline := GetTickCount64; + i := 1; + while i <= Length(Text) do + begin + if Terminated or Aborted then Break; + SendChar(Text[i], Deadline); + // Дописанное в очередь по ходу передачи подхватываем без паузы — + // иначе поток допередал бы «старый» текст и уснул на 200 мс. + if i = Length(Text) then Text := Text + TakeText; + Inc(i); + end; + finally + Key(False); // ключ всегда отпущен, что бы ни случилось + FLock.Enter; + try + FBusy := False; + finally + FLock.Leave; + end; + end; + end; + Key(False); +end; + +end. diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 6a7680a..349503a 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -169,6 +169,7 @@ type FAlexEnabled: Boolean; FAlexConfig: TAlexSettings; // per-band antenna + routing config FStepAtten: Byte; // ADC0 step attenuator 0-31 dB + FCWXKey: Boolean; // бит CWX (HP байт 5): программный ключ // ---- OC Control (Open Collector выходы Penny/Alex, byte 1401) ---- FOCConfig: TOCSettings; FTuning: Boolean; // TUN активен (для TX pin action) @@ -269,6 +270,7 @@ type Transmitting, PAEnabled, AlexEnabled: Boolean; Tuning: Boolean = False; TwoTone: Boolean = False); override; procedure SetStepAtten(dB: Byte); override; + procedure SetCWXKey(Down: Boolean); override; procedure SetFreqCalPPM(P: Double); override; // Частота с поправкой опорника — для частотного слова DDC/DUC. function CalFreq(Hz: Double): Double; @@ -1801,6 +1803,15 @@ begin FFreqCalPPM := P; end; +procedure THPSDRNetwork.SetCWXKey(Down: Boolean); +// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка +// доставки = один UDP-пакет (так же делает Thetis, cwx.cs setkey). +begin + if FCWXKey = Down then Exit; + FCWXKey := Down; + if FConnected then SendFullHP; +end; + procedure THPSDRNetwork.SetStepAtten(dB: Byte); begin if dB > 31 then dB := 31; @@ -1840,6 +1851,9 @@ begin if FPTTActive then Buf[4] := Buf[4] or HP_PTT0; end; + // Byte 5 bit0: CWX — программный ключ телеграфа (несущую даёт прошивка). + if FCWXKey then Buf[5] := Buf[5] or $01; + // IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10) IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10 DDCBase := DDCBaseIndex; diff --git a/MainForm.pas b/MainForm.pas index 6f78e61..ce1241b 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -43,7 +43,7 @@ uses WinFirewall, BoardUtils, WisdomBuilder, UISync, FlatEdit, FMRepeater, - ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder, + ChannelStore, ChannelsForm, CWMessagesForm, BeaconScopeForm, DMRDecoder, PowerInhibit, DeviceStore, RadioBackend, PlutoBackend, @@ -367,6 +367,7 @@ type BtnTXProfile: TFlatButton; FTXProfileDropDown: TFlatDropDown; FChannelsForm: TObject; // TChannelsForm (cast при использовании) + FCWMsgForm: TObject; // TCWMessagesForm (память телеграфа) FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON) // ---- Right panel ---- @@ -763,6 +764,11 @@ type procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna) procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnTXSettingsChange(const T: TTXSettings); + procedure OnCWSettingsChange(const C: TCWSettings); + procedure ShowCWMessages; + procedure OnCWMessageSend(const Text: string); + procedure OnCWMessageAbort(Sender: TObject); + procedure FormKeyDownCW(Sender: TObject; var Key: Word; Shift: TShiftState); procedure BtnAGCModeClick(Sender: TObject); procedure TrkAGCChange(Sender: TObject); procedure TrkRxGainChange(Sender: TObject); @@ -1065,7 +1071,12 @@ begin FController.FTXSpecGridStep := 10.0; FSettingsForm := nil; FChannelsForm := nil; + FCWMsgForm := nil; FBeaconScopeForm := nil; + // F1..F8 для памяти телеграфа. Обработчик молчит, пока окно памяти не + // открывали и пока режим не CW, поэтому прочим клавишам ничего не мешает. + KeyPreview := True; + OnKeyDown := FormKeyDownCW; FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0); // Инициализируем кэш диапазонов умолчаниями @@ -3400,6 +3411,7 @@ begin T := CurrentAppTheme; if FDeviceDialog <> nil then FDeviceDialog.SetTheme(T); if FChannelsForm <> nil then TChannelsForm(FChannelsForm).ApplyTheme(T); + if FCWMsgForm <> nil then TCWMessagesForm(FCWMsgForm).ApplyTheme(T); if FBeaconScopeForm <> nil then FBeaconScopeForm.ApplyTheme(T); FController.FSettings.SaveTheme(V); FController.FSettings.Save; @@ -5831,6 +5843,7 @@ begin TSettingsForm(FSettingsForm).LoadTXSettings(FController.FTXSettings, IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); + TSettingsForm(FSettingsForm).LoadCWSettings(FController.CWSettings); PushTXProfilesToSettings; end; // Пилюли профиля на флагах: список общий, привязки могли съехать после @@ -8122,6 +8135,54 @@ begin end; end; +procedure TMainForm.OnCWSettingsChange(const C: TCWSettings); +// Телеграф целиком живёт в контроллере: pitch → движок, кейер/сайдтон → в +// прошивку, персист — там же. Форме остаётся только передать запись. +begin + FController.SetCWSettings(C); + // Окно памяти показывает те же ячейки — держим его в курсе правок из настроек. + if FCWMsgForm <> nil then TCWMessagesForm(FCWMsgForm).LoadFrom(FController.CWSettings); +end; + +procedure TMainForm.ShowCWMessages; +begin + if FCWMsgForm = nil then + begin + FCWMsgForm := TCWMessagesForm.Create(Self); + TCWMessagesForm(FCWMsgForm).OnSend := OnCWMessageSend; + TCWMessagesForm(FCWMsgForm).OnAbort := OnCWMessageAbort; + TCWMessagesForm(FCWMsgForm).OnChanged := OnCWSettingsChange; + end; + TCWMessagesForm(FCWMsgForm).LoadFrom(FController.CWSettings); + TCWMessagesForm(FCWMsgForm).ApplyTheme(CurrentAppTheme); + TCWMessagesForm(FCWMsgForm).Show; +end; + +procedure TMainForm.OnCWMessageSend(const Text: string); +begin + FController.CWXSend(Text); +end; + +procedure TMainForm.OnCWMessageAbort(Sender: TObject); +begin + FController.CWXAbort; +end; + +procedure TMainForm.FormKeyDownCW(Sender: TObject; var Key: Word; + Shift: TShiftState); +// F1..F8 — ячейки памяти телеграфа. Работают только в CW и только когда окно +// памяти уже открывали (иначе клавиши остаются свободными для всего прочего). +var Slot: Integer; +begin + if FCWMsgForm = nil then Exit; + if Shift <> [] then Exit; + if not (FController.ActiveTXMode in [MODE_CWL, MODE_CWU]) then Exit; + if (Key < VK_F1) or (Key > VK_F8) then Exit; + Slot := Key - VK_F1; + TCWMessagesForm(FCWMsgForm).SendSlot(Slot); + Key := 0; +end; + procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings); begin FController.FAlexSettings := A; @@ -9047,6 +9108,8 @@ begin SF.OnThemeChange := SetLightTheme; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; SF.OnTXChange := OnTXSettingsChange; + SF.OnCWChange := OnCWSettingsChange; + SF.OnCWMessagesOpen := ShowCWMessages; SF.OnTXProfileSelect := OnTXProfileSelectFromSettings; SF.OnTXProfileAdd := OnTXProfileAddFromSettings; SF.OnTXProfileRename := OnTXProfileRenameFromSettings; @@ -9063,6 +9126,7 @@ begin PushTXProfilesToSettings; SF.LoadTXSettings(FController.FTXSettings, IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); + SF.LoadCWSettings(FController.CWSettings); SF.LoadAlexSettings(FController.FAlexSettings, IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); // Вкладка Antenna: у AD936x (Pluto/LibreSDR) вместо Alex-таблицы — таблица @@ -9159,6 +9223,7 @@ begin EnsureWDSPWisdom; end; + // =========================================================================== // CAT subsystem // =========================================================================== diff --git a/RadioBackend.pas b/RadioBackend.pas index 1790b51..b456a71 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -180,6 +180,10 @@ type procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); virtual; procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual; procedure SendHighPriority(const Pkt: THighPriorityPacket); virtual; + // Бит CWX (High Priority байт 5 bit0): «ключ нажат» для программной + // передачи телеграфа — несущую по нему формирует прошивка. Пакет уходит + // немедленно, каждый фронт (тайминги держит PC). Pluto — no-op. + procedure SetCWXKey(Down: Boolean); virtual; procedure SetStepAtten(dB: Byte); virtual; // RX-усиление (Pluto/AD936x). ModeIdx: 0=manual, 1=fast_attack, 2=slow_attack, // 3=hybrid. GainDb применяется только в manual. HPSDR — no-op. @@ -277,6 +281,10 @@ procedure TRadioBackend.SendHighPriority(const Pkt: THighPriorityPacket); begin end; +procedure TRadioBackend.SetCWXKey(Down: Boolean); +begin +end; + procedure TRadioBackend.SetStepAtten(dB: Byte); begin end; diff --git a/RadioController.pas b/RadioController.pas index 7ed9f52..fd7b13d 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -50,7 +50,7 @@ uses Classes, SysUtils, Math, HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings, WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder, - Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; + Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse; // Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для // движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1). @@ -433,6 +433,22 @@ type // TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent; // активный профиль ведётся за ними (StoreActiveTXProfile). FTXProfiles: TTXProfileList; + // Телеграф: параметры кейера/сайдтона/pitch (DUC Specific 5..13,17 + CW-тракт). + FCWSettings: TCWSettings; + // Программная передача текста (бит CWX). Поток создаётся лениво — у тех, + // кто телеграфом не пользуется, лишней нити не заводится. + FCWSender: TCWSender; + // Состояние ключа для ПРОГРАММНОГО сайдтона. Пишется потоком передачи и + // разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone — + // толерантная гонка на одном булеве, как FHPp*. + FCWKeyDown: Boolean; + // Последний отправленный байт 5 — чтобы не гнать DUC-пакет на каждую + // смену режима, а только когда телеграфная конфигурация реально поменялась. + FLastCWOptions: Byte; + FCWSTPhase: Double; // фаза генератора (только DSP-поток) + FCWSTEnv: Double; // огибающая 0..1, сглаживает щелчки (DSP-поток) + FCWSTBufL: array of Single; // буферы подмешивания (только DSP-поток) + FCWSTBufR: array of Single; FAlexSettings: TAlexSettings; FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only) // Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер. @@ -584,6 +600,15 @@ type procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP + // ---- Телеграф ---------------------------------------------------------- + function CWTXActive: Boolean; // передачей владеет FPGA-кейер + function CWOptionsByte: Byte; // DUC Specific байт 5 + procedure SyncCWKeyer; // байт 5 под текущий TX-режим + procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи + // Подмешать программный сайдтон в блок RX-аудио (DSP-поток). Возвращает + // False, если подмешивать нечего — тогда вызывающий отдаёт исходный блок. + function MixSidetone(const Left, Right: array of Single; + Count: Integer): Boolean; function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате) // ---- TX-профили -------------------------------------------------------- @@ -917,6 +942,19 @@ type procedure SetMOX(On_: Boolean); procedure SetTune(On_: Boolean); // TUN + // ---- Телеграф ---------------------------------------------------------- + // Настройки применяются целиком: pitch уезжает в движок (там он двигает + // гетеродин и полосу), остальное — в DUC Specific прошивке. + function CWSettings: TCWSettings; + procedure SetCWSettings(const C: TCWSettings); + // Программная передача текста: PC сам выдерживает тайминги и дёргает бит + // CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон + // Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание + // манипулятора или снятие MOX обрывает передачу (CWXAbort). + procedure CWXSend(const Text: string); + procedure CWXAbort; + function CWXBusy: Boolean; + // ---- PureSignal ---- // Доступность PS у активного бэкенда/платы (Caps.HasPureSignal). function PSAvailable: Boolean; @@ -1108,6 +1146,8 @@ begin DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры ApplyFilterSlot(FFilter); FAGCMode := 1; FAGCTop := 90; + TSettingsManager.DefaultCW(FCWSettings); // до подключения — заводской телеграф + FLastCWOptions := $FF; // «ещё не слали» — первый пуш пройдёт FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0; @@ -1239,6 +1279,12 @@ end; destructor TRadioController.Destroy; begin + // Поток передачи телеграфа гасим ПЕРВЫМ: его колбэк ключа лезет в бэкенд. + if Assigned(FCWSender) then + begin + FCWSender.AbortSending; + FreeAndNil(FCWSender); + end; FreeEngines; FreeAndNil(FDeviceStore); FreeAndNil(FSettings); @@ -1385,6 +1431,7 @@ var SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double; Bi: Integer; Is6m, IsTx: Boolean; + KeyDot, KeyDash: Boolean; begin FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; @@ -1447,6 +1494,17 @@ begin FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0; FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0; FHPpOverload := Status.ADCOverload; + // Состояние ключа/манипулятора в разъёме трансивера (сетевой поток). + // Любое касание обрывает программную передачу — как в Thetis/pihpsdr + // («key hit» останавливает CAT CW), иначе оператор не может вклиниться. + KeyDot := (Status.StatusBits and HPS_DOT) <> 0; + KeyDash := (Status.StatusBits and HPS_DASH) <> 0; + if (KeyDot or KeyDash) and CWXBusy then CWXAbort; + // Программный сайдтон от лепестков верен ТОЛЬКО у прямого ключа: при иамбике + // элементы считает FPGA, а PC видит лишь замыкание контакта. + if (FCWSettings.KeyerMode = 0) and (ActiveTXMode in [MODE_CWL, MODE_CWU]) + and (not CWXBusy) then + FCWKeyDown := KeyDot or KeyDash; Invoke(ApplyHPStatus); end; @@ -1533,6 +1591,19 @@ begin // the independent pre-volume tap is handled by OnDemodAudioReady. if FMode in [MODE_DMR, MODE_FMRAW] then Exit; + // Программный сайдтон подмешиваем ДО всех потребителей: его должны слышать + // и локальная звуковуха, и web-клиент, и динамик трансивера. + if MixSidetone(Left, Right, Count) then + begin + if FSendAudioToRadio then + FNetwork.SendSpeakerAudio(FCWSTBufL, FCWSTBufR, Count); + if Assigned(FOnAudioConsume) and FOnAudioConsume(FCWSTBufL, FCWSTBufR, Count) then + Exit; + if FLocalAudio then + FAudioOut.Write(FCWSTBufL, FCWSTBufR, Count); + Exit; + end; + if FSendAudioToRadio then FNetwork.SendSpeakerAudio(Left, Right, Count); if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then @@ -2255,6 +2326,7 @@ begin ActiveMicInput.Flush; end; end; + if FTxSliceId = Id then SyncCWKeyer; end; procedure TRadioController.SetSliceFilter(Id, Low, High: Integer); @@ -2909,17 +2981,26 @@ begin // Мультислайс-TX: если выбран слайс-источник — передаём на его частоте // (слайсы не несут repeater-конфиг, FM-сдвиг не применяем). if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then - Exit(S.TargetHz); - // TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B), - // независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал - // за активным RX (TX-A горит, но передача шла на B при активном B). - if FSplitTxB then Result := FVfoB - else Result := FVfoA; - if FMode = MODE_FM then - case FFMRptDir of - RPT_MINUS: Result := Result - FFMRptOffsetHz; - RPT_PLUS: Result := Result + FFMRptOffsetHz; - end; + Result := S.TargetHz + else + begin + // TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B), + // независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал + // за активным RX (TX-A горит, но передача шла на B при активном B). + if FSplitTxB then Result := FVfoB + else Result := FVfoA; + if FMode = MODE_FM then + case FFMRptDir of + RPT_MINUS: Result := Result - FFMRptOffsetHz; + RPT_PLUS: Result := Result + FFMRptOffsetHz; + end; + end; + // Телеграф + TUN: несущую даёт SSB-модулятор из тона pitch, поэтому DUC надо + // сдвинуть навстречу — иначе настроечная несущая уедет от VFO на pitch. При + // работе ключом (кейер/CWX) сдвигать нечего: РЧ формирует прошивка на DUC. + if FTuning and (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FWDSPReady + and Assigned(FDSPEngine) then + Result := Result + FDSPEngine.CWLOOffset(ActiveTXMode); end; function TRadioController.ActiveTXMode: Integer; @@ -2975,6 +3056,7 @@ begin // без сброса первые ~200мс передачи ушли бы в эфир «из прошлого». if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then ActiveMicInput.Flush; + SyncCWKeyer; // кейер прошивки — под режим нового источника FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx @@ -4043,8 +4125,15 @@ begin ActiveMicInput.Flush; end; // Если TUN активен — пересчитываем знак частоты под новый режим - if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); + if FTuning then + begin + if ActiveTXMode in [MODE_CWL, MODE_CWU] then + FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG) + else + FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); + end; end; + SyncCWKeyer; ApplyModeFilter; Changed(rfMode); end; @@ -4941,21 +5030,65 @@ begin FDSPEngine.SetANF(FANF); end; +function TRadioController.CWTXActive: Boolean; +// «Телеграфом управляет железо»: режим ИСТОЧНИКА передачи — CW и кейер в +// прошивке. В этом состоянии голосовой TXA не запускается вовсе (эталон +// Thetis, console.cs:29570), а точки/тире формирует FPGA. +begin + Result := (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FCWSettings.FWKeyer; +end; + +function TRadioController.CWOptionsByte: Byte; +// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же +// объяснение, почему iambic-бит выставляется и для Mode B). +// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT +// на замыкание ключа посреди SSB (Thetis, console.cs:27394). +begin + Result := 0; + if not CWTXActive then Exit; + Result := Result or CW_MODE; + if FCWSettings.ReverseKeys then Result := Result or CW_REVERSE_KEYS; + case FCWSettings.KeyerMode of + 1: Result := Result or CW_IAMBIC; // Mode A + 2: Result := Result or CW_IAMBIC or CW_MODE_B; // Mode B + end; // 0 = прямой ключ + if FCWSettings.SidetoneHW and (FCWSettings.SidetoneHWLevel > 0) then + Result := Result or CW_SIDETONE; + if FCWSettings.StrictSpacing then Result := Result or CW_STRICT_SPACING; + if FCWSettings.BreakIn then Result := Result or CW_BREAK_IN; +end; + procedure TRadioController.SendDUCSpecificFromSettings; var DUCPkt: TDUCSpecificPacket; RateKsps: Word; + HangMs: Word; begin if not (FNetwork.Connected and FNetwork.Running) then Exit; FillChar(DUCPkt, SizeOf(DUCPkt), 0); // DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX). RateKsps := FDSPEngine.TXSampleRate div 1000; DUCPkt.NumDACs := 1; - DUCPkt.SidetoneLevel := 50; - DUCPkt.SidetoneFreqHi := Hi(600); - DUCPkt.SidetoneFreqLo := Lo(600); - DUCPkt.KeyerSpeed := 20; - DUCPkt.KeyerWeight := 50; + DUCPkt.CWOptions := CWOptionsByte; + // Уровень/частота аппаратного сайдтона — тон в наушниках САМОГО трансивера. + if FCWSettings.SidetoneHW then + DUCPkt.SidetoneLevel := Byte(EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127)) + else + DUCPkt.SidetoneLevel := 0; + DUCPkt.SidetoneFreqHi := Hi(Word(EnsureRange(FCWSettings.Pitch, 200, 1200))); + DUCPkt.SidetoneFreqLo := Lo(Word(EnsureRange(FCWSettings.Pitch, 200, 1200))); + DUCPkt.KeyerSpeed := Byte(EnsureRange(FCWSettings.Speed, 1, 60)); + DUCPkt.KeyerWeight := Byte(EnsureRange(FCWSettings.Weight, 33, 66)); + // Hang — сколько прошивка держит передачу после последнего элемента (semi + // break-in). При выключенном break-in железу его знать незачем. + if FCWSettings.BreakIn then + HangMs := Word(EnsureRange(FCWSettings.HangTimeMS, 0, 2000)) + else + HangMs := 0; + DUCPkt.HangDelayHi := Hi(HangMs); + DUCPkt.HangDelayLo := Lo(HangMs); + DUCPkt.RFDelay := Byte(EnsureRange(FCWSettings.RFDelayMS, 0, 255)); + DUCPkt.CWRampPeriod := Byte(EnsureRange(FCWSettings.RampMS, 0, 20)); DUCPkt.DUC0RateHi := Hi(RateKsps); DUCPkt.DUC0RateLo := Lo(RateKsps); DUCPkt.DUC0Bits := 24; @@ -4969,6 +5102,7 @@ begin DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31)); DUCPkt.StepAtten1 := DUCPkt.StepAtten0; DUCPkt.StepAtten2 := DUCPkt.StepAtten0; + FLastCWOptions := DUCPkt.CWOptions; FNetwork.SendDUCSpecific(DUCPkt); end; @@ -5316,6 +5450,9 @@ begin and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; WasTransmitting := FTransmitting; + // Снятие MOX руками обрывает программную передачу текста: оператор решил + // прекратить — ключ обязан отпуститься немедленно. + if (not On_) and CWXBusy then CWXAbort; FTransmitting := On_; // 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник. if (not On_) and FTwoTone then @@ -5386,13 +5523,27 @@ begin ActiveMicInput.Flush; // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. - FDSPEngine.KeepRXDuringTX := FDisplayDuplex; + // ★Телеграф — исключение: приёмник должен жить, иначе с ним умолкает и + // программный сайдтон (он подмешивается в RX-аудио), а при break-in + // пропадал бы эфир между посылками. От утечки прикрывает ATT on TX. + FDSPEngine.KeepRXDuringTX := FDisplayDuplex or + (ActiveTXMode in [MODE_CWL, MODE_CWU]); // Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог // оставить главный режим). Ставим перед стартом TX-потока. if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode); // PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc). FDSPEngine.PSSetMox(FTransmitting); - FDSPEngine.SetTXRun(FTransmitting); + // ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis, + // console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому + // без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц. + // Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN. + // Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки + // голос в телеграфном участке недопустим (тогда MOX = только PTT). + if FTransmitting and (ActiveTXMode in [MODE_CWL, MODE_CWU]) + and (not FTuning) and (not FTwoTone) then + FDSPEngine.SetTXRun(False) + else + FDSPEngine.SetTXRun(FTransmitting); if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then begin FDSPEngine.FlushRX; @@ -5439,8 +5590,16 @@ begin if On_ then begin // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. + // В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz), + // и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis + // (console.cs:31757, ветка cw_fw_keyer && chkTUN). if FWDSPReady and Assigned(FDSPEngine) then - FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); + begin + if ActiveTXMode in [MODE_CWL, MODE_CWU] then + FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG) + else + FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG); + end; FTuning := True; FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень) ApplyWDSPDrive(FTXSettings.TUNLevel); @@ -5465,6 +5624,131 @@ begin Changed(rfTuning); end; +// =========================================================================== +// Телеграф +// =========================================================================== + +function TRadioController.CWSettings: TCWSettings; +begin + Result := FCWSettings; +end; + +procedure TRadioController.SetCWSettings(const C: TCWSettings); +// Одна дверь для любой правки телеграфа: pitch уезжает в движок (там он +// двигает гетеродин и полосу всех CW-каналов), кейер/сайдтон — в прошивку, +// скорость — в поток программной передачи. +begin + FCWSettings := C; + FCWSettings.Pitch := EnsureRange(FCWSettings.Pitch, 200, 1200); + FCWSettings.Speed := EnsureRange(FCWSettings.Speed, 5, 60); + FCWSettings.Weight := EnsureRange(FCWSettings.Weight, 33, 66); + FCWSettings.SidetoneHWLevel := EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127); + FCWSettings.SidetoneSWLevel := EnsureRange(FCWSettings.SidetoneSWLevel, 0, 100); + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetCWPitch(FCWSettings.Pitch); + if Assigned(FCWSender) then + FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight); + SendDUCSpecificFromSettings; + // Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune): + // если крутят pitch прямо на настройке — обновляем живьём. + if FTuning and FWDSPReady and Assigned(FDSPEngine) + and (ActiveTXMode in [MODE_CWL, MODE_CWU]) then + FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG); + if FDevConnected and Assigned(FSettings) then + begin + FSettings.SaveCW(FDevMAC, FCWSettings); + FSettings.Save; + end; +end; + +procedure TRadioController.SyncCWKeyer; +// Байт 5 зависит от режима ИСТОЧНИКА передачи, поэтому каждая смена режима или +// TX-слайса обязана его перепослать: вне телеграфа он должен стать нулевым, +// иначе прошивка поднимет PTT на замыкание ключа посреди SSB. +begin + if not (ActiveTXMode in [MODE_CWL, MODE_CWU]) then CWXAbort; + if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings; +end; + +procedure TRadioController.CWXKeyEvent(Down: Boolean); +// Поток передачи, НЕ GUI. Обе операции потокобезопасны: SetCWXKey шлёт HP-пакет +// под своим локом бэкенда, FCWKeyDown читает DSP-поток. +begin + FCWKeyDown := Down; + if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then + FNetwork.SetCWXKey(Down); +end; + +function TRadioController.CWXBusy: Boolean; +begin + Result := Assigned(FCWSender) and FCWSender.Busy; +end; + +procedure TRadioController.CWXSend(const Text: string); +// Передача текста. Break-in ВКЛ — PTT поднимает прошивка сама (как на ключ); +// выключен — оператор держит MOX руками, как при ручном T/R. +begin + if Text = '' then Exit; + if not (ActiveTXMode in [MODE_CWL, MODE_CWU]) then Exit; + if not FCWSettings.FWKeyer then Exit; + if not Assigned(FCWSender) then + FCWSender := TCWSender.Create(Self.CWXKeyEvent); + FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight); + FCWSender.Enqueue(Text); +end; + +procedure TRadioController.CWXAbort; +begin + if Assigned(FCWSender) then FCWSender.AbortSending; +end; + +function TRadioController.MixSidetone(const Left, Right: array of Single; + Count: Integer): Boolean; +// DSP-поток. Генератор синуса на pitch с трапецеидальной огибающей — без неё +// фронт посылки даёт щелчок в наушниках громче самой посылки. Результат +// кладём в свои буферы (входные массивы const), вызывающий отдаёт их дальше. +const + RAMP_MS = 5; + AUDIO_SR = 48000; +var + i: Integer; + Amp, Step, Rate, S: Double; +begin + Result := False; + if not FCWSettings.SidetoneSW then Exit; + if FCWSettings.SidetoneSWLevel <= 0 then Exit; + // Ключ отпущен и огибающая уже сошлась в ноль — блок трогать незачем. + if (not FCWKeyDown) and (FCWSTEnv <= 0.0) then Exit; + if Count <= 0 then Exit; + if Length(FCWSTBufL) < Count then + begin + SetLength(FCWSTBufL, Count); + SetLength(FCWSTBufR, Count); + end; + Amp := (FCWSettings.SidetoneSWLevel / 100.0) * 0.7; + Step := 2.0 * Pi * FCWSettings.Pitch / AUDIO_SR; + Rate := 1000.0 / (RAMP_MS * AUDIO_SR); + for i := 0 to Count - 1 do + begin + if FCWKeyDown then + begin + FCWSTEnv := FCWSTEnv + Rate; + if FCWSTEnv > 1.0 then FCWSTEnv := 1.0; + end + else + begin + FCWSTEnv := FCWSTEnv - Rate; + if FCWSTEnv < 0.0 then FCWSTEnv := 0.0; + end; + FCWSTPhase := FCWSTPhase + Step; + if FCWSTPhase > 2.0 * Pi then FCWSTPhase := FCWSTPhase - 2.0 * Pi; + S := Amp * FCWSTEnv * Sin(FCWSTPhase); + FCWSTBufL[i] := EnsureRange(Left[i] + S, -1.0, 1.0); + FCWSTBufR[i] := EnsureRange(Right[i] + S, -1.0, 1.0); + end; + Result := True; +end; + // =========================================================================== // PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2) // =========================================================================== @@ -5964,6 +6248,10 @@ begin ApplyAntPorts; // QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено. ApplyRxMuteToEngine; + // Тон телеграфа в движок: на момент ConnectDevice канал WDSP мог быть ещё + // не открыт, а здесь он гарантированно жив. + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetCWPitch(FCWSettings.Pitch); SendDUCSpecificFromSettings; // Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...). ApplyTXSettingsToDSP; @@ -6175,6 +6463,9 @@ begin FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache); // TX/Alex/XVTR per-device FSettings.LoadTX(FDevMAC, FTXSettings); + FSettings.LoadCW(FDevMAC, FCWSettings); + if FWDSPReady and Assigned(FDSPEngine) then + FDSPEngine.SetCWPitch(FCWSettings.Pitch); FSettings.LoadAlex(FDevMAC, FAlexSettings); FSettings.LoadAnt936x(FDevMAC, FAnt936x); FNetwork.SetAlexConfig(FAlexSettings); diff --git a/Settings.pas b/Settings.pas index 9ffbd2d..336305d 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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; diff --git a/SettingsForm.pas b/SettingsForm.pas index 9a6886b..64a17a8 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -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 diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 6607bc1..03f6f86 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -472,6 +472,7 @@ type FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс) FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels) FShiftHz: Double; // NCO shift для CTUN + FCWPitch: Integer; // Гц, тон телеграфа (см. CWLOOffset) FNRMode: Integer; FNBMode: Integer; FSNBEnabled: Boolean; @@ -505,6 +506,14 @@ type // Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака, // через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB. procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double); + // --- CW pitch ----------------------------------------------------------- + // Смещение гетеродина демодулятора относительно VFO в телеграфе: станция, + // стоящая ТОЧНО на частоте VFO, обязана звучать тоном CWPitch, а не нулём. + // CWL слушаем ниже (гетеродин выше на pitch), CWU — наоборот. Всё остальное + // в проекте (кромки фильтра, полоска на спектре, маркер VFO) остаётся в + // координатах «относительно VFO»: конвертер ровно один — эти три метода. + procedure PushShift(Chan: Integer; ShiftHz: Double; Mode: Integer); + procedure PushPassband(Chan, Lo, Hi, Mode, Rate: Integer); // Mode-dependent TX overlay. FM RAW uses the flat WFM modulator and // bypasses every speech processor without overwriting the saved TX setup. procedure ApplyTXModeSettings(Mode: Integer); @@ -641,6 +650,14 @@ type procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096) procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг + // Тон телеграфа. Меняет положение гетеродина и полосы во ВСЕХ CW-каналах — + // применяется немедленно (главный + слайсы), персист делает контроллер. + procedure SetCWPitch(Hz: Integer); + function CWPitch: Integer; + // Смещение гетеродина демодулятора относительно VFO в телеграфе (см. + // комментарий у PushShift). Публично: тем же сдвигом контроллер двигает + // DUC при настройке TUN, чтобы несущая легла ровно на VFO. + function CWLOOffset(Mode: Integer): Double; procedure SetNRMode(Mode: Integer); procedure SetNR(Enable: Boolean); procedure SetNBMode(Mode: Integer); @@ -1231,6 +1248,35 @@ begin end; end; +function TWDSPEngine.CWLOOffset(Mode: Integer): Double; +// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr +// (new_protocol.c:773 rxFrequency -= pitch для CWU). +begin + case Mode of + MODE_CWL: Result := FCWPitch; + MODE_CWU: Result := -FCWPitch; + else Result := 0.0; + end; +end; + +procedure TWDSPEngine.PushShift(Chan: Integer; ShiftHz: Double; Mode: Integer); +// ShiftHz приходит в координатах «VFO минус центр DDC»; в эфир гетеродин +// уезжает ещё на CWLOOffset, чтобы принимаемая посылка легла на pitch. +begin + SetRXAShiftFreq(Chan, ShiftHz + CWLOOffset(Mode)); +end; + +procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer); +// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг +// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину, +// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338). +var Off: Integer; +begin + Off := Round(CWLOOffset(Mode)); + RXASetPassband(Chan, ClampToNyquist(Lo - Off, Rate), + ClampToNyquist(Hi - Off, Rate)); +end; + procedure TWDSPEngine.ApplyTXModeSettings(Mode: Integer); begin if not FInitialized then Exit; @@ -1323,6 +1369,7 @@ begin FFilterHigh := 2400; FAGCMode := agcMedium; FShiftHz := 0.0; + FCWPitch := 600; FAGCTop := -90.0; FVolume := 0.7; FSMeter := -130.0; @@ -1932,8 +1979,8 @@ begin SetRXABandpassWindow(RXA_CHAN, 1); SetRXAMode(RXA_CHAN, ModeToWDSP(FMode)); SetRXAShiftRun(RXA_CHAN, 1); - SetRXAShiftFreq(RXA_CHAN, FShiftHz); - RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh); + PushShift(RXA_CHAN, FShiftHz, FMode); + PushPassband(RXA_CHAN, FFilterLow, FFilterHigh, FMode, FRXDSPRate); SetAGC(FAGCMode, 50.0); SetRXAPanelGain1(RXA_CHAN, FVolume); SetRXAPanelSelect(RXA_CHAN, 3); @@ -2457,9 +2504,9 @@ begin SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); ApplyRXModeSettings(Chan, FSlices[Idx].Mode); SetRXAShiftRun(Chan, 1); - SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); - RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), - ClampToNyquist(FSlices[Idx].FilterHi, DSPRate)); + PushShift(Chan, FSlices[Idx].ShiftHz, FSlices[Idx].Mode); + PushPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi, + FSlices[Idx].Mode, DSPRate); ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0); // NR/NB/SNB/ANF из сохранённого состояния слайса. ApplyNRToChan(Chan, FSlices[Idx].NRMode); @@ -2733,7 +2780,8 @@ begin try idx := FindSliceIdx(Id); if idx < 0 then Exit; FSlices[idx].ShiftHz := ShiftHz; - if FSlices[idx].Opened then SetRXAShiftFreq(FSlices[idx].Chan, ShiftHz); + if FSlices[idx].Opened then + PushShift(FSlices[idx].Chan, ShiftHz, FSlices[idx].Mode); finally FSliceLock.Leave; end; @@ -2768,6 +2816,14 @@ begin // шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор. ApplySliceFMSQ(idx); end; + // Приход/уход телеграфа меняет CWLOOffset — гетеродин и полосу надо + // переложить под новый режим, даже если канал не пересоздавали. + if FSlices[idx].Opened then + begin + PushShift(FSlices[idx].Chan, FSlices[idx].ShiftHz, Mode); + PushPassband(FSlices[idx].Chan, FSlices[idx].FilterLo, FSlices[idx].FilterHi, + Mode, SliceDSPRateFor(Mode, SliceInRate(idx))); + end; finally FSliceLock.Leave; end; @@ -2784,8 +2840,7 @@ begin if FSlices[idx].Opened then begin Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)); - RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate), - ClampToNyquist(High, Rate)); + PushPassband(FSlices[idx].Chan, Low, High, FSlices[idx].Mode, Rate); end; finally FSliceLock.Leave; @@ -2965,6 +3020,9 @@ begin ApplyDSPRate(Mode); SetRXAMode(RXA_CHAN, ModeToWDSP(Mode)); ApplyRXModeSettings(RXA_CHAN, Mode); + // Вход/выход из телеграфа меняет CWLOOffset: гетеродин надо переложить сразу + // (кромки приедут из UI следом, но сдвиг оттуда не приходит). + PushShift(RXA_CHAN, FShiftHz, Mode); ApplyTXMode(FTXMode); // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI end; @@ -2978,9 +3036,7 @@ begin // до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что // уходит в WDSP — запрошенные значения храним как есть, иначе понижение // SampleRate необратимо «съело» бы полосу. - // RXASetPassband — правильный unified API, пересчитывает фильтр целиком - RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate), - ClampToNyquist(High, FRXDSPRate)); + PushPassband(RXA_CHAN, Low, High, FMode, FRXDSPRate); end; procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double); @@ -2999,7 +3055,39 @@ procedure TWDSPEngine.SetShift(ShiftHz: Double); begin FShiftHz := ShiftHz; if not FInitialized then Exit; - SetRXAShiftFreq(RXA_CHAN, ShiftHz); + PushShift(RXA_CHAN, ShiftHz, FMode); +end; + +function TWDSPEngine.CWPitch: Integer; +begin + Result := FCWPitch; +end; + +procedure TWDSPEngine.SetCWPitch(Hz: Integer); +var i, Rate: Integer; +begin + if Hz = FCWPitch then Exit; + FCWPitch := Hz; + if not FInitialized then Exit; + if FMode in [MODE_CWL, MODE_CWU] then + begin + PushShift(RXA_CHAN, FShiftHz, FMode); + PushPassband(RXA_CHAN, FFilterLow, FFilterHigh, FMode, FRXDSPRate); + end; + FSliceLock.Enter; + try + for i := 0 to MAX_SLICES - 1 do + if FSlices[i].Active and FSlices[i].Opened and + (FSlices[i].Mode in [MODE_CWL, MODE_CWU]) then + begin + Rate := SliceDSPRateFor(FSlices[i].Mode, SliceInRate(i)); + PushShift(FSlices[i].Chan, FSlices[i].ShiftHz, FSlices[i].Mode); + PushPassband(FSlices[i].Chan, FSlices[i].FilterLo, FSlices[i].FilterHi, + FSlices[i].Mode, Rate); + end; + finally + FSliceLock.Leave; + end; end; procedure TWDSPEngine.SetAGCTop(TopDBm: Double); diff --git a/ewsdr.lpi b/ewsdr.lpi index 9b5246a..a80c357 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -308,6 +308,14 @@ + + + + + + + +