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).
Этот коммит содержится в:
@@ -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()
|
||||
Ссылка в новой задаче
Block a user