mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Merge feature/cw: телеграф (кейер прошивки + программный), терминал и декодер CW
Телеграф на openHPSDR P2 и Pluto/LibreSDR: кейер в FPGA (тайминги в железе) и программный кейер для бэкендов без него, pitch как ЕДИНЫЙ сдвиг гетеродина в движке, сайдтон, CWX/F1..F8, CAT KY/KS/ZZKM, терминал с набором с клавиатуры и декодером приёма. Голос в CW заблокирован, реле T/R и антенна ходят за ключом прошивки, запрет передачи (RX-only XVTR, DoNotTx) действует и на кейер FPGA. Попутно: окно повторов Specific-пакетов только со старта (e87ca6b), правки в трансвертере больше не травят память КВ-диапазона (827902f), перестройка слайса по CAT внутри полосы захвата обновляет флаг (00860e5). 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;
|
||||||
|
|||||||
+462
@@ -0,0 +1,462 @@
|
|||||||
|
unit CWDecoder;
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Декодер телеграфа: RX-аудио → текст.
|
||||||
|
//
|
||||||
|
// ОТКУДА СИГНАЛ. Отвод берётся ДО громкости и мьюта (OnDemodAudioReady) — там
|
||||||
|
// нет программного сайдтона (он подмешивается позже) и там уже отработал узкий
|
||||||
|
// CW-фильтр приёмника. Лучшего предетектора не найти: полоса 250-500 Гц режет
|
||||||
|
// всё, кроме корреспондента, а тон стоит ровно на pitch, потому что сдвиг
|
||||||
|
// гетеродина в телеграфе делаем мы сами (WDSPEngine.CWLOOffset).
|
||||||
|
//
|
||||||
|
// ЦЕПОЧКА. Гёрцель гребёнкой бинов вокруг pitch (оператор редко попадает в
|
||||||
|
// ноль, поэтому берём максимум и заодно знаем расстройку) → огибающая →
|
||||||
|
// адаптивный порог с гистерезисом → длительности посылок и пауз → адаптивная
|
||||||
|
// ТОЧКА → дерево Морзе.
|
||||||
|
//
|
||||||
|
// ★Длительности живут в ШАГАХ анализа (то есть в сэмплах), а не в показаниях
|
||||||
|
// часов: разбор идёт пачками с таймера окна, и привязка ко времени вызова
|
||||||
|
// ломала бы тайминг на любой загрузке. Ровно то же правило, что в CWKeyer.
|
||||||
|
//
|
||||||
|
// ГРАНИЦА, которую надо знать. Машинную передачу (кейер, компьютер, маяк) такой
|
||||||
|
// декодер читает почти без ошибок; рукопашный «фист» с плавающим весом — плохо.
|
||||||
|
// Это свойство задачи, а не реализации: у fldigi и трансиверов с декодером на
|
||||||
|
// борту ровно та же картина.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, SyncObjs, Math, CWMorse;
|
||||||
|
|
||||||
|
const
|
||||||
|
CWD_RATE = 48000; // rate отвода аудио
|
||||||
|
CWD_N = 512; // окно Гёрцеля (10.7 мс — полоса ~100 Гц)
|
||||||
|
CWD_HOP = 256; // шаг анализа (5.33 мс), окна перекрываются вдвое
|
||||||
|
CWD_BINS = 5; // бинов в гребёнке (центр ± 2 шага)
|
||||||
|
CWD_BIN_HZ = 100; // шаг гребёнки, Гц
|
||||||
|
|
||||||
|
type
|
||||||
|
TCWDecoder = class
|
||||||
|
private
|
||||||
|
FLock: TCriticalSection;
|
||||||
|
// ---- кольцо аудио (продюсер — DSP-поток, консьюмер — Process) ----------
|
||||||
|
FRing: array of Single;
|
||||||
|
FHead: Integer;
|
||||||
|
FTail: Integer;
|
||||||
|
// ---- параметры ---------------------------------------------------------
|
||||||
|
FPitch: Integer;
|
||||||
|
FEnabled: Boolean;
|
||||||
|
// ---- гребёнка Гёрцеля --------------------------------------------------
|
||||||
|
FCoef: array[0..CWD_BINS-1] of Double;
|
||||||
|
FWin: array[0..CWD_N-1] of Double; // окно Ханна
|
||||||
|
FBuf: array[0..CWD_N-1] of Double; // оконный кадр
|
||||||
|
FBestBin: Integer;
|
||||||
|
FBestPow: Double; // уровень лучшего бина (для гейта расстройки)
|
||||||
|
// ---- пороги ------------------------------------------------------------
|
||||||
|
FNoise: Double;
|
||||||
|
FPeak: Double;
|
||||||
|
FMarkNow: Boolean;
|
||||||
|
FRunLen: Integer; // длина текущего состояния в шагах
|
||||||
|
FCandLen: Integer; // длина «дребезга» — кандидата на смену состояния
|
||||||
|
FCandMark: Boolean;
|
||||||
|
FWarmup: Integer; // шаги до первого решения (пороги должны устояться)
|
||||||
|
FDebounce: Integer; // сколько шагов подряд держать новое состояние
|
||||||
|
// ---- разбор ------------------------------------------------------------
|
||||||
|
FDitMs: Double; // адаптивная длина точки
|
||||||
|
FPattern: string; // накопленный знак ('.' и '-')
|
||||||
|
FIdleSteps: Integer; // тишина после последнего знака (шаги)
|
||||||
|
FWordDone: Boolean; // межсловный пробел уже выдан
|
||||||
|
FOut: string; // очередь декодированного текста (под FLock)
|
||||||
|
FSignal: Boolean;
|
||||||
|
procedure BuildTables;
|
||||||
|
procedure StepEnvelope(Mag: Double);
|
||||||
|
procedure OnMark(Steps: Integer);
|
||||||
|
procedure OnSpace(Steps: Integer);
|
||||||
|
procedure FlushChar;
|
||||||
|
procedure Emit(const S: string);
|
||||||
|
function StepMs: Double;
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
|
||||||
|
procedure SetPitch(Hz: Integer);
|
||||||
|
procedure SetSpeedHint(WPM: Integer); // старт адаптации (своя скорость)
|
||||||
|
procedure Reset;
|
||||||
|
property Enabled: Boolean read FEnabled write FEnabled;
|
||||||
|
|
||||||
|
// DSP-поток: только копирование в кольцо.
|
||||||
|
procedure FeedAudio(const Left: array of Single; Count: Integer);
|
||||||
|
// Поток-хозяин (таймер): разбор накопленного.
|
||||||
|
procedure Process;
|
||||||
|
// Забрать декодированное (и очистить очередь).
|
||||||
|
function TakeText: string;
|
||||||
|
|
||||||
|
function SpeedWPM: Integer; // оценка скорости корреспондента
|
||||||
|
function ToneOffsetHz: Integer; // расстройка: где нашёлся тон
|
||||||
|
function SignalPresent: Boolean;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Обратная таблица: код ('.', '-') → знак. '' если такого кода нет.
|
||||||
|
function MorseDecode(const Code: string): string;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
RING_BITS = 15;
|
||||||
|
RING_SIZE = 1 shl RING_BITS; // 32768 сэмплов ≈ 0.68 с
|
||||||
|
RING_MASK = RING_SIZE - 1;
|
||||||
|
DIT_MIN_MS = 20.0; // 60 WPM
|
||||||
|
DIT_MAX_MS = 240.0; // 5 WPM
|
||||||
|
SNR_GATE = 2.2; // во сколько раз пик должен быть выше шума
|
||||||
|
|
||||||
|
var
|
||||||
|
GCodes: array[0..127] of string; // знак → код, строится один раз
|
||||||
|
GBuilt: Boolean = False;
|
||||||
|
|
||||||
|
procedure BuildCodeTable;
|
||||||
|
var C: Char;
|
||||||
|
begin
|
||||||
|
if GBuilt then Exit;
|
||||||
|
for C := #32 to #126 do GCodes[Ord(C)] := MorseCode(C);
|
||||||
|
GBuilt := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function MorseDecode(const Code: string): string;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
if Code = '' then Exit;
|
||||||
|
BuildCodeTable;
|
||||||
|
for i := 32 to 126 do
|
||||||
|
if GCodes[i] = Code then
|
||||||
|
begin
|
||||||
|
// Таблица кодирования отдаёт заглавные — их и печатаем.
|
||||||
|
Result := UpCase(Chr(i));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Служебные сочетания, которых нет в таблице передачи (принимают их часто).
|
||||||
|
if Code = '...-.-' then Result := '<SK>'
|
||||||
|
else if Code = '-.--.' then Result := '<KN>'
|
||||||
|
else if Code = '.-...' then Result := '<AS>'
|
||||||
|
else if Code = '........' then Result := '<ERR>';
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ── TCWDecoder ────────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
|
constructor TCWDecoder.Create;
|
||||||
|
begin
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
SetLength(FRing, RING_SIZE);
|
||||||
|
FPitch := 600;
|
||||||
|
FDitMs := 60.0; // 20 WPM — стартовая догадка, дальше адаптация
|
||||||
|
BuildTables;
|
||||||
|
Reset;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TCWDecoder.Destroy;
|
||||||
|
begin
|
||||||
|
FLock.Free;
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWDecoder.StepMs: Double;
|
||||||
|
begin
|
||||||
|
Result := 1000.0 * CWD_HOP / CWD_RATE;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.BuildTables;
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
F: Double;
|
||||||
|
begin
|
||||||
|
for i := 0 to CWD_BINS - 1 do
|
||||||
|
begin
|
||||||
|
F := FPitch + (i - CWD_BINS div 2) * CWD_BIN_HZ;
|
||||||
|
if F < 100 then F := 100;
|
||||||
|
FCoef[i] := 2.0 * Cos(2.0 * Pi * F / CWD_RATE);
|
||||||
|
end;
|
||||||
|
for i := 0 to CWD_N - 1 do
|
||||||
|
FWin[i] := 0.5 - 0.5 * Cos(2.0 * Pi * i / (CWD_N - 1)); // Ханн
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.SetPitch(Hz: Integer);
|
||||||
|
begin
|
||||||
|
Hz := EnsureRange(Hz, 200, 1200);
|
||||||
|
if Hz = FPitch then Exit;
|
||||||
|
FPitch := Hz;
|
||||||
|
BuildTables;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.SetSpeedHint(WPM: Integer);
|
||||||
|
begin
|
||||||
|
WPM := EnsureRange(WPM, 5, 60);
|
||||||
|
FDitMs := 1200.0 / WPM;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.Reset;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FHead := 0; FTail := 0; FOut := '';
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FNoise := 0; FPeak := 0;
|
||||||
|
// Пол шума и пик стартуют с нуля — пока они не устоялись, порог стоит где
|
||||||
|
// попало, и первый же знак прочитался бы с лишним элементом. Полсекунды
|
||||||
|
// молчания в начале никому не мешают: приёмник работает задолго до сигнала.
|
||||||
|
FWarmup := Round(500.0 / StepMs);
|
||||||
|
FMarkNow := False; FRunLen := 0; FCandLen := 0; FCandMark := False;
|
||||||
|
FPattern := ''; FIdleSteps := 0; FWordDone := True;
|
||||||
|
FSignal := False; FBestBin := CWD_BINS div 2;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.FeedAudio(const Left: array of Single; Count: Integer);
|
||||||
|
// DSP-поток. Кольцо на 0.68 с: таймер разбора ходит в разы чаще, а если
|
||||||
|
// подвиснет — потеряем старое, но не сломаем состояние (это лучше, чем ждать).
|
||||||
|
var
|
||||||
|
i, H: Integer;
|
||||||
|
begin
|
||||||
|
if not FEnabled then Exit;
|
||||||
|
if Count > Length(Left) then Count := Length(Left);
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
H := FHead;
|
||||||
|
for i := 0 to Count - 1 do
|
||||||
|
begin
|
||||||
|
FRing[H] := Left[i];
|
||||||
|
H := (H + 1) and RING_MASK;
|
||||||
|
if H = FTail then FTail := (FTail + 1) and RING_MASK; // переполнение
|
||||||
|
end;
|
||||||
|
FHead := H;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.Process;
|
||||||
|
// Поток-хозяин. Гёрцель по перекрывающимся окнам: на каждый шаг — одно решение
|
||||||
|
// «есть тон / нет тона».
|
||||||
|
var
|
||||||
|
Avail, i, k, T, BestK: Integer;
|
||||||
|
S0, S1, S2, W, P, Best: Double;
|
||||||
|
begin
|
||||||
|
if not FEnabled then Exit;
|
||||||
|
while True do
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Avail := (FHead - FTail) and RING_MASK;
|
||||||
|
if Avail < CWD_N then Break; // finally отработает, кольцо не тронуто
|
||||||
|
T := FTail;
|
||||||
|
for i := 0 to CWD_N - 1 do
|
||||||
|
begin
|
||||||
|
FBuf[i] := FRing[T] * FWin[i];
|
||||||
|
T := (T + 1) and RING_MASK;
|
||||||
|
end;
|
||||||
|
FTail := (FTail + CWD_HOP) and RING_MASK;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Best := 0; BestK := FBestBin;
|
||||||
|
for k := 0 to CWD_BINS - 1 do
|
||||||
|
begin
|
||||||
|
S1 := 0; S2 := 0; W := FCoef[k];
|
||||||
|
for i := 0 to CWD_N - 1 do
|
||||||
|
begin
|
||||||
|
S0 := FBuf[i] + W * S1 - S2;
|
||||||
|
S2 := S1;
|
||||||
|
S1 := S0;
|
||||||
|
end;
|
||||||
|
P := S1 * S1 + S2 * S2 - W * S1 * S2;
|
||||||
|
if P > Best then begin Best := P; BestK := k; end;
|
||||||
|
end;
|
||||||
|
FBestPow := Sqrt(Max(0.0, Best)) / (CWD_N / 2);
|
||||||
|
// ★Расстройку запоминаем ТОЛЬКО на посылке: в паузе максимум выбирает шум,
|
||||||
|
// и индикатор в окне плясал бы от −200 до +200 Гц. Между посылками
|
||||||
|
// показываем последнее осмысленное значение.
|
||||||
|
// Только на ПОЛНОЙ амплитуде посылки: на спаде фронта максимум уже
|
||||||
|
// разыгрывают шум и соседи по полосе.
|
||||||
|
if FMarkNow and (FBestPow > FNoise + (FPeak - FNoise) * 0.7) then
|
||||||
|
FBestBin := BestK;
|
||||||
|
StepEnvelope(FBestPow);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.StepEnvelope(Mag: Double);
|
||||||
|
// Порог плавает между полом шума и пиком сигнала: QSB, разные корреспонденты и
|
||||||
|
// разные уровни AGC иначе требовали бы ручной подстройки на каждую связь.
|
||||||
|
// Гистерезис (0.6/0.4) не даёт дребезжать на фронте посылки.
|
||||||
|
var
|
||||||
|
Span, ThrOn, ThrOff: Double;
|
||||||
|
IsMark: Boolean;
|
||||||
|
begin
|
||||||
|
// Пол шума: быстро вниз, очень медленно вверх (иначе он «догоняет» длинное
|
||||||
|
// тире и посылка обрывается посередине).
|
||||||
|
if Mag < FNoise then FNoise := FNoise + (Mag - FNoise) * 0.05
|
||||||
|
else FNoise := FNoise + (Mag - FNoise) * 0.002;
|
||||||
|
if Mag > FPeak then FPeak := Mag
|
||||||
|
else FPeak := FNoise + (FPeak - FNoise) * 0.9995;
|
||||||
|
// ★Пик обязан быть не ниже пола: на старте (и в полной тишине) он ещё нулевой,
|
||||||
|
// размах уходил в минус, порог оказывался НИЖЕ шума — и первым же «знаком»
|
||||||
|
// декодер читал собственный шум, портя начало передачи.
|
||||||
|
if FPeak < FNoise then FPeak := FNoise;
|
||||||
|
|
||||||
|
Span := FPeak - FNoise;
|
||||||
|
FSignal := Span > FNoise * (SNR_GATE - 1.0) + 1e-9;
|
||||||
|
if FWarmup > 0 then
|
||||||
|
begin
|
||||||
|
Dec(FWarmup);
|
||||||
|
Exit; // трекеры набирают уровень, решений не принимаем
|
||||||
|
end;
|
||||||
|
ThrOn := FNoise + Span * 0.6;
|
||||||
|
ThrOff := FNoise + Span * 0.4;
|
||||||
|
|
||||||
|
if FMarkNow then IsMark := Mag > ThrOff
|
||||||
|
else IsMark := Mag > ThrOn;
|
||||||
|
// Пока сигнал не выделяется из шума, посылок не бывает по определению.
|
||||||
|
if not FSignal then IsMark := False;
|
||||||
|
|
||||||
|
// Дребезг не считается сменой состояния: это либо импульсная помеха, либо
|
||||||
|
// провал на границе окна. Порог берём от текущей точки (четверть её длины,
|
||||||
|
// но не меньше двух шагов): фиксированный либо пропускал бы щелчки на 20 WPM,
|
||||||
|
// либо съедал бы настоящие посылки на 60.
|
||||||
|
FDebounce := Round(0.25 * FDitMs / StepMs);
|
||||||
|
if FDebounce < 2 then FDebounce := 2;
|
||||||
|
if IsMark <> FMarkNow then
|
||||||
|
begin
|
||||||
|
if FCandMark = IsMark then Inc(FCandLen)
|
||||||
|
else begin FCandMark := IsMark; FCandLen := 1; end;
|
||||||
|
if FCandLen >= FDebounce then
|
||||||
|
begin
|
||||||
|
// ★Длину меряем ЗА ВЫЧЕТОМ подтверждения: пока копился кандидат, сигнал
|
||||||
|
// уже сменился, и эти шаги принадлежат новому состоянию. Без поправки
|
||||||
|
// каждая посылка выходила длиннее на порог дребезга — на 20 WPM это
|
||||||
|
// давало оценку 17 WPM и смещало границу «точка/тире».
|
||||||
|
if FMarkNow then OnMark(FRunLen - (FDebounce - 1))
|
||||||
|
else OnSpace(FRunLen - (FDebounce - 1));
|
||||||
|
FMarkNow := IsMark;
|
||||||
|
FRunLen := FDebounce;
|
||||||
|
FCandLen := 0;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
FCandLen := 0;
|
||||||
|
Inc(FRunLen);
|
||||||
|
|
||||||
|
// Хвосты в тишине: знак пора закрыть, а слово — отбить пробелом.
|
||||||
|
if not FMarkNow then
|
||||||
|
begin
|
||||||
|
Inc(FIdleSteps);
|
||||||
|
if (FPattern <> '') and (FIdleSteps * StepMs > 2.5 * FDitMs) then FlushChar;
|
||||||
|
if (not FWordDone) and (FIdleSteps * StepMs > 6.0 * FDitMs) then
|
||||||
|
begin
|
||||||
|
Emit(' ');
|
||||||
|
FWordDone := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.OnMark(Steps: Integer);
|
||||||
|
// Посылка кончилась: точка это или тире — решает адаптивная длина точки, и она
|
||||||
|
// же по этой посылке уточняется. Порог 2 точки — классический.
|
||||||
|
var
|
||||||
|
Ms: Double;
|
||||||
|
begin
|
||||||
|
Ms := Steps * StepMs;
|
||||||
|
if Ms < 0.5 * DIT_MIN_MS then Exit; // мусор
|
||||||
|
if Ms < 2.0 * FDitMs then
|
||||||
|
begin
|
||||||
|
FPattern := FPattern + '.';
|
||||||
|
// ★Вниз — сразу, вверх — плавно. Корреспондент может оказаться вдвое быстрее
|
||||||
|
// нашей подсказки, и тогда его тире мы читаем как точку; пока оценка ползёт
|
||||||
|
// вверх-вниз усреднением, теряется целое слово. Посылка заметно короче
|
||||||
|
// текущей точки — это и есть новая точка, спорить не о чем.
|
||||||
|
if Ms < 0.6 * FDitMs then FDitMs := Ms
|
||||||
|
else FDitMs := FDitMs + 0.3 * (Ms - FDitMs);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
FPattern := FPattern + '-';
|
||||||
|
FDitMs := FDitMs + 0.15 * (Ms / 3.0 - FDitMs);
|
||||||
|
end;
|
||||||
|
FDitMs := EnsureRange(FDitMs, DIT_MIN_MS, DIT_MAX_MS);
|
||||||
|
FIdleSteps := 0;
|
||||||
|
FWordDone := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.OnSpace(Steps: Integer);
|
||||||
|
// Пауза кончилась (началась посылка): по её длине понимаем, был ли это разрыв
|
||||||
|
// внутри знака, между знаками или между словами.
|
||||||
|
var
|
||||||
|
Ms: Double;
|
||||||
|
begin
|
||||||
|
Ms := Steps * StepMs;
|
||||||
|
FIdleSteps := 0;
|
||||||
|
if Ms > 2.0 * FDitMs then FlushChar;
|
||||||
|
if Ms > 5.0 * FDitMs then
|
||||||
|
begin
|
||||||
|
if not FWordDone then
|
||||||
|
begin
|
||||||
|
Emit(' ');
|
||||||
|
FWordDone := True;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.FlushChar;
|
||||||
|
var
|
||||||
|
S: string;
|
||||||
|
begin
|
||||||
|
if FPattern = '' then Exit;
|
||||||
|
S := MorseDecode(FPattern);
|
||||||
|
if S = '' then S := '*'; // не опознали — метка, а не тишина
|
||||||
|
FPattern := '';
|
||||||
|
Emit(S);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWDecoder.Emit(const S: string);
|
||||||
|
begin
|
||||||
|
if S = '' then Exit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FOut := FOut + S;
|
||||||
|
if Length(FOut) > 4096 then FOut := Copy(FOut, Length(FOut) - 4095, 4096);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWDecoder.TakeText: string;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FOut;
|
||||||
|
FOut := '';
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWDecoder.SpeedWPM: Integer;
|
||||||
|
begin
|
||||||
|
if FDitMs <= 0 then Exit(0);
|
||||||
|
Result := Round(1200.0 / FDitMs);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWDecoder.ToneOffsetHz: Integer;
|
||||||
|
begin
|
||||||
|
Result := (FBestBin - CWD_BINS div 2) * CWD_BIN_HZ;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWDecoder.SignalPresent: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FSignal;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+1069
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||||
+382
@@ -0,0 +1,382 @@
|
|||||||
|
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)
|
||||||
|
// Эхо для окна-терминала: ушедшее в эфир и хвост очереди (под FLock).
|
||||||
|
FSent: string;
|
||||||
|
FUnsent: string;
|
||||||
|
FTrimTail: Boolean; // Backspace дотянулся до уже взятого текста
|
||||||
|
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;
|
||||||
|
// Терминал: знаки, ушедшие в эфир с прошлого опроса; хвост очереди; стереть
|
||||||
|
// последний ненабранный знак. Смысл тот же, что у TCWLocalKeyer.
|
||||||
|
function TakeSentText: string;
|
||||||
|
function PendingText: string;
|
||||||
|
function Backspace: 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.TakeSentText: string;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FSent;
|
||||||
|
FSent := '';
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWSender.PendingText: string;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FUnsent + FPending;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCWSender.Backspace: Boolean;
|
||||||
|
// Стираем с хвоста очереди: то, что уже звучит, вернуть нельзя.
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := False;
|
||||||
|
if FPending <> '' then
|
||||||
|
begin
|
||||||
|
SetLength(FPending, Length(FPending) - 1);
|
||||||
|
Result := True;
|
||||||
|
end
|
||||||
|
else if FUnsent <> '' then
|
||||||
|
begin
|
||||||
|
SetLength(FUnsent, Length(FUnsent) - 1);
|
||||||
|
FTrimTail := True;
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
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, KeepLen: Integer;
|
||||||
|
Trim: Boolean;
|
||||||
|
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;
|
||||||
|
// Backspace мог съесть хвост уже взятого текста — отсекаем.
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Trim := FTrimTail;
|
||||||
|
FTrimTail := False;
|
||||||
|
KeepLen := i - 1 + Length(FUnsent);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Trim then
|
||||||
|
begin
|
||||||
|
if KeepLen < i then Break; // стёрли всё впереди
|
||||||
|
if Length(Text) > KeepLen then SetLength(Text, KeepLen);
|
||||||
|
if i > Length(Text) then Break;
|
||||||
|
end;
|
||||||
|
// Эхо в окно-терминал: знак пошёл в эфир; хвост очереди — для строки
|
||||||
|
// набора (там должно остаться только НЕ переданное).
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FSent := FSent + Text[i];
|
||||||
|
FUnsent := Copy(Text, i + 1, MaxInt);
|
||||||
|
if Length(FSent) > 4096 then FSent := Copy(FSent, Length(FSent) - 4095, 4096);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
SendChar(Text[i], Deadline);
|
||||||
|
// Дописанное в очередь по ходу передачи подхватываем без паузы —
|
||||||
|
// иначе поток допередал бы «старый» текст и уснул на 200 мс.
|
||||||
|
if i = Length(Text) then Text := Text + TakeText;
|
||||||
|
Inc(i);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Key(False); // ключ всегда отпущен, что бы ни случилось
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FUnsent := '';
|
||||||
|
FTrimTail := False;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FBusy := False;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Key(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,678 @@
|
|||||||
|
unit CWTerminalForm;
|
||||||
|
|
||||||
|
{ TCWTerminalForm — телеграфный терминал: лента связи + набор с клавиатуры.
|
||||||
|
|
||||||
|
Почему одно окно, а не два. В работе читаешь и отвечаешь одновременно, поэтому
|
||||||
|
принятое декодером и своя передача идут ОДНОЙ лентой (своё — другим цветом),
|
||||||
|
а строка набора висит под ней. Так устроены fldigi и CW-окна контест-логгеров,
|
||||||
|
и по делу это единственная удобная раскладка.
|
||||||
|
|
||||||
|
Набор идёт В ЭФИР ПО МЕРЕ ПЕЧАТИ, а не по Enter: строка набора показывает
|
||||||
|
ровно то, что ещё НЕ передано (очередь генератора), и знаки уходят из неё по
|
||||||
|
мере отправки. Backspace стирает с хвоста очереди — то, что уже звучит,
|
||||||
|
вернуть нельзя.
|
||||||
|
|
||||||
|
Ленту рисуем сами (TPaintBox), а не TMemo: нужен цвет по фрагментам (приём /
|
||||||
|
своя передача) и постоянная дописка снизу без мигания. }
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, Math,
|
||||||
|
Forms, Controls, Graphics, StdCtrls, ExtCtrls, LCLType,
|
||||||
|
FlatButton, FlatEdit, FlatSpinEdit, AppTheme, Settings, DpiUtils;
|
||||||
|
|
||||||
|
type
|
||||||
|
TCWTermSend = procedure(const Text: string) of object;
|
||||||
|
TCWTermFlag = procedure(Value: Boolean) of object;
|
||||||
|
TCWTermKey = procedure(Dot, Dash: Boolean) of object;
|
||||||
|
TCWTermSpeed = procedure(WPM: Integer) of object;
|
||||||
|
|
||||||
|
// Фрагмент ленты: текст одного происхождения подряд.
|
||||||
|
TCWRun = record
|
||||||
|
Text: string;
|
||||||
|
IsTX: Boolean;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TCWTerminalForm = class(TForm)
|
||||||
|
private
|
||||||
|
FTheme: TAppTheme;
|
||||||
|
FRuns: array of TCWRun;
|
||||||
|
FLines: TStringList; // разложенная по ширине лента (текст)
|
||||||
|
FLineTX: TStringList; // '1'/'0' на каждый знак строки — цвет
|
||||||
|
FLog: TPaintBox;
|
||||||
|
FInput: TFlatEdit;
|
||||||
|
FBtnStop: TFlatButton;
|
||||||
|
FBtnClear: TFlatButton;
|
||||||
|
FBtnDec: TFlatButton;
|
||||||
|
FBtnKbd: TFlatButton;
|
||||||
|
FBtnClose: TFlatButton;
|
||||||
|
FEdWPM: TFlatSpinEdit;
|
||||||
|
FLblStatus: TLabel;
|
||||||
|
FMacroBtns: array[0..CW_MSG_COUNT-1] of TFlatButton;
|
||||||
|
FCW: TCWSettings;
|
||||||
|
FUpdating: Boolean;
|
||||||
|
FDecOn: Boolean;
|
||||||
|
FKbdOn: Boolean;
|
||||||
|
FKbdDot: Boolean;
|
||||||
|
FKbdDash: Boolean;
|
||||||
|
FCharW: Integer;
|
||||||
|
FLineH: Integer;
|
||||||
|
FOnSend: TCWTermSend;
|
||||||
|
FOnAbort: TNotifyEvent;
|
||||||
|
FOnBackspace:TNotifyEvent;
|
||||||
|
FOnDecoder: TCWTermFlag;
|
||||||
|
FOnKbdKey: TCWTermKey;
|
||||||
|
FOnSpeed: TCWTermSpeed;
|
||||||
|
FOnMacro: TNotifyEvent; // Sender.Tag = номер ячейки
|
||||||
|
procedure BuildUI;
|
||||||
|
procedure LogPaint(Sender: TObject);
|
||||||
|
procedure LogResize(Sender: TObject);
|
||||||
|
procedure RewrapAll;
|
||||||
|
procedure AppendToWrap(const S: string; IsTX: Boolean);
|
||||||
|
procedure StyleBtn(B: TFlatButton);
|
||||||
|
procedure StopClick(Sender: TObject);
|
||||||
|
procedure ClearClick(Sender: TObject);
|
||||||
|
procedure DecClick(Sender: TObject);
|
||||||
|
procedure KbdClick(Sender: TObject);
|
||||||
|
procedure CloseClick(Sender: TObject);
|
||||||
|
procedure MacroClick(Sender: TObject);
|
||||||
|
procedure WPMChange(Sender: TObject);
|
||||||
|
procedure FormUTF8Key(Sender: TObject; var UTF8Key: TUTF8Char);
|
||||||
|
procedure FormKeyDownEx(Sender: TObject; var Key: Word; Shift: TShiftState);
|
||||||
|
procedure FormKeyUpEx(Sender: TObject; var Key: Word; Shift: TShiftState);
|
||||||
|
procedure PushKbdKey;
|
||||||
|
procedure FocusInput;
|
||||||
|
procedure FormLostFocus(Sender: TObject);
|
||||||
|
public
|
||||||
|
constructor Create(AOwner: TComponent); override;
|
||||||
|
destructor Destroy; override;
|
||||||
|
procedure ApplyTheme(const T: TAppTheme);
|
||||||
|
procedure LoadFrom(const C: TCWSettings);
|
||||||
|
procedure DoShow; override;
|
||||||
|
// Прибавка ленты (из ServiceCWDecoder хозяина).
|
||||||
|
procedure AppendLog(const S: string; IsTX: Boolean);
|
||||||
|
// Строка набора = очередь передачи; статус — скорость/расстройка/сигнал.
|
||||||
|
procedure SetPending(const S: string);
|
||||||
|
procedure SetStatus(const S: string);
|
||||||
|
property OnSend: TCWTermSend read FOnSend write FOnSend;
|
||||||
|
property OnAbort: TNotifyEvent read FOnAbort write FOnAbort;
|
||||||
|
property OnBackspace: TNotifyEvent read FOnBackspace write FOnBackspace;
|
||||||
|
property OnDecoder: TCWTermFlag read FOnDecoder write FOnDecoder;
|
||||||
|
property OnKbdKey: TCWTermKey read FOnKbdKey write FOnKbdKey;
|
||||||
|
property OnSpeed: TCWTermSpeed read FOnSpeed write FOnSpeed;
|
||||||
|
property OnMacro: TNotifyEvent read FOnMacro write FOnMacro;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
{$IFDEF WINDOWS}
|
||||||
|
UI_FONT = 'Segoe UI';
|
||||||
|
MONO_FONT = 'Consolas';
|
||||||
|
{$ELSE}
|
||||||
|
UI_FONT = 'Sans';
|
||||||
|
MONO_FONT = 'Monospace';
|
||||||
|
{$ENDIF}
|
||||||
|
FORM_W = 700;
|
||||||
|
FORM_H = 460;
|
||||||
|
PAD = 10;
|
||||||
|
ROW_H = 26;
|
||||||
|
MAX_LINES = 500;
|
||||||
|
|
||||||
|
constructor TCWTerminalForm.Create(AOwner: TComponent);
|
||||||
|
begin
|
||||||
|
inherited CreateNew(AOwner);
|
||||||
|
FTheme := DarkTheme;
|
||||||
|
FLines := TStringList.Create;
|
||||||
|
FLineTX := TStringList.Create;
|
||||||
|
TSettingsManager.DefaultCW(FCW);
|
||||||
|
Scaled := False;
|
||||||
|
Caption := 'CW Terminal';
|
||||||
|
BorderStyle := bsSizeable;
|
||||||
|
if AOwner is TCustomForm then
|
||||||
|
begin
|
||||||
|
Position := poOwnerFormCenter;
|
||||||
|
// ★Держим окно ПОВЕРХ главного, пока его не закрыли: набор идёт вслепую по
|
||||||
|
// ленте, и если терминал ныряет под главное окно от любого клика по спектру,
|
||||||
|
// работать в нём невозможно. Через transient-родителя, а не fsStayOnTop:
|
||||||
|
// так окно поднимается над своим приложением, но не лезет поверх чужих
|
||||||
|
// (и это единственный способ, который корректно работает на Wayland).
|
||||||
|
PopupMode := pmExplicit;
|
||||||
|
PopupParent := TCustomForm(AOwner);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Position := poScreenCenter;
|
||||||
|
Width := DpiScale(FORM_W);
|
||||||
|
Height := DpiScale(FORM_H);
|
||||||
|
Constraints.MinWidth := DpiScale(520);
|
||||||
|
Constraints.MinHeight := DpiScale(300);
|
||||||
|
KeyPreview := True;
|
||||||
|
OnUTF8KeyPress := @FormUTF8Key;
|
||||||
|
OnKeyDown := @FormKeyDownEx;
|
||||||
|
OnKeyUp := @FormKeyUpEx;
|
||||||
|
// ★Отпускание клавиши приходит только сфокусированному окну. Ушёл фокус с
|
||||||
|
// зажатым Ctrl (alt-tab, клик по главному окну) — и несущая осталась бы в
|
||||||
|
// эфире навсегда. Снимаем ключ на любой потере фокуса и при скрытии окна.
|
||||||
|
OnDeactivate := @FormLostFocus;
|
||||||
|
OnHide := @FormLostFocus;
|
||||||
|
BuildUI;
|
||||||
|
ApplyTheme(DarkTheme);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TCWTerminalForm.Destroy;
|
||||||
|
begin
|
||||||
|
FLines.Free;
|
||||||
|
FLineTX.Free;
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.BuildUI;
|
||||||
|
var
|
||||||
|
i, X: Integer;
|
||||||
|
begin
|
||||||
|
// ---- лента ---------------------------------------------------------------
|
||||||
|
FLog := TPaintBox.Create(Self);
|
||||||
|
FLog.Parent := Self;
|
||||||
|
FLog.Align := alClient;
|
||||||
|
FLog.BorderSpacing.Around := DpiScale(PAD);
|
||||||
|
FLog.BorderSpacing.Bottom := DpiScale(PAD + 3 * ROW_H + 3 * 6);
|
||||||
|
FLog.OnPaint := @LogPaint;
|
||||||
|
FLog.OnResize := @LogResize;
|
||||||
|
|
||||||
|
// ---- строка набора -------------------------------------------------------
|
||||||
|
FInput := TFlatEdit.Create(Self);
|
||||||
|
FInput.Parent := Self;
|
||||||
|
FInput.Anchors := [akLeft, akRight, akBottom];
|
||||||
|
FInput.Font.Name := MONO_FONT;
|
||||||
|
FInput.Font.Size := 10;
|
||||||
|
// ★Обработчики ввода НЕ вешаем на поле: TFlatEdit вставляет знак сам в
|
||||||
|
// UTF8KeyPress и гасит клавишу, поэтому его OnKeyPress не вызывается вовсе —
|
||||||
|
// именно из-за этого набранное «исчезало», а в эфир не уходило. Весь ввод
|
||||||
|
// ловим на форме (KeyPreview), а поле остаётся показывать очередь передачи.
|
||||||
|
|
||||||
|
// ---- нижние ряды ---------------------------------------------------------
|
||||||
|
FBtnStop := TFlatButton.Create(Self);
|
||||||
|
FBtnStop.Parent := Self;
|
||||||
|
FBtnStop.Caption := 'Stop';
|
||||||
|
FBtnStop.OnClick := @StopClick;
|
||||||
|
FBtnStop.Anchors := [akLeft, akBottom];
|
||||||
|
|
||||||
|
FBtnClear := TFlatButton.Create(Self);
|
||||||
|
FBtnClear.Parent := Self;
|
||||||
|
FBtnClear.Caption := 'Clear';
|
||||||
|
FBtnClear.OnClick := @ClearClick;
|
||||||
|
FBtnClear.Anchors := [akLeft, akBottom];
|
||||||
|
|
||||||
|
FBtnDec := TFlatButton.Create(Self);
|
||||||
|
FBtnDec.Parent := Self;
|
||||||
|
FBtnDec.Caption := 'Decoder';
|
||||||
|
FBtnDec.OnClick := @DecClick;
|
||||||
|
FBtnDec.Anchors := [akLeft, akBottom];
|
||||||
|
|
||||||
|
FBtnKbd := TFlatButton.Create(Self);
|
||||||
|
FBtnKbd.Parent := Self;
|
||||||
|
FBtnKbd.Caption := 'Ctrl = key';
|
||||||
|
FBtnKbd.OnClick := @KbdClick;
|
||||||
|
FBtnKbd.Anchors := [akLeft, akBottom];
|
||||||
|
|
||||||
|
FEdWPM := TFlatSpinEdit.Create(Self);
|
||||||
|
FEdWPM.Parent := Self;
|
||||||
|
FEdWPM.MinValue := 5;
|
||||||
|
FEdWPM.MaxValue := 60;
|
||||||
|
FEdWPM.Value := 20;
|
||||||
|
FEdWPM.OnChange := @WPMChange;
|
||||||
|
FEdWPM.Anchors := [akLeft, akBottom];
|
||||||
|
|
||||||
|
FLblStatus := TLabel.Create(Self);
|
||||||
|
FLblStatus.Parent := Self;
|
||||||
|
FLblStatus.Font.Name := UI_FONT;
|
||||||
|
FLblStatus.Font.Size := 8;
|
||||||
|
FLblStatus.Anchors := [akLeft, akRight, akBottom];
|
||||||
|
FLblStatus.AutoSize := False;
|
||||||
|
|
||||||
|
FBtnClose := TFlatButton.Create(Self);
|
||||||
|
FBtnClose.Parent := Self;
|
||||||
|
FBtnClose.Caption := 'Close';
|
||||||
|
FBtnClose.OnClick := @CloseClick;
|
||||||
|
FBtnClose.Anchors := [akRight, akBottom];
|
||||||
|
|
||||||
|
X := PAD;
|
||||||
|
for i := 0 to CW_MSG_COUNT - 1 do
|
||||||
|
begin
|
||||||
|
FMacroBtns[i] := TFlatButton.Create(Self);
|
||||||
|
FMacroBtns[i].Parent := Self;
|
||||||
|
FMacroBtns[i].Caption := 'F' + IntToStr(i + 1);
|
||||||
|
FMacroBtns[i].Tag := i;
|
||||||
|
FMacroBtns[i].OnClick := @MacroClick;
|
||||||
|
FMacroBtns[i].Anchors := [akLeft, akBottom];
|
||||||
|
Inc(X, 46);
|
||||||
|
end;
|
||||||
|
LogResize(nil);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.LogResize(Sender: TObject);
|
||||||
|
// Раскладка нижних рядов руками: якорей хватает по горизонтали, но ряды идут
|
||||||
|
// снизу вверх, и считать их проще одной формулой, чем городить панели.
|
||||||
|
var
|
||||||
|
i, Y, X, W: Integer;
|
||||||
|
begin
|
||||||
|
if FInput = nil then Exit;
|
||||||
|
W := ClientWidth;
|
||||||
|
Y := ClientHeight - DpiScale(PAD) - DpiScale(ROW_H);
|
||||||
|
// ряд 3 (низ): макросы + Close
|
||||||
|
X := DpiScale(PAD);
|
||||||
|
for i := 0 to CW_MSG_COUNT - 1 do
|
||||||
|
begin
|
||||||
|
FMacroBtns[i].SetBounds(X, Y, DpiScale(42), DpiScale(ROW_H));
|
||||||
|
Inc(X, DpiScale(46));
|
||||||
|
end;
|
||||||
|
FBtnClose.SetBounds(W - DpiScale(PAD + 70), Y, DpiScale(70), DpiScale(ROW_H));
|
||||||
|
// ряд 2: кнопки + скорость + статус
|
||||||
|
Dec(Y, DpiScale(ROW_H + 6));
|
||||||
|
X := DpiScale(PAD);
|
||||||
|
FBtnStop.SetBounds(X, Y, DpiScale(60), DpiScale(ROW_H)); Inc(X, DpiScale(64));
|
||||||
|
FBtnClear.SetBounds(X, Y, DpiScale(60), DpiScale(ROW_H)); Inc(X, DpiScale(64));
|
||||||
|
FBtnDec.SetBounds(X, Y, DpiScale(80), DpiScale(ROW_H)); Inc(X, DpiScale(84));
|
||||||
|
FBtnKbd.SetBounds(X, Y, DpiScale(90), DpiScale(ROW_H)); Inc(X, DpiScale(94));
|
||||||
|
FEdWPM.SetBounds(X, Y, DpiScale(70), DpiScale(ROW_H)); Inc(X, DpiScale(78));
|
||||||
|
FLblStatus.SetBounds(X, Y + DpiScale(6), W - X - DpiScale(PAD), DpiScale(18));
|
||||||
|
// ряд 1: строка набора
|
||||||
|
Dec(Y, DpiScale(ROW_H + 6));
|
||||||
|
FInput.SetBounds(DpiScale(PAD), Y, W - DpiScale(2 * PAD), DpiScale(ROW_H));
|
||||||
|
RewrapAll;
|
||||||
|
if FLog <> nil then FLog.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ── лента ─────────────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.AppendToWrap(const S: string; IsTX: Boolean);
|
||||||
|
// Дописываем знаки в последнюю строку, перенося по ширине окна. Цвет храним
|
||||||
|
// параллельной строкой флагов — так рисование не зависит от разбиения на
|
||||||
|
// фрагменты и переносы получаются посимвольно точными.
|
||||||
|
var
|
||||||
|
i, Cols: Integer;
|
||||||
|
Flag: Char;
|
||||||
|
begin
|
||||||
|
if S = '' then Exit;
|
||||||
|
if FCharW <= 0 then FCharW := 8;
|
||||||
|
Cols := Max(8, (FLog.Width - DpiScale(8)) div FCharW);
|
||||||
|
if FLines.Count = 0 then
|
||||||
|
begin
|
||||||
|
FLines.Add('');
|
||||||
|
FLineTX.Add('');
|
||||||
|
end;
|
||||||
|
if IsTX then Flag := '1' else Flag := '0';
|
||||||
|
for i := 1 to Length(S) do
|
||||||
|
begin
|
||||||
|
if S[i] = #10 then
|
||||||
|
begin
|
||||||
|
FLines.Add(''); FLineTX.Add('');
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
if Length(FLines[FLines.Count - 1]) >= Cols then
|
||||||
|
begin
|
||||||
|
FLines.Add(''); FLineTX.Add('');
|
||||||
|
end;
|
||||||
|
FLines[FLines.Count - 1] := FLines[FLines.Count - 1] + S[i];
|
||||||
|
FLineTX[FLineTX.Count - 1] := FLineTX[FLineTX.Count - 1] + Flag;
|
||||||
|
end;
|
||||||
|
while FLines.Count > MAX_LINES do
|
||||||
|
begin
|
||||||
|
FLines.Delete(0);
|
||||||
|
FLineTX.Delete(0);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.RewrapAll;
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
if FLog = nil then Exit;
|
||||||
|
FLines.Clear;
|
||||||
|
FLineTX.Clear;
|
||||||
|
for i := 0 to High(FRuns) do
|
||||||
|
AppendToWrap(FRuns[i].Text, FRuns[i].IsTX);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.AppendLog(const S: string; IsTX: Boolean);
|
||||||
|
var
|
||||||
|
n: Integer;
|
||||||
|
begin
|
||||||
|
if S = '' then Exit;
|
||||||
|
n := Length(FRuns);
|
||||||
|
// Соседние фрагменты одного происхождения склеиваем — иначе массив распухнет
|
||||||
|
// на каждый принятый знак.
|
||||||
|
if (n > 0) and (FRuns[n-1].IsTX = IsTX) and (Length(FRuns[n-1].Text) < 4096) then
|
||||||
|
FRuns[n-1].Text := FRuns[n-1].Text + S
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
SetLength(FRuns, n + 1);
|
||||||
|
FRuns[n].Text := S;
|
||||||
|
FRuns[n].IsTX := IsTX;
|
||||||
|
end;
|
||||||
|
// Держим историю ограниченной (окно не журнал связи).
|
||||||
|
if Length(FRuns) > 400 then
|
||||||
|
begin
|
||||||
|
Move(FRuns[100], FRuns[0], (Length(FRuns) - 100) * SizeOf(TCWRun));
|
||||||
|
SetLength(FRuns, Length(FRuns) - 100);
|
||||||
|
RewrapAll;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
AppendToWrap(S, IsTX);
|
||||||
|
FLog.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.LogPaint(Sender: TObject);
|
||||||
|
var
|
||||||
|
C: TCanvas;
|
||||||
|
i, j, Y, X, First, Rows: Integer;
|
||||||
|
L, F: string;
|
||||||
|
begin
|
||||||
|
C := FLog.Canvas;
|
||||||
|
C.Brush.Color := FTheme.Panel;
|
||||||
|
C.FillRect(0, 0, FLog.Width, FLog.Height);
|
||||||
|
C.Pen.Color := FTheme.Border;
|
||||||
|
C.Brush.Style := bsClear;
|
||||||
|
C.Rectangle(0, 0, FLog.Width, FLog.Height);
|
||||||
|
C.Font.Name := MONO_FONT;
|
||||||
|
C.Font.Size := 10;
|
||||||
|
if FLineH <= 0 then
|
||||||
|
begin
|
||||||
|
FLineH := C.TextHeight('Wg') + 2;
|
||||||
|
FCharW := C.TextWidth('W');
|
||||||
|
if FCharW <= 0 then FCharW := 8;
|
||||||
|
end;
|
||||||
|
Rows := Max(1, (FLog.Height - DpiScale(8)) div FLineH);
|
||||||
|
First := Max(0, FLines.Count - Rows); // всегда показываем хвост ленты
|
||||||
|
Y := DpiScale(4);
|
||||||
|
for i := First to FLines.Count - 1 do
|
||||||
|
begin
|
||||||
|
L := FLines[i];
|
||||||
|
F := FLineTX[i];
|
||||||
|
X := DpiScale(4);
|
||||||
|
for j := 1 to Length(L) do
|
||||||
|
begin
|
||||||
|
// Своя передача — акцентом, приём — обычным текстом. Разделять их
|
||||||
|
// строками нельзя: в QSK они перемежаются посреди фразы.
|
||||||
|
if (j <= Length(F)) and (F[j] = '1') then C.Font.Color := FTheme.BtnTextActive
|
||||||
|
else C.Font.Color := FTheme.Text;
|
||||||
|
C.TextOut(X, Y, L[j]);
|
||||||
|
Inc(X, FCharW);
|
||||||
|
end;
|
||||||
|
Inc(Y, FLineH);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ── управление ────────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.SetPending(const S: string);
|
||||||
|
begin
|
||||||
|
if FInput = nil then Exit;
|
||||||
|
if FInput.Text = S then Exit;
|
||||||
|
FUpdating := True;
|
||||||
|
try
|
||||||
|
FInput.Text := S;
|
||||||
|
FInput.CaretPos := MaxInt; // курсор всегда в конце очереди набора
|
||||||
|
finally
|
||||||
|
FUpdating := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.SetStatus(const S: string);
|
||||||
|
begin
|
||||||
|
if FLblStatus <> nil then FLblStatus.Caption := S;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.LoadFrom(const C: TCWSettings);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
FCW := C;
|
||||||
|
FUpdating := True;
|
||||||
|
try
|
||||||
|
if FEdWPM <> nil then FEdWPM.Value := EnsureRange(C.Speed, 5, 60);
|
||||||
|
for i := 0 to CW_MSG_COUNT - 1 do
|
||||||
|
if FMacroBtns[i] <> nil then
|
||||||
|
begin
|
||||||
|
FMacroBtns[i].Hint := C.Messages[i];
|
||||||
|
FMacroBtns[i].ShowHint := C.Messages[i] <> '';
|
||||||
|
end;
|
||||||
|
FDecOn := C.Decoder;
|
||||||
|
FKbdOn := C.KbdKey;
|
||||||
|
finally
|
||||||
|
FUpdating := False;
|
||||||
|
end;
|
||||||
|
ApplyTheme(FTheme); // состояние кнопок-переключателей рисуется цветом
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.DoShow;
|
||||||
|
begin
|
||||||
|
inherited DoShow;
|
||||||
|
// Набор — главное занятие в этом окне, курсор должен быть уже там.
|
||||||
|
if FInput <> nil then FInput.SetFocus;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.FormUTF8Key(Sender: TObject; var UTF8Key: TUTF8Char);
|
||||||
|
// ★Печать уходит в эфир СРАЗУ, посимвольно. Строка набора нами не редактируется:
|
||||||
|
// её содержимое — очередь генератора, и она перезаписывается через SetPending по
|
||||||
|
// мере передачи. Знак гасим, чтобы поле не вставило его ещё и от себя.
|
||||||
|
var Key: Char;
|
||||||
|
begin
|
||||||
|
if UTF8Key = '' then Exit;
|
||||||
|
if ActiveControl <> FInput then Exit; // печатаем только в строке набора
|
||||||
|
Key := UTF8Key[1];
|
||||||
|
UTF8Key := '';
|
||||||
|
if Key < #32 then Exit;
|
||||||
|
if Assigned(FOnSend) then FOnSend(UpCase(Key));
|
||||||
|
// Показываем знак в очереди сразу, не дожидаясь таймера: иначе набор
|
||||||
|
// ощущается «залипающим». Через 100 мс SetPending всё равно перепишет строку
|
||||||
|
// настоящей очередью генератора.
|
||||||
|
FUpdating := True;
|
||||||
|
try
|
||||||
|
FInput.Text := FInput.Text + UpCase(Key);
|
||||||
|
FInput.CaretPos := MaxInt;
|
||||||
|
finally
|
||||||
|
FUpdating := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.FocusInput;
|
||||||
|
// ★Клик по любой кнопке уводит фокус на неё, и печать после этого молча
|
||||||
|
// перестаёт уходить в эфир (ввод ловится, только пока фокус в строке набора).
|
||||||
|
// Поэтому после каждого действия возвращаем курсор туда.
|
||||||
|
begin
|
||||||
|
if (FInput <> nil) and FInput.CanFocus then FInput.SetFocus;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.PushKbdKey;
|
||||||
|
begin
|
||||||
|
if Assigned(FOnKbdKey) then FOnKbdKey(FKbdDot, FKbdDash);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.FormLostFocus(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if FKbdDot or FKbdDash then
|
||||||
|
begin
|
||||||
|
FKbdDot := False;
|
||||||
|
FKbdDash := False;
|
||||||
|
PushKbdKey;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.FormKeyDownEx(Sender: TObject; var Key: Word;
|
||||||
|
Shift: TShiftState);
|
||||||
|
begin
|
||||||
|
// F1..F8 — те же ячейки памяти, что в главном окне.
|
||||||
|
if (Key >= VK_F1) and (Key <= VK_F8) then
|
||||||
|
begin
|
||||||
|
if Assigned(FOnMacro) then
|
||||||
|
begin
|
||||||
|
FBtnStop.Tag := Key - VK_F1; // переиспользуем Tag как переносчик номера
|
||||||
|
MacroClick(FBtnStop);
|
||||||
|
end;
|
||||||
|
Key := 0;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Правка очереди передачи — только пока фокус в строке набора. Поле не
|
||||||
|
// редактируем сами: его содержимое задаёт генератор (SetPending), поэтому
|
||||||
|
// клавиши правки гасим здесь, до контрола.
|
||||||
|
if ActiveControl = FInput then
|
||||||
|
case Key of
|
||||||
|
VK_BACK:
|
||||||
|
begin
|
||||||
|
if Assigned(FOnBackspace) then FOnBackspace(Self);
|
||||||
|
if FInput.Text <> '' then
|
||||||
|
begin
|
||||||
|
FUpdating := True;
|
||||||
|
try
|
||||||
|
FInput.Text := Copy(FInput.Text, 1, Length(FInput.Text) - 1);
|
||||||
|
FInput.CaretPos := MaxInt;
|
||||||
|
finally
|
||||||
|
FUpdating := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Key := 0;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
VK_ESCAPE:
|
||||||
|
begin
|
||||||
|
if Assigned(FOnAbort) then FOnAbort(Self);
|
||||||
|
Key := 0;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Стрелки/Delete/Enter внутри очереди смысла не имеют: эфир ушёл вперёд.
|
||||||
|
VK_RETURN, VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, VK_DELETE, VK_HOME, VK_END:
|
||||||
|
begin
|
||||||
|
Key := 0;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not FKbdOn then Exit;
|
||||||
|
// Ctrl = манипулятор. Левый — точка, правый — тире: при иамбике это полноценные
|
||||||
|
// лепестки, при прямом ключе годится любой.
|
||||||
|
if Key = VK_LCONTROL then begin FKbdDot := True; PushKbdKey; Key := 0; end
|
||||||
|
else if Key = VK_RCONTROL then begin FKbdDash := True; PushKbdKey; Key := 0; end
|
||||||
|
else if Key = VK_CONTROL then begin FKbdDot := True; PushKbdKey; Key := 0; end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.FormKeyUpEx(Sender: TObject; var Key: Word;
|
||||||
|
Shift: TShiftState);
|
||||||
|
begin
|
||||||
|
if not FKbdOn then Exit;
|
||||||
|
if Key = VK_LCONTROL then begin FKbdDot := False; PushKbdKey; Key := 0; end
|
||||||
|
else if Key = VK_RCONTROL then begin FKbdDash := False; PushKbdKey; Key := 0; end
|
||||||
|
else if Key = VK_CONTROL then
|
||||||
|
begin
|
||||||
|
FKbdDot := False; FKbdDash := False; PushKbdKey; Key := 0;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.MacroClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnMacro) then FOnMacro(Sender);
|
||||||
|
FocusInput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.StopClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnAbort) then FOnAbort(Self);
|
||||||
|
FocusInput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.ClearClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
SetLength(FRuns, 0);
|
||||||
|
FLines.Clear;
|
||||||
|
FLineTX.Clear;
|
||||||
|
FLog.Invalidate;
|
||||||
|
FocusInput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.DecClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FDecOn := not FDecOn;
|
||||||
|
if Assigned(FOnDecoder) then FOnDecoder(FDecOn);
|
||||||
|
ApplyTheme(FTheme);
|
||||||
|
FocusInput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.KbdClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FKbdOn := not FKbdOn;
|
||||||
|
if not FKbdOn then
|
||||||
|
begin
|
||||||
|
FKbdDot := False; FKbdDash := False;
|
||||||
|
PushKbdKey; // отпустить, если выключили с зажатой
|
||||||
|
end;
|
||||||
|
FCW.KbdKey := FKbdOn;
|
||||||
|
ApplyTheme(FTheme);
|
||||||
|
FocusInput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.CloseClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
Hide;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.WPMChange(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if FUpdating then Exit;
|
||||||
|
if Assigned(FOnSpeed) then FOnSpeed(FEdWPM.Value);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TCWTerminalForm.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 TCWTerminalForm.ApplyTheme(const T: TAppTheme);
|
||||||
|
var
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
FTheme := T;
|
||||||
|
Color := T.BG;
|
||||||
|
StyleBtn(FBtnStop);
|
||||||
|
StyleBtn(FBtnClear);
|
||||||
|
StyleBtn(FBtnDec);
|
||||||
|
StyleBtn(FBtnKbd);
|
||||||
|
StyleBtn(FBtnClose);
|
||||||
|
for i := 0 to CW_MSG_COUNT - 1 do StyleBtn(FMacroBtns[i]);
|
||||||
|
// Переключатели: включённое состояние — акцентной рамкой и текстом.
|
||||||
|
if FBtnDec <> nil then
|
||||||
|
begin
|
||||||
|
if FDecOn then
|
||||||
|
begin
|
||||||
|
FBtnDec.ClrBorder := T.BtnTextActive;
|
||||||
|
FBtnDec.ClrText := T.BtnTextActive;
|
||||||
|
end;
|
||||||
|
FBtnDec.Invalidate;
|
||||||
|
end;
|
||||||
|
if FBtnKbd <> nil then
|
||||||
|
begin
|
||||||
|
if FKbdOn then
|
||||||
|
begin
|
||||||
|
FBtnKbd.ClrBorder := T.BtnTextActive;
|
||||||
|
FBtnKbd.ClrText := T.BtnTextActive;
|
||||||
|
end;
|
||||||
|
FBtnKbd.Invalidate;
|
||||||
|
end;
|
||||||
|
if FInput <> nil then FInput.SetAppTheme(T);
|
||||||
|
if FEdWPM <> nil then FEdWPM.SetAppTheme(T);
|
||||||
|
if FLblStatus <> nil then FLblStatus.Font.Color := T.TextDim;
|
||||||
|
if FLog <> nil then FLog.Invalidate;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -71,6 +71,10 @@ type
|
|||||||
procedure SelectAll;
|
procedure SelectAll;
|
||||||
|
|
||||||
property Text: string read FText write SetText;
|
property Text: string read FText write SetText;
|
||||||
|
// Позиция курсора (в знаках, не в байтах). Нужна тем, кто задаёт текст
|
||||||
|
// программно: SetText курсор НЕ двигает — он лишь клампится в диапазон, и
|
||||||
|
// при внешней подстановке текста оставался бы в начале строки.
|
||||||
|
property CaretPos: Integer read FCaretPos write SetCaretPos;
|
||||||
property TextHint: string read FTextHint write SetTextHint;
|
property TextHint: string read FTextHint write SetTextHint;
|
||||||
property PasswordChar: Char read FPasswordChar write SetPasswordChar;
|
property PasswordChar: Char read FPasswordChar write SetPasswordChar;
|
||||||
property MaxLength: Integer read FMaxLength write SetMaxLength;
|
property MaxLength: Integer read FMaxLength write SetMaxLength;
|
||||||
|
|||||||
+42
-4
@@ -169,6 +169,8 @@ 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): программный ключ
|
||||||
|
FCWKeyerArmed: Boolean; // кейер прошивки вооружён (см. байт 345)
|
||||||
// ---- 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 +271,8 @@ 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 SetCWKeyerArmed(Armed: 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;
|
||||||
@@ -727,6 +731,8 @@ begin
|
|||||||
Result.NumADCs := 1;
|
Result.NumADCs := 1;
|
||||||
// PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2.
|
// PureSignal: нужны свободные DDC0/1 под feedback — платы с DDCBase=2.
|
||||||
Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10];
|
Result.HasPureSignal := FDevice.BoardType in [3, 4, 5, 10];
|
||||||
|
// Кейер в прошивке есть у всех плат P2 (DUC Specific байты 5..13).
|
||||||
|
Result.HasCWKeyer := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
constructor THPSDRNetwork.Create;
|
constructor THPSDRNetwork.Create;
|
||||||
@@ -1696,7 +1702,13 @@ begin
|
|||||||
SendDDCSpecific(Pkt);
|
SendDDCSpecific(Pkt);
|
||||||
FCachedDDCSpec := Pkt;
|
FCachedDDCSpec := Pkt;
|
||||||
FCachedDDCValid := True;
|
FCachedDDCValid := True;
|
||||||
FResendCount := 0;
|
// ★СЧЁТЧИК ПЕРЕ-ОТПРАВКИ ЗДЕСЬ НЕ СБРАСЫВАЕМ. Окно повторов — сугубо
|
||||||
|
// СТАРТОВЫЙ костыль (порты 1025/1026 открываются только после Run=1), и
|
||||||
|
// живёт оно от Run=1. Сброс отсюда превращал его в рецидивирующий: любая
|
||||||
|
// пере-сборка на живой сессии — вход/выход PureSignal в TX (каждый MOX!),
|
||||||
|
// добавление пана, смена rate — заводила ЕЩЁ 10 пере-латчей DDC Specific в
|
||||||
|
// течение 5 секунд. А пере-латч конфига DDC на ходу трогает и ГЛАВНЫЙ
|
||||||
|
// приёмник: ровно этим уже был получен «мусорный» поток на доп. панах.
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure THPSDRNetwork.SetPanDDC(DDCIdx: Integer; Enabled: Boolean;
|
procedure THPSDRNetwork.SetPanDDC(DDCIdx: Integer; Enabled: Boolean;
|
||||||
@@ -1801,6 +1813,22 @@ 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.SetCWKeyerArmed(Armed: Boolean);
|
||||||
|
begin
|
||||||
|
if FCWKeyerArmed = Armed then Exit;
|
||||||
|
FCWKeyerArmed := Armed;
|
||||||
|
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 +1868,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;
|
||||||
@@ -1885,8 +1916,11 @@ begin
|
|||||||
Buf[331] := (Ph shr 8) and $FF;
|
Buf[331] := (Ph shr 8) and $FF;
|
||||||
Buf[332] := Ph and $FF;
|
Buf[332] := Ph and $FF;
|
||||||
|
|
||||||
// Drive level (byte 345)
|
// Drive level (byte 345). Обычно уровень нужен только на передаче, но с
|
||||||
if FIsTransmitting then
|
// вооружённым кейером телеграфа «передачи» в понимании софта не бывает:
|
||||||
|
// прошивка замыкает ключ сама, и в кадре, который она в этот момент видит,
|
||||||
|
// уровень уже обязан стоять — иначе посылка уходит с чужой мощностью.
|
||||||
|
if FIsTransmitting or FCWKeyerArmed then
|
||||||
Buf[345] := FCurrentDrive;
|
Buf[345] := FCurrentDrive;
|
||||||
|
|
||||||
// ALEX0 filter bits (bytes 1432-1435)
|
// ALEX0 filter bits (bytes 1432-1435)
|
||||||
@@ -1957,7 +1991,11 @@ procedure THPSDRNetwork.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Doubl
|
|||||||
begin
|
begin
|
||||||
// При Run=1 аппаратура сбрасывает счётчик HP-последовательности.
|
// При Run=1 аппаратура сбрасывает счётчик HP-последовательности.
|
||||||
// Синхронизируем свой счётчик чтобы не получить SEQ ERROR на первом пакете.
|
// Синхронизируем свой счётчик чтобы не получить SEQ ERROR на первом пакете.
|
||||||
if Run then FSeqHP := 0;
|
if Run then
|
||||||
|
begin
|
||||||
|
FSeqHP := 0;
|
||||||
|
FResendCount := 0; // окно повторов Specific-пакетов — только со старта
|
||||||
|
end;
|
||||||
FRunning := Run;
|
FRunning := Run;
|
||||||
FCurrentRXFreq := DDC0FreqHz;
|
FCurrentRXFreq := DDC0FreqHz;
|
||||||
FCurrentTXFreq := DUCFreqHz;
|
FCurrentTXFreq := DUCFreqHz;
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ var
|
|||||||
out val: clonglong): cint; cdecl;
|
out val: clonglong): cint; cdecl;
|
||||||
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
||||||
val: clonglong): cint; cdecl;
|
val: clonglong): cint; cdecl;
|
||||||
|
// 'calib_mode' — режим калибровок AD936x (строка): 'auto' = драйвер сам
|
||||||
|
// переигрывает TX quad (утечка LO + квадратура) на каждой смене TX LO.
|
||||||
|
iio_device_attr_write: function(dev: Piio_device; attr: PAnsiChar;
|
||||||
|
src: PAnsiChar): ptrint; cdecl;
|
||||||
|
|
||||||
// ---- Debug-атрибуты устройства (debugfs) ----
|
// ---- Debug-атрибуты устройства (debugfs) ----
|
||||||
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
|
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
|
||||||
@@ -195,6 +199,7 @@ begin
|
|||||||
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
|
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
|
||||||
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
|
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
|
||||||
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
|
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
|
||||||
|
iio_device_attr_write := Sym('iio_device_attr_write');
|
||||||
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
|
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
|
||||||
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
|
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
|
||||||
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
|
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
|
||||||
|
|||||||
+226
-9
@@ -43,7 +43,7 @@ uses
|
|||||||
WinFirewall,
|
WinFirewall,
|
||||||
BoardUtils, WisdomBuilder, UISync,
|
BoardUtils, WisdomBuilder, UISync,
|
||||||
FlatEdit, FMRepeater,
|
FlatEdit, FMRepeater,
|
||||||
ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder,
|
ChannelStore, ChannelsForm, CWMessagesForm, CWTerminalForm, BeaconScopeForm, DMRDecoder,
|
||||||
PowerInhibit,
|
PowerInhibit,
|
||||||
DeviceStore,
|
DeviceStore,
|
||||||
RadioBackend, PlutoBackend,
|
RadioBackend, PlutoBackend,
|
||||||
@@ -367,6 +367,8 @@ type
|
|||||||
BtnTXProfile: TFlatButton;
|
BtnTXProfile: TFlatButton;
|
||||||
FTXProfileDropDown: TFlatDropDown;
|
FTXProfileDropDown: TFlatDropDown;
|
||||||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||||
|
FCWMsgForm: TObject; // TCWMessagesForm (память телеграфа)
|
||||||
|
FCWTermForm: TObject; // TCWTerminalForm (лента + набор с клавиатуры)
|
||||||
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
||||||
|
|
||||||
// ---- Right panel ----
|
// ---- Right panel ----
|
||||||
@@ -472,6 +474,8 @@ type
|
|||||||
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
|
||||||
procedure BtnBandClick(Sender: TObject);
|
procedure BtnBandClick(Sender: TObject);
|
||||||
procedure BtnModeClick(Sender: TObject);
|
procedure BtnModeClick(Sender: TObject);
|
||||||
|
procedure BtnModeMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure BtnFilterClick(Sender: TObject);
|
procedure BtnFilterClick(Sender: TObject);
|
||||||
procedure BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
@@ -763,6 +767,19 @@ 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 ShowCWTerminal;
|
||||||
|
procedure ServiceCWTerminal; // таймер: разбор + обновление окна
|
||||||
|
procedure OnCWLogAppend(const S: string; IsTX: Boolean);
|
||||||
|
procedure OnCWTermBackspace(Sender: TObject);
|
||||||
|
procedure OnCWTermDecoder(Value: Boolean);
|
||||||
|
procedure OnCWTermKbdKey(Dot, Dash: Boolean);
|
||||||
|
procedure OnCWTermSpeed(WPM: Integer);
|
||||||
|
procedure OnCWTermMacro(Sender: TObject);
|
||||||
|
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 +1082,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);
|
||||||
|
|
||||||
// Инициализируем кэш диапазонов умолчаниями
|
// Инициализируем кэш диапазонов умолчаниями
|
||||||
@@ -1465,7 +1487,9 @@ begin
|
|||||||
FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex;
|
FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex;
|
||||||
// Красная полоса главного VFO — только когда передаём с него: при
|
// Красная полоса главного VFO — только когда передаём с него: при
|
||||||
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
// TX со слайса краснеет полоса ТОГО слайса (DrawSliceFilterMarkers).
|
||||||
FSpecView.TXOverlay := FController.FTransmitting and
|
// ★RadioKeyed, а не FTransmitting: в телеграфе ключ замыкает прошивка,
|
||||||
|
// софт «не передаёт», и полоса оставалась бы приёмной всю связь.
|
||||||
|
FSpecView.TXOverlay := FController.RadioKeyed and
|
||||||
(FController.FTxSliceId = 0);
|
(FController.FTxSliceId = 0);
|
||||||
PushFilterEdgesToView; // на передаче полоса рисуется по TX-фильтру
|
PushFilterEdgesToView; // на передаче полоса рисуется по TX-фильтру
|
||||||
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
ApplySpecViewGridFromState; // сетка спектра под активный тракт
|
||||||
@@ -1798,6 +1822,17 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
rfSliceFreq:
|
||||||
|
begin
|
||||||
|
// Слайс перестроен по своему CAT-порту в пределах захваченной полосы:
|
||||||
|
// окно DDC не двигалось (rfPanFreq не придёт), но цифры и полоса
|
||||||
|
// фильтра во флаге — его собственные поля, их надо перезалить.
|
||||||
|
PushFlagStateAllPans(FController.FSliceFreqId);
|
||||||
|
LayoutFlagsAllPans;
|
||||||
|
for i := 0 to MAX_PANS - 1 do
|
||||||
|
if FPans[i] <> nil then MarkPanDirty(FPans[i]);
|
||||||
|
end;
|
||||||
|
|
||||||
rfPanFreq:
|
rfPanFreq:
|
||||||
begin
|
begin
|
||||||
// Центр доп. пана переехал не из UI (CAT слайса перестроил его за
|
// Центр доп. пана переехал не из UI (CAT слайса перестроил его за
|
||||||
@@ -2410,6 +2445,7 @@ begin
|
|||||||
LeftPanelButtonWidth(LEFT_W, 4, i mod 4),
|
LeftPanelButtonWidth(LEFT_W, 4, i mod 4),
|
||||||
BTN_SM, BtnModeClick);
|
BTN_SM, BtnModeClick);
|
||||||
B.Tag := i;
|
B.Tag := i;
|
||||||
|
B.OnMouseDown := BtnModeMouseDown; // ПКМ по CWL/CWU → терминал
|
||||||
BtnMode[i] := B;
|
BtnMode[i] := B;
|
||||||
StyleButton(B, i = FController.FMode);
|
StyleButton(B, i = FController.FMode);
|
||||||
end;
|
end;
|
||||||
@@ -3400,6 +3436,8 @@ 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 FCWTermForm <> nil then TCWTerminalForm(FCWTermForm).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;
|
||||||
@@ -3674,6 +3712,11 @@ begin
|
|||||||
FController.ServiceBeaconLock;
|
FController.ServiceBeaconLock;
|
||||||
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
|
// Pluto: опрос температуры/RSSI (троттлинг внутри; no-op для HPSDR).
|
||||||
FController.ServicePlutoTelemetry;
|
FController.ServicePlutoTelemetry;
|
||||||
|
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет — гнать индикацию
|
||||||
|
// и отпускать реле T/R после выдержки больше некому).
|
||||||
|
FController.ServiceCWKeyed;
|
||||||
|
// Телеграф: разбор принятого в текст + окно-терминал + лента под спектром.
|
||||||
|
ServiceCWTerminal;
|
||||||
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
|
// PureSignal: state machine команд + auto-attenuate + опрос GetPSInfo
|
||||||
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
|
// (no-op на бэкендах без PS; рендер кнопок/поповера через rfPureSignal).
|
||||||
FController.PureSignalTick;
|
FController.PureSignalTick;
|
||||||
@@ -3699,7 +3742,9 @@ begin
|
|||||||
FSpecView.SMeterMin := FSMeterMin;
|
FSpecView.SMeterMin := FSMeterMin;
|
||||||
FSpecView.LastFwdW := FController.FLastFwdW;
|
FSpecView.LastFwdW := FController.FLastFwdW;
|
||||||
FSpecView.LastSWR := FController.FLastSWR;
|
FSpecView.LastSWR := FController.FLastSWR;
|
||||||
FSpecView.Transmitting := FController.FTransmitting;
|
// Метр — по RadioKeyed, а не по FTransmitting: в телеграфе софт «не передаёт»,
|
||||||
|
// ключ замыкает прошивка, и стрелка осталась бы на S-метре всю связь.
|
||||||
|
FSpecView.Transmitting := FController.RadioKeyed;
|
||||||
|
|
||||||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||||||
// PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus —
|
// PWR/SWR метки и бары обновляем здесь (10 Гц), а не в DoUpdateStatus —
|
||||||
@@ -4585,7 +4630,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -4607,7 +4652,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -5053,7 +5098,7 @@ begin
|
|||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
||||||
XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True,
|
FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True,
|
||||||
FController.FTuning, FController.FTwoTone);
|
FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -5565,6 +5610,17 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BtnModeMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
// ПКМ по CWL/CWU открывает телеграфный терминал — по той же идиоме, что ПКМ по
|
||||||
|
// кнопке фильтра открывает редактор фильтров. ЛКМ не трогаем: переключение
|
||||||
|
// режима не должно тащить за собой окно.
|
||||||
|
begin
|
||||||
|
if Button <> mbRight then Exit;
|
||||||
|
if not ((Sender as TFlatButton).Tag in [MODE_CWL, MODE_CWU]) then Exit;
|
||||||
|
ShowCWTerminal;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
||||||
@@ -5831,6 +5887,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;
|
||||||
// Пилюли профиля на флагах: список общий, привязки могли съехать после
|
// Пилюли профиля на флагах: список общий, привязки могли съехать после
|
||||||
@@ -8023,7 +8080,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||||||
@@ -8058,7 +8115,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel, FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||||||
@@ -8104,7 +8161,7 @@ begin
|
|||||||
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel,
|
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel,
|
||||||
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -8122,6 +8179,158 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWSettingsChange(const C: TCWSettings);
|
||||||
|
// Телеграф целиком живёт в контроллере: pitch → движок, кейер/сайдтон → в
|
||||||
|
// прошивку, персист — там же. Форме остаётся только передать запись.
|
||||||
|
begin
|
||||||
|
FController.SetCWSettings(C);
|
||||||
|
// Скорость кейера = ширина полосы телеграфа на экране. Движок вьюху сам не
|
||||||
|
// уведомляет — правило то же, что для профилей и кромок TX.
|
||||||
|
PushFilterEdgesToView;
|
||||||
|
if FController.FTransmitting then FSpectrumDirty := True;
|
||||||
|
// Окно памяти показывает те же ячейки — держим его в курсе правок из настроек.
|
||||||
|
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.ShowCWTerminal;
|
||||||
|
begin
|
||||||
|
if FCWTermForm = nil then
|
||||||
|
begin
|
||||||
|
FCWTermForm := TCWTerminalForm.Create(Self);
|
||||||
|
TCWTerminalForm(FCWTermForm).OnSend := OnCWMessageSend;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnAbort := OnCWMessageAbort;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnBackspace := OnCWTermBackspace;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnDecoder := OnCWTermDecoder;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnKbdKey := OnCWTermKbdKey;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnSpeed := OnCWTermSpeed;
|
||||||
|
TCWTerminalForm(FCWTermForm).OnMacro := OnCWTermMacro;
|
||||||
|
// Лента собирается в контроллере (там же эхо своей передачи) и приходит
|
||||||
|
// сюда прибавками — окно только рисует.
|
||||||
|
FController.OnCWLog := OnCWLogAppend;
|
||||||
|
end;
|
||||||
|
TCWTerminalForm(FCWTermForm).LoadFrom(FController.CWSettings);
|
||||||
|
TCWTerminalForm(FCWTermForm).ApplyTheme(CurrentAppTheme);
|
||||||
|
TCWTerminalForm(FCWTermForm).Show;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWLogAppend(const S: string; IsTX: Boolean);
|
||||||
|
begin
|
||||||
|
if FCWTermForm <> nil then TCWTerminalForm(FCWTermForm).AppendLog(S, IsTX);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWTermBackspace(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FController.CWXBackspace;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWTermDecoder(Value: Boolean);
|
||||||
|
begin
|
||||||
|
FController.SetCWDecoder(Value);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWTermKbdKey(Dot, Dash: Boolean);
|
||||||
|
begin
|
||||||
|
FController.CWKeyboardKey(Dot, Dash);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWTermSpeed(WPM: Integer);
|
||||||
|
var C: TCWSettings;
|
||||||
|
begin
|
||||||
|
C := FController.CWSettings;
|
||||||
|
if C.Speed = WPM then Exit;
|
||||||
|
C.Speed := WPM;
|
||||||
|
FController.SetCWSettings(C);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnCWTermMacro(Sender: TObject);
|
||||||
|
var Slot: Integer;
|
||||||
|
begin
|
||||||
|
Slot := TComponent(Sender).Tag;
|
||||||
|
if (Slot < 0) or (Slot >= CW_MSG_COUNT) then Exit;
|
||||||
|
if FController.CWSettings.Messages[Slot] = '' then Exit;
|
||||||
|
FController.CWXSend(FController.CWSettings.Messages[Slot]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.ServiceCWTerminal;
|
||||||
|
// Таймер: разбор принятого аудио + обновление окна и ленты под спектром.
|
||||||
|
// Разбор идёт ВСЕГДА, когда декодер включён, даже если окно закрыто: лента под
|
||||||
|
// спектром живёт сама по себе.
|
||||||
|
var
|
||||||
|
St: string;
|
||||||
|
begin
|
||||||
|
FController.ServiceCWDecoder;
|
||||||
|
if FCWTermForm <> nil then
|
||||||
|
begin
|
||||||
|
TCWTerminalForm(FCWTermForm).SetPending(FController.CWXPendingText);
|
||||||
|
if FController.CWDecoderOn then
|
||||||
|
begin
|
||||||
|
St := Format('RX %d WPM', [FController.CWDecSpeedWPM]);
|
||||||
|
if FController.CWDecToneOffsetHz <> 0 then
|
||||||
|
St := St + Format(', tone %+d Hz', [FController.CWDecToneOffsetHz]);
|
||||||
|
if not FController.CWDecSignal then St := St + ', no signal';
|
||||||
|
end
|
||||||
|
else
|
||||||
|
St := 'decoder off';
|
||||||
|
// Набор молча уходит в никуда, если телеграф не вооружён (не CW, запрет
|
||||||
|
// передачи на бэнде или в слоте трансвертера) — говорим об этом прямо.
|
||||||
|
if not FController.CWTXActive then St := St + ' | TX not armed';
|
||||||
|
TCWTerminalForm(FCWTermForm).SetStatus(St);
|
||||||
|
end;
|
||||||
|
if FPans[0] <> nil then
|
||||||
|
if FPans[0].SetCWStrip(FController.CWSettings.DecoderStrip and FController.CWDecoderOn,
|
||||||
|
FController.CWLogTail(300)) then
|
||||||
|
ResizeSpectrumPanels; // строка появилась/пропала — переложить пан
|
||||||
|
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 — ячейки памяти телеграфа. Единственное условие — телеграф на
|
||||||
|
// передающем источнике; в прочих режимах клавиши свободны для всего другого.
|
||||||
|
// ★Текст берём ИЗ НАСТРОЕК, а не из окна памяти: окно создаётся лениво, и
|
||||||
|
// раньше клавиши молчали, пока его хоть раз не открыли.
|
||||||
|
var Slot: Integer;
|
||||||
|
begin
|
||||||
|
if Shift <> [] then Exit;
|
||||||
|
// F9 — окно телеграфного терминала (лента приёма + набор с клавиатуры).
|
||||||
|
if Key = VK_F9 then
|
||||||
|
begin
|
||||||
|
Key := 0;
|
||||||
|
ShowCWTerminal;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if (Key < VK_F1) or (Key > VK_F8) then Exit;
|
||||||
|
if not (FController.ActiveTXMode in [MODE_CWL, MODE_CWU]) then Exit;
|
||||||
|
Slot := Key - VK_F1;
|
||||||
|
if (Slot < 0) or (Slot >= CW_MSG_COUNT) then Exit;
|
||||||
|
Key := 0;
|
||||||
|
if FController.CWSettings.Messages[Slot] = '' then Exit;
|
||||||
|
FController.CWXSend(FController.CWSettings.Messages[Slot]);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
|
procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings);
|
||||||
begin
|
begin
|
||||||
FController.FAlexSettings := A;
|
FController.FAlexSettings := A;
|
||||||
@@ -9047,6 +9256,9 @@ 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.OnCWTerminalOpen := ShowCWTerminal;
|
||||||
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
|
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
|
||||||
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
|
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
|
||||||
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
|
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
|
||||||
@@ -9063,6 +9275,10 @@ 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));
|
||||||
|
// Телеграф: есть ли кейер в железе. У AD936x его нет — там телеграф может
|
||||||
|
// быть только программным, и настройки прошивки в группе гасятся.
|
||||||
|
SF.SetCWBackend(FController.BackendCaps.HasCWKeyer);
|
||||||
|
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 +9375,7 @@ begin
|
|||||||
EnsureWDSPWisdom;
|
EnsureWDSPWisdom;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// CAT subsystem
|
// CAT subsystem
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
+77
-2
@@ -115,6 +115,14 @@ type
|
|||||||
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
||||||
FHeaderVisible: Boolean;
|
FHeaderVisible: Boolean;
|
||||||
FShowZoomRow: Boolean; // False у панов N>0 (пан-зум — этап 3.3+)
|
FShowZoomRow: Boolean; // False у панов N>0 (пан-зум — этап 3.3+)
|
||||||
|
// Бегущая лента расшифровки телеграфа под спектром (только пан 0). Своя
|
||||||
|
// строка, а не окно: смотреть в эфир и читать расшифровку удобнее в одном
|
||||||
|
// месте, а полноценный терминал открывается отдельно.
|
||||||
|
FCWStripOn: Boolean;
|
||||||
|
FCWStripText: string;
|
||||||
|
FPbCWStrip: TPaintBox;
|
||||||
|
FThemeBG: TColor;
|
||||||
|
FThemeText: TColor;
|
||||||
FOnPanClose: TNotifyEvent;
|
FOnPanClose: TNotifyEvent;
|
||||||
FOnAddPan: TNotifyEvent;
|
FOnAddPan: TNotifyEvent;
|
||||||
FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума
|
FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума
|
||||||
@@ -152,6 +160,7 @@ type
|
|||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure PanZoomDblClick(Sender: TObject);
|
procedure PanZoomDblClick(Sender: TObject);
|
||||||
procedure PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean);
|
procedure PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean);
|
||||||
|
procedure CWStripPaint(Sender: TObject);
|
||||||
public
|
public
|
||||||
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
|
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
|
||||||
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
|
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
|
||||||
@@ -245,6 +254,9 @@ type
|
|||||||
property BtnAddPan: TFlatButton read FBtnAddPan;
|
property BtnAddPan: TFlatButton read FBtnAddPan;
|
||||||
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
||||||
property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow;
|
property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow;
|
||||||
|
// Лента расшифровки телеграфа: включить/выключить и обновить текст.
|
||||||
|
// True — состояние видимости изменилось, хозяину нужно переразложить пан.
|
||||||
|
function SetCWStrip(AOn: Boolean; const AText: string): Boolean;
|
||||||
// «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин.
|
// «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин.
|
||||||
property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted;
|
property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted;
|
||||||
// «▦/▤» тумблер грид-раскладки (пан 0, виден при >=2 доп. панах).
|
// «▦/▤» тумблер грид-раскладки (пан 0, виден при >=2 доп. панах).
|
||||||
@@ -432,6 +444,12 @@ begin
|
|||||||
FPbPanZoom.OnMouseUp := PanZoomMouseUp;
|
FPbPanZoom.OnMouseUp := PanZoomMouseUp;
|
||||||
FPbPanZoom.OnDblClick := PanZoomDblClick;
|
FPbPanZoom.OnDblClick := PanZoomDblClick;
|
||||||
FZoomBar.PaintBox := FPbPanZoom;
|
FZoomBar.PaintBox := FPbPanZoom;
|
||||||
|
|
||||||
|
// ---- Лента расшифровки телеграфа (пан 0, включается хозяином) ----
|
||||||
|
FPbCWStrip := TPaintBox.Create(Self);
|
||||||
|
FPbCWStrip.Parent := AParent;
|
||||||
|
FPbCWStrip.OnPaint := CWStripPaint;
|
||||||
|
FPbCWStrip.Visible := False;
|
||||||
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
||||||
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
||||||
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
||||||
@@ -501,6 +519,7 @@ begin
|
|||||||
FSplitter.Parent := NewParent;
|
FSplitter.Parent := NewParent;
|
||||||
FPbWaterfall.Parent := NewParent;
|
FPbWaterfall.Parent := NewParent;
|
||||||
FPbPanZoom.Parent := NewParent;
|
FPbPanZoom.Parent := NewParent;
|
||||||
|
if FPbCWStrip <> nil then FPbCWStrip.Parent := NewParent;
|
||||||
FBtnZoomOut.Parent := NewParent;
|
FBtnZoomOut.Parent := NewParent;
|
||||||
FBtnZoomDef.Parent := NewParent;
|
FBtnZoomDef.Parent := NewParent;
|
||||||
FBtnZoomIn.Parent := NewParent;
|
FBtnZoomIn.Parent := NewParent;
|
||||||
@@ -668,6 +687,44 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TPanafallPanel.SetCWStrip(AOn: Boolean; const AText: string): Boolean;
|
||||||
|
// Хозяин зовёт с таймера. Раскладку трогаем только на смене видимости — текст
|
||||||
|
// меняется по несколько знаков в секунду, и дёргать Layout на каждый знак
|
||||||
|
// незачем.
|
||||||
|
begin
|
||||||
|
Result := AOn <> FCWStripOn;
|
||||||
|
FCWStripOn := AOn;
|
||||||
|
if AText <> FCWStripText then
|
||||||
|
begin
|
||||||
|
FCWStripText := AText;
|
||||||
|
if (FPbCWStrip <> nil) and FPbCWStrip.Visible then FPbCWStrip.Invalidate;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanafallPanel.CWStripPaint(Sender: TObject);
|
||||||
|
// Хвост расшифровки, вытянутый по ширине: сколько знаков влезло, столько и
|
||||||
|
// показываем (лента, а не журнал — история живёт в окне терминала).
|
||||||
|
var
|
||||||
|
C: TCanvas;
|
||||||
|
Cols, ChW: Integer;
|
||||||
|
S: string;
|
||||||
|
begin
|
||||||
|
if FPbCWStrip = nil then Exit;
|
||||||
|
C := FPbCWStrip.Canvas;
|
||||||
|
C.Brush.Color := FThemeBG;
|
||||||
|
C.FillRect(0, 0, FPbCWStrip.Width, FPbCWStrip.Height);
|
||||||
|
C.Font.Name := {$IFDEF WINDOWS}'Consolas'{$ELSE}'Monospace'{$ENDIF};
|
||||||
|
C.Font.Size := 9;
|
||||||
|
C.Font.Color := FThemeText;
|
||||||
|
C.Brush.Style := bsClear;
|
||||||
|
ChW := C.TextWidth('W');
|
||||||
|
if ChW <= 0 then ChW := 8;
|
||||||
|
Cols := Max(1, (FPbCWStrip.Width - DpiScale(8)) div ChW);
|
||||||
|
S := FCWStripText;
|
||||||
|
if Length(S) > Cols then S := Copy(S, Length(S) - Cols + 1, Cols);
|
||||||
|
C.TextOut(DpiScale(4), DpiScale(2), S);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.PositionPanZoomBar(X0, BottomY, RW, H: Integer;
|
procedure TPanafallPanel.PositionPanZoomBar(X0, BottomY, RW, H: Integer;
|
||||||
AVisible: Boolean);
|
AVisible: Boolean);
|
||||||
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd, WantGrid: Boolean;
|
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd, WantGrid: Boolean;
|
||||||
@@ -725,6 +782,8 @@ begin
|
|||||||
if FHeaderVisible then Inc(Result, HEADER_H);
|
if FHeaderVisible then Inc(Result, HEADER_H);
|
||||||
if (AShowSpectrum or AShowWaterfall) and FShowZoomRow then
|
if (AShowSpectrum or AShowWaterfall) and FShowZoomRow then
|
||||||
Inc(Result, PANZOOM_H);
|
Inc(Result, PANZOOM_H);
|
||||||
|
if FCWStripOn and (AShowSpectrum or AShowWaterfall) then
|
||||||
|
Inc(Result, DpiScale(20));
|
||||||
if AShowSpectrum then Inc(Result, MIN_SH + RULER_H);
|
if AShowSpectrum then Inc(Result, MIN_SH + RULER_H);
|
||||||
if AShowWaterfall then
|
if AShowWaterfall then
|
||||||
begin
|
begin
|
||||||
@@ -746,6 +805,7 @@ var
|
|||||||
RULER_H: Integer;
|
RULER_H: Integer;
|
||||||
PANZOOM_H: Integer;
|
PANZOOM_H: Integer;
|
||||||
HEADER_H: Integer;
|
HEADER_H: Integer;
|
||||||
|
CWSTRIP_H: Integer;
|
||||||
ShowPZ: Boolean;
|
ShowPZ: Boolean;
|
||||||
EffRatio: Double;
|
EffRatio: Double;
|
||||||
begin
|
begin
|
||||||
@@ -784,6 +844,17 @@ begin
|
|||||||
ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow;
|
ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow;
|
||||||
PositionPanZoomBar(X0, RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
PositionPanZoomBar(X0, RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||||
if ShowPZ then RH := RH - PANZOOM_H;
|
if ShowPZ then RH := RH - PANZOOM_H;
|
||||||
|
// Лента расшифровки телеграфа — ещё одной строкой над рядом пан/зума.
|
||||||
|
if FPbCWStrip <> nil then
|
||||||
|
begin
|
||||||
|
FPbCWStrip.Visible := FCWStripOn and (AShowSpectrum or AShowWaterfall);
|
||||||
|
if FPbCWStrip.Visible then
|
||||||
|
begin
|
||||||
|
CWSTRIP_H := DpiScale(20);
|
||||||
|
FPbCWStrip.SetBounds(X0, RH - CWSTRIP_H, RW, CWSTRIP_H);
|
||||||
|
RH := RH - CWSTRIP_H;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
TopOff := ATopOffset + FStackTop;
|
TopOff := ATopOffset + FStackTop;
|
||||||
if FHeaderVisible then Inc(TopOff, HEADER_H);
|
if FHeaderVisible then Inc(TopOff, HEADER_H);
|
||||||
|
|
||||||
@@ -1366,7 +1437,7 @@ begin
|
|||||||
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
|
O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
|
||||||
// TxSel: слайс — источник передачи; Tx: передача идёт именно с него.
|
// TxSel: слайс — источник передачи; Tx: передача идёт именно с него.
|
||||||
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
|
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
|
||||||
FController.FTransmitting and (FController.FTxSliceId = SliceId),
|
FController.RadioKeyed and (FController.FTxSliceId = SliceId),
|
||||||
FController.FTxSliceId = SliceId);
|
FController.FTxSliceId = SliceId);
|
||||||
// Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX.
|
// Auto TX (настройка CAT-порта слайса) — бейдж TX подписан AutoTX.
|
||||||
O.SetAutoTxState(FController.SliceAutoTx(SliceId));
|
O.SetAutoTxState(FController.SliceAutoTx(SliceId));
|
||||||
@@ -1417,7 +1488,7 @@ begin
|
|||||||
// передаём со слайса, ActiveTXFreqHz берёт его частоту, а не VFO B.
|
// передаём со слайса, ActiveTXFreqHz берёт его частоту, а не VFO B.
|
||||||
FMainFlag.SetExtState(FController.ActiveVolume, FController.FMuted,
|
FMainFlag.SetExtState(FController.ActiveVolume, FController.FMuted,
|
||||||
FController.FSplitTxB and (FController.FTxSliceId = 0),
|
FController.FSplitTxB and (FController.FTxSliceId = 0),
|
||||||
FController.FTransmitting and (FController.FTxSliceId = 0),
|
FController.RadioKeyed and (FController.FTxSliceId = 0),
|
||||||
FController.FTxSliceId = 0);
|
FController.FTxSliceId = 0);
|
||||||
PushFlagTXProfiles(FMainFlag, 0);
|
PushFlagTXProfiles(FMainFlag, 0);
|
||||||
end;
|
end;
|
||||||
@@ -1533,6 +1604,10 @@ end;
|
|||||||
procedure TPanafallPanel.SetFlagsTheme(const T: TAppTheme);
|
procedure TPanafallPanel.SetFlagsTheme(const T: TAppTheme);
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
|
// Лента расшифровки рисуется своими руками — цвета держим здесь же.
|
||||||
|
FThemeBG := T.Panel;
|
||||||
|
FThemeText := T.Text;
|
||||||
|
if (FPbCWStrip <> nil) and FPbCWStrip.Visible then FPbCWStrip.Invalidate;
|
||||||
if Assigned(FMainFlag) then FMainFlag.SetTheme(T);
|
if Assigned(FMainFlag) then FMainFlag.SetTheme(T);
|
||||||
for i := 0 to High(FSliceFlags) do
|
for i := 0 to High(FSliceFlags) do
|
||||||
if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T);
|
if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T);
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ begin
|
|||||||
Result.HasWideband := False;
|
Result.HasWideband := False;
|
||||||
Result.HasDitherRandom := False;
|
Result.HasDitherRandom := False;
|
||||||
Result.HasHWMic := False;
|
Result.HasHWMic := False;
|
||||||
|
Result.HasCWKeyer := False; // голый AD936x — телеграф формирует PC
|
||||||
Result.HasPLLStatus := False;
|
Result.HasPLLStatus := False;
|
||||||
Result.HasHWGain := True; // manual gain / hw-AGC
|
Result.HasHWGain := True; // manual gain / hw-AGC
|
||||||
Result.HasRFBandwidth := True;
|
Result.HasRFBandwidth := True;
|
||||||
@@ -634,6 +635,15 @@ begin
|
|||||||
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
|
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
|
||||||
|
|
||||||
FConnected := True;
|
FConnected := True;
|
||||||
|
// ★Калибровки — в автомат. AD936x нулит утечку гетеродина и квадратурный
|
||||||
|
// разбаланс передатчика калибровкой TX quad, и в режиме 'auto' драйвер сам
|
||||||
|
// прогоняет её на каждой смене TX LO. Найдено на живом Pluto: там стояло
|
||||||
|
// 'manual_tx_quad', то есть калибровка НЕ переигрывалась вовсе — утечка LO
|
||||||
|
// (в телеграфе она видна отдельной несущей в стороне от рабочей частоты)
|
||||||
|
// оставалась такой, какой её оставила прошлая программа. Состояние живёт в
|
||||||
|
// чипе между сессиями, поэтому выставляем явно.
|
||||||
|
if Assigned(iio_device_attr_write) then
|
||||||
|
iio_device_attr_write(FPhy, 'calib_mode', 'auto');
|
||||||
// Приводим железо в известное состояние: валидный rate + вход + усиление.
|
// Приводим железо в известное состояние: валидный rate + вход + усиление.
|
||||||
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
|
||||||
ApplySampleRate(FSampleRate);
|
ApplySampleRate(FSampleRate);
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ type
|
|||||||
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
|
// feedback (Angelia/Orion/Orion2/Saturn). Гейтит кнопки PS/2TON.
|
||||||
// Достоверно после Connect.
|
// Достоверно после Connect.
|
||||||
HasPureSignal: Boolean;
|
HasPureSignal: Boolean;
|
||||||
|
// Телеграфный кейер в железе: точки/тире формирует прошивка по параметрам из
|
||||||
|
// DUC Specific (openHPSDR P2), а манипулятор воткнут в сам трансивер. False
|
||||||
|
// (голый AD936x) означает, что телеграф обязан формировать локальный
|
||||||
|
// генератор — см. CWKeyer.pas.
|
||||||
|
HasCWKeyer: Boolean;
|
||||||
MinSampleRate: Integer;
|
MinSampleRate: Integer;
|
||||||
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
|
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
|
||||||
MinFreqHz: Double;
|
MinFreqHz: Double;
|
||||||
@@ -180,6 +185,15 @@ 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;
|
||||||
|
// Кейер в прошивке вооружён: железо может замкнуть ключ САМО, без нашего
|
||||||
|
// PTT. Пока флаг взведён, уровень мощности обязан лежать в каждом HP-кадре
|
||||||
|
// (обычно он шлётся только на передаче — а «передачи» с точки зрения софта
|
||||||
|
// тут не происходит вовсе). Pluto — no-op.
|
||||||
|
procedure SetCWKeyerArmed(Armed: 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 +291,14 @@ procedure TRadioBackend.SendHighPriority(const Pkt: THighPriorityPacket);
|
|||||||
begin
|
begin
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioBackend.SetCWXKey(Down: Boolean);
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioBackend.SetCWKeyerArmed(Armed: Boolean);
|
||||||
|
begin
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioBackend.SetStepAtten(dB: Byte);
|
procedure TRadioBackend.SetStepAtten(dB: Byte);
|
||||||
begin
|
begin
|
||||||
end;
|
end;
|
||||||
|
|||||||
+1035
-53
File diff suppressed because it is too large
Load Diff
+184
@@ -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,66 @@ 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];
|
||||||
|
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
|
||||||
|
// ключа нет). ВМЕСТЕ с LocalKeyer образует одну развилку «кто рисует точки»,
|
||||||
|
// в UI это один список: прошивка / программа / выкл.
|
||||||
|
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 % от полной шкалы аудио
|
||||||
|
// ---- Локальный (программный) кейер --------------------------------------
|
||||||
|
// Манипуляцию формирует САМ EWSDR: генератор рисует несущую с огибающей
|
||||||
|
// прямо в поток IQ (CWKeyer.pas). Для Pluto/LibreSDR это единственный
|
||||||
|
// возможный путь — кейера в железе там нет, и выбор «прошивка» вырождается
|
||||||
|
// туда сам; для openHPSDR это опция, по умолчанию выключенная (тайминги в
|
||||||
|
// FPGA точнее любой программы).
|
||||||
|
LocalKeyer: Boolean;
|
||||||
|
// Сдвиг несущей манипуляции от гетеродина. 0 = авто: у openHPSDR 0 (DUC —
|
||||||
|
// цифровой, утекать нечему), у zero-IF AD936x — CW_AUTO_OFFSET_HZ, иначе
|
||||||
|
// утечка LO сидит РОВНО на рабочей частоте и слышна между посылками.
|
||||||
|
CarrierOffsetHz: Integer;
|
||||||
|
// Манипулятор на модемных линиях COM/USB-serial (DTR/RTS питают ключ,
|
||||||
|
// CTS/DSR/DCD/RI читаются как лепестки).
|
||||||
|
KeyPortEnabled: Boolean;
|
||||||
|
KeyPort: string[63];
|
||||||
|
KeyDotLine: Integer; // 0=CTS, 1=DSR, 2=DCD, 3=RI
|
||||||
|
KeyDashLine: Integer;
|
||||||
|
KeyInvert: Boolean;
|
||||||
|
KeyPowerDTR: Boolean;
|
||||||
|
KeyPowerRTS: Boolean;
|
||||||
|
// ---- Окно-терминал: набор с клавиатуры + декодер приёма ----------------
|
||||||
|
Decoder: Boolean; // разбирать принимаемый телеграф в текст
|
||||||
|
DecoderStrip: Boolean; // бегущая лента расшифровки под спектром
|
||||||
|
KbdKey: Boolean; // Ctrl в окне терминала работает манипулятором
|
||||||
|
end;
|
||||||
|
|
||||||
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
|
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
|
||||||
// Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию
|
// Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию
|
||||||
// микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку,
|
// микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку,
|
||||||
@@ -646,6 +707,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 +1798,125 @@ 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.LocalKeyer := False; // у openHPSDR тайминги в FPGA; Pluto включит сам
|
||||||
|
C.CarrierOffsetHz := 0; // 0 = авто (по типу бэкенда)
|
||||||
|
C.KeyPortEnabled := False;
|
||||||
|
C.KeyPort := '';
|
||||||
|
C.KeyDotLine := 0; // CTS
|
||||||
|
C.KeyDashLine := 1; // DSR
|
||||||
|
C.KeyInvert := False;
|
||||||
|
C.KeyPowerDTR := True; // типовая распайка: ключ питается от DTR/RTS
|
||||||
|
C.KeyPowerRTS := True;
|
||||||
|
C.Decoder := True; // окно терминала без декодера бессмысленно
|
||||||
|
C.DecoderStrip := False; // лента под спектром — по желанию
|
||||||
|
C.KbdKey := False; // Ctrl-манипулятор включают осознанно
|
||||||
|
// Заводские заготовки: типовой вызов, ответ и стандартные концовки.
|
||||||
|
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);
|
||||||
|
C.LocalKeyer := JB(O,'local_keyer', C.LocalKeyer);
|
||||||
|
C.CarrierOffsetHz := EnsureRange(JI(O,'carrier_offset_hz', C.CarrierOffsetHz), -1, 48000);
|
||||||
|
C.KeyPortEnabled := JB(O,'key_port_on', C.KeyPortEnabled);
|
||||||
|
C.KeyPort := Copy(JS(O,'key_port', C.KeyPort), 1, 63);
|
||||||
|
C.KeyDotLine := EnsureRange(JI(O,'key_dot_line', C.KeyDotLine), 0, 3);
|
||||||
|
C.KeyDashLine := EnsureRange(JI(O,'key_dash_line', C.KeyDashLine), 0, 3);
|
||||||
|
C.KeyInvert := JB(O,'key_invert', C.KeyInvert);
|
||||||
|
C.KeyPowerDTR := JB(O,'key_power_dtr', C.KeyPowerDTR);
|
||||||
|
C.KeyPowerRTS := JB(O,'key_power_rts', C.KeyPowerRTS);
|
||||||
|
C.Decoder := JB(O,'decoder', C.Decoder);
|
||||||
|
C.DecoderStrip := JB(O,'decoder_strip', C.DecoderStrip);
|
||||||
|
C.KbdKey := JB(O,'kbd_key', C.KbdKey);
|
||||||
|
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);
|
||||||
|
JW(O,'local_keyer', C.LocalKeyer);
|
||||||
|
JW(O,'carrier_offset_hz', C.CarrierOffsetHz);
|
||||||
|
JW(O,'key_port_on', C.KeyPortEnabled);
|
||||||
|
JWS(O,'key_port', C.KeyPort);
|
||||||
|
JW(O,'key_dot_line', C.KeyDotLine);
|
||||||
|
JW(O,'key_dash_line', C.KeyDashLine);
|
||||||
|
JW(O,'key_invert', C.KeyInvert);
|
||||||
|
JW(O,'key_power_dtr', C.KeyPowerDTR);
|
||||||
|
JW(O,'key_power_rts', C.KeyPowerRTS);
|
||||||
|
JW(O,'decoder', C.Decoder);
|
||||||
|
JW(O,'decoder_strip', C.DecoderStrip);
|
||||||
|
JW(O,'kbd_key', C.KbdKey);
|
||||||
|
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,9 @@ 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;
|
||||||
|
TOnCWTerminalOpen = 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 +288,37 @@ type
|
|||||||
// CTCSS
|
// CTCSS
|
||||||
FChkTXCTCSSOn: TFlatCheckBox;
|
FChkTXCTCSSOn: TFlatCheckBox;
|
||||||
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
FEdTXCTCSSFreq: TFlatFloatSpinEdit;
|
||||||
|
// CW (телеграф) — свойство устройства, профилем не переключается
|
||||||
|
FCW: TCWSettings;
|
||||||
|
// Кто рисует точки: прошивка / программа / никто. Одна развилка — один
|
||||||
|
// список (две галки на неё разъезжались во взаимоисключающих состояниях).
|
||||||
|
FCmbCWKeyerSrc: TFlatComboBox;
|
||||||
|
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;
|
||||||
|
// Локальный кейер: манипуляцию рисует сам EWSDR (обязателен на AD936x)
|
||||||
|
FEdCWOffset: TFlatSpinEdit;
|
||||||
|
FChkCWKeyPort: TFlatCheckBox;
|
||||||
|
FEdCWKeyPort: TFlatEdit;
|
||||||
|
FCmbCWDotLine: TFlatComboBox;
|
||||||
|
FCmbCWDashLine: TFlatComboBox;
|
||||||
|
FChkCWDecoder: TFlatCheckBox;
|
||||||
|
FChkCWDecStrip: TFlatCheckBox;
|
||||||
|
FChkCWKeyInvert: TFlatCheckBox;
|
||||||
|
FChkCWKeyDTR: TFlatCheckBox;
|
||||||
|
FChkCWKeyRTS: TFlatCheckBox;
|
||||||
|
FCWHasFWKeyer: Boolean; // у железа есть кейер в прошивке
|
||||||
// TX Display (отдельный analyzer)
|
// TX Display (отдельный analyzer)
|
||||||
FCmbTXFFT: TFlatComboBox;
|
FCmbTXFFT: TFlatComboBox;
|
||||||
FCmbTXWindow: TFlatComboBox;
|
FCmbTXWindow: TFlatComboBox;
|
||||||
@@ -446,6 +480,9 @@ type
|
|||||||
FOnWfAGCNFChange: TOnWfAGCNFChange;
|
FOnWfAGCNFChange: TOnWfAGCNFChange;
|
||||||
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
|
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
|
||||||
FOnTXChange: TOnTXSettingsChange;
|
FOnTXChange: TOnTXSettingsChange;
|
||||||
|
FOnCWChange: TOnCWSettingsChange;
|
||||||
|
FOnCWMessagesOpen: TOnCWMessagesOpen;
|
||||||
|
FOnCWTerminalOpen: TOnCWTerminalOpen;
|
||||||
FOnTXProfileSelect: TOnTXProfileIdx;
|
FOnTXProfileSelect: TOnTXProfileIdx;
|
||||||
FOnTXProfileAdd: TOnTXProfileName;
|
FOnTXProfileAdd: TOnTXProfileName;
|
||||||
FOnTXProfileRename: TOnTXProfileIdxName;
|
FOnTXProfileRename: TOnTXProfileIdxName;
|
||||||
@@ -500,6 +537,11 @@ 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 BtnCWTerminalClick(Sender: TObject);
|
||||||
procedure OnMicJackChange(Sender: TObject);
|
procedure OnMicJackChange(Sender: TObject);
|
||||||
procedure UpdateMicJackVisibility;
|
procedure UpdateMicJackVisibility;
|
||||||
// Подвкладки и полоса профиля на странице Transmit
|
// Подвкладки и полоса профиля на странице Transmit
|
||||||
@@ -632,6 +674,9 @@ 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);
|
||||||
|
// Есть ли у активного железа кейер в прошивке (openHPSDR — да, AD936x — нет).
|
||||||
|
procedure SetCWBackend(HasFWKeyer: Boolean);
|
||||||
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
|
||||||
// главном окне, здесь показываем его значение как часть профиля).
|
// главном окне, здесь показываем его значение как часть профиля).
|
||||||
procedure LoadTXProfiles(const Names: array of string;
|
procedure LoadTXProfiles(const Names: array of string;
|
||||||
@@ -678,6 +723,9 @@ 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 OnCWTerminalOpen: TOnCWTerminalOpen read FOnCWTerminalOpen write FOnCWTerminalOpen;
|
||||||
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 +1843,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 +2060,88 @@ 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, 502);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Keyer', PAD, R1 - 1, 50);
|
||||||
|
FCmbCWKeyerSrc := MkCmbCW(Grp, PAD + 54, R1 - 6, 194);
|
||||||
|
FCmbCWKeyerSrc.Items.AddStrings(['Radio firmware','Software (PC)','Off (MOX only)']);
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||||
|
MkBtn(Grp, PAD + 260, R1 - 6, 100, 'Messages...', BtnCWMessagesClick);
|
||||||
|
MkBtn(Grp, PAD + 366, R1 - 6, 100, 'Terminal...', BtnCWTerminalClick);
|
||||||
|
MakeLbl(Grp, 'Pitch (Hz)', PAD + 480, R1 - 1, 90);
|
||||||
|
FEdCWPitch := MkSpinCW(Grp, PAD + 574, 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);
|
||||||
|
|
||||||
|
// ---- Программный кейер: манипуляцию рисует сам EWSDR --------------------
|
||||||
|
MakeLbl(Grp, 'Carrier offset (Hz)', PAD, R1 + 5 * STEP + 5, LW);
|
||||||
|
FEdCWOffset := MkSpinCW(Grp, CX, R1 + 5 * STEP, -1, 48000, 0);
|
||||||
|
MakeLbl(Grp, '0 = auto, -1 = none (software keyer only)', PAD + 320, R1 + 5 * STEP + 5, 260);
|
||||||
|
FChkCWDecoder := MkChkCW(Grp, PAD, R1 + 6 * STEP, 160, 'Decode RX');
|
||||||
|
FChkCWDecStrip := MkChkCW(Grp, PAD + 176, R1 + 6 * STEP, 210, 'Text under spectrum');
|
||||||
|
|
||||||
|
FChkCWKeyPort := MkChkCW(Grp, PAD, R1 + 7 * STEP, 250, 'Paddle on serial port');
|
||||||
|
FEdCWKeyPort := TFlatEdit.Create(Self);
|
||||||
|
FEdCWKeyPort.Parent := Grp;
|
||||||
|
FEdCWKeyPort.SetBounds(DpiScale(PAD + 260), DpiScale(R1 + 7 * STEP),
|
||||||
|
DpiScale(220), DpiScale(BTN_H));
|
||||||
|
FEdCWKeyPort.Color := CLR_INPUT;
|
||||||
|
FEdCWKeyPort.Font.Color := CLR_INPUT_TEXT;
|
||||||
|
FEdCWKeyPort.Font.Size := 9;
|
||||||
|
FEdCWKeyPort.OnExit := OnCWAnyChange;
|
||||||
|
MakeLbl(Grp, '/dev/ttyUSB0, COM3', PAD + 492, R1 + 7 * STEP + 5, 200);
|
||||||
|
|
||||||
|
MakeLbl(Grp, 'Dot line', PAD, R1 + 8 * STEP + 5, 70);
|
||||||
|
FCmbCWDotLine := MkCmbCW(Grp, PAD + 74, R1 + 8 * STEP, 90);
|
||||||
|
FCmbCWDotLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||||
|
FCmbCWDotLine.ItemIndex := 0;
|
||||||
|
MakeLbl(Grp, 'Dash line', PAD + 176, R1 + 8 * STEP + 5, 74);
|
||||||
|
FCmbCWDashLine := MkCmbCW(Grp, PAD + 254, R1 + 8 * STEP, 90);
|
||||||
|
FCmbCWDashLine.Items.AddStrings(['CTS','DSR','DCD','RI']);
|
||||||
|
FCmbCWDashLine.ItemIndex := 1;
|
||||||
|
FChkCWKeyInvert := MkChkCW(Grp, PAD + 360, R1 + 8 * STEP, 90, 'Invert');
|
||||||
|
FChkCWKeyDTR := MkChkCW(Grp, PAD + 456, R1 + 8 * STEP, 100, 'DTR power');
|
||||||
|
FChkCWKeyRTS := MkChkCW(Grp, PAD + 562, R1 + 8 * STEP, 100, 'RTS power');
|
||||||
|
|
||||||
|
TxNote(Grp, 'Firmware keyer: the radio produces dot and dash timing, so PC '
|
||||||
|
+ 'load never affects it — the default where the hardware has one, and the '
|
||||||
|
+ 'paddle plugs into the radio. Software keyer: EWSDR draws the keyed '
|
||||||
|
+ 'carrier into the IQ stream itself (the only option on Pluto/LibreSDR) '
|
||||||
|
+ 'and the paddle comes in over a serial port; element lengths are counted '
|
||||||
|
+ 'in samples, so only the paddle input jitters, never the rhythm. Carrier '
|
||||||
|
+ 'offset 0 means auto: zero on openHPSDR, 12 kHz on zero-IF hardware, '
|
||||||
|
+ 'where LO leakage would otherwise sit right on the working frequency '
|
||||||
|
+ 'between elements. The voice transmit chain is not started in CW at all.',
|
||||||
|
R1 + 9 * STEP - 6, 92);
|
||||||
|
|
||||||
|
Inc(Y, 502 + 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 +2329,137 @@ begin
|
|||||||
FireTXChange;
|
FireTXChange;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.CollectCWFromUI;
|
||||||
|
var Src: Integer;
|
||||||
|
begin
|
||||||
|
// Список «кто рисует точки» → пара флагов. На железе без кейера в прошивке
|
||||||
|
// первого пункта в списке нет вовсе, поэтому индексы сдвинуты на один.
|
||||||
|
if FCmbCWKeyerSrc <> nil then
|
||||||
|
begin
|
||||||
|
Src := FCmbCWKeyerSrc.ItemIndex;
|
||||||
|
if Src < 0 then Src := 0;
|
||||||
|
if not FCWHasFWKeyer then Inc(Src);
|
||||||
|
case Src of
|
||||||
|
0: begin FCW.FWKeyer := True; FCW.LocalKeyer := False; end; // прошивка
|
||||||
|
1: begin FCW.FWKeyer := True; FCW.LocalKeyer := True; end; // программа
|
||||||
|
else begin FCW.FWKeyer := False; FCW.LocalKeyer := False; end; // выключен
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if FEdCWPitch <> nil then FCW.Pitch := FEdCWPitch.Value;
|
||||||
|
if FCmbCWKeyerMode <> nil then FCW.KeyerMode := EnsureRange(FCmbCWKeyerMode.ItemIndex, 0, 2);
|
||||||
|
if FChkCWReverse <> nil then FCW.ReverseKeys := FChkCWReverse.Checked;
|
||||||
|
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;
|
||||||
|
if FEdCWOffset <> nil then FCW.CarrierOffsetHz := FEdCWOffset.Value;
|
||||||
|
if FChkCWKeyPort <> nil then FCW.KeyPortEnabled := FChkCWKeyPort.Checked;
|
||||||
|
if FEdCWKeyPort <> nil then FCW.KeyPort := Copy(FEdCWKeyPort.Text, 1, 63);
|
||||||
|
if FCmbCWDotLine <> nil then FCW.KeyDotLine := EnsureRange(FCmbCWDotLine.ItemIndex, 0, 3);
|
||||||
|
if FCmbCWDashLine <> nil then FCW.KeyDashLine := EnsureRange(FCmbCWDashLine.ItemIndex, 0, 3);
|
||||||
|
if FChkCWDecoder <> nil then FCW.Decoder := FChkCWDecoder.Checked;
|
||||||
|
if FChkCWDecStrip <> nil then FCW.DecoderStrip := FChkCWDecStrip.Checked;
|
||||||
|
if FChkCWKeyInvert <> nil then FCW.KeyInvert := FChkCWKeyInvert.Checked;
|
||||||
|
if FChkCWKeyDTR <> nil then FCW.KeyPowerDTR := FChkCWKeyDTR.Checked;
|
||||||
|
if FChkCWKeyRTS <> nil then FCW.KeyPowerRTS := FChkCWKeyRTS.Checked;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.SetCWBackend(HasFWKeyer: Boolean);
|
||||||
|
// Гейт по железу: у AD936x кейера в прошивке нет вовсе, значит нет ни этого
|
||||||
|
// пункта в списке, ни аппаратного сайдтона (он живёт в наушниках самого
|
||||||
|
// трансивера) — телеграф там может быть только программным.
|
||||||
|
// ★Обязан отработать ДО LoadCWSettings: тот считает индекс списка по FCWHasFWKeyer.
|
||||||
|
begin
|
||||||
|
FCWHasFWKeyer := HasFWKeyer;
|
||||||
|
if FChkCWSidetoneHW <> nil then FChkCWSidetoneHW.Enabled := HasFWKeyer;
|
||||||
|
if FEdCWSidetoneHWLv <> nil then FEdCWSidetoneHWLv.Enabled := HasFWKeyer;
|
||||||
|
if FCmbCWKeyerSrc = nil then Exit;
|
||||||
|
FLoading := True; // правка Items дёргает OnChange — настройки трогать нельзя
|
||||||
|
try
|
||||||
|
FCmbCWKeyerSrc.Items.Clear;
|
||||||
|
if HasFWKeyer then FCmbCWKeyerSrc.Items.Add('Radio firmware');
|
||||||
|
FCmbCWKeyerSrc.Items.Add('Software (PC)');
|
||||||
|
FCmbCWKeyerSrc.Items.Add('Off (MOX only)');
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := 0;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.FireCWChange;
|
||||||
|
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.BtnCWTerminalClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FOnCWTerminalOpen) then FOnCWTerminalOpen;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSettingsForm.LoadCWSettings(const C: TCWSettings);
|
||||||
|
var Src: Integer;
|
||||||
|
begin
|
||||||
|
FLoading := True;
|
||||||
|
try
|
||||||
|
FCW := C;
|
||||||
|
if FCmbCWKeyerSrc <> nil then
|
||||||
|
begin
|
||||||
|
// Пара флагов → пункт списка. «Прошивка» на железе без кейера означает
|
||||||
|
// программный кейер: другого пути там нет, и врать в списке незачем.
|
||||||
|
if (not C.FWKeyer) and (not C.LocalKeyer) then Src := 2
|
||||||
|
else if C.LocalKeyer or (not FCWHasFWKeyer) then Src := 1
|
||||||
|
else Src := 0;
|
||||||
|
if not FCWHasFWKeyer then Dec(Src);
|
||||||
|
FCmbCWKeyerSrc.ItemIndex := EnsureRange(Src, 0, FCmbCWKeyerSrc.Items.Count - 1);
|
||||||
|
end;
|
||||||
|
if FEdCWPitch <> nil then FEdCWPitch.Value := EnsureRange(C.Pitch, 200, 1200);
|
||||||
|
if FCmbCWKeyerMode <> nil then FCmbCWKeyerMode.ItemIndex := EnsureRange(C.KeyerMode, 0, 2);
|
||||||
|
if FChkCWReverse <> nil then FChkCWReverse.Checked := C.ReverseKeys;
|
||||||
|
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);
|
||||||
|
if FEdCWOffset <> nil then FEdCWOffset.Value := EnsureRange(C.CarrierOffsetHz, -1, 48000);
|
||||||
|
if FChkCWKeyPort <> nil then FChkCWKeyPort.Checked := C.KeyPortEnabled;
|
||||||
|
if FEdCWKeyPort <> nil then FEdCWKeyPort.Text := C.KeyPort;
|
||||||
|
if FCmbCWDotLine <> nil then FCmbCWDotLine.ItemIndex := EnsureRange(C.KeyDotLine, 0, 3);
|
||||||
|
if FCmbCWDashLine <> nil then FCmbCWDashLine.ItemIndex := EnsureRange(C.KeyDashLine, 0, 3);
|
||||||
|
if FChkCWDecoder <> nil then FChkCWDecoder.Checked := C.Decoder;
|
||||||
|
if FChkCWDecStrip <> nil then FChkCWDecStrip.Checked := C.DecoderStrip;
|
||||||
|
if FChkCWKeyInvert <> nil then FChkCWKeyInvert.Checked := C.KeyInvert;
|
||||||
|
if FChkCWKeyDTR <> nil then FChkCWKeyDTR.Checked := C.KeyPowerDTR;
|
||||||
|
if FChkCWKeyRTS <> nil then FChkCWKeyRTS.Checked := C.KeyPowerRTS;
|
||||||
|
finally
|
||||||
|
FLoading := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.UpdateMicJackVisibility;
|
procedure TSettingsForm.UpdateMicJackVisibility;
|
||||||
var LineIn: Boolean;
|
var LineIn: Boolean;
|
||||||
begin
|
begin
|
||||||
|
|||||||
+123
-12
@@ -472,6 +472,8 @@ 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)
|
||||||
|
FCWSpeed: Integer; // WPM — только для занимаемой полосы на экране
|
||||||
FNRMode: Integer;
|
FNRMode: Integer;
|
||||||
FNBMode: Integer;
|
FNBMode: Integer;
|
||||||
FSNBEnabled: Boolean;
|
FSNBEnabled: Boolean;
|
||||||
@@ -505,6 +507,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 +651,17 @@ 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;
|
||||||
|
// Скорость кейера нужна движку ровно для одного — ширины полосы телеграфа
|
||||||
|
// на экране (TXFilterEdgesHz). Тайминги делает прошивка, не мы.
|
||||||
|
procedure SetCWSpeed(WPM: 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 +1252,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 +1373,8 @@ begin
|
|||||||
FFilterHigh := 2400;
|
FFilterHigh := 2400;
|
||||||
FAGCMode := agcMedium;
|
FAGCMode := agcMedium;
|
||||||
FShiftHz := 0.0;
|
FShiftHz := 0.0;
|
||||||
|
FCWPitch := 600;
|
||||||
|
FCWSpeed := 20;
|
||||||
FAGCTop := -90.0;
|
FAGCTop := -90.0;
|
||||||
FVolume := 0.7;
|
FVolume := 0.7;
|
||||||
FSMeter := -130.0;
|
FSMeter := -130.0;
|
||||||
@@ -1932,8 +1984,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 +2509,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 +2785,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 +2821,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 +2845,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 +3025,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 +3041,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 +3060,46 @@ 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;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetCWSpeed(WPM: Integer);
|
||||||
|
begin
|
||||||
|
if WPM < 5 then WPM := 5;
|
||||||
|
if WPM > 60 then WPM := 60;
|
||||||
|
FCWSpeed := WPM;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
|
procedure TWDSPEngine.SetAGCTop(TopDBm: Double);
|
||||||
@@ -3733,6 +3833,17 @@ begin
|
|||||||
MODE_FM: Half := FTXFMDeviation + FTXFMHighCut;
|
MODE_FM: Half := FTXFMDeviation + FTXFMHighCut;
|
||||||
MODE_WFM: Half := WFM_DEVIATION + WFM_AF_HIGH;
|
MODE_WFM: Half := WFM_DEVIATION + WFM_AF_HIGH;
|
||||||
MODE_FMRAW: Half := FRawFMDeviation + RAW_AF_HIGH;
|
MODE_FMRAW: Half := FRawFMDeviation + RAW_AF_HIGH;
|
||||||
|
// ★Телеграф. TXSignedEdges отдаёт для CWL/CWU ГОЛОСОВУЮ боковую (bp0 обязан
|
||||||
|
// оставаться открытым: через него идёт тон pitch при настройке TUN), но в
|
||||||
|
// эфир уходит манипулируемая НЕСУЩАЯ на самом VFO. Рисовать её юбкой SSB
|
||||||
|
// нельзя — полоса выглядела как в SSB и уезжала вбок от несущей.
|
||||||
|
// Занимаемая полоса телеграфа: B = WPM/1.2 бод, BW ≈ 5·B при мягкой
|
||||||
|
// манипуляции (фронт CWRampPeriod). Симметрично несущей.
|
||||||
|
MODE_CWL, MODE_CWU:
|
||||||
|
begin
|
||||||
|
Half := 5.0 * (FCWSpeed / 1.2) / 2.0;
|
||||||
|
if Half < 25.0 then Half := 25.0; // не вырождаться в нулевую полоску
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
if Half > 0.0 then
|
if Half > 0.0 then
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -308,6 +308,26 @@
|
|||||||
<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="CWKeyer.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="CWDecoder.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="CWTerminalForm.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"/>
|
||||||
|
|||||||
@@ -467,6 +467,9 @@ begin
|
|||||||
begin
|
begin
|
||||||
if FCtrl.FRunning and FCtrl.FWDSPReady then
|
if FCtrl.FRunning and FCtrl.FWDSPReady then
|
||||||
FCtrl.ServiceBeaconLock;
|
FCtrl.ServiceBeaconLock;
|
||||||
|
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и
|
||||||
|
// отпустить реле T/R после выдержки больше некому).
|
||||||
|
FCtrl.ServiceCWKeyed;
|
||||||
lastBeacon := nowMs;
|
lastBeacon := nowMs;
|
||||||
end;
|
end;
|
||||||
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
||||||
|
|||||||
Reference in New Issue
Block a user