chore: refine project documentation

This commit is contained in:
2026-08-25 22:44:36 +03:00
parent 8237f03930
commit 98d90e1e5a
45 changed files with 457 additions and 516 deletions
+15 -17
View File
@@ -27,11 +27,9 @@ unit AndromedaMap;
«индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
контроллера — этим занимается AndromedaPanel.pas.
Соответствие протоколу Thetis:
• действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions;
• номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis
(MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты
12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch).
Таблицы соответствуют нумерации команд аппаратной панели. Для LED-индикаторов
используются номера 1=MOX, 3=Tune, 7=CTUN и т.д.; свободные слоты 12..17
заняты функциями EWSDR (Split/NB/NR/SNB/ANF/Squelch).
}
{$mode objfpc}{$H+}
@@ -44,18 +42,18 @@ type
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
abCTCSS, abSquelch, abFilterUp, abFilterDown,
abVFOAtoB, // копировать VFO A → B (Thetis eBBVFOAtoB)
abVFOBtoA, // копировать VFO B → A (Thetis eBBVFOBtoA)
abVFOLock, // блокировка перестройки VFO (Thetis eBBVFOLockOnOff)
abStartStop, // старт/стоп SDR (Thetis eBBStartStop)
abShift); // модификатор band-keypad (Thetis eBBShift) — обрабатывается в панели
abVFOAtoB, // копировать VFO A → B
abVFOBtoA, // копировать VFO B → A
abVFOLock, // блокировка перестройки VFO
abStartStop, // старт/стоп SDR
abShift); // модификатор band-keypad — обрабатывается в панели
// Параметр, управляемый поворотным энкодером панели.
TAndEncoder = (
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
// Индикатор (LED) панели в порядке протокольной нумерации.
TAndIndicator = (
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
@@ -67,8 +65,8 @@ const
AND_MAX_ENCODERS = 20;
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
// Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based
// индекс = «Pushbutton Number» Thetis 1. Проверено на реальной панели
// Внутренний индекс 0-based на единицу меньше номера кнопки в протоколе.
// Нумерация проверена на реальной панели
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
abMute, // 0 RX1 Mute
@@ -121,14 +119,14 @@ const
abTune); // 47 TUN
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
// Нумерация = «Encoder Number» Thetis 1 (дефолт MakeNewAndromedaDataset).
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
// Внутренний индекс на единицу меньше номера энкодера в протоколе.
// Энкодер #1 управляет AGC Level, поэтому используется aeAGCLevel.
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
// энкодер «ширина» — в дефолтной раскладке не используется.
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
aeAFGain, // 0 RX1 AF Gain
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
aeAGCLevel, // 1 RX1 AGC Level
aeNone, // 2 RX2 AF — нет второго приёмника
aeNone, // 3 RX2 AGC
aeFilterHigh, // 4 Filter High — верхний край
@@ -142,7 +140,7 @@ const
aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone);
// Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет.
// 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах.
// 1..11 — базовая раскладка панели; 12..17 — функции EWSDR.
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
{aiNone} 0,
{aiMOX} 1,
+1 -1
View File
@@ -157,7 +157,7 @@ begin
end;
procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
// Раскладка зеркалит Thetis: кнопка Shift (0-based 28) — модификатор-защёлка;
// Кнопка Shift (0-based 28) — модификатор-защёлка;
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
+9 -11
View File
@@ -20,11 +20,10 @@
unit AudioInput;
{
PortAudio input — TX microphone capture.
Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas.
PortAudio input — TX microphone capture, parallel to AudioOutput.pas.
Key features:
- Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR)
- Mono input stream, paFloat32, fixed at 48000 Hz
- Pa_Initialize called once in Initialize
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
- Callback (PaInCallback) writes mic samples into the ring buffer
@@ -44,7 +43,7 @@ uses
const
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono), как в piHPSDR
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono)
type
TPaError = LongInt;
@@ -110,7 +109,7 @@ type
FLastError: string;
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
// Ring buffer (mono double), как в piHPSDR
// Ring buffer (mono double)
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
FInPt: Integer; // write pointer (callback writes)
FOutPt: Integer; // read pointer (main thread reads via ReadSample)
@@ -208,8 +207,7 @@ const
PA_CONTINUE = LongInt(0);
// ---------------------------------------------------------------------------
// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер.
// Точно по образцу pa_in_cb из piHPSDR/portaudio.c
// Callback — читает mono float32 из inputBuffer и пишет в кольцевой буфер.
// ---------------------------------------------------------------------------
function PaInCallback(inputBuffer, outputBuffer: Pointer;
@@ -461,7 +459,7 @@ begin
end;
// ---------------------------------------------------------------------------
// Open — открывает input stream, как audio_open_input в piHPSDR
// Open — открывает входной аудиопоток.
// ---------------------------------------------------------------------------
function TAudioInput.Open: Boolean;
@@ -480,7 +478,7 @@ begin
if not Initialize then Exit;
FillChar(InParam, SizeOf(InParam), 0);
InParam.channelCount := 1; // Mono, как в piHPSDR
InParam.channelCount := 1; // Mono input
InParam.device := FDeviceIndex;
InParam.sampleFormat := PA_FLOAT32;
@@ -533,7 +531,7 @@ begin
end;
// ---------------------------------------------------------------------------
// Close — как audio_close_input в piHPSDR
// Close — останавливает и закрывает входной аудиопоток.
// ---------------------------------------------------------------------------
procedure TAudioInput.Close;
@@ -556,7 +554,7 @@ begin
end;
// ---------------------------------------------------------------------------
// ReadSample — как audio_get_next_mic_sample в piHPSDR
// ReadSample — возвращает следующий микрофонный сэмпл из буфера.
// ---------------------------------------------------------------------------
function TAudioInput.ReadSample: Double;
+13 -15
View File
@@ -20,12 +20,12 @@
unit AudioOutput;
{
PortAudio output — реализация по образцу piHPSDR/portaudio.c (DL1YCF)
PortAudio output with a buffered low-latency stereo stream.
Ключевые особенности:
- Pa_Initialize вызывается один раз в Create (до открытия стрима)
- suggestedLatency = 0.0 (минимум устройства)
- Ring buffer из Double (как в оригинале)
- Ring buffer из Double
- Callback читает по одному сэмплу, обновляет outpt внутри цикла
- Low water mark: вставляет тишину + полбуфера silence
- High water mark: удаляет лишние сэмплы
@@ -43,10 +43,10 @@ uses
Classes, SysUtils, Math, SyncObjs, DynLibs;
const
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR
MY_RING_BUFFER_SIZE = 9600; // как piHPSDR
MY_RING_LOW_WATER = 512; // как piHPSDR
MY_RING_HIGH_WATER = 9000; // как piHPSDR
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback
MY_RING_BUFFER_SIZE = 9600;
MY_RING_LOW_WATER = 512;
MY_RING_HIGH_WATER = 9000;
type
TPaError = LongInt;
@@ -119,8 +119,8 @@ type
FPAInited: Boolean; // Pa_Initialize прошла
FLastError: string;
// Ring buffer (interleaved double stereo, как в оригинале)
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double; // как piHPSDR
// Ring buffer (interleaved double stereo)
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double;
FInPt: Integer; // audio_buffer_inpt (write)
FOutPt: Integer; // audio_buffer_outpt (read)
FMutex: TCriticalSection;
@@ -159,7 +159,7 @@ type
// Найти индекс устройства по имени (-1 если не найдено)
function FindDeviceByName(const AName: string): Integer;
// Пишем стерео double сэмплы — как audio_write() в оригинале
// Записывает пару стереосэмплов Double в кольцевой буфер.
procedure WriteDouble(Left, Right: Double);
// Convenience: массив Single
procedure Write(const Left, Right: array of Single; Count: Integer);
@@ -215,13 +215,12 @@ const
PA_CONTINUE = LongInt(0);
PA_NO_DEV = TPaDeviceIndex(-1);
// Callback modeled after pa_out_cb from piHPSDR/portaudio.c
// Callback reads interleaved stereo samples from the ring buffer.
function PaOutCallback(inputBuffer, outputBuffer: Pointer;
framesPerBuffer: LongWord;
timeInfo: Pointer;
statusFlags: LongWord;
userData: Pointer): LongInt; cdecl;
// Точная копия pa_out_cb из piHPSDR/portaudio.c
var
Audio: TAudioOutput;
Out_: PSingle;
@@ -478,7 +477,7 @@ begin
end;
// ---------------------------------------------------------------------------
// Open — точная последовательность как в audio_open_output()
// Open — подготавливает параметры и открывает выходной поток PortAudio.
// ---------------------------------------------------------------------------
function TAudioOutput.Open: Boolean;
@@ -505,7 +504,7 @@ begin
Exit;
end;
// Точно как в оригинале: bzero + suggestedLatency = 0.0
// Обнуляем параметры и запрашиваем минимальную задержку устройства.
FillChar(OutParam, SizeOf(OutParam), 0);
OutParam.channelCount := 2;
OutParam.device := Dev;
@@ -597,8 +596,7 @@ begin
end;
end;
// WriteDouble — modeled after audio_write() from piHPSDR/portaudio.c
// Вызывается per-sample из Write. Мьютекс держится весь цикл в Write.
// Вызывается для каждой пары сэмплов из Write. Мьютекс держится весь цикл.
procedure TAudioOutput.WriteDouble(Left, Right: Double);
var
avail: Integer;
+2 -2
View File
@@ -740,7 +740,7 @@ begin
end;
procedure TCATAdapter.SyncTXEQ;
// Частоты полос команда не несёт (их в формате Thetis попросту нет) — трогаем
// Частоты полос команда не несёт (их в формате CAT попросту нет) — трогаем
// только усиления и число полос, сетка остаётся той, что настроена в UI.
var
T: TTXSettings;
@@ -866,7 +866,7 @@ procedure TCATAdapter.SyncAtten;
begin FController.SetAtten(FSyncInt); end;
function TCATAdapter.GetSupplyVolts: Double;
// Телеметрия платы; -1 = данных ещё нет (движок отдаст ноль, как Thetis).
// Телеметрия платы; -1 означает, что данных ещё нет, и наружу отдаётся ноль.
begin Result := FController.FLastSupplyV; end;
procedure TCATAdapter.DoVfoSwap;
+61 -62
View File
@@ -19,10 +19,10 @@
unit CATEngine;
{
CATEngine.pas CAT command engine (Kenwood + Thetis ZZ* extensions).
CATEngine.pas CAT command engine (Kenwood + extended ZZ* commands).
Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs
реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают
Движок поддерживает базовые команды Kenwood и расширения ZZ*. Команды, для
которых в EWSDR ещё нет функционала, возвращают
заглушку и сопровождены комментарием "STUB: <причина>".
Формат команд:
@@ -160,7 +160,7 @@ type
CWTextBusy: TCATGetBoolFunc;
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
// (ZZCM у Thetis это «monitor DISABLE», инверсия делается в движке).
// ZZCM означает «monitor DISABLE», инверсия делается в движке.
GetCWPitch: TCATGetIntFunc; // Гц
SetCWPitch: TCATSetIntProc;
GetCWBreakIn: TCATGetBoolFunc;
@@ -500,7 +500,7 @@ begin if B then Result := '1' else Result := '0'; end;
function IsParamless(const Pfx: string): Boolean;
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка
// Команды не принимают аргументов; любой хвост у них — ошибка
// формата, а не значение.
const
NoArg: array[0..9] of string = (
@@ -753,7 +753,7 @@ end;
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis —
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi],
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
var v: Integer;
begin
@@ -769,7 +769,7 @@ begin
end;
function TCATEngine.StepIdxToHz(Idx: Integer): Double;
// Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц).
// Шаг настройки по протокольному индексу 0..14, результат в Гц.
const Steps: array[0..14] of Double = (
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
10000, 100000, 250000, 500000, 1000000);
@@ -823,8 +823,8 @@ begin
Result := ParseZZ(ext, sfx);
end else begin
sfx := Copy(s, 3, tpos - 3);
// Команды без параметров: эталон отбраковывает лишний суффикс в парсере,
// по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список
// Команды без параметров должны отбраковывать лишний суффикс в парсере
// ДО вызова команды. Таблицы ширин у нас нет — держим список
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
if pfx = 'AG' then Result := CmdAG(sfx)
@@ -933,7 +933,7 @@ begin
end;
function TCATEngine.CmdCT(const s: string): string;
// CT: включение CTCSS — обёртка над ZZTA, как и в эталоне.
// CT: включение CTCSS — обёртка над ZZTA.
begin
Result := ZZTA(s);
end;
@@ -994,7 +994,7 @@ end;
function TCATEngine.CmdFW(const s: string): string;
// FW: filter width (4 digits) — maps to filter index
// Thetis encodes filter by index into 4-digit string 0000-0009
// Filter index is encoded as a 4-digit string from 0000 to 0009.
var idx: Integer;
begin
if Length(s) = 4 then begin
@@ -1028,7 +1028,7 @@ begin
end;
function TCATEngine.CmdIF: string;
// IF: составной статус — 35 символов ответа (как у Thetis; «37 байт» из
// IF: составной статус — 35 символов ответа («37 байт» из
// прежнего комментария — это вся посылка вместе с префиксом и ';').
var
freq, step, incr, rit, xit, tx, mode, split: string;
@@ -1108,7 +1108,7 @@ end;
function TCATEngine.CmdMG(const s: string): string;
// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
// n/1.43, дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
var v: Integer;
begin
@@ -1159,22 +1159,21 @@ begin
end;
function TCATEngine.CmdOF(const s: string): string;
// OF: сдвиг репитера — обёртка над ZZOT, как и в эталоне.
// OF: сдвиг репитера — обёртка над ZZOT.
begin
Result := ZZOT(s);
end;
function TCATEngine.CmdOS(const s: string): string;
// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне.
// OS: направление сдвига репитера — обёртка над ZZOS.
begin
Result := ZZOS(s);
end;
function TCATEngine.ZZOS(const s: string): string;
// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх,
// ZZOS: направление сдвига репитера. В протоколе 0=симплекс, 1=вверх,
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection),
// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение.
// ★Некорректное значение трактуется как симплекс для совместимости клиентов.
var dir: Integer;
begin
if Length(s) = 1 then begin
@@ -1196,9 +1195,9 @@ begin
end;
function TCATEngine.ZZOT(const s: string): string;
// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью
// знаками после запятой (вставляет точку после третьего разряда) — численно это
// ровно те же герцы, что и у нас. Знак не передаётся, направление в ZZOS.
// ZZOT: величина сдвига репитера, 9 цифр. Поле кодирует МГц с шестью знаками
// после запятой, что численно соответствует целому значению в герцах. Знак не
// передаётся, направление задаётся в ZZOS.
var v: Int64;
begin
if Length(s) = 9 then begin
@@ -1228,7 +1227,7 @@ begin
end;
function TCATEngine.CmdPR(const s: string): string;
// PR: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0');
// PR: речевой компрессор вкл/выкл. Базовая команда доступна только для чтения;
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
// кенвудовские клиенты от него не ломаются.
begin
@@ -1311,7 +1310,7 @@ end;
function TCATEngine.CmdSM(const s: string): string;
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все:
// Совместимые клиенты присылают селектор '0', '1' или '2'; принимаем все:
// второго приёмника нет, отвечаем одним и тем же.
var sm: Integer;
begin
@@ -1401,7 +1400,7 @@ begin
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
else if ext = 'CB' then ans := ZZCB(s)
else if ext = 'CD' then ans := ZZCD(s)
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа (UI Thetis)
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа в UI
else if ext = 'CI' then ans := ZZCI(s)
else if ext = 'CL' then ans := ZZCL(s)
else if ext = 'CM' then ans := ZZCM(s)
@@ -1443,10 +1442,10 @@ begin
else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет
else if ext = 'FT' then ans := ZZFT(s)
else if ext = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет
else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет
else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет
else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет
else if ext = 'FV' then ans := '' // STUB: чтение вендорской шины (1 байт)
else if ext = 'FW' then ans := '' // STUB: чтение вендорской шины (2 байта)
else if ext = 'FX' then ans := '' // STUB: запись вендорской шины (1 байт)
else if ext = 'FY' then ans := '' // STUB: запись вендорской шины (2 байта)
else if ext = 'GA' then ans := ZZGA(s)
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет
@@ -1466,16 +1465,16 @@ begin
else if ext = 'IS' then ans := ZZIS(s)
else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JO' then ans := '' // STUB: команда не описана
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда
else if ext = 'KM' then ans := ZZKM(s)
else if ext = 'KO' then ans := '' // STUB: без описания у Thetis
else if ext = 'KO' then ans := '' // STUB: команда не описана
else if ext = 'KS' then ans := ZZKS(s)
else if ext = 'KY' then ans := ZZKY(s)
else if ext = 'LA' then ans := '' // STUB: уровень RX0 — микшер Thetis
else if ext = 'LA' then ans := '' // STUB: уровень RX0 внешнего микшера
else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет
@@ -1487,7 +1486,7 @@ begin
else if ext = 'MA' then ans := ZZMA(s)
else if ext = 'MB' then ans := ZZMB(s)
else if ext = 'MD' then ans := ZZMD(s)
else if ext = 'ME' then ans := '' // STUB: без описания у Thetis
else if ext = 'ME' then ans := '' // STUB: команда не описана
else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
else if ext = 'MG' then ans := ZZMG(s)
else if ext = 'ML' then ans := ZZML
@@ -1528,7 +1527,7 @@ begin
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF
else if ext = 'OJ' then ans := '' // STUB: без описания у Thetis
else if ext = 'OJ' then ans := '' // STUB: команда не описана
else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
else if ext = 'OS' then ans := ZZOS(s)
else if ext = 'OT' then ans := ZZOT(s)
@@ -1548,7 +1547,7 @@ begin
else if ext = 'PS' then ans := ZZPS(s)
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено
// ZZQK: у Thetis break-in делится на semi и QSK; у нас break-in один
// ZZQK различает semi и QSK; у нас break-in один
// (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
else if ext = 'QM' then ans := ZZQM
@@ -1580,7 +1579,7 @@ begin
else if ext = 'SP' then ans := ZZSP(s)
else if ext = 'SQ' then ans := ZZSQ(s)
else if ext = 'SR' then ans := ZZSR(s)
else if ext = 'SS' then ans := '' // STUB: без описания у Thetis
else if ext = 'SS' then ans := '' // STUB: команда не описана
else if ext = 'ST' then ans := ZZST(s)
else if ext = 'SU' then begin ZZSU; ans := ''; end
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет
@@ -1590,14 +1589,14 @@ begin
else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
else if ext = 'TA' then ans := ZZTA(s)
else if ext = 'TB' then ans := ZZTB(s)
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре (UI Thetis)
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре
else if ext = 'TH' then ans := ZZTH(s)
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
else if ext = 'TL' then ans := ZZTL(s)
else if ext = 'TM' then ans := ZZTM(s)
else if ext = 'TO' then ans := ZZTO(s)
else if ext = 'TP' then ans := ZZTP(s)
else if ext = 'TS' then ans := '' // STUB: датчик температуры Flex 5000
else if ext = 'TS' then ans := '' // STUB: неподдерживаемый датчик температуры
else if ext = 'TU' then ans := ZZTU(s)
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
else if ext = 'TX' then ans := ZZTX(s)
@@ -1629,7 +1628,7 @@ begin
else if ext = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет
else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет
else if ext = 'VW' then ans := '' // STUB: без описания у Thetis
else if ext = 'VW' then ans := '' // STUB: команда не описана
else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет
@@ -1654,7 +1653,7 @@ begin
else if ext = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT
else if ext = 'WW' then ans := '' // STUB: уровень выхода FlexWire в микшере F1500 — см. ZZWT
else if ext = 'WW' then ans := '' // STUB: уровень выхода вендорской шины — см. ZZWT
else if ext = 'XA' then ans := ZZXA(s)
else if ext = 'XC' then ans := ZZXC
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
@@ -1846,7 +1845,7 @@ begin
end;
function TCATEngine.ZZBS(const s: string): string;
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
// ZZBS: выбор диапазона. В EWSDR поле двузначное, а базовый формат использует
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
@@ -1877,10 +1876,10 @@ begin
end;
function TCATEngine.ZZCD(const s: string): string;
// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает
// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же
// ZZCD: задержка отпускания break-in (hang time), мс. В EWSDR потолок 2000 —
// это намеренный предел: столько же
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры.
// Значение выше подрезается по общему правилу, формат — те же 4 цифры.
begin
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
end;
@@ -1948,7 +1947,7 @@ begin
end;
function TCATEngine.ZZEB(const s: string): string;
// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis).
// ZZEB: значения TX-эквалайзера одной строкой в протокольном формате.
// nnn + (nnn){nb+1}, добито нулями до 36 символов;
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
var
@@ -2146,7 +2145,7 @@ end;
function TCATEngine.ZZKM(const s: string): string;
// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений
// (окно CW Messages, F1..F8). Write-only, как в Thetis.
// (окно CW Messages, F1..F8). Команда доступна только для записи.
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
// иамбик вкл/выкл доступен через ZZCI.
var v: Integer;
@@ -2198,7 +2197,7 @@ begin
end;
function TCATEngine.ZZMG(const s: string): string;
// ZZMG: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две
// ZZMG: усиление микрофона в дБ. SET — три цифры либо знак+две
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
var v: Integer;
@@ -2206,7 +2205,7 @@ begin
if Length(s) = 0 then
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
// как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не
// Допустимы две протокольные длины поля. Более короткие поля — ошибка формата, а не
// «маленькое значение»: молча крутить усиление по ним нельзя.
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
@@ -2220,7 +2219,7 @@ function TCATEngine.ZZML: string;
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
function TCATEngine.ZZMN(const s: string): string;
// ZZMN: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты
// ZZMN: имя режима по номеру. В базовом формате ZZMN отдаёт пресеты
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
@@ -2441,12 +2440,12 @@ begin
end;
function TCATEngine.ZZRV: string;
// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа).
// ZZRV: напряжение питания PA, формат «NN.N» (4 символа).
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
var v: Double;
begin
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1;
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём, как Thetis
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём
if v > 99.9 then v := 99.9;
Result := Format('%04.1f', [v]);
end;
@@ -2459,7 +2458,7 @@ begin
end;
function TCATEngine.ZZSA: string;
// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO
// ZZSA: шаг настройки вниз. В EWSDR шагает активный VFO,
// (отдельного «шага VFO A» в контроллере нет).
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
@@ -2652,8 +2651,8 @@ begin
Result := '';
end;
// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) —
// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его
// ZZWA..ZZWS: неподдерживаемые команды внешнего микшера — STUB.
// Водопад тут ни при чём: его
// уровни живут в ZZDN/ZZDO.
function TCATEngine.ZZWA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
@@ -2714,7 +2713,7 @@ function TCATEngine.ZZYR(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
// служебные команды; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
// Всё — STUB: либо чужое железо, либо в CAT не проброшено.
function TCATEngine.ZZZA(const s: string): string;
@@ -2732,7 +2731,7 @@ begin
end;
function TCATEngine.ZZZE(const s: string): string;
// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры номер
// ZZZE: Andromeda — поворот энкодера. Верхние 2 цифры кодируют номер
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
var raw, enc, step: Integer;
begin
@@ -2797,7 +2796,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZW(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZZ: string;
// ZZZZ: без описания у Thetis — STUB.
// ZZZZ: команда не описана — STUB.
begin Result := ''; end;
function TCATEngine.ZZZO(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
@@ -2806,7 +2805,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZCI(const s: string): string;
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по
// (прямой ключ / iambic A / iambic B) — включение выбирает B по
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
begin
@@ -2827,7 +2826,7 @@ begin
end;
function TCATEngine.ZZCM(const s: string): string;
// ZZCM: у Thetis это «CW Monitor DISABLE» значение инвертировано к сайдтону.
// ZZCM означает «CW Monitor DISABLE», значение инвертировано к сайдтону.
begin
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
+1 -2
View File
@@ -31,8 +31,7 @@ unit CATSliceEndpoint;
// └── TCATSliceEndpoint ── TCATEngine ── TCP 19091 (слайс B)
// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
//
// Эталон поведения — SmartSDR CAT: порт привязан к слайсу, а флаг Auto TX
// («Auto Switch TX Slice») разрешает команде TX самой сделать этот слайс
// Порт привязан к слайсу, а флаг Auto TX разрешает команде TX сделать его
// источником передачи. Вся защита «в эфире одновременно только один слайс»
// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
//
+1 -1
View File
@@ -19,7 +19,7 @@
unit CATTcp;
{
CATTcp.pas TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis).
CATTcp.pas многопоточный TCP-сервер команд CAT.
Архитектура:
Один listening socket (TCATTcpServer)
+5 -5
View File
@@ -29,8 +29,8 @@ unit CWKeyer;
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
// Эталон — pihpsdr: src/iambic.c (автомат) + cw_shape_buffer в transmitter.c
// (огибающая прямо в TX-буфере, мимо WDSP).
// Автомат формирует элементы и записывает огибающую прямо в TX-буфер,
// минуя WDSP.
//
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
@@ -228,8 +228,8 @@ type
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
CTS/DSR/DCD/RI читаются как лепестки. Так же делают Thetis (cwkeyer.cs),
hamlib и fldigi это единственный вход ключа, доступный на любом железе. }
CTS/DSR/DCD/RI читаются как лепестки. Такой вход доступен на любом железе
через стандартный последовательный интерфейс. }
TCWKeyPort = class(TThread)
private
FLock: TCriticalSection;
@@ -780,7 +780,7 @@ begin
end;
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
// Автомат иамбика (логика pihpsdr src/iambic.c в терминах элементов):
// Автомат иамбика в терминах точек, тире и межэлементных пауз:
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
// следующим; режим A памяти не имеет (в этом и вся разница);
+1 -1
View File
@@ -24,7 +24,7 @@ unit CWMessagesForm;
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
так что окно висит рядом ровно как окно CWX у Thetis. Те же ячейки
поэтому окно располагается рядом. Используются те же ячейки
привязаны к F1..F8 в главном окне.
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
+1 -1
View File
@@ -30,7 +30,7 @@ unit CWMorse;
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
// нажать и отпустить, решает PC. Именно этот путь используется для
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs).
// передачи текста через макросы и команду CAT KY.
//
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
+3 -3
View File
@@ -27,7 +27,7 @@ unit DMRDecoder;
sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
This first layer deliberately stops at burst sync. The following layer will
pass aligned dibits to the trimmed dsd-fme DMR/FEC/AMBE core.
pass aligned dibits to the DMR/FEC/AMBE decoding core.
}
{$IFDEF FPC}
@@ -203,7 +203,7 @@ const
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3;
// One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight
// One bit per sync character: '1'=0, '3'=1. These are the eight
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
SYNC_TEXT: array[0..7] of string = (
'313333111331131131331131', // BS data
@@ -512,7 +512,7 @@ begin
if FHistoryCount[Phase] >= 24 then
begin
Distance := PopCount24(FSyncShift[Phase] xor Pat);
// Real on-air sync commonly carries one or two sign errors. DSD-FME
// Real on-air sync commonly carries one or two sign errors. The
// likewise uses a bounded sync tolerance; requiring an exact 24-symbol
// match lost almost every superframe in recorded handheld-radio IQ.
DeltaSamples := FSampleNo - FLastSyncSample;
+2 -2
View File
@@ -22,8 +22,8 @@ unit DMRProtocol;
{
Small, allocation-free DMR burst protocol layer.
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h).
Implements CACH/AMBE interleavers and Hamming/Golay matrices.
The applicable third-party permissive notice follows.
Copyright (C) 2010 DSD Author
Permission to use, copy, modify, and/or distribute this software for any
+2 -2
View File
@@ -115,7 +115,7 @@ const
MAX_FREQ = 20000.0;
TOP_H = 44;
BOTTOM_H = 82;
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под
// Кривая соответствует расчёту частотной характеристики WDSP под
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
@@ -357,7 +357,7 @@ begin
end;
procedure TEqualizerControl.BuildResponse;
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G —
// Рассчитывает амплитудную характеристику эквалайзера в дБ без построения FIR. Точки F/G —
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
+3 -3
View File
@@ -30,8 +30,8 @@ unit FilterPopup;
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
длиной FIR (fircore) и никаким пользовательским параметром не управляется.
Эталон содержимого Thetis FilterForm.cs (12 слотов + Name/Low/High/Width);
графика полосы наша добавка, у них её нет.
Содержимое включает 12 слотов и поля Name/Low/High/Width, а график наглядно
показывает положение и форму полосы.
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно)
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
@@ -374,7 +374,7 @@ begin
end;
procedure TFilterPopup.WidthChange(Sender: TObject);
// Width производная: держим центр полосы, разъезжаются оба края (как Thetis).
// Width производная: центр полосы сохраняется, изменяются оба края.
var W, C, Lo, Hi: Integer;
begin
if FLoading then Exit;
+13 -13
View File
@@ -498,7 +498,7 @@ procedure TDUCIQSenderThread.Execute;
const
DUC_IQ_PAIRS_PER_PACKET = 240;
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
// Пейсинг DUC-FIFO радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread).
// Пейсинг DUC-FIFO синхронизирован с аппаратным тактированием радио.
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
@@ -611,7 +611,7 @@ begin
DrainVirtualFIFO;
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
// При пустой очереди ждём сигнала о появлении данных.
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
@@ -668,7 +668,7 @@ begin
Sleep(50);
if Terminated then Break;
if not FNet.FConnected then Continue;
// Отправляем полный HP с частотами и ALEX — как piHPSDR
// Отправляем полный HP-пакет с частотами и состоянием ALEX.
FNet.SendFullHP;
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
@@ -1351,14 +1351,14 @@ begin
end;
// ---------------------------------------------------------------------------
// ALEX фильтры — логика из piHPSDR new_protocol.c
// Формирование регистра фильтров ALEX.
// Возвращает 32-bit ALEX0 register value
// RXFreqHz — частота приёма ADC0
// TXFreqHz — частота передачи (или = RXFreqHz при RX)
// Transmitting — режим передачи
// IsOrion2 — ANAN-7000/8000 (другие BPF)
// ---------------------------------------------------------------------------
// Преобразует частоту в индекс диапазона 0=160m..10=6m (Thetis-compatible).
// Преобразует частоту в протокольный индекс диапазона 0=160m..10=6m.
function FreqToBandIndex(FreqHz: Double): Integer;
begin
if FreqHz >= 39850000 then Result := 10 // 6m
@@ -1386,7 +1386,7 @@ end;
// XVTR без усиления.
// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11
// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3.
// WidebandBypass — True когда открыт raw ADC wideband. Как Thetis, раскрываем
// WidebandBypass — True, когда открыт raw ADC wideband. В этом режиме раскрываем
// RX front-end через HF bypass, иначе видна только полоса
// текущего HPF/BPF, а не весь ADC span.
function CalcAlex0(RXFreqHz, TXFreqHz: Double;
@@ -1411,7 +1411,7 @@ begin
if not XvtrDisablePA then
begin
Result := Result or $08000000; // bit 27: T/R relay
// Bit 18 = PureSignal (ALEX_PS_BIT в pihpsdr): на TX подключает
// Bit 18 = PureSignal: на TX подключает
// внутренний мост обратной связи PA к RX-тракту ADC0.
if PureSignalTX then
Result := Result or $00040000;
@@ -1604,8 +1604,8 @@ end;
// Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) —
// в тех же координатах, в которых маски лежат в настройках и рисуются в UI.
// Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP).
// Портировано из Thetis (Penny.UpdateExtCtrl/adjustForTXAction), урезано:
// без PA-override и split-pins VFO A/B (см. project_oc_control память).
// Реализация учитывает только возможности EWSDR: без PA-override и
// раздельных пинов VFO A/B.
// RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый
// пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их
// комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON
@@ -1735,7 +1735,7 @@ begin
if FPanDDCEnabled[ddc] then
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]);
// PureSignal feedback (как pihpsdr new_protocol_receive_specific): на время
// PureSignal feedback: на время
// PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback,
// оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) —
// его сэмплы приходят interleaved внутри потока DDC0 (порт 1035).
@@ -1866,7 +1866,7 @@ end;
procedure THPSDRNetwork.SetCWXKey(Down: Boolean);
// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка
// доставки = один UDP-пакет (так же делает Thetis, cwx.cs setkey).
// доставки определяется отправкой одного UDP-пакета.
begin
if FCWXKey = Down then Exit;
FCWXKey := Down;
@@ -1945,7 +1945,7 @@ begin
Buf[12 + ddc*4] := Ph and $FF;
end;
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой (pihpsdr, bytes 9..16).
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой из байтов 9..16.
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
if FPSTXActive then
begin
@@ -2000,7 +2000,7 @@ begin
end;
// XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы.
// Логика как в Thetis:
// Правила установки бита:
// в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен)
// в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab)
if FXvtrEnable then
+5 -7
View File
@@ -419,7 +419,7 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean;
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards.
// VoffMV/SensMVA — калибровка датчика (Thetis AmpVoff/AmpSens, ACS712-style).
// VoffMV/SensMVA — смещение нуля и чувствительность датчика тока.
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double;
@@ -429,8 +429,7 @@ function ADCToWatts100(RawADC: Word): Double;
// Convert raw ADC value to RF power in Watts (ANAN-10)
function ADCToWatts10(RawADC: Word): Double;
// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/
// computeRefPower по модели трансивера:
// Alex bridge FWD/REV detector → Watts; параметры зависят от модели трансивера:
// 0 = ANAN-10/100/100B/100D (Vref 3.3V)
// 1 = ANAN-200D / Orion (Vref 5.0V)
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
@@ -482,7 +481,7 @@ end;
function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double;
begin
// Protocol Appendix A gives the ADC reference voltage only.
// The PCB voltage divider (from Thetis source) must also be applied.
// The PCB voltage divider must also be applied.
// Board 0 (Atlas): no supply voltage measurement.
// Boards 1,2,3 (Hermes/Angelia): Vref=3.3V, PCB divider R1=4.7kΩ/R2=0.82kΩ.
// Board 4 (Orion): protocol reports supply voltage with a 5.0V constant.
@@ -511,8 +510,8 @@ function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
VoffMV: Double; SensMVA: Double): Double;
begin
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
// Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А
// (Thetis, ACS712-style); переопределяются калибровкой.
// Formula: amps = (mV - Voff) / Sens. Defaults: Voff=360 mV,
// Sens=120 mV/A; both values can be overridden by calibration.
if (BoardType = 5) and (SensMVA > 0) then
begin
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
@@ -545,7 +544,6 @@ var
Bridge, Vref, V: Double;
CalOff: Integer;
begin
// Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower).
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
case DetModel of
1: begin // ANAN-200D / Orion
+11 -13
View File
@@ -266,7 +266,7 @@ type
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
// --- CAT ---
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
FTCIAdapter: TTCIAdapter; // адаптер TCI (WebSocket-сервер Expert Electronics)
FTCIAdapter: TTCIAdapter; // адаптер TCI для WebSocket-сервера
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
@@ -749,7 +749,7 @@ type
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // Flex-style смена банда пана
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // смена диапазона отдельного пана
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
// Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней.
@@ -4027,7 +4027,7 @@ begin
end;
end;
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
// Маленький FFT даёт новые строки чаще.
if FSpectrumDirty then
begin
FSpecView.DrawSpectrum;
@@ -4986,12 +4986,11 @@ end;
// CTUN ВЫКЛ (классический режим):
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
//
// CTUN ВКЛ (piHPSDR/Thetis режим):
// CTUN ВКЛ:
// FController.FCenterFreq статичен — спектр/водопад не двигаются
// FController.FVfoA движется по дисплею (маркер гуляет)
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
// hits the edge of the display")
// Перецентрирование — только когда край полосы фильтра выходит за границу
// дисплея.
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
begin
@@ -5067,7 +5066,7 @@ begin
end;
// ---------------------------------------------------------------------------
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
// DUP toggle — локальный режим дисплея без аппаратной команды.
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyDUP(Active: Boolean);
begin
@@ -5401,7 +5400,7 @@ begin
end;
// ---------------------------------------------------------------------------
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
// TUN — тон WDSP TXA PostGen + MOX с отдельным уровнем Drive.
// ---------------------------------------------------------------------------
procedure TMainForm.ApplyTUN(Active: Boolean);
begin
@@ -6095,8 +6094,7 @@ end;
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
// ПКМ по кнопке фильтра открывает редактор на этом слоте. ЛКМ не трогаем —
// иначе окно вылезало бы при каждом обычном переключении фильтра.
begin
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
@@ -7185,7 +7183,7 @@ begin
end;
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
// Flex-style смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
// Смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
// band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC
// (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы).
var
@@ -8904,7 +8902,7 @@ end;
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3).
procedure TMainForm.ApplyXvtrToNetwork;
begin
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
+2 -2
View File
@@ -217,7 +217,7 @@ const
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
// Размер TX-буфера масштабируется от sample-rate (как SoapyPlutoSDR):
// Размер TX-буфера масштабируется от sample-rate:
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
@@ -387,7 +387,7 @@ begin
end;
function TxBufPairsForRate(Rate: Integer): Integer;
// Размер TX-буфера в IQ-парах под device-rate (как SoapyPlutoSDR): round(rate/60)
// Размер TX-буфера в IQ-парах под device-rate: round(rate/60)
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
// 576k→16384, 1536k→32768, 3072k→65536.
var
+2 -2
View File
@@ -116,7 +116,7 @@ const
CLR_INPUT = TColor($00FFFFFF);
CLR_INPUT_TEXT = TColor($00202020);
// Лампа feedback — пороги от цели PSFBTarget (Thetis-Enhanced eu2av):
// Лампа feedback — пороги относительно цели PSFBTarget:
// >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет
// сигнала. TColor = BGR.
CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал)
@@ -390,7 +390,7 @@ begin
end;
procedure TPureSignalPopup.DefPkClick(Sender: TObject);
// Дефолт SetPk для протокола 2 (Thetis clsHardwareSpecific.PSDefaultPeak).
// Значение SetPk по умолчанию для протокола 2.
begin
FSePk.Value := 0.2899; // OnChange → Apply
end;
+1 -1
View File
@@ -192,7 +192,7 @@ type
procedure ClearDUCIQQueue; virtual;
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
// первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto
// первых миллисекунд). База/Pluto — no-op: у Pluto
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
procedure PrimeDUCIQ(Pairs: Integer); virtual;
+42 -45
View File
@@ -428,7 +428,7 @@ type
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
FPSSingleCal: Boolean; // запрошена одиночная калибровка
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
FPSCmdState: Integer; // состояние автомата команд PureSignal
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
FPSAASaveSingle: Integer;
@@ -497,7 +497,7 @@ type
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
FTXSpecRefLevel: Double; // опорный уровень TX-сетки при FTransmitting
FTXSpecRange: Double;
FTXSpecGridStep: Double;
@@ -718,7 +718,7 @@ type
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
procedure ApplyModeFilter;
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
// Вычисляет аппаратный drive-байт из текущего состояния мощности.
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte;
@@ -1152,8 +1152,8 @@ type
function CWSettings: TCWSettings;
procedure SetCWSettings(const C: TCWSettings);
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
// CWX в High Priority — точки/тире рисует прошивка по этому биту.
// Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
procedure CWXSend(const Text: string);
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
@@ -1192,8 +1192,8 @@ type
procedure SetPureSignal(On_: Boolean);
procedure PSSingleCal; // одиночная калибровка
procedure PSReset; // выключить и сбросить коррекцию
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX.
// Тик PS (~10 Гц, из UI-таймера): автомат команд, опрос GetPSInfo и
// автоматическая регулировка AttOnTX по уровню обратной связи.
procedure PureSignalTick;
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
@@ -1202,7 +1202,7 @@ type
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis).
// замеры на Anvelina PRO3; для остальных используется значение 152.
function PSFBTarget: Integer;
function PSCalCount: Integer; // info[5]
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
@@ -1337,8 +1337,8 @@ const
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
TUN_TONE_MAG = 0.83;
// Состояния командной state machine PureSignal (порт Thetis eCMDState;
// Restore-ветка (7) не реализована — Save/Restore коррекции нет).
// Состояния автомата команд PureSignal. Восстановление сохранённой
// коррекции не реализовано.
PS_ST_OFF = 0;
PS_ST_TURNON_AUTO = 1;
PS_ST_AUTOCAL = 2;
@@ -1827,7 +1827,7 @@ begin
SupplyV := -1.0;
SupplyA := -1.0;
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1.
// Прочие платы — в SupplyVolts-байтах, без тока.
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin
@@ -1849,7 +1849,7 @@ begin
end;
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
// (как Thetis PABandCalMult).
// и используется только для калибровки показаний мощности.
Bi := TxBandIdx;
Is6m := Bi = BAND_6M;
DetK := 1.0;
@@ -1859,7 +1859,7 @@ begin
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW); только при значимой
// fwd-мощности.
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
begin
@@ -1884,8 +1884,8 @@ begin
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload;
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
// Касание обрывает программную передачу, чтобы оператор мог вклиниться.
// ★Обрываем по
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
@@ -2989,7 +2989,7 @@ function TRadioController.FrontEndCalDB: Double;
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
// или само и не должно двигать абсолютную шкалу dBm.
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
// показания (так же делают Thetis и piHPSDR).
// показания.
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
// его само) → вычитаем текущее значение.
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
@@ -3356,8 +3356,8 @@ end;
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
Tune: Boolean = False);
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. При
// включённом Auto TX слайс сам становится
// TX-источником, без него — передаёт, только если оператор уже выбрал его
// источником (бейдж TX на флаге).
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
@@ -4049,7 +4049,7 @@ begin
Hi := FFilterHi;
if FWDSPReady and Assigned(FDSPEngine) then
begin
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
// Только RX. TX-полоса НЕ следует за RX-фильтром — её
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
FDSPEngine.SetFilter(Lo, Hi);
@@ -4148,7 +4148,7 @@ end;
// =====================================================================
function TRadioController.CalcDriveByte: Byte;
// Thetis-совместимая формула:
// Преобразование целевой мощности в аппаратный байт drive:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
@@ -4385,9 +4385,9 @@ begin
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
// --- Фильтр: санитизация FM до применения к DSP ----------------------
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
// отдельно по режимам. Иначе у VAR было бы два источника истины —
// ровно та болезнь, от которой уходили.
ApplyFilterSlot(B.FilterIdx);
ApplyModeFilter;
@@ -5814,7 +5814,7 @@ end;
function TRadioController.CWTXActive: Boolean;
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570),
// состоянии голосовой TXA не запускается вовсе,
// а несущую даёт кейер / бит CWX / тон TUN.
begin
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
@@ -5851,10 +5851,9 @@ begin
end;
function TRadioController.CWOptionsByte: Byte;
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
// объяснение, почему iambic-бит выставляется и для Mode B).
// Формирует DUC Specific byte 5; iambic-бит выставляется и для Mode B.
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
// на замыкание ключа посреди SSB.
begin
Result := 0;
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
@@ -5913,10 +5912,10 @@ begin
DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24;
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB с шагом 1.5 dB.
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
// ATT on TX — ступенчатый аттенюатор RX-ADC во время передачи.
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
@@ -6269,7 +6268,7 @@ end;
procedure TRadioController.ApplyTXSettingsToDSP;
begin
if not FWDSPReady then Exit;
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
// Для openHPSDR Protocol 2 CFIR постоянно включён:
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
// имеет, поэтому при смене бэкенда явно выключаем его.
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
@@ -6418,8 +6417,8 @@ begin
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
FDSPEngine.PSSetMox(FTransmitting);
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis,
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ. В WDSP режим TXA_CWL —
// буквально ветка SSB, поэтому
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
@@ -6476,8 +6475,7 @@ begin
begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон.
if FWDSPReady and Assigned(FDSPEngine) then
begin
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
@@ -6902,7 +6900,7 @@ procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
// ключ (это же ограничение у Thetis).
// ключ; это ограничение удалённого управления телеграфом.
begin
if not CWTXActive then
begin
@@ -7071,7 +7069,7 @@ begin
end;
// ===========================================================================
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
// PureSignal — оркестрация автомата калибровки и обратной связи.
// ===========================================================================
function TRadioController.PSAvailable: Boolean;
@@ -7097,7 +7095,7 @@ begin
end;
procedure TRadioController.SetPureSignal(On_: Boolean);
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение —
// Кнопка PS включает режим автоматической калибровки. Выключение —
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
begin
if On_ = FPSEnabled then Exit;
@@ -7114,7 +7112,7 @@ begin
end;
procedure TRadioController.PSSingleCal;
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается.
// Одиночная калибровка выключает автоматический режим.
begin
if not PSAvailable then Exit;
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
@@ -7140,7 +7138,7 @@ begin
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
else
Result := 152; // классическая цель Thetis
Result := 152; // стандартный целевой уровень
end;
end;
@@ -7150,7 +7148,7 @@ begin
if Result < 0 then
begin
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av
Result := 5.0 // Orion MkII: значение по умолчанию
else
Result := 0.0; // выключен
end;
@@ -7203,9 +7201,8 @@ begin
end;
procedure TRadioController.PureSignalTick;
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы)
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
// Вызывается из UI-таймера (~10 Гц). Автомат управляет калибровкой без
// восстановления сохранённой коррекции и подстраивает AttOnTX по feedback.
var
NewAtt: Integer;
FBTarget: Integer;
@@ -7287,7 +7284,7 @@ begin
PS_ST_TURNOFF:
begin
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF).
// не дойдёт до LRESET при PSEnabled=true в состоянии TurnOFF.
FDSPEngine.PSControl(1, 0, 0, 0);
PSSetFeedbackPath(True);
FPSResetReq := False;
@@ -7300,7 +7297,7 @@ begin
end;
end;
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) ---
// --- Автоматическая регулировка аттенюатора обратной связи ----------
// По завершении очередной попытки калибровки: если feedback слишком горячий
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
@@ -7350,7 +7347,7 @@ begin
end;
procedure TRadioController.SetTwoTone(On_: Boolean);
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный
// 2TON: двухтональник PostGen + MOX. Уровень — обычный
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
begin
if On_ = FTwoTone then Exit;
+1 -1
View File
@@ -27,7 +27,7 @@ unit SerialPort;
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
телеграфный ключ приходилось глушить заглушкой.
И скопировать чужой исходник было нельзя. `Serial.SerSetParams` кладёт
Готовая реализация `Serial.SerSetParams` кладёт
B-константу скорости прямо в c_cflag идиома Linux, где это битовая маска.
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
+31 -35
View File
@@ -33,8 +33,7 @@ const
CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
// TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs):
// при каком состоянии передачи пин активен.
// TX pin action: определяет, при каком состоянии передачи пин активен.
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
OCA_TUNE = 1; // только TUNE
OCA_TWOTONE = 2; // только 2TON
@@ -52,7 +51,7 @@ const
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице)
// PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults
// PA gain defaults (dB) for HERMES/HPSDR hardware.
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
@@ -128,7 +127,7 @@ const
('6.25k', '10k', '12.5k', '20k', '25k');
// --- Редактируемая таблица фильтров -------------------------------------
// 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки
// 10 пресетов + VAR1/VAR2 на режим. Кромки
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
// своя — поэтому при применении фильтра конвертировать боковую больше не
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
@@ -249,11 +248,11 @@ type
// Mic gain (dB, применяется в WDSP PanelGain1)
MicGainDB: Double; // -10..+30
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB.
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2), default 31 dB.
AttOnTX: Integer; // 0..31 dB
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
// Уровень — отдельный от слайдера Drive, проходит через ту же
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz.
// per-band калибровку. Defaults: level=10 %, freq=750 Hz.
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
// TX bandpass filter
@@ -301,11 +300,11 @@ type
TXWfDetector: Integer;
TXWfAvgMode: Integer;
TXWfAvgTimeMS: Double;
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
// TX Grid задаётся отдельно от RX и использует собственные границы.
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
TXSpecRange: Double; // dB range, e.g. 80
TXSpecGridStep: Double; // grid step in dB, e.g. 10
// PureSignal (WDSP calcc; дефолты = Thetis PSForm, протокол 2)
// PureSignal (WDSP calcc, протокол 2)
PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
@@ -318,7 +317,7 @@ type
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
// Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152
// Калибровка фидбек-тракта: на Orion MkII цель 152
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
// (Orion MkII → 22, иначе 152)
@@ -331,10 +330,9 @@ type
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
// Раскладка байта 5 HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs)
// и pihpsdr (new_protocol.c:1353).
// Раскладка байта 5 задана константами HPSDRProtocol.CW_*.
TCWSettings = record
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
Pitch: Integer; // Гц, тон приёма/сайдтона, default 600
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
Messages: array[0..CW_MSG_COUNT-1] of string[63];
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
@@ -348,7 +346,7 @@ type
Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
HangTimeMS: Integer; // сколько держать TX после последнего элемента
RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс)
RampMS: Integer; // форма фронта посылки, default 9 мс
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
SidetoneHWLevel: Integer; // 0..127
@@ -394,8 +392,7 @@ type
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
// зависит: переключение профиля не должно перерисовывать спектр, менять
// задержку тракта или ломать калибровку PS.
// Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без
// её хвоста из VAC/DSP-буферов, которых у нас нет.
// Профиль не содержит параметры VAC и DSP-буферов, которых в EWSDR нет.
TTXProfile = record
Name: string[31];
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
@@ -428,7 +425,7 @@ type
EQFreqs: array[0..10] of Double;
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
AMCarrierLevel: Double;
// Мощность (Thetis держит Power/Tune_Power в профиле)
// Мощность Power/Tune_Power хранится в профиле.
DrivePercent: Integer; // 0..100
TUNLevel: Integer; // 0..100
end;
@@ -478,12 +475,12 @@ type
SpecPixels: Integer;
end;
// TCI-сервер (протокол Expert Electronics поверх WebSocket). Авторизации в
// TCI-сервер поверх WebSocket. Авторизации в
// протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые
// программы обычно живут на том же ПК, а наружу порт открывается осознанно.
TTCISettings = record
Enabled: Boolean;
Port: Integer; // 1..65535, умолчание 40001 (как у ExpertSDR3)
Port: Integer; // 1..65535, по умолчанию 40001
BindAddr: string; // '127.0.0.1' или '0.0.0.0'
// ★Единственный каталог, куда TCI пишет записи линейного выхода
// (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records.
@@ -579,12 +576,11 @@ type
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
// Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis
// (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён:
// Хранятся в JSON-секции "oc" под MAC. Поддерживается сокращённый набор:
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings).
// VHF-группа — 8 слотов XVTR (аналог Thetis VHF0-13, но по нашим слотам).
// VHF-группа — 8 слотов XVTR по внутренней нумерации EWSDR.
TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
TOCSettings = record
@@ -599,13 +595,13 @@ type
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
// (Vref, bridge voltage и ADC offset).
TCalibration = record
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360)
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120)
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
@@ -613,11 +609,11 @@ type
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его
// Display Cal Offset: у display-тракта своё окно/детектор, и его
// уровень отличается от показаний стрелки на пару дБ.
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Частотная калибровка опорника, ppm (Frequency Calibration в Thetis).
// Частотная калибровка опорника, ppm.
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
// ниже истинной частоты; положительное значение поднимает показания.
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров
@@ -720,8 +716,8 @@ type
// Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
// Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по
// CAT-порту слайса сама назначает этот слайс источником передачи.
// Auto TX: команда TX по CAT-порту слайса сама назначает этот слайс
// источником передачи.
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
// приложения / общий вход). Имена, а не индексы: порядок перечисления
@@ -1710,21 +1706,21 @@ begin
T.MicBias := True;
T.LineInGainDB := 0.0; // 0 dB
T.MicGainDB := 10.0;
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи)
// ATT on TX — default 31 dB (полная защита RX-ADC во время передачи)
T.AttOnTX := 31;
// Tune — Thetis defaults
// Tune defaults
T.TUNLevel := 10;
T.TUNFreq := 750;
// SSB-фильтр по умолчанию
T.FilterLow := 200;
T.FilterHigh := 3100;
T.FilterNC := 1024;
T.FilterMP := 1; // Low Latency (как в piHPSDR)
T.FilterMP := 1; // Low Latency
T.FilterWindow := 1; // BH4 / Blackman-Harris
// Compressor — выкл по умолчанию
T.CompressorOn := False;
T.CompressorGain:= 5.0;
// Leveler вкл, типичные значения piHPSDR/Thetis
// Leveler включён с типичными начальными значениями.
T.LevelerOn := True;
T.LevelerTop := 5.0;
T.LevelerDecay := 500;
@@ -1767,7 +1763,7 @@ begin
T.TXSpecRefLevel := 0.0;
T.TXSpecRange := 80.0;
T.TXSpecGridStep := 10.0;
// PureSignal — дефолты Thetis PSForm (протокол 2)
// PureSignal — значения по умолчанию для протокола 2.
T.PSMoxDelay := 0.2;
T.PSLoopDelay := 0.0;
T.PSTXDelayNs := 150;
@@ -1868,7 +1864,7 @@ end;
class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
begin
FillChar(C, SizeOf(C), 0);
C.Pitch := 600; // Thetis cw_pitch default
C.Pitch := 600; // default CW pitch
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
C.KeyerMode := 2; // Iambic B
C.ReverseKeys := False;
@@ -1877,7 +1873,7 @@ begin
C.Weight := 50;
C.BreakIn := True;
C.HangTimeMS := 300;
C.RampMS := 9; // Thetis SetCWEdgeLength(9)
C.RampMS := 9; // default CW edge length
C.RFDelayMS := 0;
C.SidetoneHW := True;
C.SidetoneHWLevel := 50;
+10 -11
View File
@@ -1940,7 +1940,7 @@ begin
end;
// ---------------------------------------------------------------------------
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
// BuildTransmitTab — все настройки TX-тракта.
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildTransmitTab;
@@ -3283,7 +3283,7 @@ begin
end;
procedure TSettingsForm.BuildCalibrationTab;
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
// Вкладка Calibration: модель детектора мощности
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
@@ -3428,7 +3428,7 @@ begin
end;
// ── Spectrum / waterfall ────────────────────────────────────────────────
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display-
// Отдельная от S-метра калибровка Display Cal Offset: у display-
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
// сигналы) — это калибровка уровня, а не подавление шума.
@@ -3477,7 +3477,7 @@ begin
end;
// ── Reference frequency ─────────────────────────────────────────────────
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis):
// Одна поправка опорника на всё железо:
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
// рознь, и где-то уход больше, где-то меньше.
@@ -3691,7 +3691,7 @@ procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
begin
if FLoading then Exit;
// Смена модели детектора подставляет модельные дефолты датчика тока
// (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88,
// ANAN-7000DLE/Anvelina Pro 3 — 340/88,
// остальные — 360/120. Правится руками после, если датчик нестандартный.
if Sender = FCmbPwrDetModel then
begin
@@ -5117,7 +5117,7 @@ begin
FCATTcpPort := Spin;
// ── TCI Server ────────────────────────────────────────────────────────────
// Протокол Expert Electronics поверх WebSocket: логгеры, скиммеры, цифра.
// TCI поверх WebSocket: логгеры, скиммеры, цифровые виды связи.
Grp := MakeGroupPanel(FPageCAT, 'TCI Server',
MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H);
Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT;
@@ -5317,8 +5317,8 @@ begin
Spin.OnChange := OnSliceCATCtrlChange;
FSliceCATPort[i] := Spin;
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире,
// Auto TX: команда TX по этому порту сама делает слайс источником
// передачи. Пока кто-то уже в эфире,
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
// (в ширину группы), пояснение — строкой ниже.
Chk := TFlatCheckBox.Create(Self);
@@ -6239,9 +6239,8 @@ end;
// ===========================================================================
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
// Реализован сокращённый набор: без PA-override/hot-switching внешнего PA
// и без раздельных пинов VFO A/B. Только openHPSDR —
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
// ===========================================================================
+4 -4
View File
@@ -114,7 +114,7 @@ type
FTXOverlay: Boolean;
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
// TX-шкала НЕ следует за RX, поэтому при передаче полоса рисуется по
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
FRXEdgeLo, FRXEdgeHi: Integer;
FTXEdgeLo, FTXEdgeHi: Integer;
@@ -813,7 +813,7 @@ begin
M[i].LevelDB := PeakDB(M[i].FreqHz);
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
// Все маркеры — абсолютный уровень шкалы (dBm, как в Thetis); относительная
// Все маркеры — абсолютный уровень шкалы в dBm; относительная
// величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы.
for i := 0 to 3 do
M[i].Caption := Format('%.0f', [M[i].LevelDB]);
@@ -1398,7 +1398,7 @@ end;
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая
// два умножения на dst-лейны (нормализация /256, кривая
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
var
Row, Col, Alpha, YMin, A256, I256: Integer;
@@ -1701,7 +1701,7 @@ begin
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
// считается по TX-фильтру: его ширина независима от приёмной,
// и раньше в эфир шло одно, а на экране рисовалось другое.
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
begin
+25 -27
View File
@@ -71,7 +71,7 @@ const
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
// и «второй приёмник» там существует только как слайс главного пана — при
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1.
// номеров: некоторые клиенты позволяют выбрать только приёмники 0 и 1.
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
@@ -114,9 +114,9 @@ const
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: MSHV пишет свой
// TX-буфер granule'ами STREAM_C = 4096 отсчётов при 96 кГц (network.cpp), то
// есть 42.7 мс, и отвечает на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: клиент может писать
// TX-буфер гранулами по 4096 отсчётов при 96 кГц, то есть 42.7 мс, и отвечать
// на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
@@ -125,7 +125,7 @@ const
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после
// подключения иначе стартует с pre-roll 31.75 мс против первого ответа MSHV
// подключения иначе стартует с pre-roll 31.75 мс до первого ответа клиента
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
// успевает: аванс появляется только после первого ответа.
TCI_TX_LEAD_DEF_MS = 50;
@@ -254,7 +254,7 @@ type
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
// подрос — попросили ещё больше. Это положительная обратная связь. Период
// же равен внутренней грануле клиента (у MSHV — STREAM_C/96 кГц = 42.7 мс)
// же равен внутренней грануле клиента (например, 4096/96 кГц = 42.7 мс)
// и от нашего темпа не зависит вовсе.
FTxGapUs: Double; // оценка периода между пачками, мкс
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
@@ -857,8 +857,8 @@ begin
FServer.Broadcast(TCIBuild('rx_channel_enable',
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
// входящие по arg1 (см. StrTrx), а некоторые также связывают с этим флагом
// разрешение передачи. Слайс, созданный
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
FServer.Broadcast(StrTxEnable(Rx));
end;
@@ -1195,8 +1195,8 @@ end;
function TTCIAdapter.StrTrx(Rx: Integer): string;
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
// его слайс. Клиенты могут фильтровать входящие строки по номеру приёмника,
// поэтому автору команды
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
// слайс сейчас источник передачи (`TxRx`).
@@ -1309,8 +1309,7 @@ end;
procedure TTCIAdapter.BroadcastTxEnable;
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача
// (`set_ptt`: `if (!tci_tx_enable) return;`).
// приёмнике 1 может её отбросить и не разрешить передачу.
var Rx: Integer;
begin
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
@@ -1830,8 +1829,7 @@ begin
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
// настоящую частоту и завершить инициализацию несуществующего приёмника.
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
@@ -2157,7 +2155,7 @@ begin
begin
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
// берётся из аудиопотока этого клиента. Остальные значения
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
// (mic1/mic2/micpc/ecoder2) называют физические входы TCI, которых
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
@@ -2196,8 +2194,8 @@ begin
// ничего не изменила, всё равно её перебивала: клиент, уже
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает
// (network.cpp:231), то есть передача просто замолкает.
// с этого мига уходят под номером 0. Клиент второго приёмника может
// отбросить такие блоки, и передача просто замолкнет.
if B then
begin
if Started and FromTCI then
@@ -2236,7 +2234,7 @@ begin
FTxRx := 0;
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
// идущей передачи. Раньше эти две строки расходились, и штатный
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым.
// конец посылки (`trx:N,false`) оставлял FTxRunning поднятым.
FTxRunning := False;
end;
finally FTxLock.Leave; end;
@@ -2789,7 +2787,7 @@ begin
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
begin
Client.AudioRate := V;
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и
// Число сэмплов в блоке у TCI своё на каждую частоту (§4.3), и
// клиент вправе на это рассчитывать, пока не задал своё явно.
Client.AudioSamples := TCIDefaultAudioSamples(V);
RestartStreams(Client, tstRXAudio);
@@ -3740,7 +3738,8 @@ end;
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
//
// ★Зачем: MSHV отвечает на первый маркер только через ~41 мс, а сам маркер
// ★Зачем: некоторые клиенты отвечают на первый маркер только через ~41 мс,
// а сам маркер
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
@@ -4001,9 +4000,8 @@ begin
(Sent < TCI_TX_MARKERS_PER_TICK) do
begin
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
// Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV,
// network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а
// TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono).
// Клиент может фильтровать ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver, а
// TX-аудио отправлять только в ответ на этот маркер.
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
// маркеры до него не доходили вовсе.
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
@@ -4069,8 +4067,8 @@ begin
// ровно то, сколько их и распаковалось. Верим меньшему из двух: клиент
// вправе прислать короткий хвост, но не длиннее уместившегося. Заведомо
// чужое число (мусор в заголовке) просто игнорируем — длину нам и так
// ограничил размер кадра. Так же считает и MSHV, когда сам заполняет
// заголовок: `t_txStream->length = (cr3/bit_s)`, где cr3 — байты блока.
// ограничил размер кадра. Значение заголовка задаётся числом распакованных
// отсчётов, а не числом байт блока.
if H.DataLength > 0 then
begin
Want := Integer(H.DataLength);
@@ -4103,8 +4101,8 @@ begin
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
// просили, и зажим в ноль заставил бы переспросить уже полученное.
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
// отсчётов: первые ответы MSHV — законные нули (network.cpp, ветка
// _reset_sta_ <= 5), и сторож не должен считать их отсутствием прогресса.
// отсчётов: первые ответы клиента могут быть законными нулями, и сторож не
// должен считать их отсутствием прогресса.
NowUs := MonotonicUs;
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
// очередями по нескольку блоков — по своей внутренней зернистости записи.
+11 -18
View File
@@ -20,7 +20,7 @@
unit TCIProtocol;
{
TCIProtocol.pas протокол TCI 2.0 (Expert Electronics), чистый слой.
TCIProtocol.pas протокол TCI 2.0, чистый слой.
Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера.
Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и
@@ -34,7 +34,7 @@ unit TCIProtocol;
Зарезервированные символы: ':' ',' ';' внутри аргументов запрещены и
заменяются на '^' '~' '*' (§3.2.1), обратная подстановка TCIUnescape.
Регистр значения не имеет. ExpertSDR3 шлёт всё в нижнем регистре, и часть
Регистр значения не имеет. TCI шлёт всё в нижнем регистре, и часть
клиентов сравнивает строки как есть поэтому наружу тоже пишем строчными,
а внутрь принимаем любой.
}
@@ -52,17 +52,17 @@ uses
const
TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL
TCI_APP_NAME = 'EWSDR';
TCI_DEFAULT_PORT = 40001; // порт TCI-сервера ExpertSDR3
TCI_DEFAULT_PORT = 40001; // стандартный порт TCI-сервера
TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B)
// Список видов связи для MODULATIONS_LIST. Первые десять — словарь
// ExpertSDR3 (клиенты сверяются именно с ними), dmr/fmraw — наше
// расширение: протокол расширяемый по замыслу (§1.4).
// Список видов связи для MODULATIONS_LIST. Первые десять — стандартные
// режимы TCI; dmr/fmraw — расширения EWSDR. Протокол допускает расширение
// этого списка (§1.4).
TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw';
// Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно).
// Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока,
// а не его размер: больше в один блок не кладут ни ExpertSDR3, ни клиенты.
// а не его размер: больше в один блок не кладут ни TCI, ни клиенты.
TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке
TCI_STREAM_HDR_SIZE = 64; // 16 × uint32
TCI_STREAM_MAX = TCI_STREAM_HDR_SIZE + TCI_STREAM_DATA_MAX;
@@ -164,7 +164,7 @@ function TCISampleTypeByName(const S: string; out T: TTCISampleType): Boolean;
function TCISampleTypeName(T: TTCISampleType): string;
function TCISampleBytes(T: TTCISampleType): Integer;
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у ExpertSDR3
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у TCI
своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. }
function TCIDefaultAudioSamples(RateHz: Integer): Integer;
@@ -178,22 +178,15 @@ function TCIMaxBlockSamples(T: TTCISampleType; Channels: Integer): Integer;
Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как
Stream.length/Stream.channels»), а для аудио формулировка §4.3 про
AUDIO_STREAM_SAMPLES («arg1 количество сэмплов, указываемое в поле
Stream.length») однажды уже увела нас в «сэмплы на канал». ★Так делать
нельзя, и вот доказательство из живого клиента MSHV, network.cpp:
int cr2 = (int)pStream->length*bit_s; // сколько БАЙТ разбирать
for (int i = 0; i < cr2; i+=chan*bit_s) // шаг = кадр всех каналов
...
quint32 cr3 = pStream->length*bit_s; //plength = plength*channels*4-bytes
То есть клиент считает по length число байт блока. Стоит объявить length «на
Stream.length») может быть прочитана как «сэмплы на канал». ★Так делать
нельзя: клиент вычисляет по length число байт блока. Стоит объявить length «на
канал» и у стерео он разбирает ровно половину блока, а вторую выбрасывает:
звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не
декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора
звука и 1649 не до конца разобранных блоков за минуту.
Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES сэмплы НА
КАНАЛ (2048 при 48 кГц = 42.7 мс, как у ExpertSDR3), и потолок data[16384]
КАНАЛ (2048 при 48 кГц = 42.7 мс), и потолок data[16384]
сходится ровно: 2048 × 2 канала × float32. }
procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType;
Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer);
+2 -2
View File
@@ -20,8 +20,8 @@
unit TCIServer;
{
TCIServer.pas транспорт TCI: WebSocket-сервер (роль сервера играем мы,
как ExpertSDR3; клиенты логгеры, скиммеры, программы цифровых видов).
TCIServer.pas транспорт TCI: WebSocket-сервер для логгеров, скиммеров
и программ цифровых видов связи.
Что делает:
слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket
+3 -3
View File
@@ -222,7 +222,7 @@ type
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
начало активной записи затирало само себя, а SAVE через час после START
отдавал файл, которого у ExpertSDR3 давно бы не было.
отдавал бы файл уже после завершения заданного окна записи.
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
(мьют, пауза приёмника), а время записи всё равно идёт. }
@@ -682,8 +682,8 @@ begin
F := TCIDecimFactor(ASrcRate, AWantRate);
FOutRate := ASrcRate div F;
// Блок не имеет права вылезти за data[16384]: столько ExpertSDR3 объявил
// потолком, и клиенты держат приёмный буфер ровно под него.
// Блок не имеет права вылезти за data[16384]: это установленный протоколом
// предел, под который клиенты выделяют приёмный буфер.
MaxB := TCIMaxBlockSamples(FSampleT, FChannels);
FBlock := EnsureRange(ABlock, 1, MaxB);
+1 -1
View File
@@ -22,7 +22,7 @@ unit VfoOverlay;
{$mode objfpc}{$H+}
{
TVfoOverlay накладка-«флаг слайса» поверх спектра, стиль SmartSDR.
TVfoOverlay накладка-«флаг слайса» поверх спектра.
Компоновка (сверху вниз):
шапка : [A] 2.7k SPLIT [TX]
метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40
+40 -44
View File
@@ -196,7 +196,7 @@ const
TX_MIC_RING = 8192;
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
// TX rate постоянен: 192k для Hermes/Saturn DUC0,
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
TX_SAMPLE_RATE = 192000;
@@ -323,7 +323,7 @@ type
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double;
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
// Отдельный небольшой feed для display-анализатора.
FDispBuf: array of Double;
FDispPos: Integer;
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
@@ -372,7 +372,7 @@ type
FWfAvgMode: Integer;
FWfAvgTimeMS: Double;
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP:
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
@@ -531,7 +531,7 @@ type
FFilterLow: Integer;
FFilterHigh: Integer;
FAGCMode: TWDSPAGCMode;
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
FAGCTop: Double; // AGC gain in dB
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
FAGCHangThreshold: Integer; // порог hang 0..100
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
@@ -580,7 +580,7 @@ type
// главного RXA и для слайсов — иначе дублирование по трём местам.
procedure ApplyRXModeSettings(Chan, Mode: Integer);
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
// Единственный конвертер знака краёв TX-фильтра для выбранного режима,
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// --- CW pitch -----------------------------------------------------------
@@ -783,7 +783,7 @@ type
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
// по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr).
// по 0.49, чтобы сумма оставалась чуть ниже полной шкалы.
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
@@ -795,7 +795,7 @@ type
procedure SetPureSignalArmed(Armed: Boolean);
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
// потока (как Thetis/pihpsdr — pscc потокобезопасен относительно
// потока; pscc потокобезопасен относительно
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
TotalPairs: Integer);
@@ -958,7 +958,7 @@ implementation
// ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c
// Поток обработки TX-аудио и формирования IQ.
// ===========================================================================
type
TDisplayThread = class(TThread)
@@ -981,7 +981,7 @@ type
function UsePanNormOneHz(Detector: Integer): Integer;
begin
// В Thetis нормализация к 1 Гц включается только для panadapter
// Нормализация к 1 Гц включается только для panadapter
// и только для detector modes 2/3/4.
if Detector in [2, 3, 4] then
Result := 1
@@ -1029,8 +1029,7 @@ begin
end;
procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c:
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Поток работает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var
@@ -1052,7 +1051,7 @@ begin
Dec(AccUs, PeriodMs * 1000);
// Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
// Здесь timing задаётся таймаутом, синхронизированным с размером буфера.
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
if Terminated then Break;
if not FEngine.FTXActive then Continue;
@@ -1085,7 +1084,7 @@ begin
if Terminated then Break;
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
// Silence-инъекция УБРАНА. Раньше при пустом ring
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
@@ -1098,9 +1097,8 @@ begin
end;
// ===========================================================================
// DSP поток обрабатывает IQ пакеты из очереди
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
// Аналог iq_thread в piHPSDR/new_protocol.c
// DSP поток обрабатывает IQ-пакеты из очереди.
// Сетевой поток только кладёт пакеты, этот поток занимается DSP.
// ===========================================================================
type
TDSPThread = class(TThread)
@@ -1125,7 +1123,7 @@ var
begin
while not Terminated do
begin
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
// Ждём сигнала от сетевого потока.
RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break;
@@ -1339,7 +1337,7 @@ begin
end;
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы
// Вход — магнитуда TX-аудио-полосы
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
var
@@ -1376,8 +1374,7 @@ begin
end;
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
// Для CWL смещение положительное, для CWU — отрицательное.
begin
case Mode of
MODE_CWL: Result := FCWPitch;
@@ -1396,7 +1393,7 @@ end;
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338).
// т.е. у CWU получается pitch±bw/2.
var Off: Integer;
begin
Off := Round(CWLOOffset(Mode));
@@ -1428,7 +1425,7 @@ begin
begin
// Restore the user's normal voice/data chain after leaving FM RAW.
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам.
// bp0 всегда активен сам; здесь обновляем остальную цепочку TX.
ApplyTXChainSettings;
if Mode = MODE_WFM then ApplyTXWFMSettings;
end;
@@ -1478,9 +1475,9 @@ begin
inherited Create;
FSampleRate := SampleRate;
FAudioRate := AudioRate;
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
// BufSize = dsp_size @ AudioRate; внутренний DSP buffer_size = 1024.
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
// in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz.
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
@@ -1579,7 +1576,7 @@ begin
FTXToneOn := False;
FTXTTOn := False;
FKeepRXDuringTX := False;
// PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2)
// PureSignal — значения по умолчанию для протокола 2.
FPSArmed := False;
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
@@ -2210,7 +2207,7 @@ begin
end;
try
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
// Анализатор открывается перед DSP-каналом.
OpenAnalyzer;
if not FAnalyzerOpen then
begin
@@ -2237,7 +2234,7 @@ begin
0,
0.010, 0.010, 0.010, 0.010,
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
// bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
);
@@ -2277,7 +2274,7 @@ begin
Result := True;
// Запускаем DSP поток после успешной инициализации WDSP
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
// Запускаем отдельный поток обработки IQ.
FDSPThread := TDSPThread.Create(Self);
FDSPThread.Priority := tpHighest; // DSP критический поток
@@ -2531,7 +2528,7 @@ begin
Item^.PanId := 0;
FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
RTLEventSetEvent(FQueueSem); // будим DSP-поток
end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
@@ -2815,7 +2812,7 @@ begin
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
// При TX RX-аудио глушим в режиме non-duplex MOX,
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
@@ -2824,7 +2821,7 @@ begin
FPostTXMuteUntil := 0;
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
// FPostTXMuteUntil-хвост уважаем и при non-duplex MOX.
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then
begin
@@ -3523,7 +3520,7 @@ begin
FAGCMode := Mode;
if not FInitialized then Exit;
// Точно по piHPSDR receiver.c: set_agc()
// Настройка параметров AGC приёмного канала.
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
// Читаем обратно hang level и thresh для линии на спектре
@@ -3976,7 +3973,7 @@ end;
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
// Амплитуды 0.49+0.49 сумма чуть ниже полной шкалы (как Thetis/pihpsdr).
// Амплитуды 0.49+0.49 дают сумму чуть ниже полной шкалы.
var
F1, F2: Double;
begin
@@ -4026,9 +4023,8 @@ end;
procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
// если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
// Если mic ring buffer содержит достаточно данных — берём их,
// иначе обрабатываем тишину (нули).
var
i, tail: Integer;
Err: Integer;
@@ -4055,7 +4051,7 @@ begin
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end else
begin
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
// Нет mic-данных — заполняем входной TX-буфер нулями.
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end;
@@ -4258,7 +4254,7 @@ end;
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
DataOffset: Integer; TotalPairs: Integer);
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... →
// деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data):
// Деинтерливим feedback-потоки и передаём их в WDSP pscc:
// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
var
i, n, Pos: Integer;
@@ -4329,7 +4325,7 @@ procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
// величину и без нас (порт Thetis), но пусть формула будет в одном месте.
// величину и без нас, но формула остаётся в одном месте.
var L, H, Half: Double;
begin
Half := 0.0;
@@ -4423,7 +4419,7 @@ end;
procedure TWDSPEngine.PushTXEQProfile;
// Единственная точка, отдающая кривую EQ в WDSP.
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
// F[1..n] — узлы ломаной АЧХ, между ними интерполяция по
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
@@ -4694,7 +4690,7 @@ end;
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin
// Thetis-style display FFT range.
// Диапазон display FFT.
if FFTSize < 4096 then FFTSize := 4096;
Result := 4096;
while Result < FFTSize do
@@ -4705,7 +4701,7 @@ end;
function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin
// FFT Size теперь снова управляет display-analyzer как в Thetis,
// FFT Size управляет display-analyzer,
// но не влияет на аудио DSP цепочку.
Result := FFFTSize;
end;
@@ -4716,7 +4712,7 @@ var
TargetFPS: Double;
begin
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
// Thetis/piHPSDR-style timing:
// Тайминг обновления дисплея:
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
EffectiveFPS := TargetFPS;
@@ -4729,7 +4725,7 @@ begin
if ForWaterfall then
begin
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
// Peak hold на водопаде даёт грязную и залипающую картинку.
if AvgMode < 0 then AvgMode := 1;
end
else
+2 -2
View File
@@ -40,7 +40,7 @@ const
WF_MAX_PIXELS = 4096;
// Палитры водопада (индексы совпадают с настройкой wf_palette)
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема)
WF_PAL_CLASSIC = 0; // классическая тёмная / пастельная светлая палитра
WF_PAL_INFERNO = 1;
WF_PAL_TURBO = 2;
@@ -139,7 +139,7 @@ begin
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1]
// Классическая тёмная палитра, T∈[0..1].
function ClassicDarkColor(T: Double): LongWord;
var
Local: Double;
+1 -1
View File
@@ -22,7 +22,7 @@ unit WebServer;
{
WebServer.pas HTTP + WebSocket сервер для удалённого управления трансивером.
Архитектура (по образцу OpenWebRX):
Архитектура веб-подсистемы:
─────────────────────────────────
HTTP GET / index.html (см. WebPageHtml)
HTTP GET /ws Upgrade: WebSocket
+4 -4
View File
@@ -24,7 +24,7 @@ unit WidebandView;
The network layer feeds 16-bit ADC samples collected from Protocol V4
wideband packets. This view feeds them to a dedicated WDSP analyzer and
renders a compact Thetis-style wideband panadapter. CPU paint uses a bitmap;
renders a compact wideband panadapter. CPU paint uses a bitmap;
when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
primitives.
}
@@ -308,16 +308,16 @@ begin
if not FWDSPAnalyzerConfigured then
begin
SampleRate := Max(1, Round(FSampleRateHz));
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // Thetis wideband default.
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // default wideband averaging.
SetAnalyzer(
FWDSPAnalyzerID,
2,
1,
1, // Feed like piHPSDR: interleaved I/Q via Spectrum0.
1, // Interleaved I/Q feed via Spectrum0.
@FWDSPFlp[0],
WB_WDSP_FFT,
WB_WDSP_BLOCK,
6, // Thetis wideband default window.
6, // default wideband window.
14.0,
0, // no overlap for discontinuous WB packets.
0,
+15 -18
View File
@@ -4,8 +4,8 @@
Дата последнего обновления: 2026-06-26.
Документ описывает поддержку аппаратной передней панели Apache Labs
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD).
За эталон протокола взят Thetis (`Andromeda/Andromeda.cs`, `CAT/CATCommands.cs`).
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD)
через команды CAT, используемые панелью.
См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы.
@@ -84,9 +84,8 @@ Andromeda — это **не отдельный набор команд**, а д
## 3. Раскладка по умолчанию (AndromedaMap.pas)
Раскладка **зашита в коде** и зеркалит дефолтную таблицу Thetis
(`MakeNewAndromedaDataset`): наш 0-based индекс = «Pushbutton/Encoder Number»
Thetis 1. Номера **проверены на реальной панели** (см.
Раскладка **зашита в коде**: внутренний 0-based индекс на единицу меньше номера
кнопки или энкодера, передаваемого панелью. Номера **проверены на реальной панели** (см.
`andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`.
### Кнопки (`ZZZP`, индекс 0-based после разбора)
@@ -105,12 +104,12 @@ Thetis 1. Номера **проверены на реальной панел
| | | 47 | TUN |
Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT).
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены
(в Thetis — на тачскрине), действия в enum сохранены для кастомной раскладки.
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены;
действия в enum сохранены для пользовательской раскладки.
### Shift / band-keypad
Кнопка #28 — модификатор-защёлка (как `eBBShift` в Thetis). Пока активен
Кнопка #28 — модификатор-защёлка. Пока он активен
(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую
(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается
повторным Shift либо автоматически после выбора диапазона (non-sticky).
@@ -120,7 +119,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
| # | Параметр | Шаг |
|---|---|---|
| 0 | AF Gain (громкость) | ±1 |
| 1 | AGC-T (в Thetis — слайдер «RF») | ±1 dB |
| 1 | AGC-T | ±1 dB |
| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) |
| 11 | Drive (мощность) | ±1 % |
| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) |
@@ -129,7 +128,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован.
> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта.
### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis)
### Индикаторы (`ZZZI`, номера соответствуют протоколу панели)
| LED # | Индикатор | LED # | Индикатор |
|---|---|---|---|
| 1 | MOX | 12 | Split |
@@ -142,7 +141,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
| 11 | VFO Lock | | |
Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR,
SNB, ANF, Squelch**. Остальные слоты дефолтной таблицы Thetis (ATU/RIT/XIT)
SNB, ANF, Squelch**. Остальные слоты таблицы панели (ATU/RIT/XIT)
в ewsdr функций не имеют и не зажигаются.
---
@@ -169,7 +168,7 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
- Транспорт: **serial** (USB CAT).
- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие).
- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов).
- Раскладка — зашитый дефолт, выверена по Thetis и реальной панели.
- Раскладка по умолчанию проверена с реальной панелью.
- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`.
- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`.
- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели.
@@ -178,11 +177,11 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
### Осталось / возможные улучшения
| Задача | Заметки |
|---|---|
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и passband дисплея (sideband-aware, как в Thetis) |
| **Sticky shift** | сейчас shift всегда non-sticky (снимается band-нажатием); в Thetis есть `AndromedaStickyShift` |
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и sideband-aware passband дисплея |
| **Sticky shift** | сейчас shift всегда non-sticky и снимается нажатием кнопки диапазона; можно добавить сохраняемую настройку |
| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) |
| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` |
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) |
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг и пользовательский интерфейс |
| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) |
| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` |
| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано |
@@ -197,6 +196,4 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` +
push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю).
- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы).
- Сверяться с форматами Thetis: `Andromeda/Andromeda.cs` (логика панели,
`EIndicatorActions`/`EButtonBarActions`/`EEncoderActions`) и
`CAT/CATCommands.cs` (`ZZZ*`).
- Форматы команд `ZZZ*` и нумерацию элементов проверять по протоколу панели.
+5 -9
View File
@@ -6,13 +6,9 @@
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
(19090) остаются за главным приёмником (слайс A) и не тронуты.
Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу
(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»:
> When enabled Auto Switch TX Slice automatically switches the Transmit Slice to
> the VFO A Slice selected above when a CAT TX command is sent on the com port…
> If this option is not enabled, the operator will have to manually designate the
> Transmit Slice.
Каждый порт привязан к определённому слайсу. Флаг **Auto TX** разрешает
команде передачи автоматически назначить этот слайс источником TX; без флага
источник выбирается оператором вручную.
---
@@ -72,8 +68,8 @@ TX-источника атомарны относительно GUI.
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в
SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает.
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе.
Бейдж на флаге это показывает.
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
+32 -36
View File
@@ -5,8 +5,8 @@
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
полный перечень реализованных команд, осознанные заглушки и план дальнейших
работ. За эталон протокола взят **Thetis / PowerSDR** (`CATParser.cs`,
`CATCommands.cs`, `CATStructs.xml`) — диалект Kenwood TS-2000 + расширения `ZZ*`.
работ. Поддерживается диалект Kenwood TS-2000 с расширениями `ZZ*`,
используемый совместимыми SDR-приложениями.
---
@@ -23,7 +23,7 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
| `CATTcp.pas` (~507 строк) | TCP-сервер (Thetis-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
| `CATTcp.pas` (~507 строк) | TCP-сервер (CAT-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
### 1.1 Принципы
@@ -50,29 +50,29 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
| | Кол-во |
|---|---|
| Всего `ZZ*` в Thetis | 315 |
| Всего известных `ZZ*` | 315 |
| Реально реализовано в EWSDR | **~120** |
| Заглушки (отвечают валидным dummy, функции нет) | ~190 |
| Совсем не обрабатываются (вернут `?;`) | 5 |
Подписи всех 315 команд сверены с `CATCommands.cs` (см. §2.5) — комментарию у
Подписи всех 315 команд проверены по протокольному поведению (см. §2.5) — комментарию у
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).
### 1.3 Осознанные отклонения от Thetis
### 1.3 Осознанные особенности реализации
Три места, где EWSDR намеренно расходится с эталоном. Больше таких нет.
Три команды имеют локальный формат, сложившийся в EWSDR.
| Команда | У Thetis | У нас | Почему |
| Команда | Базовое значение | Реализация EWSDR | Почему |
|---|---|---|---|
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |
Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
эталон принимает в 150..5000 мс, у нас потолок 2000 — столько же стоит в
в EWSDR имеет потолок 2000 мс — столько же стоит в
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
выше подрезаются, как и всё прочее у Thetis.
выше ограничиваются общим механизмом проверки входных значений.
---
@@ -153,7 +153,7 @@ S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`,
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (0014) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |
Шкала шагов `ZZAU/ZZBP` `StepIdxToHz` (зеркало Thetis `Step2Freq`):
Шкала шагов `ZZAU/ZZBP` задаётся функцией `StepIdxToHz`:
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
@@ -176,7 +176,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
| `ZZET` | кнопка TXEQ | `EQOn` |
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (поле длиной 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
| `ZZTO` | мощность настройки | `TUNLevel` |
| `ZZTU` | кнопка TUN | `SetTune` |
| `ZZUT` | двухтональник 2TON | `SetTwoTone` |
@@ -194,7 +194,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
| `ZZCB` | break-in прошивки | `.BreakIn` |
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
| `ZZCM` | сайдтон (у Thetis — «monitor **disable**», значение инвертировано) | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
| `ZZCM` | сайдтон; поле `monitor disable` использует инвертированное значение | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
@@ -216,10 +216,10 @@ CW: по CAT прилетает что угодно, а контроллер п
### 2.5 Ревизия алиасов и подписей
Подписи писались по буквам кода, а не по Thetis, и врали примерно в 150
Подписи писались по буквам кода, а не по CAT, и врали примерно в 150
местах. Хуже: часть **работающих** команд была привязана не к своей функции —
внешний софт получал осмысленный, но неверный ответ, что хуже честной
заглушки. Всё сверено с `CATCommands.cs` и исправлено.
заглушки. Все подписи проверены и исправлены.
**Команда делала не своё дело — теперь делает своё:**
@@ -253,42 +253,39 @@ CW: по CAT прилетает что угодно, а контроллер п
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
> Во всех случаях старое поведение было отсебятиной.
**Кенвудовские команды.** Сверены отдельно: все 40 команд, реализованных в
Thetis, у нас есть, ширины полей совпадают с `CATStructs.xml`. Исправлено:
**Кенвудовские команды.** Сверены отдельно: реализованы все 40 команд,
необходимых для совместимости, и проверена ширина полей. Исправлено:
| Команда | Что было не так |
|---|---|
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;``SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2`, как шлёт Thetis |
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;``SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2` |
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь обёртка над `ZZTA`, как в эталоне, и сама `ZZTA` проверяет значение строго |
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент Thetis обращается как раз к `ZZ*` и не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь это обёртка над `ZZTA`, а сама `ZZTA` проверяет значение строго |
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент расширенных команд не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу:
| Где | Что было не так |
|---|---|
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` |
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Теперь лишний суффикс отбраковывается в парсере, а список безаргументных команд хранится в `IsParamless` |
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS` — там мусор трактуется как симплекс намеренно, по эталону |
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS`, где некорректное значение намеренно трактуется как симплекс |
| `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками
после запятой (вставляет точку после третьего разряда) — численно это ровно те
же герцы, что пишем мы, поле совместимо. Мусор в `ZZOS` эталон трактует как
симплекс (`default` в `String2OffsetDirection`), и мы намеренно повторяем это,
а не отвечаем ошибкой.
Формат `ZZOT` проверен отдельно: 9 цифр кодируют МГц с шестью знаками после
запятой, что численно соответствует целому значению в герцах. Некорректное
значение `ZZOS` намеренно трактуется как симплекс для совместимости клиентов.
Формат ответа на `ZZ*` (`ZZ` + код + **эхо суффикса запроса** + значение) сверен
с `CATParser.ParseExtended` — совпадает; это важно для `ZZSM0;`, где запрос
несёт селектор приёмника.
Формат ответа на `ZZ*` `ZZ` + код + **эхо суффикса запроса** + значение.
Эхо суффикса важно для `ZZSM0;`, где запрос несёт селектор приёмника.
---
@@ -314,9 +311,8 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
> Прежде чем реализовывать новую команду, сверяйся с форматом поля в
> `Thetis/.../CAT/CATStructs.xml` (`<nsetparms>` / `<ngetparms>`) и телом
> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. И проверяй
> Прежде чем реализовывать новую команду, проверяй ширину, знак и смысл поля
> по документации протокола и поведению совместимых клиентов. Также проверяй
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).
### 3.1 Совсем не обрабатываются (вернут `?;`)
@@ -339,9 +335,9 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма |
| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) |
| **Микшер Flex 5000 / F1500, FlexWire** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | железо другого вендора |
| **Вендорские команды микшера и служебной шины** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | неподдерживаемое оборудование |
| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера |
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки Thetis |
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки CAT |
---
@@ -382,7 +378,7 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
## 6. Как добавить новую CAT-команду (чек-лист)
1. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis).
1. **Сверить формат** с документацией протокола и тестами совместимости.
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
+3 -3
View File
@@ -249,11 +249,11 @@ end;
(блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
один `sampling_frequency`).
- **Размер TX-буфера** масштабируется от device-rate (модель SoapyPlutoSDR):
- **Размер TX-буфера** масштабируется от device-rate:
`round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
- Подтверждено по каноничному драйверу (SoapyPlutoSDR): TX full-scale = 32768,
- Для TX используется full-scale 32768,
MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.
### 4.7 Усиление / телеметрия
@@ -394,7 +394,7 @@ end;
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
full-duplex >1 Msps — TX-буфер масштабируется от rate (модель SoapyPlutoSDR).
full-duplex >1 Msps — TX-буфер масштабируется от rate.
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
`SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
+13 -13
View File
@@ -1,11 +1,11 @@
# Мультислайсы и панадаптеры (как во FlexRadio)
# Мультислайсы и панадаптеры
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
`3049f91`, `a3ac459`).
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер**
edge-маркером» принята модель **аппаратный DDC = отдельный панадаптер**
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
альтернативы» в конце файла. Реализация Этапа 3 не начата.
@@ -44,9 +44,9 @@ edge-маркером» принята Flex-модель: **аппаратный
---
## Модель FlexRadio (итог разбора 2026-07-10)
## Модель панадаптеров и слайсов (итог разбора 2026-07-10)
Во Flex **две независимые сущности**, и это ключ ко всему дизайну:
В интерфейсе используются **две независимые сущности**, и это ключ ко всему дизайну:
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
@@ -58,7 +58,7 @@ edge-маркером» принята Flex-модель: **аппаратный
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
6600 — 4/4, 6700 — 8/8.
**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit):
**Второй приёмник (второй АЦП)** представлен отдельным трактом захвата:
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
@@ -66,8 +66,8 @@ edge-маркером» принята Flex-модель: **аппаратный
двух SCU) — отдельный режим поверх.
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели.
перетаскиваемыми разделителями). EWSDR дополнительно поддерживает вынос
панафолла в отдельное окно ОС.
---
@@ -86,12 +86,12 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
один слайс в его центре (поведение Flex).
один слайс в его центре.
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
Показываем именно АЦП, а не антенну: на HPSDR-платах
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
4. Буквы слайсов — глобальный пул A..G через все паны:
буква = личность слайса, пан = место жительства.
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
прячет/блокирует кнопки, а не хардкодит «if Pluto».
@@ -324,7 +324,7 @@ ChangeSampleRate).
### 3.3 — UI: второй панадаптер в главном окне
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
`TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист.
`TSplitter` между ними. Пан 0 сверху. Высоты сохраняются в настройках.
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
@@ -480,7 +480,7 @@ ChangeSampleRate).
### 3.5 — Pop-out: панадаптер в отдельном OS-окне
Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке.
Кнопка `⧉` в шапке выносит панадаптер в отдельное окно ОС.
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
@@ -767,7 +767,7 @@ ApplyAll, тёплый рестарт восстанавливает override п
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC =
чипами слайсов). Отвергнуто 2026-07-10 в пользу модели «DDC =
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
+22 -28
View File
@@ -3,9 +3,9 @@
Ветка разработки: `feature/tci-protocol`.
Дата последнего обновления: 2026-08-18.
Эталон протокола — «Протокол TCI, версия 2.0» Expert Electronics
Реализация соответствует спецификации «Протокол TCI, версия 2.0»
(`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером**
(как ExpertSDR3): порт слушаем мы, клиенты — логгеры, скиммеры, программы
по стандартной серверной модели: порт слушаем мы, клиенты — логгеры, скиммеры, программы
цифровых видов, внешние усилители и коммутаторы.
---
@@ -187,13 +187,12 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
как в ExpertSDR3, где приёмник = DDC. У нас это разошлось с жизнью сразу на
в модели, где приёмник соответствует DDC. В EWSDR это разошлось с жизнью сразу на
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
приёмник существует там **только** как слайс главного пана — при нумерации по
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице. С
другого конца — клиенты: у MSHV в настройках всего две модели, «TCI Client rx1»
и «rx2», то есть приёмники 0 и 1, и никакого третьего номера ввести некуда
(`hvrigcontrol.cpp`: `netServPort[3]`/`[4]`, `network.cpp`: `tci_trx`). Значит
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице.
Кроме того, некоторые клиенты позволяют выбрать только приёмники 0 и 1.
Значит
«первый добавленный слайс» обязан быть приёмником **1** на любом железе.
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
@@ -255,9 +254,8 @@ B независимо от того, где он живёт: на openHPSDR э
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту;
для MSHV это вообще фатально — именно ответом на `vfo:<rx>,0;` он завершает
инициализацию, то есть подключился бы к пустоте. Состояние появляется в момент
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту,
завершая инициализацию несуществующего приёмника. Состояние появляется в момент
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
включая `tx_enable` с его номером.
@@ -501,10 +499,8 @@ web-клиента: явная просьба сильнее умолчания
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт
TX-аудио не по своей воле, а строго в ответ на маркер, и все входящие бинарные
блоки фильтрует по номеру приёмника — у MSHV это первая строка обработчика
(`network.cpp`: `if (pStream->receiver != tci_trx) return;`, ветка `TxChrono`
там же собирает и отправляет блок). С жёстким нулём в заголовке клиент,
TX-аудио строго в ответ на маркер и может фильтровать входящие бинарные блоки
по номеру приёмника. С жёстким нулём в заголовке клиент,
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
клиентом-модулятором запоминается номер приёмника из его же `TRX`
@@ -536,8 +532,8 @@ TX-аудио не по своей воле, а строго в ответ на
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
затирало само себя, а `SAVE` через час после `START` отдавал файл, которого у
ExpertSDR3 давно бы не было.
затирало само себя, а `SAVE` через час после `START` отдавало бы файл уже после
завершения заданного окна записи.
`SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки
мегабайт, а команда пришла в потоке клиента, который в это время не читает свой
сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный
@@ -794,9 +790,8 @@ running`), а сам телеграф уходит **текущему TX-ист
настройки обязана вставать на тот же слайс, что и модуляция;
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
клиенты фильтруют входящие строки по `arg1`: MSHV, например, отбрасывает
всё, что адресовано не его приёмнику (`network.cpp`:
`if (ls2.at(0)!=tci_trx) continue;`), — и отказ, названный чужим номером, до
клиенты могут фильтровать входящие строки по `arg1`, и отказ, названный
чужим номером, до
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
@@ -872,8 +867,8 @@ running`), а сам телеграф уходит **текущему TX-ист
Порядок обратный: сперва UI, потом TCI.
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
и `ANF` — они в TCI настоящие. `ANC` и `DSE` — блоки собственной обработки
Expert, не WDSP; `APF` и `NF` были бы новыми узлами в цепочке.
и `ANF` — они в TCI настоящие. Для `ANC`, `DSE`, `APF` и `NF` потребовались
бы новые узлы обработки в цепочке WDSP.
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
@@ -898,7 +893,7 @@ running`), а сам телеграф уходит **текущему TX-ист
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A |
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а ExpertSDR3 IQ иначе и не шлёт |
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а TCI IQ иначе и не шлёт |
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
@@ -918,7 +913,7 @@ running`), а сам телеграф уходит **текущему TX-ист
одинаковыми — в телеметрии платы одно значение forward power.
У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы ExpertSDR3, которых у
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы TCI, которых у
нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что
и отсутствие аргумента.
@@ -933,7 +928,7 @@ running`), а сам телеграф уходит **текущему TX-ист
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
нет вовсе. Вторая по существу: канал B у ExpertSDR3 — второй приёмник внутри
нет вовсе. Вторая по существу: канал B у TCI — второй приёмник внутри
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
@@ -1086,12 +1081,11 @@ running`), а сам телеграф уходит **текущему TX-ист
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой,
слушается командой `vfo:1,0,<Гц>`, отдаёт своё
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу
MSHV, сидящего на втором слайсе; а `trx:0,true,tci` поверх этой же передачи
маркеры `TX_CHRONO` **под номером 1**; а `trx:0,true,tci` поверх этой же передачи
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
целиком — вместо прежнего `vfo:1,0,0`.
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
+1 -1
View File
@@ -237,7 +237,7 @@ end;
const
{ Команды только для чтения: ответ на опрос составной статус, обратно его
не пишут ни Kenwood, ни Thetis. Круговой прогон их законно не проходит. }
не пишут ни Kenwood, ни CAT. Круговой прогон их законно не проходит. }
READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID');
function IsReadOnly(const Cmd: string): Boolean;
+10 -12
View File
@@ -46,27 +46,25 @@ line-out для всех обнаруженных приёмников. Он н
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
уже на передаче, прогон аварийно прекращается.
## Стенд «как MSHV»
## Стенд совместимости TCI-клиента
```sh
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
test/tci/mshv_sim.py --rx 0
test/tci/mshv_sim.py --rx 1 --audio
```
`mshv_sim.py` повторяет логику TCI-клиента MSHV по его исходникам — включая обе
особенности, из-за которых он ведёт себя не так, как наш стенд:
`mshv_sim.py` проверяет две особенности поведения совместимых TCI-клиентов:
* **фильтр по номеру приёмника**`if (ls2.at(0)!=tci_trx) continue;`: строка,
адресованная не его приёмнику, для MSHV не существует (и то же самое для
* **фильтр по номеру приёмника**: строка,
адресованная другому приёмнику, игнорируется (и то же самое для
бинарных блоков, по полю `receiver` заголовка);
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в
* **инициализация — один запрос и один ответ**: клиент шлёт `vfo:<rx>,0;` раз в
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим**
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с
`if (!tci_tx_enable) return;`).
Поэтому стенд диагностирует причину ошибки подключения одной строкой, а заодно
ловит то, что иначе проявляется молча: без `tx_enable` со **своим** номером
приёмника клиент не разрешает PTT.
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
+10 -13
View File
@@ -1,22 +1,19 @@
#!/usr/bin/env python3
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит,
почему он не подключается.
"""Стенд совместимости с TCI-клиентом MSHV.
Зачем: MSHV самый распространённый TCI-клиент для цифры, и ведёт он себя не
так, как наш собственный стенд. Две его особенности определяют всё:
1. ВСЕ входящие строки он фильтрует по номеру приёмника:
if (ls2.at(0)!=tci_trx) continue; // network.cpp
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
RX2 (tci_trx = "1") строка `trx:0,false`
не существует в природе.
2. Инициализация это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался
«Error To Initialize TCI Server» (network.cpp, Network::initAll,
ветка s_ModelID==3||4). Никаких других причин у этой ошибки нет.
сообщает об ошибке инициализации.
Соответствие настроек MSHV: «TCI Client rx1» приёмник 0, «TCI Client rx2»
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx).
Соответствие настроек: «TCI Client rx1» приёмник 0, «TCI Client rx2»
приёмник 1.
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
TX-аудио не отправляются. Передающую часть включает явный --tx.
@@ -38,8 +35,8 @@ from live_rx_test import WS, split_commands, parse_command # noqa: E402
HDR = struct.Struct("<16I")
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2,
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс.
# Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
# частота 48 кГц и буфер TX 24 мс.
MSHV_CHANNELS = "2"
MSHV_SAMPLES = "2048"
MSHV_TYPE = "float32"
@@ -65,7 +62,7 @@ class Verdict:
class MshvClient:
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения."""
"""Минимальная модель поведения совместимого TCI-клиента."""
def __init__(self, ws, rx, verdict):
self.ws = ws
@@ -148,7 +145,7 @@ class MshvClient:
if len(payload) < HDR.size:
return
head = HDR.unpack(payload[:HDR.size])
# MSHV: if (pStream->receiver != tci_trx) return;
# Блоки других приёмников клиент игнорирует.
if head[0] != int(self.rx):
self.audio_foreign += 1
return
+15 -19
View File
@@ -121,7 +121,7 @@ begin
Check('header rate', H.SampleRate = 12000);
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на
// считают по нему число байт. Объявишь «на
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
Check('header length аудио = ×каналы', H.DataLength = 1024);
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
@@ -484,7 +484,7 @@ begin
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
(H.Format <> LongWord(Ord(tsyFloat32))) or
// ★Правило, по которому живут клиенты: байт данных = length × размер
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`).
// отсчёта. Так клиент определяет размер полезной нагрузки.
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
Inc(Blocks);
end;
@@ -2054,7 +2054,7 @@ begin
end;
C.WaitText('ready;', 2000);
// Клиент назвался ровно как MSHV: 48 кГц, два канала, блок 2048.
// Клиент запросил 48 кГц, два канала и блок 2048.
C.SendText('audio_samplerate:48000;');
C.SendText('audio_stream_channels:2;');
C.SendText('audio_stream_samples:2048;');
@@ -2174,7 +2174,7 @@ begin
// идти дальше, а не прекратиться до конца передачи.
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
// это редкость (на живом MSHV — два неотвеченных маркера за 12 с), но
// это редкость, но
// строка ниже печатает просадку, чтобы регресс был виден.
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
@@ -2182,8 +2182,8 @@ begin
Markers > 40, IntToStr(Markers));
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
// ★Регресс с железа. Штатный конец посылки (`trx:N,false`)
// обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
// миллисекунд против шага тика в полкванта, так что промах выпадал через
@@ -2586,19 +2586,17 @@ begin
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV.
// первым номером приёмника после нулевого.
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
agcMedium, 0.5, -1, '', 0);
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
C.Pump(300);
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
// Инициализация MSHV — это ровно один запрос и ровно один ответ
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
// «Error To Initialize TCI Server»).
// Инициализация совместимого клиента — это один запрос и один ответ.
C.SendText('vfo:1,0;');
S := C.WaitText('vfo:1,0,', 1500);
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S);
Check('слайс отвечает на vfo:1,0 при инициализации', S <> '', S);
Check('слайс отдаёт свою частоту',
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
@@ -2611,7 +2609,7 @@ begin
FloatToStr(SV.TargetHz));
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231).
// Клиенты могут отбрасывать блоки с чужим receiver.
C.SendText('audio_start:1;');
C.Pump(100);
for k := 0 to 40 do
@@ -2691,13 +2689,11 @@ begin
while C.NextFrame(Op, Pay) do ;
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if
// (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288
// и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе
// Совместимый клиент шлёт TX-аудио только в ответ на маркер TX_CHRONO и
// может отбрасывать входящий блок с чужим receiver. С жёстким нулём в
// заголовке клиент, сидящий на втором слайсе
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
// без них он не отправляет ни одного блока. Ровно это и наблюдалось на
// живом железе с MSHV.
// без них он не отправляет ни одного блока.
Ctrl.FWDSPReady := True;
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
C.SendText('trx:1,true,tci;');
@@ -2717,7 +2713,7 @@ begin
end;
end;
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
Check('TX слайса: маркер назван номером приёмника',
(n > 0) and (BadRx = 0), IntToStr(BadRx));
// ★Команда, которая ничего не сделала, не должна перебивать пару