mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
@@ -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
@@ -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
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+300
-9
@@ -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,7 +2981,9 @@ 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
|
||||||
|
else
|
||||||
|
begin
|
||||||
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
||||||
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
|
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
|
||||||
// за активным RX (TX-A горит, но передача шла на B при активном B).
|
// за активным RX (TX-A горит, но передача шла на B при активном B).
|
||||||
@@ -2921,6 +2995,13 @@ begin
|
|||||||
RPT_PLUS: Result := Result + FFMRptOffsetHz;
|
RPT_PLUS: Result := Result + FFMRptOffsetHz;
|
||||||
end;
|
end;
|
||||||
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;
|
function TRadioController.ActiveTXMode: Integer;
|
||||||
var S: TCtrlSlice;
|
var S: TCtrlSlice;
|
||||||
@@ -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,12 +5523,26 @@ 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);
|
||||||
|
// ★Телеграф: голосовой 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);
|
FDSPEngine.SetTXRun(FTransmitting);
|
||||||
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
||||||
begin
|
begin
|
||||||
@@ -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
|
||||||
|
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);
|
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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user