#!/usr/bin/env python3 """RT-4D DMR-модуль (HF6853 / AUCTUS A6) — починка частотного смещения TX. Модуль RT-4D — это одночиповый Auctus A6 (RDA8809), у которого частотная калибровка TCXO хранится в NVRAM и правится СКРЫТЫМ ATE-интерфейсом: AT+GETFREQERR -> прочитать N (в десятках Гц) AT+DMOFREQERR= -> записать N; offset[Гц] = -2500 + 10*N, N=0..500 После записи СМЕНИТЬ КАНАЛ на рации, чтобы применилось. Наш случай ([[rt4d-dmr-tx-broken]]): TX смещена ~1100 Гц (в пределах ±2500), поэтому один AT+DMOFREQERR должен вернуть декодирование хотспотом. Реверс/библиотека — jhart99/a6tools (копия в research/a6tools). См. docs/hf6853-auctus-a6.md. БЕЗОПАСНОСТЬ: * По умолчанию РЕЖИМ probe/read (--probe / --read). Запись только с --write И --yes. * Точное значение требует ИЗМЕРЕНИЯ реальной TX по SDR (RTL-SDR). * Рация боевая — вслепую не писать. Транспорт: два варианта COM-доступа — --port /dev/ttyUSBx прямой pyserial (если модуль виден напрямую) --bridge через powershell-мост WSL (как tools/fm100b.py) — TODO, заготовка ниже; сперва проверить probe напрямую. Скорость Auctus по умолчанию 921600 8N1 XON/XOFF. Режим рации: «Обновление DMR чипа» (вкл. с зажатой MENH/MENU) — прозрачный мост PC<->модуль. Примеры: python3 a6_freqfix.py --port /dev/ttyUSB0 --probe python3 a6_freqfix.py --port /dev/ttyUSB0 --read # измерили: рация реально шлёт 438.799100 вместо 438.800000 (=-900 Гц) python3 a6_freqfix.py --port /dev/ttyUSB0 --measured 438799100 --nominal 438800000 --write --yes """ import sys, os, time, argparse # использовать вендоренную библиотеку a6tools HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "research", "a6tools")) def load_a6(port, baud, verbosity): from a6.serialio import (SerialIO, send_ate_command, atecps_resp_read, send_uart_setup, get_freq_err, set_freq_err) from a6.a6commands import set_uart_to_host from a6.serialio import write_flush_pause SerialIO(port, baud, verbosity) # singleton init return dict(send_ate=send_ate_command, resp=atecps_resp_read, setup=send_uart_setup, geterr=get_freq_err, seterr=set_freq_err, to_host=set_uart_to_host, wfp=write_flush_pause) def probe(a6): """Проверить, отвечает ли модуль на RDA-debug (knock). content==0x80 => ок.""" print("[probe] knock RDA-debug (read reg 0x03)…") ok = a6["setup"]() print(" ответ:", "МОДУЛЬ ОТВЕЧАЕТ (debug активен)" if ok else "нет ответа") if not ok: print(" -> модуль не в RDA-debug. Проверь режим рации / скорость (пробуй --baud 115200).") return ok def read_err(a6): a6["send_ate"]("AT+DMOCONNECT") n = a6["geterr"]() print(f"[read] GETFREQERR N (в получаемом виде) -> offset={n} Гц") return n def main(): ap = argparse.ArgumentParser(description="HF6853/A6 TX freq-offset fix") ap.add_argument("--port", required=True, help="COM/tty (напр. /dev/ttyUSB0)") ap.add_argument("--baud", type=int, default=921600) ap.add_argument("-v", "--verbose", action="count", default=1) ap.add_argument("--probe", action="store_true", help="только проверить связь") ap.add_argument("--read", action="store_true", help="прочитать текущий freqerr") ap.add_argument("--measured", type=int, help="реально измеренная TX-частота, Гц (по SDR)") ap.add_argument("--nominal", type=int, help="номинальная (запрограммированная) частота, Гц") ap.add_argument("--write", action="store_true", help="применить новое значение") ap.add_argument("--yes", action="store_true", help="подтвердить запись (обязателен с --write)") a = ap.parse_args() a6 = load_a6(a.port, a.baud, a.verbose) if not probe(a6): if a.probe: return print("Прерываю: без связи писать нельзя.") return if a.probe: return cur = read_err(a6) if a.read and not a.write: return if a.write: if a.measured is None or a.nominal is None: print("Для --write нужны --measured и --nominal (по SDR).") return delta = a.nominal - a.measured # сколько добавить к текущему offset target = cur + delta if abs(target) > 2500: print(f"Целевой offset {target} Гц вне ±2500 — так фиксить нельзя (нужна другая калибровка).") return N = int((target + 2500) / 10) print(f"[plan] cur={cur} Гц, delta={delta} Гц, target={target} Гц -> N={N}") if not a.yes: print("Добавь --yes чтобы записать. (ничего не записано)") return a6["send_ate"]("AT+DMOCONNECT") a6["seterr"](N) print(f"[write] AT+DMOFREQERR={N} отправлено. ТЕПЕРЬ СМЕНИ КАНАЛ на рации и проверь BER.") if __name__ == "__main__": main()