Files
rt-4d/tools/spi_restore.py
T
viktor 6d49ba3cba 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).
2026-07-24 02:52:51 +09:00

118 строки
4.9 KiB
Python

#!/usr/bin/env python3
"""Полное восстановление SPI рации из заводского эталона (radio-spi-dump.bin, до экспериментов).
Пишет все регионы codeplug/калибровки CPS-протоколом (USART6) из WSL через powershell.exe.
Рация — в ОБЫЧНОМ режиме. Возвращает рацию к состоянию на момент дампа.
Регионы (region_id, address, size) — из rt4d-cps constants.
"""
import argparse, subprocess, sys, os, json
WIN_TMP_WSL = "/mnt/c/Users/vikto/AppData/Local/Temp"
WIN_TMP_WIN = r"C:\Users\vikto\AppData\Local\Temp"
REF = "radio-spi-dump.bin"
REGIONS = [
("calibration", 0x40, 0x000000, 0x001000),
("main_settings", 0x90, 0x002000, 0x001000),
("channels", 0x91, 0x004000, 0x00C000),
("zones", 0x92, 0x01C000, 0x020000),
("contacts", 0x93, 0x05C000, 0x010000),
("groups", 0x94, 0x07C000, 0x003000),
("dmr_keys", 0x95, 0x082000, 0x003000),
("call_log", 0x96, 0x088000, 0x00C000),
("default_sms", 0x97, 0x094000, 0x001000),
("schedules", 0x98, 0x0C6000, 0x008000),
("fm_settings", 0x99, 0x0D6000, 0x001000),
]
PS = r'''
$ErrorActionPreference='Stop'
$data=[System.IO.File]::ReadAllBytes("{datawin}")
$plan=ConvertFrom-Json '{planjson}'
$p=New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
$p.ReadTimeout=1500; $p.WriteTimeout=3000
try {{ $p.Open() }} catch {{ Write-Output "ERR:openfail"; exit 1 }}
function ck([byte[]]$a,$n){{ $s=0; for($i=0;$i -lt $n;$i++){{ $s=($s+$a[$i]) -band 0xFF }}; return [byte]$s }}
$p.DiscardInBuffer()
$nf=[byte[]](0x34,0x00,0x00,0x10,0x44); $p.Write($nf,0,5)
$ok=$false; $sw=[Diagnostics.Stopwatch]::StartNew()
while($sw.ElapsedMilliseconds -lt 3000){{ try {{ if($p.ReadByte() -eq 6){{ $ok=$true; break }} }} catch {{}} }}
if(-not $ok){{ Write-Output "ERR:notify"; $p.Close(); exit 1 }}
Write-Output "INFO:notify ok"
$doff=0
foreach($r in $plan){{
$region=[int]$r.region; $size=[int]$r.size; $name=$r.name
Write-Output ("REGION:"+$name)
for($i=0; $i -lt $size; $i+=1024){{
$cmd=New-Object byte[] 1028
$cmd[0]=$region
$blk=[int]($i/1024)
$cmd[1]=(($blk -shr 8) -band 0xFF); $cmd[2]=($blk -band 0xFF)
[Array]::Copy($data,$doff+$i,$cmd,3,1024)
$cmd[1027]=ck $cmd 1027
$p.DiscardInBuffer(); $p.Write($cmd,0,1028)
$ok=$false; $sw2=[Diagnostics.Stopwatch]::StartNew()
while($sw2.ElapsedMilliseconds -lt 3000){{ try {{ if($p.ReadByte() -eq 6){{ $ok=$true; break }} }} catch {{}} }}
if(-not $ok){{ Write-Output ("ERR:"+$name+"@"+$blk); $p.Close(); exit 1 }}
}}
$doff += $size
}}
$cl=[byte[]](0x34,0x52,0x05,0xEE,0x79); $p.Write($cl,0,5)
$p.Close()
Write-Output "OK"
'''
def find_port():
out = subprocess.run(["powershell.exe","-NoProfile","-Command",
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
capture_output=True, text=True).stdout
ports=[p for p in out.strip().replace("\r","").split(",") if p]
if not ports: sys.exit("нет COM-порта")
return ports[0]
def main():
ap=argparse.ArgumentParser()
ap.add_argument("--only", help="только регионы через запятую (напр. calibration,main_settings)")
ap.add_argument("--port")
a=ap.parse_args()
ref=open(REF,"rb").read()
sel=REGIONS
if a.only:
want=set(a.only.split(","))
sel=[r for r in REGIONS if r[0] in want]
total=sum(r[3] for r in sel)
print("восстановление SPI из эталона (до экспериментов):")
for n,rid,addr,sz in sel:
print(f" {n:14s} region 0x{rid:02X} 0x{addr:06X} {sz//1024:4d} КБ")
print(f" ИТОГО {total//1024} КБ, ~{total//1024} блоков")
# собрать данные подряд в порядке регионов
blob=b"".join(ref[addr:addr+sz] for _,_,addr,sz in sel)
name="rt4d_restore.bin"
open(os.path.join(WIN_TMP_WSL,name),"wb").write(blob)
datawin=os.path.join(WIN_TMP_WIN,name)
plan=[{"name":n,"region":rid,"size":sz} for n,rid,addr,sz in sel]
port=a.port or find_port()
script=PS.format(datawin=datawin.replace("\\","\\\\"),port=port,
planjson=json.dumps(plan).replace('"','\\"'))
print()
proc=subprocess.Popen(["powershell.exe","-NoProfile","-Command",script],
stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,encoding="utf-8",errors="replace")
rc=0
for line in proc.stdout:
line=line.strip()
if line.startswith("REGION:"): print(f" → {line[7:]}")
elif line.startswith("INFO:"): print(f" {line[5:]}")
elif line.startswith("ERR:"): print(f" ОШИБКА: {line[4:]}"); rc=1
elif line=="OK": print(" ✅ SPI восстановлен из эталона")
proc.wait()
sys.exit(rc)
if __name__=="__main__":
main()