From 98d90e1e5aebb8f1c4fbaa54042390f72e953805 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 25 Aug 2026 22:44:36 +0300 Subject: [PATCH] chore: refine project documentation --- AndromedaMap.pas | 32 +++++---- AndromedaPanel.pas | 2 +- AudioInput.pas | 20 +++--- AudioOutput.pas | 28 ++++---- CATAdapter.pas | 4 +- CATEngine.pas | 123 +++++++++++++++++----------------- CATSliceEndpoint.pas | 3 +- CATTcp.pas | 2 +- CWKeyer.pas | 10 +-- CWMessagesForm.pas | 2 +- CWMorse.pas | 2 +- DMRDecoder.pas | 6 +- DMRProtocol.pas | 4 +- EqualizerControl.pas | 4 +- FilterPopup.pas | 6 +- HPSDRNetwork.pas | 26 +++---- HPSDRProtocol.pas | 12 ++-- MainForm.pas | 24 +++---- PlutoBackend.pas | 4 +- PureSignalPopup.pas | 4 +- RadioBackend.pas | 2 +- RadioController.pas | 87 ++++++++++++------------ SerialPort.pas | 2 +- Settings.pas | 66 +++++++++--------- SettingsForm.pas | 21 +++--- SpectrumView.pas | 8 +-- TCIAdapter.pas | 52 +++++++------- TCIProtocol.pas | 29 +++----- TCIServer.pas | 4 +- TCIStreams.pas | 6 +- VfoOverlay.pas | 2 +- WDSPEngine.pas | 84 +++++++++++------------ WaterfallView.pas | 4 +- WebServer.pas | 2 +- WidebandView.pas | 8 +-- doc/ANDROMEDA.md | 33 +++++---- doc/CAT_SLICES.md | 14 ++-- doc/CAT_STATUS.md | 68 +++++++++---------- doc/PLUTO_INTEGRATION_PLAN.md | 6 +- doc/SLICES_PLAN.md | 26 +++---- doc/TCI.md | 50 ++++++-------- test/cat/cattest.pas | 2 +- test/tci/README.md | 22 +++--- test/tci/mshv_sim.py | 23 +++---- test/tci/tcitest.pas | 34 +++++----- 45 files changed, 457 insertions(+), 516 deletions(-) diff --git a/AndromedaMap.pas b/AndromedaMap.pas index faa86a5..73978d1 100644 --- a/AndromedaMap.pas +++ b/AndromedaMap.pas @@ -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, diff --git a/AndromedaPanel.pas b/AndromedaPanel.pas index 6055dc2..64f6aab 100644 --- a/AndromedaPanel.pas +++ b/AndromedaPanel.pas @@ -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-нажатием). diff --git a/AudioInput.pas b/AudioInput.pas index d697809..9a2072a 100644 --- a/AudioInput.pas +++ b/AudioInput.pas @@ -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; diff --git a/AudioOutput.pas b/AudioOutput.pas index d3334ff..1cd68e5 100644 --- a/AudioOutput.pas +++ b/AudioOutput.pas @@ -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; diff --git a/CATAdapter.pas b/CATAdapter.pas index ee98e37..2345399 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -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; diff --git a/CATEngine.pas b/CATEngine.pas index 65473f5..03c7e7e 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -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); diff --git a/CATSliceEndpoint.pas b/CATSliceEndpoint.pas index e1a01f6..a255cf3 100644 --- a/CATSliceEndpoint.pas +++ b/CATSliceEndpoint.pas @@ -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 (выполняется в потоке контроллера). // diff --git a/CATTcp.pas b/CATTcp.pas index e49fd95..0a3bb30 100644 --- a/CATTcp.pas +++ b/CATTcp.pas @@ -19,7 +19,7 @@ unit CATTcp; { - CATTcp.pas — TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis). + CATTcp.pas — многопоточный TCP-сервер команд CAT. Архитектура: • Один listening socket (TCATTcpServer) diff --git a/CWKeyer.pas b/CWKeyer.pas index 7444ef8..47b68a2 100644 --- a/CWKeyer.pas +++ b/CWKeyer.pas @@ -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 памяти не имеет (в этом и вся разница); diff --git a/CWMessagesForm.pas b/CWMessagesForm.pas index e1a4463..15a68e0 100644 --- a/CWMessagesForm.pas +++ b/CWMessagesForm.pas @@ -24,7 +24,7 @@ unit CWMessagesForm; Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки «отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV), - так что окно висит рядом — ровно как окно CWX у Thetis. Те же ячейки + поэтому окно располагается рядом. Используются те же ячейки привязаны к F1..F8 в главном окне. Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить» diff --git a/CWMorse.pas b/CWMorse.pas index d7e107b..4a54e28 100644 --- a/CWMorse.pas +++ b/CWMorse.pas @@ -30,7 +30,7 @@ unit CWMorse; // * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по // нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА // нажать и отпустить, решает PC. Именно этот путь используется для -// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs). +// передачи текста через макросы и команду CAT KY. // // Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и // дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 902bf07..31b6f41 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -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; diff --git a/DMRProtocol.pas b/DMRProtocol.pas index f26fa78..d1e290c 100644 --- a/DMRProtocol.pas +++ b/DMRProtocol.pas @@ -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 diff --git a/EqualizerControl.pas b/EqualizerControl.pas index 8923265..85735a1 100644 --- a/EqualizerControl.pas +++ b/EqualizerControl.pas @@ -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 работает как diff --git a/FilterPopup.pas b/FilterPopup.pas index b0d91f4..b077dcc 100644 --- a/FilterPopup.pas +++ b/FilterPopup.pas @@ -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; diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 2866118..1699d3c 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -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 diff --git a/HPSDRProtocol.pas b/HPSDRProtocol.pas index f7edf0c..627e55d 100644 --- a/HPSDRProtocol.pas +++ b/HPSDRProtocol.pas @@ -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 diff --git a/MainForm.pas b/MainForm.pas index 56109c0..370af09 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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. diff --git a/PlutoBackend.pas b/PlutoBackend.pas index 8405a3e..45d02a5 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -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 diff --git a/PureSignalPopup.pas b/PureSignalPopup.pas index 097f5c1..d2a19c2 100644 --- a/PureSignalPopup.pas +++ b/PureSignalPopup.pas @@ -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; diff --git a/RadioBackend.pas b/RadioBackend.pas index 667ae5e..7410127 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -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; diff --git a/RadioController.pas b/RadioController.pas index 3301b51..b96549e 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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; diff --git a/SerialPort.pas b/SerialPort.pas index 2c6b8bf..ed7695b 100644 --- a/SerialPort.pas +++ b/SerialPort.pas @@ -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: diff --git a/Settings.pas b/Settings.pas index db7d58a..8c41646 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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; diff --git a/SettingsForm.pas b/SettingsForm.pas index 95869e2..57c0990 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -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. // =========================================================================== diff --git a/SpectrumView.pas b/SpectrumView.pas index 8584da5..de922e1 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -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 diff --git a/TCIAdapter.pas b/TCIAdapter.pas index b768305..d012841 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -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; // Границы пачки: клиент отвечает не на каждый маркер по отдельности, а // очередями по нескольку блоков — по своей внутренней зернистости записи. diff --git a/TCIProtocol.pas b/TCIProtocol.pas index 2884592..08cc4cf 100644 --- a/TCIProtocol.pas +++ b/TCIProtocol.pas @@ -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); diff --git a/TCIServer.pas b/TCIServer.pas index c825a3a..4afbf37 100644 --- a/TCIServer.pas +++ b/TCIServer.pas @@ -20,8 +20,8 @@ unit TCIServer; { - TCIServer.pas — транспорт TCI: WebSocket-сервер (роль сервера играем мы, - как ExpertSDR3; клиенты — логгеры, скиммеры, программы цифровых видов). + TCIServer.pas — транспорт TCI: WebSocket-сервер для логгеров, скиммеров + и программ цифровых видов связи. Что делает: • слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket diff --git a/TCIStreams.pas b/TCIStreams.pas index 401418e..65f4115 100644 --- a/TCIStreams.pas +++ b/TCIStreams.pas @@ -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); diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 9425429..00a3294 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -22,7 +22,7 @@ unit VfoOverlay; {$mode objfpc}{$H+} { - TVfoOverlay — накладка-«флаг слайса» поверх спектра, стиль SmartSDR. + TVfoOverlay — накладка-«флаг слайса» поверх спектра. Компоновка (сверху вниз): шапка : [A] 2.7k SPLIT [TX] метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40 diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 2cb23ae..df1cf1f 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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 diff --git a/WaterfallView.pas b/WaterfallView.pas index 76d8c3e..18641e2 100644 --- a/WaterfallView.pas +++ b/WaterfallView.pas @@ -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; diff --git a/WebServer.pas b/WebServer.pas index c8f0cc6..3bc2e03 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -22,7 +22,7 @@ unit WebServer; { WebServer.pas — HTTP + WebSocket сервер для удалённого управления трансивером. - Архитектура (по образцу OpenWebRX): + Архитектура веб-подсистемы: ───────────────────────────────── HTTP GET / → index.html (см. WebPageHtml) HTTP GET /ws → Upgrade: WebSocket diff --git a/WidebandView.pas b/WidebandView.pas index df97851..e7e1744 100644 --- a/WidebandView.pas +++ b/WidebandView.pas @@ -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, diff --git a/doc/ANDROMEDA.md b/doc/ANDROMEDA.md index 5e75cdc..5b5ed55 100644 --- a/doc/ANDROMEDA.md +++ b/doc/ANDROMEDA.md @@ -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*` и нумерацию элементов проверять по протоколу панели. diff --git a/doc/CAT_SLICES.md b/doc/CAT_SLICES.md index 19d80d7..dfd7de4 100644 --- a/doc/CAT_SLICES.md +++ b/doc/CAT_SLICES.md @@ -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 diff --git a/doc/CAT_STATUS.md b/doc/CAT_STATUS.md index 3e9948f..0bb2077 100644 --- a/doc/CAT_STATUS.md +++ b/doc/CAT_STATUS.md @@ -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 строк) | Движок протокола: разбор `;`, формирование ответов, реализации всех команд 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` (`` / ``) и телом -> метода в `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*`-хелпер diff --git a/doc/PLUTO_INTEGRATION_PLAN.md b/doc/PLUTO_INTEGRATION_PLAN.md index 60f20a4..2b49b33 100644 --- a/doc/PLUTO_INTEGRATION_PLAN.md +++ b/doc/PLUTO_INTEGRATION_PLAN.md @@ -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` по маяку (ручная/полуавтомат). diff --git a/doc/SLICES_PLAN.md b/doc/SLICES_PLAN.md index f5502d0..55f30a4 100644 --- a/doc/SLICES_PLAN.md +++ b/doc/SLICES_PLAN.md @@ -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), а рейл дублирует флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов — diff --git a/doc/TCI.md b/doc/TCI.md index 14dc3c7..a43bae0 100644 --- a/doc/TCI.md +++ b/doc/TCI.md @@ -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:,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 подаётся diff --git a/test/cat/cattest.pas b/test/cat/cattest.pas index 590d210..828af87 100644 --- a/test/cat/cattest.pas +++ b/test/cat/cattest.pas @@ -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; diff --git a/test/tci/README.md b/test/tci/README.md index 7ca62bc..3944cd9 100644 --- a/test/tci/README.md +++ b/test/tci/README.md @@ -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:,0;` раз в +* **инициализация — один запрос и один ответ**: клиент шлёт `vfo:,0;` раз в 500 мс, максимум пять раз, и ждёт `vfo:,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`. diff --git a/test/tci/mshv_sim.py b/test/tci/mshv_sim.py index 6ecd15d..d2132a5 100755 --- a/test/tci/mshv_sim.py +++ b/test/tci/mshv_sim.py @@ -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:,0;` раз в 500 мс, максимум 5 попыток, и ждёт `vfo:,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 diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index 86cf934..59e2184 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -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:,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)); // ★Команда, которая ничего не сделала, не должна перебивать пару