Баузбенд FM100B: - найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём - расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации - инструмент tools/fm100b.py (ping/send/raw/scan) - cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом) - разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд - cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14 - исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md) Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md): - вызывается как функция горячей клавиши №22 Analog Spectrum - вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318 - спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC Декомпозиция (docs/firmware-parts.md): - два процессора + внешний SPI = три канала внедрения - карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode - найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки) - дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке - аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля Прочее: - везде исправлен режим прошивки: тангента PTT вместо клавиши "*" - устаревший Ru-4D_Flasher.exe удалён из репозитория - добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
112 строки
6.7 KiB
Python
112 строки
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a single-file WebSerial flasher (flasher.html) with the firmware
|
|
embedded. Open in Chrome/Edge, one button flashes the RT-4D."""
|
|
import base64, os
|
|
|
|
# project root = parent of this script's directory (portable)
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
|
|
OUT = f"{ROOT}/dist/Ru-4D_flasher.html"
|
|
|
|
fw = open(FW, "rb").read()
|
|
b64 = base64.b64encode(fw).decode()
|
|
|
|
HTML = """<!doctype html><html lang="ru"><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Ru-4D — Прошивка</title>
|
|
<style>
|
|
:root{--a:#2b7;--b:#0a0e14;--c:#131a24;--t:#dfe7ef;--m:#7f8ea3}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--b);color:var(--t);font:15px/1.5 system-ui,Segoe UI,Roboto,sans-serif;
|
|
min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px}
|
|
.card{background:var(--c);border:1px solid #223;border-radius:16px;max-width:520px;width:100%;
|
|
padding:26px 26px 22px;box-shadow:0 10px 40px #0008}
|
|
h1{margin:0 0 2px;font-size:22px}
|
|
.ver{color:var(--m);font-size:13px;margin-bottom:18px}
|
|
ol{margin:0 0 18px;padding-left:20px;color:#c3cdd9}
|
|
ol b{color:var(--t)}
|
|
button{width:100%;border:0;border-radius:12px;background:var(--a);color:#04120b;font-weight:700;
|
|
font-size:18px;padding:15px;cursor:pointer;transition:.15s}
|
|
button:hover{filter:brightness(1.08)} button:disabled{opacity:.5;cursor:default}
|
|
.bar{height:12px;background:#0c1219;border-radius:8px;overflow:hidden;margin:16px 0 8px;display:none}
|
|
.bar>i{display:block;height:100%;width:0;background:var(--a);transition:.15s}
|
|
#log{white-space:pre-wrap;font:12px/1.5 ui-monospace,Consolas,monospace;color:#9fb0c2;
|
|
background:#0c1219;border-radius:10px;padding:10px 12px;margin-top:12px;max-height:170px;overflow:auto;display:none}
|
|
.ok{color:#3d9} .err{color:#f77}
|
|
.warn{background:#3a2410;border:1px solid #6a3;color:#fc9;border-radius:10px;padding:10px 12px;font-size:13px;margin-bottom:14px;display:none}
|
|
</style></head><body>
|
|
<div class="card">
|
|
<h1>Ru-4D — Прошивка рации</h1>
|
|
<div class="ver" id="ver"></div>
|
|
<div class="warn" id="nowebserial">Откройте этот файл в <b>Google Chrome</b> или <b>Microsoft Edge</b> (нужен WebSerial).</div>
|
|
<ol>
|
|
<li><b>Выключите</b> рацию.</li>
|
|
<li>Включите её, <b>удерживая тангенту PTT</b> (режим прошивки).</li>
|
|
<li>Подключите рацию к ПК кабелем USB-C.</li>
|
|
<li>Нажмите <b>«Прошить»</b> и выберите COM-порт рации.</li>
|
|
<li><b>Не отключайте</b> кабель до конца записи.</li>
|
|
</ol>
|
|
<button id="go">Прошить</button>
|
|
<div class="bar" id="bar"><i></i></div>
|
|
<div id="log"></div>
|
|
</div>
|
|
<script>
|
|
const FW_B64="__B64__";
|
|
const FW_INFO="__INFO__";
|
|
document.getElementById('ver').textContent=FW_INFO;
|
|
const SEED=0x48, ACK=0x06;
|
|
const $=id=>document.getElementById(id);
|
|
const logEl=$('log'), barEl=$('bar'), barI=barEl.querySelector('i'), btn=$('go');
|
|
function log(m,c){logEl.style.display='block';const s=document.createElement('span');if(c)s.className=c;s.textContent=m+"\\n";logEl.appendChild(s);logEl.scrollTop=logEl.scrollHeight;}
|
|
function prog(f){barEl.style.display='block';barI.style.width=Math.round(f*100)+'%';}
|
|
function b64ToBytes(b){const s=atob(b),a=new Uint8Array(s.length);for(let i=0;i<s.length;i++)a[i]=s.charCodeAt(i);return a;}
|
|
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
|
|
function cksum(a){let s=SEED;for(const b of a)s=(s+b)&0xFF;return s;}
|
|
|
|
class Port{
|
|
constructor(p){this.p=p;this.q=[];this.run=false;}
|
|
async open(){await this.p.open({baudRate:115200});this.r=this.p.readable.getReader();this.w=this.p.writable.getWriter();this.run=true;this._loop();}
|
|
async _loop(){try{while(this.run){const{value,done}=await this.r.read();if(done)break;if(value)for(const b of value)this.q.push(b);}}catch(e){}}
|
|
async write(a){await this.w.write(new Uint8Array(a));}
|
|
flush(){this.q.length=0;}
|
|
async readByte(ms){const e=Date.now()+ms;while(Date.now()<e){if(this.q.length)return this.q.shift();await sleep(2);}return null;}
|
|
async close(){this.run=false;try{await this.r.cancel();}catch(e){}try{this.r.releaseLock();this.w.releaseLock();}catch(e){}try{await this.p.close();}catch(e){}}
|
|
}
|
|
|
|
async function doFlash(){
|
|
const fw=b64ToBytes(FW_B64);
|
|
let port;
|
|
try{port=new Port(await navigator.serial.requestPort());}catch(e){log('Порт не выбран.','err');return;}
|
|
try{
|
|
await port.open();
|
|
log('Порт открыт. Синхронизация с бутлоадером...');
|
|
port.flush();let ready=false;const end=Date.now()+7000;
|
|
while(Date.now()<end){const b=await port.readByte(150);if(b===null){await port.write([0xFF]);}else if(b===0xFF){ready=true;break;}}
|
|
if(!ready)throw new Error('Рация не отвечает. Выключите и включите её с зажатой тангентой PTT, затем повторите.');
|
|
log('Бутлоадер найден. Стирание...');
|
|
for(const trig of [0x10,0x55]){const p=[0x39,0x33,0x05,trig];p.push(cksum(p));port.flush();await port.write(p);
|
|
let ok=false;const e2=Date.now()+4000;while(Date.now()<e2){if(await port.readByte(200)===ACK){ok=true;break;}}if(!ok)throw new Error('Ошибка стирания.');}
|
|
log('Запись прошивки...');
|
|
const total=Math.ceil(fw.length/1024)*1024;const data=new Uint8Array(total);data.fill(0xFF);data.set(fw);
|
|
for(let off=0;off<total;off+=1024){const chunk=data.subarray(off,off+1024);
|
|
const p=[0x57,(off>>8)&0xFF,off&0xFF];for(const b of chunk)p.push(b);p.push(cksum(p));
|
|
port.flush();await port.write(p);
|
|
let ok=false;const e3=Date.now()+3000;while(Date.now()<e3){if(await port.readByte(100)===ACK){ok=true;break;}}
|
|
if(!ok)throw new Error('Ошибка записи на 0x'+off.toString(16)+'.');
|
|
prog((off+1024)/total);}
|
|
prog(1);log('✅ Готово! Выключите и включите рацию обычно.','ok');
|
|
}catch(e){log('✖ '+e.message,'err');}
|
|
finally{try{await port.close();}catch(e){}}
|
|
}
|
|
|
|
if(!('serial'in navigator)){$('nowebserial').style.display='block';btn.disabled=true;}
|
|
btn.onclick=async()=>{btn.disabled=true;logEl.innerHTML='';try{await doFlash();}finally{btn.disabled=false;}};
|
|
</script></body></html>"""
|
|
|
|
info = f"Прошивка Ru-4D V3.25 (русифицированная) · {len(fw)} байт"
|
|
html = HTML.replace("__B64__", b64).replace("__INFO__", info)
|
|
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
open(OUT, "w", encoding="utf-8").write(html)
|
|
print(f"firmware {len(fw)} B -> base64 {len(b64)} chars")
|
|
print(f"wrote {OUT} ({len(html)} bytes)")
|