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:
2026-08-12 23:47:57 +03:00
co-authored by Claude Opus 5
20 changed files with 4966 additions and 92 deletions
+58
View File
@@ -38,6 +38,7 @@ type
FSyncFreq: Double; FSyncFreq: Double;
FSyncInt: Integer; FSyncInt: Integer;
FSyncBool: Boolean; FSyncBool: Boolean;
FSyncStr: string;
// ── Геттеры (CAT-поток, read-only) ── // ── Геттеры (CAT-поток, read-only) ──
function GetVfoA: Double; function GetVfoA: Double;
@@ -86,6 +87,15 @@ type
procedure SetSNB(V: Boolean); procedure SetSNB(V: Boolean);
procedure SetANF(V: Boolean); procedure SetANF(V: Boolean);
procedure SetTransmitting(V: Boolean); procedure SetTransmitting(V: Boolean);
function GetCWSpeed: Integer;
procedure SetCWSpeed(V: Integer);
function GetCWKeyerMode: Integer;
procedure SetCWKeyerMode(V: Integer);
procedure SendCWText(const S: string);
function GetCWTextBusy: Boolean;
procedure SyncCWSpeed;
procedure SyncCWKeyerMode;
procedure SyncCWText;
procedure DoBandUp; procedure DoBandUp;
procedure DoBandDown; procedure DoBandDown;
procedure DoTuneUp; procedure DoTuneUp;
@@ -217,6 +227,13 @@ begin
Ctx.SetAGCTop := @SetAGCTop; Ctx.SetAGCTop := @SetAGCTop;
Ctx.GetCTun := @GetCTun; Ctx.GetCTun := @GetCTun;
Ctx.SetCTun := @SetCTun; Ctx.SetCTun := @SetCTun;
// Телеграф: скорость/режим кейера и программная передача текста (KY/ZZKY).
Ctx.GetCWSpeed := @GetCWSpeed;
Ctx.SetCWSpeed := @SetCWSpeed;
Ctx.GetCWKeyerMode := @GetCWKeyerMode;
Ctx.SetCWKeyerMode := @SetCWKeyerMode;
Ctx.SendCWText := @SendCWText;
Ctx.CWTextBusy := @GetCWTextBusy;
Ctx.OnPanelVfoStep := @PanelVfoStep; Ctx.OnPanelVfoStep := @PanelVfoStep;
Ctx.OnPanelEncoder := @PanelEncoder; Ctx.OnPanelEncoder := @PanelEncoder;
Ctx.OnPanelButton := @PanelButton; Ctx.OnPanelButton := @PanelButton;
@@ -418,6 +435,47 @@ begin FController.SetANF(FSyncBool); end;
procedure TCATAdapter.SetTransmitting(V: Boolean); procedure TCATAdapter.SetTransmitting(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncMOX); end; begin FSyncBool := V; FController.Invoke(@SyncMOX); end;
// ---- Телеграф (KS/KY/ZZKM/ZZKS/ZZKY) --------------------------------------
// Правки настроек и запуск передачи маршалим в поток контроллера, как всё
// остальное в адаптере: SetCWSettings трогает движок и пишет конфиг.
function TCATAdapter.GetCWSpeed: Integer;
begin Result := FController.CWSettings.Speed; end;
procedure TCATAdapter.SetCWSpeed(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWSpeed); end;
procedure TCATAdapter.SyncCWSpeed;
var C: TCWSettings;
begin
C := FController.CWSettings;
C.Speed := FSyncInt;
FController.SetCWSettings(C);
end;
function TCATAdapter.GetCWKeyerMode: Integer;
begin Result := FController.CWSettings.KeyerMode; end;
procedure TCATAdapter.SetCWKeyerMode(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCWKeyerMode); end;
procedure TCATAdapter.SyncCWKeyerMode;
var C: TCWSettings;
begin
C := FController.CWSettings;
C.KeyerMode := FSyncInt;
FController.SetCWSettings(C);
end;
procedure TCATAdapter.SendCWText(const S: string);
begin FSyncStr := S; FController.Invoke(@SyncCWText); end;
procedure TCATAdapter.SyncCWText;
begin FController.CWXSend(FSyncStr); end;
function TCATAdapter.GetCWTextBusy: Boolean;
begin Result := FController.CWXBusy; end;
procedure TCATAdapter.SyncMOX; procedure TCATAdapter.SyncMOX;
begin FController.SetMOX(FSyncBool); end; begin FController.SetMOX(FSyncBool); end;
+54 -12
View File
@@ -112,6 +112,14 @@ type
GetFilterHigh: TCATGetIntFunc; GetFilterHigh: TCATGetIntFunc;
SetFilterLow: TCATSetIntProc; SetFilterLow: TCATSetIntProc;
SetFilterHigh: TCATSetIntProc; SetFilterHigh: TCATSetIntProc;
// Телеграф (KS/KY/ZZKS/ZZKY/ZZKM). Скорость — WPM 5..60; SendCWText ставит
// текст в очередь программной передачи, CWTextBusy = «очередь занята».
GetCWSpeed: TCATGetIntFunc;
SetCWSpeed: TCATSetIntProc;
GetCWKeyerMode: TCATGetIntFunc; // 0=прямой ключ, 1=iambic A, 2=iambic B
SetCWKeyerMode: TCATSetIntProc;
SendCWText: procedure(const S: string) of object;
CWTextBusy: TCATGetBoolFunc;
// Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS) // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
@@ -136,6 +144,7 @@ type
function SafeGetVfoA: Double; function SafeGetVfoA: Double;
function SafeGetVfoB: Double; function SafeGetVfoB: Double;
function SafeGetMode: Integer; function SafeGetMode: Integer;
function SafeGetCWSpeed: Integer;
function SafeGetVolume: Integer; function SafeGetVolume: Integer;
function SafeGetDriveLevel: Integer; function SafeGetDriveLevel: Integer;
function SafeGetFilterIdx: Integer; function SafeGetFilterIdx: Integer;
@@ -812,21 +821,43 @@ begin
Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000'; Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000';
end; end;
function TCATEngine.SafeGetCWSpeed: Integer;
begin
if Assigned(FCtx.GetCWSpeed) then Result := FCtx.GetCWSpeed else Result := 20;
end;
function TCATEngine.CmdKS(const s: string): string; function TCATEngine.CmdKS(const s: string): string;
// KS: CW keying speed (3 digits, WPM) — STUB: no CW keyer // KS: CW keying speed (3 digits, WPM).
var v: Integer;
begin begin
if Length(s) = 3 then if Length(s) = 3 then
Result := '' // accepted but ignored begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWSpeed) then FCtx.SetCWSpeed(v);
Result := '';
end
else if Length(s) = 0 then else if Length(s) = 0 then
Result := '025' // default 25 WPM Result := Format('%.3d', [SafeGetCWSpeed])
else else
Result := CAT_ERROR; Result := CAT_ERROR;
end; end;
function TCATEngine.CmdKY(const s: string): string; function TCATEngine.CmdKY(const s: string): string;
// KY: send CW text — STUB: no CW keyer implemented // KY: передать текст телеграфом. Kenwood-семантика: первый символ — пробел-
// заполнитель признака «не занято», далее сам текст; пустой аргумент = опрос
// занятости ('1' — очередь занята, логгер ждёт).
var Txt: string;
begin begin
Result := '0'; // queue not full if Length(s) = 0 then
begin
if Assigned(FCtx.CWTextBusy) and FCtx.CWTextBusy then Result := '1'
else Result := '0';
Exit;
end;
Txt := s;
if (Length(Txt) > 0) and (Txt[1] = ' ') then Delete(Txt, 1, 1);
if Assigned(FCtx.SendCWText) then FCtx.SendCWText(Txt);
Result := '';
end; end;
function TCATEngine.CmdMD(const s: string): string; function TCATEngine.CmdMD(const s: string): string;
@@ -1754,23 +1785,34 @@ begin
end; end;
function TCATEngine.ZZKM(const s: string): string; function TCATEngine.ZZKM(const s: string): string;
// ZZKM: CW key mode — STUB: no CW keyer // ZZKM: режим кейера (0=прямой ключ, 1=iambic A, 2=iambic B).
var v: Integer;
begin begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' if Length(s) = 1 then
begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if (v < 0) or (v > 2) then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWKeyerMode) then FCtx.SetCWKeyerMode(v);
Result := '';
end
else if Length(s) = 0 then
begin
if Assigned(FCtx.GetCWKeyerMode) then Result := IntToStr(FCtx.GetCWKeyerMode)
else Result := '0';
end
else Result := CAT_ERROR; else Result := CAT_ERROR;
end; end;
function TCATEngine.ZZKS(const s: string): string; function TCATEngine.ZZKS(const s: string): string;
// ZZKS: CW speed (WPM) — STUB: no CW keyer // ZZKS: скорость кейера, WPM.
begin begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025' Result := CmdKS(s);
else Result := CAT_ERROR;
end; end;
function TCATEngine.ZZKY(const s: string): string; function TCATEngine.ZZKY(const s: string): string;
// ZZKY: CW text to send — STUB: no CW keyer implemented // ZZKY: передать текст телеграфом (то же, что KY).
begin begin
Result := '0'; // buffer not full Result := CmdKY(s);
end; end;
function TCATEngine.ZZMA(const s: string): string; function TCATEngine.ZZMA(const s: string): string;
+462
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+231
View File
@@ -0,0 +1,231 @@
unit CWMessagesForm;
{ TCWMessagesForm — память телеграфных сообщений (CW_MSG_COUNT ячеек).
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
так что окно висит рядом — ровно как окно CWX у Thetis. Те же ячейки
привязаны к F1..F8 в главном окне.
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
нет), через тот же OnChanged, что и вкладка настроек. }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, StdCtrls, ExtCtrls,
FlatButton, FlatEdit, AppTheme, Settings, DpiUtils;
type
TCWSendEvent = procedure(const Text: string) of object;
TCWMsgsChanged = procedure(const C: TCWSettings) of object;
TCWMessagesForm = class(TForm)
private
FCW: TCWSettings;
FTheme: TAppTheme;
FUpdating: Boolean;
FLbls: array[0..CW_MSG_COUNT-1] of TLabel;
FEdits: array[0..CW_MSG_COUNT-1] of TFlatEdit;
FSendBtns: array[0..CW_MSG_COUNT-1] of TFlatButton;
FBtnStop: TFlatButton;
FBtnClose: TFlatButton;
FHint: TLabel;
FOnSend: TCWSendEvent;
FOnAbort: TNotifyEvent;
FOnChanged: TCWMsgsChanged;
procedure BuildUI;
procedure EditChange(Sender: TObject);
procedure SendClick(Sender: TObject);
procedure StopClick(Sender: TObject);
procedure CloseClick(Sender: TObject);
procedure StyleBtn(B: TFlatButton);
public
constructor Create(AOwner: TComponent); override;
procedure LoadFrom(const C: TCWSettings);
procedure ApplyTheme(const T: TAppTheme);
// Отправить ячейку по индексу (F1..F8 из главного окна).
procedure SendSlot(Idx: Integer);
property OnSend: TCWSendEvent read FOnSend write FOnSend;
property OnAbort: TNotifyEvent read FOnAbort write FOnAbort;
property OnChanged: TCWMsgsChanged read FOnChanged write FOnChanged;
end;
implementation
const
{$IFDEF WINDOWS}
UI_FONT = 'Segoe UI';
{$ELSE}
UI_FONT = 'Sans';
{$ENDIF}
FORM_W = 470;
PAD = 14;
ROW_H = 26;
ROW_GAP = 6;
LBL_W = 30;
BTN_W = 66;
constructor TCWMessagesForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
FTheme := DarkTheme;
FUpdating := False;
TSettingsManager.DefaultCW(FCW);
Scaled := False;
Caption := 'CW Messages';
BorderStyle := bsSingle;
if AOwner is TCustomForm then Position := poOwnerFormCenter
else Position := poScreenCenter;
Width := DpiScale(FORM_W);
// Ряды + строка кнопок + подсказка в ДВЕ строки под ними (в один ряд с
// кнопками она налезала на Close).
Height := DpiScale(2 * PAD + CW_MSG_COUNT * (ROW_H + ROW_GAP)
+ ROW_H + 8 + 34);
BuildUI;
ApplyTheme(DarkTheme);
end;
procedure TCWMessagesForm.BuildUI;
var
i, Y: Integer;
begin
Y := PAD;
for i := 0 to CW_MSG_COUNT - 1 do
begin
FLbls[i] := TLabel.Create(Self);
FLbls[i].Parent := Self;
FLbls[i].Caption := 'F' + IntToStr(i + 1);
FLbls[i].Font.Name := UI_FONT;
FLbls[i].Font.Size := 9;
FLbls[i].SetBounds(DpiScale(PAD), DpiScale(Y + 5),
DpiScale(LBL_W), DpiScale(18));
FEdits[i] := TFlatEdit.Create(Self);
FEdits[i].Parent := Self;
FEdits[i].Font.Name := UI_FONT;
FEdits[i].Font.Size := 9;
FEdits[i].MaxLength := 63;
FEdits[i].Tag := i;
FEdits[i].OnChange := @EditChange;
FEdits[i].SetBounds(DpiScale(PAD + LBL_W), DpiScale(Y),
DpiScale(FORM_W - 2 * PAD - LBL_W - BTN_W - 8),
DpiScale(ROW_H - 2));
FSendBtns[i] := TFlatButton.Create(Self);
FSendBtns[i].Parent := Self;
FSendBtns[i].Caption := 'Send';
FSendBtns[i].Tag := i;
FSendBtns[i].OnClick := @SendClick;
FSendBtns[i].SetBounds(DpiScale(FORM_W - PAD - BTN_W), DpiScale(Y),
DpiScale(BTN_W), DpiScale(ROW_H - 2));
Inc(Y, ROW_H + ROW_GAP);
end;
FBtnStop := TFlatButton.Create(Self);
FBtnStop.Parent := Self;
FBtnStop.Caption := 'Stop';
FBtnStop.OnClick := @StopClick;
FBtnStop.SetBounds(DpiScale(PAD), DpiScale(Y), DpiScale(BTN_W), DpiScale(ROW_H));
FBtnClose := TFlatButton.Create(Self);
FBtnClose.Parent := Self;
FBtnClose.Caption := 'Close';
FBtnClose.OnClick := @CloseClick;
FBtnClose.SetBounds(DpiScale(FORM_W - PAD - BTN_W), DpiScale(Y),
DpiScale(BTN_W), DpiScale(ROW_H));
FHint := TLabel.Create(Self);
FHint.Parent := Self;
FHint.Font.Name := UI_FONT;
FHint.Font.Size := 8;
FHint.AutoSize := False;
FHint.WordWrap := True;
FHint.Caption := 'F1..F8 in the main window send the same slots. '
+ 'Touching the paddle or dropping MOX stops sending.';
FHint.SetBounds(DpiScale(PAD), DpiScale(Y + ROW_H + 8),
DpiScale(FORM_W - 2 * PAD), DpiScale(34));
end;
procedure TCWMessagesForm.StyleBtn(B: TFlatButton);
begin
if B = nil then Exit;
B.ClrNorm := FTheme.BtnNorm;
B.ClrBorder := FTheme.BtnBorderNorm;
B.ClrHot := FTheme.BtnHot;
B.ClrActive := FTheme.BtnActive;
B.ClrText := FTheme.BtnText;
B.ClrTextAct := FTheme.BtnTextActive;
B.Font.Name := UI_FONT;
B.Font.Size := 9;
B.Invalidate;
end;
procedure TCWMessagesForm.ApplyTheme(const T: TAppTheme);
var i: Integer;
begin
FTheme := T;
Color := T.BG;
for i := 0 to CW_MSG_COUNT - 1 do
begin
FLbls[i].Font.Color := T.TextDim;
FEdits[i].SetAppTheme(T);
StyleBtn(FSendBtns[i]);
end;
StyleBtn(FBtnStop);
StyleBtn(FBtnClose);
FHint.Font.Color := T.TextDim;
Invalidate;
end;
procedure TCWMessagesForm.LoadFrom(const C: TCWSettings);
var i: Integer;
begin
FUpdating := True;
try
FCW := C;
for i := 0 to CW_MSG_COUNT - 1 do
FEdits[i].Text := C.Messages[i];
finally
FUpdating := False;
end;
end;
procedure TCWMessagesForm.EditChange(Sender: TObject);
var i: Integer;
begin
if FUpdating then Exit;
i := TFlatEdit(Sender).Tag;
if (i < 0) or (i >= CW_MSG_COUNT) then Exit;
FCW.Messages[i] := Copy(TFlatEdit(Sender).Text, 1, 63);
if Assigned(FOnChanged) then FOnChanged(FCW);
end;
procedure TCWMessagesForm.SendSlot(Idx: Integer);
begin
if (Idx < 0) or (Idx >= CW_MSG_COUNT) then Exit;
if FCW.Messages[Idx] = '' then Exit;
if Assigned(FOnSend) then FOnSend(FCW.Messages[Idx]);
end;
procedure TCWMessagesForm.SendClick(Sender: TObject);
begin
SendSlot(TFlatButton(Sender).Tag);
end;
procedure TCWMessagesForm.StopClick(Sender: TObject);
begin
if Assigned(FOnAbort) then FOnAbort(Self);
end;
procedure TCWMessagesForm.CloseClick(Sender: TObject);
begin
Hide;
end;
end.
+382
View File
@@ -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.
+678
View File
@@ -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.
+4
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -43,7 +43,7 @@ uses
WinFirewall, WinFirewall,
BoardUtils, WisdomBuilder, UISync, BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater, FlatEdit, FMRepeater,
ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder, ChannelStore, ChannelsForm, CWMessagesForm, 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
View File
@@ -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);
+10
View File
@@ -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);
+22
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+184
View File
@@ -28,6 +28,7 @@ const
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090) CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
CW_MSG_COUNT = 8; // ячеек памяти телеграфных сообщений (F1..F8)
TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
@@ -306,6 +307,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;
+281
View File
@@ -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
View File
@@ -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
+20
View File
@@ -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"/>
+3
View File
@@ -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