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