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