mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(cw): терминал телеграфа — набор с клавиатуры и декодер приёма
Окно (ПКМ по CWL/CWU, F9, кнопка в настройках CW): одна лента, где принятое декодером и СВОЯ передача идут вперемешку — своё акцентным цветом. Разделять их нельзя: в QSK они перемежаются посреди фразы. Под лентой строка набора. Печать уходит в эфир ПОСИМВОЛЬНО, а не по Enter: строка показывает ровно то, что ещё не передано (очередь генератора), знаки уходят из неё по мере отправки, Backspace стирает с хвоста очереди — то, что уже звучит, вернуть нельзя. Ctrl работает манипулятором (левый точка, правый тире; у кейера прошивки лепестков нет — там это прямой ключ через бит CWX), по умолчанию выключено, чтобы Ctrl+C не уводил в эфир. Отпускание ключа ловится и на потере фокуса: иначе уход из окна с зажатым Ctrl оставил бы несущую в эфире навсегда. CWDecoder.pas: аудио → текст. Отвод берётся до громкости и мьюта — там нет программного сайдтона, зато уже отработал узкий CW-фильтр, лучшего предетектора не найти. Гёрцель гребёнкой из пяти бинов вокруг pitch (заодно показывает расстройку) → огибающая → адаптивный порог с гистерезисом → длительности → адаптивная точка → обратная таблица Морзе. Длительности живут в шагах анализа, а не в показаниях часов: разбор идёт пачками с таймера, и привязка ко времени вызова ломала бы тайминг на любой загрузке. Вылезло на тестах и учтено: пик обязан клампиться не ниже пола шума (иначе на старте порог уходит НИЖЕ шума и первым «знаком» читается собственный шум); порог дребезга берётся от текущей точки, фиксированный либо пропускает щелчки на медленной передаче, либо ест посылки на быстрой; длина посылки меряется за вычетом подтверждения дребезга, иначе скорость занижалась на 15%; расстройка запоминается только на полной амплитуде посылки, иначе индикатор пляшет. Граница честная: при вдвое неверной подсказке скорости теряется первое слово — пока не услышана настоящая точка, длина элемента неизвестна. Лента расшифровки продублирована строкой под спектром (пан 0), эхо передачи и очередь набора появились у обоих отправителей — и у локального генератора, и у кейера прошивки. ★TFlatEdit получил публичный CaretPos: он вставляет знак сам в UTF8KeyPress и гасит клавишу, поэтому OnKeyPress контрола не вызывается вовсе — из-за этого набранное «исчезало», а в эфир не уходило. Ввод перенесён на уровень формы. Проверено оффлайн: 12/20/40 WPM, расстройка, шум, слабый сигнал, цифры и знаки; плюс сквозной прогон против шести станций CW-стенда в hpsdrsim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,14 @@ type
|
||||
FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0)
|
||||
FHeaderVisible: Boolean;
|
||||
FShowZoomRow: Boolean; // False у панов N>0 (пан-зум — этап 3.3+)
|
||||
// Бегущая лента расшифровки телеграфа под спектром (только пан 0). Своя
|
||||
// строка, а не окно: смотреть в эфир и читать расшифровку удобнее в одном
|
||||
// месте, а полноценный терминал открывается отдельно.
|
||||
FCWStripOn: Boolean;
|
||||
FCWStripText: string;
|
||||
FPbCWStrip: TPaintBox;
|
||||
FThemeBG: TColor;
|
||||
FThemeText: TColor;
|
||||
FOnPanClose: TNotifyEvent;
|
||||
FOnAddPan: TNotifyEvent;
|
||||
FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума
|
||||
@@ -152,6 +160,7 @@ type
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanZoomDblClick(Sender: TObject);
|
||||
procedure PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean);
|
||||
procedure CWStripPaint(Sender: TObject);
|
||||
public
|
||||
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
|
||||
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
|
||||
@@ -245,6 +254,9 @@ type
|
||||
property BtnAddPan: TFlatButton read FBtnAddPan;
|
||||
// Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+).
|
||||
property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow;
|
||||
// Лента расшифровки телеграфа: включить/выключить и обновить текст.
|
||||
// True — состояние видимости изменилось, хозяину нужно переразложить пан.
|
||||
function SetCWStrip(AOn: Boolean; const AText: string): Boolean;
|
||||
// «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин.
|
||||
property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted;
|
||||
// «▦/▤» тумблер грид-раскладки (пан 0, виден при >=2 доп. панах).
|
||||
@@ -432,6 +444,12 @@ begin
|
||||
FPbPanZoom.OnMouseUp := PanZoomMouseUp;
|
||||
FPbPanZoom.OnDblClick := PanZoomDblClick;
|
||||
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);
|
||||
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
||||
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
||||
@@ -501,6 +519,7 @@ begin
|
||||
FSplitter.Parent := NewParent;
|
||||
FPbWaterfall.Parent := NewParent;
|
||||
FPbPanZoom.Parent := NewParent;
|
||||
if FPbCWStrip <> nil then FPbCWStrip.Parent := NewParent;
|
||||
FBtnZoomOut.Parent := NewParent;
|
||||
FBtnZoomDef.Parent := NewParent;
|
||||
FBtnZoomIn.Parent := NewParent;
|
||||
@@ -668,6 +687,44 @@ begin
|
||||
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;
|
||||
AVisible: Boolean);
|
||||
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd, WantGrid: Boolean;
|
||||
@@ -725,6 +782,8 @@ begin
|
||||
if FHeaderVisible then Inc(Result, HEADER_H);
|
||||
if (AShowSpectrum or AShowWaterfall) and FShowZoomRow then
|
||||
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 AShowWaterfall then
|
||||
begin
|
||||
@@ -746,6 +805,7 @@ var
|
||||
RULER_H: Integer;
|
||||
PANZOOM_H: Integer;
|
||||
HEADER_H: Integer;
|
||||
CWSTRIP_H: Integer;
|
||||
ShowPZ: Boolean;
|
||||
EffRatio: Double;
|
||||
begin
|
||||
@@ -784,6 +844,17 @@ begin
|
||||
ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow;
|
||||
PositionPanZoomBar(X0, RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||
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;
|
||||
if FHeaderVisible then Inc(TopOff, HEADER_H);
|
||||
|
||||
@@ -1533,6 +1604,10 @@ end;
|
||||
procedure TPanafallPanel.SetFlagsTheme(const T: TAppTheme);
|
||||
var i: Integer;
|
||||
begin
|
||||
// Лента расшифровки рисуется своими руками — цвета держим здесь же.
|
||||
FThemeBG := T.Panel;
|
||||
FThemeText := T.Text;
|
||||
if (FPbCWStrip <> nil) and FPbCWStrip.Visible then FPbCWStrip.Invalidate;
|
||||
if Assigned(FMainFlag) then FMainFlag.SetTheme(T);
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T);
|
||||
|
||||
Reference in New Issue
Block a user