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:
2026-08-10 12:20:56 +03:00
co-authored by Claude Opus 5
parent 0ec120c2d5
commit 53329e82e8
12 changed files with 1421 additions and 46 deletions
+58
View File
@@ -38,6 +38,7 @@ type
FSyncFreq: Double; FSyncFreq: Double;
FSyncInt: Integer; FSyncInt: Integer;
FSyncBool: Boolean; FSyncBool: Boolean;
FSyncStr: string;
// ── Геттеры (CAT-поток, read-only) ── // ── Геттеры (CAT-поток, read-only) ──
function GetVfoA: Double; function GetVfoA: Double;
@@ -86,6 +87,15 @@ type
procedure SetSNB(V: Boolean); procedure SetSNB(V: Boolean);
procedure SetANF(V: Boolean); procedure SetANF(V: Boolean);
procedure SetTransmitting(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 DoBandUp;
procedure DoBandDown; procedure DoBandDown;
procedure DoTuneUp; procedure DoTuneUp;
@@ -217,6 +227,13 @@ begin
Ctx.SetAGCTop := @SetAGCTop; Ctx.SetAGCTop := @SetAGCTop;
Ctx.GetCTun := @GetCTun; Ctx.GetCTun := @GetCTun;
Ctx.SetCTun := @SetCTun; 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.OnPanelVfoStep := @PanelVfoStep;
Ctx.OnPanelEncoder := @PanelEncoder; Ctx.OnPanelEncoder := @PanelEncoder;
Ctx.OnPanelButton := @PanelButton; Ctx.OnPanelButton := @PanelButton;
@@ -418,6 +435,47 @@ begin FController.SetANF(FSyncBool); end;
procedure TCATAdapter.SetTransmitting(V: Boolean); procedure TCATAdapter.SetTransmitting(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncMOX); end; 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; procedure TCATAdapter.SyncMOX;
begin FController.SetMOX(FSyncBool); end; begin FController.SetMOX(FSyncBool); end;
+54 -12
View File
@@ -112,6 +112,14 @@ type
GetFilterHigh: TCATGetIntFunc; GetFilterHigh: TCATGetIntFunc;
SetFilterLow: TCATSetIntProc; SetFilterLow: TCATSetIntProc;
SetFilterHigh: 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) // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
@@ -136,6 +144,7 @@ type
function SafeGetVfoA: Double; function SafeGetVfoA: Double;
function SafeGetVfoB: Double; function SafeGetVfoB: Double;
function SafeGetMode: Integer; function SafeGetMode: Integer;
function SafeGetCWSpeed: Integer;
function SafeGetVolume: Integer; function SafeGetVolume: Integer;
function SafeGetDriveLevel: Integer; function SafeGetDriveLevel: Integer;
function SafeGetFilterIdx: Integer; function SafeGetFilterIdx: Integer;
@@ -812,21 +821,43 @@ begin
Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000'; Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000';
end; 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; 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 begin
if Length(s) = 3 then 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 else if Length(s) = 0 then
Result := '025' // default 25 WPM Result := Format('%.3d', [SafeGetCWSpeed])
else else
Result := CAT_ERROR; Result := CAT_ERROR;
end; end;
function TCATEngine.CmdKY(const s: string): string; function TCATEngine.CmdKY(const s: string): string;
// KY: send CW text — STUB: no CW keyer implemented // KY: передать текст телеграфом. Kenwood-семантика: первый символ — пробел-
// заполнитель признака «не занято», далее сам текст; пустой аргумент = опрос
// занятости ('1' — очередь занята, логгер ждёт).
var Txt: string;
begin 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; end;
function TCATEngine.CmdMD(const s: string): string; function TCATEngine.CmdMD(const s: string): string;
@@ -1754,23 +1785,34 @@ begin
end; end;
function TCATEngine.ZZKM(const s: string): string; 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 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; else Result := CAT_ERROR;
end; end;
function TCATEngine.ZZKS(const s: string): string; function TCATEngine.ZZKS(const s: string): string;
// ZZKS: CW speed (WPM) — STUB: no CW keyer // ZZKS: скорость кейера, WPM.
begin begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025' Result := CmdKS(s);
else Result := CAT_ERROR;
end; end;
function TCATEngine.ZZKY(const s: string): string; function TCATEngine.ZZKY(const s: string): string;
// ZZKY: CW text to send — STUB: no CW keyer implemented // ZZKY: передать текст телеграфом (то же, что KY).
begin begin
Result := '0'; // buffer not full Result := CmdKY(s);
end; end;
function TCATEngine.ZZMA(const s: string): string; function TCATEngine.ZZMA(const s: string): string;
+231
View File
@@ -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.
+297
View File
@@ -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.
+14
View File
@@ -169,6 +169,7 @@ type
FAlexEnabled: Boolean; FAlexEnabled: Boolean;
FAlexConfig: TAlexSettings; // per-band antenna + routing config FAlexConfig: TAlexSettings; // per-band antenna + routing config
FStepAtten: Byte; // ADC0 step attenuator 0-31 dB FStepAtten: Byte; // ADC0 step attenuator 0-31 dB
FCWXKey: Boolean; // бит CWX (HP байт 5): программный ключ
// ---- OC Control (Open Collector выходы Penny/Alex, byte 1401) ---- // ---- OC Control (Open Collector выходы Penny/Alex, byte 1401) ----
FOCConfig: TOCSettings; FOCConfig: TOCSettings;
FTuning: Boolean; // TUN активен (для TX pin action) FTuning: Boolean; // TUN активен (для TX pin action)
@@ -269,6 +270,7 @@ type
Transmitting, PAEnabled, AlexEnabled: Boolean; Transmitting, PAEnabled, AlexEnabled: Boolean;
Tuning: Boolean = False; TwoTone: Boolean = False); override; Tuning: Boolean = False; TwoTone: Boolean = False); override;
procedure SetStepAtten(dB: Byte); override; procedure SetStepAtten(dB: Byte); override;
procedure SetCWXKey(Down: Boolean); override;
procedure SetFreqCalPPM(P: Double); override; procedure SetFreqCalPPM(P: Double); override;
// Частота с поправкой опорника — для частотного слова DDC/DUC. // Частота с поправкой опорника — для частотного слова DDC/DUC.
function CalFreq(Hz: Double): Double; function CalFreq(Hz: Double): Double;
@@ -1801,6 +1803,15 @@ begin
FFreqCalPPM := P; FFreqCalPPM := P;
end; 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); procedure THPSDRNetwork.SetStepAtten(dB: Byte);
begin begin
if dB > 31 then dB := 31; if dB > 31 then dB := 31;
@@ -1840,6 +1851,9 @@ begin
if FPTTActive then Buf[4] := Buf[4] or HP_PTT0; if FPTTActive then Buf[4] := Buf[4] or HP_PTT0;
end; 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: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10)
IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10 IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10
DDCBase := DDCBaseIndex; DDCBase := DDCBaseIndex;
+66 -1
View File
@@ -43,7 +43,7 @@ uses
WinFirewall, WinFirewall,
BoardUtils, WisdomBuilder, UISync, BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater, FlatEdit, FMRepeater,
ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder, ChannelStore, ChannelsForm, CWMessagesForm, BeaconScopeForm, DMRDecoder,
PowerInhibit, PowerInhibit,
DeviceStore, DeviceStore,
RadioBackend, PlutoBackend, RadioBackend, PlutoBackend,
@@ -367,6 +367,7 @@ type
BtnTXProfile: TFlatButton; BtnTXProfile: TFlatButton;
FTXProfileDropDown: TFlatDropDown; FTXProfileDropDown: TFlatDropDown;
FChannelsForm: TObject; // TChannelsForm (cast при использовании) FChannelsForm: TObject; // TChannelsForm (cast при использовании)
FCWMsgForm: TObject; // TCWMessagesForm (память телеграфа)
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON) FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
// ---- Right panel ---- // ---- Right panel ----
@@ -763,6 +764,11 @@ type
procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna) procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna)
procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
procedure OnTXSettingsChange(const T: TTXSettings); 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 BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject); procedure TrkAGCChange(Sender: TObject);
procedure TrkRxGainChange(Sender: TObject); procedure TrkRxGainChange(Sender: TObject);
@@ -1065,7 +1071,12 @@ begin
FController.FTXSpecGridStep := 10.0; FController.FTXSpecGridStep := 10.0;
FSettingsForm := nil; FSettingsForm := nil;
FChannelsForm := nil; FChannelsForm := nil;
FCWMsgForm := nil;
FBeaconScopeForm := nil; FBeaconScopeForm := nil;
// F1..F8 для памяти телеграфа. Обработчик молчит, пока окно памяти не
// открывали и пока режим не CW, поэтому прочим клавишам ничего не мешает.
KeyPreview := True;
OnKeyDown := FormKeyDownCW;
FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0); FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями // Инициализируем кэш диапазонов умолчаниями
@@ -3400,6 +3411,7 @@ begin
T := CurrentAppTheme; T := CurrentAppTheme;
if FDeviceDialog <> nil then FDeviceDialog.SetTheme(T); if FDeviceDialog <> nil then FDeviceDialog.SetTheme(T);
if FChannelsForm <> nil then TChannelsForm(FChannelsForm).ApplyTheme(T); if FChannelsForm <> nil then TChannelsForm(FChannelsForm).ApplyTheme(T);
if FCWMsgForm <> nil then TCWMessagesForm(FCWMsgForm).ApplyTheme(T);
if FBeaconScopeForm <> nil then FBeaconScopeForm.ApplyTheme(T); if FBeaconScopeForm <> nil then FBeaconScopeForm.ApplyTheme(T);
FController.FSettings.SaveTheme(V); FController.FSettings.SaveTheme(V);
FController.FSettings.Save; FController.FSettings.Save;
@@ -5831,6 +5843,7 @@ begin
TSettingsForm(FSettingsForm).LoadTXSettings(FController.FTXSettings, TSettingsForm(FSettingsForm).LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected, IfThen(FController.FNetwork.Connected,
FController.FNetwork.Device.BoardType, FPendingBoardType)); FController.FNetwork.Device.BoardType, FPendingBoardType));
TSettingsForm(FSettingsForm).LoadCWSettings(FController.CWSettings);
PushTXProfilesToSettings; PushTXProfilesToSettings;
end; end;
// Пилюли профиля на флагах: список общий, привязки могли съехать после // Пилюли профиля на флагах: список общий, привязки могли съехать после
@@ -8122,6 +8135,54 @@ begin
end; end;
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); procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
begin begin
FController.FAlexSettings := A; FController.FAlexSettings := A;
@@ -9047,6 +9108,8 @@ begin
SF.OnThemeChange := SetLightTheme; SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange; SF.OnTXChange := OnTXSettingsChange;
SF.OnCWChange := OnCWSettingsChange;
SF.OnCWMessagesOpen := ShowCWMessages;
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings; SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
SF.OnTXProfileAdd := OnTXProfileAddFromSettings; SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
SF.OnTXProfileRename := OnTXProfileRenameFromSettings; SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
@@ -9063,6 +9126,7 @@ begin
PushTXProfilesToSettings; PushTXProfilesToSettings;
SF.LoadTXSettings(FController.FTXSettings, SF.LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadCWSettings(FController.CWSettings);
SF.LoadAlexSettings(FController.FAlexSettings, SF.LoadAlexSettings(FController.FAlexSettings,
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
// Вкладка Antenna: у AD936x (Pluto/LibreSDR) вместо Alex-таблицы — таблица // Вкладка Antenna: у AD936x (Pluto/LibreSDR) вместо Alex-таблицы — таблица
@@ -9159,6 +9223,7 @@ begin
EnsureWDSPWisdom; EnsureWDSPWisdom;
end; end;
// =========================================================================== // ===========================================================================
// CAT subsystem // CAT subsystem
// =========================================================================== // ===========================================================================
+8
View File
@@ -180,6 +180,10 @@ type
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); virtual; procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); virtual;
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual; procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual;
procedure SendHighPriority(const Pkt: THighPriorityPacket); 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; procedure SetStepAtten(dB: Byte); virtual;
// RX-усиление (Pluto/AD936x). ModeIdx: 0=manual, 1=fast_attack, 2=slow_attack, // RX-усиление (Pluto/AD936x). ModeIdx: 0=manual, 1=fast_attack, 2=slow_attack,
// 3=hybrid. GainDb применяется только в manual. HPSDR — no-op. // 3=hybrid. GainDb применяется только в manual. HPSDR — no-op.
@@ -277,6 +281,10 @@ procedure TRadioBackend.SendHighPriority(const Pkt: THighPriorityPacket);
begin begin
end; end;
procedure TRadioBackend.SetCWXKey(Down: Boolean);
begin
end;
procedure TRadioBackend.SetStepAtten(dB: Byte); procedure TRadioBackend.SetStepAtten(dB: Byte);
begin begin
end; end;
+312 -21
View File
@@ -50,7 +50,7 @@ uses
Classes, SysUtils, Math, Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings, HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder, WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse;
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для // Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1). // движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
@@ -433,6 +433,22 @@ type
// TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent; // TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent;
// активный профиль ведётся за ними (StoreActiveTXProfile). // активный профиль ведётся за ними (StoreActiveTXProfile).
FTXProfiles: TTXProfileList; 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; FAlexSettings: TAlexSettings;
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only) FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер. // Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
@@ -584,6 +600,15 @@ type
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP 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 (авто по плате) function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
// ---- TX-профили -------------------------------------------------------- // ---- TX-профили --------------------------------------------------------
@@ -917,6 +942,19 @@ type
procedure SetMOX(On_: Boolean); procedure SetMOX(On_: Boolean);
procedure SetTune(On_: Boolean); // TUN 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 ---- // ---- PureSignal ----
// Доступность PS у активного бэкенда/платы (Caps.HasPureSignal). // Доступность PS у активного бэкенда/платы (Caps.HasPureSignal).
function PSAvailable: Boolean; function PSAvailable: Boolean;
@@ -1108,6 +1146,8 @@ begin
DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры
ApplyFilterSlot(FFilter); ApplyFilterSlot(FFilter);
FAGCMode := 1; FAGCTop := 90; FAGCMode := 1; FAGCTop := 90;
TSettingsManager.DefaultCW(FCWSettings); // до подключения — заводской телеграф
FLastCWOptions := $FF; // «ещё не слали» — первый пуш пройдёт
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX) FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0; FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
@@ -1239,6 +1279,12 @@ end;
destructor TRadioController.Destroy; destructor TRadioController.Destroy;
begin begin
// Поток передачи телеграфа гасим ПЕРВЫМ: его колбэк ключа лезет в бэкенд.
if Assigned(FCWSender) then
begin
FCWSender.AbortSending;
FreeAndNil(FCWSender);
end;
FreeEngines; FreeEngines;
FreeAndNil(FDeviceStore); FreeAndNil(FDeviceStore);
FreeAndNil(FSettings); FreeAndNil(FSettings);
@@ -1385,6 +1431,7 @@ var
SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double; SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
Bi: Integer; Bi: Integer;
Is6m, IsTx: Boolean; Is6m, IsTx: Boolean;
KeyDot, KeyDash: Boolean;
begin begin
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
@@ -1447,6 +1494,17 @@ begin
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0; FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0; FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload; 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); Invoke(ApplyHPStatus);
end; end;
@@ -1533,6 +1591,19 @@ begin
// the independent pre-volume tap is handled by OnDemodAudioReady. // the independent pre-volume tap is handled by OnDemodAudioReady.
if FMode in [MODE_DMR, MODE_FMRAW] then Exit; 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 if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count); FNetwork.SendSpeakerAudio(Left, Right, Count);
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
@@ -2255,6 +2326,7 @@ begin
ActiveMicInput.Flush; ActiveMicInput.Flush;
end; end;
end; end;
if FTxSliceId = Id then SyncCWKeyer;
end; end;
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer); procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
@@ -2909,17 +2981,26 @@ begin
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте // Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем). // (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then
Exit(S.TargetHz); Result := S.TargetHz
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B), else
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал begin
// за активным RX (TX-A горит, но передача шла на B при активном B). // TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
if FSplitTxB then Result := FVfoB // независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
else Result := FVfoA; // за активным RX (TX-A горит, но передача шла на B при активном B).
if FMode = MODE_FM then if FSplitTxB then Result := FVfoB
case FFMRptDir of else Result := FVfoA;
RPT_MINUS: Result := Result - FFMRptOffsetHz; if FMode = MODE_FM then
RPT_PLUS: Result := Result + FFMRptOffsetHz; case FFMRptDir of
end; 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; end;
function TRadioController.ActiveTXMode: Integer; function TRadioController.ActiveTXMode: Integer;
@@ -2975,6 +3056,7 @@ begin
// без сброса первые ~200мс передачи ушли бы в эфир «из прошлого». // без сброса первые ~200мс передачи ушли бы в эфир «из прошлого».
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
ActiveMicInput.Flush; ActiveMicInput.Flush;
SyncCWKeyer; // кейер прошивки — под режим нового источника
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
@@ -4043,8 +4125,15 @@ begin
ActiveMicInput.Flush; ActiveMicInput.Flush;
end; end;
// Если TUN активен — пересчитываем знак частоты под новый режим // Если 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; end;
SyncCWKeyer;
ApplyModeFilter; ApplyModeFilter;
Changed(rfMode); Changed(rfMode);
end; end;
@@ -4941,21 +5030,65 @@ begin
FDSPEngine.SetANF(FANF); FDSPEngine.SetANF(FANF);
end; 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; procedure TRadioController.SendDUCSpecificFromSettings;
var var
DUCPkt: TDUCSpecificPacket; DUCPkt: TDUCSpecificPacket;
RateKsps: Word; RateKsps: Word;
HangMs: Word;
begin begin
if not (FNetwork.Connected and FNetwork.Running) then Exit; if not (FNetwork.Connected and FNetwork.Running) then Exit;
FillChar(DUCPkt, SizeOf(DUCPkt), 0); FillChar(DUCPkt, SizeOf(DUCPkt), 0);
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX). // DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
RateKsps := FDSPEngine.TXSampleRate div 1000; RateKsps := FDSPEngine.TXSampleRate div 1000;
DUCPkt.NumDACs := 1; DUCPkt.NumDACs := 1;
DUCPkt.SidetoneLevel := 50; DUCPkt.CWOptions := CWOptionsByte;
DUCPkt.SidetoneFreqHi := Hi(600); // Уровень/частота аппаратного сайдтона — тон в наушниках САМОГО трансивера.
DUCPkt.SidetoneFreqLo := Lo(600); if FCWSettings.SidetoneHW then
DUCPkt.KeyerSpeed := 20; DUCPkt.SidetoneLevel := Byte(EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127))
DUCPkt.KeyerWeight := 50; 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.DUC0RateHi := Hi(RateKsps);
DUCPkt.DUC0RateLo := Lo(RateKsps); DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24; DUCPkt.DUC0Bits := 24;
@@ -4969,6 +5102,7 @@ begin
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31)); DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
DUCPkt.StepAtten1 := DUCPkt.StepAtten0; DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
DUCPkt.StepAtten2 := DUCPkt.StepAtten0; DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
FLastCWOptions := DUCPkt.CWOptions;
FNetwork.SendDUCSpecific(DUCPkt); FNetwork.SendDUCSpecific(DUCPkt);
end; end;
@@ -5316,6 +5450,9 @@ begin
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
Exit; Exit;
WasTransmitting := FTransmitting; WasTransmitting := FTransmitting;
// Снятие MOX руками обрывает программную передачу текста: оператор решил
// прекратить — ключ обязан отпуститься немедленно.
if (not On_) and CWXBusy then CWXAbort;
FTransmitting := On_; FTransmitting := On_;
// 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник. // 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник.
if (not On_) and FTwoTone then if (not On_) and FTwoTone then
@@ -5386,13 +5523,27 @@ begin
ActiveMicInput.Flush; ActiveMicInput.Flush;
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. // 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: передающий тракт под режим слайса-источника (SetMode мог
// оставить главный режим). Ставим перед стартом TX-потока. // оставить главный режим). Ставим перед стартом TX-потока.
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode); if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc). // PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
FDSPEngine.PSSetMox(FTransmitting); 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 if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin begin
FDSPEngine.FlushRX; FDSPEngine.FlushRX;
@@ -5439,8 +5590,16 @@ begin
if On_ then if On_ then
begin begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
if FWDSPReady and Assigned(FDSPEngine) then 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; FTuning := True;
FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень) FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень)
ApplyWDSPDrive(FTXSettings.TUNLevel); ApplyWDSPDrive(FTXSettings.TUNLevel);
@@ -5465,6 +5624,131 @@ begin
Changed(rfTuning); Changed(rfTuning);
end; 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) // PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
// =========================================================================== // ===========================================================================
@@ -5964,6 +6248,10 @@ begin
ApplyAntPorts; ApplyAntPorts;
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено. // QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
ApplyRxMuteToEngine; ApplyRxMuteToEngine;
// Тон телеграфа в движок: на момент ConnectDevice канал WDSP мог быть ещё
// не открыт, а здесь он гарантированно жив.
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
SendDUCSpecificFromSettings; SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...). // Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP; ApplyTXSettingsToDSP;
@@ -6175,6 +6463,9 @@ begin
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache); FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
// TX/Alex/XVTR per-device // TX/Alex/XVTR per-device
FSettings.LoadTX(FDevMAC, FTXSettings); FSettings.LoadTX(FDevMAC, FTXSettings);
FSettings.LoadCW(FDevMAC, FCWSettings);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
FSettings.LoadAlex(FDevMAC, FAlexSettings); FSettings.LoadAlex(FDevMAC, FAlexSettings);
FSettings.LoadAnt936x(FDevMAC, FAnt936x); FSettings.LoadAnt936x(FDevMAC, FAnt936x);
FNetwork.SetAlexConfig(FAlexSettings); FNetwork.SetAlexConfig(FAlexSettings);
+121
View File
@@ -28,6 +28,7 @@ const
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090) CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
CW_MSG_COUNT = 8; // ячеек памяти телеграфных сообщений (F1..F8)
TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
@@ -306,6 +307,39 @@ type
// (Orion MkII → 5.0, иначе выкл.), 0 = выкл. // (Orion MkII → 5.0, иначе выкл.), 0 = выкл.
end; 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-профили (именованные снимки «как я звучу») ----------------------
// Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию // Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию
// микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку, // микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку,
@@ -646,6 +680,10 @@ type
// в любом случае T заполняется (дефолтами или сохранёнными значениями). // в любом случае T заполняется (дефолтами или сохранёнными значениями).
function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean; function LoadTX(const MAC: array of Byte; out T: TTXSettings): Boolean;
procedure SaveTX(const MAC: array of Byte; const T: TTXSettings); 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 function MacToStr(const MAC: array of Byte): string;
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings); class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
class procedure DefaultGlobal(out G: TGlobalSettings); class procedure DefaultGlobal(out G: TGlobalSettings);
@@ -1733,6 +1771,89 @@ begin
T.PSOutlierSigma := JD(O,'ps_outlier_sigma', T.PSOutlierSigma); T.PSOutlierSigma := JD(O,'ps_outlier_sigma', T.PSOutlierSigma);
end; 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; procedure TSettingsManager.SaveTX(const MAC: array of Byte;
const T: TTXSettings); const T: TTXSettings);
var O: TJSONObject; i: Integer; var O: TJSONObject; i: Integer;
+152
View File
@@ -59,6 +59,8 @@ type
TOnCalibrationChange = procedure(const C: TCalibration) of object; TOnCalibrationChange = procedure(const C: TCalibration) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой) // TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object; TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
TOnCWSettingsChange = procedure(const C: TCWSettings) of object;
TOnCWMessagesOpen = procedure of object;
// TX-профили: выбор/переименование/удаление по индексу, создание — по имени. // TX-профили: выбор/переименование/удаление по индексу, создание — по имени.
TOnTXProfileIdx = procedure(Idx: Integer) of object; TOnTXProfileIdx = procedure(Idx: Integer) of object;
TOnTXProfileName = procedure(const AName: string) of object; TOnTXProfileName = procedure(const AName: string) of object;
@@ -285,6 +287,23 @@ type
// CTCSS // CTCSS
FChkTXCTCSSOn: TFlatCheckBox; FChkTXCTCSSOn: TFlatCheckBox;
FEdTXCTCSSFreq: TFlatFloatSpinEdit; 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) // TX Display (отдельный analyzer)
FCmbTXFFT: TFlatComboBox; FCmbTXFFT: TFlatComboBox;
FCmbTXWindow: TFlatComboBox; FCmbTXWindow: TFlatComboBox;
@@ -446,6 +465,8 @@ type
FOnWfAGCNFChange: TOnWfAGCNFChange; FOnWfAGCNFChange: TOnWfAGCNFChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange; FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange; FOnTXChange: TOnTXSettingsChange;
FOnCWChange: TOnCWSettingsChange;
FOnCWMessagesOpen: TOnCWMessagesOpen;
FOnTXProfileSelect: TOnTXProfileIdx; FOnTXProfileSelect: TOnTXProfileIdx;
FOnTXProfileAdd: TOnTXProfileName; FOnTXProfileAdd: TOnTXProfileName;
FOnTXProfileRename: TOnTXProfileIdxName; FOnTXProfileRename: TOnTXProfileIdxName;
@@ -500,6 +521,10 @@ type
procedure CollectTXFromUI; procedure CollectTXFromUI;
procedure FireTXChange; procedure FireTXChange;
procedure OnTXAnyChange(Sender: TObject); procedure OnTXAnyChange(Sender: TObject);
procedure CollectCWFromUI;
procedure FireCWChange;
procedure OnCWAnyChange(Sender: TObject);
procedure BtnCWMessagesClick(Sender: TObject);
procedure OnMicJackChange(Sender: TObject); procedure OnMicJackChange(Sender: TObject);
procedure UpdateMicJackVisibility; procedure UpdateMicJackVisibility;
// Подвкладки и полоса профиля на странице Transmit // Подвкладки и полоса профиля на странице Transmit
@@ -632,6 +657,7 @@ type
const SerAndromeda: array of Boolean; const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer); TcpEnabled: Boolean; TcpPort: Integer);
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0); procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
procedure LoadCWSettings(const C: TCWSettings);
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в // Список TX-профилей + активный + drive активного (ползунок DRV живёт в
// главном окне, здесь показываем его значение как часть профиля). // главном окне, здесь показываем его значение как часть профиля).
procedure LoadTXProfiles(const Names: array of string; procedure LoadTXProfiles(const Names: array of string;
@@ -678,6 +704,8 @@ type
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange; property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange; property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange; 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 OnTXProfileSelect: TOnTXProfileIdx read FOnTXProfileSelect write FOnTXProfileSelect;
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd; property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename; property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
@@ -1795,6 +1823,27 @@ var
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange); Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
end; 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; procedure TxNote(AParent: TWinControl; const AText: string;
ATop, AHeight: Integer); ATop, AHeight: Integer);
var var
@@ -1991,7 +2040,49 @@ begin
TxNote(Grp, 'Skirt steepness and path latency, not timbre — hence outside ' TxNote(Grp, 'Skirt steepness and path latency, not timbre — hence outside '
+ 'the profile.', R1 + 3 * STEP - 2, 22); + 'the profile.', R1 + 3 * STEP - 2, 22);
// ---- CW: телеграфом в openHPSDR P2 управляет прошивка -------------------
Inc(Y, 190 + 12); 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); Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW); MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
@@ -2179,6 +2270,67 @@ begin
FireTXChange; FireTXChange;
end; 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; procedure TSettingsForm.UpdateMicJackVisibility;
var LineIn: Boolean; var LineIn: Boolean;
begin begin
+100 -12
View File
@@ -472,6 +472,7 @@ type
FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс) FLastSpectrumW: Integer; // последняя известная ширина спектра (пикс)
FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels) FDisplayResLimit: Integer; // потолок точек анализатора (настройка display_pixels)
FShiftHz: Double; // NCO shift для CTUN FShiftHz: Double; // NCO shift для CTUN
FCWPitch: Integer; // Гц, тон телеграфа (см. CWLOOffset)
FNRMode: Integer; FNRMode: Integer;
FNBMode: Integer; FNBMode: Integer;
FSNBEnabled: Boolean; FSNBEnabled: Boolean;
@@ -505,6 +506,14 @@ type
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака, // Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB. // через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double); 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 // Mode-dependent TX overlay. FM RAW uses the flat WFM modulator and
// bypasses every speech processor without overwriting the saved TX setup. // bypasses every speech processor without overwriting the saved TX setup.
procedure ApplyTXModeSettings(Mode: Integer); procedure ApplyTXModeSettings(Mode: Integer);
@@ -641,6 +650,14 @@ type
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096) procedure SetDisplayResLimit(V: Integer); // потолок точек анализатора (1024/2048/4096)
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг 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 SetNRMode(Mode: Integer);
procedure SetNR(Enable: Boolean); procedure SetNR(Enable: Boolean);
procedure SetNBMode(Mode: Integer); procedure SetNBMode(Mode: Integer);
@@ -1231,6 +1248,35 @@ begin
end; end;
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); procedure TWDSPEngine.ApplyTXModeSettings(Mode: Integer);
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
@@ -1323,6 +1369,7 @@ begin
FFilterHigh := 2400; FFilterHigh := 2400;
FAGCMode := agcMedium; FAGCMode := agcMedium;
FShiftHz := 0.0; FShiftHz := 0.0;
FCWPitch := 600;
FAGCTop := -90.0; FAGCTop := -90.0;
FVolume := 0.7; FVolume := 0.7;
FSMeter := -130.0; FSMeter := -130.0;
@@ -1932,8 +1979,8 @@ begin
SetRXABandpassWindow(RXA_CHAN, 1); SetRXABandpassWindow(RXA_CHAN, 1);
SetRXAMode(RXA_CHAN, ModeToWDSP(FMode)); SetRXAMode(RXA_CHAN, ModeToWDSP(FMode));
SetRXAShiftRun(RXA_CHAN, 1); SetRXAShiftRun(RXA_CHAN, 1);
SetRXAShiftFreq(RXA_CHAN, FShiftHz); PushShift(RXA_CHAN, FShiftHz, FMode);
RXASetPassband(RXA_CHAN, FFilterLow, FFilterHigh); PushPassband(RXA_CHAN, FFilterLow, FFilterHigh, FMode, FRXDSPRate);
SetAGC(FAGCMode, 50.0); SetAGC(FAGCMode, 50.0);
SetRXAPanelGain1(RXA_CHAN, FVolume); SetRXAPanelGain1(RXA_CHAN, FVolume);
SetRXAPanelSelect(RXA_CHAN, 3); SetRXAPanelSelect(RXA_CHAN, 3);
@@ -2457,9 +2504,9 @@ begin
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
ApplyRXModeSettings(Chan, FSlices[Idx].Mode); ApplyRXModeSettings(Chan, FSlices[Idx].Mode);
SetRXAShiftRun(Chan, 1); SetRXAShiftRun(Chan, 1);
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); PushShift(Chan, FSlices[Idx].ShiftHz, FSlices[Idx].Mode);
RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), PushPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi,
ClampToNyquist(FSlices[Idx].FilterHi, DSPRate)); FSlices[Idx].Mode, DSPRate);
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0); ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
// NR/NB/SNB/ANF из сохранённого состояния слайса. // NR/NB/SNB/ANF из сохранённого состояния слайса.
ApplyNRToChan(Chan, FSlices[Idx].NRMode); ApplyNRToChan(Chan, FSlices[Idx].NRMode);
@@ -2733,7 +2780,8 @@ begin
try try
idx := FindSliceIdx(Id); if idx < 0 then Exit; idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].ShiftHz := ShiftHz; 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 finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
@@ -2768,6 +2816,14 @@ begin
// шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор. // шумодава в не-FM режимах нет); вернулись в FM — восстановить выбор.
ApplySliceFMSQ(idx); ApplySliceFMSQ(idx);
end; 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 finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
@@ -2784,8 +2840,7 @@ begin
if FSlices[idx].Opened then if FSlices[idx].Opened then
begin begin
Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx)); Rate := SliceDSPRateFor(FSlices[idx].Mode, SliceInRate(idx));
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate), PushPassband(FSlices[idx].Chan, Low, High, FSlices[idx].Mode, Rate);
ClampToNyquist(High, Rate));
end; end;
finally finally
FSliceLock.Leave; FSliceLock.Leave;
@@ -2965,6 +3020,9 @@ begin
ApplyDSPRate(Mode); ApplyDSPRate(Mode);
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode)); SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
ApplyRXModeSettings(RXA_CHAN, Mode); ApplyRXModeSettings(RXA_CHAN, Mode);
// Вход/выход из телеграфа меняет CWLOOffset: гетеродин надо переложить сразу
// (кромки приедут из UI следом, но сдвиг оттуда не приходит).
PushShift(RXA_CHAN, FShiftHz, Mode);
ApplyTXMode(FTXMode); ApplyTXMode(FTXMode);
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end; end;
@@ -2978,9 +3036,7 @@ begin
// до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что // до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что
// уходит в WDSP — запрошенные значения храним как есть, иначе понижение // уходит в WDSP — запрошенные значения храним как есть, иначе понижение
// SampleRate необратимо «съело» бы полосу. // SampleRate необратимо «съело» бы полосу.
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком PushPassband(RXA_CHAN, Low, High, FMode, FRXDSPRate);
RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate),
ClampToNyquist(High, FRXDSPRate));
end; end;
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double); procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
@@ -2999,7 +3055,39 @@ procedure TWDSPEngine.SetShift(ShiftHz: Double);
begin begin
FShiftHz := ShiftHz; FShiftHz := ShiftHz;
if not FInitialized then Exit; 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; end;
procedure TWDSPEngine.SetAGCTop(TopDBm: Double); procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
+8
View File
@@ -308,6 +308,14 @@
<Filename Value="ChannelsForm.pas"/> <Filename Value="ChannelsForm.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>
</Unit> </Unit>
<Unit>
<Filename Value="CWMorse.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="CWMessagesForm.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit> <Unit>
<Filename Value="FlatSpinEdit.pas"/> <Filename Value="FlatSpinEdit.pas"/>
<IsPartOfProject Value="True"/> <IsPartOfProject Value="True"/>