#!/usr/bin/env python3 """Generate a self-contained tkinter GUI flasher (firmware embedded) and build it into a single Windows .exe with PyInstaller.""" import base64, os, subprocess, sys ROOT = "C:/Users/vikto/Documents/Claude/rt-4d" FW = f"{ROOT}/stock-fw/rt4d_ru_full.bin" DIST = f"{ROOT}/dist" fw = open(FW, "rb").read() b64 = base64.b64encode(fw).decode() info = f"Ru-4D V3.25 (русифицированная) - {len(fw)} байт" GUI = r'''# -*- coding: utf-8 -*- import base64, time, threading, queue import tkinter as tk from tkinter import ttk import serial from serial.tools import list_ports FW = base64.b64decode("__B64__") INFO = "__INFO__" SEED = 0x48 def cksum(b): return bytes([(SEED + sum(b)) & 0xFF]) def find_ports(): return list(list_ports.comports()) def auto_port(ports): for p in ports: # CH343 = VID 0x1A86 if (p.vid or 0) == 0x1A86: return p.device return ports[0].device if ports else None def do_flash(port_name, log, prog): p = serial.Serial(port_name, 115200, timeout=0.3, write_timeout=3) try: log("Порт " + port_name + " открыт. Синхронизация...") p.reset_input_buffer() ready = False; t0 = time.time() while time.time() - t0 < 7: b = p.read(1) if not b: p.write(b"\xff") elif b == b"\xff": ready = True; break if not ready: raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой клавишей * , затем повторите.") log("Бутлоадер найден. Стирание...") for trig in (0x10, 0x55): pl = bytes([0x39, 0x33, 0x05, trig]); pl += cksum(pl) p.reset_input_buffer(); p.write(pl) ok = False; t = time.time() while time.time() - t < 4: if p.read(1) == b"\x06": ok = True; break if not ok: raise RuntimeError("Ошибка стирания.") log("Запись прошивки...") pad = (-len(FW)) % 1024 data = FW + b"\xff" * pad; total = len(data) for off in range(0, total, 1024): chunk = data[off:off + 1024] pl = bytes([0x57, (off >> 8) & 0xFF, off & 0xFF]) + chunk; pl += cksum(pl) p.reset_input_buffer(); p.write(pl) ok = False; t = time.time() while time.time() - t < 3: if p.read(1) == b"\x06": ok = True; break if not ok: raise RuntimeError("Ошибка записи на 0x%X." % off) prog((off + 1024) / total) prog(1.0) log("[OK] Готово! Выключите и включите рацию обычно.") finally: p.close() class App: def __init__(self, root): self.root = root; self.q = queue.Queue(); self.busy = False root.title("Ru-4D - Прошивка рации"); root.geometry("460x520"); root.resizable(False, False) root.configure(bg="#131a24") F = dict(bg="#131a24", fg="#dfe7ef") tk.Label(root, text="Ru-4D - Прошивка рации", font=("Segoe UI", 15, "bold"), **F).pack(pady=(16, 0)) tk.Label(root, text=INFO, font=("Segoe UI", 9), bg="#131a24", fg="#7f8ea3").pack() steps = ("1. Выключите рацию.\n" "2. Включите её, удерживая клавишу * (режим прошивки).\n" "3. Подключите рацию к ПК кабелем USB-C.\n" "4. Выберите COM-порт и нажмите Прошить .\n" "5. Не отключайте кабель до конца записи.") tk.Label(root, text=steps, justify="left", font=("Segoe UI", 10), bg="#131a24", fg="#c3cdd9").pack(padx=18, pady=12, anchor="w") row = tk.Frame(root, bg="#131a24"); row.pack(fill="x", padx=18) tk.Label(row, text="Порт:", **F).pack(side="left") self.port_var = tk.StringVar() self.port_cb = ttk.Combobox(row, textvariable=self.port_var, state="readonly", width=28) self.port_cb.pack(side="left", padx=6) tk.Button(row, text="↻", command=self.refresh, width=3).pack(side="left") self.btn = tk.Button(root, text="Прошить", font=("Segoe UI", 13, "bold"), bg="#22bb77", fg="#04120b", activebackground="#2ad", relief="flat", command=self.start) self.btn.pack(fill="x", padx=18, pady=(14, 6)) self.pb = ttk.Progressbar(root, maximum=1000); self.pb.pack(fill="x", padx=18, pady=4) self.log = tk.Text(root, height=9, font=("Consolas", 9), bg="#0c1219", fg="#9fb0c2", relief="flat", wrap="word"); self.log.pack(fill="both", expand=True, padx=18, pady=(6, 14)) self.refresh(); self.poll() def refresh(self): ports = find_ports() self.port_cb["values"] = [f"{p.device} - {p.description}" for p in ports] ap = auto_port(ports) if ap: for i, p in enumerate(ports): if p.device == ap: self.port_cb.current(i); break def logline(self, m, tag=None): self.log.insert("end", m + "\n"); self.log.see("end") def prog(self, f): self.pb["value"] = int(f * 1000) def start(self): if self.busy: return sel = self.port_var.get() if not sel: self.logline("Выберите COM-порт (подключите рацию в режиме прошивки)."); return port = sel.split(" - ")[0] self.busy = True; self.btn.config(state="disabled"); self.log.delete("1.0", "end"); self.pb["value"] = 0 threading.Thread(target=self.worker, args=(port,), daemon=True).start() def worker(self, port): try: do_flash(port, lambda m: self.q.put(("log", m)), lambda f: self.q.put(("prog", f))) except Exception as e: self.q.put(("log", "[ОШИБКА] " + str(e))) self.q.put(("done", None)) def poll(self): try: while True: k, v = self.q.get_nowait() if k == "log": self.logline(v) elif k == "prog": self.prog(v) elif k == "done": self.busy = False; self.btn.config(state="normal") except queue.Empty: pass self.root.after(60, self.poll) if __name__ == "__main__": root = tk.Tk() try: App(root); root.mainloop() except Exception as e: import traceback; traceback.print_exc() ''' gui = GUI.replace("__B64__", b64).replace("__INFO__", info) os.makedirs(DIST, exist_ok=True) gui_path = f"{DIST}/flasher_gui.py" open(gui_path, "w", encoding="utf-8").write(gui) print("wrote", gui_path, len(gui), "bytes") cmd = [sys.executable, "-m", "PyInstaller", "--onefile", "--windowed", "--name", "Ru-4D_Flasher", "--distpath", DIST, "--workpath", f"{DIST}/build", "--specpath", f"{DIST}/build", "--clean", "--noconfirm", gui_path] print("running:", " ".join(cmd[2:])) r = subprocess.run(cmd, capture_output=True, text=True) print(r.stdout[-1500:]); print(r.stderr[-1500:]) print("EXIT", r.returncode)