Главное: - Опознан 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).
88 строки
3.3 KiB
Python
88 строки
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Применить патч из редактора прошивки к образу.
|
|
|
|
python3 apply_patch.py patch.json [--src stock-fw/rt4d_ru_batt.bin]
|
|
[--out stock-fw/rt4d_custom.bin]
|
|
|
|
Формат патча (его выдаёт кнопка «Скопировать патч»):
|
|
{"texts":[{"va":134370285,"txt":"ОСН. НАСТР"}, ...],
|
|
"logo":{"va":134500045,"b64":"..."}}
|
|
|
|
Текст пишется в CP1251 и добивается пробелами ровно до исходной ширины поля —
|
|
записи в прошивке фиксированной длины, сдвигать их нельзя.
|
|
"""
|
|
import argparse, base64, json, sys
|
|
|
|
BASE = 0x08002800
|
|
# доступные глифы: ASCII + 33 заглавные кириллические буквы, дописанные при русификации
|
|
CYR = "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯЁ"
|
|
|
|
|
|
def widths(src_len):
|
|
"""допустимые ширины полей — берём из таблиц редактора"""
|
|
return None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("patch")
|
|
ap.add_argument("--src", default="stock-fw/rt4d_ru_batt.bin")
|
|
ap.add_argument("--out", default="stock-fw/rt4d_custom.bin")
|
|
ap.add_argument("--meta", default="tools/display-editor/field-widths.json",
|
|
help="ширины полей (va -> max), пишется при сборке редактора")
|
|
a = ap.parse_args()
|
|
|
|
patch = json.load(open(a.patch, encoding="utf-8"))
|
|
img = bytearray(open(a.src, "rb").read())
|
|
try:
|
|
W = {int(k): v for k, v in json.load(open(a.meta, encoding="utf-8")).items()}
|
|
except Exception:
|
|
sys.exit(f"нет файла ширин полей {a.meta} — пересоберите редактор")
|
|
|
|
bad = []
|
|
n = 0
|
|
for t in patch.get("texts", []):
|
|
va, txt = t["va"], t["txt"]
|
|
if va not in W:
|
|
bad.append(f"неизвестный адрес 0x{va:08X}")
|
|
continue
|
|
w = W[va]
|
|
for ch in txt:
|
|
if ch not in CYR and not (0x20 <= ord(ch) <= 0x7E):
|
|
bad.append(f"0x{va:08X}: нет глифа для «{ch}»")
|
|
if len(txt) > w:
|
|
bad.append(f"0x{va:08X}: «{txt}» длиннее поля ({len(txt)} > {w})")
|
|
continue
|
|
try:
|
|
enc = txt.encode("cp1251")
|
|
except UnicodeEncodeError as e:
|
|
bad.append(f"0x{va:08X}: не кодируется в CP1251 ({e})")
|
|
continue
|
|
img[va - BASE: va - BASE + w] = enc.ljust(w, b" ")
|
|
n += 1
|
|
|
|
lg = patch.get("logo")
|
|
if lg:
|
|
raw = base64.b64decode(lg["b64"])
|
|
if len(raw) != 288:
|
|
bad.append(f"логотип: ожидалось 288 байт, пришло {len(raw)}")
|
|
else:
|
|
va = lg["va"]
|
|
img[va - BASE: va - BASE + 288] = raw
|
|
print("логотип обновлён (288 Б)")
|
|
|
|
if bad:
|
|
print("ОШИБКИ:")
|
|
for b in bad:
|
|
print(" " + b)
|
|
if not n:
|
|
sys.exit(1)
|
|
|
|
open(a.out, "wb").write(bytes(img))
|
|
print(f"применено текстов: {n}")
|
|
print(f"готово: {a.out}, {len(img)} Б")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|