FM100B = Sicomm SCT3258TD (документирован); Talker Alias, агент, SPI-инструменты, восстановление TX

Главное:
- Опознан DMR-чип: FM100B = Sicomm SCT3258TD. Датащит Packet Interface (126 стр) в docs/.
  Открывает калибровку/AMBE/вокодер по документации. Родня: UV-4R, китайские клоны.

Реализовано:
- Приём Talker Alias (cmd 0x60) — позывной крупно на экране (build_ta.py)
- Интерактивный агент — управление FM100B по COM без перешивки (build_agent.py + agent.py)
- Полная карта команд FM100B с живой рации (fm100b-commands.md)
- Работа с SPI: чтение/запись/восстановление codeplug и калибровки (spi_read/spi_wr/spi_restore)
- Конструктор дисплея + редактор прошивки (tools/display-editor)

Документация:
- docs/SCT3258_packet_interface.txt/.pdf — спека DMR-чипа
- docs/fm100b-commands.md — карта команд
- docs/dmr-tx-recovery.md — диагноз/восстановление DMR-передачи

Инструменты: flash_ps.py (прошивка через WSL/powershell), sniff.py (снифер кадров),
hotspot_rxoffset_sweep.py, apply_patch.py, build_* сборщики.

Важно: DMR TX-калибровка сбита в экспериментах со SET-командами модулю; восстановление
через offset-компенсацию / .updat Radtel / калибровку по прибору (docs/dmr-tx-recovery.md).
Этот коммит содержится в:
2026-07-24 02:52:51 +09:00
родитель 5f4d207aa5
Коммит 6d49ba3cba
21 изменённых файлов: 11662 добавлений и 0 удалений
+88
Просмотреть файл
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Подбор RXOffset хотспота под смещённую TX-частоту рации (без прибора).
Идея: TX рации смещена на постоянный X Гц (дефолтная калибровка). Двигаем приёмное
смещение хотспота (RXOffset) и при передаче рации (PTT) ищем в логе MMDVMHost
«received RF» — значит offset поймал передачу. По BER выбираем лучший.
Запуск на самом хотспоте (или по SSH). Владелец УДЕРЖИВАЕТ PTT на DMR-канале во время
каждого шага (окно ~6 с). Меняет RXOffset в MMDVM.ini, рестартит MMDVMHost, читает лог.
python3 hotspot_rxoffset_sweep.py --range 1000 --step 100 --dwell 7
Требует sudo (запись MMDVM.ini + restart). Значения offset в Гц.
"""
import argparse, subprocess, re, time, sys
INI = "/opt/MMDVMHost/MMDVM.ini"
SVC = "radiostar-mmdvmhost"
def set_rxoffset(val):
# заменить RXOffset= в секции [Modem], сохранив регистр
sub = subprocess.run(
["sudo","sed","-i",f"s/^RXOffset=.*/RXOffset={val}/",INI],
capture_output=True, text=True)
return sub.returncode == 0
def restart():
subprocess.run(["sudo","systemctl","restart",SVC],capture_output=True)
def watch_rf(dwell):
"""слушать журнал dwell секунд, вернуть (got_rf, ber)"""
t0 = time.time()
got = False; ber = None
proc = subprocess.Popen(
["sudo","journalctl","-u",SVC,"-f","--since","now","-o","cat"],
stdout=subprocess.PIPE, text=True)
try:
while time.time() - t0 < dwell:
line = proc.stdout.readline()
if not line: continue
if "received RF" in line or "RF voice" in line or "RF late entry" in line:
got = True
m = re.search(r"BER:\s*([\d.]+)", line)
if m: ber = float(m.group(1))
if "BER:" in line and "RF" in line:
m = re.search(r"BER:\s*([\d.]+)", line)
if m: ber = float(m.group(1))
finally:
proc.terminate()
return got, ber
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--range", type=int, default=1000, help="± Гц")
ap.add_argument("--step", type=int, default=100)
ap.add_argument("--dwell", type=int, default=7)
a = ap.parse_args()
offsets = list(range(-a.range, a.range+1, a.step))
# порядок: от центра наружу
offsets.sort(key=lambda x: abs(x))
print(f"перебор RXOffset {offsets[0]}..{offsets[-1]} Гц, шаг {a.step}, окно {a.dwell}с")
print("УДЕРЖИВАЙ PTT на DMR-канале рации во время КАЖДОГО шага!\n")
best = None
for off in offsets:
set_rxoffset(off); restart(); time.sleep(2)
print(f" RXOffset={off:+5d} Гц … жми PTT", flush=True)
got, ber = watch_rf(a.dwell)
if got:
print(f" ✅ ПОЙМАНО RF! BER={ber}")
if best is None or (ber is not None and ber < best[1]):
best = (off, ber if ber is not None else 99)
if ber is not None and ber < 3:
print(f" отличный BER — offset {off} рабочий, стоп")
break
if best:
set_rxoffset(best[0]); restart()
print(f"\nЛУЧШИЙ RXOffset={best[0]} Гц (BER {best[1]}) — записан и применён")
else:
print("\nне поймано — TX смещена сильнее диапазона или проблема в девиации, не частоте")
if __name__ == "__main__":
main()