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
родитель 5f4d207aa5
Коммит 6d49ba3cba
21 изменённых файлов: 11662 добавлений и 0 удалений
+130
Просмотреть файл
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Прошивка RT-4D из WSL через powershell.exe (WSL2 не отдаёт COM напрямую).
python3 tools/flash_ps.py stock-fw/rt4d_ru_batt.bin [--port COM4]
Рация должна быть в режиме прошивки: выключить -> включить, удерживая тангенту PTT.
Протокол бутлоадера (тот же, что в flash_rt4d.py):
рукопожатие : слать 0xFF, пока не придёт 0xFF
стирание : [39 33 05 10]+CK, затем [39 33 05 55]+CK -> ACK 0x06
запись : [57 hi lo] + 1024 Б + CK -> ACK 0x06
CK = (0x48 + сумма) & 0xFF
"""
import argparse, os, shutil, subprocess, sys
WIN_TMP_WSL = "/mnt/c/Users/vikto/AppData/Local/Temp"
WIN_TMP_WIN = r"C:\Users\vikto\AppData\Local\Temp"
PS = r'''
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$fw = [System.IO.File]::ReadAllBytes("{fwpath}")
$p = New-Object System.IO.Ports.SerialPort "{port}",115200,"None",8,"one"
$p.ReadTimeout = 250
$p.WriteTimeout = 5000
try {{ $p.Open() }} catch {{ Write-Output "ERR: порт занят или недоступен: $($_.Exception.Message)"; exit 1 }}
Write-Output "INFO: прошивка $($fw.Length) байт, порт {port}"
# --- рукопожатие ---
$p.DiscardInBuffer()
$ready = $false
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.ElapsedMilliseconds -lt 8000) {{
try {{ if ($p.ReadByte() -eq 0xFF) {{ $ready = $true; break }} }}
catch {{ $p.Write([byte[]]@(0xFF), 0, 1) }}
}}
if (-not $ready) {{
Write-Output "ERR: бутлоадер не отвечает. Выключите рацию и включите с зажатой тангентой PTT."
$p.Close(); exit 1
}}
Write-Output "INFO: бутлоадер найден"
# --- стирание ---
foreach ($trig in 0x10, 0x55) {{
$pl = [byte[]]@(0x39, 0x33, 0x05, $trig, 0)
$s = 0x48; for ($i = 0; $i -lt 4; $i++) {{ $s = ($s + $pl[$i]) -band 0xFF }}
$pl[4] = [byte]$s
$p.DiscardInBuffer(); $p.Write($pl, 0, 5)
$ok = $false; $sw2 = [Diagnostics.Stopwatch]::StartNew()
while ($sw2.ElapsedMilliseconds -lt 6000) {{ try {{ if ($p.ReadByte() -eq 6) {{ $ok = $true; break }} }} catch {{}} }}
if (-not $ok) {{ Write-Output "ERR: ошибка стирания"; $p.Close(); exit 1 }}
}}
Write-Output "INFO: область приложения стёрта"
# --- дополнение до кратности 1024 ---
$pad = (1024 - ($fw.Length % 1024)) % 1024
if ($pad -gt 0) {{
$t = New-Object byte[] ($fw.Length + $pad)
[Array]::Copy($fw, $t, $fw.Length)
for ($i = $fw.Length; $i -lt $t.Length; $i++) {{ $t[$i] = 0xFF }}
$fw = $t
}}
$total = $fw.Length
$blocks = $total / 1024
# --- запись ---
$pl = New-Object byte[] 1028
for ($off = 0; $off -lt $total; $off += 1024) {{
$pl[0] = 0x57
$pl[1] = [byte](($off -shr 8) -band 0xFF)
$pl[2] = [byte]($off -band 0xFF)
[Array]::Copy($fw, $off, $pl, 3, 1024)
$s = 0x48
for ($i = 0; $i -lt 1027; $i++) {{ $s = ($s + $pl[$i]) -band 0xFF }}
$pl[1027] = [byte]$s
$p.DiscardInBuffer(); $p.Write($pl, 0, 1028)
$ok = $false; $sw3 = [Diagnostics.Stopwatch]::StartNew()
while ($sw3.ElapsedMilliseconds -lt 4000) {{ try {{ if ($p.ReadByte() -eq 6) {{ $ok = $true; break }} }} catch {{}} }}
if (-not $ok) {{ Write-Output ("ERR: нет ACK на блоке 0x{{0:X5}}" -f $off); $p.Close(); exit 1 }}
$n = ($off / 1024) + 1
if (($n % 16) -eq 0 -or $n -eq $blocks) {{ Write-Output ("PROG: {{0}}/{{1}} блоков" -f $n, $blocks) }}
}}
$p.Close()
Write-Output "OK: записано. Выключите и включите рацию обычным образом."
'''
def find_port() -> str:
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("firmware")
ap.add_argument("--port")
a = ap.parse_args()
fw = os.path.abspath(a.firmware)
if not os.path.exists(fw):
sys.exit(f"нет файла: {fw}")
name = "rt4d_flash_tmp.bin"
shutil.copy(fw, os.path.join(WIN_TMP_WSL, name))
win_fw = os.path.join(WIN_TMP_WIN, name)
port = a.port or find_port()
script = PS.format(fwpath=win_fw.replace("\\", "\\\\"), port=port)
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.rstrip("\r\n")
if line:
print(line, flush=True)
if line.startswith("ERR:"):
rc = 1
proc.wait()
sys.exit(rc or proc.returncode)
if __name__ == "__main__":
main()