mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
chore: refine project documentation
This commit is contained in:
+15
-17
@@ -27,11 +27,9 @@ unit AndromedaMap;
|
||||
«индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
|
||||
контроллера — этим занимается AndromedaPanel.pas.
|
||||
|
||||
Соответствие протоколу Thetis:
|
||||
• действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions;
|
||||
• номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis
|
||||
(MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты
|
||||
12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch).
|
||||
Таблицы соответствуют нумерации команд аппаратной панели. Для LED-индикаторов
|
||||
используются номера 1=MOX, 3=Tune, 7=CTUN и т.д.; свободные слоты 12..17
|
||||
заняты функциями EWSDR (Split/NB/NR/SNB/ANF/Squelch).
|
||||
}
|
||||
|
||||
{$mode objfpc}{$H+}
|
||||
@@ -44,18 +42,18 @@ type
|
||||
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
|
||||
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
|
||||
abCTCSS, abSquelch, abFilterUp, abFilterDown,
|
||||
abVFOAtoB, // копировать VFO A → B (Thetis eBBVFOAtoB)
|
||||
abVFOBtoA, // копировать VFO B → A (Thetis eBBVFOBtoA)
|
||||
abVFOLock, // блокировка перестройки VFO (Thetis eBBVFOLockOnOff)
|
||||
abStartStop, // старт/стоп SDR (Thetis eBBStartStop)
|
||||
abShift); // модификатор band-keypad (Thetis eBBShift) — обрабатывается в панели
|
||||
abVFOAtoB, // копировать VFO A → B
|
||||
abVFOBtoA, // копировать VFO B → A
|
||||
abVFOLock, // блокировка перестройки VFO
|
||||
abStartStop, // старт/стоп SDR
|
||||
abShift); // модификатор band-keypad — обрабатывается в панели
|
||||
|
||||
// Параметр, управляемый поворотным энкодером панели.
|
||||
TAndEncoder = (
|
||||
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
||||
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
|
||||
|
||||
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
|
||||
// Индикатор (LED) панели в порядке протокольной нумерации.
|
||||
TAndIndicator = (
|
||||
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
|
||||
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
|
||||
@@ -67,8 +65,8 @@ const
|
||||
AND_MAX_ENCODERS = 20;
|
||||
|
||||
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
|
||||
// Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based
|
||||
// индекс = «Pushbutton Number» Thetis − 1. Проверено на реальной панели
|
||||
// Внутренний индекс 0-based на единицу меньше номера кнопки в протоколе.
|
||||
// Нумерация проверена на реальной панели
|
||||
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
|
||||
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
|
||||
abMute, // 0 RX1 Mute
|
||||
@@ -121,14 +119,14 @@ const
|
||||
abTune); // 47 TUN
|
||||
|
||||
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
||||
// Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset).
|
||||
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
|
||||
// Внутренний индекс на единицу меньше номера энкодера в протоколе.
|
||||
// Энкодер #1 управляет AGC Level, поэтому используется aeAGCLevel.
|
||||
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
|
||||
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
|
||||
// энкодер «ширина» — в дефолтной раскладке не используется.
|
||||
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
||||
aeAFGain, // 0 RX1 AF Gain
|
||||
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
|
||||
aeAGCLevel, // 1 RX1 AGC Level
|
||||
aeNone, // 2 RX2 AF — нет второго приёмника
|
||||
aeNone, // 3 RX2 AGC
|
||||
aeFilterHigh, // 4 Filter High — верхний край
|
||||
@@ -142,7 +140,7 @@ const
|
||||
aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone);
|
||||
|
||||
// Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет.
|
||||
// 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах.
|
||||
// 1..11 — базовая раскладка панели; 12..17 — функции EWSDR.
|
||||
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
|
||||
{aiNone} 0,
|
||||
{aiMOX} 1,
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
|
||||
// Раскладка зеркалит Thetis: кнопка Shift (0-based 28) — модификатор-защёлка;
|
||||
// Кнопка Shift (0-based 28) — модификатор-защёлка;
|
||||
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
|
||||
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
|
||||
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
|
||||
|
||||
+9
-11
@@ -20,11 +20,10 @@
|
||||
unit AudioInput;
|
||||
|
||||
{
|
||||
PortAudio input — TX microphone capture.
|
||||
Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas.
|
||||
PortAudio input — TX microphone capture, parallel to AudioOutput.pas.
|
||||
|
||||
Key features:
|
||||
- Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR)
|
||||
- Mono input stream, paFloat32, fixed at 48000 Hz
|
||||
- Pa_Initialize called once in Initialize
|
||||
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
|
||||
- Callback (PaInCallback) writes mic samples into the ring buffer
|
||||
@@ -44,7 +43,7 @@ uses
|
||||
|
||||
const
|
||||
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback
|
||||
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono), как в piHPSDR
|
||||
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono)
|
||||
|
||||
type
|
||||
TPaError = LongInt;
|
||||
@@ -110,7 +109,7 @@ type
|
||||
FLastError: string;
|
||||
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
|
||||
|
||||
// Ring buffer (mono double), как в piHPSDR
|
||||
// Ring buffer (mono double)
|
||||
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
|
||||
FInPt: Integer; // write pointer (callback writes)
|
||||
FOutPt: Integer; // read pointer (main thread reads via ReadSample)
|
||||
@@ -208,8 +207,7 @@ const
|
||||
PA_CONTINUE = LongInt(0);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер.
|
||||
// Точно по образцу pa_in_cb из piHPSDR/portaudio.c
|
||||
// Callback — читает mono float32 из inputBuffer и пишет в кольцевой буфер.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function PaInCallback(inputBuffer, outputBuffer: Pointer;
|
||||
@@ -461,7 +459,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open — открывает input stream, как audio_open_input в piHPSDR
|
||||
// Open — открывает входной аудиопоток.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TAudioInput.Open: Boolean;
|
||||
@@ -480,7 +478,7 @@ begin
|
||||
if not Initialize then Exit;
|
||||
|
||||
FillChar(InParam, SizeOf(InParam), 0);
|
||||
InParam.channelCount := 1; // Mono, как в piHPSDR
|
||||
InParam.channelCount := 1; // Mono input
|
||||
InParam.device := FDeviceIndex;
|
||||
InParam.sampleFormat := PA_FLOAT32;
|
||||
|
||||
@@ -533,7 +531,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Close — как audio_close_input в piHPSDR
|
||||
// Close — останавливает и закрывает входной аудиопоток.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TAudioInput.Close;
|
||||
@@ -556,7 +554,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReadSample — как audio_get_next_mic_sample в piHPSDR
|
||||
// ReadSample — возвращает следующий микрофонный сэмпл из буфера.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TAudioInput.ReadSample: Double;
|
||||
|
||||
+13
-15
@@ -20,12 +20,12 @@
|
||||
unit AudioOutput;
|
||||
|
||||
{
|
||||
PortAudio output — реализация по образцу piHPSDR/portaudio.c (DL1YCF)
|
||||
PortAudio output with a buffered low-latency stereo stream.
|
||||
|
||||
Ключевые особенности:
|
||||
- Pa_Initialize вызывается один раз в Create (до открытия стрима)
|
||||
- suggestedLatency = 0.0 (минимум устройства)
|
||||
- Ring buffer из Double (как в оригинале)
|
||||
- Ring buffer из Double
|
||||
- Callback читает по одному сэмплу, обновляет outpt внутри цикла
|
||||
- Low water mark: вставляет тишину + полбуфера silence
|
||||
- High water mark: удаляет лишние сэмплы
|
||||
@@ -43,10 +43,10 @@ uses
|
||||
Classes, SysUtils, Math, SyncObjs, DynLibs;
|
||||
|
||||
const
|
||||
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR
|
||||
MY_RING_BUFFER_SIZE = 9600; // как piHPSDR
|
||||
MY_RING_LOW_WATER = 512; // как piHPSDR
|
||||
MY_RING_HIGH_WATER = 9000; // как piHPSDR
|
||||
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback
|
||||
MY_RING_BUFFER_SIZE = 9600;
|
||||
MY_RING_LOW_WATER = 512;
|
||||
MY_RING_HIGH_WATER = 9000;
|
||||
|
||||
type
|
||||
TPaError = LongInt;
|
||||
@@ -119,8 +119,8 @@ type
|
||||
FPAInited: Boolean; // Pa_Initialize прошла
|
||||
FLastError: string;
|
||||
|
||||
// Ring buffer (interleaved double stereo, как в оригинале)
|
||||
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double; // как piHPSDR
|
||||
// Ring buffer (interleaved double stereo)
|
||||
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double;
|
||||
FInPt: Integer; // audio_buffer_inpt (write)
|
||||
FOutPt: Integer; // audio_buffer_outpt (read)
|
||||
FMutex: TCriticalSection;
|
||||
@@ -159,7 +159,7 @@ type
|
||||
// Найти индекс устройства по имени (-1 если не найдено)
|
||||
function FindDeviceByName(const AName: string): Integer;
|
||||
|
||||
// Пишем стерео double сэмплы — как audio_write() в оригинале
|
||||
// Записывает пару стереосэмплов Double в кольцевой буфер.
|
||||
procedure WriteDouble(Left, Right: Double);
|
||||
// Convenience: массив Single
|
||||
procedure Write(const Left, Right: array of Single; Count: Integer);
|
||||
@@ -215,13 +215,12 @@ const
|
||||
PA_CONTINUE = LongInt(0);
|
||||
PA_NO_DEV = TPaDeviceIndex(-1);
|
||||
|
||||
// Callback modeled after pa_out_cb from piHPSDR/portaudio.c
|
||||
// Callback reads interleaved stereo samples from the ring buffer.
|
||||
function PaOutCallback(inputBuffer, outputBuffer: Pointer;
|
||||
framesPerBuffer: LongWord;
|
||||
timeInfo: Pointer;
|
||||
statusFlags: LongWord;
|
||||
userData: Pointer): LongInt; cdecl;
|
||||
// Точная копия pa_out_cb из piHPSDR/portaudio.c
|
||||
var
|
||||
Audio: TAudioOutput;
|
||||
Out_: PSingle;
|
||||
@@ -478,7 +477,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open — точная последовательность как в audio_open_output()
|
||||
// Open — подготавливает параметры и открывает выходной поток PortAudio.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TAudioOutput.Open: Boolean;
|
||||
@@ -505,7 +504,7 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Точно как в оригинале: bzero + suggestedLatency = 0.0
|
||||
// Обнуляем параметры и запрашиваем минимальную задержку устройства.
|
||||
FillChar(OutParam, SizeOf(OutParam), 0);
|
||||
OutParam.channelCount := 2;
|
||||
OutParam.device := Dev;
|
||||
@@ -597,8 +596,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// WriteDouble — modeled after audio_write() from piHPSDR/portaudio.c
|
||||
// Вызывается per-sample из Write. Мьютекс держится весь цикл в Write.
|
||||
// Вызывается для каждой пары сэмплов из Write. Мьютекс держится весь цикл.
|
||||
procedure TAudioOutput.WriteDouble(Left, Right: Double);
|
||||
var
|
||||
avail: Integer;
|
||||
|
||||
+2
-2
@@ -740,7 +740,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TCATAdapter.SyncTXEQ;
|
||||
// Частоты полос команда не несёт (их в формате Thetis попросту нет) — трогаем
|
||||
// Частоты полос команда не несёт (их в формате CAT попросту нет) — трогаем
|
||||
// только усиления и число полос, сетка остаётся той, что настроена в UI.
|
||||
var
|
||||
T: TTXSettings;
|
||||
@@ -866,7 +866,7 @@ procedure TCATAdapter.SyncAtten;
|
||||
begin FController.SetAtten(FSyncInt); end;
|
||||
|
||||
function TCATAdapter.GetSupplyVolts: Double;
|
||||
// Телеметрия платы; -1 = данных ещё нет (движок отдаст ноль, как Thetis).
|
||||
// Телеметрия платы; -1 означает, что данных ещё нет, и наружу отдаётся ноль.
|
||||
begin Result := FController.FLastSupplyV; end;
|
||||
|
||||
procedure TCATAdapter.DoVfoSwap;
|
||||
|
||||
+61
-62
@@ -19,10 +19,10 @@
|
||||
|
||||
unit CATEngine;
|
||||
{
|
||||
CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions).
|
||||
CATEngine.pas — CAT command engine (Kenwood + extended ZZ* commands).
|
||||
|
||||
Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs
|
||||
реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают
|
||||
Движок поддерживает базовые команды Kenwood и расширения ZZ*. Команды, для
|
||||
которых в EWSDR ещё нет функционала, возвращают
|
||||
заглушку и сопровождены комментарием "STUB: <причина>".
|
||||
|
||||
Формат команд:
|
||||
@@ -160,7 +160,7 @@ type
|
||||
CWTextBusy: TCATGetBoolFunc;
|
||||
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
|
||||
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
|
||||
// (ZZCM — у Thetis это «monitor DISABLE», инверсия делается в движке).
|
||||
// ZZCM означает «monitor DISABLE», инверсия делается в движке.
|
||||
GetCWPitch: TCATGetIntFunc; // Гц
|
||||
SetCWPitch: TCATSetIntProc;
|
||||
GetCWBreakIn: TCATGetBoolFunc;
|
||||
@@ -500,7 +500,7 @@ begin if B then Result := '1' else Result := '0'; end;
|
||||
|
||||
function IsParamless(const Pfx: string): Boolean;
|
||||
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
|
||||
// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка
|
||||
// Команды не принимают аргументов; любой хвост у них — ошибка
|
||||
// формата, а не значение.
|
||||
const
|
||||
NoArg: array[0..9] of string = (
|
||||
@@ -753,7 +753,7 @@ end;
|
||||
|
||||
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
|
||||
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
|
||||
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis —
|
||||
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi],
|
||||
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
|
||||
var v: Integer;
|
||||
begin
|
||||
@@ -769,7 +769,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.StepIdxToHz(Idx: Integer): Double;
|
||||
// Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц).
|
||||
// Шаг настройки по протокольному индексу 0..14, результат в Гц.
|
||||
const Steps: array[0..14] of Double = (
|
||||
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
|
||||
10000, 100000, 250000, 500000, 1000000);
|
||||
@@ -823,8 +823,8 @@ begin
|
||||
Result := ParseZZ(ext, sfx);
|
||||
end else begin
|
||||
sfx := Copy(s, 3, tpos - 3);
|
||||
// Команды без параметров: эталон отбраковывает лишний суффикс в парсере,
|
||||
// по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список
|
||||
// Команды без параметров должны отбраковывать лишний суффикс в парсере
|
||||
// ДО вызова команды. Таблицы ширин у нас нет — держим список
|
||||
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
|
||||
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
|
||||
if pfx = 'AG' then Result := CmdAG(sfx)
|
||||
@@ -933,7 +933,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.CmdCT(const s: string): string;
|
||||
// CT: включение CTCSS — обёртка над ZZTA, как и в эталоне.
|
||||
// CT: включение CTCSS — обёртка над ZZTA.
|
||||
begin
|
||||
Result := ZZTA(s);
|
||||
end;
|
||||
@@ -994,7 +994,7 @@ end;
|
||||
|
||||
function TCATEngine.CmdFW(const s: string): string;
|
||||
// FW: filter width (4 digits) — maps to filter index
|
||||
// Thetis encodes filter by index into 4-digit string 0000-0009
|
||||
// Filter index is encoded as a 4-digit string from 0000 to 0009.
|
||||
var idx: Integer;
|
||||
begin
|
||||
if Length(s) = 4 then begin
|
||||
@@ -1028,7 +1028,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.CmdIF: string;
|
||||
// IF: составной статус — 35 символов ответа (как у Thetis; «37 байт» из
|
||||
// IF: составной статус — 35 символов ответа («37 байт» из
|
||||
// прежнего комментария — это вся посылка вместе с префиксом и ';').
|
||||
var
|
||||
freq, step, incr, rit, xit, tx, mode, split: string;
|
||||
@@ -1108,7 +1108,7 @@ end;
|
||||
|
||||
function TCATEngine.CmdMG(const s: string): string;
|
||||
// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
|
||||
// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
|
||||
// n/1.43, дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
|
||||
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
|
||||
var v: Integer;
|
||||
begin
|
||||
@@ -1159,22 +1159,21 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.CmdOF(const s: string): string;
|
||||
// OF: сдвиг репитера — обёртка над ZZOT, как и в эталоне.
|
||||
// OF: сдвиг репитера — обёртка над ZZOT.
|
||||
begin
|
||||
Result := ZZOT(s);
|
||||
end;
|
||||
|
||||
function TCATEngine.CmdOS(const s: string): string;
|
||||
// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне.
|
||||
// OS: направление сдвига репитера — обёртка над ZZOS.
|
||||
begin
|
||||
Result := ZZOS(s);
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZOS(const s: string): string;
|
||||
// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх,
|
||||
// ZZOS: направление сдвига репитера. В протоколе 0=симплекс, 1=вверх,
|
||||
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
|
||||
// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection),
|
||||
// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение.
|
||||
// ★Некорректное значение трактуется как симплекс для совместимости клиентов.
|
||||
var dir: Integer;
|
||||
begin
|
||||
if Length(s) = 1 then begin
|
||||
@@ -1196,9 +1195,9 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZOT(const s: string): string;
|
||||
// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью
|
||||
// знаками после запятой (вставляет точку после третьего разряда) — численно это
|
||||
// ровно те же герцы, что и у нас. Знак не передаётся, направление — в ZZOS.
|
||||
// ZZOT: величина сдвига репитера, 9 цифр. Поле кодирует МГц с шестью знаками
|
||||
// после запятой, что численно соответствует целому значению в герцах. Знак не
|
||||
// передаётся, направление задаётся в ZZOS.
|
||||
var v: Int64;
|
||||
begin
|
||||
if Length(s) = 9 then begin
|
||||
@@ -1228,7 +1227,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.CmdPR(const s: string): string;
|
||||
// PR: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0');
|
||||
// PR: речевой компрессор вкл/выкл. Базовая команда доступна только для чтения;
|
||||
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
|
||||
// кенвудовские клиенты от него не ломаются.
|
||||
begin
|
||||
@@ -1311,7 +1310,7 @@ end;
|
||||
function TCATEngine.CmdSM(const s: string): string;
|
||||
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
|
||||
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
|
||||
// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все:
|
||||
// Совместимые клиенты присылают селектор '0', '1' или '2'; принимаем все:
|
||||
// второго приёмника нет, отвечаем одним и тем же.
|
||||
var sm: Integer;
|
||||
begin
|
||||
@@ -1401,7 +1400,7 @@ begin
|
||||
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
|
||||
else if ext = 'CB' then ans := ZZCB(s)
|
||||
else if ext = 'CD' then ans := ZZCD(s)
|
||||
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа (UI Thetis)
|
||||
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа в UI
|
||||
else if ext = 'CI' then ans := ZZCI(s)
|
||||
else if ext = 'CL' then ans := ZZCL(s)
|
||||
else if ext = 'CM' then ans := ZZCM(s)
|
||||
@@ -1443,10 +1442,10 @@ begin
|
||||
else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
|
||||
else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет
|
||||
else if ext = 'FT' then ans := ZZFT(s)
|
||||
else if ext = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет
|
||||
else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет
|
||||
else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет
|
||||
else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет
|
||||
else if ext = 'FV' then ans := '' // STUB: чтение вендорской шины (1 байт)
|
||||
else if ext = 'FW' then ans := '' // STUB: чтение вендорской шины (2 байта)
|
||||
else if ext = 'FX' then ans := '' // STUB: запись вендорской шины (1 байт)
|
||||
else if ext = 'FY' then ans := '' // STUB: запись вендорской шины (2 байта)
|
||||
else if ext = 'GA' then ans := ZZGA(s)
|
||||
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
|
||||
else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет
|
||||
@@ -1466,16 +1465,16 @@ begin
|
||||
else if ext = 'IS' then ans := ZZIS(s)
|
||||
else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
|
||||
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
|
||||
else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis
|
||||
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis
|
||||
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis
|
||||
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis
|
||||
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis
|
||||
else if ext = 'JO' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда
|
||||
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда
|
||||
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда
|
||||
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда
|
||||
else if ext = 'KM' then ans := ZZKM(s)
|
||||
else if ext = 'KO' then ans := '' // STUB: без описания у Thetis
|
||||
else if ext = 'KO' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'KS' then ans := ZZKS(s)
|
||||
else if ext = 'KY' then ans := ZZKY(s)
|
||||
else if ext = 'LA' then ans := '' // STUB: уровень RX0 — микшер Thetis
|
||||
else if ext = 'LA' then ans := '' // STUB: уровень RX0 внешнего микшера
|
||||
else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
|
||||
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
|
||||
else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет
|
||||
@@ -1487,7 +1486,7 @@ begin
|
||||
else if ext = 'MA' then ans := ZZMA(s)
|
||||
else if ext = 'MB' then ans := ZZMB(s)
|
||||
else if ext = 'MD' then ans := ZZMD(s)
|
||||
else if ext = 'ME' then ans := '' // STUB: без описания у Thetis
|
||||
else if ext = 'ME' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
|
||||
else if ext = 'MG' then ans := ZZMG(s)
|
||||
else if ext = 'ML' then ans := ZZML
|
||||
@@ -1528,7 +1527,7 @@ begin
|
||||
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
|
||||
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
|
||||
else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF
|
||||
else if ext = 'OJ' then ans := '' // STUB: без описания у Thetis
|
||||
else if ext = 'OJ' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
|
||||
else if ext = 'OS' then ans := ZZOS(s)
|
||||
else if ext = 'OT' then ans := ZZOT(s)
|
||||
@@ -1548,7 +1547,7 @@ begin
|
||||
else if ext = 'PS' then ans := ZZPS(s)
|
||||
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
|
||||
else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено
|
||||
// ZZQK: у Thetis break-in делится на semi и QSK; у нас break-in один
|
||||
// ZZQK различает semi и QSK; у нас break-in один
|
||||
// (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
|
||||
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
|
||||
else if ext = 'QM' then ans := ZZQM
|
||||
@@ -1580,7 +1579,7 @@ begin
|
||||
else if ext = 'SP' then ans := ZZSP(s)
|
||||
else if ext = 'SQ' then ans := ZZSQ(s)
|
||||
else if ext = 'SR' then ans := ZZSR(s)
|
||||
else if ext = 'SS' then ans := '' // STUB: без описания у Thetis
|
||||
else if ext = 'SS' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'ST' then ans := ZZST(s)
|
||||
else if ext = 'SU' then begin ZZSU; ans := ''; end
|
||||
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет
|
||||
@@ -1590,14 +1589,14 @@ begin
|
||||
else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
|
||||
else if ext = 'TA' then ans := ZZTA(s)
|
||||
else if ext = 'TB' then ans := ZZTB(s)
|
||||
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре (UI Thetis)
|
||||
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре
|
||||
else if ext = 'TH' then ans := ZZTH(s)
|
||||
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
|
||||
else if ext = 'TL' then ans := ZZTL(s)
|
||||
else if ext = 'TM' then ans := ZZTM(s)
|
||||
else if ext = 'TO' then ans := ZZTO(s)
|
||||
else if ext = 'TP' then ans := ZZTP(s)
|
||||
else if ext = 'TS' then ans := '' // STUB: датчик температуры Flex 5000
|
||||
else if ext = 'TS' then ans := '' // STUB: неподдерживаемый датчик температуры
|
||||
else if ext = 'TU' then ans := ZZTU(s)
|
||||
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
|
||||
else if ext = 'TX' then ans := ZZTX(s)
|
||||
@@ -1629,7 +1628,7 @@ begin
|
||||
else if ext = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
|
||||
else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет
|
||||
else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет
|
||||
else if ext = 'VW' then ans := '' // STUB: без описания у Thetis
|
||||
else if ext = 'VW' then ans := '' // STUB: команда не описана
|
||||
else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
|
||||
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
|
||||
else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет
|
||||
@@ -1654,7 +1653,7 @@ begin
|
||||
else if ext = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора
|
||||
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
|
||||
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT
|
||||
else if ext = 'WW' then ans := '' // STUB: уровень выхода FlexWire в микшере F1500 — см. ZZWT
|
||||
else if ext = 'WW' then ans := '' // STUB: уровень выхода вендорской шины — см. ZZWT
|
||||
else if ext = 'XA' then ans := ZZXA(s)
|
||||
else if ext = 'XC' then ans := ZZXC
|
||||
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
|
||||
@@ -1846,7 +1845,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZBS(const s: string): string;
|
||||
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
|
||||
// ZZBS: выбор диапазона. В EWSDR поле двузначное, а базовый формат использует
|
||||
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
|
||||
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
|
||||
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
|
||||
@@ -1877,10 +1876,10 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZCD(const s: string): string;
|
||||
// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает
|
||||
// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же
|
||||
// ZZCD: задержка отпускания break-in (hang time), мс. В EWSDR потолок 2000 —
|
||||
// это намеренный предел: столько же
|
||||
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
|
||||
// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры.
|
||||
// Значение выше подрезается по общему правилу, формат — те же 4 цифры.
|
||||
begin
|
||||
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
|
||||
end;
|
||||
@@ -1948,7 +1947,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZEB(const s: string): string;
|
||||
// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis).
|
||||
// ZZEB: значения TX-эквалайзера одной строкой в протокольном формате.
|
||||
// nnn + (nnn){nb+1}, добито нулями до 36 символов;
|
||||
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
|
||||
var
|
||||
@@ -2146,7 +2145,7 @@ end;
|
||||
|
||||
function TCATEngine.ZZKM(const s: string): string;
|
||||
// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений
|
||||
// (окно CW Messages, F1..F8). Write-only, как в Thetis.
|
||||
// (окно CW Messages, F1..F8). Команда доступна только для записи.
|
||||
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
|
||||
// иамбик вкл/выкл доступен через ZZCI.
|
||||
var v: Integer;
|
||||
@@ -2198,7 +2197,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZMG(const s: string): string;
|
||||
// ZZMG: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две
|
||||
// ZZMG: усиление микрофона в дБ. SET — три цифры либо знак+две
|
||||
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
|
||||
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
|
||||
var v: Integer;
|
||||
@@ -2206,7 +2205,7 @@ begin
|
||||
if Length(s) = 0 then
|
||||
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
|
||||
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
|
||||
// как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не
|
||||
// Допустимы две протокольные длины поля. Более короткие поля — ошибка формата, а не
|
||||
// «маленькое значение»: молча крутить усиление по ним нельзя.
|
||||
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
|
||||
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
|
||||
@@ -2220,7 +2219,7 @@ function TCATEngine.ZZML: string;
|
||||
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
|
||||
|
||||
function TCATEngine.ZZMN(const s: string): string;
|
||||
// ZZMN: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты
|
||||
// ZZMN: имя режима по номеру. В базовом формате ZZMN отдаёт пресеты
|
||||
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
|
||||
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
|
||||
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
|
||||
@@ -2441,12 +2440,12 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZRV: string;
|
||||
// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа).
|
||||
// ZZRV: напряжение питания PA, формат «NN.N» (4 символа).
|
||||
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
|
||||
var v: Double;
|
||||
begin
|
||||
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1;
|
||||
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём, как Thetis
|
||||
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём
|
||||
if v > 99.9 then v := 99.9;
|
||||
Result := Format('%04.1f', [v]);
|
||||
end;
|
||||
@@ -2459,7 +2458,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZSA: string;
|
||||
// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO
|
||||
// ZZSA: шаг настройки вниз. В EWSDR шагает активный VFO,
|
||||
// (отдельного «шага VFO A» в контроллере нет).
|
||||
begin
|
||||
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
|
||||
@@ -2652,8 +2651,8 @@ begin
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) —
|
||||
// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его
|
||||
// ZZWA..ZZWS: неподдерживаемые команды внешнего микшера — STUB.
|
||||
// Водопад тут ни при чём: его
|
||||
// уровни живут в ZZDN/ZZDO.
|
||||
function TCATEngine.ZZWA(const s: string): string;
|
||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
@@ -2714,7 +2713,7 @@ function TCATEngine.ZZYR(const s: string): string;
|
||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
|
||||
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
|
||||
// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
|
||||
// служебные команды; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
|
||||
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
|
||||
// Всё — STUB: либо чужое железо, либо в CAT не проброшено.
|
||||
function TCATEngine.ZZZA(const s: string): string;
|
||||
@@ -2732,7 +2731,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZZE(const s: string): string;
|
||||
// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры — номер
|
||||
// ZZZE: Andromeda — поворот энкодера. Верхние 2 цифры кодируют номер
|
||||
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
|
||||
var raw, enc, step: Integer;
|
||||
begin
|
||||
@@ -2797,7 +2796,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
function TCATEngine.ZZZW(const s: string): string;
|
||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
function TCATEngine.ZZZZ: string;
|
||||
// ZZZZ: без описания у Thetis — STUB.
|
||||
// ZZZZ: команда не описана — STUB.
|
||||
begin Result := ''; end;
|
||||
function TCATEngine.ZZZO(const s: string): string;
|
||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
@@ -2806,7 +2805,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||
|
||||
function TCATEngine.ZZCI(const s: string): string;
|
||||
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
|
||||
// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по
|
||||
// (прямой ключ / iambic A / iambic B) — включение выбирает B по
|
||||
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
|
||||
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
|
||||
begin
|
||||
@@ -2827,7 +2826,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCATEngine.ZZCM(const s: string): string;
|
||||
// ZZCM: у Thetis это «CW Monitor DISABLE» — значение инвертировано к сайдтону.
|
||||
// ZZCM означает «CW Monitor DISABLE», значение инвертировано к сайдтону.
|
||||
begin
|
||||
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
|
||||
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
|
||||
|
||||
@@ -31,8 +31,7 @@ unit CATSliceEndpoint;
|
||||
// └── TCATSliceEndpoint ── TCATEngine ── TCP 19091 (слайс B)
|
||||
// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
|
||||
//
|
||||
// Эталон поведения — SmartSDR CAT: порт привязан к слайсу, а флаг Auto TX
|
||||
// («Auto Switch TX Slice») разрешает команде TX самой сделать этот слайс
|
||||
// Порт привязан к слайсу, а флаг Auto TX разрешает команде TX сделать его
|
||||
// источником передачи. Вся защита «в эфире одновременно только один слайс»
|
||||
// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
|
||||
//
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
unit CATTcp;
|
||||
{
|
||||
CATTcp.pas — TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis).
|
||||
CATTcp.pas — многопоточный TCP-сервер команд CAT.
|
||||
|
||||
Архитектура:
|
||||
• Один listening socket (TCATTcpServer)
|
||||
|
||||
+5
-5
@@ -29,8 +29,8 @@ unit CWKeyer;
|
||||
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
|
||||
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
|
||||
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
|
||||
// Эталон — pihpsdr: src/iambic.c (автомат) + cw_shape_buffer в transmitter.c
|
||||
// (огибающая прямо в TX-буфере, мимо WDSP).
|
||||
// Автомат формирует элементы и записывает огибающую прямо в TX-буфер,
|
||||
// минуя WDSP.
|
||||
//
|
||||
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
|
||||
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
|
||||
@@ -228,8 +228,8 @@ type
|
||||
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
|
||||
|
||||
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
|
||||
CTS/DSR/DCD/RI читаются как лепестки. Так же делают Thetis (cwkeyer.cs),
|
||||
hamlib и fldigi — это единственный вход ключа, доступный на любом железе. }
|
||||
CTS/DSR/DCD/RI читаются как лепестки. Такой вход доступен на любом железе
|
||||
через стандартный последовательный интерфейс. }
|
||||
TCWKeyPort = class(TThread)
|
||||
private
|
||||
FLock: TCriticalSection;
|
||||
@@ -780,7 +780,7 @@ begin
|
||||
end;
|
||||
|
||||
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
|
||||
// Автомат иамбика (логика pihpsdr src/iambic.c в терминах элементов):
|
||||
// Автомат иамбика в терминах точек, тире и межэлементных пауз:
|
||||
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
|
||||
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
|
||||
// следующим; режим A памяти не имеет (в этом и вся разница);
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ unit CWMessagesForm;
|
||||
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
|
||||
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
|
||||
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
|
||||
так что окно висит рядом — ровно как окно CWX у Thetis. Те же ячейки
|
||||
поэтому окно располагается рядом. Используются те же ячейки
|
||||
привязаны к F1..F8 в главном окне.
|
||||
|
||||
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ unit CWMorse;
|
||||
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
|
||||
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
|
||||
// нажать и отпустить, решает PC. Именно этот путь используется для
|
||||
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs).
|
||||
// передачи текста через макросы и команду CAT KY.
|
||||
//
|
||||
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
|
||||
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ unit DMRDecoder;
|
||||
sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
|
||||
|
||||
This first layer deliberately stops at burst sync. The following layer will
|
||||
pass aligned dibits to the trimmed dsd-fme DMR/FEC/AMBE core.
|
||||
pass aligned dibits to the DMR/FEC/AMBE decoding core.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
@@ -203,7 +203,7 @@ const
|
||||
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
|
||||
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3;
|
||||
|
||||
// One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight
|
||||
// One bit per sync character: '1'=0, '3'=1. These are the eight
|
||||
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
|
||||
SYNC_TEXT: array[0..7] of string = (
|
||||
'313333111331131131331131', // BS data
|
||||
@@ -512,7 +512,7 @@ begin
|
||||
if FHistoryCount[Phase] >= 24 then
|
||||
begin
|
||||
Distance := PopCount24(FSyncShift[Phase] xor Pat);
|
||||
// Real on-air sync commonly carries one or two sign errors. DSD-FME
|
||||
// Real on-air sync commonly carries one or two sign errors. The
|
||||
// likewise uses a bounded sync tolerance; requiring an exact 24-symbol
|
||||
// match lost almost every superframe in recorded handheld-radio IQ.
|
||||
DeltaSamples := FSampleNo - FLastSyncSample;
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ unit DMRProtocol;
|
||||
{
|
||||
Small, allocation-free DMR burst protocol layer.
|
||||
|
||||
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from
|
||||
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h).
|
||||
Implements CACH/AMBE interleavers and Hamming/Golay matrices.
|
||||
The applicable third-party permissive notice follows.
|
||||
|
||||
Copyright (C) 2010 DSD Author
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
|
||||
@@ -115,7 +115,7 @@ const
|
||||
MAX_FREQ = 20000.0;
|
||||
TOP_H = 44;
|
||||
BOTTOM_H = 82;
|
||||
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под
|
||||
// Кривая соответствует расчёту частотной характеристики WDSP под
|
||||
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
|
||||
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
|
||||
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
|
||||
@@ -357,7 +357,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TEqualizerControl.BuildResponse;
|
||||
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G —
|
||||
// Рассчитывает амплитудную характеристику эквалайзера в дБ без построения FIR. Точки F/G —
|
||||
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
|
||||
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
|
||||
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
|
||||
|
||||
+3
-3
@@ -30,8 +30,8 @@ unit FilterPopup;
|
||||
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
|
||||
длиной FIR (fircore) и никаким пользовательским параметром не управляется.
|
||||
|
||||
Эталон содержимого — Thetis FilterForm.cs (12 слотов + Name/Low/High/Width);
|
||||
графика полосы — наша добавка, у них её нет.
|
||||
Содержимое включает 12 слотов и поля Name/Low/High/Width, а график наглядно
|
||||
показывает положение и форму полосы.
|
||||
|
||||
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) —
|
||||
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
|
||||
@@ -374,7 +374,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TFilterPopup.WidthChange(Sender: TObject);
|
||||
// Width производная: держим центр полосы, разъезжаются оба края (как Thetis).
|
||||
// Width — производная: центр полосы сохраняется, изменяются оба края.
|
||||
var W, C, Lo, Hi: Integer;
|
||||
begin
|
||||
if FLoading then Exit;
|
||||
|
||||
+13
-13
@@ -498,7 +498,7 @@ procedure TDUCIQSenderThread.Execute;
|
||||
const
|
||||
DUC_IQ_PAIRS_PER_PACKET = 240;
|
||||
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
||||
// Пейсинг DUC-FIFO радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread).
|
||||
// Пейсинг DUC-FIFO синхронизирован с аппаратным тактированием радио.
|
||||
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
|
||||
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
|
||||
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
|
||||
@@ -611,7 +611,7 @@ begin
|
||||
DrainVirtualFIFO;
|
||||
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
||||
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
||||
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
||||
// При пустой очереди ждём сигнала о появлении данных.
|
||||
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
|
||||
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
|
||||
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
|
||||
@@ -668,7 +668,7 @@ begin
|
||||
Sleep(50);
|
||||
if Terminated then Break;
|
||||
if not FNet.FConnected then Continue;
|
||||
// Отправляем полный HP с частотами и ALEX — как piHPSDR
|
||||
// Отправляем полный HP-пакет с частотами и состоянием ALEX.
|
||||
FNet.SendFullHP;
|
||||
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
|
||||
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
|
||||
@@ -1351,14 +1351,14 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ALEX фильтры — логика из piHPSDR new_protocol.c
|
||||
// Формирование регистра фильтров ALEX.
|
||||
// Возвращает 32-bit ALEX0 register value
|
||||
// RXFreqHz — частота приёма ADC0
|
||||
// TXFreqHz — частота передачи (или = RXFreqHz при RX)
|
||||
// Transmitting — режим передачи
|
||||
// IsOrion2 — ANAN-7000/8000 (другие BPF)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Преобразует частоту в индекс диапазона 0=160m..10=6m (Thetis-compatible).
|
||||
// Преобразует частоту в протокольный индекс диапазона 0=160m..10=6m.
|
||||
function FreqToBandIndex(FreqHz: Double): Integer;
|
||||
begin
|
||||
if FreqHz >= 39850000 then Result := 10 // 6m
|
||||
@@ -1386,7 +1386,7 @@ end;
|
||||
// XVTR без усиления.
|
||||
// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11
|
||||
// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3.
|
||||
// WidebandBypass — True когда открыт raw ADC wideband. Как Thetis, раскрываем
|
||||
// WidebandBypass — True, когда открыт raw ADC wideband. В этом режиме раскрываем
|
||||
// RX front-end через HF bypass, иначе видна только полоса
|
||||
// текущего HPF/BPF, а не весь ADC span.
|
||||
function CalcAlex0(RXFreqHz, TXFreqHz: Double;
|
||||
@@ -1411,7 +1411,7 @@ begin
|
||||
if not XvtrDisablePA then
|
||||
begin
|
||||
Result := Result or $08000000; // bit 27: T/R relay
|
||||
// Bit 18 = PureSignal (ALEX_PS_BIT в pihpsdr): на TX подключает
|
||||
// Bit 18 = PureSignal: на TX подключает
|
||||
// внутренний мост обратной связи PA к RX-тракту ADC0.
|
||||
if PureSignalTX then
|
||||
Result := Result or $00040000;
|
||||
@@ -1604,8 +1604,8 @@ end;
|
||||
// Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) —
|
||||
// в тех же координатах, в которых маски лежат в настройках и рисуются в UI.
|
||||
// Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP).
|
||||
// Портировано из Thetis (Penny.UpdateExtCtrl/adjustForTXAction), урезано:
|
||||
// без PA-override и split-pins VFO A/B (см. project_oc_control память).
|
||||
// Реализация учитывает только возможности EWSDR: без PA-override и
|
||||
// раздельных пинов VFO A/B.
|
||||
// RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый
|
||||
// пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их
|
||||
// комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON
|
||||
@@ -1735,7 +1735,7 @@ begin
|
||||
if FPanDDCEnabled[ddc] then
|
||||
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]);
|
||||
|
||||
// PureSignal feedback (как pihpsdr new_protocol_receive_specific): на время
|
||||
// PureSignal feedback: на время
|
||||
// PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback,
|
||||
// оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) —
|
||||
// его сэмплы приходят interleaved внутри потока DDC0 (порт 1035).
|
||||
@@ -1866,7 +1866,7 @@ end;
|
||||
|
||||
procedure THPSDRNetwork.SetCWXKey(Down: Boolean);
|
||||
// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка
|
||||
// доставки = один UDP-пакет (так же делает Thetis, cwx.cs setkey).
|
||||
// доставки определяется отправкой одного UDP-пакета.
|
||||
begin
|
||||
if FCWXKey = Down then Exit;
|
||||
FCWXKey := Down;
|
||||
@@ -1945,7 +1945,7 @@ begin
|
||||
Buf[12 + ddc*4] := Ph and $FF;
|
||||
end;
|
||||
|
||||
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой (pihpsdr, bytes 9..16).
|
||||
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой из байтов 9..16.
|
||||
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
|
||||
if FPSTXActive then
|
||||
begin
|
||||
@@ -2000,7 +2000,7 @@ begin
|
||||
end;
|
||||
|
||||
// XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы.
|
||||
// Логика как в Thetis:
|
||||
// Правила установки бита:
|
||||
// в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен)
|
||||
// в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab)
|
||||
if FXvtrEnable then
|
||||
|
||||
+5
-7
@@ -419,7 +419,7 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean;
|
||||
|
||||
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
|
||||
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards.
|
||||
// VoffMV/SensMVA — калибровка датчика (Thetis AmpVoff/AmpSens, ACS712-style).
|
||||
// VoffMV/SensMVA — смещение нуля и чувствительность датчика тока.
|
||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
||||
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double;
|
||||
|
||||
@@ -429,8 +429,7 @@ function ADCToWatts100(RawADC: Word): Double;
|
||||
// Convert raw ADC value to RF power in Watts (ANAN-10)
|
||||
function ADCToWatts10(RawADC: Word): Double;
|
||||
|
||||
// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/
|
||||
// computeRefPower по модели трансивера:
|
||||
// Alex bridge FWD/REV detector → Watts; параметры зависят от модели трансивера:
|
||||
// 0 = ANAN-10/100/100B/100D (Vref 3.3V)
|
||||
// 1 = ANAN-200D / Orion (Vref 5.0V)
|
||||
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
|
||||
@@ -482,7 +481,7 @@ end;
|
||||
function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double;
|
||||
begin
|
||||
// Protocol Appendix A gives the ADC reference voltage only.
|
||||
// The PCB voltage divider (from Thetis source) must also be applied.
|
||||
// The PCB voltage divider must also be applied.
|
||||
// Board 0 (Atlas): no supply voltage measurement.
|
||||
// Boards 1,2,3 (Hermes/Angelia): Vref=3.3V, PCB divider R1=4.7kΩ/R2=0.82kΩ.
|
||||
// Board 4 (Orion): protocol reports supply voltage with a 5.0V constant.
|
||||
@@ -511,8 +510,8 @@ function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
||||
VoffMV: Double; SensMVA: Double): Double;
|
||||
begin
|
||||
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
|
||||
// Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А
|
||||
// (Thetis, ACS712-style); переопределяются калибровкой.
|
||||
// Formula: amps = (mV - Voff) / Sens. Defaults: Voff=360 mV,
|
||||
// Sens=120 mV/A; both values can be overridden by calibration.
|
||||
if (BoardType = 5) and (SensMVA > 0) then
|
||||
begin
|
||||
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
|
||||
@@ -545,7 +544,6 @@ var
|
||||
Bridge, Vref, V: Double;
|
||||
CalOff: Integer;
|
||||
begin
|
||||
// Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower).
|
||||
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
|
||||
case DetModel of
|
||||
1: begin // ANAN-200D / Orion
|
||||
|
||||
+11
-13
@@ -266,7 +266,7 @@ type
|
||||
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
|
||||
// --- CAT ---
|
||||
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
|
||||
FTCIAdapter: TTCIAdapter; // адаптер TCI (WebSocket-сервер Expert Electronics)
|
||||
FTCIAdapter: TTCIAdapter; // адаптер TCI для WebSocket-сервера
|
||||
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
|
||||
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
|
||||
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
|
||||
@@ -749,7 +749,7 @@ type
|
||||
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
|
||||
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
|
||||
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
|
||||
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // Flex-style смена банда пана
|
||||
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // смена диапазона отдельного пана
|
||||
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
|
||||
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
|
||||
// Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней.
|
||||
@@ -4027,7 +4027,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
// Маленький FFT даёт новые строки чаще.
|
||||
if FSpectrumDirty then
|
||||
begin
|
||||
FSpecView.DrawSpectrum;
|
||||
@@ -4986,12 +4986,11 @@ end;
|
||||
// CTUN ВЫКЛ (классический режим):
|
||||
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||||
//
|
||||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
||||
// CTUN ВКЛ:
|
||||
// FController.FCenterFreq статичен — спектр/водопад не двигаются
|
||||
// FController.FVfoA движется по дисплею (маркер гуляет)
|
||||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
||||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
||||
// hits the edge of the display")
|
||||
// Перецентрирование — только когда край полосы фильтра выходит за границу
|
||||
// дисплея.
|
||||
// ---------------------------------------------------------------------------
|
||||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||||
begin
|
||||
@@ -5067,7 +5066,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
|
||||
// DUP toggle — локальный режим дисплея без аппаратной команды.
|
||||
// ---------------------------------------------------------------------------
|
||||
procedure TMainForm.ApplyDUP(Active: Boolean);
|
||||
begin
|
||||
@@ -5401,7 +5400,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
|
||||
// TUN — тон WDSP TXA PostGen + MOX с отдельным уровнем Drive.
|
||||
// ---------------------------------------------------------------------------
|
||||
procedure TMainForm.ApplyTUN(Active: Boolean);
|
||||
begin
|
||||
@@ -6095,8 +6094,7 @@ end;
|
||||
|
||||
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
||||
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
|
||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте. ЛКМ не трогаем —
|
||||
// иначе окно вылезало бы при каждом обычном переключении фильтра.
|
||||
begin
|
||||
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
|
||||
@@ -7185,7 +7183,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
|
||||
// Flex-style смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
|
||||
// Смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
|
||||
// band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC
|
||||
// (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы).
|
||||
var
|
||||
@@ -8904,7 +8902,7 @@ end;
|
||||
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
||||
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
||||
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
||||
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
|
||||
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3).
|
||||
procedure TMainForm.ApplyXvtrToNetwork;
|
||||
begin
|
||||
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
||||
|
||||
+2
-2
@@ -217,7 +217,7 @@ const
|
||||
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
|
||||
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
|
||||
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
|
||||
// Размер TX-буфера масштабируется от sample-rate (как SoapyPlutoSDR):
|
||||
// Размер TX-буфера масштабируется от sample-rate:
|
||||
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
|
||||
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
||||
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
||||
@@ -387,7 +387,7 @@ begin
|
||||
end;
|
||||
|
||||
function TxBufPairsForRate(Rate: Integer): Integer;
|
||||
// Размер TX-буфера в IQ-парах под device-rate (как SoapyPlutoSDR): round(rate/60)
|
||||
// Размер TX-буфера в IQ-парах под device-rate: round(rate/60)
|
||||
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
|
||||
// 576k→16384, 1536k→32768, 3072k→65536.
|
||||
var
|
||||
|
||||
+2
-2
@@ -116,7 +116,7 @@ const
|
||||
CLR_INPUT = TColor($00FFFFFF);
|
||||
CLR_INPUT_TEXT = TColor($00202020);
|
||||
|
||||
// Лампа feedback — пороги от цели PSFBTarget (Thetis-Enhanced eu2av):
|
||||
// Лампа feedback — пороги относительно цели PSFBTarget:
|
||||
// >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет
|
||||
// сигнала. TColor = BGR.
|
||||
CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал)
|
||||
@@ -390,7 +390,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TPureSignalPopup.DefPkClick(Sender: TObject);
|
||||
// Дефолт SetPk для протокола 2 (Thetis clsHardwareSpecific.PSDefaultPeak).
|
||||
// Значение SetPk по умолчанию для протокола 2.
|
||||
begin
|
||||
FSePk.Value := 0.2899; // OnChange → Apply
|
||||
end;
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ type
|
||||
procedure ClearDUCIQQueue; virtual;
|
||||
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
|
||||
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
|
||||
// первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto
|
||||
// первых миллисекунд). База/Pluto — no-op: у Pluto
|
||||
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
|
||||
procedure PrimeDUCIQ(Pairs: Integer); virtual;
|
||||
|
||||
|
||||
+42
-45
@@ -428,7 +428,7 @@ type
|
||||
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
|
||||
FPSSingleCal: Boolean; // запрошена одиночная калибровка
|
||||
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
|
||||
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
|
||||
FPSCmdState: Integer; // состояние автомата команд PureSignal
|
||||
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
|
||||
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
|
||||
FPSAASaveSingle: Integer;
|
||||
@@ -497,7 +497,7 @@ type
|
||||
FSpecRefLevel: Double;
|
||||
FSpecRange: Double;
|
||||
FSpecGridStep: Double;
|
||||
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
|
||||
FTXSpecRefLevel: Double; // опорный уровень TX-сетки при FTransmitting
|
||||
FTXSpecRange: Double;
|
||||
FTXSpecGridStep: Double;
|
||||
|
||||
@@ -718,7 +718,7 @@ type
|
||||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||||
procedure ApplyModeFilter;
|
||||
|
||||
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
|
||||
// Вычисляет аппаратный drive-байт из текущего состояния мощности.
|
||||
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
|
||||
function CalcDriveByte: Byte;
|
||||
|
||||
@@ -1152,8 +1152,8 @@ type
|
||||
function CWSettings: TCWSettings;
|
||||
procedure SetCWSettings(const C: TCWSettings);
|
||||
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
||||
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон
|
||||
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||||
// CWX в High Priority — точки/тире рисует прошивка по этому биту.
|
||||
// Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||||
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
||||
procedure CWXSend(const Text: string);
|
||||
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
|
||||
@@ -1192,8 +1192,8 @@ type
|
||||
procedure SetPureSignal(On_: Boolean);
|
||||
procedure PSSingleCal; // одиночная калибровка
|
||||
procedure PSReset; // выключить и сбросить коррекцию
|
||||
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis
|
||||
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX.
|
||||
// Тик PS (~10 Гц, из UI-таймера): автомат команд, опрос GetPSInfo и
|
||||
// автоматическая регулировка AttOnTX по уровню обратной связи.
|
||||
procedure PureSignalTick;
|
||||
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
|
||||
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
|
||||
@@ -1202,7 +1202,7 @@ type
|
||||
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
|
||||
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
|
||||
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
|
||||
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis).
|
||||
// замеры на Anvelina PRO3; для остальных используется значение 152.
|
||||
function PSFBTarget: Integer;
|
||||
function PSCalCount: Integer; // info[5]
|
||||
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
|
||||
@@ -1337,8 +1337,8 @@ const
|
||||
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
|
||||
TUN_TONE_MAG = 0.83;
|
||||
|
||||
// Состояния командной state machine PureSignal (порт Thetis eCMDState;
|
||||
// Restore-ветка (7) не реализована — Save/Restore коррекции нет).
|
||||
// Состояния автомата команд PureSignal. Восстановление сохранённой
|
||||
// коррекции не реализовано.
|
||||
PS_ST_OFF = 0;
|
||||
PS_ST_TURNON_AUTO = 1;
|
||||
PS_ST_AUTOCAL = 2;
|
||||
@@ -1827,7 +1827,7 @@ begin
|
||||
SupplyV := -1.0;
|
||||
SupplyA := -1.0;
|
||||
|
||||
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
|
||||
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1.
|
||||
// Прочие платы — в SupplyVolts-байтах, без тока.
|
||||
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
|
||||
begin
|
||||
@@ -1849,7 +1849,7 @@ begin
|
||||
end;
|
||||
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
||||
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
||||
// (как Thetis PABandCalMult).
|
||||
// и используется только для калибровки показаний мощности.
|
||||
Bi := TxBandIdx;
|
||||
Is6m := Bi = BAND_6M;
|
||||
DetK := 1.0;
|
||||
@@ -1859,7 +1859,7 @@ begin
|
||||
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
||||
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
||||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
|
||||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW); только при значимой
|
||||
// fwd-мощности.
|
||||
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
||||
begin
|
||||
@@ -1884,8 +1884,8 @@ begin
|
||||
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
||||
FHPpOverload := Status.ADCOverload;
|
||||
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
||||
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
|
||||
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
|
||||
// Касание обрывает программную передачу, чтобы оператор мог вклиниться.
|
||||
// ★Обрываем по
|
||||
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
|
||||
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
|
||||
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
|
||||
@@ -2989,7 +2989,7 @@ function TRadioController.FrontEndCalDB: Double;
|
||||
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
|
||||
// или само и не должно двигать абсолютную шкалу dBm.
|
||||
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
|
||||
// показания (так же делают Thetis и piHPSDR).
|
||||
// показания.
|
||||
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
|
||||
// его само) → вычитаем текущее значение.
|
||||
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
|
||||
@@ -3356,8 +3356,8 @@ end;
|
||||
|
||||
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||||
Tune: Boolean = False);
|
||||
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог
|
||||
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится
|
||||
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. При
|
||||
// включённом Auto TX слайс сам становится
|
||||
// TX-источником, без него — передаёт, только если оператор уже выбрал его
|
||||
// источником (бейдж TX на флаге).
|
||||
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
|
||||
@@ -4049,7 +4049,7 @@ begin
|
||||
Hi := FFilterHi;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
begin
|
||||
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
||||
// Только RX. TX-полоса НЕ следует за RX-фильтром — её
|
||||
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
|
||||
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
|
||||
FDSPEngine.SetFilter(Lo, Hi);
|
||||
@@ -4148,7 +4148,7 @@ end;
|
||||
// =====================================================================
|
||||
|
||||
function TRadioController.CalcDriveByte: Byte;
|
||||
// Thetis-совместимая формула:
|
||||
// Преобразование целевой мощности в аппаратный байт drive:
|
||||
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
|
||||
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
|
||||
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
|
||||
@@ -4385,9 +4385,9 @@ begin
|
||||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||
|
||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||||
// --- Фильтр: санитизация FM до применения к DSP ----------------------
|
||||
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
||||
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
|
||||
// отдельно по режимам. Иначе у VAR было бы два источника истины —
|
||||
// ровно та болезнь, от которой уходили.
|
||||
ApplyFilterSlot(B.FilterIdx);
|
||||
ApplyModeFilter;
|
||||
@@ -5814,7 +5814,7 @@ end;
|
||||
function TRadioController.CWTXActive: Boolean;
|
||||
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
|
||||
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
|
||||
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570),
|
||||
// состоянии голосовой TXA не запускается вовсе,
|
||||
// а несущую даёт кейер / бит CWX / тон TUN.
|
||||
begin
|
||||
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||||
@@ -5851,10 +5851,9 @@ begin
|
||||
end;
|
||||
|
||||
function TRadioController.CWOptionsByte: Byte;
|
||||
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
||||
// объяснение, почему iambic-бит выставляется и для Mode B).
|
||||
// Формирует DUC Specific byte 5; iambic-бит выставляется и для Mode B.
|
||||
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
|
||||
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
||||
// на замыкание ключа посреди SSB.
|
||||
begin
|
||||
Result := 0;
|
||||
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
|
||||
@@ -5913,10 +5912,10 @@ begin
|
||||
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||||
DUCPkt.DUC0Bits := 24;
|
||||
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||||
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
|
||||
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB с шагом 1.5 dB.
|
||||
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
|
||||
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
|
||||
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
|
||||
// ATT on TX — ступенчатый аттенюатор RX-ADC во время передачи.
|
||||
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
|
||||
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
|
||||
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
||||
@@ -6269,7 +6268,7 @@ end;
|
||||
procedure TRadioController.ApplyTXSettingsToDSP;
|
||||
begin
|
||||
if not FWDSPReady then Exit;
|
||||
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
|
||||
// Для openHPSDR Protocol 2 CFIR постоянно включён:
|
||||
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
|
||||
// имеет, поэтому при смене бэкенда явно выключаем его.
|
||||
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
|
||||
@@ -6418,8 +6417,8 @@ begin
|
||||
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
||||
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
||||
FDSPEngine.PSSetMox(FTransmitting);
|
||||
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis,
|
||||
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому
|
||||
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ. В WDSP режим TXA_CWL —
|
||||
// буквально ветка SSB, поэтому
|
||||
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
|
||||
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
|
||||
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
|
||||
@@ -6476,8 +6475,7 @@ begin
|
||||
begin
|
||||
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
||||
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
|
||||
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
|
||||
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
|
||||
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон.
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
begin
|
||||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||||
@@ -6902,7 +6900,7 @@ procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
|
||||
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
|
||||
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
|
||||
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
|
||||
// ключ (это же ограничение у Thetis).
|
||||
// ключ; это ограничение удалённого управления телеграфом.
|
||||
begin
|
||||
if not CWTXActive then
|
||||
begin
|
||||
@@ -7071,7 +7069,7 @@ begin
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
|
||||
// PureSignal — оркестрация автомата калибровки и обратной связи.
|
||||
// ===========================================================================
|
||||
|
||||
function TRadioController.PSAvailable: Boolean;
|
||||
@@ -7097,7 +7095,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetPureSignal(On_: Boolean);
|
||||
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение —
|
||||
// Кнопка PS включает режим автоматической калибровки. Выключение —
|
||||
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
|
||||
begin
|
||||
if On_ = FPSEnabled then Exit;
|
||||
@@ -7114,7 +7112,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.PSSingleCal;
|
||||
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается.
|
||||
// Одиночная калибровка выключает автоматический режим.
|
||||
begin
|
||||
if not PSAvailable then Exit;
|
||||
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
|
||||
@@ -7140,7 +7138,7 @@ begin
|
||||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||||
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
|
||||
else
|
||||
Result := 152; // классическая цель Thetis
|
||||
Result := 152; // стандартный целевой уровень
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -7150,7 +7148,7 @@ begin
|
||||
if Result < 0 then
|
||||
begin
|
||||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||||
Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av
|
||||
Result := 5.0 // Orion MkII: значение по умолчанию
|
||||
else
|
||||
Result := 0.0; // выключен
|
||||
end;
|
||||
@@ -7203,9 +7201,8 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.PureSignalTick;
|
||||
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis
|
||||
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы)
|
||||
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
|
||||
// Вызывается из UI-таймера (~10 Гц). Автомат управляет калибровкой без
|
||||
// восстановления сохранённой коррекции и подстраивает AttOnTX по feedback.
|
||||
var
|
||||
NewAtt: Integer;
|
||||
FBTarget: Integer;
|
||||
@@ -7287,7 +7284,7 @@ begin
|
||||
PS_ST_TURNOFF:
|
||||
begin
|
||||
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
|
||||
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF).
|
||||
// не дойдёт до LRESET при PSEnabled=true в состоянии TurnOFF.
|
||||
FDSPEngine.PSControl(1, 0, 0, 0);
|
||||
PSSetFeedbackPath(True);
|
||||
FPSResetReq := False;
|
||||
@@ -7300,7 +7297,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) ---
|
||||
// --- Автоматическая регулировка аттенюатора обратной связи ----------
|
||||
// По завершении очередной попытки калибровки: если feedback слишком горячий
|
||||
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
|
||||
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
|
||||
@@ -7350,7 +7347,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetTwoTone(On_: Boolean);
|
||||
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный
|
||||
// 2TON: двухтональник PostGen + MOX. Уровень — обычный
|
||||
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
|
||||
begin
|
||||
if On_ = FTwoTone then Exit;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ unit SerialPort;
|
||||
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
|
||||
телеграфный ключ приходилось глушить заглушкой.
|
||||
|
||||
★И скопировать чужой исходник было нельзя. `Serial.SerSetParams` кладёт
|
||||
★Готовая реализация `Serial.SerSetParams` кладёт
|
||||
B-константу скорости прямо в c_cflag — идиома Linux, где это битовая маска.
|
||||
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
|
||||
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
|
||||
|
||||
+31
-35
@@ -33,8 +33,7 @@ const
|
||||
CFG_BAND_COUNT = 12;
|
||||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
||||
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
|
||||
// TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs):
|
||||
// при каком состоянии передачи пин активен.
|
||||
// TX pin action: определяет, при каком состоянии передачи пин активен.
|
||||
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
|
||||
OCA_TUNE = 1; // только TUNE
|
||||
OCA_TWOTONE = 2; // только 2TON
|
||||
@@ -52,7 +51,7 @@ const
|
||||
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
||||
// (split-LO транспондер; настраивается на
|
||||
// отдельной странице, не в общей XVTR-таблице)
|
||||
// PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults
|
||||
// PA gain defaults (dB) for HERMES/HPSDR hardware.
|
||||
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
|
||||
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
|
||||
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
|
||||
@@ -128,7 +127,7 @@ const
|
||||
('6.25k', '10k', '12.5k', '20k', '25k');
|
||||
|
||||
// --- Редактируемая таблица фильтров -------------------------------------
|
||||
// 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки
|
||||
// 10 пресетов + VAR1/VAR2 на режим. Кромки
|
||||
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
|
||||
// своя — поэтому при применении фильтра конвертировать боковую больше не
|
||||
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
|
||||
@@ -249,11 +248,11 @@ type
|
||||
// Mic gain (dB, применяется в WDSP PanelGain1)
|
||||
MicGainDB: Double; // -10..+30
|
||||
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
|
||||
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB.
|
||||
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2), default 31 dB.
|
||||
AttOnTX: Integer; // 0..31 dB
|
||||
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
|
||||
// Уровень — отдельный от слайдера Drive, проходит через ту же
|
||||
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz.
|
||||
// per-band калибровку. Defaults: level=10 %, freq=750 Hz.
|
||||
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
|
||||
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
|
||||
// TX bandpass filter
|
||||
@@ -301,11 +300,11 @@ type
|
||||
TXWfDetector: Integer;
|
||||
TXWfAvgMode: Integer;
|
||||
TXWfAvgTimeMS: Double;
|
||||
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
|
||||
// TX Grid задаётся отдельно от RX и использует собственные границы.
|
||||
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
|
||||
TXSpecRange: Double; // dB range, e.g. 80
|
||||
TXSpecGridStep: Double; // grid step in dB, e.g. 10
|
||||
// PureSignal (WDSP calcc; дефолты = Thetis PSForm, протокол 2)
|
||||
// PureSignal (WDSP calcc, протокол 2)
|
||||
PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
|
||||
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
|
||||
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
|
||||
@@ -318,7 +317,7 @@ type
|
||||
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
|
||||
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
|
||||
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
|
||||
// Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152
|
||||
// Калибровка фидбек-тракта: на Orion MkII цель 152
|
||||
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
|
||||
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
|
||||
// (Orion MkII → 22, иначе 152)
|
||||
@@ -331,10 +330,9 @@ type
|
||||
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
|
||||
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
|
||||
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
|
||||
// Раскладка байта 5 — HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs)
|
||||
// и pihpsdr (new_protocol.c:1353).
|
||||
// Раскладка байта 5 задана константами HPSDRProtocol.CW_*.
|
||||
TCWSettings = record
|
||||
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
|
||||
Pitch: Integer; // Гц, тон приёма/сайдтона, default 600
|
||||
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
||||
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
||||
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
|
||||
@@ -348,7 +346,7 @@ type
|
||||
Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
|
||||
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
|
||||
HangTimeMS: Integer; // сколько держать TX после последнего элемента
|
||||
RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс)
|
||||
RampMS: Integer; // форма фронта посылки, default 9 мс
|
||||
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
|
||||
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
|
||||
SidetoneHWLevel: Integer; // 0..127
|
||||
@@ -394,8 +392,7 @@ type
|
||||
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
|
||||
// зависит: переключение профиля не должно перерисовывать спектр, менять
|
||||
// задержку тракта или ломать калибровку PS.
|
||||
// Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без
|
||||
// её хвоста из VAC/DSP-буферов, которых у нас нет.
|
||||
// Профиль не содержит параметры VAC и DSP-буферов, которых в EWSDR нет.
|
||||
TTXProfile = record
|
||||
Name: string[31];
|
||||
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
|
||||
@@ -428,7 +425,7 @@ type
|
||||
EQFreqs: array[0..10] of Double;
|
||||
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
|
||||
AMCarrierLevel: Double;
|
||||
// Мощность (Thetis держит Power/Tune_Power в профиле)
|
||||
// Мощность Power/Tune_Power хранится в профиле.
|
||||
DrivePercent: Integer; // 0..100
|
||||
TUNLevel: Integer; // 0..100
|
||||
end;
|
||||
@@ -478,12 +475,12 @@ type
|
||||
SpecPixels: Integer;
|
||||
end;
|
||||
|
||||
// TCI-сервер (протокол Expert Electronics поверх WebSocket). Авторизации в
|
||||
// TCI-сервер поверх WebSocket. Авторизации в
|
||||
// протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые
|
||||
// программы обычно живут на том же ПК, а наружу порт открывается осознанно.
|
||||
TTCISettings = record
|
||||
Enabled: Boolean;
|
||||
Port: Integer; // 1..65535, умолчание 40001 (как у ExpertSDR3)
|
||||
Port: Integer; // 1..65535, по умолчанию 40001
|
||||
BindAddr: string; // '127.0.0.1' или '0.0.0.0'
|
||||
// ★Единственный каталог, куда TCI пишет записи линейного выхода
|
||||
// (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records.
|
||||
@@ -579,12 +576,11 @@ type
|
||||
|
||||
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
|
||||
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
|
||||
// Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis
|
||||
// (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён:
|
||||
// Хранятся в JSON-секции "oc" под MAC. Поддерживается сокращённый набор:
|
||||
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
|
||||
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
|
||||
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings).
|
||||
// VHF-группа — 8 слотов XVTR (аналог Thetis VHF0-13, но по нашим слотам).
|
||||
// VHF-группа — 8 слотов XVTR по внутренней нумерации EWSDR.
|
||||
TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
|
||||
|
||||
TOCSettings = record
|
||||
@@ -599,13 +595,13 @@ type
|
||||
|
||||
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
|
||||
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
|
||||
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
|
||||
// (Vref, bridge voltage и ADC offset).
|
||||
TCalibration = record
|
||||
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
|
||||
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
|
||||
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
|
||||
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360)
|
||||
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120)
|
||||
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ
|
||||
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А
|
||||
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
|
||||
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
|
||||
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
|
||||
@@ -613,11 +609,11 @@ type
|
||||
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
||||
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
||||
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
|
||||
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его
|
||||
// Display Cal Offset: у display-тракта своё окно/детектор, и его
|
||||
// уровень отличается от показаний стрелки на пару дБ.
|
||||
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
||||
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
||||
// Частотная калибровка опорника, ppm (Frequency Calibration в Thetis).
|
||||
// Частотная калибровка опорника, ppm.
|
||||
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
|
||||
// ниже истинной частоты; положительное значение поднимает показания.
|
||||
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров
|
||||
@@ -720,8 +716,8 @@ type
|
||||
// Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
|
||||
CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
||||
CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
|
||||
// Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по
|
||||
// CAT-порту слайса сама назначает этот слайс источником передачи.
|
||||
// Auto TX: команда TX по CAT-порту слайса сама назначает этот слайс
|
||||
// источником передачи.
|
||||
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
||||
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
|
||||
// приложения / общий вход). Имена, а не индексы: порядок перечисления
|
||||
@@ -1710,21 +1706,21 @@ begin
|
||||
T.MicBias := True;
|
||||
T.LineInGainDB := 0.0; // 0 dB
|
||||
T.MicGainDB := 10.0;
|
||||
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи)
|
||||
// ATT on TX — default 31 dB (полная защита RX-ADC во время передачи)
|
||||
T.AttOnTX := 31;
|
||||
// Tune — Thetis defaults
|
||||
// Tune defaults
|
||||
T.TUNLevel := 10;
|
||||
T.TUNFreq := 750;
|
||||
// SSB-фильтр по умолчанию
|
||||
T.FilterLow := 200;
|
||||
T.FilterHigh := 3100;
|
||||
T.FilterNC := 1024;
|
||||
T.FilterMP := 1; // Low Latency (как в piHPSDR)
|
||||
T.FilterMP := 1; // Low Latency
|
||||
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
||||
// Compressor — выкл по умолчанию
|
||||
T.CompressorOn := False;
|
||||
T.CompressorGain:= 5.0;
|
||||
// Leveler — вкл, типичные значения piHPSDR/Thetis
|
||||
// Leveler включён с типичными начальными значениями.
|
||||
T.LevelerOn := True;
|
||||
T.LevelerTop := 5.0;
|
||||
T.LevelerDecay := 500;
|
||||
@@ -1767,7 +1763,7 @@ begin
|
||||
T.TXSpecRefLevel := 0.0;
|
||||
T.TXSpecRange := 80.0;
|
||||
T.TXSpecGridStep := 10.0;
|
||||
// PureSignal — дефолты Thetis PSForm (протокол 2)
|
||||
// PureSignal — значения по умолчанию для протокола 2.
|
||||
T.PSMoxDelay := 0.2;
|
||||
T.PSLoopDelay := 0.0;
|
||||
T.PSTXDelayNs := 150;
|
||||
@@ -1868,7 +1864,7 @@ end;
|
||||
class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
|
||||
begin
|
||||
FillChar(C, SizeOf(C), 0);
|
||||
C.Pitch := 600; // Thetis cw_pitch default
|
||||
C.Pitch := 600; // default CW pitch
|
||||
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
|
||||
C.KeyerMode := 2; // Iambic B
|
||||
C.ReverseKeys := False;
|
||||
@@ -1877,7 +1873,7 @@ begin
|
||||
C.Weight := 50;
|
||||
C.BreakIn := True;
|
||||
C.HangTimeMS := 300;
|
||||
C.RampMS := 9; // Thetis SetCWEdgeLength(9)
|
||||
C.RampMS := 9; // default CW edge length
|
||||
C.RFDelayMS := 0;
|
||||
C.SidetoneHW := True;
|
||||
C.SidetoneHWLevel := 50;
|
||||
|
||||
+10
-11
@@ -1940,7 +1940,7 @@ begin
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
|
||||
// BuildTransmitTab — все настройки TX-тракта.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TSettingsForm.BuildTransmitTab;
|
||||
@@ -3283,7 +3283,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TSettingsForm.BuildCalibrationTab;
|
||||
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
|
||||
// Вкладка Calibration: модель детектора мощности
|
||||
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
|
||||
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
|
||||
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
|
||||
@@ -3428,7 +3428,7 @@ begin
|
||||
end;
|
||||
|
||||
// ── Spectrum / waterfall ────────────────────────────────────────────────
|
||||
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display-
|
||||
// Отдельная от S-метра калибровка Display Cal Offset: у display-
|
||||
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
|
||||
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
|
||||
// сигналы) — это калибровка уровня, а не подавление шума.
|
||||
@@ -3477,7 +3477,7 @@ begin
|
||||
end;
|
||||
|
||||
// ── Reference frequency ─────────────────────────────────────────────────
|
||||
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis):
|
||||
// Одна поправка опорника на всё железо:
|
||||
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
|
||||
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
|
||||
// рознь, и где-то уход больше, где-то меньше.
|
||||
@@ -3691,7 +3691,7 @@ procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
|
||||
begin
|
||||
if FLoading then Exit;
|
||||
// Смена модели детектора подставляет модельные дефолты датчика тока
|
||||
// (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88,
|
||||
// ANAN-7000DLE/Anvelina Pro 3 — 340/88,
|
||||
// остальные — 360/120. Правится руками после, если датчик нестандартный.
|
||||
if Sender = FCmbPwrDetModel then
|
||||
begin
|
||||
@@ -5117,7 +5117,7 @@ begin
|
||||
FCATTcpPort := Spin;
|
||||
|
||||
// ── TCI Server ────────────────────────────────────────────────────────────
|
||||
// Протокол Expert Electronics поверх WebSocket: логгеры, скиммеры, цифра.
|
||||
// TCI поверх WebSocket: логгеры, скиммеры, цифровые виды связи.
|
||||
Grp := MakeGroupPanel(FPageCAT, 'TCI Server',
|
||||
MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H);
|
||||
Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT;
|
||||
@@ -5317,8 +5317,8 @@ begin
|
||||
Spin.OnChange := OnSliceCATCtrlChange;
|
||||
FSliceCATPort[i] := Spin;
|
||||
|
||||
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому
|
||||
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире,
|
||||
// Auto TX: команда TX по этому порту сама делает слайс источником
|
||||
// передачи. Пока кто-то уже в эфире,
|
||||
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
|
||||
// (в ширину группы), пояснение — строкой ниже.
|
||||
Chk := TFlatCheckBox.Create(Self);
|
||||
@@ -6239,9 +6239,8 @@ end;
|
||||
|
||||
// ===========================================================================
|
||||
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
|
||||
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/
|
||||
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/
|
||||
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
|
||||
// Реализован сокращённый набор: без PA-override/hot-switching внешнего PA
|
||||
// и без раздельных пинов VFO A/B. Только openHPSDR —
|
||||
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
|
||||
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
|
||||
// ===========================================================================
|
||||
|
||||
+4
-4
@@ -114,7 +114,7 @@ type
|
||||
FTXOverlay: Boolean;
|
||||
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
||||
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
||||
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||
// TX-шкала НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
||||
FRXEdgeLo, FRXEdgeHi: Integer;
|
||||
FTXEdgeLo, FTXEdgeHi: Integer;
|
||||
@@ -813,7 +813,7 @@ begin
|
||||
M[i].LevelDB := PeakDB(M[i].FreqHz);
|
||||
|
||||
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
|
||||
// Все маркеры — абсолютный уровень шкалы (dBm, как в Thetis); относительная
|
||||
// Все маркеры — абсолютный уровень шкалы в dBm; относительная
|
||||
// величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы.
|
||||
for i := 0 to 3 do
|
||||
M[i].Caption := Format('%.0f', [M[i].LevelDB]);
|
||||
@@ -1398,7 +1398,7 @@ end;
|
||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
|
||||
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
|
||||
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая
|
||||
// два умножения на dst-лейны (нормализация /256, кривая
|
||||
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
|
||||
var
|
||||
Row, Col, Alpha, YMin, A256, I256: Integer;
|
||||
@@ -1701,7 +1701,7 @@ begin
|
||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
||||
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
|
||||
// считается по TX-фильтру: его ширина независима от приёмной,
|
||||
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
||||
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
|
||||
begin
|
||||
|
||||
+25
-27
@@ -71,7 +71,7 @@ const
|
||||
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
|
||||
// и «второй приёмник» там существует только как слайс главного пана — при
|
||||
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
|
||||
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1.
|
||||
// номеров: некоторые клиенты позволяют выбрать только приёмники 0 и 1.
|
||||
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
|
||||
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
|
||||
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
|
||||
@@ -114,9 +114,9 @@ const
|
||||
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
|
||||
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
|
||||
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
|
||||
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: MSHV пишет свой
|
||||
// TX-буфер granule'ами STREAM_C = 4096 отсчётов при 96 кГц (network.cpp), то
|
||||
// есть 42.7 мс, и отвечает на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
|
||||
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: клиент может писать
|
||||
// TX-буфер гранулами по 4096 отсчётов при 96 кГц, то есть 42.7 мс, и отвечать
|
||||
// на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
|
||||
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
|
||||
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
|
||||
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
|
||||
@@ -125,7 +125,7 @@ const
|
||||
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
|
||||
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
|
||||
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после
|
||||
// подключения иначе стартует с pre-roll 31.75 мс против первого ответа MSHV
|
||||
// подключения иначе стартует с pre-roll 31.75 мс до первого ответа клиента
|
||||
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
|
||||
// успевает: аванс появляется только после первого ответа.
|
||||
TCI_TX_LEAD_DEF_MS = 50;
|
||||
@@ -254,7 +254,7 @@ type
|
||||
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
|
||||
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
|
||||
// подрос — попросили ещё больше. Это положительная обратная связь. Период
|
||||
// же равен внутренней грануле клиента (у MSHV — STREAM_C/96 кГц = 42.7 мс)
|
||||
// же равен внутренней грануле клиента (например, 4096/96 кГц = 42.7 мс)
|
||||
// и от нашего темпа не зависит вовсе.
|
||||
FTxGapUs: Double; // оценка периода между пачками, мкс
|
||||
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
|
||||
@@ -857,8 +857,8 @@ begin
|
||||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||||
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
||||
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
|
||||
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
|
||||
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
|
||||
// входящие по arg1 (см. StrTrx), а некоторые также связывают с этим флагом
|
||||
// разрешение передачи. Слайс, созданный
|
||||
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
|
||||
FServer.Broadcast(StrTxEnable(Rx));
|
||||
end;
|
||||
@@ -1195,8 +1195,8 @@ end;
|
||||
|
||||
function TTCIAdapter.StrTrx(Rx: Integer): string;
|
||||
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
|
||||
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
|
||||
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
|
||||
// его слайс. Клиенты могут фильтровать входящие строки по номеру приёмника,
|
||||
// поэтому автору команды
|
||||
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
|
||||
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
|
||||
// слайс сейчас источник передачи (`TxRx`).
|
||||
@@ -1309,8 +1309,7 @@ end;
|
||||
procedure TTCIAdapter.BroadcastTxEnable;
|
||||
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
|
||||
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
|
||||
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача
|
||||
// (`set_ptt`: `if (!tci_tx_enable) return;`).
|
||||
// приёмнике 1 может её отбросить и не разрешить передачу.
|
||||
var Rx: Integer;
|
||||
begin
|
||||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||||
@@ -1830,8 +1829,7 @@ begin
|
||||
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
||||
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
|
||||
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
|
||||
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
|
||||
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
|
||||
// настоящую частоту и завершить инициализацию несуществующего приёмника.
|
||||
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
|
||||
|
||||
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
||||
@@ -2157,7 +2155,7 @@ begin
|
||||
begin
|
||||
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
|
||||
// берётся из аудиопотока этого клиента. Остальные значения
|
||||
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
|
||||
// (mic1/mic2/micpc/ecoder2) называют физические входы TCI, которых
|
||||
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
|
||||
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
|
||||
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
|
||||
@@ -2196,8 +2194,8 @@ begin
|
||||
// ничего не изменила, всё равно её перебивала: клиент, уже
|
||||
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
|
||||
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
|
||||
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает
|
||||
// (network.cpp:231), то есть передача просто замолкает.
|
||||
// с этого мига уходят под номером 0. Клиент второго приёмника может
|
||||
// отбросить такие блоки, и передача просто замолкнет.
|
||||
if B then
|
||||
begin
|
||||
if Started and FromTCI then
|
||||
@@ -2236,7 +2234,7 @@ begin
|
||||
FTxRx := 0;
|
||||
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
|
||||
// идущей передачи. Раньше эти две строки расходились, и штатный
|
||||
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым.
|
||||
// конец посылки (`trx:N,false`) оставлял FTxRunning поднятым.
|
||||
FTxRunning := False;
|
||||
end;
|
||||
finally FTxLock.Leave; end;
|
||||
@@ -2789,7 +2787,7 @@ begin
|
||||
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
|
||||
begin
|
||||
Client.AudioRate := V;
|
||||
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и
|
||||
// Число сэмплов в блоке у TCI своё на каждую частоту (§4.3), и
|
||||
// клиент вправе на это рассчитывать, пока не задал своё явно.
|
||||
Client.AudioSamples := TCIDefaultAudioSamples(V);
|
||||
RestartStreams(Client, tstRXAudio);
|
||||
@@ -3740,7 +3738,8 @@ end;
|
||||
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
|
||||
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
|
||||
//
|
||||
// ★Зачем: MSHV отвечает на первый маркер только через ~41 мс, а сам маркер
|
||||
// ★Зачем: некоторые клиенты отвечают на первый маркер только через ~41 мс,
|
||||
// а сам маркер
|
||||
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
|
||||
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
|
||||
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
|
||||
@@ -4001,9 +4000,8 @@ begin
|
||||
(Sent < TCI_TX_MARKERS_PER_TICK) do
|
||||
begin
|
||||
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
|
||||
// Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV,
|
||||
// network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а
|
||||
// TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono).
|
||||
// Клиент может фильтровать ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver, а
|
||||
// TX-аудио отправлять только в ответ на этот маркер.
|
||||
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
|
||||
// маркеры до него не доходили вовсе.
|
||||
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
|
||||
@@ -4069,8 +4067,8 @@ begin
|
||||
// ровно то, сколько их и распаковалось. Верим меньшему из двух: клиент
|
||||
// вправе прислать короткий хвост, но не длиннее уместившегося. Заведомо
|
||||
// чужое число (мусор в заголовке) просто игнорируем — длину нам и так
|
||||
// ограничил размер кадра. Так же считает и MSHV, когда сам заполняет
|
||||
// заголовок: `t_txStream->length = (cr3/bit_s)`, где cr3 — байты блока.
|
||||
// ограничил размер кадра. Значение заголовка задаётся числом распакованных
|
||||
// отсчётов, а не числом байт блока.
|
||||
if H.DataLength > 0 then
|
||||
begin
|
||||
Want := Integer(H.DataLength);
|
||||
@@ -4103,8 +4101,8 @@ begin
|
||||
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
|
||||
// просили, и зажим в ноль заставил бы переспросить уже полученное.
|
||||
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
|
||||
// отсчётов: первые ответы MSHV — законные нули (network.cpp, ветка
|
||||
// _reset_sta_ <= 5), и сторож не должен считать их отсутствием прогресса.
|
||||
// отсчётов: первые ответы клиента могут быть законными нулями, и сторож не
|
||||
// должен считать их отсутствием прогресса.
|
||||
NowUs := MonotonicUs;
|
||||
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
|
||||
// очередями по нескольку блоков — по своей внутренней зернистости записи.
|
||||
|
||||
+11
-18
@@ -20,7 +20,7 @@
|
||||
unit TCIProtocol;
|
||||
|
||||
{
|
||||
TCIProtocol.pas — протокол TCI 2.0 (Expert Electronics), чистый слой.
|
||||
TCIProtocol.pas — протокол TCI 2.0, чистый слой.
|
||||
|
||||
Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера.
|
||||
Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и
|
||||
@@ -34,7 +34,7 @@ unit TCIProtocol;
|
||||
Зарезервированные символы: ':' ',' ';' — внутри аргументов запрещены и
|
||||
заменяются на '^' '~' '*' (§3.2.1), обратная подстановка — TCIUnescape.
|
||||
|
||||
Регистр значения не имеет. ExpertSDR3 шлёт всё в нижнем регистре, и часть
|
||||
Регистр значения не имеет. TCI шлёт всё в нижнем регистре, и часть
|
||||
клиентов сравнивает строки как есть — поэтому наружу тоже пишем строчными,
|
||||
а внутрь принимаем любой.
|
||||
}
|
||||
@@ -52,17 +52,17 @@ uses
|
||||
const
|
||||
TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL
|
||||
TCI_APP_NAME = 'EWSDR';
|
||||
TCI_DEFAULT_PORT = 40001; // порт TCI-сервера ExpertSDR3
|
||||
TCI_DEFAULT_PORT = 40001; // стандартный порт TCI-сервера
|
||||
TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B)
|
||||
|
||||
// Список видов связи для MODULATIONS_LIST. Первые десять — словарь
|
||||
// ExpertSDR3 (клиенты сверяются именно с ними), dmr/fmraw — наше
|
||||
// расширение: протокол расширяемый по замыслу (§1.4).
|
||||
// Список видов связи для MODULATIONS_LIST. Первые десять — стандартные
|
||||
// режимы TCI; dmr/fmraw — расширения EWSDR. Протокол допускает расширение
|
||||
// этого списка (§1.4).
|
||||
TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw';
|
||||
|
||||
// Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно).
|
||||
// Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока,
|
||||
// а не его размер: больше в один блок не кладут ни ExpertSDR3, ни клиенты.
|
||||
// а не его размер: больше в один блок не кладут ни TCI, ни клиенты.
|
||||
TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке
|
||||
TCI_STREAM_HDR_SIZE = 64; // 16 × uint32
|
||||
TCI_STREAM_MAX = TCI_STREAM_HDR_SIZE + TCI_STREAM_DATA_MAX;
|
||||
@@ -164,7 +164,7 @@ function TCISampleTypeByName(const S: string; out T: TTCISampleType): Boolean;
|
||||
function TCISampleTypeName(T: TTCISampleType): string;
|
||||
function TCISampleBytes(T: TTCISampleType): Integer;
|
||||
|
||||
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у ExpertSDR3
|
||||
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у TCI
|
||||
своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. }
|
||||
function TCIDefaultAudioSamples(RateHz: Integer): Integer;
|
||||
|
||||
@@ -178,22 +178,15 @@ function TCIMaxBlockSamples(T: TTCISampleType; Channels: Integer): Integer;
|
||||
Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как
|
||||
Stream.length/Stream.channels»), а для аудио формулировка §4.3 про
|
||||
AUDIO_STREAM_SAMPLES («arg1 — количество сэмплов, указываемое в поле
|
||||
Stream.length») однажды уже увела нас в «сэмплы на канал». ★Так делать
|
||||
нельзя, и вот доказательство из живого клиента — MSHV, network.cpp:
|
||||
|
||||
int cr2 = (int)pStream->length*bit_s; // сколько БАЙТ разбирать
|
||||
for (int i = 0; i < cr2; i+=chan*bit_s) // шаг = кадр всех каналов
|
||||
...
|
||||
quint32 cr3 = pStream->length*bit_s; //plength = plength*channels*4-bytes
|
||||
|
||||
То есть клиент считает по length число байт блока. Стоит объявить length «на
|
||||
Stream.length») может быть прочитана как «сэмплы на канал». ★Так делать
|
||||
нельзя: клиент вычисляет по length число байт блока. Стоит объявить length «на
|
||||
канал» — и у стерео он разбирает ровно половину блока, а вторую выбрасывает:
|
||||
звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не
|
||||
декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора
|
||||
звука и 1649 не до конца разобранных блоков за минуту.
|
||||
|
||||
Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES — сэмплы НА
|
||||
КАНАЛ (2048 при 48 кГц = 42.7 мс, как у ExpertSDR3), и потолок data[16384]
|
||||
КАНАЛ (2048 при 48 кГц = 42.7 мс), и потолок data[16384]
|
||||
сходится ровно: 2048 × 2 канала × float32. }
|
||||
procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType;
|
||||
Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer);
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@
|
||||
unit TCIServer;
|
||||
|
||||
{
|
||||
TCIServer.pas — транспорт TCI: WebSocket-сервер (роль сервера играем мы,
|
||||
как ExpertSDR3; клиенты — логгеры, скиммеры, программы цифровых видов).
|
||||
TCIServer.pas — транспорт TCI: WebSocket-сервер для логгеров, скиммеров
|
||||
и программ цифровых видов связи.
|
||||
|
||||
Что делает:
|
||||
• слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket
|
||||
|
||||
+3
-3
@@ -222,7 +222,7 @@ type
|
||||
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
|
||||
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
|
||||
начало активной записи затирало само себя, а SAVE через час после START
|
||||
отдавал файл, которого у ExpertSDR3 давно бы не было.
|
||||
отдавал бы файл уже после завершения заданного окна записи.
|
||||
|
||||
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
|
||||
(мьют, пауза приёмника), а время записи всё равно идёт. }
|
||||
@@ -682,8 +682,8 @@ begin
|
||||
F := TCIDecimFactor(ASrcRate, AWantRate);
|
||||
FOutRate := ASrcRate div F;
|
||||
|
||||
// Блок не имеет права вылезти за data[16384]: столько ExpertSDR3 объявил
|
||||
// потолком, и клиенты держат приёмный буфер ровно под него.
|
||||
// Блок не имеет права вылезти за data[16384]: это установленный протоколом
|
||||
// предел, под который клиенты выделяют приёмный буфер.
|
||||
MaxB := TCIMaxBlockSamples(FSampleT, FChannels);
|
||||
FBlock := EnsureRange(ABlock, 1, MaxB);
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ unit VfoOverlay;
|
||||
{$mode objfpc}{$H+}
|
||||
|
||||
{
|
||||
TVfoOverlay — накладка-«флаг слайса» поверх спектра, стиль SmartSDR.
|
||||
TVfoOverlay — накладка-«флаг слайса» поверх спектра.
|
||||
Компоновка (сверху вниз):
|
||||
шапка : [A] 2.7k SPLIT [TX]
|
||||
метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40
|
||||
|
||||
+40
-44
@@ -196,7 +196,7 @@ const
|
||||
TX_MIC_RING = 8192;
|
||||
|
||||
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
|
||||
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
|
||||
// TX rate постоянен: 192k для Hermes/Saturn DUC0,
|
||||
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
|
||||
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
|
||||
TX_SAMPLE_RATE = 192000;
|
||||
@@ -323,7 +323,7 @@ type
|
||||
|
||||
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
|
||||
FSpecBuf: array of Double;
|
||||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
||||
// Отдельный небольшой feed для display-анализатора.
|
||||
FDispBuf: array of Double;
|
||||
FDispPos: Integer;
|
||||
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
||||
@@ -372,7 +372,7 @@ type
|
||||
FWfAvgMode: Integer;
|
||||
FWfAvgTimeMS: Double;
|
||||
|
||||
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
|
||||
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP:
|
||||
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
|
||||
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
|
||||
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
|
||||
@@ -531,7 +531,7 @@ type
|
||||
FFilterLow: Integer;
|
||||
FFilterHigh: Integer;
|
||||
FAGCMode: TWDSPAGCMode;
|
||||
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
|
||||
FAGCTop: Double; // AGC gain in dB
|
||||
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
|
||||
FAGCHangThreshold: Integer; // порог hang 0..100
|
||||
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
||||
@@ -580,7 +580,7 @@ type
|
||||
// главного RXA и для слайсов — иначе дублирование по трём местам.
|
||||
procedure ApplyRXModeSettings(Chan, Mode: Integer);
|
||||
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
|
||||
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
|
||||
// Единственный конвертер знака краёв TX-фильтра для выбранного режима,
|
||||
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
|
||||
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
||||
// --- CW pitch -----------------------------------------------------------
|
||||
@@ -783,7 +783,7 @@ type
|
||||
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||||
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
|
||||
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
|
||||
// по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr).
|
||||
// по 0.49, чтобы сумма оставалась чуть ниже полной шкалы.
|
||||
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
||||
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
|
||||
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
|
||||
@@ -795,7 +795,7 @@ type
|
||||
procedure SetPureSignalArmed(Armed: Boolean);
|
||||
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
|
||||
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
|
||||
// потока (как Thetis/pihpsdr — pscc потокобезопасен относительно
|
||||
// потока; pscc потокобезопасен относительно
|
||||
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
|
||||
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
|
||||
TotalPairs: Integer);
|
||||
@@ -958,7 +958,7 @@ implementation
|
||||
// ===========================================================================
|
||||
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
|
||||
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
|
||||
// Аналог tx_thread в piHPSDR/transmitter.c
|
||||
// Поток обработки TX-аудио и формирования IQ.
|
||||
// ===========================================================================
|
||||
type
|
||||
TDisplayThread = class(TThread)
|
||||
@@ -981,7 +981,7 @@ type
|
||||
|
||||
function UsePanNormOneHz(Detector: Integer): Integer;
|
||||
begin
|
||||
// В Thetis нормализация к 1 Гц включается только для panadapter
|
||||
// Нормализация к 1 Гц включается только для panadapter
|
||||
// и только для detector modes 2/3/4.
|
||||
if Detector in [2, 3, 4] then
|
||||
Result := 1
|
||||
@@ -1029,8 +1029,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TTXDSPThread.Execute;
|
||||
// Аналог tx_thread в piHPSDR/transmitter.c:
|
||||
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||||
// Поток работает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||||
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
||||
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
||||
var
|
||||
@@ -1052,7 +1051,7 @@ begin
|
||||
Dec(AccUs, PeriodMs * 1000);
|
||||
|
||||
// Ждём сигнала от mic-данных ИЛИ истечения периода —
|
||||
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
|
||||
// Здесь timing задаётся таймаутом, синхронизированным с размером буфера.
|
||||
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
|
||||
if Terminated then Break;
|
||||
if not FEngine.FTXActive then Continue;
|
||||
@@ -1085,7 +1084,7 @@ begin
|
||||
|
||||
if Terminated then Break;
|
||||
|
||||
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
|
||||
// Silence-инъекция УБРАНА. Раньше при пустом ring
|
||||
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
|
||||
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
|
||||
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
|
||||
@@ -1098,9 +1097,8 @@ begin
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// DSP поток — обрабатывает IQ пакеты из очереди
|
||||
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
|
||||
// Аналог iq_thread в piHPSDR/new_protocol.c
|
||||
// DSP поток обрабатывает IQ-пакеты из очереди.
|
||||
// Сетевой поток только кладёт пакеты, этот поток занимается DSP.
|
||||
// ===========================================================================
|
||||
type
|
||||
TDSPThread = class(TThread)
|
||||
@@ -1125,7 +1123,7 @@ var
|
||||
begin
|
||||
while not Terminated do
|
||||
begin
|
||||
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
|
||||
// Ждём сигнала от сетевого потока.
|
||||
RTLEventWaitFor(FEngine.FQueueSem, 100);
|
||||
if Terminated then Break;
|
||||
|
||||
@@ -1339,7 +1337,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
||||
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы
|
||||
// Вход — магнитуда TX-аудио-полосы
|
||||
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
|
||||
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
|
||||
var
|
||||
@@ -1376,8 +1374,7 @@ begin
|
||||
end;
|
||||
|
||||
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
|
||||
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr
|
||||
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
|
||||
// Для CWL смещение положительное, для CWU — отрицательное.
|
||||
begin
|
||||
case Mode of
|
||||
MODE_CWL: Result := FCWPitch;
|
||||
@@ -1396,7 +1393,7 @@ end;
|
||||
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
|
||||
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
|
||||
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
|
||||
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338).
|
||||
// т.е. у CWU получается pitch±bw/2.
|
||||
var Off: Integer;
|
||||
begin
|
||||
Off := Round(CWLOOffset(Mode));
|
||||
@@ -1428,7 +1425,7 @@ begin
|
||||
begin
|
||||
// Restore the user's normal voice/data chain after leaving FM RAW.
|
||||
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
|
||||
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам.
|
||||
// bp0 всегда активен сам; здесь обновляем остальную цепочку TX.
|
||||
ApplyTXChainSettings;
|
||||
if Mode = MODE_WFM then ApplyTXWFMSettings;
|
||||
end;
|
||||
@@ -1478,9 +1475,9 @@ begin
|
||||
inherited Create;
|
||||
FSampleRate := SampleRate;
|
||||
FAudioRate := AudioRate;
|
||||
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
|
||||
// BufSize = dsp_size @ AudioRate; внутренний DSP buffer_size = 1024.
|
||||
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
|
||||
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
|
||||
// in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz.
|
||||
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
||||
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
||||
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
||||
@@ -1579,7 +1576,7 @@ begin
|
||||
FTXToneOn := False;
|
||||
FTXTTOn := False;
|
||||
FKeepRXDuringTX := False;
|
||||
// PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2)
|
||||
// PureSignal — значения по умолчанию для протокола 2.
|
||||
FPSArmed := False;
|
||||
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
|
||||
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
|
||||
@@ -2210,7 +2207,7 @@ begin
|
||||
end;
|
||||
|
||||
try
|
||||
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
|
||||
// Анализатор открывается перед DSP-каналом.
|
||||
OpenAnalyzer;
|
||||
if not FAnalyzerOpen then
|
||||
begin
|
||||
@@ -2237,7 +2234,7 @@ begin
|
||||
0,
|
||||
0.010, 0.010, 0.010, 0.010,
|
||||
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
|
||||
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
|
||||
// bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
|
||||
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
|
||||
);
|
||||
|
||||
@@ -2277,7 +2274,7 @@ begin
|
||||
Result := True;
|
||||
|
||||
// Запускаем DSP поток после успешной инициализации WDSP
|
||||
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
|
||||
// Запускаем отдельный поток обработки IQ.
|
||||
FDSPThread := TDSPThread.Create(Self);
|
||||
FDSPThread.Priority := tpHighest; // DSP критический поток
|
||||
|
||||
@@ -2531,7 +2528,7 @@ begin
|
||||
Item^.PanId := 0;
|
||||
|
||||
FQueueHead := NextHead;
|
||||
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
|
||||
RTLEventSetEvent(FQueueSem); // будим DSP-поток
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
|
||||
@@ -2815,7 +2812,7 @@ begin
|
||||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||||
|
||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
||||
// При TX RX-аудио глушим в режиме non-duplex MOX,
|
||||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||||
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
|
||||
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
|
||||
@@ -2824,7 +2821,7 @@ begin
|
||||
FPostTXMuteUntil := 0;
|
||||
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
||||
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
||||
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
||||
// FPostTXMuteUntil-хвост уважаем и при non-duplex MOX.
|
||||
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||
and (FPostTXMuteUntil = 0) then
|
||||
begin
|
||||
@@ -3523,7 +3520,7 @@ begin
|
||||
FAGCMode := Mode;
|
||||
if not FInitialized then Exit;
|
||||
|
||||
// Точно по piHPSDR receiver.c: set_agc()
|
||||
// Настройка параметров AGC приёмного канала.
|
||||
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
|
||||
|
||||
// Читаем обратно hang level и thresh для линии на спектре
|
||||
@@ -3976,7 +3973,7 @@ end;
|
||||
|
||||
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
||||
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
|
||||
// Амплитуды 0.49+0.49 — сумма чуть ниже полной шкалы (как Thetis/pihpsdr).
|
||||
// Амплитуды 0.49+0.49 дают сумму чуть ниже полной шкалы.
|
||||
var
|
||||
F1, F2: Double;
|
||||
begin
|
||||
@@ -4026,9 +4023,8 @@ end;
|
||||
procedure TWDSPEngine.ProcessTXBlock;
|
||||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||||
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
||||
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
||||
// если mic ring buffer содержит достаточно данных — берём их,
|
||||
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
||||
// Если mic ring buffer содержит достаточно данных — берём их,
|
||||
// иначе обрабатываем тишину (нули).
|
||||
var
|
||||
i, tail: Integer;
|
||||
Err: Integer;
|
||||
@@ -4055,7 +4051,7 @@ begin
|
||||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||||
end else
|
||||
begin
|
||||
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
|
||||
// Нет mic-данных — заполняем входной TX-буфер нулями.
|
||||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||||
end;
|
||||
|
||||
@@ -4258,7 +4254,7 @@ end;
|
||||
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
|
||||
DataOffset: Integer; TotalPairs: Integer);
|
||||
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... →
|
||||
// деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data):
|
||||
// Деинтерливим feedback-потоки и передаём их в WDSP pscc:
|
||||
// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
|
||||
var
|
||||
i, n, Pos: Integer;
|
||||
@@ -4329,7 +4325,7 @@ procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
|
||||
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
|
||||
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
|
||||
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
|
||||
// величину и без нас (порт Thetis), но пусть формула будет в одном месте.
|
||||
// величину и без нас, но формула остаётся в одном месте.
|
||||
var L, H, Half: Double;
|
||||
begin
|
||||
Half := 0.0;
|
||||
@@ -4423,7 +4419,7 @@ end;
|
||||
|
||||
procedure TWDSPEngine.PushTXEQProfile;
|
||||
// Единственная точка, отдающая кривую EQ в WDSP.
|
||||
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
|
||||
// F[1..n] — узлы ломаной АЧХ, между ними интерполяция по
|
||||
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
|
||||
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
|
||||
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
|
||||
@@ -4694,7 +4690,7 @@ end;
|
||||
|
||||
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
||||
begin
|
||||
// Thetis-style display FFT range.
|
||||
// Диапазон display FFT.
|
||||
if FFTSize < 4096 then FFTSize := 4096;
|
||||
Result := 4096;
|
||||
while Result < FFTSize do
|
||||
@@ -4705,7 +4701,7 @@ end;
|
||||
|
||||
function TWDSPEngine.CalcDisplayFFTSize: Integer;
|
||||
begin
|
||||
// FFT Size теперь снова управляет display-analyzer как в Thetis,
|
||||
// FFT Size управляет display-analyzer,
|
||||
// но не влияет на аудио DSP цепочку.
|
||||
Result := FFFTSize;
|
||||
end;
|
||||
@@ -4716,7 +4712,7 @@ var
|
||||
TargetFPS: Double;
|
||||
begin
|
||||
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
|
||||
// Thetis/piHPSDR-style timing:
|
||||
// Тайминг обновления дисплея:
|
||||
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
|
||||
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
|
||||
EffectiveFPS := TargetFPS;
|
||||
@@ -4729,7 +4725,7 @@ begin
|
||||
|
||||
if ForWaterfall then
|
||||
begin
|
||||
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
|
||||
// Peak hold на водопаде даёт грязную и залипающую картинку.
|
||||
if AvgMode < 0 then AvgMode := 1;
|
||||
end
|
||||
else
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ const
|
||||
WF_MAX_PIXELS = 4096;
|
||||
|
||||
// Палитры водопада (индексы совпадают с настройкой wf_palette)
|
||||
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема)
|
||||
WF_PAL_CLASSIC = 0; // классическая тёмная / пастельная светлая палитра
|
||||
WF_PAL_INFERNO = 1;
|
||||
WF_PAL_TURBO = 2;
|
||||
|
||||
@@ -139,7 +139,7 @@ begin
|
||||
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
||||
end;
|
||||
|
||||
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1]
|
||||
// Классическая тёмная палитра, T∈[0..1].
|
||||
function ClassicDarkColor(T: Double): LongWord;
|
||||
var
|
||||
Local: Double;
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ unit WebServer;
|
||||
{
|
||||
WebServer.pas — HTTP + WebSocket сервер для удалённого управления трансивером.
|
||||
|
||||
Архитектура (по образцу OpenWebRX):
|
||||
Архитектура веб-подсистемы:
|
||||
─────────────────────────────────
|
||||
HTTP GET / → index.html (см. WebPageHtml)
|
||||
HTTP GET /ws → Upgrade: WebSocket
|
||||
|
||||
+4
-4
@@ -24,7 +24,7 @@ unit WidebandView;
|
||||
|
||||
The network layer feeds 16-bit ADC samples collected from Protocol V4
|
||||
wideband packets. This view feeds them to a dedicated WDSP analyzer and
|
||||
renders a compact Thetis-style wideband panadapter. CPU paint uses a bitmap;
|
||||
renders a compact wideband panadapter. CPU paint uses a bitmap;
|
||||
when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
|
||||
primitives.
|
||||
}
|
||||
@@ -308,16 +308,16 @@ begin
|
||||
if not FWDSPAnalyzerConfigured then
|
||||
begin
|
||||
SampleRate := Max(1, Round(FSampleRateHz));
|
||||
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // Thetis wideband default.
|
||||
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // default wideband averaging.
|
||||
SetAnalyzer(
|
||||
FWDSPAnalyzerID,
|
||||
2,
|
||||
1,
|
||||
1, // Feed like piHPSDR: interleaved I/Q via Spectrum0.
|
||||
1, // Interleaved I/Q feed via Spectrum0.
|
||||
@FWDSPFlp[0],
|
||||
WB_WDSP_FFT,
|
||||
WB_WDSP_BLOCK,
|
||||
6, // Thetis wideband default window.
|
||||
6, // default wideband window.
|
||||
14.0,
|
||||
0, // no overlap for discontinuous WB packets.
|
||||
0,
|
||||
|
||||
+15
-18
@@ -4,8 +4,8 @@
|
||||
Дата последнего обновления: 2026-06-26.
|
||||
|
||||
Документ описывает поддержку аппаратной передней панели Apache Labs
|
||||
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD).
|
||||
За эталон протокола взят Thetis (`Andromeda/Andromeda.cs`, `CAT/CATCommands.cs`).
|
||||
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD)
|
||||
через команды CAT, используемые панелью.
|
||||
|
||||
См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы.
|
||||
|
||||
@@ -84,9 +84,8 @@ Andromeda — это **не отдельный набор команд**, а д
|
||||
|
||||
## 3. Раскладка по умолчанию (AndromedaMap.pas)
|
||||
|
||||
Раскладка **зашита в коде** и зеркалит дефолтную таблицу Thetis
|
||||
(`MakeNewAndromedaDataset`): наш 0-based индекс = «Pushbutton/Encoder Number»
|
||||
Thetis − 1. Номера **проверены на реальной панели** (см.
|
||||
Раскладка **зашита в коде**: внутренний 0-based индекс на единицу меньше номера
|
||||
кнопки или энкодера, передаваемого панелью. Номера **проверены на реальной панели** (см.
|
||||
`andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`.
|
||||
|
||||
### Кнопки (`ZZZP`, индекс 0-based после разбора)
|
||||
@@ -105,12 +104,12 @@ Thetis − 1. Номера **проверены на реальной панел
|
||||
| | | 47 | TUN |
|
||||
|
||||
Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT).
|
||||
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены
|
||||
(в Thetis — на тачскрине), действия в enum сохранены для кастомной раскладки.
|
||||
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены;
|
||||
действия в enum сохранены для пользовательской раскладки.
|
||||
|
||||
### Shift / band-keypad
|
||||
|
||||
Кнопка #28 — модификатор-защёлка (как `eBBShift` в Thetis). Пока активен
|
||||
Кнопка #28 — модификатор-защёлка. Пока он активен
|
||||
(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую
|
||||
(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается
|
||||
повторным Shift либо автоматически после выбора диапазона (non-sticky).
|
||||
@@ -120,7 +119,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
||||
| # | Параметр | Шаг |
|
||||
|---|---|---|
|
||||
| 0 | AF Gain (громкость) | ±1 |
|
||||
| 1 | AGC-T (в Thetis — слайдер «RF») | ±1 dB |
|
||||
| 1 | AGC-T | ±1 dB |
|
||||
| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) |
|
||||
| 11 | Drive (мощность) | ±1 % |
|
||||
| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) |
|
||||
@@ -129,7 +128,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
||||
> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован.
|
||||
> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта.
|
||||
|
||||
### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis)
|
||||
### Индикаторы (`ZZZI`, номера соответствуют протоколу панели)
|
||||
| LED # | Индикатор | LED # | Индикатор |
|
||||
|---|---|---|---|
|
||||
| 1 | MOX | 12 | Split |
|
||||
@@ -142,7 +141,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
||||
| 11 | VFO Lock | | |
|
||||
|
||||
Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR,
|
||||
SNB, ANF, Squelch**. Остальные слоты дефолтной таблицы Thetis (ATU/RIT/XIT)
|
||||
SNB, ANF, Squelch**. Остальные слоты таблицы панели (ATU/RIT/XIT)
|
||||
в ewsdr функций не имеют и не зажигаются.
|
||||
|
||||
---
|
||||
@@ -169,7 +168,7 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
||||
- Транспорт: **serial** (USB CAT).
|
||||
- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие).
|
||||
- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов).
|
||||
- Раскладка — зашитый дефолт, выверена по Thetis и реальной панели.
|
||||
- Раскладка по умолчанию проверена с реальной панелью.
|
||||
- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`.
|
||||
- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`.
|
||||
- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели.
|
||||
@@ -178,11 +177,11 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
||||
### Осталось / возможные улучшения
|
||||
| Задача | Заметки |
|
||||
|---|---|
|
||||
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и passband дисплея (sideband-aware, как в Thetis) |
|
||||
| **Sticky shift** | сейчас shift всегда non-sticky (снимается band-нажатием); в Thetis есть `AndromedaStickyShift` |
|
||||
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и sideband-aware passband дисплея |
|
||||
| **Sticky shift** | сейчас shift всегда non-sticky и снимается нажатием кнопки диапазона; можно добавить сохраняемую настройку |
|
||||
| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) |
|
||||
| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` |
|
||||
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) |
|
||||
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг и пользовательский интерфейс |
|
||||
| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) |
|
||||
| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` |
|
||||
| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано |
|
||||
@@ -197,6 +196,4 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
||||
- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` +
|
||||
push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю).
|
||||
- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы).
|
||||
- Сверяться с форматами Thetis: `Andromeda/Andromeda.cs` (логика панели,
|
||||
`EIndicatorActions`/`EButtonBarActions`/`EEncoderActions`) и
|
||||
`CAT/CATCommands.cs` (`ZZZ*`).
|
||||
- Форматы команд `ZZZ*` и нумерацию элементов проверять по протоколу панели.
|
||||
|
||||
+5
-9
@@ -6,13 +6,9 @@
|
||||
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
|
||||
(19090) остаются за главным приёмником (слайс A) и не тронуты.
|
||||
|
||||
Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу
|
||||
(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»:
|
||||
|
||||
> When enabled Auto Switch TX Slice automatically switches the Transmit Slice to
|
||||
> the VFO A Slice selected above when a CAT TX command is sent on the com port…
|
||||
> If this option is not enabled, the operator will have to manually designate the
|
||||
> Transmit Slice.
|
||||
Каждый порт привязан к определённому слайсу. Флаг **Auto TX** разрешает
|
||||
команде передачи автоматически назначить этот слайс источником TX; без флага
|
||||
источник выбирается оператором вручную.
|
||||
|
||||
---
|
||||
|
||||
@@ -72,8 +68,8 @@ TX-источника атомарны относительно GUI.
|
||||
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
|
||||
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
|
||||
|
||||
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в
|
||||
SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает.
|
||||
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе.
|
||||
Бейдж на флаге это показывает.
|
||||
|
||||
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
|
||||
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
|
||||
|
||||
+32
-36
@@ -5,8 +5,8 @@
|
||||
|
||||
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
|
||||
полный перечень реализованных команд, осознанные заглушки и план дальнейших
|
||||
работ. За эталон протокола взят **Thetis / PowerSDR** (`CATParser.cs`,
|
||||
`CATCommands.cs`, `CATStructs.xml`) — диалект Kenwood TS-2000 + расширения `ZZ*`.
|
||||
работ. Поддерживается диалект Kenwood TS-2000 с расширениями `ZZ*`,
|
||||
используемый совместимыми SDR-приложениями.
|
||||
|
||||
---
|
||||
|
||||
@@ -23,7 +23,7 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
|
||||
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
|
||||
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
|
||||
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
|
||||
| `CATTcp.pas` (~507 строк) | TCP-сервер (Thetis-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
|
||||
| `CATTcp.pas` (~507 строк) | TCP-сервер (CAT-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
|
||||
|
||||
### 1.1 Принципы
|
||||
|
||||
@@ -50,29 +50,29 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
|
||||
|
||||
| | Кол-во |
|
||||
|---|---|
|
||||
| Всего `ZZ*` в Thetis | 315 |
|
||||
| Всего известных `ZZ*` | 315 |
|
||||
| Реально реализовано в EWSDR | **~120** |
|
||||
| Заглушки (отвечают валидным dummy, функции нет) | ~190 |
|
||||
| Совсем не обрабатываются (вернут `?;`) | 5 |
|
||||
|
||||
Подписи всех 315 команд сверены с `CATCommands.cs` (см. §2.5) — комментарию у
|
||||
Подписи всех 315 команд проверены по протокольному поведению (см. §2.5) — комментарию у
|
||||
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
|
||||
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).
|
||||
|
||||
### 1.3 Осознанные отклонения от Thetis
|
||||
### 1.3 Осознанные особенности реализации
|
||||
|
||||
Три места, где EWSDR намеренно расходится с эталоном. Больше таких нет.
|
||||
Три команды имеют локальный формат, сложившийся в EWSDR.
|
||||
|
||||
| Команда | У Thetis | У нас | Почему |
|
||||
| Команда | Базовое значение | Реализация EWSDR | Почему |
|
||||
|---|---|---|---|
|
||||
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
|
||||
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
|
||||
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |
|
||||
|
||||
Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
|
||||
эталон принимает в 150..5000 мс, у нас потолок 2000 — столько же стоит в
|
||||
в EWSDR имеет потолок 2000 мс — столько же стоит в
|
||||
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
|
||||
выше подрезаются, как и всё прочее у Thetis.
|
||||
выше ограничиваются общим механизмом проверки входных значений.
|
||||
|
||||
---
|
||||
|
||||
@@ -153,7 +153,7 @@ S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`,
|
||||
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (00–14) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
|
||||
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |
|
||||
|
||||
Шкала шагов `ZZAU/ZZBP` — `StepIdxToHz` (зеркало Thetis `Step2Freq`):
|
||||
Шкала шагов `ZZAU/ZZBP` задаётся функцией `StepIdxToHz`:
|
||||
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
|
||||
|
||||
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
|
||||
@@ -176,7 +176,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
|
||||
| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
|
||||
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
|
||||
| `ZZET` | кнопка TXEQ | `EQOn` |
|
||||
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
|
||||
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (поле длиной 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
|
||||
| `ZZTO` | мощность настройки | `TUNLevel` |
|
||||
| `ZZTU` | кнопка TUN | `SetTune` |
|
||||
| `ZZUT` | двухтональник 2TON | `SetTwoTone` |
|
||||
@@ -194,7 +194,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
|
||||
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
|
||||
| `ZZCB` | break-in прошивки | `.BreakIn` |
|
||||
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
|
||||
| `ZZCM` | сайдтон (у Thetis — «monitor **disable**», значение инвертировано) | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
|
||||
| `ZZCM` | сайдтон; поле `monitor disable` использует инвертированное значение | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
|
||||
|
||||
**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
|
||||
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
|
||||
@@ -216,10 +216,10 @@ CW: по CAT прилетает что угодно, а контроллер п
|
||||
|
||||
### 2.5 Ревизия алиасов и подписей
|
||||
|
||||
Подписи писались по буквам кода, а не по Thetis, и врали примерно в 150
|
||||
Подписи писались по буквам кода, а не по CAT, и врали примерно в 150
|
||||
местах. Хуже: часть **работающих** команд была привязана не к своей функции —
|
||||
внешний софт получал осмысленный, но неверный ответ, что хуже честной
|
||||
заглушки. Всё сверено с `CATCommands.cs` и исправлено.
|
||||
заглушки. Все подписи проверены и исправлены.
|
||||
|
||||
**Команда делала не своё дело — теперь делает своё:**
|
||||
|
||||
@@ -253,42 +253,39 @@ CW: по CAT прилетает что угодно, а контроллер п
|
||||
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
|
||||
> Во всех случаях старое поведение было отсебятиной.
|
||||
|
||||
**Кенвудовские команды.** Сверены отдельно: все 40 команд, реализованных в
|
||||
Thetis, у нас есть, ширины полей совпадают с `CATStructs.xml`. Исправлено:
|
||||
**Кенвудовские команды.** Сверены отдельно: реализованы все 40 команд,
|
||||
необходимых для совместимости, и проверена ширина полей. Исправлено:
|
||||
|
||||
| Команда | Что было не так |
|
||||
|---|---|
|
||||
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2`, как шлёт Thetis |
|
||||
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2` |
|
||||
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
|
||||
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
|
||||
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
|
||||
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
|
||||
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь обёртка над `ZZTA`, как в эталоне, и сама `ZZTA` проверяет значение строго |
|
||||
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент Thetis обращается как раз к `ZZ*` и не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
|
||||
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь это обёртка над `ZZTA`, а сама `ZZTA` проверяет значение строго |
|
||||
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент расширенных команд не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
|
||||
|
||||
**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу:
|
||||
|
||||
| Где | Что было не так |
|
||||
|---|---|
|
||||
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` |
|
||||
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Теперь лишний суффикс отбраковывается в парсере, а список безаргументных команд хранится в `IsParamless` |
|
||||
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
|
||||
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
|
||||
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS` — там мусор трактуется как симплекс намеренно, по эталону |
|
||||
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS`, где некорректное значение намеренно трактуется как симплекс |
|
||||
| `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
|
||||
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
|
||||
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
|
||||
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
|
||||
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
|
||||
|
||||
Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками
|
||||
после запятой (вставляет точку после третьего разряда) — численно это ровно те
|
||||
же герцы, что пишем мы, поле совместимо. Мусор в `ZZOS` эталон трактует как
|
||||
симплекс (`default` в `String2OffsetDirection`), и мы намеренно повторяем это,
|
||||
а не отвечаем ошибкой.
|
||||
Формат `ZZOT` проверен отдельно: 9 цифр кодируют МГц с шестью знаками после
|
||||
запятой, что численно соответствует целому значению в герцах. Некорректное
|
||||
значение `ZZOS` намеренно трактуется как симплекс для совместимости клиентов.
|
||||
|
||||
Формат ответа на `ZZ*` (`ZZ` + код + **эхо суффикса запроса** + значение) сверен
|
||||
с `CATParser.ParseExtended` — совпадает; это важно для `ZZSM0;`, где запрос
|
||||
несёт селектор приёмника.
|
||||
Формат ответа на `ZZ*` — `ZZ` + код + **эхо суффикса запроса** + значение.
|
||||
Эхо суффикса важно для `ZZSM0;`, где запрос несёт селектор приёмника.
|
||||
|
||||
---
|
||||
|
||||
@@ -314,9 +311,8 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
||||
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
|
||||
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
|
||||
|
||||
> Прежде чем реализовывать новую команду, сверяйся с форматом поля в
|
||||
> `Thetis/.../CAT/CATStructs.xml` (`<nsetparms>` / `<ngetparms>`) и телом
|
||||
> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. И проверяй
|
||||
> Прежде чем реализовывать новую команду, проверяй ширину, знак и смысл поля
|
||||
> по документации протокола и поведению совместимых клиентов. Также проверяй
|
||||
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).
|
||||
|
||||
### 3.1 Совсем не обрабатываются (вернут `?;`)
|
||||
@@ -339,9 +335,9 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
||||
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
|
||||
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма |
|
||||
| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) |
|
||||
| **Микшер Flex 5000 / F1500, FlexWire** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | железо другого вендора |
|
||||
| **Вендорские команды микшера и служебной шины** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | неподдерживаемое оборудование |
|
||||
| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера |
|
||||
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки Thetis |
|
||||
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки CAT |
|
||||
|
||||
---
|
||||
|
||||
@@ -382,7 +378,7 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
||||
|
||||
## 6. Как добавить новую CAT-команду (чек-лист)
|
||||
|
||||
1. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis).
|
||||
1. **Сверить формат** с документацией протокола и тестами совместимости.
|
||||
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
|
||||
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
|
||||
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
|
||||
|
||||
@@ -249,11 +249,11 @@ end;
|
||||
(блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
|
||||
full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
|
||||
один `sampling_frequency`).
|
||||
- **Размер TX-буфера** масштабируется от device-rate (модель SoapyPlutoSDR):
|
||||
- **Размер TX-буфера** масштабируется от device-rate:
|
||||
`round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
|
||||
1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
|
||||
FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
|
||||
- Подтверждено по каноничному драйверу (SoapyPlutoSDR): TX full-scale = 32768,
|
||||
- Для TX используется full-scale 32768,
|
||||
MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.
|
||||
|
||||
### 4.7 Усиление / телеметрия
|
||||
@@ -394,7 +394,7 @@ end;
|
||||
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
|
||||
192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
|
||||
образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
|
||||
full-duplex >1 Msps — TX-буфер масштабируется от rate (модель SoapyPlutoSDR).
|
||||
full-duplex >1 Msps — TX-буфер масштабируется от rate.
|
||||
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
|
||||
`SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
|
||||
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
|
||||
|
||||
+13
-13
@@ -1,11 +1,11 @@
|
||||
# Мультислайсы и панадаптеры (как во FlexRadio)
|
||||
# Мультислайсы и панадаптеры
|
||||
|
||||
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
|
||||
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
|
||||
`3049f91`, `a3ac459`).
|
||||
|
||||
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
|
||||
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер**
|
||||
edge-маркером» принята модель **аппаратный DDC = отдельный панадаптер**
|
||||
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
|
||||
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
|
||||
альтернативы» в конце файла. Реализация Этапа 3 не начата.
|
||||
@@ -44,9 +44,9 @@ edge-маркером» принята Flex-модель: **аппаратный
|
||||
|
||||
---
|
||||
|
||||
## Модель FlexRadio (итог разбора 2026-07-10)
|
||||
## Модель панадаптеров и слайсов (итог разбора 2026-07-10)
|
||||
|
||||
Во Flex **две независимые сущности**, и это ключ ко всему дизайну:
|
||||
В интерфейсе используются **две независимые сущности**, и это ключ ко всему дизайну:
|
||||
|
||||
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
|
||||
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
|
||||
@@ -58,7 +58,7 @@ edge-маркером» принята Flex-модель: **аппаратный
|
||||
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
|
||||
6600 — 4/4, 6700 — 8/8.
|
||||
|
||||
**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit):
|
||||
**Второй приёмник (второй АЦП)** представлен отдельным трактом захвата:
|
||||
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
|
||||
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
|
||||
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
|
||||
@@ -66,8 +66,8 @@ edge-маркером» принята Flex-модель: **аппаратный
|
||||
двух SCU) — отдельный режим поверх.
|
||||
|
||||
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
|
||||
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный
|
||||
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели.
|
||||
перетаскиваемыми разделителями). EWSDR дополнительно поддерживает вынос
|
||||
панафолла в отдельное окно ОС.
|
||||
|
||||
---
|
||||
|
||||
@@ -86,12 +86,12 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
|
||||
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
|
||||
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
|
||||
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
|
||||
один слайс в его центре (поведение Flex).
|
||||
один слайс в его центре.
|
||||
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
||||
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||
Показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
|
||||
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
|
||||
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
||||
4. Буквы слайсов — глобальный пул A..G через все паны:
|
||||
буква = личность слайса, пан = место жительства.
|
||||
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
||||
прячет/блокирует кнопки, а не хардкодит «if Pluto».
|
||||
@@ -324,7 +324,7 @@ ChangeSampleRate).
|
||||
### 3.3 — UI: второй панадаптер в главном окне
|
||||
|
||||
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
|
||||
`TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист.
|
||||
`TSplitter` между ними. Пан 0 сверху. Высоты сохраняются в настройках.
|
||||
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
|
||||
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
|
||||
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
|
||||
@@ -480,7 +480,7 @@ ChangeSampleRate).
|
||||
|
||||
### 3.5 — Pop-out: панадаптер в отдельном OS-окне
|
||||
|
||||
Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке.
|
||||
Кнопка `⧉` в шапке выносит панадаптер в отдельное окно ОС.
|
||||
|
||||
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
|
||||
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
|
||||
@@ -767,7 +767,7 @@ ApplyAll, тёплый рестарт восстанавливает override п
|
||||
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
|
||||
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
|
||||
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
|
||||
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC =
|
||||
чипами слайсов). Отвергнуто 2026-07-10 в пользу модели «DDC =
|
||||
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
|
||||
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
|
||||
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
|
||||
|
||||
+22
-28
@@ -3,9 +3,9 @@
|
||||
Ветка разработки: `feature/tci-protocol`.
|
||||
Дата последнего обновления: 2026-08-18.
|
||||
|
||||
Эталон протокола — «Протокол TCI, версия 2.0» Expert Electronics
|
||||
Реализация соответствует спецификации «Протокол TCI, версия 2.0»
|
||||
(`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером**
|
||||
(как ExpertSDR3): порт слушаем мы, клиенты — логгеры, скиммеры, программы
|
||||
по стандартной серверной модели: порт слушаем мы, клиенты — логгеры, скиммеры, программы
|
||||
цифровых видов, внешние усилители и коммутаторы.
|
||||
|
||||
---
|
||||
@@ -187,13 +187,12 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
|
||||
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
|
||||
|
||||
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
|
||||
как в ExpertSDR3, где приёмник = DDC. У нас это разошлось с жизнью сразу на
|
||||
в модели, где приёмник соответствует DDC. В EWSDR это разошлось с жизнью сразу на
|
||||
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
|
||||
приёмник существует там **только** как слайс главного пана — при нумерации по
|
||||
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице. С
|
||||
другого конца — клиенты: у MSHV в настройках всего две модели, «TCI Client rx1»
|
||||
и «rx2», то есть приёмники 0 и 1, и никакого третьего номера ввести некуда
|
||||
(`hvrigcontrol.cpp`: `netServPort[3]`/`[4]`, `network.cpp`: `tci_trx`). Значит
|
||||
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице.
|
||||
Кроме того, некоторые клиенты позволяют выбрать только приёмники 0 и 1.
|
||||
Значит
|
||||
«первый добавленный слайс» обязан быть приёмником **1** на любом железе.
|
||||
|
||||
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
|
||||
@@ -255,9 +254,8 @@ B независимо от того, где он живёт: на openHPSDR э
|
||||
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
|
||||
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
|
||||
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
|
||||
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту;
|
||||
для MSHV это вообще фатально — именно ответом на `vfo:<rx>,0;` он завершает
|
||||
инициализацию, то есть подключился бы к пустоте. Состояние появляется в момент
|
||||
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту,
|
||||
завершая инициализацию несуществующего приёмника. Состояние появляется в момент
|
||||
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
|
||||
включая `tx_enable` с его номером.
|
||||
|
||||
@@ -501,10 +499,8 @@ web-клиента: явная просьба сильнее умолчания
|
||||
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
|
||||
|
||||
**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт
|
||||
TX-аудио не по своей воле, а строго в ответ на маркер, и все входящие бинарные
|
||||
блоки фильтрует по номеру приёмника — у MSHV это первая строка обработчика
|
||||
(`network.cpp`: `if (pStream->receiver != tci_trx) return;`, ветка `TxChrono`
|
||||
там же собирает и отправляет блок). С жёстким нулём в заголовке клиент,
|
||||
TX-аудио строго в ответ на маркер и может фильтровать входящие бинарные блоки
|
||||
по номеру приёмника. С жёстким нулём в заголовке клиент,
|
||||
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
|
||||
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
|
||||
клиентом-модулятором запоминается номер приёмника из его же `TRX`
|
||||
@@ -536,8 +532,8 @@ TX-аудио не по своей воле, а строго в ответ на
|
||||
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
|
||||
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
|
||||
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
|
||||
затирало само себя, а `SAVE` через час после `START` отдавал файл, которого у
|
||||
ExpertSDR3 давно бы не было.
|
||||
затирало само себя, а `SAVE` через час после `START` отдавало бы файл уже после
|
||||
завершения заданного окна записи.
|
||||
`SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки
|
||||
мегабайт, а команда пришла в потоке клиента, который в это время не читает свой
|
||||
сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный
|
||||
@@ -794,9 +790,8 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
настройки обязана вставать на тот же слайс, что и модуляция;
|
||||
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
|
||||
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
|
||||
клиенты фильтруют входящие строки по `arg1`: MSHV, например, отбрасывает
|
||||
всё, что адресовано не его приёмнику (`network.cpp`:
|
||||
`if (ls2.at(0)!=tci_trx) continue;`), — и отказ, названный чужим номером, до
|
||||
клиенты могут фильтровать входящие строки по `arg1`, и отказ, названный
|
||||
чужим номером, до
|
||||
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
|
||||
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
|
||||
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
|
||||
@@ -872,8 +867,8 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
Порядок обратный: сперва UI, потом TCI.
|
||||
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
|
||||
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
|
||||
и `ANF` — они в TCI настоящие. `ANC` и `DSE` — блоки собственной обработки
|
||||
Expert, не WDSP; `APF` и `NF` были бы новыми узлами в цепочке.
|
||||
и `ANF` — они в TCI настоящие. Для `ANC`, `DSE`, `APF` и `NF` потребовались
|
||||
бы новые узлы обработки в цепочке WDSP.
|
||||
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
|
||||
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
|
||||
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
|
||||
@@ -898,7 +893,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
|
||||
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
|
||||
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A |
|
||||
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а ExpertSDR3 IQ иначе и не шлёт |
|
||||
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а TCI IQ иначе и не шлёт |
|
||||
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
||||
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
||||
|
||||
@@ -918,7 +913,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
одинаковыми — в телеметрии платы одно значение forward power.
|
||||
|
||||
У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения
|
||||
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы ExpertSDR3, которых у
|
||||
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы TCI, которых у
|
||||
нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что
|
||||
и отсутствие аргумента.
|
||||
|
||||
@@ -933,7 +928,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
|
||||
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
|
||||
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
|
||||
нет вовсе. Вторая по существу: канал B у ExpertSDR3 — второй приёмник внутри
|
||||
нет вовсе. Вторая по существу: канал B у TCI — второй приёмник внутри
|
||||
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
|
||||
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
|
||||
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
||||
@@ -1086,12 +1081,11 @@ running`), а сам телеграф уходит **текущему TX-ист
|
||||
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
|
||||
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
|
||||
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
||||
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
|
||||
есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё
|
||||
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой,
|
||||
слушается командой `vfo:1,0,<Гц>`, отдаёт своё
|
||||
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
|
||||
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
|
||||
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу
|
||||
MSHV, сидящего на втором слайсе; а `trx:0,true,tci` поверх этой же передачи
|
||||
маркеры `TX_CHRONO` **под номером 1**; а `trx:0,true,tci` поверх этой же передачи
|
||||
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
|
||||
целиком — вместо прежнего `vfo:1,0,0`.
|
||||
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
||||
|
||||
@@ -237,7 +237,7 @@ end;
|
||||
|
||||
const
|
||||
{ Команды только для чтения: ответ на опрос — составной статус, обратно его
|
||||
не пишут ни Kenwood, ни Thetis. Круговой прогон их законно не проходит. }
|
||||
не пишут ни Kenwood, ни CAT. Круговой прогон их законно не проходит. }
|
||||
READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID');
|
||||
|
||||
function IsReadOnly(const Cmd: string): Boolean;
|
||||
|
||||
+10
-12
@@ -46,27 +46,25 @@ line-out для всех обнаруженных приёмников. Он н
|
||||
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
|
||||
уже на передаче, прогон аварийно прекращается.
|
||||
|
||||
## Стенд «как MSHV»
|
||||
## Стенд совместимости TCI-клиента
|
||||
|
||||
```sh
|
||||
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
|
||||
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
|
||||
test/tci/mshv_sim.py --rx 0
|
||||
test/tci/mshv_sim.py --rx 1 --audio
|
||||
```
|
||||
|
||||
`mshv_sim.py` повторяет логику TCI-клиента MSHV по его исходникам — включая обе
|
||||
особенности, из-за которых он ведёт себя не так, как наш стенд:
|
||||
`mshv_sim.py` проверяет две особенности поведения совместимых TCI-клиентов:
|
||||
|
||||
* **фильтр по номеру приёмника** — `if (ls2.at(0)!=tci_trx) continue;`: строка,
|
||||
адресованная не его приёмнику, для MSHV не существует (и то же самое для
|
||||
* **фильтр по номеру приёмника**: строка,
|
||||
адресованная другому приёмнику, игнорируется (и то же самое для
|
||||
бинарных блоков, по полю `receiver` заголовка);
|
||||
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в
|
||||
* **инициализация — один запрос и один ответ**: клиент шлёт `vfo:<rx>,0;` раз в
|
||||
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
|
||||
|
||||
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
|
||||
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим**
|
||||
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с
|
||||
`if (!tci_tx_enable) return;`).
|
||||
Поэтому стенд диагностирует причину ошибки подключения одной строкой, а заодно
|
||||
ловит то, что иначе проявляется молча: без `tx_enable` со **своим** номером
|
||||
приёмника клиент не разрешает PTT.
|
||||
|
||||
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
|
||||
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
|
||||
|
||||
+10
-13
@@ -1,22 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит,
|
||||
почему он не подключается.
|
||||
"""Стенд совместимости с TCI-клиентом MSHV.
|
||||
|
||||
Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
|
||||
так, как наш собственный стенд. Две его особенности определяют всё:
|
||||
|
||||
1. ВСЕ входящие строки он фильтрует по номеру приёмника:
|
||||
if (ls2.at(0)!=tci_trx) continue; // network.cpp
|
||||
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
|
||||
1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
|
||||
RX2 (tci_trx = "1") строка `trx:0,false`
|
||||
не существует в природе.
|
||||
|
||||
2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
|
||||
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||
«Error To Initialize TCI Server» (network.cpp, Network::initAll,
|
||||
ветка s_ModelID==3||4). Никаких других причин у этой ошибки нет.
|
||||
сообщает об ошибке инициализации.
|
||||
|
||||
Соответствие настроек MSHV: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
|
||||
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx).
|
||||
Соответствие настроек: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
|
||||
приёмник 1.
|
||||
|
||||
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
|
||||
TX-аудио не отправляются. Передающую часть включает явный --tx.
|
||||
@@ -38,8 +35,8 @@ from live_rx_test import WS, split_commands, parse_command # noqa: E402
|
||||
|
||||
HDR = struct.Struct("<16I")
|
||||
|
||||
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2,
|
||||
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс.
|
||||
# Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
|
||||
# частота 48 кГц и буфер TX 24 мс.
|
||||
MSHV_CHANNELS = "2"
|
||||
MSHV_SAMPLES = "2048"
|
||||
MSHV_TYPE = "float32"
|
||||
@@ -65,7 +62,7 @@ class Verdict:
|
||||
|
||||
|
||||
class MshvClient:
|
||||
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения."""
|
||||
"""Минимальная модель поведения совместимого TCI-клиента."""
|
||||
|
||||
def __init__(self, ws, rx, verdict):
|
||||
self.ws = ws
|
||||
@@ -148,7 +145,7 @@ class MshvClient:
|
||||
if len(payload) < HDR.size:
|
||||
return
|
||||
head = HDR.unpack(payload[:HDR.size])
|
||||
# MSHV: if (pStream->receiver != tci_trx) return;
|
||||
# Блоки других приёмников клиент игнорирует.
|
||||
if head[0] != int(self.rx):
|
||||
self.audio_foreign += 1
|
||||
return
|
||||
|
||||
+15
-19
@@ -121,7 +121,7 @@ begin
|
||||
Check('header rate', H.SampleRate = 12000);
|
||||
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
|
||||
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
|
||||
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на
|
||||
// считают по нему число байт. Объявишь «на
|
||||
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
|
||||
Check('header length аудио = ×каналы', H.DataLength = 1024);
|
||||
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
|
||||
@@ -484,7 +484,7 @@ begin
|
||||
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
|
||||
(H.Format <> LongWord(Ord(tsyFloat32))) or
|
||||
// ★Правило, по которому живут клиенты: байт данных = length × размер
|
||||
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`).
|
||||
// отсчёта. Так клиент определяет размер полезной нагрузки.
|
||||
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
|
||||
Inc(Blocks);
|
||||
end;
|
||||
@@ -2054,7 +2054,7 @@ begin
|
||||
end;
|
||||
C.WaitText('ready;', 2000);
|
||||
|
||||
// Клиент назвался ровно как MSHV: 48 кГц, два канала, блок 2048.
|
||||
// Клиент запросил 48 кГц, два канала и блок 2048.
|
||||
C.SendText('audio_samplerate:48000;');
|
||||
C.SendText('audio_stream_channels:2;');
|
||||
C.SendText('audio_stream_samples:2048;');
|
||||
@@ -2174,7 +2174,7 @@ begin
|
||||
// идти дальше, а не прекратиться до конца передачи.
|
||||
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
|
||||
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
|
||||
// это редкость (на живом MSHV — два неотвеченных маркера за 12 с), но
|
||||
// это редкость, но
|
||||
// строка ниже печатает просадку, чтобы регресс был виден.
|
||||
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
|
||||
'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
|
||||
@@ -2182,8 +2182,8 @@ begin
|
||||
Markers > 40, IntToStr(Markers));
|
||||
|
||||
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
|
||||
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает
|
||||
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
|
||||
// ★Регресс с железа. Штатный конец посылки (`trx:N,false`)
|
||||
// обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
|
||||
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
|
||||
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
|
||||
// миллисекунд против шага тика в полкванта, так что промах выпадал через
|
||||
@@ -2586,19 +2586,17 @@ begin
|
||||
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
|
||||
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
|
||||
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
|
||||
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV.
|
||||
// первым номером приёмника после нулевого.
|
||||
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
|
||||
agcMedium, 0.5, -1, '', 0);
|
||||
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
|
||||
C.Pump(300);
|
||||
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
|
||||
|
||||
// Инициализация MSHV — это ровно один запрос и ровно один ответ
|
||||
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
|
||||
// «Error To Initialize TCI Server»).
|
||||
// Инициализация совместимого клиента — это один запрос и один ответ.
|
||||
C.SendText('vfo:1,0;');
|
||||
S := C.WaitText('vfo:1,0,', 1500);
|
||||
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S);
|
||||
Check('слайс отвечает на vfo:1,0 при инициализации', S <> '', S);
|
||||
Check('слайс отдаёт свою частоту',
|
||||
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
|
||||
|
||||
@@ -2611,7 +2609,7 @@ begin
|
||||
FloatToStr(SV.TargetHz));
|
||||
|
||||
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
|
||||
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231).
|
||||
// Клиенты могут отбрасывать блоки с чужим receiver.
|
||||
C.SendText('audio_start:1;');
|
||||
C.Pump(100);
|
||||
for k := 0 to 40 do
|
||||
@@ -2691,13 +2689,11 @@ begin
|
||||
while C.NextFrame(Op, Pay) do ;
|
||||
|
||||
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
|
||||
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает
|
||||
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if
|
||||
// (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288
|
||||
// и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе
|
||||
// Совместимый клиент шлёт TX-аудио только в ответ на маркер TX_CHRONO и
|
||||
// может отбрасывать входящий блок с чужим receiver. С жёстким нулём в
|
||||
// заголовке клиент, сидящий на втором слайсе
|
||||
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
|
||||
// без них он не отправляет ни одного блока. Ровно это и наблюдалось на
|
||||
// живом железе с MSHV.
|
||||
// без них он не отправляет ни одного блока.
|
||||
Ctrl.FWDSPReady := True;
|
||||
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
|
||||
C.SendText('trx:1,true,tci;');
|
||||
@@ -2717,7 +2713,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
|
||||
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
|
||||
Check('TX слайса: маркер назван номером приёмника',
|
||||
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
||||
|
||||
// ★Команда, которая ничего не сделала, не должна перебивать пару
|
||||
|
||||
Reference in New Issue
Block a user