Files
viktor 6d49ba3cba 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

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()