Files
viktorиClaude Opus 4.8 7c6c2dfea7 Реверс прошивки модуля FM100B: архитектура + что такое вокодер
docs/fm100b-firmware-re.md: образ = ARM(старт)+Thumb-2/FPU, 1.5МБ, без символов,
база RAM ~0x0360xxxx. Карта регионов. КЛЮЧЕВОЕ: «Vocoder» в прошивке = WebRTC
audio (шумодав NSX + AGC, открытый код), а НЕ AMBE. Сам AMBE — вне ARM-образа
(DSP/ROM A6), в прошивке только интерфейс ATVoiceEnc/Dec + RecvAmbedata. Найдены
open-source либы: EasyFlash (NV/калибровка), WebRTC (аудио). Честная оценка: свой
вокодер для эфирного DMR невозможен (нет AMBE-совместимости); реально моддить —
WebRTC-параметры, кодек-гейны, NV-калибровку, логику DMR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NikMGoqQWWk9wy2ww2vJAr
2026-07-24 04:28:45 +09:00

123 строки
5.3 KiB
Python

#!/usr/bin/env python3
"""RT-4D: РИД-ОНЛИ снятие калибровки DMR-модуля протоколом штатного тюнера.
Реплика последовательности Radio Tuning Software (реверс, docs/tuning-software-protocol.md):
одна сессия COM @115200, шлём кадры и ловим ответы. ТОЛЬКО ЧТЕНИЕ — ни одной write/set-команды
калибровки (CmdWriteAll/Set* НЕ шлём). Рация в режиме «Обновление DMR чипа».
Под WSL COM через powershell.exe. Порядок:
CmdConnect(34 52 05 10 9b) -> [опц. CmdOpenCal 68 b0..01] -> Read* (68 b?..) -> CmdColseCal -> CmdEnd
"""
import argparse, subprocess, sys, json
# --- команды (из реверса, research/tuning-re/command-templates.txt) ---
CMDS = {
"Ping": "68 84 01 01 00 00 00 01 00 10",
"CmdConnect": "34 52 05 10 9b",
"CmdOpenCal": "68 b0 01 01 00 00 00 01 01 10",
"ReadDigVCCN": "68 b4 01 01 00 00 00 02 00 02 10",
"ReadAnaVCCN": "68 b2 01 01 00 00 00 02 00 02 10",
"ReadMod": "68 b6 01 01 00 00 00 03 00 01 00 10",
"ReadPWR": "68 b8 01 01 00 00 00 04 00 01 01 00 10",
"ReadSlot": "68 b7 01 01 00 00 00 01 00 10",
"CmdColseCal": "68 b0 01 01 00 00 00 01 00 10",
"CmdEnd": "34 52 05 ee 79",
}
def hx(name):
if name.startswith("hex:"):
return bytes.fromhex(name[4:].replace(" ", ""))
return bytes.fromhex(CMDS[name].replace(" ", ""))
# powershell: open port, run a sequence of (write, wait) steps, capture each response
PS = r'''
$ErrorActionPreference = "Stop"
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
$p.ReadTimeout = 60
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
$p.DiscardInBuffer(); $p.DiscardOutBuffer()
$steps = New-Object System.Collections.ArrayList
{steps}
foreach ($s in $steps) {{
$name = $s[0]; $tx = [byte[]]$s[1]
$p.DiscardInBuffer()
$p.Write($tx,0,$tx.Length)
Start-Sleep -Milliseconds {dwell}
$buf = New-Object System.Collections.Generic.List[byte]
$sw=[Diagnostics.Stopwatch]::StartNew(); $last=0
while ($sw.ElapsedMilliseconds -lt {tmo}) {{
try {{ $buf.Add($p.ReadByte()); $last=$sw.ElapsedMilliseconds }} catch {{}}
if ($buf.Count -gt 0 -and ($sw.ElapsedMilliseconds-$last) -gt {idle}) {{ break }}
}}
$hex = ($buf | ForEach-Object {{ "{{0:x2}}" -f $_ }}) -join ""
Write-Output ("RSP|"+$name+"|"+$hex)
}}
$p.Close()
Write-Output "DONE"
'''
def find_port():
out = subprocess.run(["powershell.exe","-NoProfile","-Command",
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
capture_output=True, text=True).stdout.strip().replace("\r","")
ports=[p for p in out.split(",") if p]
if not ports: sys.exit("COM не найден.")
return ports[0]
def ps_arr(b): return "[byte[]]("+",".join(str(x) for x in b)+")"
def run(port, seq, dwell=120, tmo=700, idle=200):
steps = "\n".join(f"$null = $steps.Add(@('{n}',{ps_arr(hx(n))}))" for n in seq)
script = PS.format(port=port, steps=steps, dwell=dwell, tmo=tmo, idle=idle)
r = subprocess.run(["powershell.exe","-NoProfile","-Command",script],
capture_output=True, text=True)
out=r.stdout.replace("\r","")
res=[]
for line in out.split("\n"):
if line.startswith("ERR:"): sys.exit(line)
if line.startswith("RSP|"):
_,name,hexs=line.split("|",2)
res.append((name, bytes.fromhex(hexs) if hexs else b""))
return res
def parse68(rx):
if len(rx)<9 or rx[0]!=0x68: return None
cmd=rx[1]; ln=(rx[6]<<8)|rx[7]; pl=rx[8:8+ln]
return dict(cmd=cmd, status=rx[3], length=ln, payload=pl)
def main():
ap=argparse.ArgumentParser()
ap.add_argument("--port")
ap.add_argument("--connect-only", action="store_true", help="только CmdConnect (проверка отклика)")
ap.add_argument("--direct", action="store_true", help="без 34.. Connect: 0x68 напрямую в модуль (режим Обновление DMR)")
ap.add_argument("--seq", help="произвольная последовательность имён через запятую")
a=ap.parse_args()
port=a.port or find_port()
print(f"[порт {port} @115200] РИД-ОНЛИ снятие калибровки\n")
if a.seq:
seq=[s.strip() for s in a.seq.split(",")]
elif a.connect_only:
seq=["CmdConnect"]
elif a.direct:
seq=["Ping","CmdOpenCal","ReadDigVCCN","ReadAnaVCCN","ReadMod","ReadPWR",
"ReadSlot","CmdColseCal"]
else:
seq=["CmdConnect","CmdOpenCal","ReadDigVCCN","ReadAnaVCCN","ReadMod","ReadPWR",
"ReadSlot","CmdColseCal","CmdEnd"]
res=run(port, seq)
if not res:
print("Нет ответов от рации (powershell не вернул RSP). Порт/режим?")
return
for name,rx in res:
tag=f" {name:14s} TX={CMDS.get(name, name[4:] if name.startswith('hex:') else name)}"
if not rx:
print(tag+" RX=(нет ответа)")
else:
p=parse68(rx)
extra=f" [cmd=0x{p['cmd']:02X} st=0x{p['status']:02X} len={p['length']} pl={p['payload'].hex(' ')}]" if p else ""
print(tag+f" RX={rx.hex(' ')}"+extra)
if __name__=="__main__":
main()