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
+10 -12
View File
@@ -46,27 +46,25 @@ line-out для всех обнаруженных приёмников. Он н
`START`, `STOP`, установку `TRX`/`TUNE`, TX-аудио и CW-текст. Если радио
уже на передаче, прогон аварийно прекращается.
## Стенд «как MSHV»
## Стенд совместимости TCI-клиента
```sh
test/tci/mshv_sim.py --rx 0 # в MSHV это «TCI Client rx1»
test/tci/mshv_sim.py --rx 1 --audio # «TCI Client rx2»
test/tci/mshv_sim.py --rx 0
test/tci/mshv_sim.py --rx 1 --audio
```
`mshv_sim.py` повторяет логику TCI-клиента MSHV по его исходникам — включая обе
особенности, из-за которых он ведёт себя не так, как наш стенд:
`mshv_sim.py` проверяет две особенности поведения совместимых TCI-клиентов:
* **фильтр по номеру приёмника**`if (ls2.at(0)!=tci_trx) continue;`: строка,
адресованная не его приёмнику, для MSHV не существует (и то же самое для
* **фильтр по номеру приёмника**: строка,
адресованная другому приёмнику, игнорируется (и то же самое для
бинарных блоков, по полю `receiver` заголовка);
* **инициализация — один запрос и один ответ**: он шлёт `vfo:<rx>,0;` раз в
* **инициализация — один запрос и один ответ**: клиент шлёт `vfo:<rx>,0;` раз в
500 мс, максимум пять раз, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
«Error To Initialize TCI Server». Других причин у этой ошибки нет.
Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а
заодно ловит то, что иначе проявляется молча: без `tx_enable` со **своим**
номером приёмника у MSHV не работает PTT (`set_ptt` начинается с
`if (!tci_tx_enable) return;`).
Поэтому стенд диагностирует причину ошибки подключения одной строкой, а заодно
ловит то, что иначе проявляется молча: без `tx_enable` со **своим** номером
приёмника клиент не разрешает PTT.
По умолчанию стенд безопасен на приём: ни `TRX`, ни `TUNE`, ни `START`/`STOP`,
ни TX-аудио он не шлёт. Передачу включает явный `--tx`.
+10 -13
View File
@@ -1,22 +1,19 @@
#!/usr/bin/env python3
"""Стенд «как MSHV»: повторяет TCI-клиента MSHV по его исходникам и говорит,
почему он не подключается.
"""Стенд совместимости с TCI-клиентом MSHV.
Зачем: MSHV — самый распространённый TCI-клиент для цифры, и ведёт он себя не
так, как наш собственный стенд. Две его особенности определяют всё:
1. ВСЕ входящие строки он фильтрует по номеру приёмника:
if (ls2.at(0)!=tci_trx) continue; // network.cpp
то есть клиенту в режиме RX2 (tci_trx = "1") строка `trx:0,false`
1. Входящие строки фильтруются по номеру приёмника, поэтому клиенту в режиме
RX2 (tci_trx = "1") строка `trx:0,false`
не существует в природе.
2. Инициализация — это ожидание ОДНОГО ответа. Он шлёт `vfo:<rx>,0;` раз в
500 мс, максимум 5 попыток, и ждёт `vfo:<rx>,0,<частота>`. Не дождался —
«Error To Initialize TCI Server» (network.cpp, Network::initAll,
ветка s_ModelID==3||4). Никаких других причин у этой ошибки нет.
сообщает об ошибке инициализации.
Соответствие настроек MSHV: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
приёмник 1 (hvrigcontrol.cpp: netServPort[3]/[4], network.cpp: tci_trx).
Соответствие настроек: «TCI Client rx1» → приёмник 0, «TCI Client rx2» →
приёмник 1.
Стенд по умолчанию БЕЗОПАСЕН на приём: ни TRX, ни TUNE, ни START/STOP, ни
TX-аудио не отправляются. Передающую часть включает явный --tx.
@@ -38,8 +35,8 @@ from live_rx_test import WS, split_commands, parse_command # noqa: E402
HDR = struct.Struct("<16I")
# Умолчания MSHV из его же диалога настроек (hvrigcontrol.cpp): каналов 2,
# сэмплов 2048, тип float32, частота 48 кГц, буфер TX 24 мс.
# Параметры совместимого клиента: 2 канала, 2048 сэмплов, float32,
# частота 48 кГц и буфер TX 24 мс.
MSHV_CHANNELS = "2"
MSHV_SAMPLES = "2048"
MSHV_TYPE = "float32"
@@ -65,7 +62,7 @@ class Verdict:
class MshvClient:
"""Ровно та логика, что в Network::* у MSHV — включая её ограничения."""
"""Минимальная модель поведения совместимого TCI-клиента."""
def __init__(self, ws, rx, verdict):
self.ws = ws
@@ -148,7 +145,7 @@ class MshvClient:
if len(payload) < HDR.size:
return
head = HDR.unpack(payload[:HDR.size])
# MSHV: if (pStream->receiver != tci_trx) return;
# Блоки других приёмников клиент игнорирует.
if head[0] != int(self.rx):
self.audio_foreign += 1
return
+15 -19
View File
@@ -121,7 +121,7 @@ begin
Check('header rate', H.SampleRate = 12000);
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на
// считают по нему число байт. Объявишь «на
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
Check('header length аудио = ×каналы', H.DataLength = 1024);
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
@@ -484,7 +484,7 @@ begin
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
(H.Format <> LongWord(Ord(tsyFloat32))) or
// ★Правило, по которому живут клиенты: байт данных = length × размер
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`).
// отсчёта. Так клиент определяет размер полезной нагрузки.
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
Inc(Blocks);
end;
@@ -2054,7 +2054,7 @@ begin
end;
C.WaitText('ready;', 2000);
// Клиент назвался ровно как MSHV: 48 кГц, два канала, блок 2048.
// Клиент запросил 48 кГц, два канала и блок 2048.
C.SendText('audio_samplerate:48000;');
C.SendText('audio_stream_channels:2;');
C.SendText('audio_stream_samples:2048;');
@@ -2174,7 +2174,7 @@ begin
// идти дальше, а не прекратиться до конца передачи.
// ★ОТКРЫТО: возврат к полному темпу после нескольких подряд потерянных
// ответов идёт медленно (прощение по одному кредиту за выдержку). В эфире
// это редкость (на живом MSHV — два неотвеченных маркера за 12 с), но
// это редкость, но
// строка ниже печатает просадку, чтобы регресс был виден.
WriteLn(Format(' .. фаза 3: возврат к темпу пока неполный — %d маркеров ' +
'из ~94/с, просадка %d кадров', [Markers, MinReserve]));
@@ -2182,8 +2182,8 @@ begin
Markers > 40, IntToStr(Markers));
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
// ★Регресс с железа. Штатный конец посылки (`trx:N,false`)
// обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
// миллисекунд против шага тика в полкванта, так что промах выпадал через
@@ -2586,19 +2586,17 @@ begin
// Ровно случай Pluto: панов больше одного там не бывает, и «второй
// приёмник» существует только как слайс. Приёмник = слот слайса, поэтому
// первый созданный слайс (слот 0, буква B) обязан стать приёмником 1 —
// единственным номером сверх нулевого, который умеют клиенты вроде MSHV.
// первым номером приёмника после нулевого.
SliceId := Ctrl.AddSlice(Ctrl.FCenterFreq + 3000, MODE_USB, 200, 2800,
agcMedium, 0.5, -1, '', 0);
Check('слайс на главном пане создан', SliceId > 0, IntToStr(SliceId));
C.Pump(300);
while C.NextFrame(Op, Pay) do ; // выгребаем рассылку о появлении
// Инициализация MSHV — это ровно один запрос и ровно один ответ
// (network.cpp, Network::initAll: `vfo:<rx>,0;` пять раз, потом
// «Error To Initialize TCI Server»).
// Инициализация совместимого клиента — это один запрос и один ответ.
C.SendText('vfo:1,0;');
S := C.WaitText('vfo:1,0,', 1500);
Check('слайс отвечает на vfo:1,0 (инициализация MSHV)', S <> '', S);
Check('слайс отвечает на vfo:1,0 при инициализации', S <> '', S);
Check('слайс отдаёт свою частоту',
Pos(IntToStr(Round(Ctrl.FCenterFreq + 3000)), S) > 0, S);
@@ -2611,7 +2609,7 @@ begin
FloatToStr(SV.TargetHz));
// Аудиопоток приёмника 1 — это звук слайса, и в заголовке стоит его номер
// (MSHV отбрасывает блоки с чужим receiver: network.cpp:231).
// Клиенты могут отбрасывать блоки с чужим receiver.
C.SendText('audio_start:1;');
C.Pump(100);
for k := 0 to 40 do
@@ -2691,13 +2689,11 @@ begin
while C.NextFrame(Op, Pay) do ;
// ── ★TX со слайса: маркер обязан нести НОМЕР ЭТОГО приёмника ─────────
// MSHV шлёт TX-аудио только в ответ на маркер TX_CHRONO и отбрасывает
// ЛЮБОЙ входящий блок с чужим receiver (network.cpp:231 — `if
// (pStream->receiver != tci_trx) return;`, ветка TxChrono — network.cpp:288
// и далее). С жёстким нулём в заголовке клиент, сидящий на втором слайсе
// Совместимый клиент шлёт TX-аудио только в ответ на маркер TX_CHRONO и
// может отбрасывать входящий блок с чужим receiver. С жёстким нулём в
// заголовке клиент, сидящий на втором слайсе
// (tci_trx = 1), поднимал эфир и молчал: маркеры до него не доходили, а
// без них он не отправляет ни одного блока. Ровно это и наблюдалось на
// живом железе с MSHV.
// без них он не отправляет ни одного блока.
Ctrl.FWDSPReady := True;
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), True);
C.SendText('trx:1,true,tci;');
@@ -2717,7 +2713,7 @@ begin
end;
end;
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
Check('TX слайса: маркер назван номером приёмника',
(n > 0) and (BadRx = 0), IntToStr(BadRx));
// ★Команда, которая ничего не сделала, не должна перебивать пару