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)». Без логики и без зависимостей от «индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
контроллера — этим занимается AndromedaPanel.pas. контроллера — этим занимается AndromedaPanel.pas.
Соответствие протоколу Thetis: Таблицы соответствуют нумерации команд аппаратной панели. Для LED-индикаторов
• действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions; используются номера 1=MOX, 3=Tune, 7=CTUN и т.д.; свободные слоты 12..17
• номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis заняты функциями EWSDR (Split/NB/NR/SNB/ANF/Squelch).
(MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты
12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch).
} }
{$mode objfpc}{$H+} {$mode objfpc}{$H+}
@@ -44,18 +42,18 @@ type
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown, abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN, abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
abCTCSS, abSquelch, abFilterUp, abFilterDown, abCTCSS, abSquelch, abFilterUp, abFilterDown,
abVFOAtoB, // копировать VFO A → B (Thetis eBBVFOAtoB) abVFOAtoB, // копировать VFO A → B
abVFOBtoA, // копировать VFO B → A (Thetis eBBVFOBtoA) abVFOBtoA, // копировать VFO B → A
abVFOLock, // блокировка перестройки VFO (Thetis eBBVFOLockOnOff) abVFOLock, // блокировка перестройки VFO
abStartStop, // старт/стоп SDR (Thetis eBBStartStop) abStartStop, // старт/стоп SDR
abShift); // модификатор band-keypad (Thetis eBBShift) — обрабатывается в панели abShift); // модификатор band-keypad — обрабатывается в панели
// Параметр, управляемый поворотным энкодером панели. // Параметр, управляемый поворотным энкодером панели.
TAndEncoder = ( TAndEncoder = (
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch, aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti); aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis. // Индикатор (LED) панели в порядке протокольной нумерации.
TAndIndicator = ( TAndIndicator = (
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync, aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch, aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
@@ -67,8 +65,8 @@ const
AND_MAX_ENCODERS = 20; AND_MAX_ENCODERS = 20;
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие. // Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
// Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based // Внутренний индекс 0-based на единицу меньше номера кнопки в протоколе.
// индекс = «Pushbutton Number» Thetis 1. Проверено на реальной панели // Нумерация проверена на реальной панели
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone. // (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = ( AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
abMute, // 0 RX1 Mute abMute, // 0 RX1 Mute
@@ -121,14 +119,14 @@ const
abTune); // 47 TUN abTune); // 47 TUN
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр. // Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
// Нумерация = «Encoder Number» Thetis 1 (дефолт MakeNewAndromedaDataset). // Внутренний индекс на единицу меньше номера энкодера в протоколе.
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel. // Энкодер #1 управляет AGC Level, поэтому используется aeAGCLevel.
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2). // #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один // aeFilterWidth оставлен в перечислении для раскладок, где нужен один
// энкодер «ширина» — в дефолтной раскладке не используется. // энкодер «ширина» — в дефолтной раскладке не используется.
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = ( AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
aeAFGain, // 0 RX1 AF Gain aeAFGain, // 0 RX1 AF Gain
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF») aeAGCLevel, // 1 RX1 AGC Level
aeNone, // 2 RX2 AF — нет второго приёмника aeNone, // 2 RX2 AF — нет второго приёмника
aeNone, // 3 RX2 AGC aeNone, // 3 RX2 AGC
aeFilterHigh, // 4 Filter High — верхний край aeFilterHigh, // 4 Filter High — верхний край
@@ -142,7 +140,7 @@ const
aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone); aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone);
// Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет. // Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет.
// 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах. // 1..11 — базовая раскладка панели; 12..17 — функции EWSDR.
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = ( AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
{aiNone} 0, {aiNone} 0,
{aiMOX} 1, {aiMOX} 1,
+1 -1
View File
@@ -157,7 +157,7 @@ begin
end; end;
procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean); procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
// Раскладка зеркалит Thetis: кнопка Shift (0-based 28) — модификатор-защёлка; // Кнопка Shift (0-based 28) — модификатор-защёлка;
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую // пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда, // (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием). // shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
+9 -11
View File
@@ -20,11 +20,10 @@
unit AudioInput; unit AudioInput;
{ {
PortAudio input — TX microphone capture. PortAudio input — TX microphone capture, parallel to AudioOutput.pas.
Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas.
Key features: Key features:
- Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR) - Mono input stream, paFloat32, fixed at 48000 Hz
- Pa_Initialize called once in Initialize - Pa_Initialize called once in Initialize
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples - Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
- Callback (PaInCallback) writes mic samples into the ring buffer - Callback (PaInCallback) writes mic samples into the ring buffer
@@ -44,7 +43,7 @@ uses
const const
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback 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 type
TPaError = LongInt; TPaError = LongInt;
@@ -110,7 +109,7 @@ type
FLastError: string; FLastError: string;
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора) FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
// Ring buffer (mono double), как в piHPSDR // Ring buffer (mono double)
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double; FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
FInPt: Integer; // write pointer (callback writes) FInPt: Integer; // write pointer (callback writes)
FOutPt: Integer; // read pointer (main thread reads via ReadSample) FOutPt: Integer; // read pointer (main thread reads via ReadSample)
@@ -208,8 +207,7 @@ const
PA_CONTINUE = LongInt(0); PA_CONTINUE = LongInt(0);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер. // Callback — читает mono float32 из inputBuffer и пишет в кольцевой буфер.
// Точно по образцу pa_in_cb из piHPSDR/portaudio.c
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function PaInCallback(inputBuffer, outputBuffer: Pointer; function PaInCallback(inputBuffer, outputBuffer: Pointer;
@@ -461,7 +459,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Open — открывает input stream, как audio_open_input в piHPSDR // Open — открывает входной аудиопоток.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function TAudioInput.Open: Boolean; function TAudioInput.Open: Boolean;
@@ -480,7 +478,7 @@ begin
if not Initialize then Exit; if not Initialize then Exit;
FillChar(InParam, SizeOf(InParam), 0); FillChar(InParam, SizeOf(InParam), 0);
InParam.channelCount := 1; // Mono, как в piHPSDR InParam.channelCount := 1; // Mono input
InParam.device := FDeviceIndex; InParam.device := FDeviceIndex;
InParam.sampleFormat := PA_FLOAT32; InParam.sampleFormat := PA_FLOAT32;
@@ -533,7 +531,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Close — как audio_close_input в piHPSDR // Close — останавливает и закрывает входной аудиопоток.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TAudioInput.Close; procedure TAudioInput.Close;
@@ -556,7 +554,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ReadSample — как audio_get_next_mic_sample в piHPSDR // ReadSample — возвращает следующий микрофонный сэмпл из буфера.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function TAudioInput.ReadSample: Double; function TAudioInput.ReadSample: Double;
+13 -15
View File
@@ -20,12 +20,12 @@
unit AudioOutput; unit AudioOutput;
{ {
PortAudio output — реализация по образцу piHPSDR/portaudio.c (DL1YCF) PortAudio output with a buffered low-latency stereo stream.
Ключевые особенности: Ключевые особенности:
- Pa_Initialize вызывается один раз в Create (до открытия стрима) - Pa_Initialize вызывается один раз в Create (до открытия стрима)
- suggestedLatency = 0.0 (минимум устройства) - suggestedLatency = 0.0 (минимум устройства)
- Ring buffer из Double (как в оригинале) - Ring buffer из Double
- Callback читает по одному сэмплу, обновляет outpt внутри цикла - Callback читает по одному сэмплу, обновляет outpt внутри цикла
- Low water mark: вставляет тишину + полбуфера silence - Low water mark: вставляет тишину + полбуфера silence
- High water mark: удаляет лишние сэмплы - High water mark: удаляет лишние сэмплы
@@ -43,10 +43,10 @@ uses
Classes, SysUtils, Math, SyncObjs, DynLibs; Classes, SysUtils, Math, SyncObjs, DynLibs;
const const
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback
MY_RING_BUFFER_SIZE = 9600; // как piHPSDR MY_RING_BUFFER_SIZE = 9600;
MY_RING_LOW_WATER = 512; // как piHPSDR MY_RING_LOW_WATER = 512;
MY_RING_HIGH_WATER = 9000; // как piHPSDR MY_RING_HIGH_WATER = 9000;
type type
TPaError = LongInt; TPaError = LongInt;
@@ -119,8 +119,8 @@ type
FPAInited: Boolean; // Pa_Initialize прошла FPAInited: Boolean; // Pa_Initialize прошла
FLastError: string; FLastError: string;
// Ring buffer (interleaved double stereo, как в оригинале) // Ring buffer (interleaved double stereo)
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double; // как piHPSDR FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double;
FInPt: Integer; // audio_buffer_inpt (write) FInPt: Integer; // audio_buffer_inpt (write)
FOutPt: Integer; // audio_buffer_outpt (read) FOutPt: Integer; // audio_buffer_outpt (read)
FMutex: TCriticalSection; FMutex: TCriticalSection;
@@ -159,7 +159,7 @@ type
// Найти индекс устройства по имени (-1 если не найдено) // Найти индекс устройства по имени (-1 если не найдено)
function FindDeviceByName(const AName: string): Integer; function FindDeviceByName(const AName: string): Integer;
// Пишем стерео double сэмплы — как audio_write() в оригинале // Записывает пару стереосэмплов Double в кольцевой буфер.
procedure WriteDouble(Left, Right: Double); procedure WriteDouble(Left, Right: Double);
// Convenience: массив Single // Convenience: массив Single
procedure Write(const Left, Right: array of Single; Count: Integer); procedure Write(const Left, Right: array of Single; Count: Integer);
@@ -215,13 +215,12 @@ const
PA_CONTINUE = LongInt(0); PA_CONTINUE = LongInt(0);
PA_NO_DEV = TPaDeviceIndex(-1); 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; function PaOutCallback(inputBuffer, outputBuffer: Pointer;
framesPerBuffer: LongWord; framesPerBuffer: LongWord;
timeInfo: Pointer; timeInfo: Pointer;
statusFlags: LongWord; statusFlags: LongWord;
userData: Pointer): LongInt; cdecl; userData: Pointer): LongInt; cdecl;
// Точная копия pa_out_cb из piHPSDR/portaudio.c
var var
Audio: TAudioOutput; Audio: TAudioOutput;
Out_: PSingle; Out_: PSingle;
@@ -478,7 +477,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Open — точная последовательность как в audio_open_output() // Open — подготавливает параметры и открывает выходной поток PortAudio.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function TAudioOutput.Open: Boolean; function TAudioOutput.Open: Boolean;
@@ -505,7 +504,7 @@ begin
Exit; Exit;
end; end;
// Точно как в оригинале: bzero + suggestedLatency = 0.0 // Обнуляем параметры и запрашиваем минимальную задержку устройства.
FillChar(OutParam, SizeOf(OutParam), 0); FillChar(OutParam, SizeOf(OutParam), 0);
OutParam.channelCount := 2; OutParam.channelCount := 2;
OutParam.device := Dev; OutParam.device := Dev;
@@ -597,8 +596,7 @@ begin
end; end;
end; end;
// WriteDouble — modeled after audio_write() from piHPSDR/portaudio.c // Вызывается для каждой пары сэмплов из Write. Мьютекс держится весь цикл.
// Вызывается per-sample из Write. Мьютекс держится весь цикл в Write.
procedure TAudioOutput.WriteDouble(Left, Right: Double); procedure TAudioOutput.WriteDouble(Left, Right: Double);
var var
avail: Integer; avail: Integer;
+2 -2
View File
@@ -740,7 +740,7 @@ begin
end; end;
procedure TCATAdapter.SyncTXEQ; procedure TCATAdapter.SyncTXEQ;
// Частоты полос команда не несёт (их в формате Thetis попросту нет) — трогаем // Частоты полос команда не несёт (их в формате CAT попросту нет) — трогаем
// только усиления и число полос, сетка остаётся той, что настроена в UI. // только усиления и число полос, сетка остаётся той, что настроена в UI.
var var
T: TTXSettings; T: TTXSettings;
@@ -866,7 +866,7 @@ procedure TCATAdapter.SyncAtten;
begin FController.SetAtten(FSyncInt); end; begin FController.SetAtten(FSyncInt); end;
function TCATAdapter.GetSupplyVolts: Double; function TCATAdapter.GetSupplyVolts: Double;
// Телеметрия платы; -1 = данных ещё нет (движок отдаст ноль, как Thetis). // Телеметрия платы; -1 означает, что данных ещё нет, и наружу отдаётся ноль.
begin Result := FController.FLastSupplyV; end; begin Result := FController.FLastSupplyV; end;
procedure TCATAdapter.DoVfoSwap; procedure TCATAdapter.DoVfoSwap;
+61 -62
View File
@@ -19,10 +19,10 @@
unit CATEngine; 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 Движок поддерживает базовые команды Kenwood и расширения ZZ*. Команды, для
реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают которых в EWSDR ещё нет функционала, возвращают
заглушку и сопровождены комментарием "STUB: <причина>". заглушку и сопровождены комментарием "STUB: <причина>".
Формат команд: Формат команд:
@@ -160,7 +160,7 @@ type
CWTextBusy: TCATGetBoolFunc; CWTextBusy: TCATGetBoolFunc;
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки // Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции // (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
// (ZZCM у Thetis это «monitor DISABLE», инверсия делается в движке). // ZZCM означает «monitor DISABLE», инверсия делается в движке.
GetCWPitch: TCATGetIntFunc; // Гц GetCWPitch: TCATGetIntFunc; // Гц
SetCWPitch: TCATSetIntProc; SetCWPitch: TCATSetIntProc;
GetCWBreakIn: TCATGetBoolFunc; GetCWBreakIn: TCATGetBoolFunc;
@@ -500,7 +500,7 @@ begin if B then Result := '1' else Result := '0'; end;
function IsParamless(const Pfx: string): Boolean; function IsParamless(const Pfx: string): Boolean;
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом // Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка // Команды не принимают аргументов; любой хвост у них — ошибка
// формата, а не значение. // формата, а не значение.
const const
NoArg: array[0..9] of string = ( NoArg: array[0..9] of string = (
@@ -753,7 +753,7 @@ end;
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer; function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string; const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis — // Числовое поле фиксированной ширины: SET клампует в [Lo..Hi],
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева. // выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
var v: Integer; var v: Integer;
begin begin
@@ -769,7 +769,7 @@ begin
end; end;
function TCATEngine.StepIdxToHz(Idx: Integer): Double; function TCATEngine.StepIdxToHz(Idx: Integer): Double;
// Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц). // Шаг настройки по протокольному индексу 0..14, результат в Гц.
const Steps: array[0..14] of Double = ( const Steps: array[0..14] of Double = (
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000, 1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
10000, 100000, 250000, 500000, 1000000); 10000, 100000, 250000, 500000, 1000000);
@@ -823,8 +823,8 @@ begin
Result := ParseZZ(ext, sfx); Result := ParseZZ(ext, sfx);
end else begin end else begin
sfx := Copy(s, 3, tpos - 3); sfx := Copy(s, 3, tpos - 3);
// Команды без параметров: эталон отбраковывает лишний суффикс в парсере, // Команды без параметров должны отбраковывать лишний суффикс в парсере
// по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список // ДО вызова команды. Таблицы ширин у нас нет — держим список
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу. // здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR); if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
if pfx = 'AG' then Result := CmdAG(sfx) if pfx = 'AG' then Result := CmdAG(sfx)
@@ -933,7 +933,7 @@ begin
end; end;
function TCATEngine.CmdCT(const s: string): string; function TCATEngine.CmdCT(const s: string): string;
// CT: включение CTCSS — обёртка над ZZTA, как и в эталоне. // CT: включение CTCSS — обёртка над ZZTA.
begin begin
Result := ZZTA(s); Result := ZZTA(s);
end; end;
@@ -994,7 +994,7 @@ end;
function TCATEngine.CmdFW(const s: string): string; function TCATEngine.CmdFW(const s: string): string;
// FW: filter width (4 digits) — maps to filter index // 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; var idx: Integer;
begin begin
if Length(s) = 4 then begin if Length(s) = 4 then begin
@@ -1028,7 +1028,7 @@ begin
end; end;
function TCATEngine.CmdIF: string; function TCATEngine.CmdIF: string;
// IF: составной статус — 35 символов ответа (как у Thetis; «37 байт» из // IF: составной статус — 35 символов ответа («37 байт» из
// прежнего комментария — это вся посылка вместе с префиксом и ';'). // прежнего комментария — это вся посылка вместе с префиксом и ';').
var var
freq, step, incr, rit, xit, tx, mode, split: string; freq, step, incr, rit, xit, tx, mode, split: string;
@@ -1108,7 +1108,7 @@ end;
function TCATEngine.CmdMG(const s: string): string; function TCATEngine.CmdMG(const s: string): string;
// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в // MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ // n/1.43, дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю. // в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
var v: Integer; var v: Integer;
begin begin
@@ -1159,22 +1159,21 @@ begin
end; end;
function TCATEngine.CmdOF(const s: string): string; function TCATEngine.CmdOF(const s: string): string;
// OF: сдвиг репитера — обёртка над ZZOT, как и в эталоне. // OF: сдвиг репитера — обёртка над ZZOT.
begin begin
Result := ZZOT(s); Result := ZZOT(s);
end; end;
function TCATEngine.CmdOS(const s: string): string; function TCATEngine.CmdOS(const s: string): string;
// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне. // OS: направление сдвига репитера — обёртка над ZZOS.
begin begin
Result := ZZOS(s); Result := ZZOS(s);
end; end;
function TCATEngine.ZZOS(const s: string): string; function TCATEngine.ZZOS(const s: string): string;
// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх, // ZZOS: направление сдвига репитера. В протоколе 0=симплекс, 1=вверх,
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция. // 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection), // ★Некорректное значение трактуется как симплекс для совместимости клиентов.
// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение.
var dir: Integer; var dir: Integer;
begin begin
if Length(s) = 1 then begin if Length(s) = 1 then begin
@@ -1196,9 +1195,9 @@ begin
end; end;
function TCATEngine.ZZOT(const s: string): string; function TCATEngine.ZZOT(const s: string): string;
// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью // ZZOT: величина сдвига репитера, 9 цифр. Поле кодирует МГц с шестью знаками
// знаками после запятой (вставляет точку после третьего разряда) — численно это // после запятой, что численно соответствует целому значению в герцах. Знак не
// ровно те же герцы, что и у нас. Знак не передаётся, направление в ZZOS. // передаётся, направление задаётся в ZZOS.
var v: Int64; var v: Int64;
begin begin
if Length(s) = 9 then begin if Length(s) = 9 then begin
@@ -1228,7 +1227,7 @@ begin
end; end;
function TCATEngine.CmdPR(const s: string): string; function TCATEngine.CmdPR(const s: string): string;
// PR: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0'); // PR: речевой компрессор вкл/выкл. Базовая команда доступна только для чтения;
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх, // у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
// кенвудовские клиенты от него не ломаются. // кенвудовские клиенты от него не ломаются.
begin begin
@@ -1311,7 +1310,7 @@ end;
function TCATEngine.CmdSM(const s: string): string; function TCATEngine.CmdSM(const s: string): string;
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр // SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ). // (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все: // Совместимые клиенты присылают селектор '0', '1' или '2'; принимаем все:
// второго приёмника нет, отвечаем одним и тем же. // второго приёмника нет, отвечаем одним и тем же.
var sm: Integer; var sm: Integer;
begin begin
@@ -1401,7 +1400,7 @@ begin
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
else if ext = 'CB' then ans := ZZCB(s) else if ext = 'CB' then ans := ZZCB(s)
else if ext = 'CD' then ans := ZZCD(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 = 'CI' then ans := ZZCI(s)
else if ext = 'CL' then ans := ZZCL(s) else if ext = 'CL' then ans := ZZCL(s)
else if ext = 'CM' then ans := ZZCM(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 = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
else if ext = 'FS' 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 = 'FT' then ans := ZZFT(s)
else if ext = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет else if ext = 'FV' then ans := '' // STUB: чтение вендорской шины (1 байт)
else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет else if ext = 'FW' then ans := '' // STUB: чтение вендорской шины (2 байта)
else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет else if ext = 'FX' then ans := '' // STUB: запись вендорской шины (1 байт)
else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет else if ext = 'FY' then ans := '' // STUB: запись вендорской шины (2 байта)
else if ext = 'GA' then ans := ZZGA(s) else if ext = 'GA' then ans := ZZGA(s)
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
else if ext = 'GL' 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 = 'IS' then ans := ZZIS(s)
else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH) else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis else if ext = 'JO' then ans := '' // STUB: команда не описана
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда
else if ext = 'KM' then ans := ZZKM(s) 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 = 'KS' then ans := ZZKS(s)
else if ext = 'KY' then ans := ZZKY(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 = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
else if ext = 'LD' 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 = 'MA' then ans := ZZMA(s)
else if ext = 'MB' then ans := ZZMB(s) else if ext = 'MB' then ans := ZZMB(s)
else if ext = 'MD' then ans := ZZMD(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 = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
else if ext = 'MG' then ans := ZZMG(s) else if ext = 'MG' then ans := ZZMG(s)
else if ext = 'ML' then ans := ZZML else if ext = 'ML' then ans := ZZML
@@ -1528,7 +1527,7 @@ begin
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
else if ext = 'OH' 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 = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
else if ext = 'OS' then ans := ZZOS(s) else if ext = 'OS' then ans := ZZOS(s)
else if ext = 'OT' then ans := ZZOT(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 = 'PS' then ans := ZZPS(s)
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
else if ext = 'PZ' 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. // (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
else if ext = 'QM' then ans := ZZQM else if ext = 'QM' then ans := ZZQM
@@ -1580,7 +1579,7 @@ begin
else if ext = 'SP' then ans := ZZSP(s) else if ext = 'SP' then ans := ZZSP(s)
else if ext = 'SQ' then ans := ZZSQ(s) else if ext = 'SQ' then ans := ZZSQ(s)
else if ext = 'SR' then ans := ZZSR(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 = 'ST' then ans := ZZST(s)
else if ext = 'SU' then begin ZZSU; ans := ''; end else if ext = 'SU' then begin ZZSU; ans := ''; end
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет 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 = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
else if ext = 'TA' then ans := ZZTA(s) else if ext = 'TA' then ans := ZZTA(s)
else if ext = 'TB' then ans := ZZTB(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 = 'TH' then ans := ZZTH(s)
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер) else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
else if ext = 'TL' then ans := ZZTL(s) else if ext = 'TL' then ans := ZZTL(s)
else if ext = 'TM' then ans := ZZTM(s) else if ext = 'TM' then ans := ZZTM(s)
else if ext = 'TO' then ans := ZZTO(s) else if ext = 'TO' then ans := ZZTO(s)
else if ext = 'TP' then ans := ZZTP(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 = 'TU' then ans := ZZTU(s)
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2 else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
else if ext = 'TX' then ans := ZZTX(s) 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 = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
else if ext = 'VU' 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 = '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 = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
else if ext = 'VZ' 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 = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере 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 = 'XA' then ans := ZZXA(s)
else if ext = 'XC' then ans := ZZXC else if ext = 'XC' then ans := ZZXC
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
@@ -1846,7 +1845,7 @@ begin
end; end;
function TCATEngine.ZZBS(const s: string): string; function TCATEngine.ZZBS(const s: string): string;
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и // ZZBS: выбор диапазона. В EWSDR поле двузначное, а базовый формат использует
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке // несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты. // диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0, // ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
@@ -1877,10 +1876,10 @@ begin
end; end;
function TCATEngine.ZZCD(const s: string): string; function TCATEngine.ZZCD(const s: string): string;
// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает // ZZCD: задержка отпускания break-in (hang time), мс. В EWSDR потолок 2000 —
// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же // это намеренный предел: столько же
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific. // стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры. // Значение выше подрезается по общему правилу, формат — те же 4 цифры.
begin begin
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime); Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
end; end;
@@ -1948,7 +1947,7 @@ begin
end; end;
function TCATEngine.ZZEB(const s: string): string; function TCATEngine.ZZEB(const s: string): string;
// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis). // ZZEB: значения TX-эквалайзера одной строкой в протокольном формате.
// nnn + (nnn){nb+1}, добито нулями до 36 символов; // nnn + (nnn){nb+1}, добито нулями до 36 символов;
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'. // nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
var var
@@ -2146,7 +2145,7 @@ end;
function TCATEngine.ZZKM(const s: string): string; function TCATEngine.ZZKM(const s: string): string;
// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений // ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений
// (окно CW Messages, F1..F8). Write-only, как в Thetis. // (окно CW Messages, F1..F8). Команда доступна только для записи.
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет, // Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
// иамбик вкл/выкл доступен через ZZCI. // иамбик вкл/выкл доступен через ZZCI.
var v: Integer; var v: Integer;
@@ -2198,7 +2197,7 @@ begin
end; end;
function TCATEngine.ZZMG(const s: string): string; function TCATEngine.ZZMG(const s: string): string;
// ZZMG: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две // ZZMG: усиление микрофона в дБ. SET — три цифры либо знак+две
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный // ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit. // TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
var v: Integer; var v: Integer;
@@ -2206,7 +2205,7 @@ begin
if Length(s) = 0 then if Length(s) = 0 then
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2)); Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно // Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
// как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не // Допустимы две протокольные длины поля. Более короткие поля — ошибка формата, а не
// «маленькое значение»: молча крутить усиление по ним нельзя. // «маленькое значение»: молча крутить усиление по ним нельзя.
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR); if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
if not TryStrToInt(s, v) 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; begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
function TCATEngine.ZZMN(const s: string): string; function TCATEngine.ZZMN(const s: string): string;
// ZZMN: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты // ZZMN: имя режима по номеру. В базовом формате ZZMN отдаёт пресеты
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список // DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR. // режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос // Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
@@ -2441,12 +2440,12 @@ begin
end; end;
function TCATEngine.ZZRV: string; function TCATEngine.ZZRV: string;
// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа). // ZZRV: напряжение питания PA, формат «NN.N» (4 символа).
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.) // (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
var v: Double; var v: Double;
begin begin
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1; 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; if v > 99.9 then v := 99.9;
Result := Format('%04.1f', [v]); Result := Format('%04.1f', [v]);
end; end;
@@ -2459,7 +2458,7 @@ begin
end; end;
function TCATEngine.ZZSA: string; function TCATEngine.ZZSA: string;
// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO // ZZSA: шаг настройки вниз. В EWSDR шагает активный VFO,
// (отдельного «шага VFO A» в контроллере нет). // (отдельного «шага VFO A» в контроллере нет).
begin begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
@@ -2652,8 +2651,8 @@ begin
Result := ''; Result := '';
end; end;
// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) — // ZZWA..ZZWS: неподдерживаемые команды внешнего микшера — STUB.
// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его // Водопад тут ни при чём: его
// уровни живут в ZZDN/ZZDO. // уровни живут в ZZDN/ZZDO.
function TCATEngine.ZZWA(const s: string): string; function TCATEngine.ZZWA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end; 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; begin if Length(s) > 0 then Result := '' else Result := '0'; end;
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние // ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на // служебные команды; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split. // диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
// Всё — STUB: либо чужое железо, либо в CAT не проброшено. // Всё — STUB: либо чужое железо, либо в CAT не проброшено.
function TCATEngine.ZZZA(const s: string): string; function TCATEngine.ZZZA(const s: string): string;
@@ -2732,7 +2731,7 @@ begin
end; end;
function TCATEngine.ZZZE(const s: string): string; function TCATEngine.ZZZE(const s: string): string;
// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры номер // ZZZE: Andromeda — поворот энкодера. Верхние 2 цифры кодируют номер
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага. // энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
var raw, enc, step: Integer; var raw, enc, step: Integer;
begin begin
@@ -2797,7 +2796,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZW(const s: string): string; function TCATEngine.ZZZW(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end; begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZZ: string; function TCATEngine.ZZZZ: string;
// ZZZZ: без описания у Thetis — STUB. // ZZZZ: команда не описана — STUB.
begin Result := ''; end; begin Result := ''; end;
function TCATEngine.ZZZO(const s: string): string; function TCATEngine.ZZZO(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end; 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; function TCATEngine.ZZCI(const s: string): string;
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный // ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по // (прямой ключ / iambic A / iambic B) — включение выбирает B по
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется. // умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2; const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
begin begin
@@ -2827,7 +2826,7 @@ begin
end; end;
function TCATEngine.ZZCM(const s: string): string; function TCATEngine.ZZCM(const s: string): string;
// ZZCM: у Thetis это «CW Monitor DISABLE» значение инвертировано к сайдтону. // ZZCM означает «CW Monitor DISABLE», значение инвертировано к сайдтону.
begin begin
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor))); if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); 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 19091 (слайс B)
// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) … // TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
// //
// Эталон поведения — SmartSDR CAT: порт привязан к слайсу, а флаг Auto TX // Порт привязан к слайсу, а флаг Auto TX разрешает команде TX сделать его
// («Auto Switch TX Slice») разрешает команде TX самой сделать этот слайс
// источником передачи. Вся защита «в эфире одновременно только один слайс» // источником передачи. Вся защита «в эфире одновременно только один слайс»
// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера). // живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
// //
+1 -1
View File
@@ -19,7 +19,7 @@
unit CATTcp; unit CATTcp;
{ {
CATTcp.pas — TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis). CATTcp.pas — многопоточный TCP-сервер команд CAT.
Архитектура: Архитектура:
• Один listening socket (TCATTcpServer) • Один listening socket (TCATTcpServer)
+5 -5
View File
@@ -29,8 +29,8 @@ unit CWKeyer;
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/ // джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы // LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь. // сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
// Эталон — pihpsdr: src/iambic.c (автомат) + cw_shape_buffer в transmitter.c // Автомат формирует элементы и записывает огибающую прямо в TX-буфер,
// (огибающая прямо в TX-буфере, мимо WDSP). // минуя WDSP.
// //
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP // ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей. // режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
@@ -228,8 +228,8 @@ type
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object; TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ, { Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
CTS/DSR/DCD/RI читаются как лепестки. Так же делают Thetis (cwkeyer.cs), CTS/DSR/DCD/RI читаются как лепестки. Такой вход доступен на любом железе
hamlib и fldigi — это единственный вход ключа, доступный на любом железе. } через стандартный последовательный интерфейс. }
TCWKeyPort = class(TThread) TCWKeyPort = class(TThread)
private private
FLock: TCriticalSection; FLock: TCriticalSection;
@@ -780,7 +780,7 @@ begin
end; end;
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean; function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
// Автомат иамбика (логика pihpsdr src/iambic.c в терминах элементов): // Автомат иамбика в терминах точек, тире и межэлементных пауз:
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире); // * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается // * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
// следующим; режим A памяти не имеет (в этом и вся разница); // следующим; режим A памяти не имеет (в этом и вся разница);
+1 -1
View File
@@ -24,7 +24,7 @@ unit CWMessagesForm;
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой «отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV), панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
так что окно висит рядом ровно как окно CWX у Thetis. Те же ячейки поэтому окно располагается рядом. Используются те же ячейки
привязаны к F1..F8 в главном окне. привязаны к F1..F8 в главном окне.
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить» Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
+1 -1
View File
@@ -30,7 +30,7 @@ unit CWMorse;
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по // * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА // нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
// нажать и отпустить, решает PC. Именно этот путь используется для // нажать и отпустить, решает PC. Именно этот путь используется для
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs). // передачи текста через макросы и команду CAT KY.
// //
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и // Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка // дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
+3 -3
View File
@@ -27,7 +27,7 @@ unit DMRDecoder;
sync detection run in TDMRDecoderThread and can never stall the WDSP thread. sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
This first layer deliberately stops at burst sync. The following layer will 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} {$IFDEF FPC}
@@ -203,7 +203,7 @@ const
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000; DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3; 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. // ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
SYNC_TEXT: array[0..7] of string = ( SYNC_TEXT: array[0..7] of string = (
'313333111331131131331131', // BS data '313333111331131131331131', // BS data
@@ -512,7 +512,7 @@ begin
if FHistoryCount[Phase] >= 24 then if FHistoryCount[Phase] >= 24 then
begin begin
Distance := PopCount24(FSyncShift[Phase] xor Pat); 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 // likewise uses a bounded sync tolerance; requiring an exact 24-symbol
// match lost almost every superframe in recorded handheld-radio IQ. // match lost almost every superframe in recorded handheld-radio IQ.
DeltaSamples := FSampleNo - FLastSyncSample; DeltaSamples := FSampleNo - FLastSyncSample;
+2 -2
View File
@@ -22,8 +22,8 @@ unit DMRProtocol;
{ {
Small, allocation-free DMR burst protocol layer. Small, allocation-free DMR burst protocol layer.
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from Implements CACH/AMBE interleavers and Hamming/Golay matrices.
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h). The applicable third-party permissive notice follows.
Copyright (C) 2010 DSD Author Copyright (C) 2010 DSD Author
Permission to use, copy, modify, and/or distribute this software for any 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; MAX_FREQ = 20000.0;
TOP_H = 44; TOP_H = 44;
BOTTOM_H = 82; BOTTOM_H = 82;
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под // Кривая соответствует расчёту частотной характеристики WDSP под
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц), // геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) — // eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов // SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
@@ -357,7 +357,7 @@ begin
end; end;
procedure TEqualizerControl.BuildResponse; procedure TEqualizerControl.BuildResponse;
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G — // Рассчитывает амплитудную характеристику эквалайзера в дБ без построения FIR. Точки F/G —
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и // узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ // выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как // бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
+3 -3
View File
@@ -30,8 +30,8 @@ unit FilterPopup;
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
длиной FIR (fircore) и никаким пользовательским параметром не управляется. длиной FIR (fircore) и никаким пользовательским параметром не управляется.
Эталон содержимого Thetis FilterForm.cs (12 слотов + Name/Low/High/Width); Содержимое включает 12 слотов и поля Name/Low/High/Width, а график наглядно
графика полосы наша добавка, у них её нет. показывает положение и форму полосы.
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно)
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
@@ -374,7 +374,7 @@ begin
end; end;
procedure TFilterPopup.WidthChange(Sender: TObject); procedure TFilterPopup.WidthChange(Sender: TObject);
// Width производная: держим центр полосы, разъезжаются оба края (как Thetis). // Width производная: центр полосы сохраняется, изменяются оба края.
var W, C, Lo, Hi: Integer; var W, C, Lo, Hi: Integer;
begin begin
if FLoading then Exit; if FLoading then Exit;
+13 -13
View File
@@ -498,7 +498,7 @@ procedure TDUCIQSenderThread.Execute;
const const
DUC_IQ_PAIRS_PER_PACKET = 240; DUC_IQ_PAIRS_PER_PACKET = 240;
DUC_SAMPLE_RATE = 192000; // IQ pairs per second DUC_SAMPLE_RATE = 192000; // IQ pairs per second
// Пейсинг DUC-FIFO радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread). // Пейсинг DUC-FIFO синхронизирован с аппаратным тактированием радио.
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце // Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп // радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен // расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
@@ -611,7 +611,7 @@ begin
DrainVirtualFIFO; DrainVirtualFIFO;
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо // Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как // в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём // При пустой очереди ждём сигнала о появлении данных.
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка // следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной. // pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме // На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
@@ -668,7 +668,7 @@ begin
Sleep(50); Sleep(50);
if Terminated then Break; if Terminated then Break;
if not FNet.FConnected then Continue; if not FNet.FConnected then Continue;
// Отправляем полный HP с частотами и ALEX — как piHPSDR // Отправляем полный HP-пакет с частотами и состоянием ALEX.
FNet.SendFullHP; FNet.SendFullHP;
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс). // Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют // ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
@@ -1351,14 +1351,14 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ALEX фильтры — логика из piHPSDR new_protocol.c // Формирование регистра фильтров ALEX.
// Возвращает 32-bit ALEX0 register value // Возвращает 32-bit ALEX0 register value
// RXFreqHz — частота приёма ADC0 // RXFreqHz — частота приёма ADC0
// TXFreqHz — частота передачи (или = RXFreqHz при RX) // TXFreqHz — частота передачи (или = RXFreqHz при RX)
// Transmitting — режим передачи // Transmitting — режим передачи
// IsOrion2 — ANAN-7000/8000 (другие BPF) // IsOrion2 — ANAN-7000/8000 (другие BPF)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Преобразует частоту в индекс диапазона 0=160m..10=6m (Thetis-compatible). // Преобразует частоту в протокольный индекс диапазона 0=160m..10=6m.
function FreqToBandIndex(FreqHz: Double): Integer; function FreqToBandIndex(FreqHz: Double): Integer;
begin begin
if FreqHz >= 39850000 then Result := 10 // 6m if FreqHz >= 39850000 then Result := 10 // 6m
@@ -1386,7 +1386,7 @@ end;
// XVTR без усиления. // XVTR без усиления.
// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11 // XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11
// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3. // (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3.
// WidebandBypass — True когда открыт raw ADC wideband. Как Thetis, раскрываем // WidebandBypass — True, когда открыт raw ADC wideband. В этом режиме раскрываем
// RX front-end через HF bypass, иначе видна только полоса // RX front-end через HF bypass, иначе видна только полоса
// текущего HPF/BPF, а не весь ADC span. // текущего HPF/BPF, а не весь ADC span.
function CalcAlex0(RXFreqHz, TXFreqHz: Double; function CalcAlex0(RXFreqHz, TXFreqHz: Double;
@@ -1411,7 +1411,7 @@ begin
if not XvtrDisablePA then if not XvtrDisablePA then
begin begin
Result := Result or $08000000; // bit 27: T/R relay Result := Result or $08000000; // bit 27: T/R relay
// Bit 18 = PureSignal (ALEX_PS_BIT в pihpsdr): на TX подключает // Bit 18 = PureSignal: на TX подключает
// внутренний мост обратной связи PA к RX-тракту ADC0. // внутренний мост обратной связи PA к RX-тракту ADC0.
if PureSignalTX then if PureSignalTX then
Result := Result or $00040000; Result := Result or $00040000;
@@ -1604,8 +1604,8 @@ end;
// Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) — // Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) —
// в тех же координатах, в которых маски лежат в настройках и рисуются в UI. // в тех же координатах, в которых маски лежат в настройках и рисуются в UI.
// Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP). // Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP).
// Портировано из Thetis (Penny.UpdateExtCtrl/adjustForTXAction), урезано: // Реализация учитывает только возможности EWSDR: без PA-override и
// без PA-override и split-pins VFO A/B (см. project_oc_control память). // раздельных пинов VFO A/B.
// RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый // RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый
// пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их // пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их
// комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON // комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON
@@ -1735,7 +1735,7 @@ begin
if FPanDDCEnabled[ddc] then if FPanDDCEnabled[ddc] then
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]); 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, // PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback,
// оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) — // оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) —
// его сэмплы приходят interleaved внутри потока DDC0 (порт 1035). // его сэмплы приходят interleaved внутри потока DDC0 (порт 1035).
@@ -1866,7 +1866,7 @@ end;
procedure THPSDRNetwork.SetCWXKey(Down: Boolean); procedure THPSDRNetwork.SetCWXKey(Down: Boolean);
// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка // Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка
// доставки = один UDP-пакет (так же делает Thetis, cwx.cs setkey). // доставки определяется отправкой одного UDP-пакета.
begin begin
if FCWXKey = Down then Exit; if FCWXKey = Down then Exit;
FCWXKey := Down; FCWXKey := Down;
@@ -1945,7 +1945,7 @@ begin
Buf[12 + ddc*4] := Ph and $FF; Buf[12 + ddc*4] := Ph and $FF;
end; end;
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой (pihpsdr, bytes 9..16). // PureSignal: feedback-DDC0/DDC1 следят за TX-частотой из байтов 9..16.
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся. // DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
if FPSTXActive then if FPSTXActive then
begin begin
@@ -2000,7 +2000,7 @@ begin
end; end;
// XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы. // XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы.
// Логика как в Thetis: // Правила установки бита:
// в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен) // в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен)
// в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab) // в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab)
if FXvtrEnable then 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) // Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards. // 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; function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double; 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) // Convert raw ADC value to RF power in Watts (ANAN-10)
function ADCToWatts10(RawADC: Word): Double; function ADCToWatts10(RawADC: Word): Double;
// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/ // Alex bridge FWD/REV detector → Watts; параметры зависят от модели трансивера:
// computeRefPower по модели трансивера:
// 0 = ANAN-10/100/100B/100D (Vref 3.3V) // 0 = ANAN-10/100/100B/100D (Vref 3.3V)
// 1 = ANAN-200D / Orion (Vref 5.0V) // 1 = ANAN-200D / Orion (Vref 5.0V)
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya // 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
@@ -482,7 +481,7 @@ end;
function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double; function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double;
begin begin
// Protocol Appendix A gives the ADC reference voltage only. // 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. // Board 0 (Atlas): no supply voltage measurement.
// Boards 1,2,3 (Hermes/Angelia): Vref=3.3V, PCB divider R1=4.7kΩ/R2=0.82kΩ. // 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. // 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; VoffMV: Double; SensMVA: Double): Double;
begin begin
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000. // Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
// Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А // Formula: amps = (mV - Voff) / Sens. Defaults: Voff=360 mV,
// (Thetis, ACS712-style); переопределяются калибровкой. // Sens=120 mV/A; both values can be overridden by calibration.
if (BoardType = 5) and (SensMVA > 0) then if (BoardType = 5) and (SensMVA > 0) then
begin begin
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA; Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
@@ -545,7 +544,6 @@ var
Bridge, Vref, V: Double; Bridge, Vref, V: Double;
CalOff: Integer; CalOff: Integer;
begin begin
// Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower).
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge. // W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
case DetModel of case DetModel of
1: begin // ANAN-200D / Orion 1: begin // ANAN-200D / Orion
+11 -13
View File
@@ -266,7 +266,7 @@ type
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
// --- CAT --- // --- CAT ---
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст) FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
FTCIAdapter: TTCIAdapter; // адаптер TCI (WebSocket-сервер Expert Electronics) FTCIAdapter: TTCIAdapter; // адаптер TCI для WebSocket-сервера
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist) FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер) FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён: // Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
@@ -749,7 +749,7 @@ type
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6) procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark) function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // Flex-style смена банда пана procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // смена диапазона отдельного пана
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto) function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4) procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
// Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней. // Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней.
@@ -4027,7 +4027,7 @@ begin
end; end;
end; end;
// Панорама и водопад теперь рисуются независимо по факту новых кадров. // Панорама и водопад теперь рисуются независимо по факту новых кадров.
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. // Маленький FFT даёт новые строки чаще.
if FSpectrumDirty then if FSpectrumDirty then
begin begin
FSpecView.DrawSpectrum; FSpecView.DrawSpectrum;
@@ -4986,12 +4986,11 @@ end;
// CTUN ВЫКЛ (классический режим): // CTUN ВЫКЛ (классический режим):
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре // FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
// //
// CTUN ВКЛ (piHPSDR/Thetis режим): // CTUN ВКЛ:
// FController.FCenterFreq статичен — спектр/водопад не двигаются // FController.FCenterFreq статичен — спектр/водопад не двигаются
// FController.FVfoA движется по дисплею (маркер гуляет) // FController.FVfoA движется по дисплею (маркер гуляет)
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея // Перецентрирование — только когда край полосы фильтра выходит за границу
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband // дисплея.
// hits the edge of the display")
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TMainForm.ApplyVfoA(NewFreq: Int64); procedure TMainForm.ApplyVfoA(NewFreq: Int64);
begin begin
@@ -5067,7 +5066,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// DUP toggle (Thetis-style local display mode — без аппаратной команды) // DUP toggle — локальный режим дисплея без аппаратной команды.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TMainForm.ApplyDUP(Active: Boolean); procedure TMainForm.ApplyDUP(Active: Boolean);
begin begin
@@ -5401,7 +5400,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive // TUN — тон WDSP TXA PostGen + MOX с отдельным уровнем Drive.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TMainForm.ApplyTUN(Active: Boolean); procedure TMainForm.ApplyTUN(Active: Boolean);
begin begin
@@ -6095,8 +6094,7 @@ end;
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton; procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer); Shift: TShiftState; X, Y: Integer);
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis: // ПКМ по кнопке фильтра открывает редактор на этом слоте. ЛКМ не трогаем —
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
// иначе окно вылезало бы при каждом обычном переключении фильтра. // иначе окно вылезало бы при каждом обычном переключении фильтра.
begin begin
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag); if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
@@ -7185,7 +7183,7 @@ begin
end; end;
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
// Flex-style смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего // Смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
// band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC // band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC
// (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы). // (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы).
var var
@@ -8904,7 +8902,7 @@ end;
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его // Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
// настроек. Управляет XVTR Enable bit, T/R relay suppression. // настроек. Управляет XVTR Enable bit, T/R relay suppression.
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex // 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; procedure TMainForm.ApplyXvtrToNetwork;
begin begin
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI. // Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
+2 -2
View File
@@ -217,7 +217,7 @@ const
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s) RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData) PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE) TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
// Размер TX-буфера масштабируется от sample-rate (как SoapyPlutoSDR): // Размер TX-буфера масштабируется от sample-rate:
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex // round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN. // выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k) TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
@@ -387,7 +387,7 @@ begin
end; end;
function TxBufPairsForRate(Rate: Integer): Integer; 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]. // округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
// 576k→16384, 1536k→32768, 3072k→65536. // 576k→16384, 1536k→32768, 3072k→65536.
var var
+2 -2
View File
@@ -116,7 +116,7 @@ const
CLR_INPUT = TColor($00FFFFFF); CLR_INPUT = TColor($00FFFFFF);
CLR_INPUT_TEXT = TColor($00202020); CLR_INPUT_TEXT = TColor($00202020);
// Лампа feedback — пороги от цели PSFBTarget (Thetis-Enhanced eu2av): // Лампа feedback — пороги относительно цели PSFBTarget:
// >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет // >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет
// сигнала. TColor = BGR. // сигнала. TColor = BGR.
CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал) CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал)
@@ -390,7 +390,7 @@ begin
end; end;
procedure TPureSignalPopup.DefPkClick(Sender: TObject); procedure TPureSignalPopup.DefPkClick(Sender: TObject);
// Дефолт SetPk для протокола 2 (Thetis clsHardwareSpecific.PSDefaultPeak). // Значение SetPk по умолчанию для протокола 2.
begin begin
FSePk.Value := 0.2899; // OnChange → Apply FSePk.Value := 0.2899; // OnChange → Apply
end; end;
+1 -1
View File
@@ -192,7 +192,7 @@ type
procedure ClearDUCIQQueue; virtual; procedure ClearDUCIQQueue; virtual;
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи — // Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска // радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
// первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto // первых миллисекунд). База/Pluto — no-op: у Pluto
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями. // TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
procedure PrimeDUCIQ(Pairs: Integer); virtual; procedure PrimeDUCIQ(Pairs: Integer); virtual;
+42 -45
View File
@@ -428,7 +428,7 @@ type
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS) FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
FPSSingleCal: Boolean; // запрошена одиночная калибровка FPSSingleCal: Boolean; // запрошена одиночная калибровка
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF) FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick) FPSCmdState: Integer; // состояние автомата команд PureSignal
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore) FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт. FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
FPSAASaveSingle: Integer; FPSAASaveSingle: Integer;
@@ -497,7 +497,7 @@ type
FSpecRefLevel: Double; FSpecRefLevel: Double;
FSpecRange: Double; FSpecRange: Double;
FSpecGridStep: Double; FSpecGridStep: Double;
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting FTXSpecRefLevel: Double; // опорный уровень TX-сетки при FTransmitting
FTXSpecRange: Double; FTXSpecRange: Double;
FTXSpecGridStep: Double; FTXSpecGridStep: Double;
@@ -718,7 +718,7 @@ type
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима. // FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
procedure ApplyModeFilter; procedure ApplyModeFilter;
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния // Вычисляет аппаратный drive-байт из текущего состояния мощности.
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием. // (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte; function CalcDriveByte: Byte;
@@ -1152,8 +1152,8 @@ type
function CWSettings: TCWSettings; function CWSettings: TCWSettings;
procedure SetCWSettings(const C: TCWSettings); procedure SetCWSettings(const C: TCWSettings);
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит // Программная передача текста: PC сам выдерживает тайминги и дёргает бит
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон // CWX в High Priority — точки/тире рисует прошивка по этому биту.
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание // Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
// манипулятора или снятие MOX обрывает передачу (CWXAbort). // манипулятора или снятие MOX обрывает передачу (CWXAbort).
procedure CWXSend(const Text: string); procedure CWXSend(const Text: string);
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной // Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
@@ -1192,8 +1192,8 @@ type
procedure SetPureSignal(On_: Boolean); procedure SetPureSignal(On_: Boolean);
procedure PSSingleCal; // одиночная калибровка procedure PSSingleCal; // одиночная калибровка
procedure PSReset; // выключить и сбросить коррекцию procedure PSReset; // выключить и сбросить коррекцию
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis // Тик PS (~10 Гц, из UI-таймера): автомат команд, опрос GetPSInfo и
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX. // автоматическая регулировка AttOnTX по уровню обратной связи.
procedure PureSignalTick; procedure PureSignalTick;
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно. // Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
@@ -1202,7 +1202,7 @@ type
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по // Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC, // плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis). // замеры на Anvelina PRO3; для остальных используется значение 152.
function PSFBTarget: Integer; function PSFBTarget: Integer;
function PSCalCount: Integer; // info[5] function PSCalCount: Integer; // info[5]
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
@@ -1337,8 +1337,8 @@ const
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel. // (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
TUN_TONE_MAG = 0.83; TUN_TONE_MAG = 0.83;
// Состояния командной state machine PureSignal (порт Thetis eCMDState; // Состояния автомата команд PureSignal. Восстановление сохранённой
// Restore-ветка (7) не реализована — Save/Restore коррекции нет). // коррекции не реализовано.
PS_ST_OFF = 0; PS_ST_OFF = 0;
PS_ST_TURNON_AUTO = 1; PS_ST_TURNON_AUTO = 1;
PS_ST_AUTOCAL = 2; PS_ST_AUTOCAL = 2;
@@ -1827,7 +1827,7 @@ begin
SupplyV := -1.0; SupplyV := -1.0;
SupplyA := -1.0; SupplyA := -1.0;
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis). // Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1.
// Прочие платы — в SupplyVolts-байтах, без тока. // Прочие платы — в SupplyVolts-байтах, без тока.
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
begin begin
@@ -1849,7 +1849,7 @@ begin
end; end;
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band // Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит // множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
// (как Thetis PABandCalMult). // и используется только для калибровки показаний мощности.
Bi := TxBandIdx; Bi := TxBandIdx;
Is6m := Bi = BAND_6M; Is6m := Bi = BAND_6M;
DetK := 1.0; DetK := 1.0;
@@ -1859,7 +1859,7 @@ begin
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK; RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic). // FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой // SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW); только при значимой
// fwd-мощности. // fwd-мощности.
if IsTx and (FwdW >= SWR_MIN_FWD_W) then if IsTx and (FwdW >= SWR_MIN_FWD_W) then
begin begin
@@ -1884,8 +1884,8 @@ begin
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0; FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload; FHPpOverload := Status.ADCOverload;
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток). // Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit» // Касание обрывает программную передачу, чтобы оператор мог вклиниться.
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по // ★Обрываем по
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять // ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе. // в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
KeyDot := (Status.StatusBits and HPS_DOT) <> 0; KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
@@ -2989,7 +2989,7 @@ function TRadioController.FrontEndCalDB: Double;
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой // Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
// или само и не должно двигать абсолютную шкалу dBm. // или само и не должно двигать абсолютную шкалу dBm.
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в // openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
// показания (так же делают Thetis и piHPSDR). // показания.
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит // AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
// его само) → вычитаем текущее значение. // его само) → вычитаем текущее значение.
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами // Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
@@ -3356,8 +3356,8 @@ end;
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean; procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
Tune: Boolean = False); Tune: Boolean = False);
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог // PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. При
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится // включённом Auto TX слайс сам становится
// TX-источником, без него — передаёт, только если оператор уже выбрал его // TX-источником, без него — передаёт, только если оператор уже выбрал его
// источником (бейдж TX на флаге). // источником (бейдж TX на флаге).
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому // Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
@@ -4049,7 +4049,7 @@ begin
Hi := FFilterHi; Hi := FFilterHi;
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её // Только RX. TX-полоса НЕ следует за RX-фильтром — её
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain // единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал. // через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
FDSPEngine.SetFilter(Lo, Hi); FDSPEngine.SetFilter(Lo, Hi);
@@ -4148,7 +4148,7 @@ end;
// ===================================================================== // =====================================================================
function TRadioController.CalcDriveByte: Byte; function TRadioController.CalcDriveByte: Byte;
// Thetis-совместимая формула: // Преобразование целевой мощности в аппаратный байт drive:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) // drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 ) // target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower // 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 Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode); if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) --- // --- Фильтр: санитизация FM до применения к DSP ----------------------
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров // Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины — // отдельно по режимам. Иначе у VAR было бы два источника истины —
// ровно та болезнь, от которой уходили. // ровно та болезнь, от которой уходили.
ApplyFilterSlot(B.FilterIdx); ApplyFilterSlot(B.FilterIdx);
ApplyModeFilter; ApplyModeFilter;
@@ -5814,7 +5814,7 @@ end;
function TRadioController.CWTXActive: Boolean; function TRadioController.CWTXActive: Boolean;
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и // «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом // манипуляция кому-то поручена (прошивке или локальному генератору). В этом
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570), // состоянии голосовой TXA не запускается вовсе,
// а несущую даёт кейер / бит CWX / тон TUN. // а несущую даёт кейер / бит CWX / тон TUN.
begin begin
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU]) Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
@@ -5851,10 +5851,9 @@ begin
end; end;
function TRadioController.CWOptionsByte: Byte; function TRadioController.CWOptionsByte: Byte;
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же // Формирует DUC Specific byte 5; iambic-бит выставляется и для Mode B.
// объяснение, почему iambic-бит выставляется и для Mode B).
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT // ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
// на замыкание ключа посреди SSB (Thetis, console.cs:27394). // на замыкание ключа посреди SSB.
begin begin
Result := 0; Result := 0;
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию // ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
@@ -5913,10 +5912,10 @@ begin
DUCPkt.DUC0RateLo := Lo(RateKsps); DUCPkt.DUC0RateLo := Lo(RateKsps);
DUCPkt.DUC0Bits := 24; DUCPkt.DUC0Bits := 24;
DUCPkt.MicLineSelect := BuildMicLineSelectByte; 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. // При Mic In значение игнорируется железом, но шлём корректное для Line In.
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31)); 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-байта: на однопотоковых платах активен только // Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2. // ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31)); DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
@@ -6269,7 +6268,7 @@ end;
procedure TRadioController.ApplyTXSettingsToDSP; procedure TRadioController.ApplyTXSettingsToDSP;
begin begin
if not FWDSPReady then Exit; if not FWDSPReady then Exit;
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2: // Для openHPSDR Protocol 2 CFIR постоянно включён:
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не // он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
// имеет, поэтому при смене бэкенда явно выключаем его. // имеет, поэтому при смене бэкенда явно выключаем его.
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR); FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
@@ -6418,8 +6417,8 @@ begin
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode); if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc). // PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
FDSPEngine.PSSetMox(FTransmitting); FDSPEngine.PSSetMox(FTransmitting);
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis, // ★Телеграф: голосовой TXA не запускается ВООБЩЕ. В WDSP режим TXA_CWL —
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому // буквально ветка SSB, поэтому
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц. // без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN. // Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки // Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
@@ -6476,8 +6475,7 @@ begin
begin begin
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном. // Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz), // В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis // и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон.
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
if FWDSPReady and Assigned(FDSPEngine) then if FWDSPReady and Assigned(FDSPEngine) then
begin begin
if ActiveTXMode in [MODE_CWL, MODE_CWU] then if ActiveTXMode in [MODE_CWL, MODE_CWU] then
@@ -6902,7 +6900,7 @@ procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с // Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA // COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой // со своего разъёма, а из программы доступен только бит CWX, то есть прямой
// ключ (это же ограничение у Thetis). // ключ; это ограничение удалённого управления телеграфом.
begin begin
if not CWTXActive then if not CWTXActive then
begin begin
@@ -7071,7 +7069,7 @@ begin
end; end;
// =========================================================================== // ===========================================================================
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2) // PureSignal — оркестрация автомата калибровки и обратной связи.
// =========================================================================== // ===========================================================================
function TRadioController.PSAvailable: Boolean; function TRadioController.PSAvailable: Boolean;
@@ -7097,7 +7095,7 @@ begin
end; end;
procedure TRadioController.SetPureSignal(On_: Boolean); procedure TRadioController.SetPureSignal(On_: Boolean);
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение — // Кнопка PS включает режим автоматической калибровки. Выключение —
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт. // через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
begin begin
if On_ = FPSEnabled then Exit; if On_ = FPSEnabled then Exit;
@@ -7114,7 +7112,7 @@ begin
end; end;
procedure TRadioController.PSSingleCal; procedure TRadioController.PSSingleCal;
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается. // Одиночная калибровка выключает автоматический режим.
begin begin
if not PSAvailable then Exit; if not PSAvailable then Exit;
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
@@ -7140,7 +7138,7 @@ begin
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
else else
Result := 152; // классическая цель Thetis Result := 152; // стандартный целевой уровень
end; end;
end; end;
@@ -7150,7 +7148,7 @@ begin
if Result < 0 then if Result < 0 then
begin begin
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then 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 else
Result := 0.0; // выключен Result := 0.0; // выключен
end; end;
@@ -7203,9 +7201,8 @@ begin
end; end;
procedure TRadioController.PureSignalTick; procedure TRadioController.PureSignalTick;
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis // Вызывается из UI-таймера (~10 Гц). Автомат управляет калибровкой без
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы) // восстановления сохранённой коррекции и подстраивает AttOnTX по feedback.
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
var var
NewAtt: Integer; NewAtt: Integer;
FBTarget: Integer; FBTarget: Integer;
@@ -7287,7 +7284,7 @@ begin
PS_ST_TURNOFF: PS_ST_TURNOFF:
begin begin
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc // Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF). // не дойдёт до LRESET при PSEnabled=true в состоянии TurnOFF.
FDSPEngine.PSControl(1, 0, 0, 0); FDSPEngine.PSControl(1, 0, 0, 0);
PSSetFeedbackPath(True); PSSetFeedbackPath(True);
FPSResetReq := False; FPSResetReq := False;
@@ -7300,7 +7297,7 @@ begin
end; end;
end; end;
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) --- // --- Автоматическая регулировка аттенюатора обратной связи ----------
// По завершении очередной попытки калибровки: если feedback слишком горячий // По завершении очередной попытки калибровки: если feedback слишком горячий
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX // (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход // так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
@@ -7350,7 +7347,7 @@ begin
end; end;
procedure TRadioController.SetTwoTone(On_: Boolean); procedure TRadioController.SetTwoTone(On_: Boolean);
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный // 2TON: двухтональник PostGen + MOX. Уровень — обычный
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию). // Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
begin begin
if On_ = FTwoTone then Exit; if On_ = FTwoTone then Exit;
+1 -1
View File
@@ -27,7 +27,7 @@ unit SerialPort;
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и (rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
телеграфный ключ приходилось глушить заглушкой. телеграфный ключ приходилось глушить заглушкой.
И скопировать чужой исходник было нельзя. `Serial.SerSetParams` кладёт Готовая реализация `Serial.SerSetParams` кладёт
B-константу скорости прямо в c_cflag идиома Linux, где это битовая маска. B-константу скорости прямо в c_cflag идиома Linux, где это битовая маска.
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE: $1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
+31 -35
View File
@@ -33,8 +33,7 @@ const
CFG_BAND_COUNT = 12; CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex) 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_MOX = 0; // только обычный TX (не TUNE, не 2TON)
OCA_TUNE = 1; // только TUNE OCA_TUNE = 1; // только TUNE
OCA_TWOTONE = 2; // только 2TON OCA_TWOTONE = 2; // только 2TON
@@ -52,7 +51,7 @@ const
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице) // отдельной странице, не в общей 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 // order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом // 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда). // Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
@@ -128,7 +127,7 @@ const
('6.25k', '10k', '12.5k', '20k', '25k'); ('6.25k', '10k', '12.5k', '20k', '25k');
// --- Редактируемая таблица фильтров ------------------------------------- // --- Редактируемая таблица фильтров -------------------------------------
// 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки // 10 пресетов + VAR1/VAR2 на режим. Кромки
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB // ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
// своя — поэтому при применении фильтра конвертировать боковую больше не // своя — поэтому при применении фильтра конвертировать боковую больше не
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW, // нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
@@ -249,11 +248,11 @@ type
// Mic gain (dB, применяется в WDSP PanelGain1) // Mic gain (dB, применяется в WDSP PanelGain1)
MicGainDB: Double; // -10..+30 MicGainDB: Double; // -10..+30
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX). // 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 AttOnTX: Integer; // 0..31 dB
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen. // Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
// Уровень — отдельный от слайдера Drive, проходит через ту же // Уровень — отдельный от слайдера Drive, проходит через ту же
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz. // per-band калибровку. Defaults: level=10 %, freq=750 Hz.
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive) TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
// TX bandpass filter // TX bandpass filter
@@ -301,11 +300,11 @@ type
TXWfDetector: Integer; TXWfDetector: Integer;
TXWfAvgMode: Integer; TXWfAvgMode: Integer;
TXWfAvgTimeMS: Double; TXWfAvgTimeMS: Double;
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах) // TX Grid задаётся отдельно от RX и использует собственные границы.
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0 TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
TXSpecRange: Double; // dB range, e.g. 80 TXSpecRange: Double; // dB range, e.g. 80
TXSpecGridStep: Double; // grid step in dB, e.g. 10 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) PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0) PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150) PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
@@ -318,7 +317,7 @@ type
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn) PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700) PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900) PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
// Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152 // Калибровка фидбек-тракта: на Orion MkII цель 152
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ. // перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
// (Orion MkII → 22, иначе 152) // (Orion MkII → 22, иначе 152)
@@ -331,10 +330,9 @@ type
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому // мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от // почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает). // TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
// Раскладка байта 5 HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs) // Раскладка байта 5 задана константами HPSDRProtocol.CW_*.
// и pihpsdr (new_protocol.c:1353).
TCWSettings = record TCWSettings = record
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600) Pitch: Integer; // Гц, тон приёма/сайдтона, default 600
// Память сообщений для программной передачи (окно CW Messages, F1..F8). // Память сообщений для программной передачи (окно CW Messages, F1..F8).
Messages: array[0..CW_MSG_COUNT-1] of string[63]; Messages: array[0..CW_MSG_COUNT-1] of string[63];
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом // Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
@@ -348,7 +346,7 @@ type
Weight: Integer; // 33..66 (50 = точка:пауза 1:1) Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
HangTimeMS: Integer; // сколько держать TX после последнего элемента HangTimeMS: Integer; // сколько держать TX после последнего элемента
RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс) RampMS: Integer; // форма фронта посылки, default 9 мс
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель) RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6) SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
SidetoneHWLevel: Integer; // 0..127 SidetoneHWLevel: Integer; // 0..127
@@ -394,8 +392,7 @@ type
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ // и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
// зависит: переключение профиля не должно перерисовывать спектр, менять // зависит: переключение профиля не должно перерисовывать спектр, менять
// задержку тракта или ломать калибровку PS. // задержку тракта или ломать калибровку PS.
// Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без // Профиль не содержит параметры VAC и DSP-буферов, которых в EWSDR нет.
// её хвоста из VAC/DSP-буферов, которых у нас нет.
TTXProfile = record TTXProfile = record
Name: string[31]; Name: string[31];
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы) // Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
@@ -428,7 +425,7 @@ type
EQFreqs: array[0..10] of Double; EQFreqs: array[0..10] of Double;
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM) // Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
AMCarrierLevel: Double; AMCarrierLevel: Double;
// Мощность (Thetis держит Power/Tune_Power в профиле) // Мощность Power/Tune_Power хранится в профиле.
DrivePercent: Integer; // 0..100 DrivePercent: Integer; // 0..100
TUNLevel: Integer; // 0..100 TUNLevel: Integer; // 0..100
end; end;
@@ -478,12 +475,12 @@ type
SpecPixels: Integer; SpecPixels: Integer;
end; end;
// TCI-сервер (протокол Expert Electronics поверх WebSocket). Авторизации в // TCI-сервер поверх WebSocket. Авторизации в
// протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые // протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые
// программы обычно живут на том же ПК, а наружу порт открывается осознанно. // программы обычно живут на том же ПК, а наружу порт открывается осознанно.
TTCISettings = record TTCISettings = record
Enabled: Boolean; Enabled: Boolean;
Port: Integer; // 1..65535, умолчание 40001 (как у ExpertSDR3) Port: Integer; // 1..65535, по умолчанию 40001
BindAddr: string; // '127.0.0.1' или '0.0.0.0' BindAddr: string; // '127.0.0.1' или '0.0.0.0'
// ★Единственный каталог, куда TCI пишет записи линейного выхода // ★Единственный каталог, куда TCI пишет записи линейного выхода
// (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records. // (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records.
@@ -579,12 +576,11 @@ type
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet). // OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет. // Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
// Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis // Хранятся в JSON-секции "oc" под MAC. Поддерживается сокращённый набор:
// (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён:
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции // без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы). // на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings). // 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_* на пин TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
TOCSettings = record TOCSettings = record
@@ -599,13 +595,13 @@ type
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста, // Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста // напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower). // (Vref, bridge voltage и ADC offset).
TCalibration = record TCalibration = record
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII 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 = без коррекции) PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции) SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360) AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120) AmpSensMVA: Double; // чувствительность датчика тока, мВ/А
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление // Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл // тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у // индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
@@ -613,11 +609,11 @@ type
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double; SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double; SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как // Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его // Display Cal Offset: у display-тракта своё окно/детектор, и его
// уровень отличается от показаний стрелки на пару дБ. // уровень отличается от показаний стрелки на пару дБ.
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double; DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double; DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Частотная калибровка опорника, ppm (Frequency Calibration в Thetis). // Частотная калибровка опорника, ppm.
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются // Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
// ниже истинной частоты; положительное значение поднимает показания. // ниже истинной частоты; положительное значение поднимает показания.
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров // Действует на всё железо разом; ручные LOError/TO Error трансвертеров
@@ -720,8 +716,8 @@ type
// Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT. // Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean; CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, … CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
// Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по // Auto TX: команда TX по CAT-порту слайса сама назначает этот слайс
// CAT-порту слайса сама назначает этот слайс источником передачи. // источником передачи.
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean; CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход // Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
// приложения / общий вход). Имена, а не индексы: порядок перечисления // приложения / общий вход). Имена, а не индексы: порядок перечисления
@@ -1710,21 +1706,21 @@ begin
T.MicBias := True; T.MicBias := True;
T.LineInGainDB := 0.0; // 0 dB T.LineInGainDB := 0.0; // 0 dB
T.MicGainDB := 10.0; T.MicGainDB := 10.0;
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи) // ATT on TX — default 31 dB (полная защита RX-ADC во время передачи)
T.AttOnTX := 31; T.AttOnTX := 31;
// Tune — Thetis defaults // Tune defaults
T.TUNLevel := 10; T.TUNLevel := 10;
T.TUNFreq := 750; T.TUNFreq := 750;
// SSB-фильтр по умолчанию // SSB-фильтр по умолчанию
T.FilterLow := 200; T.FilterLow := 200;
T.FilterHigh := 3100; T.FilterHigh := 3100;
T.FilterNC := 1024; T.FilterNC := 1024;
T.FilterMP := 1; // Low Latency (как в piHPSDR) T.FilterMP := 1; // Low Latency
T.FilterWindow := 1; // BH4 / Blackman-Harris T.FilterWindow := 1; // BH4 / Blackman-Harris
// Compressor — выкл по умолчанию // Compressor — выкл по умолчанию
T.CompressorOn := False; T.CompressorOn := False;
T.CompressorGain:= 5.0; T.CompressorGain:= 5.0;
// Leveler вкл, типичные значения piHPSDR/Thetis // Leveler включён с типичными начальными значениями.
T.LevelerOn := True; T.LevelerOn := True;
T.LevelerTop := 5.0; T.LevelerTop := 5.0;
T.LevelerDecay := 500; T.LevelerDecay := 500;
@@ -1767,7 +1763,7 @@ begin
T.TXSpecRefLevel := 0.0; T.TXSpecRefLevel := 0.0;
T.TXSpecRange := 80.0; T.TXSpecRange := 80.0;
T.TXSpecGridStep := 10.0; T.TXSpecGridStep := 10.0;
// PureSignal — дефолты Thetis PSForm (протокол 2) // PureSignal — значения по умолчанию для протокола 2.
T.PSMoxDelay := 0.2; T.PSMoxDelay := 0.2;
T.PSLoopDelay := 0.0; T.PSLoopDelay := 0.0;
T.PSTXDelayNs := 150; T.PSTXDelayNs := 150;
@@ -1868,7 +1864,7 @@ end;
class procedure TSettingsManager.DefaultCW(out C: TCWSettings); class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
begin begin
FillChar(C, SizeOf(C), 0); FillChar(C, SizeOf(C), 0);
C.Pitch := 600; // Thetis cw_pitch default C.Pitch := 600; // default CW pitch
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
C.KeyerMode := 2; // Iambic B C.KeyerMode := 2; // Iambic B
C.ReverseKeys := False; C.ReverseKeys := False;
@@ -1877,7 +1873,7 @@ begin
C.Weight := 50; C.Weight := 50;
C.BreakIn := True; C.BreakIn := True;
C.HangTimeMS := 300; C.HangTimeMS := 300;
C.RampMS := 9; // Thetis SetCWEdgeLength(9) C.RampMS := 9; // default CW edge length
C.RFDelayMS := 0; C.RFDelayMS := 0;
C.SidetoneHW := True; C.SidetoneHW := True;
C.SidetoneHWLevel := 50; C.SidetoneHWLevel := 50;
+10 -11
View File
@@ -1940,7 +1940,7 @@ begin
end; end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit) // BuildTransmitTab — все настройки TX-тракта.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TSettingsForm.BuildTransmitTab; procedure TSettingsForm.BuildTransmitTab;
@@ -3283,7 +3283,7 @@ begin
end; end;
procedure TSettingsForm.BuildCalibrationTab; procedure TSettingsForm.BuildCalibrationTab;
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности // Вкладка Calibration: модель детектора мощности
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/ // Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на // тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab // слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
@@ -3428,7 +3428,7 @@ begin
end; end;
// ── Spectrum / waterfall ──────────────────────────────────────────────── // ── Spectrum / waterfall ────────────────────────────────────────────────
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display- // Отдельная от S-метра калибровка Display Cal Offset: у display-
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки // тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и // расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
// сигналы) — это калибровка уровня, а не подавление шума. // сигналы) — это калибровка уровня, а не подавление шума.
@@ -3477,7 +3477,7 @@ begin
end; end;
// ── Reference frequency ───────────────────────────────────────────────── // ── Reference frequency ─────────────────────────────────────────────────
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis): // Одна поправка опорника на всё железо:
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction. // у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту // Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
// рознь, и где-то уход больше, где-то меньше. // рознь, и где-то уход больше, где-то меньше.
@@ -3691,7 +3691,7 @@ procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
begin begin
if FLoading then Exit; if FLoading then Exit;
// Смена модели детектора подставляет модельные дефолты датчика тока // Смена модели детектора подставляет модельные дефолты датчика тока
// (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88, // ANAN-7000DLE/Anvelina Pro 3 — 340/88,
// остальные — 360/120. Правится руками после, если датчик нестандартный. // остальные — 360/120. Правится руками после, если датчик нестандартный.
if Sender = FCmbPwrDetModel then if Sender = FCmbPwrDetModel then
begin begin
@@ -5117,7 +5117,7 @@ begin
FCATTcpPort := Spin; FCATTcpPort := Spin;
// ── TCI Server ──────────────────────────────────────────────────────────── // ── TCI Server ────────────────────────────────────────────────────────────
// Протокол Expert Electronics поверх WebSocket: логгеры, скиммеры, цифра. // TCI поверх WebSocket: логгеры, скиммеры, цифровые виды связи.
Grp := MakeGroupPanel(FPageCAT, 'TCI Server', Grp := MakeGroupPanel(FPageCAT, 'TCI Server',
MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H); MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H);
Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT; Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT;
@@ -5317,8 +5317,8 @@ begin
Spin.OnChange := OnSliceCATCtrlChange; Spin.OnChange := OnSliceCATCtrlChange;
FSliceCATPort[i] := Spin; FSliceCATPort[i] := Spin;
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому // Auto TX: команда TX по этому порту сама делает слайс источником
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире, // передачи. Пока кто-то уже в эфире,
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая // заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
// (в ширину группы), пояснение — строкой ниже. // (в ширину группы), пояснение — строкой ниже.
Chk := TFlatCheckBox.Create(Self); Chk := TFlatCheckBox.Create(Self);
@@ -6239,9 +6239,8 @@ end;
// =========================================================================== // ===========================================================================
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета). // OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/ // Реализован сокращённый набор: без PA-override/hot-switching внешнего PA
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/ // и без раздельных пинов VFO A/B. Только openHPSDR —
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend). // вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR. // HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
// =========================================================================== // ===========================================================================
+4 -4
View File
@@ -114,7 +114,7 @@ type
FTXOverlay: Boolean; FTXOverlay: Boolean;
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в // Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у // приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по // TX-шкала НЕ следует за RX, поэтому при передаче полоса рисуется по
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк. // ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
FRXEdgeLo, FRXEdgeHi: Integer; FRXEdgeLo, FRXEdgeHi: Integer;
FTXEdgeLo, FTXEdgeHi: Integer; FTXEdgeLo, FTXEdgeHi: Integer;
@@ -813,7 +813,7 @@ begin
M[i].LevelDB := PeakDB(M[i].FreqHz); M[i].LevelDB := PeakDB(M[i].FreqHz);
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5; ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
// Все маркеры — абсолютный уровень шкалы (dBm, как в Thetis); относительная // Все маркеры — абсолютный уровень шкалы в dBm; относительная
// величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы. // величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы.
for i := 0 to 3 do for i := 0 to 3 do
M[i].Caption := Format('%.0f', [M[i].LevelDB]); M[i].Caption := Format('%.0f', [M[i].LevelDB]);
@@ -1398,7 +1398,7 @@ end;
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения // Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются // source-лейнов считаются один раз на строку, в пиксельном цикле остаются
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая // два умножения на dst-лейны (нормализация /256, кривая
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр. // прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
var var
Row, Col, Alpha, YMin, A256, I256: Integer; Row, Col, Alpha, YMin, A256, I256: Integer;
@@ -1701,7 +1701,7 @@ begin
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу. // поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса // Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis), // считается по TX-фильтру: его ширина независима от приёмной,
// и раньше в эфир шло одно, а на экране рисовалось другое. // и раньше в эфир шло одно, а на экране рисовалось другое.
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
begin begin
+25 -27
View File
@@ -71,7 +71,7 @@ const
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1), // Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
// и «второй приёмник» там существует только как слайс главного пана — при // и «второй приёмник» там существует только как слайс главного пана — при
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало // нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1. // номеров: некоторые клиенты позволяют выбрать только приёмники 0 и 1.
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на // Со слотами правило «первый добавленный слайс = приёмник 1» держится на
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного // любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ). // одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
@@ -114,9 +114,9 @@ const
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: MSHV пишет свой // ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: клиент может писать
// TX-буфер granule'ами STREAM_C = 4096 отсчётов при 96 кГц (network.cpp), то // TX-буфер гранулами по 4096 отсчётов при 96 кГц, то есть 42.7 мс, и отвечать
// есть 42.7 мс, и отвечает на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как // на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по // ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше // одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса, // пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
@@ -125,7 +125,7 @@ const
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи) TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после // ★Посев аванса, пока про клиента ничего не известно. Первая передача после
// подключения иначе стартует с pre-roll 31.75 мс против первого ответа MSHV // подключения иначе стартует с pre-roll 31.75 мс до первого ответа клиента
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не // на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
// успевает: аванс появляется только после первого ответа. // успевает: аванс появляется только после первого ответа.
TCI_TX_LEAD_DEF_MS = 50; TCI_TX_LEAD_DEF_MS = 50;
@@ -254,7 +254,7 @@ type
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того, // ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс // сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
// подрос — попросили ещё больше. Это положительная обратная связь. Период // подрос — попросили ещё больше. Это положительная обратная связь. Период
// же равен внутренней грануле клиента (у MSHV — STREAM_C/96 кГц = 42.7 мс) // же равен внутренней грануле клиента (например, 4096/96 кГц = 42.7 мс)
// и от нашего темпа не зависит вовсе. // и от нашего темпа не зависит вовсе.
FTxGapUs: Double; // оценка периода между пачками, мкс FTxGapUs: Double; // оценка периода между пачками, мкс
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
@@ -857,8 +857,8 @@ begin
FServer.Broadcast(TCIBuild('rx_channel_enable', FServer.Broadcast(TCIBuild('rx_channel_enable',
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)])); [TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют // ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу: // входящие по arg1 (см. StrTrx), а некоторые также связывают с этим флагом
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный // разрешение передачи. Слайс, созданный
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT. // ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
FServer.Broadcast(StrTxEnable(Rx)); FServer.Broadcast(StrTxEnable(Rx));
end; end;
@@ -1195,8 +1195,8 @@ end;
function TTCIAdapter.StrTrx(Rx: Integer): string; function TTCIAdapter.StrTrx(Rx: Integer): string;
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно // Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV, // его слайс. Клиенты могут фильтровать входящие строки по номеру приёмника,
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды // поэтому автору команды
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а // отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей // `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
// слайс сейчас источник передачи (`TxRx`). // слайс сейчас источник передачи (`TxRx`).
@@ -1309,8 +1309,7 @@ end;
procedure TTCIAdapter.BroadcastTxEnable; procedure TTCIAdapter.BroadcastTxEnable;
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает // TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на // только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача // приёмнике 1 может её отбросить и не разрешить передачу.
// (`set_ptt`: `if (!tci_tx_enable) return;`).
var Rx: Integer; var Rx: Integer;
begin begin
if (FServer = nil) or (FServer.ClientCount = 0) then Exit; if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
@@ -1830,8 +1829,7 @@ begin
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit; if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим // ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за // целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию // настоящую частоту и завершить инициализацию несуществующего приёмника.
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit; if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc» // Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
@@ -2157,7 +2155,7 @@ begin
begin begin
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция // arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
// берётся из аудиопотока этого клиента. Остальные значения // берётся из аудиопотока этого клиента. Остальные значения
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых // (mic1/mic2/micpc/ecoder2) называют физические входы TCI, которых
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно // у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2) // то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
// проверяем буквально: без AUDIO_START модулировать нечем, и молча // проверяем буквально: без AUDIO_START модулировать нечем, и молча
@@ -2196,8 +2194,8 @@ begin
// ничего не изменила, всё равно её перебивала: клиент, уже // ничего не изменила, всё равно её перебивала: клиент, уже
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик // передававший с приёмника 1, присылает trx:0,true,tci — передатчик
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи // занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает // с этого мига уходят под номером 0. Клиент второго приёмника может
// (network.cpp:231), то есть передача просто замолкает. // отбросить такие блоки, и передача просто замолкнет.
if B then if B then
begin begin
if Started and FromTCI then if Started and FromTCI then
@@ -2236,7 +2234,7 @@ begin
FTxRx := 0; FTxRx := 0;
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и // ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
// идущей передачи. Раньше эти две строки расходились, и штатный // идущей передачи. Раньше эти две строки расходились, и штатный
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым. // конец посылки (`trx:N,false`) оставлял FTxRunning поднятым.
FTxRunning := False; FTxRunning := False;
end; end;
finally FTxLock.Leave; end; finally FTxLock.Leave; end;
@@ -2789,7 +2787,7 @@ begin
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
begin begin
Client.AudioRate := V; Client.AudioRate := V;
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и // Число сэмплов в блоке у TCI своё на каждую частоту (§4.3), и
// клиент вправе на это рассчитывать, пока не задал своё явно. // клиент вправе на это рассчитывать, пока не задал своё явно.
Client.AudioSamples := TCIDefaultAudioSamples(V); Client.AudioSamples := TCIDefaultAudioSamples(V);
RestartStreams(Client, tstRXAudio); RestartStreams(Client, tstRXAudio);
@@ -3740,7 +3738,8 @@ end;
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer); procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта. // Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
// //
// ★Зачем: MSHV отвечает на первый маркер только через ~41 мс, а сам маркер // ★Зачем: некоторые клиенты отвечают на первый маркер только через ~41 мс,
// а сам маркер
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal — // уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не // ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос // хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
@@ -4001,9 +4000,8 @@ begin
(Sent < TCI_TX_MARKERS_PER_TICK) do (Sent < TCI_TX_MARKERS_PER_TICK) do
begin begin
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0. // ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
// Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV, // Клиент может фильтровать ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver, а
// network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а // TX-аудио отправлять только в ответ на этот маркер.
// TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono).
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал: // С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
// маркеры до него не доходили вовсе. // маркеры до него не доходили вовсе.
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans); 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 if H.DataLength > 0 then
begin begin
Want := Integer(H.DataLength); Want := Integer(H.DataLength);
@@ -4103,8 +4101,8 @@ begin
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем // Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
// просили, и зажим в ноль заставил бы переспросить уже полученное. // просили, и зажим в ноль заставил бы переспросить уже полученное.
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений // Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
// отсчётов: первые ответы MSHV — законные нули (network.cpp, ветка // отсчётов: первые ответы клиента могут быть законными нулями, и сторож не
// _reset_sta_ <= 5), и сторож не должен считать их отсутствием прогресса. // должен считать их отсутствием прогресса.
NowUs := MonotonicUs; NowUs := MonotonicUs;
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а // Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
// очередями по нескольку блоков — по своей внутренней зернистости записи. // очередями по нескольку блоков — по своей внутренней зернистости записи.
+11 -18
View File
@@ -20,7 +20,7 @@
unit TCIProtocol; unit TCIProtocol;
{ {
TCIProtocol.pas протокол TCI 2.0 (Expert Electronics), чистый слой. TCIProtocol.pas протокол TCI 2.0, чистый слой.
Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера. Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера.
Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и
@@ -34,7 +34,7 @@ unit TCIProtocol;
Зарезервированные символы: ':' ',' ';' внутри аргументов запрещены и Зарезервированные символы: ':' ',' ';' внутри аргументов запрещены и
заменяются на '^' '~' '*' (§3.2.1), обратная подстановка TCIUnescape. заменяются на '^' '~' '*' (§3.2.1), обратная подстановка TCIUnescape.
Регистр значения не имеет. ExpertSDR3 шлёт всё в нижнем регистре, и часть Регистр значения не имеет. TCI шлёт всё в нижнем регистре, и часть
клиентов сравнивает строки как есть поэтому наружу тоже пишем строчными, клиентов сравнивает строки как есть поэтому наружу тоже пишем строчными,
а внутрь принимаем любой. а внутрь принимаем любой.
} }
@@ -52,17 +52,17 @@ uses
const const
TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL
TCI_APP_NAME = 'EWSDR'; TCI_APP_NAME = 'EWSDR';
TCI_DEFAULT_PORT = 40001; // порт TCI-сервера ExpertSDR3 TCI_DEFAULT_PORT = 40001; // стандартный порт TCI-сервера
TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B) TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B)
// Список видов связи для MODULATIONS_LIST. Первые десять — словарь // Список видов связи для MODULATIONS_LIST. Первые десять — стандартные
// ExpertSDR3 (клиенты сверяются именно с ними), dmr/fmraw — наше // режимы TCI; dmr/fmraw — расширения EWSDR. Протокол допускает расширение
// расширение: протокол расширяемый по замыслу (§1.4). // этого списка (§1.4).
TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw'; TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw';
// Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно). // Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно).
// Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока, // Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока,
// а не его размер: больше в один блок не кладут ни ExpertSDR3, ни клиенты. // а не его размер: больше в один блок не кладут ни TCI, ни клиенты.
TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке
TCI_STREAM_HDR_SIZE = 64; // 16 × uint32 TCI_STREAM_HDR_SIZE = 64; // 16 × uint32
TCI_STREAM_MAX = TCI_STREAM_HDR_SIZE + TCI_STREAM_DATA_MAX; 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 TCISampleTypeName(T: TTCISampleType): string;
function TCISampleBytes(T: TTCISampleType): Integer; function TCISampleBytes(T: TTCISampleType): Integer;
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у ExpertSDR3 { Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у TCI
своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. } своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. }
function TCIDefaultAudioSamples(RateHz: Integer): Integer; function TCIDefaultAudioSamples(RateHz: Integer): Integer;
@@ -178,22 +178,15 @@ function TCIMaxBlockSamples(T: TTCISampleType; Channels: Integer): Integer;
Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как
Stream.length/Stream.channels»), а для аудио формулировка §4.3 про Stream.length/Stream.channels»), а для аудио формулировка §4.3 про
AUDIO_STREAM_SAMPLES («arg1 количество сэмплов, указываемое в поле AUDIO_STREAM_SAMPLES («arg1 количество сэмплов, указываемое в поле
Stream.length») однажды уже увела нас в «сэмплы на канал». ★Так делать Stream.length») может быть прочитана как «сэмплы на канал». ★Так делать
нельзя, и вот доказательство из живого клиента MSHV, network.cpp: нельзя: клиент вычисляет по length число байт блока. Стоит объявить length «на
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 «на
канал» и у стерео он разбирает ровно половину блока, а вторую выбрасывает: канал» и у стерео он разбирает ровно половину блока, а вторую выбрасывает:
звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не
декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора
звука и 1649 не до конца разобранных блоков за минуту. звука и 1649 не до конца разобранных блоков за минуту.
Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES сэмплы НА Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES сэмплы НА
КАНАЛ (2048 при 48 кГц = 42.7 мс, как у ExpertSDR3), и потолок data[16384] КАНАЛ (2048 при 48 кГц = 42.7 мс), и потолок data[16384]
сходится ровно: 2048 × 2 канала × float32. } сходится ровно: 2048 × 2 канала × float32. }
procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType; procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType;
Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer); Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer);
+2 -2
View File
@@ -20,8 +20,8 @@
unit TCIServer; unit TCIServer;
{ {
TCIServer.pas транспорт TCI: WebSocket-сервер (роль сервера играем мы, TCIServer.pas транспорт TCI: WebSocket-сервер для логгеров, скиммеров
как ExpertSDR3; клиенты логгеры, скиммеры, программы цифровых видов). и программ цифровых видов связи.
Что делает: Что делает:
слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket
+3 -3
View File
@@ -222,7 +222,7 @@ type
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны: него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
начало активной записи затирало само себя, а SAVE через час после START начало активной записи затирало само себя, а SAVE через час после START
отдавал файл, которого у ExpertSDR3 давно бы не было. отдавал бы файл уже после завершения заданного окна записи.
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
(мьют, пауза приёмника), а время записи всё равно идёт. } (мьют, пауза приёмника), а время записи всё равно идёт. }
@@ -682,8 +682,8 @@ begin
F := TCIDecimFactor(ASrcRate, AWantRate); F := TCIDecimFactor(ASrcRate, AWantRate);
FOutRate := ASrcRate div F; FOutRate := ASrcRate div F;
// Блок не имеет права вылезти за data[16384]: столько ExpertSDR3 объявил // Блок не имеет права вылезти за data[16384]: это установленный протоколом
// потолком, и клиенты держат приёмный буфер ровно под него. // предел, под который клиенты выделяют приёмный буфер.
MaxB := TCIMaxBlockSamples(FSampleT, FChannels); MaxB := TCIMaxBlockSamples(FSampleT, FChannels);
FBlock := EnsureRange(ABlock, 1, MaxB); FBlock := EnsureRange(ABlock, 1, MaxB);
+1 -1
View File
@@ -22,7 +22,7 @@ unit VfoOverlay;
{$mode objfpc}{$H+} {$mode objfpc}{$H+}
{ {
TVfoOverlay накладка-«флаг слайса» поверх спектра, стиль SmartSDR. TVfoOverlay накладка-«флаг слайса» поверх спектра.
Компоновка (сверху вниз): Компоновка (сверху вниз):
шапка : [A] 2.7k SPLIT [TX] шапка : [A] 2.7k SPLIT [TX]
метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40 метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40
+40 -44
View File
@@ -196,7 +196,7 @@ const
TX_MIC_RING = 8192; TX_MIC_RING = 8192;
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX). // TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0), // TX rate постоянен: 192k для Hermes/Saturn DUC0,
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том // меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192). // темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
TX_SAMPLE_RATE = 192000; TX_SAMPLE_RATE = 192000;
@@ -323,7 +323,7 @@ type
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает) // Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
FSpecBuf: array of Double; FSpecBuf: array of Double;
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize. // Отдельный небольшой feed для display-анализатора.
FDispBuf: array of Double; FDispBuf: array of Double;
FDispPos: Integer; FDispPos: Integer;
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками // То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
@@ -372,7 +372,7 @@ type
FWfAvgMode: Integer; FWfAvgMode: Integer;
FWfAvgTimeMS: Double; 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 = максимум. // FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz — // FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
// границы видимого окна в Гц относительно DDC-центра (low отрицательный), // границы видимого окна в Гц относительно DDC-центра (low отрицательный),
@@ -531,7 +531,7 @@ type
FFilterLow: Integer; FFilterLow: Integer;
FFilterHigh: Integer; FFilterHigh: Integer;
FAGCMode: TWDSPAGCMode; FAGCMode: TWDSPAGCMode;
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR FAGCTop: Double; // AGC gain in dB
FAGCSlope: Integer; // наклон АРУ в дБ (default 0) FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
FAGCHangThreshold: Integer; // порог hang 0..100 FAGCHangThreshold: Integer; // порог hang 0..100
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel) FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
@@ -580,7 +580,7 @@ type
// главного RXA и для слайсов — иначе дублирование по трём местам. // главного RXA и для слайсов — иначе дублирование по трём местам.
procedure ApplyRXModeSettings(Chan, Mode: Integer); procedure ApplyRXModeSettings(Chan, Mode: Integer);
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима. // Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака, // Единственный конвертер знака краёв TX-фильтра для выбранного режима,
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB. // через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double); procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// --- CW pitch ----------------------------------------------------------- // --- CW pitch -----------------------------------------------------------
@@ -783,7 +783,7 @@ type
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double); procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal). // 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды // Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
// по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr). // по 0.49, чтобы сумма оставалась чуть ниже полной шкалы.
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX. // Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен. // Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
@@ -795,7 +795,7 @@ type
procedure SetPureSignalArmed(Armed: Boolean); procedure SetPureSignalArmed(Armed: Boolean);
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 = // Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО // RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
// потока (как Thetis/pihpsdr — pscc потокобезопасен относительно // потока; pscc потокобезопасен относительно
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока). // fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer; procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
TotalPairs: Integer); TotalPairs: Integer);
@@ -958,7 +958,7 @@ implementation
// =========================================================================== // ===========================================================================
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN), // TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
// сигнализирует о готовых IQ данных через FOnTXIQ callback. // сигнализирует о готовых IQ данных через FOnTXIQ callback.
// Аналог tx_thread в piHPSDR/transmitter.c // Поток обработки TX-аудио и формирования IQ.
// =========================================================================== // ===========================================================================
type type
TDisplayThread = class(TThread) TDisplayThread = class(TThread)
@@ -981,7 +981,7 @@ type
function UsePanNormOneHz(Detector: Integer): Integer; function UsePanNormOneHz(Detector: Integer): Integer;
begin begin
// В Thetis нормализация к 1 Гц включается только для panadapter // Нормализация к 1 Гц включается только для panadapter
// и только для detector modes 2/3/4. // и только для detector modes 2/3/4.
if Detector in [2, 3, 4] then if Detector in [2, 3, 4] then
Result := 1 Result := 1
@@ -1029,8 +1029,7 @@ begin
end; end;
procedure TTXDSPThread.Execute; procedure TTXDSPThread.Execute;
// Аналог tx_thread в piHPSDR/transmitter.c: // Поток работает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину. // Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде. // Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
var var
@@ -1052,7 +1051,7 @@ begin
Dec(AccUs, PeriodMs * 1000); Dec(AccUs, PeriodMs * 1000);
// Ждём сигнала от mic-данных ИЛИ истечения периода — // Ждём сигнала от mic-данных ИЛИ истечения периода —
// в piHPSDR timing идёт от receive thread; здесь — от таймаута // Здесь timing задаётся таймаутом, синхронизированным с размером буфера.
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs); RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
if Terminated then Break; if Terminated then Break;
if not FEngine.FTXActive then Continue; if not FEngine.FTXActive then Continue;
@@ -1085,7 +1084,7 @@ begin
if Terminated then Break; if Terminated then Break;
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring // Silence-инъекция УБРАНА. Раньше при пустом ring
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался». // (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт // Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он // PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
@@ -1098,9 +1097,8 @@ begin
end; end;
// =========================================================================== // ===========================================================================
// DSP поток обрабатывает IQ пакеты из очереди // DSP поток обрабатывает IQ-пакеты из очереди.
// Сетевой поток только кладёт пакеты, этот поток занимается DSP // Сетевой поток только кладёт пакеты, этот поток занимается DSP.
// Аналог iq_thread в piHPSDR/new_protocol.c
// =========================================================================== // ===========================================================================
type type
TDSPThread = class(TThread) TDSPThread = class(TThread)
@@ -1125,7 +1123,7 @@ var
begin begin
while not Terminated do while not Terminated do
begin begin
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR) // Ждём сигнала от сетевого потока.
RTLEventWaitFor(FEngine.FQueueSem, 100); RTLEventWaitFor(FEngine.FQueueSem, 100);
if Terminated then Break; if Terminated then Break;
@@ -1339,7 +1337,7 @@ begin
end; end;
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double); procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы // Вход — магнитуда TX-аудио-полосы
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под // (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты. // боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
var var
@@ -1376,8 +1374,7 @@ begin
end; end;
function TWDSPEngine.CWLOOffset(Mode: Integer): Double; function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr // Для CWL смещение положительное, для CWU — отрицательное.
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
begin begin
case Mode of case Mode of
MODE_CWL: Result := FCWPitch; MODE_CWL: Result := FCWPitch;
@@ -1396,7 +1393,7 @@ end;
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer); procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг // Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину, // корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338). // т.е. у CWU получается pitch±bw/2.
var Off: Integer; var Off: Integer;
begin begin
Off := Round(CWLOOffset(Mode)); Off := Round(CWLOOffset(Mode));
@@ -1428,7 +1425,7 @@ begin
begin begin
// Restore the user's normal voice/data chain after leaving FM RAW. // Restore the user's normal voice/data chain after leaving FM RAW.
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через // bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам. // bp0 всегда активен сам; здесь обновляем остальную цепочку TX.
ApplyTXChainSettings; ApplyTXChainSettings;
if Mode = MODE_WFM then ApplyTXWFMSettings; if Mode = MODE_WFM then ApplyTXWFMSettings;
end; end;
@@ -1478,9 +1475,9 @@ begin
inherited Create; inherited Create;
FSampleRate := SampleRate; FSampleRate := SampleRate;
FAudioRate := AudioRate; 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) // 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 FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096 FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate. // TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
@@ -1579,7 +1576,7 @@ begin
FTXToneOn := False; FTXToneOn := False;
FTXTTOn := False; FTXTTOn := False;
FKeepRXDuringTX := False; FKeepRXDuringTX := False;
// PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2) // PureSignal — значения по умолчанию для протокола 2.
FPSArmed := False; FPSArmed := False;
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
@@ -2210,7 +2207,7 @@ begin
end; end;
try try
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR) // Анализатор открывается перед DSP-каналом.
OpenAnalyzer; OpenAnalyzer;
if not FAnalyzerOpen then if not FAnalyzerOpen then
begin begin
@@ -2237,7 +2234,7 @@ begin
0, 0,
0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010, 0.010,
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как 1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx → // bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок). // изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
); );
@@ -2277,7 +2274,7 @@ begin
Result := True; Result := True;
// Запускаем DSP поток после успешной инициализации WDSP // Запускаем DSP поток после успешной инициализации WDSP
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR // Запускаем отдельный поток обработки IQ.
FDSPThread := TDSPThread.Create(Self); FDSPThread := TDSPThread.Create(Self);
FDSPThread.Priority := tpHighest; // DSP критический поток FDSPThread.Priority := tpHighest; // DSP критический поток
@@ -2531,7 +2528,7 @@ begin
Item^.PanId := 0; Item^.PanId := 0;
FQueueHead := NextHead; FQueueHead := NextHead;
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR) RTLEventSetEvent(FQueueSem); // будим DSP-поток
end; end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte; procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
@@ -2815,7 +2812,7 @@ begin
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV); FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1). // Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX), // При TX RX-аудио глушим в режиме non-duplex MOX,
// иначе оператор слышит свой эфир через RX-цепь и получает feedback. // иначе оператор слышит свой эфир через RX-цепь и получает feedback.
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage // Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка // на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
@@ -2824,7 +2821,7 @@ begin
FPostTXMuteUntil := 0; FPostTXMuteUntil := 0;
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио // Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как // на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае. // FPostTXMuteUntil-хвост уважаем и при non-duplex MOX.
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)) if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then and (FPostTXMuteUntil = 0) then
begin begin
@@ -3523,7 +3520,7 @@ begin
FAGCMode := Mode; FAGCMode := Mode;
if not FInitialized then Exit; if not FInitialized then Exit;
// Точно по piHPSDR receiver.c: set_agc() // Настройка параметров AGC приёмного канала.
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain); ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
// Читаем обратно hang level и thresh для линии на спектре // Читаем обратно hang level и thresh для линии на спектре
@@ -3976,7 +3973,7 @@ end;
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double); procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal. // 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
// Амплитуды 0.49+0.49 сумма чуть ниже полной шкалы (как Thetis/pihpsdr). // Амплитуды 0.49+0.49 дают сумму чуть ниже полной шкалы.
var var
F1, F2: Double; F1, F2: Double;
begin begin
@@ -4026,9 +4023,8 @@ end;
procedure TWDSPEngine.ProcessTXBlock; procedure TWDSPEngine.ProcessTXBlock;
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов // Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback. // через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
// Аналог tx_full_buffer() в piHPSDR/transmitter.c: // Если mic ring buffer содержит достаточно данных — берём их,
// если mic ring buffer содержит достаточно данных — берём их, // иначе обрабатываем тишину (нули).
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
var var
i, tail: Integer; i, tail: Integer;
Err: Integer; Err: Integer;
@@ -4055,7 +4051,7 @@ begin
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end else end else
begin begin
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями) // Нет mic-данных — заполняем входной TX-буфер нулями.
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0); FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
end; end;
@@ -4258,7 +4254,7 @@ end;
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte; procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
DataOffset: Integer; TotalPairs: Integer); DataOffset: Integer; TotalPairs: Integer);
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... → // Сетевой поток: 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 (эталон). // DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
var var
i, n, Pos: Integer; i, n, Pos: Integer;
@@ -4329,7 +4325,7 @@ procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально // WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез // «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту // аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
// величину и без нас (порт Thetis), но пусть формула будет в одном месте. // величину и без нас, но формула остаётся в одном месте.
var L, H, Half: Double; var L, H, Half: Double;
begin begin
Half := 0.0; Half := 0.0;
@@ -4423,7 +4419,7 @@ end;
procedure TWDSPEngine.PushTXEQProfile; procedure TWDSPEngine.PushTXEQProfile;
// Единственная точка, отдающая кривую EQ в WDSP. // Единственная точка, отдающая кривую EQ в WDSP.
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по // F[1..n] — узлы ломаной АЧХ, между ними интерполяция по
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c) // децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому: // идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас, // • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
@@ -4694,7 +4690,7 @@ end;
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer; function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
begin begin
// Thetis-style display FFT range. // Диапазон display FFT.
if FFTSize < 4096 then FFTSize := 4096; if FFTSize < 4096 then FFTSize := 4096;
Result := 4096; Result := 4096;
while Result < FFTSize do while Result < FFTSize do
@@ -4705,7 +4701,7 @@ end;
function TWDSPEngine.CalcDisplayFFTSize: Integer; function TWDSPEngine.CalcDisplayFFTSize: Integer;
begin begin
// FFT Size теперь снова управляет display-analyzer как в Thetis, // FFT Size управляет display-analyzer,
// но не влияет на аудио DSP цепочку. // но не влияет на аудио DSP цепочку.
Result := FFFTSize; Result := FFFTSize;
end; end;
@@ -4716,7 +4712,7 @@ var
TargetFPS: Double; TargetFPS: Double;
begin begin
TargetFPS := EnsureRange(FDisplayFPS, 5, 100); TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
// Thetis/piHPSDR-style timing: // Тайминг обновления дисплея:
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT. // целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS))); Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
EffectiveFPS := TargetFPS; EffectiveFPS := TargetFPS;
@@ -4729,7 +4725,7 @@ begin
if ForWaterfall then if ForWaterfall then
begin begin
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis. // Peak hold на водопаде даёт грязную и залипающую картинку.
if AvgMode < 0 then AvgMode := 1; if AvgMode < 0 then AvgMode := 1;
end end
else else
+2 -2
View File
@@ -40,7 +40,7 @@ const
WF_MAX_PIXELS = 4096; WF_MAX_PIXELS = 4096;
// Палитры водопада (индексы совпадают с настройкой wf_palette) // Палитры водопада (индексы совпадают с настройкой wf_palette)
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема) WF_PAL_CLASSIC = 0; // классическая тёмная / пастельная светлая палитра
WF_PAL_INFERNO = 1; WF_PAL_INFERNO = 1;
WF_PAL_TURBO = 2; WF_PAL_TURBO = 2;
@@ -139,7 +139,7 @@ begin
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]); Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end; end;
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1] // Классическая тёмная палитра, T∈[0..1].
function ClassicDarkColor(T: Double): LongWord; function ClassicDarkColor(T: Double): LongWord;
var var
Local: Double; Local: Double;
+1 -1
View File
@@ -22,7 +22,7 @@ unit WebServer;
{ {
WebServer.pas HTTP + WebSocket сервер для удалённого управления трансивером. WebServer.pas HTTP + WebSocket сервер для удалённого управления трансивером.
Архитектура (по образцу OpenWebRX): Архитектура веб-подсистемы:
───────────────────────────────── ─────────────────────────────────
HTTP GET / index.html (см. WebPageHtml) HTTP GET / index.html (см. WebPageHtml)
HTTP GET /ws Upgrade: WebSocket 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 The network layer feeds 16-bit ADC samples collected from Protocol V4
wideband packets. This view feeds them to a dedicated WDSP analyzer and 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 when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
primitives. primitives.
} }
@@ -308,16 +308,16 @@ begin
if not FWDSPAnalyzerConfigured then if not FWDSPAnalyzerConfigured then
begin begin
SampleRate := Max(1, Round(FSampleRateHz)); 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( SetAnalyzer(
FWDSPAnalyzerID, FWDSPAnalyzerID,
2, 2,
1, 1,
1, // Feed like piHPSDR: interleaved I/Q via Spectrum0. 1, // Interleaved I/Q feed via Spectrum0.
@FWDSPFlp[0], @FWDSPFlp[0],
WB_WDSP_FFT, WB_WDSP_FFT,
WB_WDSP_BLOCK, WB_WDSP_BLOCK,
6, // Thetis wideband default window. 6, // default wideband window.
14.0, 14.0,
0, // no overlap for discontinuous WB packets. 0, // no overlap for discontinuous WB packets.
0, 0,
+15 -18
View File
@@ -4,8 +4,8 @@
Дата последнего обновления: 2026-06-26. Дата последнего обновления: 2026-06-26.
Документ описывает поддержку аппаратной передней панели Apache Labs Документ описывает поддержку аппаратной передней панели Apache Labs
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD). **Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD)
За эталон протокола взят Thetis (`Andromeda/Andromeda.cs`, `CAT/CATCommands.cs`). через команды CAT, используемые панелью.
См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы. См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы.
@@ -84,9 +84,8 @@ Andromeda — это **не отдельный набор команд**, а д
## 3. Раскладка по умолчанию (AndromedaMap.pas) ## 3. Раскладка по умолчанию (AndromedaMap.pas)
Раскладка **зашита в коде** и зеркалит дефолтную таблицу Thetis Раскладка **зашита в коде**: внутренний 0-based индекс на единицу меньше номера
(`MakeNewAndromedaDataset`): наш 0-based индекс = «Pushbutton/Encoder Number» кнопки или энкодера, передаваемого панелью. Номера **проверены на реальной панели** (см.
Thetis 1. Номера **проверены на реальной панели** (см.
`andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`. `andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`.
### Кнопки (`ZZZP`, индекс 0-based после разбора) ### Кнопки (`ZZZP`, индекс 0-based после разбора)
@@ -105,12 +104,12 @@ Thetis 1. Номера **проверены на реальной панел
| | | 47 | TUN | | | | 47 | TUN |
Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT). Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT).
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены;
(в Thetis — на тачскрине), действия в enum сохранены для кастомной раскладки. действия в enum сохранены для пользовательской раскладки.
### Shift / band-keypad ### Shift / band-keypad
Кнопка #28 — модификатор-защёлка (как `eBBShift` в Thetis). Пока активен Кнопка #28 — модификатор-защёлка. Пока он активен
(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую (LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую
(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается (`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается
повторным Shift либо автоматически после выбора диапазона (non-sticky). повторным Shift либо автоматически после выбора диапазона (non-sticky).
@@ -120,7 +119,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
| # | Параметр | Шаг | | # | Параметр | Шаг |
|---|---|---| |---|---|---|
| 0 | AF Gain (громкость) | ±1 | | 0 | AF Gain (громкость) | ±1 |
| 1 | AGC-T (в Thetis — слайдер «RF») | ±1 dB | | 1 | AGC-T | ±1 dB |
| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) | | 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) |
| 11 | Drive (мощность) | ±1 % | | 11 | Drive (мощность) | ±1 % |
| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) | | VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) |
@@ -129,7 +128,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован. > симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован.
> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта. > Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта.
### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis) ### Индикаторы (`ZZZI`, номера соответствуют протоколу панели)
| LED # | Индикатор | LED # | Индикатор | | LED # | Индикатор | LED # | Индикатор |
|---|---|---|---| |---|---|---|---|
| 1 | MOX | 12 | Split | | 1 | MOX | 12 | Split |
@@ -142,7 +141,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
| 11 | VFO Lock | | | | 11 | VFO Lock | | |
Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR, Драйвятся реально: **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 функций не имеют и не зажигаются. в ewsdr функций не имеют и не зажигаются.
--- ---
@@ -169,7 +168,7 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
- Транспорт: **serial** (USB CAT). - Транспорт: **serial** (USB CAT).
- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие). - Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие).
- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов). - Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов).
- Раскладка — зашитый дефолт, выверена по Thetis и реальной панели. - Раскладка по умолчанию проверена с реальной панелью.
- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`. - **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`.
- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`. - **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`.
- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели. - 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) | | **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и sideband-aware passband дисплея |
| **Sticky shift** | сейчас shift всегда non-sticky (снимается band-нажатием); в Thetis есть `AndromedaStickyShift` | | **Sticky shift** | сейчас shift всегда non-sticky и снимается нажатием кнопки диапазона; можно добавить сохраняемую настройку |
| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) | | **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) |
| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` | | **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` |
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) | | **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг и пользовательский интерфейс |
| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) | | **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) |
| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` | | **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` |
| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано | | **Мультифункц. энкодер (`aeMulti`)** | назначение не задано |
@@ -197,6 +196,4 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` + - **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` +
push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю). push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю).
- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы). - **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы).
- Сверяться с форматами Thetis: `Andromeda/Andromeda.cs` (логика панели, - Форматы команд `ZZZ*` и нумерацию элементов проверять по протоколу панели.
`EIndicatorActions`/`EButtonBarActions`/`EEncoderActions`) и
`CAT/CATCommands.cs` (`ZZZ*`).
+5 -9
View File
@@ -6,13 +6,9 @@
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
(19090) остаются за главным приёмником (слайс A) и не тронуты. (19090) остаются за главным приёмником (слайс A) и не тронуты.
Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу Каждый порт привязан к определённому слайсу. Флаг **Auto TX** разрешает
(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»: команде передачи автоматически назначить этот слайс источником TX; без флага
источник выбирается оператором вручную.
> 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.
--- ---
@@ -72,8 +68,8 @@ TX-источника атомарны относительно GUI.
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот `RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже. слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе.
SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает. Бейдж на флаге это показывает.
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот** `GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
+32 -36
View File
@@ -5,8 +5,8 @@
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру, Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
полный перечень реализованных команд, осознанные заглушки и план дальнейших полный перечень реализованных команд, осознанные заглушки и план дальнейших
работ. За эталон протокола взят **Thetis / PowerSDR** (`CATParser.cs`, работ. Поддерживается диалект Kenwood TS-2000 с расширениями `ZZ*`,
`CATCommands.cs`, `CATStructs.xml`) — диалект Kenwood TS-2000 + расширения `ZZ*`. используемый совместимыми SDR-приложениями.
--- ---
@@ -23,7 +23,7 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. | | `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. | | `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). | | `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 Принципы ### 1.1 Принципы
@@ -50,29 +50,29 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
| | Кол-во | | | Кол-во |
|---|---| |---|---|
| Всего `ZZ*` в Thetis | 315 | | Всего известных `ZZ*` | 315 |
| Реально реализовано в EWSDR | **~120** | | Реально реализовано в EWSDR | **~120** |
| Заглушки (отвечают валидным dummy, функции нет) | ~190 | | Заглушки (отвечают валидным dummy, функции нет) | ~190 |
| Совсем не обрабатываются (вернут `?;`) | 5 | | Совсем не обрабатываются (вернут `?;`) | 5 |
Подписи всех 315 команд сверены с `CATCommands.cs` (см. §2.5) — комментарию у Подписи всех 315 команд проверены по протокольному поведению (см. §2.5) — комментарию у
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4). **нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).
### 1.3 Осознанные отклонения от Thetis ### 1.3 Осознанные особенности реализации
Три места, где EWSDR намеренно расходится с эталоном. Больше таких нет. Три команды имеют локальный формат, сложившийся в EWSDR.
| Команда | У Thetis | У нас | Почему | | Команда | Базовое значение | Реализация EWSDR | Почему |
|---|---|---|---| |---|---|---|---|
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты | | `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду | | `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет | | `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |
Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in) Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
эталон принимает в 150..5000 мс, у нас потолок 2000 — столько же стоит в в EWSDR имеет потолок 2000 мс — столько же стоит в
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
выше подрезаются, как и всё прочее у Thetis. выше ограничиваются общим механизмом проверки входных значений.
--- ---
@@ -153,7 +153,7 @@ S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`,
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (0014) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` | | `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (0014) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` | | `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 МГц. 1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level, **Новые 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` | | `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) | | `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
| `ZZET` | кнопка TXEQ | `EQOn` | | `ZZET` | кнопка TXEQ | `EQOn` |
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся | | `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (поле длиной 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
| `ZZTO` | мощность настройки | `TUNLevel` | | `ZZTO` | мощность настройки | `TUNLevel` |
| `ZZTU` | кнопка TUN | `SetTune` | | `ZZTU` | кнопка TUN | `SetTune` |
| `ZZUT` | двухтональник 2TON | `SetTwoTone` | | `ZZUT` | двухтональник 2TON | `SetTwoTone` |
@@ -194,7 +194,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) | | `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
| `ZZCB` | break-in прошивки | `.BreakIn` | | `ZZCB` | break-in прошивки | `.BreakIn` |
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` | | `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
| `ZZCM` | сайдтон (у Thetis — «monitor **disable**», значение инвертировано) | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции | | `ZZCM` | сайдтон; поле `monitor disable` использует инвертированное значение | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь — **Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP, `SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
@@ -216,10 +216,10 @@ CW: по CAT прилетает что угодно, а контроллер п
### 2.5 Ревизия алиасов и подписей ### 2.5 Ревизия алиасов и подписей
Подписи писались по буквам кода, а не по Thetis, и врали примерно в 150 Подписи писались по буквам кода, а не по CAT, и врали примерно в 150
местах. Хуже: часть **работающих** команд была привязана не к своей функции — местах. Хуже: часть **работающих** команд была привязана не к своей функции —
внешний софт получал осмысленный, но неверный ответ, что хуже честной внешний софт получал осмысленный, но неверный ответ, что хуже честной
заглушки. Всё сверено с `CATCommands.cs` и исправлено. заглушки. Все подписи проверены и исправлены.
**Команда делала не своё дело — теперь делает своё:** **Команда делала не своё дело — теперь делает своё:**
@@ -253,42 +253,39 @@ CW: по CAT прилетает что угодно, а контроллер п
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`). > `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
> Во всех случаях старое поведение было отсебятиной. > Во всех случаях старое поведение было отсебятиной.
**Кенвудовские команды.** Сверены отдельно: все 40 команд, реализованных в **Кенвудовские команды.** Сверены отдельно: реализованы все 40 команд,
Thetis, у нас есть, ширины полей совпадают с `CATStructs.xml`. Исправлено: необходимых для совместимости, и проверена ширина полей. Исправлено:
| Команда | Что было не так | | Команда | Что было не так |
|---|---| |---|---|
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;``SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2`, как шлёт Thetis | | `SM` | ответ был 4 цифры вместо **5** (`SM0015;``SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2` |
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент | | `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается | | `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** | | `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код | | `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь обёртка над `ZZTA`, как в эталоне, и сама `ZZTA` проверяет значение строго | | `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь это обёртка над `ZZTA`, а сама `ZZTA` проверяет значение строго |
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент Thetis обращается как раз к `ZZ*` и не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча | | `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 символов со знаком, разбор строгий | | `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`, иначе ошибка формата | | `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;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины | | `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним | | `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время | | `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) | | `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт | | `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками Формат `ZZOT` проверен отдельно: 9 цифр кодируют МГц с шестью знаками после
после запятой (вставляет точку после третьего разряда) — численно это ровно те запятой, что численно соответствует целому значению в герцах. Некорректное
же герцы, что пишем мы, поле совместимо. Мусор в `ZZOS` эталон трактует как значение `ZZOS` намеренно трактуется как симплекс для совместимости клиентов.
симплекс (`default` в `String2OffsetDirection`), и мы намеренно повторяем это,
а не отвечаем ошибкой.
Формат ответа на `ZZ*` (`ZZ` + код + **эхо суффикса запроса** + значение) сверен Формат ответа на `ZZ*` `ZZ` + код + **эхо суффикса запроса** + значение.
с `CATParser.ParseExtended` — совпадает; это важно для `ZZSM0;`, где запрос Эхо суффикса важно для `ZZSM0;`, где запрос несёт селектор приёмника.
несёт селектор приёмника.
--- ---
@@ -314,9 +311,8 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя | | `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — | | `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
> Прежде чем реализовывать новую команду, сверяйся с форматом поля в > Прежде чем реализовывать новую команду, проверяй ширину, знак и смысл поля
> `Thetis/.../CAT/CATStructs.xml` (`<nsetparms>` / `<ngetparms>`) и телом > по документации протокола и поведению совместимых клиентов. Также проверяй
> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. И проверяй
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2). > **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).
### 3.1 Совсем не обрабатываются (вернут `?;`) ### 3.1 Совсем не обрабатываются (вернут `?;`)
@@ -339,9 +335,9 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) | | **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма | | **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`) | | **Компандер, 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` | нет тюнера | | **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-команду (чек-лист) ## 6. Как добавить новую CAT-команду (чек-лист)
1. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis). 1. **Сверить формат** с документацией протокола и тестами совместимости.
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле 2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`). в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер 3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
+3 -3
View File
@@ -249,11 +249,11 @@ end;
(блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный (блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
один `sampling_frequency`). один `sampling_frequency`).
- **Размер TX-буфера** масштабируется от device-rate (модель SoapyPlutoSDR): - **Размер TX-буфера** масштабируется от device-rate:
`round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384, `round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки. 1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`). FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
- Подтверждено по каноничному драйверу (SoapyPlutoSDR): TX full-scale = 32768, - Для TX используется full-scale 32768,
MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет. MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.
### 4.7 Усиление / телеметрия ### 4.7 Усиление / телеметрия
@@ -394,7 +394,7 @@ end;
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом 2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество 192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
full-duplex >1 Msps — TX-буфер масштабируется от rate (модель SoapyPlutoSDR). full-duplex >1 Msps — TX-буфер масштабируется от rate.
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести 3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
`SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio. `SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат). 4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
+13 -13
View File
@@ -1,11 +1,11 @@
# Мультислайсы и панадаптеры (как во FlexRadio) # Мультислайсы и панадаптеры
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`, Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`, коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
`3049f91`, `a3ac459`). `3049f91`, `a3ac459`).
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с **2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер** edge-маркером» принята модель **аппаратный DDC = отдельный панадаптер**
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера. (свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
альтернативы» в конце файла. Реализация Этапа 3 не начата. альтернативы» в конце файла. Реализация Этапа 3 не начата.
@@ -44,9 +44,9 @@ edge-маркером» принята Flex-модель: **аппаратный
--- ---
## Модель FlexRadio (итог разбора 2026-07-10) ## Модель панадаптеров и слайсов (итог разбора 2026-07-10)
Во Flex **две независимые сущности**, и это ключ ко всему дизайну: В интерфейсе используются **две независимые сущности**, и это ключ ко всему дизайну:
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой - **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада. широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
@@ -58,7 +58,7 @@ edge-маркером» принята Flex-модель: **аппаратный
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса, ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
6600 — 4/4, 6700 — 8/8. 6600 — 4/4, 6700 — 8/8.
**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit): **Второй приёмник (второй АЦП)** представлен отдельным трактом захвата:
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один. полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны** У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную (ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
@@ -66,8 +66,8 @@ edge-маркером» принята Flex-модель: **аппаратный
двух SCU) — отдельный режим поверх. двух SCU) — отдельный режим поверх.
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с **Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный перетаскиваемыми разделителями). EWSDR дополнительно поддерживает вынос
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели. панафолла в отдельное окно ОС.
--- ---
@@ -86,12 +86,12 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт 2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1 (WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся (каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
один слайс в его центре (поведение Flex). один слайс в его центре.
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке). 3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах Показываем именно АЦП, а не антенну: на HPSDR-платах
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4). ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H): 4. Буквы слайсов — глобальный пул A..G через все паны:
буква = личность слайса, пан = место жительства. буква = личность слайса, пан = место жительства.
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней 5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
прячет/блокирует кнопки, а не хардкодит «if Pluto». прячет/блокирует кнопки, а не хардкодит «if Pluto».
@@ -324,7 +324,7 @@ ChangeSampleRate).
### 3.3 — UI: второй панадаптер в главном окне ### 3.3 — UI: второй панадаптер в главном окне
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с - **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
`TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист. `TSplitter` между ними. Пан 0 сверху. Высоты сохраняются в настройках.
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги): - **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) · частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0 кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
@@ -480,7 +480,7 @@ ChangeSampleRate).
### 3.5 — Pop-out: панадаптер в отдельном OS-окне ### 3.5 — Pop-out: панадаптер в отдельном OS-окне
Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке. Кнопка `⧉` в шапке выносит панадаптер в отдельное окно ОС.
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает - `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна. `TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
@@ -767,7 +767,7 @@ ApplyAll, тёплый рестарт восстанавливает override п
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
панорамы на своём hard-DDC, представленный edge-маркером у края спектра панорамы на своём hard-DDC, представленный edge-маркером у края спектра
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с (цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC = чипами слайсов). Отвергнуто 2026-07-10 в пользу модели «DDC =
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов — флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
+22 -28
View File
@@ -3,9 +3,9 @@
Ветка разработки: `feature/tci-protocol`. Ветка разработки: `feature/tci-protocol`.
Дата последнего обновления: 2026-08-18. Дата последнего обновления: 2026-08-18.
Эталон протокола — «Протокол TCI, версия 2.0» Expert Electronics Реализация соответствует спецификации «Протокол TCI, версия 2.0»
(`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером** (`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером**
(как ExpertSDR3): порт слушаем мы, клиенты — логгеры, скиммеры, программы по стандартной серверной модели: порт слушаем мы, клиенты — логгеры, скиммеры, программы
цифровых видов, внешние усилители и коммутаторы. цифровых видов, внешние усилители и коммутаторы.
--- ---
@@ -187,13 +187,12 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего | | `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер, ★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
как в ExpertSDR3, где приёмник = DDC. У нас это разошлось с жизнью сразу на в модели, где приёмник соответствует DDC. В EWSDR это разошлось с жизнью сразу на
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
приёмник существует там **только** как слайс главного пана — при нумерации по приёмник существует там **только** как слайс главного пана — при нумерации по
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице. С панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице.
другого конца — клиенты: у MSHV в настройках всего две модели, «TCI Client rx1» Кроме того, некоторые клиенты позволяют выбрать только приёмники 0 и 1.
и «rx2», то есть приёмники 0 и 1, и никакого третьего номера ввести некуда Значит
(`hvrigcontrol.cpp`: `netServPort[3]`/`[4]`, `network.cpp`: `tci_trx`). Значит
«первый добавленный слайс» обязан быть приёмником **1** на любом железе. «первый добавленный слайс» обязан быть приёмником **1** на любом железе.
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
@@ -255,9 +254,8 @@ B независимо от того, где он живёт: на openHPSDR э
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст. слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту; уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту,
для MSHV это вообще фатально — именно ответом на `vfo:<rx>,0;` он завершает завершая инициализацию несуществующего приёмника. Состояние появляется в момент
инициализацию, то есть подключился бы к пустоте. Состояние появляется в момент
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком, создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
включая `tx_enable` с его номером. включая `tx_enable` с его номером.
@@ -501,10 +499,8 @@ web-клиента: явная просьба сильнее умолчания
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться. отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт **★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт
TX-аудио не по своей воле, а строго в ответ на маркер, и все входящие бинарные TX-аудио строго в ответ на маркер и может фильтровать входящие бинарные блоки
блоки фильтрует по номеру приёмника — у MSHV это первая строка обработчика по номеру приёмника. С жёстким нулём в заголовке клиент,
(`network.cpp`: `if (pStream->receiver != tci_trx) return;`, ветка `TxChrono`
там же собирает и отправляет блок). С жёстким нулём в заголовке клиент,
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с `trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
клиентом-модулятором запоминается номер приёмника из его же `TRX` клиентом-модулятором запоминается номер приёмника из его же `TRX`
@@ -536,8 +532,8 @@ TX-аудио не по своей воле, а строго в ответ на
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт. выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
затирало само себя, а `SAVE` через час после `START` отдавал файл, которого у затирало само себя, а `SAVE` через час после `START` отдавало бы файл уже после
ExpertSDR3 давно бы не было. завершения заданного окна записи.
`SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки `SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки
мегабайт, а команда пришла в потоке клиента, который в это время не читает свой мегабайт, а команда пришла в потоке клиента, который в это время не читает свой
сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный
@@ -794,9 +790,8 @@ running`), а сам телеграф уходит **текущему TX-ист
настройки обязана вставать на тот же слайс, что и модуляция; настройки обязана вставать на тот же слайс, что и модуляция;
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире - **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
клиенты фильтруют входящие строки по `arg1`: MSHV, например, отбрасывает клиенты могут фильтровать входящие строки по `arg1`, и отказ, названный
всё, что адресовано не его приёмнику (`network.cpp`: чужим номером, до
`if (ls2.at(0)!=tci_trx) continue;`), — и отказ, названный чужим номером, до
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
@@ -872,8 +867,8 @@ running`), а сам телеграф уходит **текущему TX-ист
Порядок обратный: сперва UI, потом TCI. Порядок обратный: сперва UI, потом TCI.
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих - **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA) узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
и `ANF` — они в TCI настоящие. `ANC` и `DSE` — блоки собственной обработки и `ANF` — они в TCI настоящие. Для `ANC`, `DSE`, `APF` и `NF` потребовались
Expert, не WDSP; `APF` и `NF` были бы новыми узлами в цепочке. бы новые узлы обработки в цепочке WDSP.
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы - **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс. `FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем. Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
@@ -898,7 +893,7 @@ running`), а сам телеграф уходит **текущему TX-ист
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости | | `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный | | DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A | | Поток канала 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). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной | | `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением | | MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
@@ -918,7 +913,7 @@ running`), а сам телеграф уходит **текущему TX-ист
одинаковыми — в телеметрии платы одно значение forward power. одинаковыми — в телеметрии платы одно значение forward power.
У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы ExpertSDR3, которых у `mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы TCI, которых у
нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что
и отсутствие аргумента. и отсутствие аргумента.
@@ -933,7 +928,7 @@ running`), а сам телеграф уходит **текущему TX-ист
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у (`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
нет вовсе. Вторая по существу: канал B у ExpertSDR3 — второй приёмник внутри нет вовсе. Вторая по существу: канал B у TCI — второй приёмник внутри
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком, того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок: рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
@@ -1086,12 +1081,11 @@ running`), а сам телеграф уходит **текущему TX-ист
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
576 кГц (Pluto) та же просьба даёт законные 192 кГц. 576 кГц (Pluto) та же просьба даёт законные 192 кГц.
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане - **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой,
есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё слушается командой `vfo:1,0,<Гц>`, отдаёт своё
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт (то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу маркеры `TX_CHRONO` **под номером 1**; а `trx:0,true,tci` поверх этой же передачи
MSHV, сидящего на втором слайсе; а `trx:0,true,tci` поверх этой же передачи
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает (команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
целиком — вместо прежнего `vfo:1,0,0`. целиком — вместо прежнего `vfo:1,0,0`.
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся - **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
+1 -1
View File
@@ -237,7 +237,7 @@ end;
const const
{ Команды только для чтения: ответ на опрос составной статус, обратно его { Команды только для чтения: ответ на опрос составной статус, обратно его
не пишут ни Kenwood, ни Thetis. Круговой прогон их законно не проходит. } не пишут ни Kenwood, ни CAT. Круговой прогон их законно не проходит. }
READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID'); READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID');
function IsReadOnly(const Cmd: string): Boolean; function IsReadOnly(const Cmd: string): Boolean;
+10 -12
View File
@@ -46,27 +46,25 @@ line-out для всех обнаруженных приёмников. Он н
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио `START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
уже на передаче, прогон аварийно прекращается. уже на передаче, прогон аварийно прекращается.
## Стенд «как MSHV» ## Стенд совместимости TCI-клиента
```sh ```sh
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1» test/tci/mshv_sim.py --rx 0
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2» 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` заголовка); бинарных блоков, по полю `receiver` заголовка);
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в * **инициализация — один запрос и один ответ**: клиент шлёт `vfo:<rx>,0;` раз в
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался — 500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
«Error To Initialize TCI Server». Других причин у этой ошибки нет. «Error To Initialize TCI Server». Других причин у этой ошибки нет.
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а Поэтому стенд диагностирует причину ошибки подключения одной строкой, а заодно
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим** ловит то, что иначе проявляется молча: без `tx_enable` со **своим** номером
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с приёмника клиент не разрешает PTT.
`if (!tci_tx_enable) return;`).
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`, По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
ни TX-аудио он не шлёт. Передачу включает явный `--tx`. ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
+10 -13
View File
@@ -1,22 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит, """Стенд совместимости с TCI-клиентом MSHV.
почему он не подключается.
Зачем: MSHV самый распространённый TCI-клиент для цифры, и ведёт он себя не Зачем: MSHV самый распространённый TCI-клиент для цифры, и ведёт он себя не
так, как наш собственный стенд. Две его особенности определяют всё: так, как наш собственный стенд. Две его особенности определяют всё:
1. ВСЕ входящие строки он фильтрует по номеру приёмника: 1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
if (ls2.at(0)!=tci_trx) continue; // network.cpp RX2 (tci_trx = "1") строка `trx:0,false`
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
не существует в природе. не существует в природе.
2. Инициализация это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в 2. Инициализация это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
500 мс, максимум 5 попыток, и ждёт `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» Соответствие настроек: «TCI Client rx1» приёмник 0, «TCI Client rx2»
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx). приёмник 1.
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
TX-аудио не отправляются. Передающую часть включает явный --tx. TX-аудио не отправляются. Передающую часть включает явный --tx.
@@ -38,8 +35,8 @@ from live_rx_test import WS, split_commands, parse_command # noqa: E402
HDR = struct.Struct("<16I") HDR = struct.Struct("<16I")
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2, # Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс. # частота 48 кГц и буфер TX 24 мс.
MSHV_CHANNELS = "2" MSHV_CHANNELS = "2"
MSHV_SAMPLES = "2048" MSHV_SAMPLES = "2048"
MSHV_TYPE = "float32" MSHV_TYPE = "float32"
@@ -65,7 +62,7 @@ class Verdict:
class MshvClient: class MshvClient:
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения.""" """Минимальная модель поведения совместимого TCI-клиента."""
def __init__(self, ws, rx, verdict): def __init__(self, ws, rx, verdict):
self.ws = ws self.ws = ws
@@ -148,7 +145,7 @@ class MshvClient:
if len(payload) < HDR.size: if len(payload) < HDR.size:
return return
head = HDR.unpack(payload[:HDR.size]) head = HDR.unpack(payload[:HDR.size])
# MSHV: if (pStream->receiver != tci_trx) return; # Блоки других приёмников клиент игнорирует.
if head[0] != int(self.rx): if head[0] != int(self.rx):
self.audio_foreign += 1 self.audio_foreign += 1
return return
+15 -19
View File
@@ -121,7 +121,7 @@ begin
Check('header rate', H.SampleRate = 12000); Check('header rate', H.SampleRate = 12000);
Check('header format', H.Format = LongWord(Ord(tsyInt16))); Check('header format', H.Format = LongWord(Ord(tsyInt16)));
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты // ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на // считают по нему число байт. Объявишь «на
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами. // канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
Check('header length аудио = ×каналы', H.DataLength = 1024); Check('header length аудио = ×каналы', H.DataLength = 1024);
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio))); Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
@@ -484,7 +484,7 @@ begin
(H.StreamType <> LongWord(Ord(tstRXAudio))) or (H.StreamType <> LongWord(Ord(tstRXAudio))) or
(H.Format <> LongWord(Ord(tsyFloat32))) or (H.Format <> LongWord(Ord(tsyFloat32))) or
// ★Правило, по которому живут клиенты: байт данных = length × размер // ★Правило, по которому живут клиенты: байт данных = length × размер
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`). // отсчёта. Так клиент определяет размер полезной нагрузки.
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False; (Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
Inc(Blocks); Inc(Blocks);
end; end;
@@ -2054,7 +2054,7 @@ begin
end; end;
C.WaitText('ready;', 2000); C.WaitText('ready;', 2000);
// Клиент назвался ровно как MSHV: 48 кГц, два канала, блок 2048. // Клиент запросил 48 кГц, два канала и блок 2048.
C.SendText('audio_samplerate:48000;'); C.SendText('audio_samplerate:48000;');
C.SendText('audio_stream_channels:2;'); C.SendText('audio_stream_channels:2;');
C.SendText('audio_stream_samples:2048;'); C.SendText('audio_stream_samples:2048;');
@@ -2174,7 +2174,7 @@ begin
// идти дальше, а не прекратиться до конца передачи. // идти дальше, а не прекратиться до конца передачи.
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных // ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире // ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
// это редкость (на живом MSHV — два неотвеченных маркера за 12 с), но // это редкость, но
// строка ниже печатает просадку, чтобы регресс был виден. // строка ниже печатает просадку, чтобы регресс был виден.
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' + WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
'из ~94/с, просадка %d кадров', [Markers, MinReserve])); 'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
@@ -2182,8 +2182,8 @@ begin
Markers > 40, IntToStr(Markers)); Markers > 40, IntToStr(Markers));
// ── Повторные передачи: бухгалтерия не переезжает в следующую ── // ── Повторные передачи: бухгалтерия не переезжает в следующую ──
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает // ★Регресс с железа. Штатный конец посылки (`trx:N,false`)
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать // обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient // флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара // с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
// миллисекунд против шага тика в полкванта, так что промах выпадал через // миллисекунд против шага тика в полкванта, так что промах выпадал через
@@ -2586,19 +2586,17 @@ begin
// Ровно случай Pluto: панов больше одного там не бывает, и «второй // Ровно случай Pluto: панов больше одного там не бывает, и «второй
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому // приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 — // первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV. // первым номером приёмника после нулевого.
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800, SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
agcMedium, 0.5, -1, '', 0); agcMedium, 0.5, -1, '', 0);
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId)); Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
C.Pump(300); C.Pump(300);
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
// Инициализация MSHV — это ровно один запрос и ровно один ответ // Инициализация совместимого клиента — это один запрос и один ответ.
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
// «Error To Initialize TCI Server»).
C.SendText('vfo:1,0;'); C.SendText('vfo:1,0;');
S := C.WaitText('vfo:1,0,', 1500); S := C.WaitText('vfo:1,0,', 1500);
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S); Check('слайс отвечает на vfo:1,0 при инициализации', S <> '', S);
Check('слайс отдаёт свою частоту', Check('слайс отдаёт свою частоту',
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S); Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
@@ -2611,7 +2609,7 @@ begin
FloatToStr(SV.TargetHz)); FloatToStr(SV.TargetHz));
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер // Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231). // Клиенты могут отбрасывать блоки с чужим receiver.
C.SendText('audio_start:1;'); C.SendText('audio_start:1;');
C.Pump(100); C.Pump(100);
for k := 0 to 40 do for k := 0 to 40 do
@@ -2691,13 +2689,11 @@ begin
while C.NextFrame(Op, Pay) do ; while C.NextFrame(Op, Pay) do ;
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ───────── // ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает // Совместимый клиент шлёт TX-аудио только в ответ на маркер TX_CHRONO и
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if // может отбрасывать входящий блок с чужим receiver. С жёстким нулём в
// (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288 // заголовке клиент, сидящий на втором слайсе
// и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а // (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
// без них он не отправляет ни одного блока. Ровно это и наблюдалось на // без них он не отправляет ни одного блока.
// живом железе с MSHV.
Ctrl.FWDSPReady := True; Ctrl.FWDSPReady := True;
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True); Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
C.SendText('trx:1,true,tci;'); C.SendText('trx:1,true,tci;');
@@ -2717,7 +2713,7 @@ begin
end; end;
end; end;
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n)); Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)', Check('TX слайса: маркер назван номером приёмника',
(n > 0) and (BadRx = 0), IntToStr(BadRx)); (n > 0) and (BadRx = 0), IntToStr(BadRx));
// ★Команда, которая ничего не сделала, не должна перебивать пару // ★Команда, которая ничего не сделала, не должна перебивать пару