#!/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()