mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
298 lines
9.4 KiB
ObjectPascal
298 lines
9.4 KiB
ObjectPascal
unit CWMorse;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Программная передача телеграфа: текст → точки/тире → ключ.
|
|
//
|
|
// Кто держит тайминги. У openHPSDR P2 есть ДВА пути в эфир:
|
|
// * манипулятор в разъёме трансивера — элементы формирует кейер в FPGA, PC
|
|
// только отдаёт ему скорость/вес (DUC Specific байт 5..10). Джиттера нет;
|
|
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
|
|
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
|
|
// нажать и отпустить, решает PC. Именно этот путь используется для
|
|
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs).
|
|
//
|
|
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
|
|
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
|
|
// каждого сна накапливалась бы и знак «плыл» бы по длине.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, SyncObjs;
|
|
|
|
type
|
|
// Вызывается из потока передачи на каждом фронте ключа.
|
|
TCWKeyEvent = procedure(Down: Boolean) of object;
|
|
|
|
TCWSender = class(TThread)
|
|
private
|
|
FKeyEvent: TCWKeyEvent;
|
|
FLock: TCriticalSection;
|
|
FWake: TEvent;
|
|
FPending: string; // очередь текста (под FLock)
|
|
FAbortReq: Boolean; // «бросить и отпустить ключ» (под FLock)
|
|
FBusy: Boolean; // идёт передача (под FLock)
|
|
FWPM: Integer; // под FLock
|
|
FWeight: Integer; // 33..66, 50 = симметрично (под FLock)
|
|
function TakeText: string;
|
|
function Aborted: Boolean;
|
|
procedure Key(Down: Boolean);
|
|
// Выдержать Ms миллисекунд, просыпаясь дробно ради отзывчивости на abort.
|
|
// Возвращает False, если передачу оборвали.
|
|
function Hold(Deadline: QWord): Boolean;
|
|
procedure SendChar(Ch: Char; var Deadline: QWord);
|
|
protected
|
|
procedure Execute; override;
|
|
public
|
|
constructor Create(AKeyEvent: TCWKeyEvent);
|
|
destructor Destroy; override;
|
|
procedure Enqueue(const S: string);
|
|
procedure AbortSending;
|
|
procedure SetSpeed(WPM, Weight: Integer);
|
|
function Busy: Boolean;
|
|
end;
|
|
|
|
// Код знака: строка из '.' и '-'. Пустая — знака нет в таблице (пропускаем,
|
|
// иначе опечатка в макросе превратилась бы в мусор в эфире).
|
|
function MorseCode(Ch: Char): string;
|
|
|
|
implementation
|
|
|
|
const
|
|
// Точка при 1 WPM = 1200 мс (стандарт PARIS).
|
|
DIT_MS_AT_1WPM = 1200;
|
|
|
|
function MorseCode(Ch: Char): string;
|
|
begin
|
|
case UpCase(Ch) of
|
|
'A': Result := '.-'; 'B': Result := '-...'; 'C': Result := '-.-.';
|
|
'D': Result := '-..'; 'E': Result := '.'; 'F': Result := '..-.';
|
|
'G': Result := '--.'; 'H': Result := '....'; 'I': Result := '..';
|
|
'J': Result := '.---'; 'K': Result := '-.-'; 'L': Result := '.-..';
|
|
'M': Result := '--'; 'N': Result := '-.'; 'O': Result := '---';
|
|
'P': Result := '.--.'; 'Q': Result := '--.-'; 'R': Result := '.-.';
|
|
'S': Result := '...'; 'T': Result := '-'; 'U': Result := '..-';
|
|
'V': Result := '...-'; 'W': Result := '.--'; 'X': Result := '-..-';
|
|
'Y': Result := '-.--'; 'Z': Result := '--..';
|
|
'0': Result := '-----'; '1': Result := '.----'; '2': Result := '..---';
|
|
'3': Result := '...--'; '4': Result := '....-'; '5': Result := '.....';
|
|
'6': Result := '-....'; '7': Result := '--...'; '8': Result := '---..';
|
|
'9': Result := '----.';
|
|
'.': Result := '.-.-.-'; ',': Result := '--..--'; '?': Result := '..--..';
|
|
'/': Result := '-..-.'; '=': Result := '-...-'; '+': Result := '.-.-.';
|
|
'-': Result := '-....-'; '(': Result := '-.--.'; ')': Result := '-.--.-';
|
|
':': Result := '---...'; '''':Result := '.----.'; '"': Result := '.-..-.';
|
|
'@': Result := '.--.-.'; '!': Result := '-.-.--'; ';': Result := '-.-.-.';
|
|
'$': Result := '...-..-';
|
|
else
|
|
Result := '';
|
|
end;
|
|
end;
|
|
|
|
constructor TCWSender.Create(AKeyEvent: TCWKeyEvent);
|
|
begin
|
|
FKeyEvent := AKeyEvent;
|
|
FLock := TCriticalSection.Create;
|
|
FWake := TEvent.Create(nil, False, False, '');
|
|
FWPM := 20;
|
|
FWeight := 50;
|
|
FreeOnTerminate := False;
|
|
inherited Create(False);
|
|
end;
|
|
|
|
destructor TCWSender.Destroy;
|
|
begin
|
|
Terminate;
|
|
FWake.SetEvent;
|
|
WaitFor;
|
|
FWake.Free;
|
|
FLock.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TCWSender.Enqueue(const S: string);
|
|
begin
|
|
if S = '' then Exit;
|
|
FLock.Enter;
|
|
try
|
|
FAbortReq := False;
|
|
FPending := FPending + S;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
FWake.SetEvent;
|
|
end;
|
|
|
|
procedure TCWSender.AbortSending;
|
|
begin
|
|
FLock.Enter;
|
|
try
|
|
FPending := '';
|
|
FAbortReq := True;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
FWake.SetEvent;
|
|
end;
|
|
|
|
procedure TCWSender.SetSpeed(WPM, Weight: Integer);
|
|
begin
|
|
if WPM < 5 then WPM := 5;
|
|
if WPM > 60 then WPM := 60;
|
|
if Weight < 33 then Weight := 33;
|
|
if Weight > 66 then Weight := 66;
|
|
FLock.Enter;
|
|
try
|
|
FWPM := WPM;
|
|
FWeight := Weight;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
|
|
function TCWSender.Busy: Boolean;
|
|
begin
|
|
FLock.Enter;
|
|
try
|
|
Result := FBusy or (FPending <> '');
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
|
|
function TCWSender.TakeText: string;
|
|
begin
|
|
FLock.Enter;
|
|
try
|
|
Result := FPending;
|
|
FPending := '';
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
|
|
function TCWSender.Aborted: Boolean;
|
|
begin
|
|
FLock.Enter;
|
|
try
|
|
Result := FAbortReq;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
|
|
procedure TCWSender.Key(Down: Boolean);
|
|
begin
|
|
if Assigned(FKeyEvent) then FKeyEvent(Down);
|
|
end;
|
|
|
|
function TCWSender.Hold(Deadline: QWord): Boolean;
|
|
var Now_: QWord; Slice: Integer;
|
|
begin
|
|
Result := True;
|
|
repeat
|
|
if Terminated or Aborted then Exit(False);
|
|
Now_ := GetTickCount64;
|
|
if Now_ >= Deadline then Exit(True);
|
|
// Дробим сон: обрыв передачи не должен ждать конца длинного тире.
|
|
Slice := Integer(Deadline - Now_);
|
|
if Slice > 5 then Slice := 5;
|
|
Sleep(Slice);
|
|
until False;
|
|
end;
|
|
|
|
procedure TCWSender.SendChar(Ch: Char; var Deadline: QWord);
|
|
// Deadline — момент, к которому знак должен закончиться; ведём его вперёд,
|
|
// чтобы длительности не «уползали» на ошибке каждого Sleep.
|
|
var
|
|
Code: string;
|
|
i, Dit, Mark, Gap, WPM, Weight: Integer;
|
|
begin
|
|
FLock.Enter;
|
|
try
|
|
WPM := FWPM;
|
|
Weight := FWeight;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
Dit := DIT_MS_AT_1WPM div WPM;
|
|
// Вес: 50 = точка равна паузе. Выше — посылки длиннее за счёт пауз внутри
|
|
// знака (межзнаковые интервалы остаются стандартными, иначе «плывёт» темп).
|
|
Mark := (Dit * Weight) div 50;
|
|
Gap := (Dit * (100 - Weight)) div 50;
|
|
if Mark < 1 then Mark := 1;
|
|
if Gap < 1 then Gap := 1;
|
|
|
|
if Ch = ' ' then
|
|
begin
|
|
// Межсловный интервал = 7 точек; 3 из них уже отданы после прошлого знака.
|
|
Inc(Deadline, 4 * Dit);
|
|
Hold(Deadline);
|
|
Exit;
|
|
end;
|
|
|
|
Code := MorseCode(Ch);
|
|
if Code = '' then Exit;
|
|
for i := 1 to Length(Code) do
|
|
begin
|
|
Key(True);
|
|
if Code[i] = '-' then Inc(Deadline, 3 * Mark) else Inc(Deadline, Mark);
|
|
if not Hold(Deadline) then begin Key(False); Exit; end;
|
|
Key(False);
|
|
Inc(Deadline, Gap); // пауза между элементами знака
|
|
if not Hold(Deadline) then Exit;
|
|
end;
|
|
Inc(Deadline, 2 * Dit); // добор до 3 точек между знаками
|
|
Hold(Deadline);
|
|
end;
|
|
|
|
procedure TCWSender.Execute;
|
|
var
|
|
Text: string;
|
|
i: Integer;
|
|
Deadline: QWord;
|
|
begin
|
|
while not Terminated do
|
|
begin
|
|
FWake.WaitFor(200);
|
|
if Terminated then Break;
|
|
Text := TakeText;
|
|
if Text = '' then Continue;
|
|
|
|
FLock.Enter;
|
|
try
|
|
FBusy := True;
|
|
FAbortReq := False;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
try
|
|
Deadline := GetTickCount64;
|
|
i := 1;
|
|
while i <= Length(Text) do
|
|
begin
|
|
if Terminated or Aborted then Break;
|
|
SendChar(Text[i], Deadline);
|
|
// Дописанное в очередь по ходу передачи подхватываем без паузы —
|
|
// иначе поток допередал бы «старый» текст и уснул на 200 мс.
|
|
if i = Length(Text) then Text := Text + TakeText;
|
|
Inc(i);
|
|
end;
|
|
finally
|
|
Key(False); // ключ всегда отпущен, что бы ни случилось
|
|
FLock.Enter;
|
|
try
|
|
FBusy := False;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
end;
|
|
Key(False);
|
|
end;
|
|
|
|
end.
|