mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали. Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска на спектре и маркер VFO верны без правок в UI, а слайсы получают свой сдвиг по своему режиму. Правила эфира: - голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только PTT; несущую даёт кейер, бит CWX или тон TUN; - байт 5 собирается из настроек и перепосылается на каждой смене режима и TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на замыкание ключа посреди SSB; - TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO; - приёмник на передаче в CW не глушится, иначе умолкает программный сайдтон и эфир между посылками. Программная передача текста (CWMorse): поток с абсолютными дедлайнами дёргает бит CWX в High Priority, элементы рисует прошивка. Касание манипулятора или снятие MOX обрывают передачу. Память сообщений — окно CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY. Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS. Программный сайдтон точен для передачи текста и прямого ключа; при иамбике таймингом владеет FPGA, поэтому там верен только аппаратный тон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user