Files
ewsdr/CWMessagesForm.pas
ew8bakandClaude Opus 5 53329e82e8 feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг
нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в
эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда
был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали.

Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются
относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают
три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых
вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица
фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска
на спектре и маркер VFO верны без правок в UI, а слайсы получают свой
сдвиг по своему режиму.

Правила эфира:
- голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только
  PTT; несущую даёт кейер, бит CWX или тон TUN;
- байт 5 собирается из настроек и перепосылается на каждой смене режима и
  TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на
  замыкание ключа посреди SSB;
- TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO;
- приёмник на передаче в CW не глушится, иначе умолкает программный
  сайдтон и эфир между посылками.

Программная передача текста (CWMorse): поток с абсолютными дедлайнами
дёргает бит CWX в High Priority, элементы рисует прошивка. Касание
манипулятора или снятие MOX обрывают передачу. Память сообщений — окно
CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY.

Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS.

Программный сайдтон точен для передачи текста и прямого ключа; при иамбике
таймингом владеет FPGA, поэтому там верен только аппаратный тон.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:20:56 +03:00

232 lines
7.1 KiB
ObjectPascal

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.