mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
chore: refine project documentation
This commit is contained in:
+15
-17
@@ -27,11 +27,9 @@ unit AndromedaMap;
|
|||||||
«индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
|
«индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
|
||||||
контроллера — этим занимается AndromedaPanel.pas.
|
контроллера — этим занимается AndromedaPanel.pas.
|
||||||
|
|
||||||
Соответствие протоколу Thetis:
|
Таблицы соответствуют нумерации команд аппаратной панели. Для LED-индикаторов
|
||||||
• действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions;
|
используются номера 1=MOX, 3=Tune, 7=CTUN и т.д.; свободные слоты 12..17
|
||||||
• номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis
|
заняты функциями EWSDR (Split/NB/NR/SNB/ANF/Squelch).
|
||||||
(MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты
|
|
||||||
12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{$mode objfpc}{$H+}
|
{$mode objfpc}{$H+}
|
||||||
@@ -44,18 +42,18 @@ type
|
|||||||
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
|
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
|
||||||
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
|
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
|
||||||
abCTCSS, abSquelch, abFilterUp, abFilterDown,
|
abCTCSS, abSquelch, abFilterUp, abFilterDown,
|
||||||
abVFOAtoB, // копировать VFO A → B (Thetis eBBVFOAtoB)
|
abVFOAtoB, // копировать VFO A → B
|
||||||
abVFOBtoA, // копировать VFO B → A (Thetis eBBVFOBtoA)
|
abVFOBtoA, // копировать VFO B → A
|
||||||
abVFOLock, // блокировка перестройки VFO (Thetis eBBVFOLockOnOff)
|
abVFOLock, // блокировка перестройки VFO
|
||||||
abStartStop, // старт/стоп SDR (Thetis eBBStartStop)
|
abStartStop, // старт/стоп SDR
|
||||||
abShift); // модификатор band-keypad (Thetis eBBShift) — обрабатывается в панели
|
abShift); // модификатор band-keypad — обрабатывается в панели
|
||||||
|
|
||||||
// Параметр, управляемый поворотным энкодером панели.
|
// Параметр, управляемый поворотным энкодером панели.
|
||||||
TAndEncoder = (
|
TAndEncoder = (
|
||||||
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
|
||||||
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
|
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
|
||||||
|
|
||||||
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
|
// Индикатор (LED) панели в порядке протокольной нумерации.
|
||||||
TAndIndicator = (
|
TAndIndicator = (
|
||||||
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
|
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
|
||||||
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
|
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
|
||||||
@@ -67,8 +65,8 @@ const
|
|||||||
AND_MAX_ENCODERS = 20;
|
AND_MAX_ENCODERS = 20;
|
||||||
|
|
||||||
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
|
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
|
||||||
// Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based
|
// Внутренний индекс 0-based на единицу меньше номера кнопки в протоколе.
|
||||||
// индекс = «Pushbutton Number» Thetis − 1. Проверено на реальной панели
|
// Нумерация проверена на реальной панели
|
||||||
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
|
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
|
||||||
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
|
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
|
||||||
abMute, // 0 RX1 Mute
|
abMute, // 0 RX1 Mute
|
||||||
@@ -121,14 +119,14 @@ const
|
|||||||
abTune); // 47 TUN
|
abTune); // 47 TUN
|
||||||
|
|
||||||
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
// Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр.
|
||||||
// Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset).
|
// Внутренний индекс на единицу меньше номера энкодера в протоколе.
|
||||||
// Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel.
|
// Энкодер #1 управляет AGC Level, поэтому используется aeAGCLevel.
|
||||||
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
|
// #4/#5 — Filter High/Low: настоящие независимые края (как на железе G2).
|
||||||
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
|
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
|
||||||
// энкодер «ширина» — в дефолтной раскладке не используется.
|
// энкодер «ширина» — в дефолтной раскладке не используется.
|
||||||
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
|
||||||
aeAFGain, // 0 RX1 AF Gain
|
aeAFGain, // 0 RX1 AF Gain
|
||||||
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
|
aeAGCLevel, // 1 RX1 AGC Level
|
||||||
aeNone, // 2 RX2 AF — нет второго приёмника
|
aeNone, // 2 RX2 AF — нет второго приёмника
|
||||||
aeNone, // 3 RX2 AGC
|
aeNone, // 3 RX2 AGC
|
||||||
aeFilterHigh, // 4 Filter High — верхний край
|
aeFilterHigh, // 4 Filter High — верхний край
|
||||||
@@ -142,7 +140,7 @@ const
|
|||||||
aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone);
|
aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone);
|
||||||
|
|
||||||
// Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет.
|
// Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет.
|
||||||
// 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах.
|
// 1..11 — базовая раскладка панели; 12..17 — функции EWSDR.
|
||||||
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
|
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
|
||||||
{aiNone} 0,
|
{aiNone} 0,
|
||||||
{aiMOX} 1,
|
{aiMOX} 1,
|
||||||
|
|||||||
+1
-1
@@ -157,7 +157,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
|
procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
|
||||||
// Раскладка зеркалит Thetis: кнопка Shift (0-based 28) — модификатор-защёлка;
|
// Кнопка Shift (0-based 28) — модификатор-защёлка;
|
||||||
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
|
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
|
||||||
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
|
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
|
||||||
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
|
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
|
||||||
|
|||||||
+9
-11
@@ -20,11 +20,10 @@
|
|||||||
unit AudioInput;
|
unit AudioInput;
|
||||||
|
|
||||||
{
|
{
|
||||||
PortAudio input — TX microphone capture.
|
PortAudio input — TX microphone capture, parallel to AudioOutput.pas.
|
||||||
Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas.
|
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
- Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR)
|
- Mono input stream, paFloat32, fixed at 48000 Hz
|
||||||
- Pa_Initialize called once in Initialize
|
- Pa_Initialize called once in Initialize
|
||||||
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
|
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
|
||||||
- Callback (PaInCallback) writes mic samples into the ring buffer
|
- Callback (PaInCallback) writes mic samples into the ring buffer
|
||||||
@@ -44,7 +43,7 @@ uses
|
|||||||
|
|
||||||
const
|
const
|
||||||
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback
|
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback
|
||||||
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono), как в piHPSDR
|
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono)
|
||||||
|
|
||||||
type
|
type
|
||||||
TPaError = LongInt;
|
TPaError = LongInt;
|
||||||
@@ -110,7 +109,7 @@ type
|
|||||||
FLastError: string;
|
FLastError: string;
|
||||||
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
|
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
|
||||||
|
|
||||||
// Ring buffer (mono double), как в piHPSDR
|
// Ring buffer (mono double)
|
||||||
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
|
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
|
||||||
FInPt: Integer; // write pointer (callback writes)
|
FInPt: Integer; // write pointer (callback writes)
|
||||||
FOutPt: Integer; // read pointer (main thread reads via ReadSample)
|
FOutPt: Integer; // read pointer (main thread reads via ReadSample)
|
||||||
@@ -208,8 +207,7 @@ const
|
|||||||
PA_CONTINUE = LongInt(0);
|
PA_CONTINUE = LongInt(0);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер.
|
// Callback — читает mono float32 из inputBuffer и пишет в кольцевой буфер.
|
||||||
// Точно по образцу pa_in_cb из piHPSDR/portaudio.c
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function PaInCallback(inputBuffer, outputBuffer: Pointer;
|
function PaInCallback(inputBuffer, outputBuffer: Pointer;
|
||||||
@@ -461,7 +459,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Open — открывает input stream, как audio_open_input в piHPSDR
|
// Open — открывает входной аудиопоток.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function TAudioInput.Open: Boolean;
|
function TAudioInput.Open: Boolean;
|
||||||
@@ -480,7 +478,7 @@ begin
|
|||||||
if not Initialize then Exit;
|
if not Initialize then Exit;
|
||||||
|
|
||||||
FillChar(InParam, SizeOf(InParam), 0);
|
FillChar(InParam, SizeOf(InParam), 0);
|
||||||
InParam.channelCount := 1; // Mono, как в piHPSDR
|
InParam.channelCount := 1; // Mono input
|
||||||
InParam.device := FDeviceIndex;
|
InParam.device := FDeviceIndex;
|
||||||
InParam.sampleFormat := PA_FLOAT32;
|
InParam.sampleFormat := PA_FLOAT32;
|
||||||
|
|
||||||
@@ -533,7 +531,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Close — как audio_close_input в piHPSDR
|
// Close — останавливает и закрывает входной аудиопоток.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
procedure TAudioInput.Close;
|
procedure TAudioInput.Close;
|
||||||
@@ -556,7 +554,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ReadSample — как audio_get_next_mic_sample в piHPSDR
|
// ReadSample — возвращает следующий микрофонный сэмпл из буфера.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function TAudioInput.ReadSample: Double;
|
function TAudioInput.ReadSample: Double;
|
||||||
|
|||||||
+13
-15
@@ -20,12 +20,12 @@
|
|||||||
unit AudioOutput;
|
unit AudioOutput;
|
||||||
|
|
||||||
{
|
{
|
||||||
PortAudio output — реализация по образцу piHPSDR/portaudio.c (DL1YCF)
|
PortAudio output with a buffered low-latency stereo stream.
|
||||||
|
|
||||||
Ключевые особенности:
|
Ключевые особенности:
|
||||||
- Pa_Initialize вызывается один раз в Create (до открытия стрима)
|
- Pa_Initialize вызывается один раз в Create (до открытия стрима)
|
||||||
- suggestedLatency = 0.0 (минимум устройства)
|
- suggestedLatency = 0.0 (минимум устройства)
|
||||||
- Ring buffer из Double (как в оригинале)
|
- Ring buffer из Double
|
||||||
- Callback читает по одному сэмплу, обновляет outpt внутри цикла
|
- Callback читает по одному сэмплу, обновляет outpt внутри цикла
|
||||||
- Low water mark: вставляет тишину + полбуфера silence
|
- Low water mark: вставляет тишину + полбуфера silence
|
||||||
- High water mark: удаляет лишние сэмплы
|
- High water mark: удаляет лишние сэмплы
|
||||||
@@ -43,10 +43,10 @@ uses
|
|||||||
Classes, SysUtils, Math, SyncObjs, DynLibs;
|
Classes, SysUtils, Math, SyncObjs, DynLibs;
|
||||||
|
|
||||||
const
|
const
|
||||||
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR
|
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback
|
||||||
MY_RING_BUFFER_SIZE = 9600; // как piHPSDR
|
MY_RING_BUFFER_SIZE = 9600;
|
||||||
MY_RING_LOW_WATER = 512; // как piHPSDR
|
MY_RING_LOW_WATER = 512;
|
||||||
MY_RING_HIGH_WATER = 9000; // как piHPSDR
|
MY_RING_HIGH_WATER = 9000;
|
||||||
|
|
||||||
type
|
type
|
||||||
TPaError = LongInt;
|
TPaError = LongInt;
|
||||||
@@ -119,8 +119,8 @@ type
|
|||||||
FPAInited: Boolean; // Pa_Initialize прошла
|
FPAInited: Boolean; // Pa_Initialize прошла
|
||||||
FLastError: string;
|
FLastError: string;
|
||||||
|
|
||||||
// Ring buffer (interleaved double stereo, как в оригинале)
|
// Ring buffer (interleaved double stereo)
|
||||||
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double; // как piHPSDR
|
FBuf: array[0..MY_RING_BUFFER_SIZE * 2 - 1] of Double;
|
||||||
FInPt: Integer; // audio_buffer_inpt (write)
|
FInPt: Integer; // audio_buffer_inpt (write)
|
||||||
FOutPt: Integer; // audio_buffer_outpt (read)
|
FOutPt: Integer; // audio_buffer_outpt (read)
|
||||||
FMutex: TCriticalSection;
|
FMutex: TCriticalSection;
|
||||||
@@ -159,7 +159,7 @@ type
|
|||||||
// Найти индекс устройства по имени (-1 если не найдено)
|
// Найти индекс устройства по имени (-1 если не найдено)
|
||||||
function FindDeviceByName(const AName: string): Integer;
|
function FindDeviceByName(const AName: string): Integer;
|
||||||
|
|
||||||
// Пишем стерео double сэмплы — как audio_write() в оригинале
|
// Записывает пару стереосэмплов Double в кольцевой буфер.
|
||||||
procedure WriteDouble(Left, Right: Double);
|
procedure WriteDouble(Left, Right: Double);
|
||||||
// Convenience: массив Single
|
// Convenience: массив Single
|
||||||
procedure Write(const Left, Right: array of Single; Count: Integer);
|
procedure Write(const Left, Right: array of Single; Count: Integer);
|
||||||
@@ -215,13 +215,12 @@ const
|
|||||||
PA_CONTINUE = LongInt(0);
|
PA_CONTINUE = LongInt(0);
|
||||||
PA_NO_DEV = TPaDeviceIndex(-1);
|
PA_NO_DEV = TPaDeviceIndex(-1);
|
||||||
|
|
||||||
// Callback modeled after pa_out_cb from piHPSDR/portaudio.c
|
// Callback reads interleaved stereo samples from the ring buffer.
|
||||||
function PaOutCallback(inputBuffer, outputBuffer: Pointer;
|
function PaOutCallback(inputBuffer, outputBuffer: Pointer;
|
||||||
framesPerBuffer: LongWord;
|
framesPerBuffer: LongWord;
|
||||||
timeInfo: Pointer;
|
timeInfo: Pointer;
|
||||||
statusFlags: LongWord;
|
statusFlags: LongWord;
|
||||||
userData: Pointer): LongInt; cdecl;
|
userData: Pointer): LongInt; cdecl;
|
||||||
// Точная копия pa_out_cb из piHPSDR/portaudio.c
|
|
||||||
var
|
var
|
||||||
Audio: TAudioOutput;
|
Audio: TAudioOutput;
|
||||||
Out_: PSingle;
|
Out_: PSingle;
|
||||||
@@ -478,7 +477,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Open — точная последовательность как в audio_open_output()
|
// Open — подготавливает параметры и открывает выходной поток PortAudio.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function TAudioOutput.Open: Boolean;
|
function TAudioOutput.Open: Boolean;
|
||||||
@@ -505,7 +504,7 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Точно как в оригинале: bzero + suggestedLatency = 0.0
|
// Обнуляем параметры и запрашиваем минимальную задержку устройства.
|
||||||
FillChar(OutParam, SizeOf(OutParam), 0);
|
FillChar(OutParam, SizeOf(OutParam), 0);
|
||||||
OutParam.channelCount := 2;
|
OutParam.channelCount := 2;
|
||||||
OutParam.device := Dev;
|
OutParam.device := Dev;
|
||||||
@@ -597,8 +596,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// WriteDouble — modeled after audio_write() from piHPSDR/portaudio.c
|
// Вызывается для каждой пары сэмплов из Write. Мьютекс держится весь цикл.
|
||||||
// Вызывается per-sample из Write. Мьютекс держится весь цикл в Write.
|
|
||||||
procedure TAudioOutput.WriteDouble(Left, Right: Double);
|
procedure TAudioOutput.WriteDouble(Left, Right: Double);
|
||||||
var
|
var
|
||||||
avail: Integer;
|
avail: Integer;
|
||||||
|
|||||||
+2
-2
@@ -740,7 +740,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TCATAdapter.SyncTXEQ;
|
procedure TCATAdapter.SyncTXEQ;
|
||||||
// Частоты полос команда не несёт (их в формате Thetis попросту нет) — трогаем
|
// Частоты полос команда не несёт (их в формате CAT попросту нет) — трогаем
|
||||||
// только усиления и число полос, сетка остаётся той, что настроена в UI.
|
// только усиления и число полос, сетка остаётся той, что настроена в UI.
|
||||||
var
|
var
|
||||||
T: TTXSettings;
|
T: TTXSettings;
|
||||||
@@ -866,7 +866,7 @@ procedure TCATAdapter.SyncAtten;
|
|||||||
begin FController.SetAtten(FSyncInt); end;
|
begin FController.SetAtten(FSyncInt); end;
|
||||||
|
|
||||||
function TCATAdapter.GetSupplyVolts: Double;
|
function TCATAdapter.GetSupplyVolts: Double;
|
||||||
// Телеметрия платы; -1 = данных ещё нет (движок отдаст ноль, как Thetis).
|
// Телеметрия платы; -1 означает, что данных ещё нет, и наружу отдаётся ноль.
|
||||||
begin Result := FController.FLastSupplyV; end;
|
begin Result := FController.FLastSupplyV; end;
|
||||||
|
|
||||||
procedure TCATAdapter.DoVfoSwap;
|
procedure TCATAdapter.DoVfoSwap;
|
||||||
|
|||||||
+61
-62
@@ -19,10 +19,10 @@
|
|||||||
|
|
||||||
unit CATEngine;
|
unit CATEngine;
|
||||||
{
|
{
|
||||||
CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions).
|
CATEngine.pas — CAT command engine (Kenwood + extended ZZ* commands).
|
||||||
|
|
||||||
Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs
|
Движок поддерживает базовые команды Kenwood и расширения ZZ*. Команды, для
|
||||||
реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают
|
которых в EWSDR ещё нет функционала, возвращают
|
||||||
заглушку и сопровождены комментарием "STUB: <причина>".
|
заглушку и сопровождены комментарием "STUB: <причина>".
|
||||||
|
|
||||||
Формат команд:
|
Формат команд:
|
||||||
@@ -160,7 +160,7 @@ type
|
|||||||
CWTextBusy: TCATGetBoolFunc;
|
CWTextBusy: TCATGetBoolFunc;
|
||||||
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
|
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
|
||||||
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
|
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
|
||||||
// (ZZCM — у Thetis это «monitor DISABLE», инверсия делается в движке).
|
// ZZCM означает «monitor DISABLE», инверсия делается в движке.
|
||||||
GetCWPitch: TCATGetIntFunc; // Гц
|
GetCWPitch: TCATGetIntFunc; // Гц
|
||||||
SetCWPitch: TCATSetIntProc;
|
SetCWPitch: TCATSetIntProc;
|
||||||
GetCWBreakIn: TCATGetBoolFunc;
|
GetCWBreakIn: TCATGetBoolFunc;
|
||||||
@@ -500,7 +500,7 @@ begin if B then Result := '1' else Result := '0'; end;
|
|||||||
|
|
||||||
function IsParamless(const Pfx: string): Boolean;
|
function IsParamless(const Pfx: string): Boolean;
|
||||||
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
|
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
|
||||||
// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка
|
// Команды не принимают аргументов; любой хвост у них — ошибка
|
||||||
// формата, а не значение.
|
// формата, а не значение.
|
||||||
const
|
const
|
||||||
NoArg: array[0..9] of string = (
|
NoArg: array[0..9] of string = (
|
||||||
@@ -753,7 +753,7 @@ end;
|
|||||||
|
|
||||||
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
|
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
|
||||||
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
|
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
|
||||||
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis —
|
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi],
|
||||||
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
|
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
|
||||||
var v: Integer;
|
var v: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -769,7 +769,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.StepIdxToHz(Idx: Integer): Double;
|
function TCATEngine.StepIdxToHz(Idx: Integer): Double;
|
||||||
// Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц).
|
// Шаг настройки по протокольному индексу 0..14, результат в Гц.
|
||||||
const Steps: array[0..14] of Double = (
|
const Steps: array[0..14] of Double = (
|
||||||
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
|
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
|
||||||
10000, 100000, 250000, 500000, 1000000);
|
10000, 100000, 250000, 500000, 1000000);
|
||||||
@@ -823,8 +823,8 @@ begin
|
|||||||
Result := ParseZZ(ext, sfx);
|
Result := ParseZZ(ext, sfx);
|
||||||
end else begin
|
end else begin
|
||||||
sfx := Copy(s, 3, tpos - 3);
|
sfx := Copy(s, 3, tpos - 3);
|
||||||
// Команды без параметров: эталон отбраковывает лишний суффикс в парсере,
|
// Команды без параметров должны отбраковывать лишний суффикс в парсере
|
||||||
// по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список
|
// ДО вызова команды. Таблицы ширин у нас нет — держим список
|
||||||
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
|
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
|
||||||
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
|
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
|
||||||
if pfx = 'AG' then Result := CmdAG(sfx)
|
if pfx = 'AG' then Result := CmdAG(sfx)
|
||||||
@@ -933,7 +933,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.CmdCT(const s: string): string;
|
function TCATEngine.CmdCT(const s: string): string;
|
||||||
// CT: включение CTCSS — обёртка над ZZTA, как и в эталоне.
|
// CT: включение CTCSS — обёртка над ZZTA.
|
||||||
begin
|
begin
|
||||||
Result := ZZTA(s);
|
Result := ZZTA(s);
|
||||||
end;
|
end;
|
||||||
@@ -994,7 +994,7 @@ end;
|
|||||||
|
|
||||||
function TCATEngine.CmdFW(const s: string): string;
|
function TCATEngine.CmdFW(const s: string): string;
|
||||||
// FW: filter width (4 digits) — maps to filter index
|
// FW: filter width (4 digits) — maps to filter index
|
||||||
// Thetis encodes filter by index into 4-digit string 0000-0009
|
// Filter index is encoded as a 4-digit string from 0000 to 0009.
|
||||||
var idx: Integer;
|
var idx: Integer;
|
||||||
begin
|
begin
|
||||||
if Length(s) = 4 then begin
|
if Length(s) = 4 then begin
|
||||||
@@ -1028,7 +1028,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.CmdIF: string;
|
function TCATEngine.CmdIF: string;
|
||||||
// IF: составной статус — 35 символов ответа (как у Thetis; «37 байт» из
|
// IF: составной статус — 35 символов ответа («37 байт» из
|
||||||
// прежнего комментария — это вся посылка вместе с префиксом и ';').
|
// прежнего комментария — это вся посылка вместе с префиксом и ';').
|
||||||
var
|
var
|
||||||
freq, step, incr, rit, xit, tx, mode, split: string;
|
freq, step, incr, rit, xit, tx, mode, split: string;
|
||||||
@@ -1108,7 +1108,7 @@ end;
|
|||||||
|
|
||||||
function TCATEngine.CmdMG(const s: string): string;
|
function TCATEngine.CmdMG(const s: string): string;
|
||||||
// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
|
// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
|
||||||
// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
|
// n/1.43, дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
|
||||||
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
|
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
|
||||||
var v: Integer;
|
var v: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -1159,22 +1159,21 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.CmdOF(const s: string): string;
|
function TCATEngine.CmdOF(const s: string): string;
|
||||||
// OF: сдвиг репитера — обёртка над ZZOT, как и в эталоне.
|
// OF: сдвиг репитера — обёртка над ZZOT.
|
||||||
begin
|
begin
|
||||||
Result := ZZOT(s);
|
Result := ZZOT(s);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.CmdOS(const s: string): string;
|
function TCATEngine.CmdOS(const s: string): string;
|
||||||
// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне.
|
// OS: направление сдвига репитера — обёртка над ZZOS.
|
||||||
begin
|
begin
|
||||||
Result := ZZOS(s);
|
Result := ZZOS(s);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZOS(const s: string): string;
|
function TCATEngine.ZZOS(const s: string): string;
|
||||||
// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх,
|
// ZZOS: направление сдвига репитера. В протоколе 0=симплекс, 1=вверх,
|
||||||
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
|
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
|
||||||
// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection),
|
// ★Некорректное значение трактуется как симплекс для совместимости клиентов.
|
||||||
// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение.
|
|
||||||
var dir: Integer;
|
var dir: Integer;
|
||||||
begin
|
begin
|
||||||
if Length(s) = 1 then begin
|
if Length(s) = 1 then begin
|
||||||
@@ -1196,9 +1195,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZOT(const s: string): string;
|
function TCATEngine.ZZOT(const s: string): string;
|
||||||
// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью
|
// ZZOT: величина сдвига репитера, 9 цифр. Поле кодирует МГц с шестью знаками
|
||||||
// знаками после запятой (вставляет точку после третьего разряда) — численно это
|
// после запятой, что численно соответствует целому значению в герцах. Знак не
|
||||||
// ровно те же герцы, что и у нас. Знак не передаётся, направление — в ZZOS.
|
// передаётся, направление задаётся в ZZOS.
|
||||||
var v: Int64;
|
var v: Int64;
|
||||||
begin
|
begin
|
||||||
if Length(s) = 9 then begin
|
if Length(s) = 9 then begin
|
||||||
@@ -1228,7 +1227,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.CmdPR(const s: string): string;
|
function TCATEngine.CmdPR(const s: string): string;
|
||||||
// PR: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0');
|
// PR: речевой компрессор вкл/выкл. Базовая команда доступна только для чтения;
|
||||||
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
|
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
|
||||||
// кенвудовские клиенты от него не ломаются.
|
// кенвудовские клиенты от него не ломаются.
|
||||||
begin
|
begin
|
||||||
@@ -1311,7 +1310,7 @@ end;
|
|||||||
function TCATEngine.CmdSM(const s: string): string;
|
function TCATEngine.CmdSM(const s: string): string;
|
||||||
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
|
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
|
||||||
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
|
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
|
||||||
// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все:
|
// Совместимые клиенты присылают селектор '0', '1' или '2'; принимаем все:
|
||||||
// второго приёмника нет, отвечаем одним и тем же.
|
// второго приёмника нет, отвечаем одним и тем же.
|
||||||
var sm: Integer;
|
var sm: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -1401,7 +1400,7 @@ begin
|
|||||||
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
|
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
|
||||||
else if ext = 'CB' then ans := ZZCB(s)
|
else if ext = 'CB' then ans := ZZCB(s)
|
||||||
else if ext = 'CD' then ans := ZZCD(s)
|
else if ext = 'CD' then ans := ZZCD(s)
|
||||||
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа (UI Thetis)
|
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа в UI
|
||||||
else if ext = 'CI' then ans := ZZCI(s)
|
else if ext = 'CI' then ans := ZZCI(s)
|
||||||
else if ext = 'CL' then ans := ZZCL(s)
|
else if ext = 'CL' then ans := ZZCL(s)
|
||||||
else if ext = 'CM' then ans := ZZCM(s)
|
else if ext = 'CM' then ans := ZZCM(s)
|
||||||
@@ -1443,10 +1442,10 @@ begin
|
|||||||
else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
|
else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
|
||||||
else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет
|
else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет
|
||||||
else if ext = 'FT' then ans := ZZFT(s)
|
else if ext = 'FT' then ans := ZZFT(s)
|
||||||
else if ext = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет
|
else if ext = 'FV' then ans := '' // STUB: чтение вендорской шины (1 байт)
|
||||||
else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет
|
else if ext = 'FW' then ans := '' // STUB: чтение вендорской шины (2 байта)
|
||||||
else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет
|
else if ext = 'FX' then ans := '' // STUB: запись вендорской шины (1 байт)
|
||||||
else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет
|
else if ext = 'FY' then ans := '' // STUB: запись вендорской шины (2 байта)
|
||||||
else if ext = 'GA' then ans := ZZGA(s)
|
else if ext = 'GA' then ans := ZZGA(s)
|
||||||
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
|
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
|
||||||
else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет
|
else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет
|
||||||
@@ -1466,16 +1465,16 @@ begin
|
|||||||
else if ext = 'IS' then ans := ZZIS(s)
|
else if ext = 'IS' then ans := ZZIS(s)
|
||||||
else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
|
else if ext = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
|
||||||
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
|
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
|
||||||
else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis
|
else if ext = 'JO' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis
|
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда
|
||||||
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis
|
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда
|
||||||
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis
|
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда
|
||||||
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis
|
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда
|
||||||
else if ext = 'KM' then ans := ZZKM(s)
|
else if ext = 'KM' then ans := ZZKM(s)
|
||||||
else if ext = 'KO' then ans := '' // STUB: без описания у Thetis
|
else if ext = 'KO' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'KS' then ans := ZZKS(s)
|
else if ext = 'KS' then ans := ZZKS(s)
|
||||||
else if ext = 'KY' then ans := ZZKY(s)
|
else if ext = 'KY' then ans := ZZKY(s)
|
||||||
else if ext = 'LA' then ans := '' // STUB: уровень RX0 — микшер Thetis
|
else if ext = 'LA' then ans := '' // STUB: уровень RX0 внешнего микшера
|
||||||
else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
|
else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
|
||||||
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
|
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
|
||||||
else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет
|
else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет
|
||||||
@@ -1487,7 +1486,7 @@ begin
|
|||||||
else if ext = 'MA' then ans := ZZMA(s)
|
else if ext = 'MA' then ans := ZZMA(s)
|
||||||
else if ext = 'MB' then ans := ZZMB(s)
|
else if ext = 'MB' then ans := ZZMB(s)
|
||||||
else if ext = 'MD' then ans := ZZMD(s)
|
else if ext = 'MD' then ans := ZZMD(s)
|
||||||
else if ext = 'ME' then ans := '' // STUB: без описания у Thetis
|
else if ext = 'ME' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
|
else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
|
||||||
else if ext = 'MG' then ans := ZZMG(s)
|
else if ext = 'MG' then ans := ZZMG(s)
|
||||||
else if ext = 'ML' then ans := ZZML
|
else if ext = 'ML' then ans := ZZML
|
||||||
@@ -1528,7 +1527,7 @@ begin
|
|||||||
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
|
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
|
||||||
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
|
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
|
||||||
else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF
|
else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF
|
||||||
else if ext = 'OJ' then ans := '' // STUB: без описания у Thetis
|
else if ext = 'OJ' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
|
else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
|
||||||
else if ext = 'OS' then ans := ZZOS(s)
|
else if ext = 'OS' then ans := ZZOS(s)
|
||||||
else if ext = 'OT' then ans := ZZOT(s)
|
else if ext = 'OT' then ans := ZZOT(s)
|
||||||
@@ -1548,7 +1547,7 @@ begin
|
|||||||
else if ext = 'PS' then ans := ZZPS(s)
|
else if ext = 'PS' then ans := ZZPS(s)
|
||||||
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
|
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
|
||||||
else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено
|
else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено
|
||||||
// ZZQK: у Thetis break-in делится на semi и QSK; у нас break-in один
|
// ZZQK различает semi и QSK; у нас break-in один
|
||||||
// (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
|
// (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
|
||||||
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
|
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
|
||||||
else if ext = 'QM' then ans := ZZQM
|
else if ext = 'QM' then ans := ZZQM
|
||||||
@@ -1580,7 +1579,7 @@ begin
|
|||||||
else if ext = 'SP' then ans := ZZSP(s)
|
else if ext = 'SP' then ans := ZZSP(s)
|
||||||
else if ext = 'SQ' then ans := ZZSQ(s)
|
else if ext = 'SQ' then ans := ZZSQ(s)
|
||||||
else if ext = 'SR' then ans := ZZSR(s)
|
else if ext = 'SR' then ans := ZZSR(s)
|
||||||
else if ext = 'SS' then ans := '' // STUB: без описания у Thetis
|
else if ext = 'SS' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'ST' then ans := ZZST(s)
|
else if ext = 'ST' then ans := ZZST(s)
|
||||||
else if ext = 'SU' then begin ZZSU; ans := ''; end
|
else if ext = 'SU' then begin ZZSU; ans := ''; end
|
||||||
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет
|
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет
|
||||||
@@ -1590,14 +1589,14 @@ begin
|
|||||||
else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
|
else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
|
||||||
else if ext = 'TA' then ans := ZZTA(s)
|
else if ext = 'TA' then ans := ZZTA(s)
|
||||||
else if ext = 'TB' then ans := ZZTB(s)
|
else if ext = 'TB' then ans := ZZTB(s)
|
||||||
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре (UI Thetis)
|
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре
|
||||||
else if ext = 'TH' then ans := ZZTH(s)
|
else if ext = 'TH' then ans := ZZTH(s)
|
||||||
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
|
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
|
||||||
else if ext = 'TL' then ans := ZZTL(s)
|
else if ext = 'TL' then ans := ZZTL(s)
|
||||||
else if ext = 'TM' then ans := ZZTM(s)
|
else if ext = 'TM' then ans := ZZTM(s)
|
||||||
else if ext = 'TO' then ans := ZZTO(s)
|
else if ext = 'TO' then ans := ZZTO(s)
|
||||||
else if ext = 'TP' then ans := ZZTP(s)
|
else if ext = 'TP' then ans := ZZTP(s)
|
||||||
else if ext = 'TS' then ans := '' // STUB: датчик температуры Flex 5000
|
else if ext = 'TS' then ans := '' // STUB: неподдерживаемый датчик температуры
|
||||||
else if ext = 'TU' then ans := ZZTU(s)
|
else if ext = 'TU' then ans := ZZTU(s)
|
||||||
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
|
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
|
||||||
else if ext = 'TX' then ans := ZZTX(s)
|
else if ext = 'TX' then ans := ZZTX(s)
|
||||||
@@ -1629,7 +1628,7 @@ begin
|
|||||||
else if ext = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
|
else if ext = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
|
||||||
else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет
|
else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет
|
||||||
else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет
|
else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет
|
||||||
else if ext = 'VW' then ans := '' // STUB: без описания у Thetis
|
else if ext = 'VW' then ans := '' // STUB: команда не описана
|
||||||
else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
|
else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
|
||||||
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
|
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
|
||||||
else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет
|
else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет
|
||||||
@@ -1654,7 +1653,7 @@ begin
|
|||||||
else if ext = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора
|
else if ext = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора
|
||||||
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
|
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
|
||||||
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT
|
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT
|
||||||
else if ext = 'WW' then ans := '' // STUB: уровень выхода FlexWire в микшере F1500 — см. ZZWT
|
else if ext = 'WW' then ans := '' // STUB: уровень выхода вендорской шины — см. ZZWT
|
||||||
else if ext = 'XA' then ans := ZZXA(s)
|
else if ext = 'XA' then ans := ZZXA(s)
|
||||||
else if ext = 'XC' then ans := ZZXC
|
else if ext = 'XC' then ans := ZZXC
|
||||||
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
|
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
|
||||||
@@ -1846,7 +1845,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZBS(const s: string): string;
|
function TCATEngine.ZZBS(const s: string): string;
|
||||||
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
|
// ZZBS: выбор диапазона. В EWSDR поле двузначное, а базовый формат использует
|
||||||
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
|
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
|
||||||
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
|
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
|
||||||
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
|
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
|
||||||
@@ -1877,10 +1876,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZCD(const s: string): string;
|
function TCATEngine.ZZCD(const s: string): string;
|
||||||
// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает
|
// ZZCD: задержка отпускания break-in (hang time), мс. В EWSDR потолок 2000 —
|
||||||
// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же
|
// это намеренный предел: столько же
|
||||||
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
|
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
|
||||||
// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры.
|
// Значение выше подрезается по общему правилу, формат — те же 4 цифры.
|
||||||
begin
|
begin
|
||||||
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
|
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
|
||||||
end;
|
end;
|
||||||
@@ -1948,7 +1947,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZEB(const s: string): string;
|
function TCATEngine.ZZEB(const s: string): string;
|
||||||
// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis).
|
// ZZEB: значения TX-эквалайзера одной строкой в протокольном формате.
|
||||||
// nnn + (nnn){nb+1}, добито нулями до 36 символов;
|
// nnn + (nnn){nb+1}, добито нулями до 36 символов;
|
||||||
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
|
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
|
||||||
var
|
var
|
||||||
@@ -2146,7 +2145,7 @@ end;
|
|||||||
|
|
||||||
function TCATEngine.ZZKM(const s: string): string;
|
function TCATEngine.ZZKM(const s: string): string;
|
||||||
// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений
|
// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений
|
||||||
// (окно CW Messages, F1..F8). Write-only, как в Thetis.
|
// (окно CW Messages, F1..F8). Команда доступна только для записи.
|
||||||
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
|
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
|
||||||
// иамбик вкл/выкл доступен через ZZCI.
|
// иамбик вкл/выкл доступен через ZZCI.
|
||||||
var v: Integer;
|
var v: Integer;
|
||||||
@@ -2198,7 +2197,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZMG(const s: string): string;
|
function TCATEngine.ZZMG(const s: string): string;
|
||||||
// ZZMG: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две
|
// ZZMG: усиление микрофона в дБ. SET — три цифры либо знак+две
|
||||||
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
|
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
|
||||||
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
|
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
|
||||||
var v: Integer;
|
var v: Integer;
|
||||||
@@ -2206,7 +2205,7 @@ begin
|
|||||||
if Length(s) = 0 then
|
if Length(s) = 0 then
|
||||||
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
|
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
|
||||||
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
|
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
|
||||||
// как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не
|
// Допустимы две протокольные длины поля. Более короткие поля — ошибка формата, а не
|
||||||
// «маленькое значение»: молча крутить усиление по ним нельзя.
|
// «маленькое значение»: молча крутить усиление по ним нельзя.
|
||||||
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
|
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
|
||||||
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
|
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
|
||||||
@@ -2220,7 +2219,7 @@ function TCATEngine.ZZML: string;
|
|||||||
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
|
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
|
||||||
|
|
||||||
function TCATEngine.ZZMN(const s: string): string;
|
function TCATEngine.ZZMN(const s: string): string;
|
||||||
// ZZMN: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты
|
// ZZMN: имя режима по номеру. В базовом формате ZZMN отдаёт пресеты
|
||||||
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
|
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
|
||||||
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
|
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
|
||||||
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
|
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
|
||||||
@@ -2441,12 +2440,12 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZRV: string;
|
function TCATEngine.ZZRV: string;
|
||||||
// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа).
|
// ZZRV: напряжение питания PA, формат «NN.N» (4 символа).
|
||||||
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
|
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
|
||||||
var v: Double;
|
var v: Double;
|
||||||
begin
|
begin
|
||||||
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1;
|
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1;
|
||||||
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём, как Thetis
|
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём
|
||||||
if v > 99.9 then v := 99.9;
|
if v > 99.9 then v := 99.9;
|
||||||
Result := Format('%04.1f', [v]);
|
Result := Format('%04.1f', [v]);
|
||||||
end;
|
end;
|
||||||
@@ -2459,7 +2458,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZSA: string;
|
function TCATEngine.ZZSA: string;
|
||||||
// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO
|
// ZZSA: шаг настройки вниз. В EWSDR шагает активный VFO,
|
||||||
// (отдельного «шага VFO A» в контроллере нет).
|
// (отдельного «шага VFO A» в контроллере нет).
|
||||||
begin
|
begin
|
||||||
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
|
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
|
||||||
@@ -2652,8 +2651,8 @@ begin
|
|||||||
Result := '';
|
Result := '';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) —
|
// ZZWA..ZZWS: неподдерживаемые команды внешнего микшера — STUB.
|
||||||
// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его
|
// Водопад тут ни при чём: его
|
||||||
// уровни живут в ZZDN/ZZDO.
|
// уровни живут в ZZDN/ZZDO.
|
||||||
function TCATEngine.ZZWA(const s: string): string;
|
function TCATEngine.ZZWA(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
@@ -2714,7 +2713,7 @@ function TCATEngine.ZZYR(const s: string): string;
|
|||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
|
|
||||||
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
|
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
|
||||||
// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
|
// служебные команды; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
|
||||||
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
|
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
|
||||||
// Всё — STUB: либо чужое железо, либо в CAT не проброшено.
|
// Всё — STUB: либо чужое железо, либо в CAT не проброшено.
|
||||||
function TCATEngine.ZZZA(const s: string): string;
|
function TCATEngine.ZZZA(const s: string): string;
|
||||||
@@ -2732,7 +2731,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZZE(const s: string): string;
|
function TCATEngine.ZZZE(const s: string): string;
|
||||||
// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры — номер
|
// ZZZE: Andromeda — поворот энкодера. Верхние 2 цифры кодируют номер
|
||||||
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
|
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
|
||||||
var raw, enc, step: Integer;
|
var raw, enc, step: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -2797,7 +2796,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
|||||||
function TCATEngine.ZZZW(const s: string): string;
|
function TCATEngine.ZZZW(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZZ: string;
|
function TCATEngine.ZZZZ: string;
|
||||||
// ZZZZ: без описания у Thetis — STUB.
|
// ZZZZ: команда не описана — STUB.
|
||||||
begin Result := ''; end;
|
begin Result := ''; end;
|
||||||
function TCATEngine.ZZZO(const s: string): string;
|
function TCATEngine.ZZZO(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
@@ -2806,7 +2805,7 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
|||||||
|
|
||||||
function TCATEngine.ZZCI(const s: string): string;
|
function TCATEngine.ZZCI(const s: string): string;
|
||||||
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
|
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
|
||||||
// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по
|
// (прямой ключ / iambic A / iambic B) — включение выбирает B по
|
||||||
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
|
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
|
||||||
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
|
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
|
||||||
begin
|
begin
|
||||||
@@ -2827,7 +2826,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCATEngine.ZZCM(const s: string): string;
|
function TCATEngine.ZZCM(const s: string): string;
|
||||||
// ZZCM: у Thetis это «CW Monitor DISABLE» — значение инвертировано к сайдтону.
|
// ZZCM означает «CW Monitor DISABLE», значение инвертировано к сайдтону.
|
||||||
begin
|
begin
|
||||||
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
|
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
|
||||||
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
|
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ unit CATSliceEndpoint;
|
|||||||
// └── TCATSliceEndpoint ── TCATEngine ── TCP 19091 (слайс B)
|
// └── TCATSliceEndpoint ── TCATEngine ── TCP 19091 (слайс B)
|
||||||
// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
|
// TCATSliceEndpoint ── TCATEngine ── TCP 19092 (слайс C) …
|
||||||
//
|
//
|
||||||
// Эталон поведения — SmartSDR CAT: порт привязан к слайсу, а флаг Auto TX
|
// Порт привязан к слайсу, а флаг Auto TX разрешает команде TX сделать его
|
||||||
// («Auto Switch TX Slice») разрешает команде TX самой сделать этот слайс
|
|
||||||
// источником передачи. Вся защита «в эфире одновременно только один слайс»
|
// источником передачи. Вся защита «в эфире одновременно только один слайс»
|
||||||
// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
|
// живёт в TRadioController.RequestSliceTx (выполняется в потоке контроллера).
|
||||||
//
|
//
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
unit CATTcp;
|
unit CATTcp;
|
||||||
{
|
{
|
||||||
CATTcp.pas — TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis).
|
CATTcp.pas — многопоточный TCP-сервер команд CAT.
|
||||||
|
|
||||||
Архитектура:
|
Архитектура:
|
||||||
• Один listening socket (TCATTcpServer)
|
• Один listening socket (TCATTcpServer)
|
||||||
|
|||||||
+5
-5
@@ -29,8 +29,8 @@ unit CWKeyer;
|
|||||||
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
|
// джиттера PC там нет вовсе, и это остаётся путём по умолчанию. У Pluto/
|
||||||
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
|
// LibreSDR такого кейера НЕТ: это голый AD936x, в эфир идёт ровно то, что мы
|
||||||
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
|
// сами положили в поток IQ. Значит телеграф там обязан формироваться здесь.
|
||||||
// Эталон — pihpsdr: src/iambic.c (автомат) + cw_shape_buffer в transmitter.c
|
// Автомат формирует элементы и записывает огибающую прямо в TX-буфер,
|
||||||
// (огибающая прямо в TX-буфере, мимо WDSP).
|
// минуя WDSP.
|
||||||
//
|
//
|
||||||
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
|
// ПОЧЕМУ МИМО WDSP. Голосовой TXA в телеграфе не запускается вообще (в WDSP
|
||||||
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
|
// режим TXA_CWL — буквально ветка SSB), да и PostGen-тон не умеет огибающей.
|
||||||
@@ -228,8 +228,8 @@ type
|
|||||||
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
|
TCWKeyPortEvent = procedure(Dot, Dash: Boolean) of object;
|
||||||
|
|
||||||
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
|
{ Опрос манипулятора на модемных линиях COM/USB-serial: DTR/RTS питают ключ,
|
||||||
CTS/DSR/DCD/RI читаются как лепестки. Так же делают Thetis (cwkeyer.cs),
|
CTS/DSR/DCD/RI читаются как лепестки. Такой вход доступен на любом железе
|
||||||
hamlib и fldigi — это единственный вход ключа, доступный на любом железе. }
|
через стандартный последовательный интерфейс. }
|
||||||
TCWKeyPort = class(TThread)
|
TCWKeyPort = class(TThread)
|
||||||
private
|
private
|
||||||
FLock: TCriticalSection;
|
FLock: TCriticalSection;
|
||||||
@@ -780,7 +780,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
|
function TCWLocalKeyer.TakeIambicElement(out IsDot: Boolean): Boolean;
|
||||||
// Автомат иамбика (логика pihpsdr src/iambic.c в терминах элементов):
|
// Автомат иамбика в терминах точек, тире и межэлементных пауз:
|
||||||
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
|
// * зажаты оба лепестка — элементы чередуются (иначе точка «съедала» бы тире);
|
||||||
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
|
// * режим B — лепесток, задетый ВО ВРЕМЯ элемента, запоминается и отыгрывается
|
||||||
// следующим; режим A памяти не имеет (в этом и вся разница);
|
// следующим; режим A памяти не имеет (в этом и вся разница);
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ unit CWMessagesForm;
|
|||||||
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
|
Зачем отдельным окном. Передача текста нужна ПОСРЕДИ связи, поэтому кнопки
|
||||||
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
|
«отправить» обязаны быть под рукой, а не в настройках. В блоке TX левой
|
||||||
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
|
панели свободных ячеек нет (MOX/TUN/PS/2TON + DUP/RX MUTE/профиль + DRV),
|
||||||
так что окно висит рядом — ровно как окно CWX у Thetis. Те же ячейки
|
поэтому окно располагается рядом. Используются те же ячейки
|
||||||
привязаны к F1..F8 в главном окне.
|
привязаны к F1..F8 в главном окне.
|
||||||
|
|
||||||
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
|
Правки текста пишутся в TCWSettings сразу (идиома EWSDR: кнопки «сохранить»
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ unit CWMorse;
|
|||||||
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
|
// * бит CWX в High Priority — это «ключ нажат», сырая линия. Прошивка по
|
||||||
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
|
// нему даёт несущую с той же формой фронта (CWRampPeriod), но КОГДА
|
||||||
// нажать и отпустить, решает PC. Именно этот путь используется для
|
// нажать и отпустить, решает PC. Именно этот путь используется для
|
||||||
// передачи текста (макросы, CAT KY) — так же устроен Thetis (cwx.cs).
|
// передачи текста через макросы и команду CAT KY.
|
||||||
//
|
//
|
||||||
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
|
// Отсюда устройство юнита: отдельный поток, который спит до дедлайнов и
|
||||||
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
|
// дёргает колбэк ключа. Дедлайны АБСОЛЮТНЫЕ (GetTickCount64), иначе ошибка
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ unit DMRDecoder;
|
|||||||
sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
|
sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
|
||||||
|
|
||||||
This first layer deliberately stops at burst sync. The following layer will
|
This first layer deliberately stops at burst sync. The following layer will
|
||||||
pass aligned dibits to the trimmed dsd-fme DMR/FEC/AMBE core.
|
pass aligned dibits to the DMR/FEC/AMBE decoding core.
|
||||||
}
|
}
|
||||||
|
|
||||||
{$IFDEF FPC}
|
{$IFDEF FPC}
|
||||||
@@ -203,7 +203,7 @@ const
|
|||||||
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
|
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
|
||||||
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3;
|
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3;
|
||||||
|
|
||||||
// One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight
|
// One bit per sync character: '1'=0, '3'=1. These are the eight
|
||||||
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
|
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
|
||||||
SYNC_TEXT: array[0..7] of string = (
|
SYNC_TEXT: array[0..7] of string = (
|
||||||
'313333111331131131331131', // BS data
|
'313333111331131131331131', // BS data
|
||||||
@@ -512,7 +512,7 @@ begin
|
|||||||
if FHistoryCount[Phase] >= 24 then
|
if FHistoryCount[Phase] >= 24 then
|
||||||
begin
|
begin
|
||||||
Distance := PopCount24(FSyncShift[Phase] xor Pat);
|
Distance := PopCount24(FSyncShift[Phase] xor Pat);
|
||||||
// Real on-air sync commonly carries one or two sign errors. DSD-FME
|
// Real on-air sync commonly carries one or two sign errors. The
|
||||||
// likewise uses a bounded sync tolerance; requiring an exact 24-symbol
|
// likewise uses a bounded sync tolerance; requiring an exact 24-symbol
|
||||||
// match lost almost every superframe in recorded handheld-radio IQ.
|
// match lost almost every superframe in recorded handheld-radio IQ.
|
||||||
DeltaSamples := FSampleNo - FLastSyncSample;
|
DeltaSamples := FSampleNo - FLastSyncSample;
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ unit DMRProtocol;
|
|||||||
{
|
{
|
||||||
Small, allocation-free DMR burst protocol layer.
|
Small, allocation-free DMR burst protocol layer.
|
||||||
|
|
||||||
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from
|
Implements CACH/AMBE interleavers and Hamming/Golay matrices.
|
||||||
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h).
|
The applicable third-party permissive notice follows.
|
||||||
|
|
||||||
Copyright (C) 2010 DSD Author
|
Copyright (C) 2010 DSD Author
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const
|
|||||||
MAX_FREQ = 20000.0;
|
MAX_FREQ = 20000.0;
|
||||||
TOP_H = 44;
|
TOP_H = 44;
|
||||||
BOTTOM_H = 82;
|
BOTTOM_H = 82;
|
||||||
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под
|
// Кривая соответствует расчёту частотной характеристики WDSP под
|
||||||
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
|
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
|
||||||
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
|
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
|
||||||
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
|
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
|
||||||
@@ -357,7 +357,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TEqualizerControl.BuildResponse;
|
procedure TEqualizerControl.BuildResponse;
|
||||||
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G —
|
// Рассчитывает амплитудную характеристику эквалайзера в дБ без построения FIR. Точки F/G —
|
||||||
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
|
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
|
||||||
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
|
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
|
||||||
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
|
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
|
||||||
|
|||||||
+3
-3
@@ -30,8 +30,8 @@ unit FilterPopup;
|
|||||||
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
|
боковой. Скат нарисован схематично: реальная крутизна у WDSP определяется
|
||||||
длиной FIR (fircore) и никаким пользовательским параметром не управляется.
|
длиной FIR (fircore) и никаким пользовательским параметром не управляется.
|
||||||
|
|
||||||
Эталон содержимого — Thetis FilterForm.cs (12 слотов + Name/Low/High/Width);
|
Содержимое включает 12 слотов и поля Name/Low/High/Width, а график наглядно
|
||||||
графика полосы — наша добавка, у них её нет.
|
показывает положение и форму полосы.
|
||||||
|
|
||||||
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) —
|
Реализован как ДОЧЕРНИЙ контрол верхнеуровневой формы (не отдельное окно) —
|
||||||
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
|
как TPureSignalPopup/TPanDisplayPopup: Wayland не даёт клиенту
|
||||||
@@ -374,7 +374,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TFilterPopup.WidthChange(Sender: TObject);
|
procedure TFilterPopup.WidthChange(Sender: TObject);
|
||||||
// Width производная: держим центр полосы, разъезжаются оба края (как Thetis).
|
// Width — производная: центр полосы сохраняется, изменяются оба края.
|
||||||
var W, C, Lo, Hi: Integer;
|
var W, C, Lo, Hi: Integer;
|
||||||
begin
|
begin
|
||||||
if FLoading then Exit;
|
if FLoading then Exit;
|
||||||
|
|||||||
+13
-13
@@ -498,7 +498,7 @@ procedure TDUCIQSenderThread.Execute;
|
|||||||
const
|
const
|
||||||
DUC_IQ_PAIRS_PER_PACKET = 240;
|
DUC_IQ_PAIRS_PER_PACKET = 240;
|
||||||
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
DUC_SAMPLE_RATE = 192000; // IQ pairs per second
|
||||||
// Пейсинг DUC-FIFO радио — модель dl1ycf pihpsdr (new_protocol_txiq_thread).
|
// Пейсинг DUC-FIFO синхронизирован с аппаратным тактированием радио.
|
||||||
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
|
// Продюсер (TTXDSPThread) mic-клочит очередь: mic-пакеты радио идут на кварце
|
||||||
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
|
// радио, а mic-ADC и DUC-DAC на одном кристалле → темп прихода mic = темп
|
||||||
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
|
// расхода DAC. Значит очередь пополняется ровно радиоклоком, и sender должен
|
||||||
@@ -611,7 +611,7 @@ begin
|
|||||||
DrainVirtualFIFO;
|
DrainVirtualFIFO;
|
||||||
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
||||||
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
||||||
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
// При пустой очереди ждём сигнала о появлении данных.
|
||||||
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
|
// следующий WDSP-блок продюсера. Короткий разрыв покрывает подушка
|
||||||
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
|
// pre-roll в FPGA-FIFO радио; mic-клок продюсера держит очередь полной.
|
||||||
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
|
// На передаче ждём коротко (быстро подхватить пришедший блок), на приёме
|
||||||
@@ -668,7 +668,7 @@ begin
|
|||||||
Sleep(50);
|
Sleep(50);
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
if not FNet.FConnected then Continue;
|
if not FNet.FConnected then Continue;
|
||||||
// Отправляем полный HP с частотами и ALEX — как piHPSDR
|
// Отправляем полный HP-пакет с частотами и состоянием ALEX.
|
||||||
FNet.SendFullHP;
|
FNet.SendFullHP;
|
||||||
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
|
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
|
||||||
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
|
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
|
||||||
@@ -1351,14 +1351,14 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ALEX фильтры — логика из piHPSDR new_protocol.c
|
// Формирование регистра фильтров ALEX.
|
||||||
// Возвращает 32-bit ALEX0 register value
|
// Возвращает 32-bit ALEX0 register value
|
||||||
// RXFreqHz — частота приёма ADC0
|
// RXFreqHz — частота приёма ADC0
|
||||||
// TXFreqHz — частота передачи (или = RXFreqHz при RX)
|
// TXFreqHz — частота передачи (или = RXFreqHz при RX)
|
||||||
// Transmitting — режим передачи
|
// Transmitting — режим передачи
|
||||||
// IsOrion2 — ANAN-7000/8000 (другие BPF)
|
// IsOrion2 — ANAN-7000/8000 (другие BPF)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Преобразует частоту в индекс диапазона 0=160m..10=6m (Thetis-compatible).
|
// Преобразует частоту в протокольный индекс диапазона 0=160m..10=6m.
|
||||||
function FreqToBandIndex(FreqHz: Double): Integer;
|
function FreqToBandIndex(FreqHz: Double): Integer;
|
||||||
begin
|
begin
|
||||||
if FreqHz >= 39850000 then Result := 10 // 6m
|
if FreqHz >= 39850000 then Result := 10 // 6m
|
||||||
@@ -1386,7 +1386,7 @@ end;
|
|||||||
// XVTR без усиления.
|
// XVTR без усиления.
|
||||||
// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11
|
// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11
|
||||||
// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3.
|
// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3.
|
||||||
// WidebandBypass — True когда открыт raw ADC wideband. Как Thetis, раскрываем
|
// WidebandBypass — True, когда открыт raw ADC wideband. В этом режиме раскрываем
|
||||||
// RX front-end через HF bypass, иначе видна только полоса
|
// RX front-end через HF bypass, иначе видна только полоса
|
||||||
// текущего HPF/BPF, а не весь ADC span.
|
// текущего HPF/BPF, а не весь ADC span.
|
||||||
function CalcAlex0(RXFreqHz, TXFreqHz: Double;
|
function CalcAlex0(RXFreqHz, TXFreqHz: Double;
|
||||||
@@ -1411,7 +1411,7 @@ begin
|
|||||||
if not XvtrDisablePA then
|
if not XvtrDisablePA then
|
||||||
begin
|
begin
|
||||||
Result := Result or $08000000; // bit 27: T/R relay
|
Result := Result or $08000000; // bit 27: T/R relay
|
||||||
// Bit 18 = PureSignal (ALEX_PS_BIT в pihpsdr): на TX подключает
|
// Bit 18 = PureSignal: на TX подключает
|
||||||
// внутренний мост обратной связи PA к RX-тракту ADC0.
|
// внутренний мост обратной связи PA к RX-тракту ADC0.
|
||||||
if PureSignalTX then
|
if PureSignalTX then
|
||||||
Result := Result or $00040000;
|
Result := Result or $00040000;
|
||||||
@@ -1604,8 +1604,8 @@ end;
|
|||||||
// Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) —
|
// Строит ЛОГИЧЕСКУЮ маску OC-пинов: бит i = пин i+1 (i = 0..OC_PIN_COUNT-1) —
|
||||||
// в тех же координатах, в которых маски лежат в настройках и рисуются в UI.
|
// в тех же координатах, в которых маски лежат в настройках и рисуются в UI.
|
||||||
// Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP).
|
// Сдвиг в проводное представление байта 1401 делает вызывающий (SendFullHP).
|
||||||
// Портировано из Thetis (Penny.UpdateExtCtrl/adjustForTXAction), урезано:
|
// Реализация учитывает только возможности EWSDR: без PA-override и
|
||||||
// без PA-override и split-pins VFO A/B (см. project_oc_control память).
|
// раздельных пинов VFO A/B.
|
||||||
// RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый
|
// RX: маска банда/слота как есть. TX: маска банда/слота, каждый включённый
|
||||||
// пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их
|
// пин дополнительно гейтится своим TX pin action (MOX/TUNE/TWOTONE и их
|
||||||
// комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON
|
// комбинации) — тот же tx-флаг остаётся True на всём протяжении TUN/2TON
|
||||||
@@ -1735,7 +1735,7 @@ begin
|
|||||||
if FPanDDCEnabled[ddc] then
|
if FPanDDCEnabled[ddc] then
|
||||||
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]);
|
AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]);
|
||||||
|
|
||||||
// PureSignal feedback (как pihpsdr new_protocol_receive_specific): на время
|
// PureSignal feedback: на время
|
||||||
// PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback,
|
// PTT DDC0 ← ADC0 (RX-feedback с моста PA), DDC1 ← «ADC 2» = TX DAC loopback,
|
||||||
// оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) —
|
// оба 192 кС/с 24 бита, DDC1 синхронизирован с DDC0 (byte 1363 bit1) —
|
||||||
// его сэмплы приходят interleaved внутри потока DDC0 (порт 1035).
|
// его сэмплы приходят interleaved внутри потока DDC0 (порт 1035).
|
||||||
@@ -1866,7 +1866,7 @@ end;
|
|||||||
|
|
||||||
procedure THPSDRNetwork.SetCWXKey(Down: Boolean);
|
procedure THPSDRNetwork.SetCWXKey(Down: Boolean);
|
||||||
// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка
|
// Каждый фронт — немедленный HP-пакет: тайминги посылки держит PC, задержка
|
||||||
// доставки = один UDP-пакет (так же делает Thetis, cwx.cs setkey).
|
// доставки определяется отправкой одного UDP-пакета.
|
||||||
begin
|
begin
|
||||||
if FCWXKey = Down then Exit;
|
if FCWXKey = Down then Exit;
|
||||||
FCWXKey := Down;
|
FCWXKey := Down;
|
||||||
@@ -1945,7 +1945,7 @@ begin
|
|||||||
Buf[12 + ddc*4] := Ph and $FF;
|
Buf[12 + ddc*4] := Ph and $FF;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой (pihpsdr, bytes 9..16).
|
// PureSignal: feedback-DDC0/DDC1 следят за TX-частотой из байтов 9..16.
|
||||||
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
|
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
|
||||||
if FPSTXActive then
|
if FPSTXActive then
|
||||||
begin
|
begin
|
||||||
@@ -2000,7 +2000,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы.
|
// XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы.
|
||||||
// Логика как в Thetis:
|
// Правила установки бита:
|
||||||
// в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен)
|
// в XVTR-режиме: бит = DisablePA (внешний T/R relay нужен только если PA подавлен)
|
||||||
// в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab)
|
// в HF-режиме: бит = Alex.EnableXvtrHf (галочка "Enable XVTR HF" в Antenna tab)
|
||||||
if FXvtrEnable then
|
if FXvtrEnable then
|
||||||
|
|||||||
+5
-7
@@ -419,7 +419,7 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean;
|
|||||||
|
|
||||||
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
|
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
|
||||||
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards.
|
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards.
|
||||||
// VoffMV/SensMVA — калибровка датчика (Thetis AmpVoff/AmpSens, ACS712-style).
|
// VoffMV/SensMVA — смещение нуля и чувствительность датчика тока.
|
||||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
||||||
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double;
|
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double;
|
||||||
|
|
||||||
@@ -429,8 +429,7 @@ function ADCToWatts100(RawADC: Word): Double;
|
|||||||
// Convert raw ADC value to RF power in Watts (ANAN-10)
|
// Convert raw ADC value to RF power in Watts (ANAN-10)
|
||||||
function ADCToWatts10(RawADC: Word): Double;
|
function ADCToWatts10(RawADC: Word): Double;
|
||||||
|
|
||||||
// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/
|
// Alex bridge FWD/REV detector → Watts; параметры зависят от модели трансивера:
|
||||||
// computeRefPower по модели трансивера:
|
|
||||||
// 0 = ANAN-10/100/100B/100D (Vref 3.3V)
|
// 0 = ANAN-10/100/100B/100D (Vref 3.3V)
|
||||||
// 1 = ANAN-200D / Orion (Vref 5.0V)
|
// 1 = ANAN-200D / Orion (Vref 5.0V)
|
||||||
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
|
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
|
||||||
@@ -482,7 +481,7 @@ end;
|
|||||||
function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double;
|
function ADCToSupplyVolts(RawADC: Word; BoardType: Integer): Double;
|
||||||
begin
|
begin
|
||||||
// Protocol Appendix A gives the ADC reference voltage only.
|
// Protocol Appendix A gives the ADC reference voltage only.
|
||||||
// The PCB voltage divider (from Thetis source) must also be applied.
|
// The PCB voltage divider must also be applied.
|
||||||
// Board 0 (Atlas): no supply voltage measurement.
|
// Board 0 (Atlas): no supply voltage measurement.
|
||||||
// Boards 1,2,3 (Hermes/Angelia): Vref=3.3V, PCB divider R1=4.7kΩ/R2=0.82kΩ.
|
// Boards 1,2,3 (Hermes/Angelia): Vref=3.3V, PCB divider R1=4.7kΩ/R2=0.82kΩ.
|
||||||
// Board 4 (Orion): protocol reports supply voltage with a 5.0V constant.
|
// Board 4 (Orion): protocol reports supply voltage with a 5.0V constant.
|
||||||
@@ -511,8 +510,8 @@ function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
|||||||
VoffMV: Double; SensMVA: Double): Double;
|
VoffMV: Double; SensMVA: Double): Double;
|
||||||
begin
|
begin
|
||||||
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
|
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
|
||||||
// Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А
|
// Formula: amps = (mV - Voff) / Sens. Defaults: Voff=360 mV,
|
||||||
// (Thetis, ACS712-style); переопределяются калибровкой.
|
// Sens=120 mV/A; both values can be overridden by calibration.
|
||||||
if (BoardType = 5) and (SensMVA > 0) then
|
if (BoardType = 5) and (SensMVA > 0) then
|
||||||
begin
|
begin
|
||||||
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
|
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
|
||||||
@@ -545,7 +544,6 @@ var
|
|||||||
Bridge, Vref, V: Double;
|
Bridge, Vref, V: Double;
|
||||||
CalOff: Integer;
|
CalOff: Integer;
|
||||||
begin
|
begin
|
||||||
// Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower).
|
|
||||||
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
|
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
|
||||||
case DetModel of
|
case DetModel of
|
||||||
1: begin // ANAN-200D / Orion
|
1: begin // ANAN-200D / Orion
|
||||||
|
|||||||
+11
-13
@@ -266,7 +266,7 @@ type
|
|||||||
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
|
FWebSpecPixels: Integer; // потолок точек кадра спектра/водопада для web
|
||||||
// --- CAT ---
|
// --- CAT ---
|
||||||
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
|
FCATAdapter: TCATAdapter; // адаптер CAT поверх контроллера (движок+транспорты+контекст)
|
||||||
FTCIAdapter: TTCIAdapter; // адаптер TCI (WebSocket-сервер Expert Electronics)
|
FTCIAdapter: TTCIAdapter; // адаптер TCI для WebSocket-сервера
|
||||||
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
|
FTCICfg: TTCISettings; // текущие настройки TCI (UI/persist)
|
||||||
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
|
FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (UI/persist; ApplySettings → адаптер)
|
||||||
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
|
// Виджеты построены (конец FormCreate). До этого рендер состояния запрещён:
|
||||||
@@ -749,7 +749,7 @@ type
|
|||||||
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
|
procedure OnPanBandClick(Sender: TObject); // «20m» шапки пана N (3.6)
|
||||||
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
|
procedure OnPanBandSelect(Tag: Integer); // выбор диапазона из flat-меню
|
||||||
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
|
function CurrentAppTheme: TAppTheme; // тек. тема (light/dark)
|
||||||
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // Flex-style смена банда пана
|
procedure ApplyPanBand(P: TPanafallPanel; BandIdx: Integer); // смена диапазона отдельного пана
|
||||||
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
|
function PanBandSelectorVisible: Boolean; // КВ (не XVTR, не Pluto)
|
||||||
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
|
procedure OnPanADCClick(Sender: TObject); // бейдж «RX1/RX2» (3.4)
|
||||||
// Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней.
|
// Per-pan дисплей (3.6): «◑» → поповер палитры/gamma/уровней.
|
||||||
@@ -4027,7 +4027,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
// Маленький FFT даёт новые строки чаще.
|
||||||
if FSpectrumDirty then
|
if FSpectrumDirty then
|
||||||
begin
|
begin
|
||||||
FSpecView.DrawSpectrum;
|
FSpecView.DrawSpectrum;
|
||||||
@@ -4986,12 +4986,11 @@ end;
|
|||||||
// CTUN ВЫКЛ (классический режим):
|
// CTUN ВЫКЛ (классический режим):
|
||||||
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
// FController.FCenterFreq = FController.FVfoA — спектр центрирован на VFO, маркер всегда в центре
|
||||||
//
|
//
|
||||||
// CTUN ВКЛ (piHPSDR/Thetis режим):
|
// CTUN ВКЛ:
|
||||||
// FController.FCenterFreq статичен — спектр/водопад не двигаются
|
// FController.FCenterFreq статичен — спектр/водопад не двигаются
|
||||||
// FController.FVfoA движется по дисплею (маркер гуляет)
|
// FController.FVfoA движется по дисплею (маркер гуляет)
|
||||||
// Перецентрирование — только когда КРАЙ ПОЛОСЫ ФИЛЬТРА выходит за край дисплея
|
// Перецентрирование — только когда край полосы фильтра выходит за границу
|
||||||
// (по образцу OpenHPSDR/PowerSDR: "re-centering occurs as the edge of the passband
|
// дисплея.
|
||||||
// hits the edge of the display")
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
procedure TMainForm.ApplyVfoA(NewFreq: Int64);
|
||||||
begin
|
begin
|
||||||
@@ -5067,7 +5066,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
|
// DUP toggle — локальный режим дисплея без аппаратной команды.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
procedure TMainForm.ApplyDUP(Active: Boolean);
|
procedure TMainForm.ApplyDUP(Active: Boolean);
|
||||||
begin
|
begin
|
||||||
@@ -5401,7 +5400,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
|
// TUN — тон WDSP TXA PostGen + MOX с отдельным уровнем Drive.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
procedure TMainForm.ApplyTUN(Active: Boolean);
|
procedure TMainForm.ApplyTUN(Active: Boolean);
|
||||||
begin
|
begin
|
||||||
@@ -6095,8 +6094,7 @@ end;
|
|||||||
|
|
||||||
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure TMainForm.BtnFilterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
// ПКМ по кнопке фильтра открывает редактор на этом слоте (эталон Thetis:
|
// ПКМ по кнопке фильтра открывает редактор на этом слоте. ЛКМ не трогаем —
|
||||||
// у них это пункт контекстного меню «RX1 Filter Configure»). ЛКМ не трогаем —
|
|
||||||
// иначе окно вылезало бы при каждом обычном переключении фильтра.
|
// иначе окно вылезало бы при каждом обычном переключении фильтра.
|
||||||
begin
|
begin
|
||||||
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
|
if Button = mbRight then OpenFilterEditor((Sender as TFlatButton).Tag);
|
||||||
@@ -7185,7 +7183,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
|
procedure TMainForm.ApplyPanBand(P: TPanafallPanel; BandIdx: Integer);
|
||||||
// Flex-style смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
|
// Смена диапазона пана N: ретюн DDC на VfoA диапазона (из общего
|
||||||
// band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC
|
// band-кэша, как пан 0), активный приёмник садится на VfoA с mode/filter/AGC
|
||||||
// (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы).
|
// (+zoom/pan) диапазона, прочие слайсы пана убираются (они вне новой полосы).
|
||||||
var
|
var
|
||||||
@@ -8904,7 +8902,7 @@ end;
|
|||||||
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
||||||
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
||||||
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
||||||
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis.
|
// tab для IF-диапазона (Alex.RxOnly[IF_band]=3).
|
||||||
procedure TMainForm.ApplyXvtrToNetwork;
|
procedure TMainForm.ApplyXvtrToNetwork;
|
||||||
begin
|
begin
|
||||||
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
// Логика перенесена в контроллер; обёртка для существующих вызовов в UI.
|
||||||
|
|||||||
+2
-2
@@ -217,7 +217,7 @@ const
|
|||||||
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
|
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
|
||||||
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
|
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
|
||||||
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
|
TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE)
|
||||||
// Размер TX-буфера масштабируется от sample-rate (как SoapyPlutoSDR):
|
// Размер TX-буфера масштабируется от sample-rate:
|
||||||
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
|
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
|
||||||
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
||||||
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
||||||
@@ -387,7 +387,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TxBufPairsForRate(Rate: Integer): Integer;
|
function TxBufPairsForRate(Rate: Integer): Integer;
|
||||||
// Размер TX-буфера в IQ-парах под device-rate (как SoapyPlutoSDR): round(rate/60)
|
// Размер TX-буфера в IQ-парах под device-rate: round(rate/60)
|
||||||
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
|
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
|
||||||
// 576k→16384, 1536k→32768, 3072k→65536.
|
// 576k→16384, 1536k→32768, 3072k→65536.
|
||||||
var
|
var
|
||||||
|
|||||||
+2
-2
@@ -116,7 +116,7 @@ const
|
|||||||
CLR_INPUT = TColor($00FFFFFF);
|
CLR_INPUT = TColor($00FFFFFF);
|
||||||
CLR_INPUT_TEXT = TColor($00202020);
|
CLR_INPUT_TEXT = TColor($00202020);
|
||||||
|
|
||||||
// Лампа feedback — пороги от цели PSFBTarget (Thetis-Enhanced eu2av):
|
// Лампа feedback — пороги относительно цели PSFBTarget:
|
||||||
// >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет
|
// >1.5×цели горячо, 0.7..1.3×цели оптимум, 0.5×..0.7× слабовато, ниже — нет
|
||||||
// сигнала. TColor = BGR.
|
// сигнала. TColor = BGR.
|
||||||
CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал)
|
CLR_FB_HOT = TColor($00FF9030); // голубой: слишком горячо (атт. мал)
|
||||||
@@ -390,7 +390,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPureSignalPopup.DefPkClick(Sender: TObject);
|
procedure TPureSignalPopup.DefPkClick(Sender: TObject);
|
||||||
// Дефолт SetPk для протокола 2 (Thetis clsHardwareSpecific.PSDefaultPeak).
|
// Значение SetPk по умолчанию для протокола 2.
|
||||||
begin
|
begin
|
||||||
FSePk.Value := 0.2899; // OnChange → Apply
|
FSePk.Value := 0.2899; // OnChange → Apply
|
||||||
end;
|
end;
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ type
|
|||||||
procedure ClearDUCIQQueue; virtual;
|
procedure ClearDUCIQQueue; virtual;
|
||||||
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
|
// Pre-roll: закачать Pairs нулевых IQ-пар в TX-очередь на старте передачи —
|
||||||
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
|
// радио начинает с наполненным DUC-FIFO (подавление underrun-всплеска
|
||||||
// первых мс, как pre-roll 1024 в piHPSDR). База/Pluto — no-op: у Pluto
|
// первых миллисекунд). База/Pluto — no-op: у Pluto
|
||||||
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
|
// TX-стрим непрерывный, его FIFO при недоборе сам добивается нулями.
|
||||||
procedure PrimeDUCIQ(Pairs: Integer); virtual;
|
procedure PrimeDUCIQ(Pairs: Integer); virtual;
|
||||||
|
|
||||||
|
|||||||
+42
-45
@@ -428,7 +428,7 @@ type
|
|||||||
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
|
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
|
||||||
FPSSingleCal: Boolean; // запрошена одиночная калибровка
|
FPSSingleCal: Boolean; // запрошена одиночная калибровка
|
||||||
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
|
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
|
||||||
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
|
FPSCmdState: Integer; // состояние автомата команд PureSignal
|
||||||
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
|
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
|
||||||
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
|
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
|
||||||
FPSAASaveSingle: Integer;
|
FPSAASaveSingle: Integer;
|
||||||
@@ -497,7 +497,7 @@ type
|
|||||||
FSpecRefLevel: Double;
|
FSpecRefLevel: Double;
|
||||||
FSpecRange: Double;
|
FSpecRange: Double;
|
||||||
FSpecGridStep: Double;
|
FSpecGridStep: Double;
|
||||||
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
|
FTXSpecRefLevel: Double; // опорный уровень TX-сетки при FTransmitting
|
||||||
FTXSpecRange: Double;
|
FTXSpecRange: Double;
|
||||||
FTXSpecGridStep: Double;
|
FTXSpecGridStep: Double;
|
||||||
|
|
||||||
@@ -718,7 +718,7 @@ type
|
|||||||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||||||
procedure ApplyModeFilter;
|
procedure ApplyModeFilter;
|
||||||
|
|
||||||
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
|
// Вычисляет аппаратный drive-байт из текущего состояния мощности.
|
||||||
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
|
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
|
||||||
function CalcDriveByte: Byte;
|
function CalcDriveByte: Byte;
|
||||||
|
|
||||||
@@ -1152,8 +1152,8 @@ type
|
|||||||
function CWSettings: TCWSettings;
|
function CWSettings: TCWSettings;
|
||||||
procedure SetCWSettings(const C: TCWSettings);
|
procedure SetCWSettings(const C: TCWSettings);
|
||||||
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
||||||
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон
|
// CWX в High Priority — точки/тире рисует прошивка по этому биту.
|
||||||
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
// Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||||||
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
||||||
procedure CWXSend(const Text: string);
|
procedure CWXSend(const Text: string);
|
||||||
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
|
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
|
||||||
@@ -1192,8 +1192,8 @@ type
|
|||||||
procedure SetPureSignal(On_: Boolean);
|
procedure SetPureSignal(On_: Boolean);
|
||||||
procedure PSSingleCal; // одиночная калибровка
|
procedure PSSingleCal; // одиночная калибровка
|
||||||
procedure PSReset; // выключить и сбросить коррекцию
|
procedure PSReset; // выключить и сбросить коррекцию
|
||||||
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis
|
// Тик PS (~10 Гц, из UI-таймера): автомат команд, опрос GetPSInfo и
|
||||||
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX.
|
// автоматическая регулировка AttOnTX по уровню обратной связи.
|
||||||
procedure PureSignalTick;
|
procedure PureSignalTick;
|
||||||
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
|
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
|
||||||
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
|
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
|
||||||
@@ -1202,7 +1202,7 @@ type
|
|||||||
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
|
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
|
||||||
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
|
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
|
||||||
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
|
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
|
||||||
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis).
|
// замеры на Anvelina PRO3; для остальных используется значение 152.
|
||||||
function PSFBTarget: Integer;
|
function PSFBTarget: Integer;
|
||||||
function PSCalCount: Integer; // info[5]
|
function PSCalCount: Integer; // info[5]
|
||||||
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
|
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
|
||||||
@@ -1337,8 +1337,8 @@ const
|
|||||||
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
|
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
|
||||||
TUN_TONE_MAG = 0.83;
|
TUN_TONE_MAG = 0.83;
|
||||||
|
|
||||||
// Состояния командной state machine PureSignal (порт Thetis eCMDState;
|
// Состояния автомата команд PureSignal. Восстановление сохранённой
|
||||||
// Restore-ветка (7) не реализована — Save/Restore коррекции нет).
|
// коррекции не реализовано.
|
||||||
PS_ST_OFF = 0;
|
PS_ST_OFF = 0;
|
||||||
PS_ST_TURNON_AUTO = 1;
|
PS_ST_TURNON_AUTO = 1;
|
||||||
PS_ST_AUTOCAL = 2;
|
PS_ST_AUTOCAL = 2;
|
||||||
@@ -1827,7 +1827,7 @@ begin
|
|||||||
SupplyV := -1.0;
|
SupplyV := -1.0;
|
||||||
SupplyA := -1.0;
|
SupplyA := -1.0;
|
||||||
|
|
||||||
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
|
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1.
|
||||||
// Прочие платы — в SupplyVolts-байтах, без тока.
|
// Прочие платы — в SupplyVolts-байтах, без тока.
|
||||||
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
|
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
|
||||||
begin
|
begin
|
||||||
@@ -1849,7 +1849,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
||||||
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
||||||
// (как Thetis PABandCalMult).
|
// и используется только для калибровки показаний мощности.
|
||||||
Bi := TxBandIdx;
|
Bi := TxBandIdx;
|
||||||
Is6m := Bi = BAND_6M;
|
Is6m := Bi = BAND_6M;
|
||||||
DetK := 1.0;
|
DetK := 1.0;
|
||||||
@@ -1859,7 +1859,7 @@ begin
|
|||||||
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
||||||
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
||||||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||||||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
|
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW); только при значимой
|
||||||
// fwd-мощности.
|
// fwd-мощности.
|
||||||
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
||||||
begin
|
begin
|
||||||
@@ -1884,8 +1884,8 @@ begin
|
|||||||
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
||||||
FHPpOverload := Status.ADCOverload;
|
FHPpOverload := Status.ADCOverload;
|
||||||
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
||||||
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
|
// Касание обрывает программную передачу, чтобы оператор мог вклиниться.
|
||||||
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
|
// ★Обрываем по
|
||||||
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
|
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
|
||||||
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
|
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
|
||||||
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
|
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
|
||||||
@@ -2989,7 +2989,7 @@ function TRadioController.FrontEndCalDB: Double;
|
|||||||
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
|
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
|
||||||
// или само и не должно двигать абсолютную шкалу dBm.
|
// или само и не должно двигать абсолютную шкалу dBm.
|
||||||
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
|
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
|
||||||
// показания (так же делают Thetis и piHPSDR).
|
// показания.
|
||||||
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
|
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
|
||||||
// его само) → вычитаем текущее значение.
|
// его само) → вычитаем текущее значение.
|
||||||
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
|
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
|
||||||
@@ -3356,8 +3356,8 @@ end;
|
|||||||
|
|
||||||
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
|
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||||||
Tune: Boolean = False);
|
Tune: Boolean = False);
|
||||||
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог
|
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. При
|
||||||
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится
|
// включённом Auto TX слайс сам становится
|
||||||
// TX-источником, без него — передаёт, только если оператор уже выбрал его
|
// TX-источником, без него — передаёт, только если оператор уже выбрал его
|
||||||
// источником (бейдж TX на флаге).
|
// источником (бейдж TX на флаге).
|
||||||
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
|
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
|
||||||
@@ -4049,7 +4049,7 @@ begin
|
|||||||
Hi := FFilterHi;
|
Hi := FFilterHi;
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
begin
|
begin
|
||||||
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
// Только RX. TX-полоса НЕ следует за RX-фильтром — её
|
||||||
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
|
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
|
||||||
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
|
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
|
||||||
FDSPEngine.SetFilter(Lo, Hi);
|
FDSPEngine.SetFilter(Lo, Hi);
|
||||||
@@ -4148,7 +4148,7 @@ end;
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
function TRadioController.CalcDriveByte: Byte;
|
function TRadioController.CalcDriveByte: Byte;
|
||||||
// Thetis-совместимая формула:
|
// Преобразование целевой мощности в аппаратный байт drive:
|
||||||
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
|
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
|
||||||
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
|
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
|
||||||
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
|
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
|
||||||
@@ -4385,9 +4385,9 @@ begin
|
|||||||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||||
|
|
||||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
// --- Фильтр: санитизация FM до применения к DSP ----------------------
|
||||||
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
||||||
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
|
// отдельно по режимам. Иначе у VAR было бы два источника истины —
|
||||||
// ровно та болезнь, от которой уходили.
|
// ровно та болезнь, от которой уходили.
|
||||||
ApplyFilterSlot(B.FilterIdx);
|
ApplyFilterSlot(B.FilterIdx);
|
||||||
ApplyModeFilter;
|
ApplyModeFilter;
|
||||||
@@ -5814,7 +5814,7 @@ end;
|
|||||||
function TRadioController.CWTXActive: Boolean;
|
function TRadioController.CWTXActive: Boolean;
|
||||||
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
|
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
|
||||||
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
|
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
|
||||||
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570),
|
// состоянии голосовой TXA не запускается вовсе,
|
||||||
// а несущую даёт кейер / бит CWX / тон TUN.
|
// а несущую даёт кейер / бит CWX / тон TUN.
|
||||||
begin
|
begin
|
||||||
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||||||
@@ -5851,10 +5851,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.CWOptionsByte: Byte;
|
function TRadioController.CWOptionsByte: Byte;
|
||||||
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
// Формирует DUC Specific byte 5; iambic-бит выставляется и для Mode B.
|
||||||
// объяснение, почему iambic-бит выставляется и для Mode B).
|
|
||||||
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
|
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
|
||||||
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
// на замыкание ключа посреди SSB.
|
||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
|
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
|
||||||
@@ -5913,10 +5912,10 @@ begin
|
|||||||
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||||||
DUCPkt.DUC0Bits := 24;
|
DUCPkt.DUC0Bits := 24;
|
||||||
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||||||
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
|
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB с шагом 1.5 dB.
|
||||||
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
|
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
|
||||||
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
|
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
|
||||||
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
|
// ATT on TX — ступенчатый аттенюатор RX-ADC во время передачи.
|
||||||
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
|
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
|
||||||
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
|
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
|
||||||
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
||||||
@@ -6269,7 +6268,7 @@ end;
|
|||||||
procedure TRadioController.ApplyTXSettingsToDSP;
|
procedure TRadioController.ApplyTXSettingsToDSP;
|
||||||
begin
|
begin
|
||||||
if not FWDSPReady then Exit;
|
if not FWDSPReady then Exit;
|
||||||
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
|
// Для openHPSDR Protocol 2 CFIR постоянно включён:
|
||||||
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
|
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
|
||||||
// имеет, поэтому при смене бэкенда явно выключаем его.
|
// имеет, поэтому при смене бэкенда явно выключаем его.
|
||||||
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
|
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
|
||||||
@@ -6418,8 +6417,8 @@ begin
|
|||||||
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
||||||
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
||||||
FDSPEngine.PSSetMox(FTransmitting);
|
FDSPEngine.PSSetMox(FTransmitting);
|
||||||
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis,
|
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ. В WDSP режим TXA_CWL —
|
||||||
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому
|
// буквально ветка SSB, поэтому
|
||||||
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
|
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
|
||||||
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
|
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
|
||||||
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
|
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
|
||||||
@@ -6476,8 +6475,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
||||||
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
|
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
|
||||||
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
|
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон.
|
||||||
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
|
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
begin
|
begin
|
||||||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||||||
@@ -6902,7 +6900,7 @@ procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
|
|||||||
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
|
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
|
||||||
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
|
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
|
||||||
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
|
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
|
||||||
// ключ (это же ограничение у Thetis).
|
// ключ; это ограничение удалённого управления телеграфом.
|
||||||
begin
|
begin
|
||||||
if not CWTXActive then
|
if not CWTXActive then
|
||||||
begin
|
begin
|
||||||
@@ -7071,7 +7069,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
|
// PureSignal — оркестрация автомата калибровки и обратной связи.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
function TRadioController.PSAvailable: Boolean;
|
function TRadioController.PSAvailable: Boolean;
|
||||||
@@ -7097,7 +7095,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetPureSignal(On_: Boolean);
|
procedure TRadioController.SetPureSignal(On_: Boolean);
|
||||||
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение —
|
// Кнопка PS включает режим автоматической калибровки. Выключение —
|
||||||
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
|
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
|
||||||
begin
|
begin
|
||||||
if On_ = FPSEnabled then Exit;
|
if On_ = FPSEnabled then Exit;
|
||||||
@@ -7114,7 +7112,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.PSSingleCal;
|
procedure TRadioController.PSSingleCal;
|
||||||
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается.
|
// Одиночная калибровка выключает автоматический режим.
|
||||||
begin
|
begin
|
||||||
if not PSAvailable then Exit;
|
if not PSAvailable then Exit;
|
||||||
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
|
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
|
||||||
@@ -7140,7 +7138,7 @@ begin
|
|||||||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||||||
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
|
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
|
||||||
else
|
else
|
||||||
Result := 152; // классическая цель Thetis
|
Result := 152; // стандартный целевой уровень
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -7150,7 +7148,7 @@ begin
|
|||||||
if Result < 0 then
|
if Result < 0 then
|
||||||
begin
|
begin
|
||||||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||||||
Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av
|
Result := 5.0 // Orion MkII: значение по умолчанию
|
||||||
else
|
else
|
||||||
Result := 0.0; // выключен
|
Result := 0.0; // выключен
|
||||||
end;
|
end;
|
||||||
@@ -7203,9 +7201,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.PureSignalTick;
|
procedure TRadioController.PureSignalTick;
|
||||||
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis
|
// Вызывается из UI-таймера (~10 Гц). Автомат управляет калибровкой без
|
||||||
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы)
|
// восстановления сохранённой коррекции и подстраивает AttOnTX по feedback.
|
||||||
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
|
|
||||||
var
|
var
|
||||||
NewAtt: Integer;
|
NewAtt: Integer;
|
||||||
FBTarget: Integer;
|
FBTarget: Integer;
|
||||||
@@ -7287,7 +7284,7 @@ begin
|
|||||||
PS_ST_TURNOFF:
|
PS_ST_TURNOFF:
|
||||||
begin
|
begin
|
||||||
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
|
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
|
||||||
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF).
|
// не дойдёт до LRESET при PSEnabled=true в состоянии TurnOFF.
|
||||||
FDSPEngine.PSControl(1, 0, 0, 0);
|
FDSPEngine.PSControl(1, 0, 0, 0);
|
||||||
PSSetFeedbackPath(True);
|
PSSetFeedbackPath(True);
|
||||||
FPSResetReq := False;
|
FPSResetReq := False;
|
||||||
@@ -7300,7 +7297,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) ---
|
// --- Автоматическая регулировка аттенюатора обратной связи ----------
|
||||||
// По завершении очередной попытки калибровки: если feedback слишком горячий
|
// По завершении очередной попытки калибровки: если feedback слишком горячий
|
||||||
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
|
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
|
||||||
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
|
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
|
||||||
@@ -7350,7 +7347,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetTwoTone(On_: Boolean);
|
procedure TRadioController.SetTwoTone(On_: Boolean);
|
||||||
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный
|
// 2TON: двухтональник PostGen + MOX. Уровень — обычный
|
||||||
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
|
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
|
||||||
begin
|
begin
|
||||||
if On_ = FTwoTone then Exit;
|
if On_ = FTwoTone then Exit;
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ unit SerialPort;
|
|||||||
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
|
(rtl-extra/fpmake.pp), поэтому под macOS проект не собирался вовсе, а CAT и
|
||||||
телеграфный ключ приходилось глушить заглушкой.
|
телеграфный ключ приходилось глушить заглушкой.
|
||||||
|
|
||||||
★И скопировать чужой исходник было нельзя. `Serial.SerSetParams` кладёт
|
★Готовая реализация `Serial.SerSetParams` кладёт
|
||||||
B-константу скорости прямо в c_cflag — идиома Linux, где это битовая маска.
|
B-константу скорости прямо в c_cflag — идиома Linux, где это битовая маска.
|
||||||
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
|
На BSD и macOS `B115200` это просто ЧИСЛО 115200, и в c_cflag прилетает
|
||||||
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
|
$1C200, то есть CLOCAL | HUPCL | CCTS_OFLOW плюс мусор в поле CSIZE:
|
||||||
|
|||||||
+31
-35
@@ -33,8 +33,7 @@ const
|
|||||||
CFG_BAND_COUNT = 12;
|
CFG_BAND_COUNT = 12;
|
||||||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
||||||
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
|
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
|
||||||
// TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs):
|
// TX pin action: определяет, при каком состоянии передачи пин активен.
|
||||||
// при каком состоянии передачи пин активен.
|
|
||||||
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
|
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
|
||||||
OCA_TUNE = 1; // только TUNE
|
OCA_TUNE = 1; // только TUNE
|
||||||
OCA_TWOTONE = 2; // только 2TON
|
OCA_TWOTONE = 2; // только 2TON
|
||||||
@@ -52,7 +51,7 @@ const
|
|||||||
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
|
||||||
// (split-LO транспондер; настраивается на
|
// (split-LO транспондер; настраивается на
|
||||||
// отдельной странице, не в общей XVTR-таблице)
|
// отдельной странице, не в общей XVTR-таблице)
|
||||||
// PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults
|
// PA gain defaults (dB) for HERMES/HPSDR hardware.
|
||||||
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
|
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
|
||||||
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
|
// 12-й слот — пустой в HF-плане (используется только 12-местным VHF/UHF-планом
|
||||||
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
|
// Pluto, где частота/режим приходят из плана BoardUtils, а не отсюда).
|
||||||
@@ -128,7 +127,7 @@ const
|
|||||||
('6.25k', '10k', '12.5k', '20k', '25k');
|
('6.25k', '10k', '12.5k', '20k', '25k');
|
||||||
|
|
||||||
// --- Редактируемая таблица фильтров -------------------------------------
|
// --- Редактируемая таблица фильтров -------------------------------------
|
||||||
// 10 пресетов + VAR1/VAR2 на режим (паритет с Thetis FilterForm). Кромки
|
// 10 пресетов + VAR1/VAR2 на режим. Кромки
|
||||||
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
|
// ЗНАКОВЫЕ и хранятся ПО РЕЖИМУ: у LSB своя строка (-2800..-100), у USB
|
||||||
// своя — поэтому при применении фильтра конвертировать боковую больше не
|
// своя — поэтому при применении фильтра конвертировать боковую больше не
|
||||||
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
|
// нужно, и таблица знака в проекте остаётся ровно одна (FilterEdgesFromBW,
|
||||||
@@ -249,11 +248,11 @@ type
|
|||||||
// Mic gain (dB, применяется в WDSP PanelGain1)
|
// Mic gain (dB, применяется в WDSP PanelGain1)
|
||||||
MicGainDB: Double; // -10..+30
|
MicGainDB: Double; // -10..+30
|
||||||
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
|
// ATT on TX — ослабление RX-ADC во время передачи (защита от утечки TX).
|
||||||
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2). Thetis default: 31 dB.
|
// DUC Specific bytes 57/58/59 (StepAtten 0/1/2), default 31 dB.
|
||||||
AttOnTX: Integer; // 0..31 dB
|
AttOnTX: Integer; // 0..31 dB
|
||||||
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
|
// Tune (кнопка TUN) — генерация тонального сигнала через WDSP PostGen.
|
||||||
// Уровень — отдельный от слайдера Drive, проходит через ту же
|
// Уровень — отдельный от слайдера Drive, проходит через ту же
|
||||||
// per-band калибровку. Thetis defaults: level=10 %, freq=750 Hz.
|
// per-band калибровку. Defaults: level=10 %, freq=750 Hz.
|
||||||
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
|
TUNLevel: Integer; // 0..100 (% от полной шкалы Drive)
|
||||||
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
|
TUNFreq: Integer; // Hz, обычно 600..1500 для SSB tune
|
||||||
// TX bandpass filter
|
// TX bandpass filter
|
||||||
@@ -301,11 +300,11 @@ type
|
|||||||
TXWfDetector: Integer;
|
TXWfDetector: Integer;
|
||||||
TXWfAvgMode: Integer;
|
TXWfAvgMode: Integer;
|
||||||
TXWfAvgTimeMS: Double;
|
TXWfAvgTimeMS: Double;
|
||||||
// TX Grid (отдельные от RX — Thetis отрисовывает спектр TX в своих границах)
|
// TX Grid задаётся отдельно от RX и использует собственные границы.
|
||||||
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
|
TXSpecRefLevel: Double; // dBm reference (top of scale), e.g. 0
|
||||||
TXSpecRange: Double; // dB range, e.g. 80
|
TXSpecRange: Double; // dB range, e.g. 80
|
||||||
TXSpecGridStep: Double; // grid step in dB, e.g. 10
|
TXSpecGridStep: Double; // grid step in dB, e.g. 10
|
||||||
// PureSignal (WDSP calcc; дефолты = Thetis PSForm, протокол 2)
|
// PureSignal (WDSP calcc, протокол 2)
|
||||||
PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
|
PSMoxDelay: Double; // с — задержка после MOX до сбора feedback (0.2)
|
||||||
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
|
PSLoopDelay: Double; // с — пауза между итерациями калибровки (0)
|
||||||
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
|
PSTXDelayNs: Integer; // нс — компенсация задержки TX-тракта (150)
|
||||||
@@ -318,7 +317,7 @@ type
|
|||||||
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
|
PSHWPeak: Double; // SetPk (0.2899 для P2, 0.6121 Saturn)
|
||||||
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
|
PSTTFreq1: Integer; // Гц — тон 1 двухтональника (700)
|
||||||
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
|
PSTTFreq2: Integer; // Гц — тон 2 двухтональника (1900)
|
||||||
// Калибровка фидбек-тракта (Thetis-Enhanced eu2av): на Orion MkII цель 152
|
// Калибровка фидбек-тракта: на Orion MkII цель 152
|
||||||
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
|
// перегружает входные каскады ADC → IMD-«юбка», оптимум FB≈22 при ATT≈10дБ.
|
||||||
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
|
PSFBTarget: Integer; // цель уровня feedback info[4]; 0 = авто по плате
|
||||||
// (Orion MkII → 22, иначе 152)
|
// (Orion MkII → 22, иначе 152)
|
||||||
@@ -331,10 +330,9 @@ type
|
|||||||
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
|
// мы только отдаём ему параметры (DUC Specific байты 5..13, 17). Поэтому
|
||||||
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
|
// почти всё здесь — свойство устройства, а не «звука», и живёт отдельно от
|
||||||
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
|
// TTXSettings/TTXProfile (профиль голоса телеграф не переключает).
|
||||||
// Раскладка байта 5 — HPSDRProtocol.CW_*; эталон Thetis (console.cs/setup.cs)
|
// Раскладка байта 5 задана константами HPSDRProtocol.CW_*.
|
||||||
// и pihpsdr (new_protocol.c:1353).
|
|
||||||
TCWSettings = record
|
TCWSettings = record
|
||||||
Pitch: Integer; // Гц, тон приёма/сайдтона (Thetis default 600)
|
Pitch: Integer; // Гц, тон приёма/сайдтона, default 600
|
||||||
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
// Память сообщений для программной передачи (окно CW Messages, F1..F8).
|
||||||
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
Messages: array[0..CW_MSG_COUNT-1] of string[63];
|
||||||
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
|
// Манипуляция разрешена вообще (False = телеграф без кейера: MOX + голосом
|
||||||
@@ -348,7 +346,7 @@ type
|
|||||||
Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
|
Weight: Integer; // 33..66 (50 = точка:пауза 1:1)
|
||||||
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
|
BreakIn: Boolean; // прошивка сама поднимает PTT на нажатие ключа
|
||||||
HangTimeMS: Integer; // сколько держать TX после последнего элемента
|
HangTimeMS: Integer; // сколько держать TX после последнего элемента
|
||||||
RampMS: Integer; // форма фронта посылки (Thetis зашил 9 мс)
|
RampMS: Integer; // форма фронта посылки, default 9 мс
|
||||||
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
|
RFDelayMS: Integer; // задержка РЧ после PTT (byte 13, реле/усилитель)
|
||||||
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
|
SidetoneHW: Boolean; // тон в наушники САМОГО трансивера (байт 5/6)
|
||||||
SidetoneHWLevel: Integer; // 0..127
|
SidetoneHWLevel: Integer; // 0..127
|
||||||
@@ -394,8 +392,7 @@ type
|
|||||||
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
|
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
|
||||||
// зависит: переключение профиля не должно перерисовывать спектр, менять
|
// зависит: переключение профиля не должно перерисовывать спектр, менять
|
||||||
// задержку тракта или ломать калибровку PS.
|
// задержку тракта или ломать калибровку PS.
|
||||||
// Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без
|
// Профиль не содержит параметры VAC и DSP-буферов, которых в EWSDR нет.
|
||||||
// её хвоста из VAC/DSP-буферов, которых у нас нет.
|
|
||||||
TTXProfile = record
|
TTXProfile = record
|
||||||
Name: string[31];
|
Name: string[31];
|
||||||
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
|
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
|
||||||
@@ -428,7 +425,7 @@ type
|
|||||||
EQFreqs: array[0..10] of Double;
|
EQFreqs: array[0..10] of Double;
|
||||||
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
|
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
|
||||||
AMCarrierLevel: Double;
|
AMCarrierLevel: Double;
|
||||||
// Мощность (Thetis держит Power/Tune_Power в профиле)
|
// Мощность Power/Tune_Power хранится в профиле.
|
||||||
DrivePercent: Integer; // 0..100
|
DrivePercent: Integer; // 0..100
|
||||||
TUNLevel: Integer; // 0..100
|
TUNLevel: Integer; // 0..100
|
||||||
end;
|
end;
|
||||||
@@ -478,12 +475,12 @@ type
|
|||||||
SpecPixels: Integer;
|
SpecPixels: Integer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// TCI-сервер (протокол Expert Electronics поверх WebSocket). Авторизации в
|
// TCI-сервер поверх WebSocket. Авторизации в
|
||||||
// протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые
|
// протоколе нет, поэтому умолчание слушает только петлю: логгеры и цифровые
|
||||||
// программы обычно живут на том же ПК, а наружу порт открывается осознанно.
|
// программы обычно живут на том же ПК, а наружу порт открывается осознанно.
|
||||||
TTCISettings = record
|
TTCISettings = record
|
||||||
Enabled: Boolean;
|
Enabled: Boolean;
|
||||||
Port: Integer; // 1..65535, умолчание 40001 (как у ExpertSDR3)
|
Port: Integer; // 1..65535, по умолчанию 40001
|
||||||
BindAddr: string; // '127.0.0.1' или '0.0.0.0'
|
BindAddr: string; // '127.0.0.1' или '0.0.0.0'
|
||||||
// ★Единственный каталог, куда TCI пишет записи линейного выхода
|
// ★Единственный каталог, куда TCI пишет записи линейного выхода
|
||||||
// (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records.
|
// (LINE_OUT_RECORDER_SAVE). Пусто — <каталог конфигурации>/records.
|
||||||
@@ -579,12 +576,11 @@ type
|
|||||||
|
|
||||||
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
|
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
|
||||||
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
|
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
|
||||||
// Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis
|
// Хранятся в JSON-секции "oc" под MAC. Поддерживается сокращённый набор:
|
||||||
// (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён:
|
|
||||||
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
|
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
|
||||||
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
|
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
|
||||||
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings).
|
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings).
|
||||||
// VHF-группа — 8 слотов XVTR (аналог Thetis VHF0-13, но по нашим слотам).
|
// VHF-группа — 8 слотов XVTR по внутренней нумерации EWSDR.
|
||||||
TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
|
TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
|
||||||
|
|
||||||
TOCSettings = record
|
TOCSettings = record
|
||||||
@@ -599,13 +595,13 @@ type
|
|||||||
|
|
||||||
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
|
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
|
||||||
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
|
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
|
||||||
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
|
// (Vref, bridge voltage и ADC offset).
|
||||||
TCalibration = record
|
TCalibration = record
|
||||||
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
|
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
|
||||||
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
|
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
|
||||||
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
|
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
|
||||||
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360)
|
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ
|
||||||
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120)
|
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А
|
||||||
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
|
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
|
||||||
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
|
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
|
||||||
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
|
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
|
||||||
@@ -613,11 +609,11 @@ type
|
|||||||
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
||||||
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
||||||
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
|
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
|
||||||
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его
|
// Display Cal Offset: у display-тракта своё окно/детектор, и его
|
||||||
// уровень отличается от показаний стрелки на пару дБ.
|
// уровень отличается от показаний стрелки на пару дБ.
|
||||||
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
|
||||||
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
|
||||||
// Частотная калибровка опорника, ppm (Frequency Calibration в Thetis).
|
// Частотная калибровка опорника, ppm.
|
||||||
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
|
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
|
||||||
// ниже истинной частоты; положительное значение поднимает показания.
|
// ниже истинной частоты; положительное значение поднимает показания.
|
||||||
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров
|
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров
|
||||||
@@ -720,8 +716,8 @@ type
|
|||||||
// Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
|
// Индекс = слот слайса (0 = слайс B), см. CFG_CAT_SLICE_COUNT.
|
||||||
CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
CATSliceEnabled: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
||||||
CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
|
CATSlicePort: array[0..CFG_CAT_SLICE_COUNT-1] of Integer; // 19091, 19092, …
|
||||||
// Auto TX (аналог «Auto Switch TX Slice» в SmartSDR CAT): команда TX по
|
// Auto TX: команда TX по CAT-порту слайса сама назначает этот слайс
|
||||||
// CAT-порту слайса сама назначает этот слайс источником передачи.
|
// источником передачи.
|
||||||
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
CATSliceAutoTX: array[0..CFG_CAT_SLICE_COUNT-1] of Boolean;
|
||||||
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
|
// Аудио слота слайса: имена PortAudio-устройств ('' = общий выход
|
||||||
// приложения / общий вход). Имена, а не индексы: порядок перечисления
|
// приложения / общий вход). Имена, а не индексы: порядок перечисления
|
||||||
@@ -1710,21 +1706,21 @@ begin
|
|||||||
T.MicBias := True;
|
T.MicBias := True;
|
||||||
T.LineInGainDB := 0.0; // 0 dB
|
T.LineInGainDB := 0.0; // 0 dB
|
||||||
T.MicGainDB := 10.0;
|
T.MicGainDB := 10.0;
|
||||||
// ATT on TX — Thetis default 31 dB (полная защита RX-ADC во время передачи)
|
// ATT on TX — default 31 dB (полная защита RX-ADC во время передачи)
|
||||||
T.AttOnTX := 31;
|
T.AttOnTX := 31;
|
||||||
// Tune — Thetis defaults
|
// Tune defaults
|
||||||
T.TUNLevel := 10;
|
T.TUNLevel := 10;
|
||||||
T.TUNFreq := 750;
|
T.TUNFreq := 750;
|
||||||
// SSB-фильтр по умолчанию
|
// SSB-фильтр по умолчанию
|
||||||
T.FilterLow := 200;
|
T.FilterLow := 200;
|
||||||
T.FilterHigh := 3100;
|
T.FilterHigh := 3100;
|
||||||
T.FilterNC := 1024;
|
T.FilterNC := 1024;
|
||||||
T.FilterMP := 1; // Low Latency (как в piHPSDR)
|
T.FilterMP := 1; // Low Latency
|
||||||
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
T.FilterWindow := 1; // BH4 / Blackman-Harris
|
||||||
// Compressor — выкл по умолчанию
|
// Compressor — выкл по умолчанию
|
||||||
T.CompressorOn := False;
|
T.CompressorOn := False;
|
||||||
T.CompressorGain:= 5.0;
|
T.CompressorGain:= 5.0;
|
||||||
// Leveler — вкл, типичные значения piHPSDR/Thetis
|
// Leveler включён с типичными начальными значениями.
|
||||||
T.LevelerOn := True;
|
T.LevelerOn := True;
|
||||||
T.LevelerTop := 5.0;
|
T.LevelerTop := 5.0;
|
||||||
T.LevelerDecay := 500;
|
T.LevelerDecay := 500;
|
||||||
@@ -1767,7 +1763,7 @@ begin
|
|||||||
T.TXSpecRefLevel := 0.0;
|
T.TXSpecRefLevel := 0.0;
|
||||||
T.TXSpecRange := 80.0;
|
T.TXSpecRange := 80.0;
|
||||||
T.TXSpecGridStep := 10.0;
|
T.TXSpecGridStep := 10.0;
|
||||||
// PureSignal — дефолты Thetis PSForm (протокол 2)
|
// PureSignal — значения по умолчанию для протокола 2.
|
||||||
T.PSMoxDelay := 0.2;
|
T.PSMoxDelay := 0.2;
|
||||||
T.PSLoopDelay := 0.0;
|
T.PSLoopDelay := 0.0;
|
||||||
T.PSTXDelayNs := 150;
|
T.PSTXDelayNs := 150;
|
||||||
@@ -1868,7 +1864,7 @@ end;
|
|||||||
class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
|
class procedure TSettingsManager.DefaultCW(out C: TCWSettings);
|
||||||
begin
|
begin
|
||||||
FillChar(C, SizeOf(C), 0);
|
FillChar(C, SizeOf(C), 0);
|
||||||
C.Pitch := 600; // Thetis cw_pitch default
|
C.Pitch := 600; // default CW pitch
|
||||||
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
|
C.FWKeyer := True; // тайминги в FPGA — джиттер PC не влияет
|
||||||
C.KeyerMode := 2; // Iambic B
|
C.KeyerMode := 2; // Iambic B
|
||||||
C.ReverseKeys := False;
|
C.ReverseKeys := False;
|
||||||
@@ -1877,7 +1873,7 @@ begin
|
|||||||
C.Weight := 50;
|
C.Weight := 50;
|
||||||
C.BreakIn := True;
|
C.BreakIn := True;
|
||||||
C.HangTimeMS := 300;
|
C.HangTimeMS := 300;
|
||||||
C.RampMS := 9; // Thetis SetCWEdgeLength(9)
|
C.RampMS := 9; // default CW edge length
|
||||||
C.RFDelayMS := 0;
|
C.RFDelayMS := 0;
|
||||||
C.SidetoneHW := True;
|
C.SidetoneHW := True;
|
||||||
C.SidetoneHWLevel := 50;
|
C.SidetoneHWLevel := 50;
|
||||||
|
|||||||
+10
-11
@@ -1940,7 +1940,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
|
// BuildTransmitTab — все настройки TX-тракта.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
procedure TSettingsForm.BuildTransmitTab;
|
procedure TSettingsForm.BuildTransmitTab;
|
||||||
@@ -3283,7 +3283,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.BuildCalibrationTab;
|
procedure TSettingsForm.BuildCalibrationTab;
|
||||||
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
|
// Вкладка Calibration: модель детектора мощности
|
||||||
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
|
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
|
||||||
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
|
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
|
||||||
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
|
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
|
||||||
@@ -3428,7 +3428,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ── Spectrum / waterfall ────────────────────────────────────────────────
|
// ── Spectrum / waterfall ────────────────────────────────────────────────
|
||||||
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display-
|
// Отдельная от S-метра калибровка Display Cal Offset: у display-
|
||||||
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
|
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
|
||||||
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
|
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
|
||||||
// сигналы) — это калибровка уровня, а не подавление шума.
|
// сигналы) — это калибровка уровня, а не подавление шума.
|
||||||
@@ -3477,7 +3477,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ── Reference frequency ─────────────────────────────────────────────────
|
// ── Reference frequency ─────────────────────────────────────────────────
|
||||||
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis):
|
// Одна поправка опорника на всё железо:
|
||||||
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
|
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
|
||||||
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
|
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
|
||||||
// рознь, и где-то уход больше, где-то меньше.
|
// рознь, и где-то уход больше, где-то меньше.
|
||||||
@@ -3691,7 +3691,7 @@ procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
if FLoading then Exit;
|
if FLoading then Exit;
|
||||||
// Смена модели детектора подставляет модельные дефолты датчика тока
|
// Смена модели детектора подставляет модельные дефолты датчика тока
|
||||||
// (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88,
|
// ANAN-7000DLE/Anvelina Pro 3 — 340/88,
|
||||||
// остальные — 360/120. Правится руками после, если датчик нестандартный.
|
// остальные — 360/120. Правится руками после, если датчик нестандартный.
|
||||||
if Sender = FCmbPwrDetModel then
|
if Sender = FCmbPwrDetModel then
|
||||||
begin
|
begin
|
||||||
@@ -5117,7 +5117,7 @@ begin
|
|||||||
FCATTcpPort := Spin;
|
FCATTcpPort := Spin;
|
||||||
|
|
||||||
// ── TCI Server ────────────────────────────────────────────────────────────
|
// ── TCI Server ────────────────────────────────────────────────────────────
|
||||||
// Протокол Expert Electronics поверх WebSocket: логгеры, скиммеры, цифра.
|
// TCI поверх WebSocket: логгеры, скиммеры, цифровые виды связи.
|
||||||
Grp := MakeGroupPanel(FPageCAT, 'TCI Server',
|
Grp := MakeGroupPanel(FPageCAT, 'TCI Server',
|
||||||
MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H);
|
MARGIN + GRP_W + GRP_GAP, gy, GRP_W, NET_GRP_H);
|
||||||
Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT;
|
Grp.Tag := TAG_RESPONSIVE_HALF_RIGHT;
|
||||||
@@ -5317,8 +5317,8 @@ begin
|
|||||||
Spin.OnChange := OnSliceCATCtrlChange;
|
Spin.OnChange := OnSliceCATCtrlChange;
|
||||||
FSliceCATPort[i] := Spin;
|
FSliceCATPort[i] := Spin;
|
||||||
|
|
||||||
// Auto TX = «Auto Switch TX Slice» из SmartSDR CAT: команда TX по этому
|
// Auto TX: команда TX по этому порту сама делает слайс источником
|
||||||
// порту сама делает слайс источником передачи. Пока кто-то уже в эфире,
|
// передачи. Пока кто-то уже в эфире,
|
||||||
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
|
// заявка игнорируется — передаёт всегда только один слайс. Подпись короткая
|
||||||
// (в ширину группы), пояснение — строкой ниже.
|
// (в ширину группы), пояснение — строкой ниже.
|
||||||
Chk := TFlatCheckBox.Create(Self);
|
Chk := TFlatCheckBox.Create(Self);
|
||||||
@@ -6239,9 +6239,8 @@ end;
|
|||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
|
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
|
||||||
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/
|
// Реализован сокращённый набор: без PA-override/hot-switching внешнего PA
|
||||||
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/
|
// и без раздельных пинов VFO A/B. Только openHPSDR —
|
||||||
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
|
|
||||||
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
|
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
|
||||||
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
|
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
+4
-4
@@ -114,7 +114,7 @@ type
|
|||||||
FTXOverlay: Boolean;
|
FTXOverlay: Boolean;
|
||||||
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
// Знаковые кромки фильтров в Гц от несущей. RX — то, что реально стоит в
|
||||||
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
// приёмнике (фильтр может быть произвольным); TX — ширина передачи, она у
|
||||||
// Thetis-модели НЕ следует за RX, поэтому при передаче полоса рисуется по
|
// TX-шкала НЕ следует за RX, поэтому при передаче полоса рисуется по
|
||||||
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
// ней, а не по приёмной. 0/0 = не задано, работает прежний фолбэк.
|
||||||
FRXEdgeLo, FRXEdgeHi: Integer;
|
FRXEdgeLo, FRXEdgeHi: Integer;
|
||||||
FTXEdgeLo, FTXEdgeHi: Integer;
|
FTXEdgeLo, FTXEdgeHi: Integer;
|
||||||
@@ -813,7 +813,7 @@ begin
|
|||||||
M[i].LevelDB := PeakDB(M[i].FreqHz);
|
M[i].LevelDB := PeakDB(M[i].FreqHz);
|
||||||
|
|
||||||
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
|
ToneAvg := (M[0].LevelDB + M[1].LevelDB) * 0.5;
|
||||||
// Все маркеры — абсолютный уровень шкалы (dBm, как в Thetis); относительная
|
// Все маркеры — абсолютный уровень шкалы в dBm; относительная
|
||||||
// величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы.
|
// величина в dBc есть в сводке FIMDSummary, чтобы не путать единицы.
|
||||||
for i := 0 to 3 do
|
for i := 0 to 3 do
|
||||||
M[i].Caption := Format('%.0f', [M[i].LevelDB]);
|
M[i].Caption := Format('%.0f', [M[i].LevelDB]);
|
||||||
@@ -1398,7 +1398,7 @@ end;
|
|||||||
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||||
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
|
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
|
||||||
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
|
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
|
||||||
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая
|
// два умножения на dst-лейны (нормализация /256, кривая
|
||||||
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
|
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
|
||||||
var
|
var
|
||||||
Row, Col, Alpha, YMin, A256, I256: Integer;
|
Row, Col, Alpha, YMin, A256, I256: Integer;
|
||||||
@@ -1701,7 +1701,7 @@ begin
|
|||||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||||
|
|
||||||
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
// Полоса фильтра (X-координаты главного и TX-VFO). На передаче полоса
|
||||||
// считается по TX-фильтру: его ширина независима от приёмной (модель Thetis),
|
// считается по TX-фильтру: его ширина независима от приёмной,
|
||||||
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
// и раньше в эфир шло одно, а на экране рисовалось другое.
|
||||||
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
|
if FTXOverlay and (FTXVfoIndex = FActiveVfo) then
|
||||||
begin
|
begin
|
||||||
|
|||||||
+25
-27
@@ -71,7 +71,7 @@ const
|
|||||||
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
|
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
|
||||||
// и «второй приёмник» там существует только как слайс главного пана — при
|
// и «второй приёмник» там существует только как слайс главного пана — при
|
||||||
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
|
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
|
||||||
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1.
|
// номеров: некоторые клиенты позволяют выбрать только приёмники 0 и 1.
|
||||||
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
|
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
|
||||||
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
|
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
|
||||||
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
|
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
|
||||||
@@ -114,9 +114,9 @@ const
|
|||||||
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
|
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
|
||||||
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
|
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
|
||||||
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
|
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
|
||||||
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: MSHV пишет свой
|
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: клиент может писать
|
||||||
// TX-буфер granule'ами STREAM_C = 4096 отсчётов при 96 кГц (network.cpp), то
|
// TX-буфер гранулами по 4096 отсчётов при 96 кГц, то есть 42.7 мс, и отвечать
|
||||||
// есть 42.7 мс, и отвечает на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
|
// на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
|
||||||
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
|
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
|
||||||
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
|
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
|
||||||
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
|
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
|
||||||
@@ -125,7 +125,7 @@ const
|
|||||||
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
|
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
|
||||||
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
|
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
|
||||||
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после
|
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после
|
||||||
// подключения иначе стартует с pre-roll 31.75 мс против первого ответа MSHV
|
// подключения иначе стартует с pre-roll 31.75 мс до первого ответа клиента
|
||||||
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
|
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
|
||||||
// успевает: аванс появляется только после первого ответа.
|
// успевает: аванс появляется только после первого ответа.
|
||||||
TCI_TX_LEAD_DEF_MS = 50;
|
TCI_TX_LEAD_DEF_MS = 50;
|
||||||
@@ -254,7 +254,7 @@ type
|
|||||||
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
|
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
|
||||||
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
|
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
|
||||||
// подрос — попросили ещё больше. Это положительная обратная связь. Период
|
// подрос — попросили ещё больше. Это положительная обратная связь. Период
|
||||||
// же равен внутренней грануле клиента (у MSHV — STREAM_C/96 кГц = 42.7 мс)
|
// же равен внутренней грануле клиента (например, 4096/96 кГц = 42.7 мс)
|
||||||
// и от нашего темпа не зависит вовсе.
|
// и от нашего темпа не зависит вовсе.
|
||||||
FTxGapUs: Double; // оценка периода между пачками, мкс
|
FTxGapUs: Double; // оценка периода между пачками, мкс
|
||||||
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
|
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
|
||||||
@@ -857,8 +857,8 @@ begin
|
|||||||
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
FServer.Broadcast(TCIBuild('rx_channel_enable',
|
||||||
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
|
||||||
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
|
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
|
||||||
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
|
// входящие по arg1 (см. StrTrx), а некоторые также связывают с этим флагом
|
||||||
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
|
// разрешение передачи. Слайс, созданный
|
||||||
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
|
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
|
||||||
FServer.Broadcast(StrTxEnable(Rx));
|
FServer.Broadcast(StrTxEnable(Rx));
|
||||||
end;
|
end;
|
||||||
@@ -1195,8 +1195,8 @@ end;
|
|||||||
|
|
||||||
function TTCIAdapter.StrTrx(Rx: Integer): string;
|
function TTCIAdapter.StrTrx(Rx: Integer): string;
|
||||||
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
|
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
|
||||||
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
|
// его слайс. Клиенты могут фильтровать входящие строки по номеру приёмника,
|
||||||
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
|
// поэтому автору команды
|
||||||
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
|
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
|
||||||
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
|
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
|
||||||
// слайс сейчас источник передачи (`TxRx`).
|
// слайс сейчас источник передачи (`TxRx`).
|
||||||
@@ -1309,8 +1309,7 @@ end;
|
|||||||
procedure TTCIAdapter.BroadcastTxEnable;
|
procedure TTCIAdapter.BroadcastTxEnable;
|
||||||
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
|
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
|
||||||
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
|
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
|
||||||
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача
|
// приёмнике 1 может её отбросить и не разрешить передачу.
|
||||||
// (`set_ptt`: `if (!tci_tx_enable) return;`).
|
|
||||||
var Rx: Integer;
|
var Rx: Integer;
|
||||||
begin
|
begin
|
||||||
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
|
||||||
@@ -1830,8 +1829,7 @@ begin
|
|||||||
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
|
||||||
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
|
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
|
||||||
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
|
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
|
||||||
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
|
// настоящую частоту и завершить инициализацию несуществующего приёмника.
|
||||||
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
|
|
||||||
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
|
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
|
||||||
|
|
||||||
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
|
||||||
@@ -2157,7 +2155,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
|
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
|
||||||
// берётся из аудиопотока этого клиента. Остальные значения
|
// берётся из аудиопотока этого клиента. Остальные значения
|
||||||
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
|
// (mic1/mic2/micpc/ecoder2) называют физические входы TCI, которых
|
||||||
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
|
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
|
||||||
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
|
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
|
||||||
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
|
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
|
||||||
@@ -2196,8 +2194,8 @@ begin
|
|||||||
// ничего не изменила, всё равно её перебивала: клиент, уже
|
// ничего не изменила, всё равно её перебивала: клиент, уже
|
||||||
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
|
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
|
||||||
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
|
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
|
||||||
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает
|
// с этого мига уходят под номером 0. Клиент второго приёмника может
|
||||||
// (network.cpp:231), то есть передача просто замолкает.
|
// отбросить такие блоки, и передача просто замолкнет.
|
||||||
if B then
|
if B then
|
||||||
begin
|
begin
|
||||||
if Started and FromTCI then
|
if Started and FromTCI then
|
||||||
@@ -2236,7 +2234,7 @@ begin
|
|||||||
FTxRx := 0;
|
FTxRx := 0;
|
||||||
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
|
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
|
||||||
// идущей передачи. Раньше эти две строки расходились, и штатный
|
// идущей передачи. Раньше эти две строки расходились, и штатный
|
||||||
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым.
|
// конец посылки (`trx:N,false`) оставлял FTxRunning поднятым.
|
||||||
FTxRunning := False;
|
FTxRunning := False;
|
||||||
end;
|
end;
|
||||||
finally FTxLock.Leave; end;
|
finally FTxLock.Leave; end;
|
||||||
@@ -2789,7 +2787,7 @@ begin
|
|||||||
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
|
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
|
||||||
begin
|
begin
|
||||||
Client.AudioRate := V;
|
Client.AudioRate := V;
|
||||||
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и
|
// Число сэмплов в блоке у TCI своё на каждую частоту (§4.3), и
|
||||||
// клиент вправе на это рассчитывать, пока не задал своё явно.
|
// клиент вправе на это рассчитывать, пока не задал своё явно.
|
||||||
Client.AudioSamples := TCIDefaultAudioSamples(V);
|
Client.AudioSamples := TCIDefaultAudioSamples(V);
|
||||||
RestartStreams(Client, tstRXAudio);
|
RestartStreams(Client, tstRXAudio);
|
||||||
@@ -3740,7 +3738,8 @@ end;
|
|||||||
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
|
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
|
||||||
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
|
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
|
||||||
//
|
//
|
||||||
// ★Зачем: MSHV отвечает на первый маркер только через ~41 мс, а сам маркер
|
// ★Зачем: некоторые клиенты отвечают на первый маркер только через ~41 мс,
|
||||||
|
// а сам маркер
|
||||||
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
|
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
|
||||||
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
|
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
|
||||||
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
|
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
|
||||||
@@ -4001,9 +4000,8 @@ begin
|
|||||||
(Sent < TCI_TX_MARKERS_PER_TICK) do
|
(Sent < TCI_TX_MARKERS_PER_TICK) do
|
||||||
begin
|
begin
|
||||||
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
|
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
|
||||||
// Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV,
|
// Клиент может фильтровать ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver, а
|
||||||
// network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а
|
// TX-аудио отправлять только в ответ на этот маркер.
|
||||||
// TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono).
|
|
||||||
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
|
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
|
||||||
// маркеры до него не доходили вовсе.
|
// маркеры до него не доходили вовсе.
|
||||||
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
|
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
|
||||||
@@ -4069,8 +4067,8 @@ begin
|
|||||||
// ровно то, сколько их и распаковалось. Верим меньшему из двух: клиент
|
// ровно то, сколько их и распаковалось. Верим меньшему из двух: клиент
|
||||||
// вправе прислать короткий хвост, но не длиннее уместившегося. Заведомо
|
// вправе прислать короткий хвост, но не длиннее уместившегося. Заведомо
|
||||||
// чужое число (мусор в заголовке) просто игнорируем — длину нам и так
|
// чужое число (мусор в заголовке) просто игнорируем — длину нам и так
|
||||||
// ограничил размер кадра. Так же считает и MSHV, когда сам заполняет
|
// ограничил размер кадра. Значение заголовка задаётся числом распакованных
|
||||||
// заголовок: `t_txStream->length = (cr3/bit_s)`, где cr3 — байты блока.
|
// отсчётов, а не числом байт блока.
|
||||||
if H.DataLength > 0 then
|
if H.DataLength > 0 then
|
||||||
begin
|
begin
|
||||||
Want := Integer(H.DataLength);
|
Want := Integer(H.DataLength);
|
||||||
@@ -4103,8 +4101,8 @@ begin
|
|||||||
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
|
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
|
||||||
// просили, и зажим в ноль заставил бы переспросить уже полученное.
|
// просили, и зажим в ноль заставил бы переспросить уже полученное.
|
||||||
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
|
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
|
||||||
// отсчётов: первые ответы MSHV — законные нули (network.cpp, ветка
|
// отсчётов: первые ответы клиента могут быть законными нулями, и сторож не
|
||||||
// _reset_sta_ <= 5), и сторож не должен считать их отсутствием прогресса.
|
// должен считать их отсутствием прогресса.
|
||||||
NowUs := MonotonicUs;
|
NowUs := MonotonicUs;
|
||||||
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
|
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
|
||||||
// очередями по нескольку блоков — по своей внутренней зернистости записи.
|
// очередями по нескольку блоков — по своей внутренней зернистости записи.
|
||||||
|
|||||||
+11
-18
@@ -20,7 +20,7 @@
|
|||||||
unit TCIProtocol;
|
unit TCIProtocol;
|
||||||
|
|
||||||
{
|
{
|
||||||
TCIProtocol.pas — протокол TCI 2.0 (Expert Electronics), чистый слой.
|
TCIProtocol.pas — протокол TCI 2.0, чистый слой.
|
||||||
|
|
||||||
Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера.
|
Только разбор/сборка строк и словари протокола: ни сокетов, ни контроллера.
|
||||||
Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и
|
Роль та же, что у CATEngine в CAT-подсистеме, но парсер тут тривиальный, и
|
||||||
@@ -34,7 +34,7 @@ unit TCIProtocol;
|
|||||||
Зарезервированные символы: ':' ',' ';' — внутри аргументов запрещены и
|
Зарезервированные символы: ':' ',' ';' — внутри аргументов запрещены и
|
||||||
заменяются на '^' '~' '*' (§3.2.1), обратная подстановка — TCIUnescape.
|
заменяются на '^' '~' '*' (§3.2.1), обратная подстановка — TCIUnescape.
|
||||||
|
|
||||||
Регистр значения не имеет. ExpertSDR3 шлёт всё в нижнем регистре, и часть
|
Регистр значения не имеет. TCI шлёт всё в нижнем регистре, и часть
|
||||||
клиентов сравнивает строки как есть — поэтому наружу тоже пишем строчными,
|
клиентов сравнивает строки как есть — поэтому наружу тоже пишем строчными,
|
||||||
а внутрь принимаем любой.
|
а внутрь принимаем любой.
|
||||||
}
|
}
|
||||||
@@ -52,17 +52,17 @@ uses
|
|||||||
const
|
const
|
||||||
TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL
|
TCI_VERSION = '2.0'; // версия протокола, отдаётся в PROTOCOL
|
||||||
TCI_APP_NAME = 'EWSDR';
|
TCI_APP_NAME = 'EWSDR';
|
||||||
TCI_DEFAULT_PORT = 40001; // порт TCI-сервера ExpertSDR3
|
TCI_DEFAULT_PORT = 40001; // стандартный порт TCI-сервера
|
||||||
TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B)
|
TCI_CHANNELS = 2; // каналов приёма на приёмник (A/B)
|
||||||
|
|
||||||
// Список видов связи для MODULATIONS_LIST. Первые десять — словарь
|
// Список видов связи для MODULATIONS_LIST. Первые десять — стандартные
|
||||||
// ExpertSDR3 (клиенты сверяются именно с ними), dmr/fmraw — наше
|
// режимы TCI; dmr/fmraw — расширения EWSDR. Протокол допускает расширение
|
||||||
// расширение: протокол расширяемый по замыслу (§1.4).
|
// этого списка (§1.4).
|
||||||
TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw';
|
TCI_MODULATIONS = 'am,sam,dsb,lsb,usb,cw,nfm,wfm,digl,digu,dmr,fmraw';
|
||||||
|
|
||||||
// Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно).
|
// Границы, оговорённые протоколом (клампим сами — клиент шлёт что угодно).
|
||||||
// Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока,
|
// Бинарные потоки (§3.4). data[16384] в структуре Stream — это ПОТОЛОК блока,
|
||||||
// а не его размер: больше в один блок не кладут ни ExpertSDR3, ни клиенты.
|
// а не его размер: больше в один блок не кладут ни TCI, ни клиенты.
|
||||||
TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке
|
TCI_STREAM_DATA_MAX = 16384; // байт данных в блоке
|
||||||
TCI_STREAM_HDR_SIZE = 64; // 16 × uint32
|
TCI_STREAM_HDR_SIZE = 64; // 16 × uint32
|
||||||
TCI_STREAM_MAX = TCI_STREAM_HDR_SIZE + TCI_STREAM_DATA_MAX;
|
TCI_STREAM_MAX = TCI_STREAM_HDR_SIZE + TCI_STREAM_DATA_MAX;
|
||||||
@@ -164,7 +164,7 @@ function TCISampleTypeByName(const S: string; out T: TTCISampleType): Boolean;
|
|||||||
function TCISampleTypeName(T: TTCISampleType): string;
|
function TCISampleTypeName(T: TTCISampleType): string;
|
||||||
function TCISampleBytes(T: TTCISampleType): Integer;
|
function TCISampleBytes(T: TTCISampleType): Integer;
|
||||||
|
|
||||||
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у ExpertSDR3
|
{ Сколько сэмплов НА КАНАЛ класть в блок по умолчанию (§4.3): у TCI
|
||||||
своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. }
|
своё число на каждую частоту дискретизации, и все они дают ~42 мс звучания. }
|
||||||
function TCIDefaultAudioSamples(RateHz: Integer): Integer;
|
function TCIDefaultAudioSamples(RateHz: Integer): Integer;
|
||||||
|
|
||||||
@@ -178,22 +178,15 @@ function TCIMaxBlockSamples(T: TTCISampleType; Channels: Integer): Integer;
|
|||||||
Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как
|
Для IQ так прямо написано в §3.4 («количество комплексных вычисляется как
|
||||||
Stream.length/Stream.channels»), а для аудио формулировка §4.3 про
|
Stream.length/Stream.channels»), а для аудио формулировка §4.3 про
|
||||||
AUDIO_STREAM_SAMPLES («arg1 — количество сэмплов, указываемое в поле
|
AUDIO_STREAM_SAMPLES («arg1 — количество сэмплов, указываемое в поле
|
||||||
Stream.length») однажды уже увела нас в «сэмплы на канал». ★Так делать
|
Stream.length») может быть прочитана как «сэмплы на канал». ★Так делать
|
||||||
нельзя, и вот доказательство из живого клиента — MSHV, network.cpp:
|
нельзя: клиент вычисляет по length число байт блока. Стоит объявить length «на
|
||||||
|
|
||||||
int cr2 = (int)pStream->length*bit_s; // сколько БАЙТ разбирать
|
|
||||||
for (int i = 0; i < cr2; i+=chan*bit_s) // шаг = кадр всех каналов
|
|
||||||
...
|
|
||||||
quint32 cr3 = pStream->length*bit_s; //plength = plength*channels*4-bytes
|
|
||||||
|
|
||||||
То есть клиент считает по length число байт блока. Стоит объявить length «на
|
|
||||||
канал» — и у стерео он разбирает ровно половину блока, а вторую выбрасывает:
|
канал» — и у стерео он разбирает ровно половину блока, а вторую выбрасывает:
|
||||||
звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не
|
звук идёт с дырами в 50%, водопад становится шире и грязнее, FT4/FT8 не
|
||||||
декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора
|
декодируются вовсе. Замерено стендом test/tci/ft4_bench.py: 50% темпа набора
|
||||||
звука и 1649 не до конца разобранных блоков за минуту.
|
звука и 1649 не до конца разобранных блоков за минуту.
|
||||||
|
|
||||||
Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES — сэмплы НА
|
Размер блока при этом остаётся прежним: AUDIO_STREAM_SAMPLES — сэмплы НА
|
||||||
КАНАЛ (2048 при 48 кГц = 42.7 мс, как у ExpertSDR3), и потолок data[16384]
|
КАНАЛ (2048 при 48 кГц = 42.7 мс), и потолок data[16384]
|
||||||
сходится ровно: 2048 × 2 канала × float32. }
|
сходится ровно: 2048 × 2 канала × float32. }
|
||||||
procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType;
|
procedure TCIFillHeader(out H: TTCIStreamHeader; Kind: TTCIStreamType;
|
||||||
Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer);
|
Rx, RateHz: Integer; T: TTCISampleType; Count, Channels: Integer);
|
||||||
|
|||||||
+2
-2
@@ -20,8 +20,8 @@
|
|||||||
unit TCIServer;
|
unit TCIServer;
|
||||||
|
|
||||||
{
|
{
|
||||||
TCIServer.pas — транспорт TCI: WebSocket-сервер (роль сервера играем мы,
|
TCIServer.pas — транспорт TCI: WebSocket-сервер для логгеров, скиммеров
|
||||||
как ExpertSDR3; клиенты — логгеры, скиммеры, программы цифровых видов).
|
и программ цифровых видов связи.
|
||||||
|
|
||||||
Что делает:
|
Что делает:
|
||||||
• слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket
|
• слушает TCP-порт (умолчание 40001), принимает HTTP-Upgrade на WebSocket
|
||||||
|
|||||||
+3
-3
@@ -222,7 +222,7 @@ type
|
|||||||
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
|
То есть START открывает окно длиной MaxSec, и всё, что не сохранили внутри
|
||||||
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
|
него, пропадает. Кольцо «последние N секунд» вело себя иначе в обе стороны:
|
||||||
начало активной записи затирало само себя, а SAVE через час после START
|
начало активной записи затирало само себя, а SAVE через час после START
|
||||||
отдавал файл, которого у ExpertSDR3 давно бы не было.
|
отдавал бы файл уже после завершения заданного окна записи.
|
||||||
|
|
||||||
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
|
Срок считаем по ЧАСАМ, а не по накопленным сэмплам: линейный выход молчит
|
||||||
(мьют, пауза приёмника), а время записи всё равно идёт. }
|
(мьют, пауза приёмника), а время записи всё равно идёт. }
|
||||||
@@ -682,8 +682,8 @@ begin
|
|||||||
F := TCIDecimFactor(ASrcRate, AWantRate);
|
F := TCIDecimFactor(ASrcRate, AWantRate);
|
||||||
FOutRate := ASrcRate div F;
|
FOutRate := ASrcRate div F;
|
||||||
|
|
||||||
// Блок не имеет права вылезти за data[16384]: столько ExpertSDR3 объявил
|
// Блок не имеет права вылезти за data[16384]: это установленный протоколом
|
||||||
// потолком, и клиенты держат приёмный буфер ровно под него.
|
// предел, под который клиенты выделяют приёмный буфер.
|
||||||
MaxB := TCIMaxBlockSamples(FSampleT, FChannels);
|
MaxB := TCIMaxBlockSamples(FSampleT, FChannels);
|
||||||
FBlock := EnsureRange(ABlock, 1, MaxB);
|
FBlock := EnsureRange(ABlock, 1, MaxB);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ unit VfoOverlay;
|
|||||||
{$mode objfpc}{$H+}
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
{
|
{
|
||||||
TVfoOverlay — накладка-«флаг слайса» поверх спектра, стиль SmartSDR.
|
TVfoOverlay — накладка-«флаг слайса» поверх спектра.
|
||||||
Компоновка (сверху вниз):
|
Компоновка (сверху вниз):
|
||||||
шапка : [A] 2.7k SPLIT [TX]
|
шапка : [A] 2.7k SPLIT [TX]
|
||||||
метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40
|
метр : NR NB ANF SNB ▂▃▅▇█▆▄ -20 +20 +40
|
||||||
|
|||||||
+40
-44
@@ -196,7 +196,7 @@ const
|
|||||||
TX_MIC_RING = 8192;
|
TX_MIC_RING = 8192;
|
||||||
|
|
||||||
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
|
// TX/DUC sample rate. Фиксирован независимо от FSampleRate (RX).
|
||||||
// Thetis/piHPSDR держат TX rate постоянным (192k для Hermes/Saturn DUC0),
|
// TX rate постоянен: 192k для Hermes/Saturn DUC0,
|
||||||
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
|
// меняя только RX rate. Это гарантирует, что DUC IQ всегда подаётся в том
|
||||||
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
|
// темпе, который ожидает железо (DUC0RateHi/Lo в DUC Specific = 192).
|
||||||
TX_SAMPLE_RATE = 192000;
|
TX_SAMPLE_RATE = 192000;
|
||||||
@@ -323,7 +323,7 @@ type
|
|||||||
|
|
||||||
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
|
// Копия входных IQ для Spectrum0 (до fexchange0, который данные in-place перезаписывает)
|
||||||
FSpecBuf: array of Double;
|
FSpecBuf: array of Double;
|
||||||
// Отдельный мелкий feed для display-анализатора, как в Thetis spec_blocksize.
|
// Отдельный небольшой feed для display-анализатора.
|
||||||
FDispBuf: array of Double;
|
FDispBuf: array of Double;
|
||||||
FDispPos: Integer;
|
FDispPos: Integer;
|
||||||
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
// То же самое для TX-analyzer: TX_DISP_ID получает сэмплы чанками
|
||||||
@@ -372,7 +372,7 @@ type
|
|||||||
FWfAvgMode: Integer;
|
FWfAvgMode: Integer;
|
||||||
FWfAvgTimeMS: Double;
|
FWfAvgTimeMS: Double;
|
||||||
|
|
||||||
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
|
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP:
|
||||||
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
|
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
|
||||||
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
|
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
|
||||||
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
|
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
|
||||||
@@ -531,7 +531,7 @@ type
|
|||||||
FFilterLow: Integer;
|
FFilterLow: Integer;
|
||||||
FFilterHigh: Integer;
|
FFilterHigh: Integer;
|
||||||
FAGCMode: TWDSPAGCMode;
|
FAGCMode: TWDSPAGCMode;
|
||||||
FAGCTop: Double; // gain (dB) = agc_gain в piHPSDR
|
FAGCTop: Double; // AGC gain in dB
|
||||||
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
|
FAGCSlope: Integer; // наклон АРУ в дБ (default 0)
|
||||||
FAGCHangThreshold: Integer; // порог hang 0..100
|
FAGCHangThreshold: Integer; // порог hang 0..100
|
||||||
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
FAGCHangLevel: Double; // читается из WDSP (GetRXAAGCHangLevel)
|
||||||
@@ -580,7 +580,7 @@ type
|
|||||||
// главного RXA и для слайсов — иначе дублирование по трём местам.
|
// главного RXA и для слайсов — иначе дублирование по трём местам.
|
||||||
procedure ApplyRXModeSettings(Chan, Mode: Integer);
|
procedure ApplyRXModeSettings(Chan, Mode: Integer);
|
||||||
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
|
// Магнитуда TX-полосы (FTXFilter*) → знаковые края bp0 по боковой режима.
|
||||||
// Порт Thetis UpdateTXLowHighFilterForMode: единственный конвертер знака,
|
// Единственный конвертер знака краёв TX-фильтра для выбранного режима,
|
||||||
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
|
// через который обязан идти ЛЮБОЙ пуш TX-bandpass, иначе LSB уходит в USB.
|
||||||
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
procedure TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
||||||
// --- CW pitch -----------------------------------------------------------
|
// --- CW pitch -----------------------------------------------------------
|
||||||
@@ -783,7 +783,7 @@ type
|
|||||||
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
procedure SetTXTone(Enabled: Boolean; FreqHz, Mag: Double);
|
||||||
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
|
// 2TON: PostGen two-tone (IMD-тест / калибровка PureSignal).
|
||||||
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
|
// Тона Freq1/Freq2 Гц (для LSB/CWL/DIGL знак инвертируется), амплитуды
|
||||||
// по 0.49 (сумма чуть ниже полной шкалы, как Thetis/pihpsdr).
|
// по 0.49, чтобы сумма оставалась чуть ниже полной шкалы.
|
||||||
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
procedure SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
||||||
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
|
// Protocol 2: компенсирующий FIR перед FPGA CIC-интерполятором TX.
|
||||||
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
|
// Для openHPSDR P2 всегда включён; для остальных бэкендов выключен.
|
||||||
@@ -795,7 +795,7 @@ type
|
|||||||
procedure SetPureSignalArmed(Armed: Boolean);
|
procedure SetPureSignalArmed(Armed: Boolean);
|
||||||
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
|
// Feedback-пакет синхронизированного DDC0 (interleaved: пара DDC0 =
|
||||||
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
|
// RX-feedback, пара DDC1 = TX DAC, 24-bit BE). Вызывается из СЕТЕВОГО
|
||||||
// потока (как Thetis/pihpsdr — pscc потокобезопасен относительно
|
// потока; pscc потокобезопасен относительно
|
||||||
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
|
// fexchange0 TX-потока). TotalPairs — всего IQ-пар в пакете (оба потока).
|
||||||
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
|
procedure PushPSFeedback(const Buf: array of Byte; DataOffset: Integer;
|
||||||
TotalPairs: Integer);
|
TotalPairs: Integer);
|
||||||
@@ -958,7 +958,7 @@ implementation
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
|
// TX DSP поток — читает mic ring buffer, вызывает fexchange0(TXA_CHAN),
|
||||||
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
|
// сигнализирует о готовых IQ данных через FOnTXIQ callback.
|
||||||
// Аналог tx_thread в piHPSDR/transmitter.c
|
// Поток обработки TX-аудио и формирования IQ.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
type
|
type
|
||||||
TDisplayThread = class(TThread)
|
TDisplayThread = class(TThread)
|
||||||
@@ -981,7 +981,7 @@ type
|
|||||||
|
|
||||||
function UsePanNormOneHz(Detector: Integer): Integer;
|
function UsePanNormOneHz(Detector: Integer): Integer;
|
||||||
begin
|
begin
|
||||||
// В Thetis нормализация к 1 Гц включается только для panadapter
|
// Нормализация к 1 Гц включается только для panadapter
|
||||||
// и только для detector modes 2/3/4.
|
// и только для detector modes 2/3/4.
|
||||||
if Detector in [2, 3, 4] then
|
if Detector in [2, 3, 4] then
|
||||||
Result := 1
|
Result := 1
|
||||||
@@ -1029,8 +1029,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTXDSPThread.Execute;
|
procedure TTXDSPThread.Execute;
|
||||||
// Аналог tx_thread в piHPSDR/transmitter.c:
|
// Поток работает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
||||||
// Тикает с периодом одного mic-буфера (FAudioBufSize/FAudioRate).
|
|
||||||
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
// Если mic-данные накопились — обрабатывает их; если нет — обрабатывает тишину.
|
||||||
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
// Это обеспечивает непрерывный поток DUC IQ к железу и TX-сигнал на спектре/водопаде.
|
||||||
var
|
var
|
||||||
@@ -1052,7 +1051,7 @@ begin
|
|||||||
Dec(AccUs, PeriodMs * 1000);
|
Dec(AccUs, PeriodMs * 1000);
|
||||||
|
|
||||||
// Ждём сигнала от mic-данных ИЛИ истечения периода —
|
// Ждём сигнала от mic-данных ИЛИ истечения периода —
|
||||||
// в piHPSDR timing идёт от receive thread; здесь — от таймаута
|
// Здесь timing задаётся таймаутом, синхронизированным с размером буфера.
|
||||||
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
|
RTLEventWaitFor(FEngine.FTXMicSem, PeriodMs);
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
if not FEngine.FTXActive then Continue;
|
if not FEngine.FTXActive then Continue;
|
||||||
@@ -1085,7 +1084,7 @@ begin
|
|||||||
|
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
|
|
||||||
// Silence-инъекция УБРАНА (модель dl1ycf pihpsdr). Раньше при пустом ring
|
// Silence-инъекция УБРАНА. Раньше при пустом ring
|
||||||
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
|
// (>6 тиков) сюда впрыскивался блок тишины «чтобы DUC IQ не прерывался».
|
||||||
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
|
// Но это фабрикация данных не на радиоклоке: для TUN такой «silence» несёт
|
||||||
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
|
// PostGen-тон, и лишний блок ломает темп DUC IQ (боковые/тр-р-р); плюс он
|
||||||
@@ -1098,9 +1097,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// DSP поток — обрабатывает IQ пакеты из очереди
|
// DSP поток обрабатывает IQ-пакеты из очереди.
|
||||||
// Сетевой поток только кладёт пакеты, этот поток занимается DSP
|
// Сетевой поток только кладёт пакеты, этот поток занимается DSP.
|
||||||
// Аналог iq_thread в piHPSDR/new_protocol.c
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
type
|
type
|
||||||
TDSPThread = class(TThread)
|
TDSPThread = class(TThread)
|
||||||
@@ -1125,7 +1123,7 @@ var
|
|||||||
begin
|
begin
|
||||||
while not Terminated do
|
while not Terminated do
|
||||||
begin
|
begin
|
||||||
// Ждём сигнала от сетевого потока (как sem_wait в piHPSDR)
|
// Ждём сигнала от сетевого потока.
|
||||||
RTLEventWaitFor(FEngine.FQueueSem, 100);
|
RTLEventWaitFor(FEngine.FQueueSem, 100);
|
||||||
if Terminated then Break;
|
if Terminated then Break;
|
||||||
|
|
||||||
@@ -1339,7 +1337,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
procedure TWDSPEngine.TXSignedEdges(Mode: Integer; out Lo, Hi: Double);
|
||||||
// Порт Thetis UpdateTXLowHighFilterForMode. Вход — магнитуда TX-аудио-полосы
|
// Вход — магнитуда TX-аудио-полосы
|
||||||
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
|
// (FTXFilterLow/High, всегда положительная); выход — знаковые края bp0 под
|
||||||
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
|
// боковую режима. Нижняя боковая зеркалит полосу в отрицательные частоты.
|
||||||
var
|
var
|
||||||
@@ -1376,8 +1374,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
|
function TWDSPEngine.CWLOOffset(Mode: Integer): Double;
|
||||||
// Знак — как в Thetis (console.cs:31757 rx_freq += pitch для CWL) и pihpsdr
|
// Для CWL смещение положительное, для CWU — отрицательное.
|
||||||
// (new_protocol.c:773 rxFrequency -= pitch для CWU).
|
|
||||||
begin
|
begin
|
||||||
case Mode of
|
case Mode of
|
||||||
MODE_CWL: Result := FCWPitch;
|
MODE_CWL: Result := FCWPitch;
|
||||||
@@ -1396,7 +1393,7 @@ end;
|
|||||||
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
|
procedure TWDSPEngine.PushPassband(Chan, Lo, Hi, Mode, Rate: Integer);
|
||||||
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
|
// Кромки хранятся относительно VFO (у CW — симметрично нулю: «500 Гц вокруг
|
||||||
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
|
// корреспондента»). В baseband демодулятора они уезжают навстречу гетеродину,
|
||||||
// т.е. у CWU получается pitch±bw/2 — ровно таблица Thetis (console.cs:5338).
|
// т.е. у CWU получается pitch±bw/2.
|
||||||
var Off: Integer;
|
var Off: Integer;
|
||||||
begin
|
begin
|
||||||
Off := Round(CWLOOffset(Mode));
|
Off := Round(CWLOOffset(Mode));
|
||||||
@@ -1428,7 +1425,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
// Restore the user's normal voice/data chain after leaving FM RAW.
|
// Restore the user's normal voice/data chain after leaving FM RAW.
|
||||||
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
|
// bp1 (aux bandpass) НЕ трогаем вручную — им управляет компрессор через
|
||||||
// TXASetupBPFilters (как в Thetis). bp0 всегда активен сам.
|
// bp0 всегда активен сам; здесь обновляем остальную цепочку TX.
|
||||||
ApplyTXChainSettings;
|
ApplyTXChainSettings;
|
||||||
if Mode = MODE_WFM then ApplyTXWFMSettings;
|
if Mode = MODE_WFM then ApplyTXWFMSettings;
|
||||||
end;
|
end;
|
||||||
@@ -1478,9 +1475,9 @@ begin
|
|||||||
inherited Create;
|
inherited Create;
|
||||||
FSampleRate := SampleRate;
|
FSampleRate := SampleRate;
|
||||||
FAudioRate := AudioRate;
|
FAudioRate := AudioRate;
|
||||||
// BufSize = dsp_size @ AudioRate (внутренний DSP буфер, piHPSDR buffer_size = 1024)
|
// BufSize = dsp_size @ AudioRate; внутренний DSP buffer_size = 1024.
|
||||||
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
|
// FBufSize = in_size @ SampleRate = BufSize * (SampleRate/AudioRate)
|
||||||
// piHPSDR: in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz
|
// in_size = buffer_size * sample_rate/48000 = 1024*4 = 4096 @ 192kHz.
|
||||||
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
FAudioBufSize := BufSize; // dsp_size = out_size @ AudioRate
|
||||||
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
FBufSize := BufSize * SampleRate div AudioRate; // in_size @ SampleRate = 4096
|
||||||
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
||||||
@@ -1579,7 +1576,7 @@ begin
|
|||||||
FTXToneOn := False;
|
FTXToneOn := False;
|
||||||
FTXTTOn := False;
|
FTXTTOn := False;
|
||||||
FKeepRXDuringTX := False;
|
FKeepRXDuringTX := False;
|
||||||
// PureSignal — дефолты как Thetis PSForm / clsHardwareSpecific (P2)
|
// PureSignal — значения по умолчанию для протокола 2.
|
||||||
FPSArmed := False;
|
FPSArmed := False;
|
||||||
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
|
FPSMoxDelay := 0.2; // с — задержка после MOX до сбора данных
|
||||||
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
|
FPSLoopDelay := 0.0; // с — пауза между итерациями калибровки
|
||||||
@@ -2210,7 +2207,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
try
|
try
|
||||||
// Анализатор ПЕРВЫМ — до OpenChannel (порядок как в piHPSDR)
|
// Анализатор открывается перед DSP-каналом.
|
||||||
OpenAnalyzer;
|
OpenAnalyzer;
|
||||||
if not FAnalyzerOpen then
|
if not FAnalyzerOpen then
|
||||||
begin
|
begin
|
||||||
@@ -2237,7 +2234,7 @@ begin
|
|||||||
0,
|
0,
|
||||||
0.010, 0.010, 0.010, 0.010,
|
0.010, 0.010, 0.010, 0.010,
|
||||||
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
|
1 // bfo=1: fexchange0 БЛОКИРУЕТСЯ до готовности выходного блока (как
|
||||||
// pihpsdr). bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
|
// bfo=0 давал async-чтение кольца r2 со сдвигом outidx →
|
||||||
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
|
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2277,7 +2274,7 @@ begin
|
|||||||
Result := True;
|
Result := True;
|
||||||
|
|
||||||
// Запускаем DSP поток после успешной инициализации WDSP
|
// Запускаем DSP поток после успешной инициализации WDSP
|
||||||
// Аналог iq_thread_id = g_thread_new("iq thread", ...) в piHPSDR
|
// Запускаем отдельный поток обработки IQ.
|
||||||
FDSPThread := TDSPThread.Create(Self);
|
FDSPThread := TDSPThread.Create(Self);
|
||||||
FDSPThread.Priority := tpHighest; // DSP критический поток
|
FDSPThread.Priority := tpHighest; // DSP критический поток
|
||||||
|
|
||||||
@@ -2531,7 +2528,7 @@ begin
|
|||||||
Item^.PanId := 0;
|
Item^.PanId := 0;
|
||||||
|
|
||||||
FQueueHead := NextHead;
|
FQueueHead := NextHead;
|
||||||
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
|
RTLEventSetEvent(FQueueSem); // будим DSP-поток
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
|
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
|
||||||
@@ -2815,7 +2812,7 @@ begin
|
|||||||
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
FSMeter := GetRXAMeter(RXA_CHAN, RXA_S_AV);
|
||||||
|
|
||||||
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
// Аудио колбэк — FAudioBufSize сэмплов @ FAudioRate (после децимации 4:1).
|
||||||
// При TX RX-аудио глушим (как в Thetis/piHPSDR при не-duplex MOX),
|
// При TX RX-аудио глушим в режиме non-duplex MOX,
|
||||||
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
// иначе оператор слышит свой эфир через RX-цепь и получает feedback.
|
||||||
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
|
// Дополнительный гейт FPostTXMuteUntil закрывает «хвост» TX leakage
|
||||||
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
|
// на Windows (radio FPGA TX-buffer + PA slew-down). Cheap-path: проверка
|
||||||
@@ -2824,7 +2821,7 @@ begin
|
|||||||
FPostTXMuteUntil := 0;
|
FPostTXMuteUntil := 0;
|
||||||
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
||||||
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
||||||
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
// FPostTXMuteUntil-хвост уважаем и при non-duplex MOX.
|
||||||
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||||
and (FPostTXMuteUntil = 0) then
|
and (FPostTXMuteUntil = 0) then
|
||||||
begin
|
begin
|
||||||
@@ -3523,7 +3520,7 @@ begin
|
|||||||
FAGCMode := Mode;
|
FAGCMode := Mode;
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
|
|
||||||
// Точно по piHPSDR receiver.c: set_agc()
|
// Настройка параметров AGC приёмного канала.
|
||||||
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
|
ApplyAGCToChan(RXA_CHAN, Mode, FixedGain);
|
||||||
|
|
||||||
// Читаем обратно hang level и thresh для линии на спектре
|
// Читаем обратно hang level и thresh для линии на спектре
|
||||||
@@ -3976,7 +3973,7 @@ end;
|
|||||||
|
|
||||||
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
procedure TWDSPEngine.SetTXTwoTone(Enabled: Boolean; Freq1, Freq2: Double);
|
||||||
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
|
// 2TON: двухтональник PostGen (mode 1) для IMD-теста и проверки PureSignal.
|
||||||
// Амплитуды 0.49+0.49 — сумма чуть ниже полной шкалы (как Thetis/pihpsdr).
|
// Амплитуды 0.49+0.49 дают сумму чуть ниже полной шкалы.
|
||||||
var
|
var
|
||||||
F1, F2: Double;
|
F1, F2: Double;
|
||||||
begin
|
begin
|
||||||
@@ -4026,9 +4023,8 @@ end;
|
|||||||
procedure TWDSPEngine.ProcessTXBlock;
|
procedure TWDSPEngine.ProcessTXBlock;
|
||||||
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
// Вызывается из TTXDSPThread — обрабатывает один блок FAudioBufSize mic сэмплов
|
||||||
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
// через WDSP TXA, выдаёт FTXOutBufSize IQ пар @ FTXSampleRate в FOnTXIQ callback.
|
||||||
// Аналог tx_full_buffer() в piHPSDR/transmitter.c:
|
// Если mic ring buffer содержит достаточно данных — берём их,
|
||||||
// если mic ring buffer содержит достаточно данных — берём их,
|
// иначе обрабатываем тишину (нули).
|
||||||
// иначе обрабатываем тишину (нули), как piHPSDR при mic_sample=0.0.
|
|
||||||
var
|
var
|
||||||
i, tail: Integer;
|
i, tail: Integer;
|
||||||
Err: Integer;
|
Err: Integer;
|
||||||
@@ -4055,7 +4051,7 @@ begin
|
|||||||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||||||
end else
|
end else
|
||||||
begin
|
begin
|
||||||
// Нет mic-данных — тишина (аналог piHPSDR: tx->mic_input_buffer заполняется нулями)
|
// Нет mic-данных — заполняем входной TX-буфер нулями.
|
||||||
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
FillChar(FTXIn[0], FAudioBufSize * 2 * SizeOf(Double), 0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -4258,7 +4254,7 @@ end;
|
|||||||
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
|
procedure TWDSPEngine.PushPSFeedback(const Buf: array of Byte;
|
||||||
DataOffset: Integer; TotalPairs: Integer);
|
DataOffset: Integer; TotalPairs: Integer);
|
||||||
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... →
|
// Сетевой поток: interleaved 24-bit BE пары [DDC0 I,Q][DDC1 I,Q]... →
|
||||||
// деинтерливим и кормим WDSP pscc (как pihpsdr process_ps_iq_data):
|
// Деинтерливим feedback-потоки и передаём их в WDSP pscc:
|
||||||
// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
|
// DDC0 = RX-feedback (искажённый сигнал после PA), DDC1 = TX DAC (эталон).
|
||||||
var
|
var
|
||||||
i, n, Pos: Integer;
|
i, n, Pos: Integer;
|
||||||
@@ -4329,7 +4325,7 @@ procedure TWDSPEngine.TXFilterEdgesHz(out Lo, Hi: Integer);
|
|||||||
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
|
// WFM (20..15000), и полоса на спектре уезжала вверх от несущей — визуально
|
||||||
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
|
// «переключалась на USB». Для ЧМ считаем по Карсону: ±(девиация + верхний срез
|
||||||
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
|
// аудио), симметрично несущей. У MODE_FM TXSignedEdges отдаёт ровно эту
|
||||||
// величину и без нас (порт Thetis), но пусть формула будет в одном месте.
|
// величину и без нас, но формула остаётся в одном месте.
|
||||||
var L, H, Half: Double;
|
var L, H, Half: Double;
|
||||||
begin
|
begin
|
||||||
Half := 0.0;
|
Half := 0.0;
|
||||||
@@ -4423,7 +4419,7 @@ end;
|
|||||||
|
|
||||||
procedure TWDSPEngine.PushTXEQProfile;
|
procedure TWDSPEngine.PushTXEQProfile;
|
||||||
// Единственная точка, отдающая кривую EQ в WDSP.
|
// Единственная точка, отдающая кривую EQ в WDSP.
|
||||||
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
|
// F[1..n] — узлы ломаной АЧХ, между ними интерполяция по
|
||||||
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
|
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
|
||||||
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
|
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
|
||||||
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
|
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
|
||||||
@@ -4694,7 +4690,7 @@ end;
|
|||||||
|
|
||||||
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
function TWDSPEngine.NormalizeFFTSize(FFTSize: Integer): Integer;
|
||||||
begin
|
begin
|
||||||
// Thetis-style display FFT range.
|
// Диапазон display FFT.
|
||||||
if FFTSize < 4096 then FFTSize := 4096;
|
if FFTSize < 4096 then FFTSize := 4096;
|
||||||
Result := 4096;
|
Result := 4096;
|
||||||
while Result < FFTSize do
|
while Result < FFTSize do
|
||||||
@@ -4705,7 +4701,7 @@ end;
|
|||||||
|
|
||||||
function TWDSPEngine.CalcDisplayFFTSize: Integer;
|
function TWDSPEngine.CalcDisplayFFTSize: Integer;
|
||||||
begin
|
begin
|
||||||
// FFT Size теперь снова управляет display-analyzer как в Thetis,
|
// FFT Size управляет display-analyzer,
|
||||||
// но не влияет на аудио DSP цепочку.
|
// но не влияет на аудио DSP цепочку.
|
||||||
Result := FFFTSize;
|
Result := FFFTSize;
|
||||||
end;
|
end;
|
||||||
@@ -4716,7 +4712,7 @@ var
|
|||||||
TargetFPS: Double;
|
TargetFPS: Double;
|
||||||
begin
|
begin
|
||||||
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
|
TargetFPS := EnsureRange(FDisplayFPS, 5, 100);
|
||||||
// Thetis/piHPSDR-style timing:
|
// Тайминг обновления дисплея:
|
||||||
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
|
// целимся в пользовательский FPS и разрешаем большой overlap даже на крупных FFT.
|
||||||
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
|
Overlap := Max(0, Ceil(AnalyzerFFT - (FSampleRate / TargetFPS)));
|
||||||
EffectiveFPS := TargetFPS;
|
EffectiveFPS := TargetFPS;
|
||||||
@@ -4729,7 +4725,7 @@ begin
|
|||||||
|
|
||||||
if ForWaterfall then
|
if ForWaterfall then
|
||||||
begin
|
begin
|
||||||
// Peak hold на водопаде даёт грязную/залипающую картинку и не похож на Thetis.
|
// Peak hold на водопаде даёт грязную и залипающую картинку.
|
||||||
if AvgMode < 0 then AvgMode := 1;
|
if AvgMode < 0 then AvgMode := 1;
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|||||||
+2
-2
@@ -40,7 +40,7 @@ const
|
|||||||
WF_MAX_PIXELS = 4096;
|
WF_MAX_PIXELS = 4096;
|
||||||
|
|
||||||
// Палитры водопада (индексы совпадают с настройкой wf_palette)
|
// Палитры водопада (индексы совпадают с настройкой wf_palette)
|
||||||
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема)
|
WF_PAL_CLASSIC = 0; // классическая тёмная / пастельная светлая палитра
|
||||||
WF_PAL_INFERNO = 1;
|
WF_PAL_INFERNO = 1;
|
||||||
WF_PAL_TURBO = 2;
|
WF_PAL_TURBO = 2;
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ begin
|
|||||||
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1]
|
// Классическая тёмная палитра, T∈[0..1].
|
||||||
function ClassicDarkColor(T: Double): LongWord;
|
function ClassicDarkColor(T: Double): LongWord;
|
||||||
var
|
var
|
||||||
Local: Double;
|
Local: Double;
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ unit WebServer;
|
|||||||
{
|
{
|
||||||
WebServer.pas — HTTP + WebSocket сервер для удалённого управления трансивером.
|
WebServer.pas — HTTP + WebSocket сервер для удалённого управления трансивером.
|
||||||
|
|
||||||
Архитектура (по образцу OpenWebRX):
|
Архитектура веб-подсистемы:
|
||||||
─────────────────────────────────
|
─────────────────────────────────
|
||||||
HTTP GET / → index.html (см. WebPageHtml)
|
HTTP GET / → index.html (см. WebPageHtml)
|
||||||
HTTP GET /ws → Upgrade: WebSocket
|
HTTP GET /ws → Upgrade: WebSocket
|
||||||
|
|||||||
+4
-4
@@ -24,7 +24,7 @@ unit WidebandView;
|
|||||||
|
|
||||||
The network layer feeds 16-bit ADC samples collected from Protocol V4
|
The network layer feeds 16-bit ADC samples collected from Protocol V4
|
||||||
wideband packets. This view feeds them to a dedicated WDSP analyzer and
|
wideband packets. This view feeds them to a dedicated WDSP analyzer and
|
||||||
renders a compact Thetis-style wideband panadapter. CPU paint uses a bitmap;
|
renders a compact wideband panadapter. CPU paint uses a bitmap;
|
||||||
when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
|
when MainForm creates a TOpenGLControl the same spectrum is rendered as GL
|
||||||
primitives.
|
primitives.
|
||||||
}
|
}
|
||||||
@@ -308,16 +308,16 @@ begin
|
|||||||
if not FWDSPAnalyzerConfigured then
|
if not FWDSPAnalyzerConfigured then
|
||||||
begin
|
begin
|
||||||
SampleRate := Max(1, Round(FSampleRateHz));
|
SampleRate := Max(1, Round(FSampleRateHz));
|
||||||
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // Thetis wideband default.
|
AvBackmult := Exp(-1.0 / (15.0 * 0.120)); // default wideband averaging.
|
||||||
SetAnalyzer(
|
SetAnalyzer(
|
||||||
FWDSPAnalyzerID,
|
FWDSPAnalyzerID,
|
||||||
2,
|
2,
|
||||||
1,
|
1,
|
||||||
1, // Feed like piHPSDR: interleaved I/Q via Spectrum0.
|
1, // Interleaved I/Q feed via Spectrum0.
|
||||||
@FWDSPFlp[0],
|
@FWDSPFlp[0],
|
||||||
WB_WDSP_FFT,
|
WB_WDSP_FFT,
|
||||||
WB_WDSP_BLOCK,
|
WB_WDSP_BLOCK,
|
||||||
6, // Thetis wideband default window.
|
6, // default wideband window.
|
||||||
14.0,
|
14.0,
|
||||||
0, // no overlap for discontinuous WB packets.
|
0, // no overlap for discontinuous WB packets.
|
||||||
0,
|
0,
|
||||||
|
|||||||
+15
-18
@@ -4,8 +4,8 @@
|
|||||||
Дата последнего обновления: 2026-06-26.
|
Дата последнего обновления: 2026-06-26.
|
||||||
|
|
||||||
Документ описывает поддержку аппаратной передней панели Apache Labs
|
Документ описывает поддержку аппаратной передней панели Apache Labs
|
||||||
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD).
|
**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD)
|
||||||
За эталон протокола взят Thetis (`Andromeda/Andromeda.cs`, `CAT/CATCommands.cs`).
|
через команды CAT, используемые панелью.
|
||||||
|
|
||||||
См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы.
|
См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы.
|
||||||
|
|
||||||
@@ -84,9 +84,8 @@ Andromeda — это **не отдельный набор команд**, а д
|
|||||||
|
|
||||||
## 3. Раскладка по умолчанию (AndromedaMap.pas)
|
## 3. Раскладка по умолчанию (AndromedaMap.pas)
|
||||||
|
|
||||||
Раскладка **зашита в коде** и зеркалит дефолтную таблицу Thetis
|
Раскладка **зашита в коде**: внутренний 0-based индекс на единицу меньше номера
|
||||||
(`MakeNewAndromedaDataset`): наш 0-based индекс = «Pushbutton/Encoder Number»
|
кнопки или энкодера, передаваемого панелью. Номера **проверены на реальной панели** (см.
|
||||||
Thetis − 1. Номера **проверены на реальной панели** (см.
|
|
||||||
`andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`.
|
`andromeda_test/AndDecode.pas`). Правится в `AndromedaMap.pas`.
|
||||||
|
|
||||||
### Кнопки (`ZZZP`, индекс 0-based после разбора)
|
### Кнопки (`ZZZP`, индекс 0-based после разбора)
|
||||||
@@ -105,12 +104,12 @@ Thetis − 1. Номера **проверены на реальной панел
|
|||||||
| | | 47 | TUN |
|
| | | 47 | TUN |
|
||||||
|
|
||||||
Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT).
|
Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT).
|
||||||
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены
|
NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены;
|
||||||
(в Thetis — на тачскрине), действия в enum сохранены для кастомной раскладки.
|
действия в enum сохранены для пользовательской раскладки.
|
||||||
|
|
||||||
### Shift / band-keypad
|
### Shift / band-keypad
|
||||||
|
|
||||||
Кнопка #28 — модификатор-защёлка (как `eBBShift` в Thetis). Пока активен
|
Кнопка #28 — модификатор-защёлка. Пока он активен
|
||||||
(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую
|
(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую
|
||||||
(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается
|
(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается
|
||||||
повторным Shift либо автоматически после выбора диапазона (non-sticky).
|
повторным Shift либо автоматически после выбора диапазона (non-sticky).
|
||||||
@@ -120,7 +119,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
|||||||
| # | Параметр | Шаг |
|
| # | Параметр | Шаг |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | AF Gain (громкость) | ±1 |
|
| 0 | AF Gain (громкость) | ±1 |
|
||||||
| 1 | AGC-T (в Thetis — слайдер «RF») | ±1 dB |
|
| 1 | AGC-T | ±1 dB |
|
||||||
| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) |
|
| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) |
|
||||||
| 11 | Drive (мощность) | ±1 % |
|
| 11 | Drive (мощность) | ±1 % |
|
||||||
| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) |
|
| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) |
|
||||||
@@ -129,7 +128,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
|||||||
> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован.
|
> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован.
|
||||||
> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта.
|
> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта.
|
||||||
|
|
||||||
### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis)
|
### Индикаторы (`ZZZI`, номера соответствуют протоколу панели)
|
||||||
| LED # | Индикатор | LED # | Индикатор |
|
| LED # | Индикатор | LED # | Индикатор |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | MOX | 12 | Split |
|
| 1 | MOX | 12 | Split |
|
||||||
@@ -142,7 +141,7 @@ NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках
|
|||||||
| 11 | VFO Lock | | |
|
| 11 | VFO Lock | | |
|
||||||
|
|
||||||
Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR,
|
Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR,
|
||||||
SNB, ANF, Squelch**. Остальные слоты дефолтной таблицы Thetis (ATU/RIT/XIT)
|
SNB, ANF, Squelch**. Остальные слоты таблицы панели (ATU/RIT/XIT)
|
||||||
в ewsdr функций не имеют и не зажигаются.
|
в ewsdr функций не имеют и не зажигаются.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -169,7 +168,7 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
|||||||
- Транспорт: **serial** (USB CAT).
|
- Транспорт: **serial** (USB CAT).
|
||||||
- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие).
|
- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие).
|
||||||
- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов).
|
- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов).
|
||||||
- Раскладка — зашитый дефолт, выверена по Thetis и реальной панели.
|
- Раскладка по умолчанию проверена с реальной панелью.
|
||||||
- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`.
|
- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`.
|
||||||
- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`.
|
- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`.
|
||||||
- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели.
|
- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели.
|
||||||
@@ -178,11 +177,11 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
|||||||
### Осталось / возможные улучшения
|
### Осталось / возможные улучшения
|
||||||
| Задача | Заметки |
|
| Задача | Заметки |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и passband дисплея (sideband-aware, как в Thetis) |
|
| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и sideband-aware passband дисплея |
|
||||||
| **Sticky shift** | сейчас shift всегда non-sticky (снимается band-нажатием); в Thetis есть `AndromedaStickyShift` |
|
| **Sticky shift** | сейчас shift всегда non-sticky и снимается нажатием кнопки диапазона; можно добавить сохраняемую настройку |
|
||||||
| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) |
|
| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) |
|
||||||
| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` |
|
| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` |
|
||||||
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) |
|
| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг и пользовательский интерфейс |
|
||||||
| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) |
|
| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) |
|
||||||
| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` |
|
| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` |
|
||||||
| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано |
|
| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано |
|
||||||
@@ -197,6 +196,4 @@ SNB, ANF, Squelch**. Остальные слоты дефолтной табли
|
|||||||
- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` +
|
- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` +
|
||||||
push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю).
|
push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю).
|
||||||
- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы).
|
- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы).
|
||||||
- Сверяться с форматами Thetis: `Andromeda/Andromeda.cs` (логика панели,
|
- Форматы команд `ZZZ*` и нумерацию элементов проверять по протоколу панели.
|
||||||
`EIndicatorActions`/`EButtonBarActions`/`EEncoderActions`) и
|
|
||||||
`CAT/CATCommands.cs` (`ZZZ*`).
|
|
||||||
|
|||||||
+5
-9
@@ -6,13 +6,9 @@
|
|||||||
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
|
как отдельным трансивером — через свой TCP-порт. COM-порты и основной TCP-порт
|
||||||
(19090) остаются за главным приёмником (слайс A) и не тронуты.
|
(19090) остаются за главным приёмником (слайс A) и не тронуты.
|
||||||
|
|
||||||
Эталон поведения — **SmartSDR CAT** (FlexRadio): порт привязан к слайсу
|
Каждый порт привязан к определённому слайсу. Флаг **Auto TX** разрешает
|
||||||
(«VFO A Slice»), а флаг **Auto TX** — это их «Auto Switch TX Slice»:
|
команде передачи автоматически назначить этот слайс источником TX; без флага
|
||||||
|
источник выбирается оператором вручную.
|
||||||
> When enabled Auto Switch TX Slice automatically switches the Transmit Slice to
|
|
||||||
> the VFO A Slice selected above when a CAT TX command is sent on the com port…
|
|
||||||
> If this option is not enabled, the operator will have to manually designate the
|
|
||||||
> Transmit Slice.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -72,8 +68,8 @@ TX-источника атомарны относительно GUI.
|
|||||||
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
|
`RX;` / `ZZTX0;`: снимает передачу **только свою** — если TX-источник именно этот
|
||||||
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
|
слайс. `RX;` на порту слайса C не обрывает эфир слайса B. TUNE снимается тоже.
|
||||||
|
|
||||||
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе (как в
|
Выбор TX-источника после отпускания PTT **остаётся** на этом слайсе.
|
||||||
SmartSDR: Transmit Slice переключён, а не одолжен). Бейдж на флаге это показывает.
|
Бейдж на флаге это показывает.
|
||||||
|
|
||||||
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
|
`GetTransmitting` (для `TX`/`IF`/`ZZTX`) на порту слайса = «передаёт **этот**
|
||||||
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
|
слайс». Чужой эфир виден как приём — иначе управляющее ПО решило бы, что его PTT
|
||||||
|
|||||||
+32
-36
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
|
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
|
||||||
полный перечень реализованных команд, осознанные заглушки и план дальнейших
|
полный перечень реализованных команд, осознанные заглушки и план дальнейших
|
||||||
работ. За эталон протокола взят **Thetis / PowerSDR** (`CATParser.cs`,
|
работ. Поддерживается диалект Kenwood TS-2000 с расширениями `ZZ*`,
|
||||||
`CATCommands.cs`, `CATStructs.xml`) — диалект Kenwood TS-2000 + расширения `ZZ*`.
|
используемый совместимыми SDR-приложениями.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
|
|||||||
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
|
| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
|
||||||
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
|
| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
|
||||||
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
|
| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
|
||||||
| `CATTcp.pas` (~507 строк) | TCP-сервер (Thetis-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
|
| `CATTcp.pas` (~507 строк) | TCP-сервер (CAT-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
|
||||||
|
|
||||||
### 1.1 Принципы
|
### 1.1 Принципы
|
||||||
|
|
||||||
@@ -50,29 +50,29 @@ CAT-транспорты ──► TCATEngine (парсер протокола)
|
|||||||
|
|
||||||
| | Кол-во |
|
| | Кол-во |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Всего `ZZ*` в Thetis | 315 |
|
| Всего известных `ZZ*` | 315 |
|
||||||
| Реально реализовано в EWSDR | **~120** |
|
| Реально реализовано в EWSDR | **~120** |
|
||||||
| Заглушки (отвечают валидным dummy, функции нет) | ~190 |
|
| Заглушки (отвечают валидным dummy, функции нет) | ~190 |
|
||||||
| Совсем не обрабатываются (вернут `?;`) | 5 |
|
| Совсем не обрабатываются (вернут `?;`) | 5 |
|
||||||
|
|
||||||
Подписи всех 315 команд сверены с `CATCommands.cs` (см. §2.5) — комментарию у
|
Подписи всех 315 команд проверены по протокольному поведению (см. §2.5) — комментарию у
|
||||||
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
|
заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть
|
||||||
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).
|
**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4).
|
||||||
|
|
||||||
### 1.3 Осознанные отклонения от Thetis
|
### 1.3 Осознанные особенности реализации
|
||||||
|
|
||||||
Три места, где EWSDR намеренно расходится с эталоном. Больше таких нет.
|
Три команды имеют локальный формат, сложившийся в EWSDR.
|
||||||
|
|
||||||
| Команда | У Thetis | У нас | Почему |
|
| Команда | Базовое значение | Реализация EWSDR | Почему |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
|
| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты |
|
||||||
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
|
| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду |
|
||||||
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |
|
| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет |
|
||||||
|
|
||||||
Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
|
Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in)
|
||||||
эталон принимает в 150..5000 мс, у нас потолок 2000 — столько же стоит в
|
в EWSDR имеет потолок 2000 мс — столько же стоит в
|
||||||
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
|
настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения
|
||||||
выше подрезаются, как и всё прочее у Thetis.
|
выше ограничиваются общим механизмом проверки входных значений.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`,
|
|||||||
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (00–14) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
|
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (00–14) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
|
||||||
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |
|
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |
|
||||||
|
|
||||||
Шкала шагов `ZZAU/ZZBP` — `StepIdxToHz` (зеркало Thetis `Step2Freq`):
|
Шкала шагов `ZZAU/ZZBP` задаётся функцией `StepIdxToHz`:
|
||||||
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
|
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
|
||||||
|
|
||||||
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
|
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
|
||||||
@@ -176,7 +176,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
|
|||||||
| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
|
| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` |
|
||||||
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
|
| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) |
|
||||||
| `ZZET` | кнопка TXEQ | `EQOn` |
|
| `ZZET` | кнопка TXEQ | `EQOn` |
|
||||||
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
|
| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (поле длиной 36 символов). Полос бывает только 3 или 10 — движок (`PushTXEQProfile`) и редактор знают ровно эти два случая, остальное отвергается и не выдаётся |
|
||||||
| `ZZTO` | мощность настройки | `TUNLevel` |
|
| `ZZTO` | мощность настройки | `TUNLevel` |
|
||||||
| `ZZTU` | кнопка TUN | `SetTune` |
|
| `ZZTU` | кнопка TUN | `SetTune` |
|
||||||
| `ZZUT` | двухтональник 2TON | `SetTwoTone` |
|
| `ZZUT` | двухтональник 2TON | `SetTwoTone` |
|
||||||
@@ -194,7 +194,7 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
|
|||||||
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
|
| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) |
|
||||||
| `ZZCB` | break-in прошивки | `.BreakIn` |
|
| `ZZCB` | break-in прошивки | `.BreakIn` |
|
||||||
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
|
| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` |
|
||||||
| `ZZCM` | сайдтон (у Thetis — «monitor **disable**», значение инвертировано) | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
|
| `ZZCM` | сайдтон; поле `monitor disable` использует инвертированное значение | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции |
|
||||||
|
|
||||||
**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
|
**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь —
|
||||||
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
|
`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP,
|
||||||
@@ -216,10 +216,10 @@ CW: по CAT прилетает что угодно, а контроллер п
|
|||||||
|
|
||||||
### 2.5 Ревизия алиасов и подписей
|
### 2.5 Ревизия алиасов и подписей
|
||||||
|
|
||||||
Подписи писались по буквам кода, а не по Thetis, и врали примерно в 150
|
Подписи писались по буквам кода, а не по CAT, и врали примерно в 150
|
||||||
местах. Хуже: часть **работающих** команд была привязана не к своей функции —
|
местах. Хуже: часть **работающих** команд была привязана не к своей функции —
|
||||||
внешний софт получал осмысленный, но неверный ответ, что хуже честной
|
внешний софт получал осмысленный, но неверный ответ, что хуже честной
|
||||||
заглушки. Всё сверено с `CATCommands.cs` и исправлено.
|
заглушки. Все подписи проверены и исправлены.
|
||||||
|
|
||||||
**Команда делала не своё дело — теперь делает своё:**
|
**Команда делала не своё дело — теперь делает своё:**
|
||||||
|
|
||||||
@@ -253,42 +253,39 @@ CW: по CAT прилетает что угодно, а контроллер п
|
|||||||
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
|
> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`).
|
||||||
> Во всех случаях старое поведение было отсебятиной.
|
> Во всех случаях старое поведение было отсебятиной.
|
||||||
|
|
||||||
**Кенвудовские команды.** Сверены отдельно: все 40 команд, реализованных в
|
**Кенвудовские команды.** Сверены отдельно: реализованы все 40 команд,
|
||||||
Thetis, у нас есть, ширины полей совпадают с `CATStructs.xml`. Исправлено:
|
необходимых для совместимости, и проверена ширина полей. Исправлено:
|
||||||
|
|
||||||
| Команда | Что было не так |
|
| Команда | Что было не так |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2`, как шлёт Thetis |
|
| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2` |
|
||||||
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
|
| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент |
|
||||||
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
|
| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается |
|
||||||
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
|
| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** |
|
||||||
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
|
| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код |
|
||||||
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь обёртка над `ZZTA`, как в эталоне, и сама `ZZTA` проверяет значение строго |
|
| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь это обёртка над `ZZTA`, а сама `ZZTA` проверяет значение строго |
|
||||||
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент Thetis обращается как раз к `ZZ*` и не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
|
| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент расширенных команд не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча |
|
||||||
|
|
||||||
**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу:
|
**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу:
|
||||||
|
|
||||||
| Где | Что было не так |
|
| Где | Что было не так |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` |
|
| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Теперь лишний суффикс отбраковывается в парсере, а список безаргументных команд хранится в `IsParamless` |
|
||||||
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
|
| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий |
|
||||||
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
|
| `ZZAU` `ZZBP` `ZZBM` `ZZBS` `ZZFI` | длину поля проверяли, а содержимое — нет: `StrToIntDef(s, 0)` превращал любую нечисловую пару символов в индекс 0. То есть `ZZBSxx;` **переключал диапазон** на нулевой вместо `?;`, `ZZBMxx;` и `ZZAUxx;`/`ZZBPxx;` двигали VFO, а `ZZFIxx;` выбирал фильтр 0. Разбор приведён к идиоме `ZZFL`/`ZZFH`: `TryStrToInt`, иначе ошибка формата |
|
||||||
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS` — там мусор трактуется как симплекс намеренно, по эталону |
|
| `CN` `FW` `GT` `NB` `PC` `SH`/`SL` `AG` `SQ`, `ZZAG` `ZZAR` `ZZNA` `ZZNB` `ZZNR` `ZZPC` `ZZSQ` `ZZST` `ZZTB` | ★тот же дефект, найденный сплошным прогоном (`test/cat`): правку получили только пять команд выше, а у остальных `StrToIntDef` остался — в том числе у **кенвудовских двойников уже исправленных величин**. `FWxxxx;`/`SHxx;`/`SLxx;` ставили фильтр 0 (тот же индекс, что `ZZFI`), `AG0xxx;` и `SQ0xxx;` — громкость и порог в ноль, `GTxxx;` — АРУ в FAST, `PCxxx;`/`ZZPCxxx;` — мощность в ноль. Разбор везде строгий. Не тронуты `FR` (сам сверяет `0`/`1`), `MD`/`ZZMD` (нечисловое даёт 0, а установка идёт от 1) и `ZZOS`, где некорректное значение намеренно трактуется как симплекс |
|
||||||
| `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
|
| `ZZGT` | опрос отвечал тремя цифрами (как кенвудовская `GT`), а установка принимала ровно один символ: клиент, прочитавший `ZZGT000;` и написавший его назад, получал `?;`. Теперь принимаются обе ширины |
|
||||||
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
|
| `ZZBE` | формы были перевёрнуты: опрос `ZZBE;` отвечал `?;`, а установка `ZZBE01;` возвращала данные (`'1'`). Вся семья «сдвиг VFO на nn шагов» (`ZZAD ZZAE ZZAF ZZBF ZZSG ZZSH`) — однострочные заглушки, `ZZBE` приведён к ним |
|
||||||
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
|
| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время |
|
||||||
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
|
| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле. ★И пишем через `WebUtils.SockSend`, а не голым `fpSend`: в нём `MSG_NOSIGNAL`, без которого запись в закрытый клиентом сокет приходит как `SIGPIPE` и убивает процесс целиком (обработчика сигнала в дереве нет, а цикл дозаписи умножает число попыток) |
|
||||||
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
|
| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт |
|
||||||
|
|
||||||
Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками
|
Формат `ZZOT` проверен отдельно: 9 цифр кодируют МГц с шестью знаками после
|
||||||
после запятой (вставляет точку после третьего разряда) — численно это ровно те
|
запятой, что численно соответствует целому значению в герцах. Некорректное
|
||||||
же герцы, что пишем мы, поле совместимо. Мусор в `ZZOS` эталон трактует как
|
значение `ZZOS` намеренно трактуется как симплекс для совместимости клиентов.
|
||||||
симплекс (`default` в `String2OffsetDirection`), и мы намеренно повторяем это,
|
|
||||||
а не отвечаем ошибкой.
|
|
||||||
|
|
||||||
Формат ответа на `ZZ*` (`ZZ` + код + **эхо суффикса запроса** + значение) сверен
|
Формат ответа на `ZZ*` — `ZZ` + код + **эхо суффикса запроса** + значение.
|
||||||
с `CATParser.ParseExtended` — совпадает; это важно для `ZZSM0;`, где запрос
|
Эхо суффикса важно для `ZZSM0;`, где запрос несёт селектор приёмника.
|
||||||
несёт селектор приёмника.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -314,9 +311,8 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
|||||||
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
|
| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя |
|
||||||
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
|
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
|
||||||
|
|
||||||
> Прежде чем реализовывать новую команду, сверяйся с форматом поля в
|
> Прежде чем реализовывать новую команду, проверяй ширину, знак и смысл поля
|
||||||
> `Thetis/.../CAT/CATStructs.xml` (`<nsetparms>` / `<ngetparms>`) и телом
|
> по документации протокола и поведению совместимых клиентов. Также проверяй
|
||||||
> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. И проверяй
|
|
||||||
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).
|
> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2).
|
||||||
|
|
||||||
### 3.1 Совсем не обрабатываются (вернут `?;`)
|
### 3.1 Совсем не обрабатываются (вернут `?;`)
|
||||||
@@ -339,9 +335,9 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
|||||||
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
|
| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) |
|
||||||
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма |
|
| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма |
|
||||||
| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) |
|
| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) |
|
||||||
| **Микшер Flex 5000 / F1500, FlexWire** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | железо другого вендора |
|
| **Вендорские команды микшера и служебной шины** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | неподдерживаемое оборудование |
|
||||||
| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера |
|
| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера |
|
||||||
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки Thetis |
|
| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки CAT |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -382,7 +378,7 @@ Thetis, у нас есть, ширины полей совпадают с `CATSt
|
|||||||
|
|
||||||
## 6. Как добавить новую CAT-команду (чек-лист)
|
## 6. Как добавить новую CAT-команду (чек-лист)
|
||||||
|
|
||||||
1. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis).
|
1. **Сверить формат** с документацией протокола и тестами совместимости.
|
||||||
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
|
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
|
||||||
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
|
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
|
||||||
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
|
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
|
||||||
|
|||||||
@@ -249,11 +249,11 @@ end;
|
|||||||
(блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
|
(блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
|
||||||
full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
|
full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
|
||||||
один `sampling_frequency`).
|
один `sampling_frequency`).
|
||||||
- **Размер TX-буфера** масштабируется от device-rate (модель SoapyPlutoSDR):
|
- **Размер TX-буфера** масштабируется от device-rate:
|
||||||
`round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
|
`round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
|
||||||
1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
|
1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
|
||||||
FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
|
FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
|
||||||
- Подтверждено по каноничному драйверу (SoapyPlutoSDR): TX full-scale = 32768,
|
- Для TX используется full-scale 32768,
|
||||||
MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.
|
MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.
|
||||||
|
|
||||||
### 4.7 Усиление / телеметрия
|
### 4.7 Усиление / телеметрия
|
||||||
@@ -394,7 +394,7 @@ end;
|
|||||||
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
|
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
|
||||||
192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
|
192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
|
||||||
образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
|
образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
|
||||||
full-duplex >1 Msps — TX-буфер масштабируется от rate (модель SoapyPlutoSDR).
|
full-duplex >1 Msps — TX-буфер масштабируется от rate.
|
||||||
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
|
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
|
||||||
`SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
|
`SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
|
||||||
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
|
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
|
||||||
|
|||||||
+13
-13
@@ -1,11 +1,11 @@
|
|||||||
# Мультислайсы и панадаптеры (как во FlexRadio)
|
# Мультислайсы и панадаптеры
|
||||||
|
|
||||||
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
|
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
|
||||||
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
|
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
|
||||||
`3049f91`, `a3ac459`).
|
`3049f91`, `a3ac459`).
|
||||||
|
|
||||||
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
|
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
|
||||||
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер**
|
edge-маркером» принята модель **аппаратный DDC = отдельный панадаптер**
|
||||||
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
|
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
|
||||||
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
|
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
|
||||||
альтернативы» в конце файла. Реализация Этапа 3 не начата.
|
альтернативы» в конце файла. Реализация Этапа 3 не начата.
|
||||||
@@ -44,9 +44,9 @@ edge-маркером» принята Flex-модель: **аппаратный
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Модель FlexRadio (итог разбора 2026-07-10)
|
## Модель панадаптеров и слайсов (итог разбора 2026-07-10)
|
||||||
|
|
||||||
Во Flex **две независимые сущности**, и это ключ ко всему дизайну:
|
В интерфейсе используются **две независимые сущности**, и это ключ ко всему дизайну:
|
||||||
|
|
||||||
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
|
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
|
||||||
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
|
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
|
||||||
@@ -58,7 +58,7 @@ edge-маркером» принята Flex-модель: **аппаратный
|
|||||||
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
|
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
|
||||||
6600 — 4/4, 6700 — 8/8.
|
6600 — 4/4, 6700 — 8/8.
|
||||||
|
|
||||||
**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit):
|
**Второй приёмник (второй АЦП)** представлен отдельным трактом захвата:
|
||||||
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
|
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
|
||||||
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
|
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
|
||||||
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
|
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
|
||||||
@@ -66,8 +66,8 @@ edge-маркером» принята Flex-модель: **аппаратный
|
|||||||
двух SCU) — отдельный режим поверх.
|
двух SCU) — отдельный режим поверх.
|
||||||
|
|
||||||
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
|
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
|
||||||
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный
|
перетаскиваемыми разделителями). EWSDR дополнительно поддерживает вынос
|
||||||
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели.
|
панафолла в отдельное окно ОС.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,12 +86,12 @@ SmartSDR НЕ умеет — наш pop-out будет улучшением по
|
|||||||
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
|
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
|
||||||
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
|
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
|
||||||
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
|
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
|
||||||
один слайс в его центре (поведение Flex).
|
один слайс в его центре.
|
||||||
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
||||||
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
Показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||||
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
|
маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
|
||||||
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
|
ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
|
||||||
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
4. Буквы слайсов — глобальный пул A..G через все паны:
|
||||||
буква = личность слайса, пан = место жительства.
|
буква = личность слайса, пан = место жительства.
|
||||||
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
||||||
прячет/блокирует кнопки, а не хардкодит «if Pluto».
|
прячет/блокирует кнопки, а не хардкодит «if Pluto».
|
||||||
@@ -324,7 +324,7 @@ ChangeSampleRate).
|
|||||||
### 3.3 — UI: второй панадаптер в главном окне
|
### 3.3 — UI: второй панадаптер в главном окне
|
||||||
|
|
||||||
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
|
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
|
||||||
`TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист.
|
`TSplitter` между ними. Пан 0 сверху. Высоты сохраняются в настройках.
|
||||||
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
|
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
|
||||||
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
|
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
|
||||||
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
|
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
|
||||||
@@ -480,7 +480,7 @@ ChangeSampleRate).
|
|||||||
|
|
||||||
### 3.5 — Pop-out: панадаптер в отдельном OS-окне
|
### 3.5 — Pop-out: панадаптер в отдельном OS-окне
|
||||||
|
|
||||||
Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке.
|
Кнопка `⧉` в шапке выносит панадаптер в отдельное окно ОС.
|
||||||
|
|
||||||
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
|
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
|
||||||
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
|
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
|
||||||
@@ -767,7 +767,7 @@ ApplyAll, тёплый рестарт восстанавливает override п
|
|||||||
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
|
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
|
||||||
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
|
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
|
||||||
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
|
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
|
||||||
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC =
|
чипами слайсов). Отвергнуто 2026-07-10 в пользу модели «DDC =
|
||||||
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
|
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
|
||||||
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
|
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
|
||||||
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
|
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
|
||||||
|
|||||||
+22
-28
@@ -3,9 +3,9 @@
|
|||||||
Ветка разработки: `feature/tci-protocol`.
|
Ветка разработки: `feature/tci-protocol`.
|
||||||
Дата последнего обновления: 2026-08-18.
|
Дата последнего обновления: 2026-08-18.
|
||||||
|
|
||||||
Эталон протокола — «Протокол TCI, версия 2.0» Expert Electronics
|
Реализация соответствует спецификации «Протокол TCI, версия 2.0»
|
||||||
(`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером**
|
(`doc/TCI Protocol_RU.pdf`, 12 января 2024). EWSDR выступает **сервером**
|
||||||
(как ExpertSDR3): порт слушаем мы, клиенты — логгеры, скиммеры, программы
|
по стандартной серверной модели: порт слушаем мы, клиенты — логгеры, скиммеры, программы
|
||||||
цифровых видов, внешние усилители и коммутаторы.
|
цифровых видов, внешние усилители и коммутаторы.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -187,13 +187,12 @@ UI — вкладка **CAT → TCI Server**, справа от «TCP CAT Server
|
|||||||
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
|
| `arg1` у `DRIVE`, `TUNE_DRIVE` | мощность одна на радио — номер не адресует ничего |
|
||||||
|
|
||||||
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
|
★**Почему приёмник — слайс, а не панорама.** Сперва приёмником был панадаптер,
|
||||||
как в ExpertSDR3, где приёмник = DDC. У нас это разошлось с жизнью сразу на
|
в модели, где приёмник соответствует DDC. В EWSDR это разошлось с жизнью сразу на
|
||||||
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
|
двух концах. У Pluto панорама ровно одна (`BackendCaps.MaxPans = 1`), и второй
|
||||||
приёмник существует там **только** как слайс главного пана — при нумерации по
|
приёмник существует там **только** как слайс главного пана — при нумерации по
|
||||||
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице. С
|
панам он был бы недоступен вообще, а `TRX_COUNT` навсегда равнялся единице.
|
||||||
другого конца — клиенты: у MSHV в настройках всего две модели, «TCI Client rx1»
|
Кроме того, некоторые клиенты позволяют выбрать только приёмники 0 и 1.
|
||||||
и «rx2», то есть приёмники 0 и 1, и никакого третьего номера ввести некуда
|
Значит
|
||||||
(`hvrigcontrol.cpp`: `netServPort[3]`/`[4]`, `network.cpp`: `tci_trx`). Значит
|
|
||||||
«первый добавленный слайс» обязан быть приёмником **1** на любом железе.
|
«первый добавленный слайс» обязан быть приёмником **1** на любом железе.
|
||||||
|
|
||||||
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
|
Слот слайса даёт ровно это. Слоты глобальные и стабильные, у каждого своя
|
||||||
@@ -255,9 +254,8 @@ B независимо от того, где он живёт: на openHPSDR э
|
|||||||
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
|
слотов слайсов) один раз и навсегда, а слот из этого потолка может быть пуст.
|
||||||
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
|
Про пустой слот не шлётся ничего — ни `vfo`, ни `dds`, ни показания
|
||||||
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
|
измерителей, и команды к нему тоже игнорируются целиком (`LiveRx`). Раньше
|
||||||
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту;
|
уходили `dds`/`vfo`/`if` с нулём, и клиент принимал ноль за настоящую частоту,
|
||||||
для MSHV это вообще фатально — именно ответом на `vfo:<rx>,0;` он завершает
|
завершая инициализацию несуществующего приёмника. Состояние появляется в момент
|
||||||
инициализацию, то есть подключился бы к пустоте. Состояние появляется в момент
|
|
||||||
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
|
создания слайса: `PushChannelMap` рассылает картину нового приёмника целиком,
|
||||||
включая `tx_enable` с его номером.
|
включая `tx_enable` с его номером.
|
||||||
|
|
||||||
@@ -501,10 +499,8 @@ web-клиента: явная просьба сильнее умолчания
|
|||||||
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
|
отбрасывается молча: отвечать ошибкой на каждый чужой блок значит захлебнуться.
|
||||||
|
|
||||||
**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт
|
**★Маркер `TX_CHRONO` называет приёмник КЛИЕНТА, а не нулевой.** Клиент шлёт
|
||||||
TX-аудио не по своей воле, а строго в ответ на маркер, и все входящие бинарные
|
TX-аудио строго в ответ на маркер и может фильтровать входящие бинарные блоки
|
||||||
блоки фильтрует по номеру приёмника — у MSHV это первая строка обработчика
|
по номеру приёмника. С жёстким нулём в заголовке клиент,
|
||||||
(`network.cpp`: `if (pStream->receiver != tci_trx) return;`, ветка `TxChrono`
|
|
||||||
там же собирает и отправляет блок). С жёстким нулём в заголовке клиент,
|
|
||||||
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
|
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
|
||||||
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
|
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
|
||||||
клиентом-модулятором запоминается номер приёмника из его же `TRX`
|
клиентом-модулятором запоминается номер приёмника из его же `TRX`
|
||||||
@@ -536,8 +532,8 @@ TX-аудио не по своей воле, а строго в ответ на
|
|||||||
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
|
Срок считается **по часам** от `START`, а не по накопленным сэмплам: линейный
|
||||||
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
|
выход может молчать (мьют, стоящий приёмник), а время записи всё равно идёт.
|
||||||
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
|
Кольцо «последние N секунд» вело себя иначе в обе стороны — начало записи
|
||||||
затирало само себя, а `SAVE` через час после `START` отдавал файл, которого у
|
затирало само себя, а `SAVE` через час после `START` отдавало бы файл уже после
|
||||||
ExpertSDR3 давно бы не было.
|
завершения заданного окна записи.
|
||||||
`SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки
|
`SAVE` завершает запись и ставит её в очередь писателю: файл бывает в десятки
|
||||||
мегабайт, а команда пришла в потоке клиента, который в это время не читает свой
|
мегабайт, а команда пришла в потоке клиента, который в это время не читает свой
|
||||||
сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный
|
сокет. MP3 не поддержан — кодера в проекте нет, и на `.mp3` уходит честный
|
||||||
@@ -794,9 +790,8 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
настройки обязана вставать на тот же слайс, что и модуляция;
|
настройки обязана вставать на тот же слайс, что и модуляция;
|
||||||
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
|
- **ответ автору называет ЕГО номер приёмника**, а состояние в нём — «в эфире
|
||||||
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
|
именно твой слайс» (`FTransmitting and (TxRx = Rx)`). Так надо потому, что
|
||||||
клиенты фильтруют входящие строки по `arg1`: MSHV, например, отбрасывает
|
клиенты могут фильтровать входящие строки по `arg1`, и отказ, названный
|
||||||
всё, что адресовано не его приёмнику (`network.cpp`:
|
чужим номером, до
|
||||||
`if (ls2.at(0)!=tci_trx) continue;`), — и отказ, названный чужим номером, до
|
|
||||||
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
|
него бы просто не дошёл. В **рассылку** уходит номер того, чей слайс сейчас
|
||||||
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
|
источник передачи (`TxRx` по `TxSliceId`): клиент на приёмнике 1 по ней
|
||||||
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
|
видит, что в эфире не он. Смена TX-источника оператором доходит до клиентов
|
||||||
@@ -872,8 +867,8 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
Порядок обратный: сперва UI, потом TCI.
|
Порядок обратный: сперва UI, потом TCI.
|
||||||
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
|
- **`RX_ANC_ENABLE`, `RX_APF_ENABLE`, `RX_DSE_ENABLE`, `RX_NF_ENABLE`.** Этих
|
||||||
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
|
узлов в тракте нет физически. У нас `NR`/`NR2` (EMNR), `NB`/`NB2` (ANB/SNBA)
|
||||||
и `ANF` — они в TCI настоящие. `ANC` и `DSE` — блоки собственной обработки
|
и `ANF` — они в TCI настоящие. Для `ANC`, `DSE`, `APF` и `NF` потребовались
|
||||||
Expert, не WDSP; `APF` и `NF` были бы новыми узлами в цепочке.
|
бы новые узлы обработки в цепочке WDSP.
|
||||||
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
|
- **`RX_NB_PARAM`.** Пороги шумодава у нас — подобранные константы
|
||||||
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
|
`FILTER_NB_*`, одинаковые для всех каналов и не выведенные даже в интерфейс.
|
||||||
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
|
Клиент, покрутивший их по TCI, оставил бы шумодав расстроенным насовсем.
|
||||||
@@ -898,7 +893,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
|
| `MUTE` и линейный выход | глушит и поток: движок под мьютом не зовёт `OnAudio`. Аудиопоток приёмника (`AUDIO_START`) мьют не трогает — он снимается до громкости |
|
||||||
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
|
| DIGL/DIGU, 2 канала | по §3.4 в цифровых модах два канала должны нести комплексный сигнал; у нас это обычное стерео с выхода WDSP — оба тапа стоят там, где сигнал уже вещественный: `rakDemod` сразу за демодулятором, `rakLineOut` за громкостью. Комплексный выход потребовал бы отдельной ветки панели RXA и третьего маршрута тапа, а цифровые клиенты берут `IQ_START` — он честно комплексный |
|
||||||
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A |
|
| Поток канала B | не бывает: в протоколе аудиопоток один на приёмник, и он всегда про канал A |
|
||||||
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а ExpertSDR3 IQ иначе и не шлёт |
|
| Формат IQ | всегда float32, два канала — `AUDIO_STREAM_SAMPLE_TYPE` относится к аудио (§4.3), а TCI IQ иначе и не шлёт |
|
||||||
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
||||||
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
||||||
|
|
||||||
@@ -918,7 +913,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
одинаковыми — в телеметрии платы одно значение forward power.
|
одинаковыми — в телеметрии платы одно значение forward power.
|
||||||
|
|
||||||
У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения
|
У `TRX` третий аргумент разобран только для `tci` (см. §2.5). Значения
|
||||||
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы ExpertSDR3, которых у
|
`mic1`/`mic2`/`micpc`/`ecoder2` называют физические входы TCI, которых у
|
||||||
нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что
|
нас нет: они значат «микрофон, выбранный в программе», то есть ровно то же, что
|
||||||
и отсутствие аргумента.
|
и отсутствие аргумента.
|
||||||
|
|
||||||
@@ -933,7 +928,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
|
(`AddSliceAtFreq` + `CreateSliceFlag` — флаг, раскладка, персист), у
|
||||||
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
|
контроллера есть только `RemoveSlice`. Адаптер TCI, как и CAT, до MainForm
|
||||||
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
|
не дотягивается и не имеет права: он обязан собираться в демоне, где LCL
|
||||||
нет вовсе. Вторая по существу: канал B у ExpertSDR3 — второй приёмник внутри
|
нет вовсе. Вторая по существу: канал B у TCI — второй приёмник внутри
|
||||||
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
|
того же DDC, а у нас слайс — полноценная сущность со своим флагом, звуком,
|
||||||
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
|
входом и CAT-портом. Клиент, «выключивший канал B», снёс бы оператору
|
||||||
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
||||||
@@ -1086,12 +1081,11 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
|
rate устройства сама переобъявляет и `iq_samplerate`, и `if_limits`, а на
|
||||||
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
|
576 кГц (Pluto) та же просьба даёт законные 192 кГц.
|
||||||
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
- **Слайс как приёмник** (часть E, живой движок): созданный на ГЛАВНОМ пане
|
||||||
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой (это и
|
слайс становится приёмником 1, отвечает на `vfo:1,0;` своей частотой,
|
||||||
есть вся инициализация MSHV), слушается командой `vfo:1,0,<Гц>`, отдаёт своё
|
слушается командой `vfo:1,0,<Гц>`, отдаёт своё
|
||||||
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
|
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
|
||||||
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
|
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
|
||||||
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу
|
маркеры `TX_CHRONO` **под номером 1**; а `trx:0,true,tci` поверх этой же передачи
|
||||||
MSHV, сидящего на втором слайсе; а `trx:0,true,tci` поверх этой же передачи
|
|
||||||
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
|
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
|
||||||
целиком — вместо прежнего `vfo:1,0,0`.
|
целиком — вместо прежнего `vfo:1,0,0`.
|
||||||
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ end;
|
|||||||
|
|
||||||
const
|
const
|
||||||
{ Команды только для чтения: ответ на опрос — составной статус, обратно его
|
{ Команды только для чтения: ответ на опрос — составной статус, обратно его
|
||||||
не пишут ни Kenwood, ни Thetis. Круговой прогон их законно не проходит. }
|
не пишут ни Kenwood, ни CAT. Круговой прогон их законно не проходит. }
|
||||||
READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID');
|
READ_ONLY: array[0..3] of string = ('IF', 'ID', 'ZZIF', 'ZZID');
|
||||||
|
|
||||||
function IsReadOnly(const Cmd: string): Boolean;
|
function IsReadOnly(const Cmd: string): Boolean;
|
||||||
|
|||||||
+10
-12
@@ -46,27 +46,25 @@ line-out для всех обнаруженных приёмников. Он н
|
|||||||
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
|
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
|
||||||
уже на передаче, прогон аварийно прекращается.
|
уже на передаче, прогон аварийно прекращается.
|
||||||
|
|
||||||
## Стенд «как MSHV»
|
## Стенд совместимости TCI-клиента
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
|
test/tci/mshv_sim.py --rx 0
|
||||||
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
|
test/tci/mshv_sim.py --rx 1 --audio
|
||||||
```
|
```
|
||||||
|
|
||||||
`mshv_sim.py` повторяет логику TCI-клиента MSHV по его исходникам — включая обе
|
`mshv_sim.py` проверяет две особенности поведения совместимых TCI-клиентов:
|
||||||
особенности, из-за которых он ведёт себя не так, как наш стенд:
|
|
||||||
|
|
||||||
* **фильтр по номеру приёмника** — `if (ls2.at(0)!=tci_trx) continue;`: строка,
|
* **фильтр по номеру приёмника**: строка,
|
||||||
адресованная не его приёмнику, для MSHV не существует (и то же самое для
|
адресованная другому приёмнику, игнорируется (и то же самое для
|
||||||
бинарных блоков, по полю `receiver` заголовка);
|
бинарных блоков, по полю `receiver` заголовка);
|
||||||
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в
|
* **инициализация — один запрос и один ответ**: клиент шлёт `vfo:<rx>,0;` раз в
|
||||||
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||||
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
|
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
|
||||||
|
|
||||||
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
|
Поэтому стенд диагностирует причину ошибки подключения одной строкой, а заодно
|
||||||
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим**
|
ловит то, что иначе проявляется молча: без `tx_enable` со **своим** номером
|
||||||
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с
|
приёмника клиент не разрешает PTT.
|
||||||
`if (!tci_tx_enable) return;`).
|
|
||||||
|
|
||||||
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
|
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
|
||||||
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
|
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
|
||||||
|
|||||||
+10
-13
@@ -1,22 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит,
|
"""Стенд совместимости с TCI-клиентом MSHV.
|
||||||
почему он не подключается.
|
|
||||||
|
|
||||||
Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
|
Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
|
||||||
так, как наш собственный стенд. Две его особенности определяют всё:
|
так, как наш собственный стенд. Две его особенности определяют всё:
|
||||||
|
|
||||||
1. ВСЕ входящие строки он фильтрует по номеру приёмника:
|
1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
|
||||||
if (ls2.at(0)!=tci_trx) continue; // network.cpp
|
RX2 (tci_trx = "1") строка `trx:0,false`
|
||||||
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
|
|
||||||
не существует в природе.
|
не существует в природе.
|
||||||
|
|
||||||
2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
|
2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
|
||||||
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
|
||||||
«Error To Initialize TCI Server» (network.cpp, Network::initAll,
|
сообщает об ошибке инициализации.
|
||||||
ветка s_ModelID==3||4). Никаких других причин у этой ошибки нет.
|
|
||||||
|
|
||||||
Соответствие настроек MSHV: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
|
Соответствие настроек: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
|
||||||
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx).
|
приёмник 1.
|
||||||
|
|
||||||
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
|
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
|
||||||
TX-аудио не отправляются. Передающую часть включает явный --tx.
|
TX-аудио не отправляются. Передающую часть включает явный --tx.
|
||||||
@@ -38,8 +35,8 @@ from live_rx_test import WS, split_commands, parse_command # noqa: E402
|
|||||||
|
|
||||||
HDR = struct.Struct("<16I")
|
HDR = struct.Struct("<16I")
|
||||||
|
|
||||||
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2,
|
# Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
|
||||||
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс.
|
# частота 48 кГц и буфер TX 24 мс.
|
||||||
MSHV_CHANNELS = "2"
|
MSHV_CHANNELS = "2"
|
||||||
MSHV_SAMPLES = "2048"
|
MSHV_SAMPLES = "2048"
|
||||||
MSHV_TYPE = "float32"
|
MSHV_TYPE = "float32"
|
||||||
@@ -65,7 +62,7 @@ class Verdict:
|
|||||||
|
|
||||||
|
|
||||||
class MshvClient:
|
class MshvClient:
|
||||||
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения."""
|
"""Минимальная модель поведения совместимого TCI-клиента."""
|
||||||
|
|
||||||
def __init__(self, ws, rx, verdict):
|
def __init__(self, ws, rx, verdict):
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
@@ -148,7 +145,7 @@ class MshvClient:
|
|||||||
if len(payload) < HDR.size:
|
if len(payload) < HDR.size:
|
||||||
return
|
return
|
||||||
head = HDR.unpack(payload[:HDR.size])
|
head = HDR.unpack(payload[:HDR.size])
|
||||||
# MSHV: if (pStream->receiver != tci_trx) return;
|
# Блоки других приёмников клиент игнорирует.
|
||||||
if head[0] != int(self.rx):
|
if head[0] != int(self.rx):
|
||||||
self.audio_foreign += 1
|
self.audio_foreign += 1
|
||||||
return
|
return
|
||||||
|
|||||||
+15
-19
@@ -121,7 +121,7 @@ begin
|
|||||||
Check('header rate', H.SampleRate = 12000);
|
Check('header rate', H.SampleRate = 12000);
|
||||||
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
|
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
|
||||||
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
|
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
|
||||||
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на
|
// считают по нему число байт. Объявишь «на
|
||||||
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
|
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
|
||||||
Check('header length аудио = ×каналы', H.DataLength = 1024);
|
Check('header length аудио = ×каналы', H.DataLength = 1024);
|
||||||
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
|
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
|
||||||
@@ -484,7 +484,7 @@ begin
|
|||||||
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
|
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
|
||||||
(H.Format <> LongWord(Ord(tsyFloat32))) or
|
(H.Format <> LongWord(Ord(tsyFloat32))) or
|
||||||
// ★Правило, по которому живут клиенты: байт данных = length × размер
|
// ★Правило, по которому живут клиенты: байт данных = length × размер
|
||||||
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`).
|
// отсчёта. Так клиент определяет размер полезной нагрузки.
|
||||||
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
|
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
|
||||||
Inc(Blocks);
|
Inc(Blocks);
|
||||||
end;
|
end;
|
||||||
@@ -2054,7 +2054,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
C.WaitText('ready;', 2000);
|
C.WaitText('ready;', 2000);
|
||||||
|
|
||||||
// Клиент назвался ровно как MSHV: 48 кГц, два канала, блок 2048.
|
// Клиент запросил 48 кГц, два канала и блок 2048.
|
||||||
C.SendText('audio_samplerate:48000;');
|
C.SendText('audio_samplerate:48000;');
|
||||||
C.SendText('audio_stream_channels:2;');
|
C.SendText('audio_stream_channels:2;');
|
||||||
C.SendText('audio_stream_samples:2048;');
|
C.SendText('audio_stream_samples:2048;');
|
||||||
@@ -2174,7 +2174,7 @@ begin
|
|||||||
// идти дальше, а не прекратиться до конца передачи.
|
// идти дальше, а не прекратиться до конца передачи.
|
||||||
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
|
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
|
||||||
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
|
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
|
||||||
// это редкость (на живом MSHV — два неотвеченных маркера за 12 с), но
|
// это редкость, но
|
||||||
// строка ниже печатает просадку, чтобы регресс был виден.
|
// строка ниже печатает просадку, чтобы регресс был виден.
|
||||||
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
|
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
|
||||||
'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
|
'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
|
||||||
@@ -2182,8 +2182,8 @@ begin
|
|||||||
Markers > 40, IntToStr(Markers));
|
Markers > 40, IntToStr(Markers));
|
||||||
|
|
||||||
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
|
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
|
||||||
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает
|
// ★Регресс с железа. Штатный конец посылки (`trx:N,false`)
|
||||||
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
|
// обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
|
||||||
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
|
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
|
||||||
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
|
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
|
||||||
// миллисекунд против шага тика в полкванта, так что промах выпадал через
|
// миллисекунд против шага тика в полкванта, так что промах выпадал через
|
||||||
@@ -2586,19 +2586,17 @@ begin
|
|||||||
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
|
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
|
||||||
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
|
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
|
||||||
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
|
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
|
||||||
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV.
|
// первым номером приёмника после нулевого.
|
||||||
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
|
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
|
||||||
agcMedium, 0.5, -1, '', 0);
|
agcMedium, 0.5, -1, '', 0);
|
||||||
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
|
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
|
||||||
C.Pump(300);
|
C.Pump(300);
|
||||||
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
|
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
|
||||||
|
|
||||||
// Инициализация MSHV — это ровно один запрос и ровно один ответ
|
// Инициализация совместимого клиента — это один запрос и один ответ.
|
||||||
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
|
|
||||||
// «Error To Initialize TCI Server»).
|
|
||||||
C.SendText('vfo:1,0;');
|
C.SendText('vfo:1,0;');
|
||||||
S := C.WaitText('vfo:1,0,', 1500);
|
S := C.WaitText('vfo:1,0,', 1500);
|
||||||
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S);
|
Check('слайс отвечает на vfo:1,0 при инициализации', S <> '', S);
|
||||||
Check('слайс отдаёт свою частоту',
|
Check('слайс отдаёт свою частоту',
|
||||||
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
|
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
|
||||||
|
|
||||||
@@ -2611,7 +2609,7 @@ begin
|
|||||||
FloatToStr(SV.TargetHz));
|
FloatToStr(SV.TargetHz));
|
||||||
|
|
||||||
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
|
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
|
||||||
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231).
|
// Клиенты могут отбрасывать блоки с чужим receiver.
|
||||||
C.SendText('audio_start:1;');
|
C.SendText('audio_start:1;');
|
||||||
C.Pump(100);
|
C.Pump(100);
|
||||||
for k := 0 to 40 do
|
for k := 0 to 40 do
|
||||||
@@ -2691,13 +2689,11 @@ begin
|
|||||||
while C.NextFrame(Op, Pay) do ;
|
while C.NextFrame(Op, Pay) do ;
|
||||||
|
|
||||||
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
|
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
|
||||||
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает
|
// Совместимый клиент шлёт TX-аудио только в ответ на маркер TX_CHRONO и
|
||||||
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if
|
// может отбрасывать входящий блок с чужим receiver. С жёстким нулём в
|
||||||
// (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288
|
// заголовке клиент, сидящий на втором слайсе
|
||||||
// и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе
|
|
||||||
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
|
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
|
||||||
// без них он не отправляет ни одного блока. Ровно это и наблюдалось на
|
// без них он не отправляет ни одного блока.
|
||||||
// живом железе с MSHV.
|
|
||||||
Ctrl.FWDSPReady := True;
|
Ctrl.FWDSPReady := True;
|
||||||
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
|
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
|
||||||
C.SendText('trx:1,true,tci;');
|
C.SendText('trx:1,true,tci;');
|
||||||
@@ -2717,7 +2713,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
|
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
|
||||||
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
|
Check('TX слайса: маркер назван номером приёмника',
|
||||||
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
||||||
|
|
||||||
// ★Команда, которая ничего не сделала, не должна перебивать пару
|
// ★Команда, которая ничего не сделала, не должна перебивать пару
|
||||||
|
|||||||
Reference in New Issue
Block a user