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,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()
|
||||
Ссылка в новой задаче
Block a user