chore: refine project documentation

This commit is contained in:
2026-08-25 22:44:36 +03:00
parent 8237f03930
commit 98d90e1e5a
45 changed files with 457 additions and 516 deletions
+25 -27
View File
@@ -71,7 +71,7 @@ const
// Панадаптером приёмник быть перестал: у Pluto пан ровно один (MaxPans=1),
// и «второй приёмник» там существует только как слайс главного пана — при
// нумерации по панам он был бы недоступен вовсе. А клиенты умеют мало
// номеров: у MSHV в настройках всего rx1/rx2, то есть приёмники 0 и 1.
// номеров: некоторые клиенты позволяют выбрать только приёмники 0 и 1.
// Со слотами правило «первый добавленный слайс = приёмник 1» держится на
// любом железе: на openHPSDR слайс второго пана и на Pluto слайс главного
// одинаково занимают слот B. Пан стал свойством слайса (его центр и IQ).
@@ -114,9 +114,9 @@ const
TCI_TX_GRACE_MS = 300; // grace на старте: клиент собирает первый блок
TCI_TX_MARKERS_PER_TICK = 8; // явный потолок пачки за одно пробуждение
TCI_TX_STALL_MAX_MS = 250; // и не дольше этого: ждать больше нечего
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: MSHV пишет свой
// TX-буфер granule'ами STREAM_C = 4096 отсчётов при 96 кГц (network.cpp), то
// есть 42.7 мс, и отвечает на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
// ★Аванс под ЗЕРНИСТОСТЬ КЛИЕНТА. Мелкого запроса мало: клиент может писать
// TX-буфер гранулами по 4096 отсчётов при 96 кГц, то есть 42.7 мс, и отвечать
// на маркеры ПАЧКАМИ по 4-5 блоков раз в ~44 мс — как
// ни дроби запрос. Замер на железе: 218 осушений очереди за 11 с, ровно по
// одному на пачку, из них 12 длиннее подушки. Лечится только запасом не меньше
// пачки. Это НЕ регулятор по уровню очереди (тот управлял бы темпом запроса,
@@ -125,7 +125,7 @@ const
TCI_TX_GAP_MAX_US = 200000; // пауза длиннее — это не зернистость, а сбой
TCI_TX_LEAD_MAX_MS = 120; // потолок аванса (он же задержка передачи)
// ★Посев аванса, пока про клиента ничего не известно. Первая передача после
// подключения иначе стартует с pre-roll 31.75 мс против первого ответа MSHV
// подключения иначе стартует с pre-roll 31.75 мс до первого ответа клиента
// на 41-й миллисекунде — и сохнет. Обучение к этому моменту физически не
// успевает: аванс появляется только после первого ответа.
TCI_TX_LEAD_DEF_MS = 50;
@@ -254,7 +254,7 @@ type
// ★Мерим ПЕРИОД между пачками, а не их размер. Размер зависит и от того,
// сколько мы запросили: попросили больше — клиент ответил длиннее — аванс
// подрос — попросили ещё больше. Это положительная обратная связь. Период
// же равен внутренней грануле клиента (у MSHV — STREAM_C/96 кГц = 42.7 мс)
// же равен внутренней грануле клиента (например, 4096/96 кГц = 42.7 мс)
// и от нашего темпа не зависит вовсе.
FTxGapUs: Double; // оценка периода между пачками, мкс
FTxLead: Integer; // выданный аванс под зернистость клиента, кадров
@@ -857,8 +857,8 @@ begin
FServer.Broadcast(TCIBuild('rx_channel_enable',
[TCIIntStr(Rx), '1', TCIBoolStr(N > 1)]));
// ★С адресом приёмника — иначе клиент его не увидит. Клиенты фильтруют
// входящие по arg1 (см. StrTrx), а MSHV на этом ещё и завязывает передачу:
// `set_ptt` начинается с `if (!tci_tx_enable) return;`. Слайс, созданный
// входящие по arg1 (см. StrTrx), а некоторые также связывают с этим флагом
// разрешение передачи. Слайс, созданный
// ПОСЛЕ подключения клиента, без этой строки получал бы молча мёртвую PTT.
FServer.Broadcast(StrTxEnable(Rx));
end;
@@ -1195,8 +1195,8 @@ end;
function TTCIAdapter.StrTrx(Rx: Integer): string;
// Состояние передатчика ДЛЯ ПРИЁМНИКА Rx: «true» значит, что в эфире именно
// его слайс. Клиенты фильтруют входящие строки по номеру приёмника (MSHV,
// например, отбрасывает всё, что адресовано не ему), поэтому автору команды
// его слайс. Клиенты могут фильтровать входящие строки по номеру приёмника,
// поэтому автору команды
// отвечаем ЕГО номером: `trx:1,false` — это «твоя заявка не прошла», а
// `trx:0,false` он бы просто не увидел. В рассылку уходит номер того, чей
// слайс сейчас источник передачи (`TxRx`).
@@ -1309,8 +1309,7 @@ end;
procedure TTCIAdapter.BroadcastTxEnable;
// TX_ENABLE — величина всего радио, но адресуется приёмником, и клиент читает
// только строки со СВОИМ номером. Одной строки `tx_enable:0,…` мало: клиент на
// приёмнике 1 её отбрасывает, а у MSHV на этом флаге висит вся передача
// (`set_ptt`: `if (!tci_tx_enable) return;`).
// приёмнике 1 может её отбросить и не разрешить передачу.
var Rx: Integer;
begin
if (FServer = nil) or (FServer.ClientCount = 0) then Exit;
@@ -1830,8 +1829,7 @@ begin
if (Ch < 0) or (Ch >= TCI_CHANNELS) then Exit;
// ★Про приёмник, которого нет (слайс не создан или уже удалён), молчим
// целиком — и на чтение тоже. Ответ «vfo:1,0,0» клиент принял бы за
// настоящую частоту, а MSHV именно этим ответом завершает инициализацию
// (network.cpp, Network::initAll) — то есть подключился бы к пустоте.
// настоящую частоту и завершить инициализацию несуществующего приёмника.
if (Ch >= ChanCount(Rx)) or not RxActive(Rx) then Exit;
// Частота ставится, только если её удалось разобрать И она годная: «vfo:0,0,abc»
@@ -2157,7 +2155,7 @@ begin
begin
// arg3 — источник сигнала (только у TRX). Наш только 'tci': модуляция
// берётся из аудиопотока этого клиента. Остальные значения
// (mic1/mic2/micpc/ecoder2) называют физические входы ExpertSDR3, которых
// (mic1/mic2/micpc/ecoder2) называют физические входы TCI, которых
// у нас нет, — они значат «микрофон, выбранный в программе», то есть ровно
// то, что и без arg3. Требование «включен аудиопоток по TCI» (§4.2)
// проверяем буквально: без AUDIO_START модулировать нечем, и молча
@@ -2196,8 +2194,8 @@ begin
// ничего не изменила, всё равно её перебивала: клиент, уже
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает
// (network.cpp:231), то есть передача просто замолкает.
// с этого мига уходят под номером 0. Клиент второго приёмника может
// отбросить такие блоки, и передача просто замолкнет.
if B then
begin
if Started and FromTCI then
@@ -2236,7 +2234,7 @@ begin
FTxRx := 0;
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
// идущей передачи. Раньше эти две строки расходились, и штатный
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым.
// конец посылки (`trx:N,false`) оставлял FTxRunning поднятым.
FTxRunning := False;
end;
finally FTxLock.Leave; end;
@@ -2789,7 +2787,7 @@ begin
if TCITryArgInt(M, 0, V) and TCIValidAudioRate(V) and (V <> Client.AudioRate) then
begin
Client.AudioRate := V;
// Число сэмплов в блоке у ExpertSDR3 своё на каждую частоту (§4.3), и
// Число сэмплов в блоке у TCI своё на каждую частоту (§4.3), и
// клиент вправе на это рассчитывать, пока не задал своё явно.
Client.AudioSamples := TCIDefaultAudioSamples(V);
RestartStreams(Client, tstRXAudio);
@@ -3740,7 +3738,8 @@ end;
procedure TTCIAdapter.TxPreWarm(Client: TTCIClient; Rx: Integer);
// Поток клиента, ДО SetMOX. Один маркер вперёд всей подготовки тракта.
//
// ★Зачем: MSHV отвечает на первый маркер только через ~41 мс, а сам маркер
// ★Зачем: некоторые клиенты отвечают на первый маркер только через ~41 мс,
// а сам маркер
// уходил лишь после того, как SetMOX отработает реле, pre-roll и PureSignal —
// ещё 9-17 мс. Эти два ожидания шли последовательно, и нулевого pre-roll не
// хватало: очередь DUC сохла на старте КАЖДОЙ первой передачи. Отправив запрос
@@ -4001,9 +4000,8 @@ begin
(Sent < TCI_TX_MARKERS_PER_TICK) do
begin
// ★Номер приёмника — ТОТ, которым назвался клиент в TRX, а не 0.
// Клиент фильтрует ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver (MSHV,
// network.cpp:231: `if (pStream->receiver != tci_trx) return;`), а
// TX-аудио шлёт ровно в ответ на этот маркер (там же, ветка TxChrono).
// Клиент может фильтровать ВХОДЯЩИЕ БИНАРНЫЕ блоки по receiver, а
// TX-аудио отправлять только в ответ на этот маркер.
// С нулём клиент на втором слайсе (tci_trx = 1) поднимал эфир и молчал:
// маркеры до него не доходили вовсе.
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
@@ -4069,8 +4067,8 @@ begin
// ровно то, сколько их и распаковалось. Верим меньшему из двух: клиент
// вправе прислать короткий хвост, но не длиннее уместившегося. Заведомо
// чужое число (мусор в заголовке) просто игнорируем — длину нам и так
// ограничил размер кадра. Так же считает и MSHV, когда сам заполняет
// заголовок: `t_txStream->length = (cr3/bit_s)`, где cr3 — байты блока.
// ограничил размер кадра. Значение заголовка задаётся числом распакованных
// отсчётов, а не числом байт блока.
if H.DataLength > 0 then
begin
Want := Integer(H.DataLength);
@@ -4103,8 +4101,8 @@ begin
// Долгу разрешено уйти в минус — клиент вправе прислать больше, чем
// просили, и зажим в ноль заставил бы переспросить уже полученное.
// Прогресс отмечаем по САМОМУ ФАКТУ валидного блока, независимо от значений
// отсчётов: первые ответы MSHV — законные нули (network.cpp, ветка
// _reset_sta_ <= 5), и сторож не должен считать их отсутствием прогресса.
// отсчётов: первые ответы клиента могут быть законными нулями, и сторож не
// должен считать их отсутствием прогресса.
NowUs := MonotonicUs;
// Границы пачки: клиент отвечает не на каждый маркер по отдельности, а
// очередями по нескольку блоков — по своей внутренней зернистости записи.