Главное: - Опознан 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).
89 строки
3.8 KiB
Python
89 строки
3.8 KiB
Python
#!/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()
|