初始提交卡包 USB 工程

纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
zhengxin
2026-10-13 21:54:50 +08:00
co-authored by Cursor
commit 851e347cf5
260 changed files with 269349 additions and 0 deletions
+328
View File
@@ -0,0 +1,328 @@
"""NAND transfer GUI: pick COM port, local folder, FLASH folder, then batch upload."""
import queue
import threading
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, simpledialog, ttk
import nand_put as nx
try:
from serial.tools import list_ports
except ImportError:
list_ports = None
LOCAL_DEFAULT = Path(r"E:\PET\EAF")
FLASH_DEFAULT = "/extflash/eaf"
def com_choices():
if list_ports is None:
return []
items = []
for port in list_ports.comports():
extra = port.description or ""
items.append((port.device, f"{port.device} {extra}".rstrip()))
items.sort(key=lambda item: (0 if "303A" in item[1].upper() or "SERIAL/JTAG" in item[1].upper() else 1, item[0]))
return items
def join_flash(root, *parts):
path = root.rstrip("/")
for part in parts:
if part:
path += "/" + str(part)
return path
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("NAND 传输")
self.geometry("820x560")
self.minsize(640, 420)
LOCAL_DEFAULT.mkdir(parents=True, exist_ok=True)
self._jobs = queue.Queue()
self._stop = threading.Event()
self._busy = False
self._build()
self.refresh_ports()
self.after(80, self._pump)
def _build(self):
pad = {"padx": 8, "pady": 4}
top = ttk.Frame(self)
top.pack(fill="x", **pad)
ttk.Label(top, text="端口").grid(row=0, column=0, sticky="w")
self.port = ttk.Combobox(top, width=42, state="readonly")
self.port.grid(row=0, column=1, sticky="ew", padx=4)
ttk.Button(top, text="刷新", command=self.refresh_ports).grid(row=0, column=2)
ttk.Label(top, text="本地目录").grid(row=1, column=0, sticky="w")
self.local = ttk.Entry(top)
self.local.insert(0, str(LOCAL_DEFAULT))
self.local.grid(row=1, column=1, sticky="ew", padx=4)
ttk.Button(top, text="浏览", command=self.browse_local).grid(row=1, column=2)
ttk.Label(top, text="FLASH目录").grid(row=2, column=0, sticky="w")
self.flash = ttk.Entry(top)
self.flash.insert(0, FLASH_DEFAULT)
self.flash.grid(row=2, column=1, sticky="ew", padx=4)
top.columnconfigure(1, weight=1)
bar = ttk.Frame(self)
bar.pack(fill="x", **pad)
self.btn_info = ttk.Button(bar, text="查询", command=self.on_info)
self.btn_info.pack(side="left", padx=(0, 6))
self.btn_list = ttk.Button(bar, text="列目录", command=self.on_list)
self.btn_list.pack(side="left", padx=(0, 6))
self.btn_put = ttk.Button(bar, text="批量写入", command=self.on_upload)
self.btn_put.pack(side="left", padx=(0, 6))
self.btn_format = ttk.Button(bar, text="格式化", command=self.on_format)
self.btn_format.pack(side="left", padx=(0, 6))
self.btn_stop = ttk.Button(bar, text="停止", command=self.on_stop, state="disabled")
self.btn_stop.pack(side="left")
self.progress = ttk.Progressbar(self, mode="determinate")
self.progress.pack(fill="x", padx=8, pady=(0, 4))
self.status = ttk.Label(self, text="空闲")
self.status.pack(anchor="w", padx=8)
pane = ttk.Panedwindow(self, orient="vertical")
pane.pack(fill="both", expand=True, padx=8, pady=4)
list_frame = ttk.Labelframe(pane, text="FLASH 目录")
log_frame = ttk.Labelframe(pane, text="日志")
pane.add(list_frame, weight=1)
pane.add(log_frame, weight=1)
self.listing = tk.Listbox(list_frame, font=("Consolas", 10), height=8)
scroll_list = ttk.Scrollbar(list_frame, command=self.listing.yview)
self.listing.configure(yscrollcommand=scroll_list.set)
self.listing.pack(side="left", fill="both", expand=True)
scroll_list.pack(side="right", fill="y")
self.log = tk.Text(log_frame, height=10, wrap="word", state="disabled")
scroll_log = ttk.Scrollbar(log_frame, command=self.log.yview)
self.log.configure(yscrollcommand=scroll_log.set)
self.log.pack(side="left", fill="both", expand=True)
scroll_log.pack(side="right", fill="y")
def refresh_ports(self):
choices = com_choices()
labels = [label for _, label in choices]
current = self.port.get()
self.port["values"] = labels
if not labels:
self.port.set("")
self._append("没有串口。确认板子已接上,并关掉其它占用 COM 的程序。")
return
devices = {label: device for device, label in choices}
self._port_map = devices
if current in devices:
self.port.set(current)
else:
self.port.current(0)
def browse_local(self):
start = self.local.get().strip() or str(LOCAL_DEFAULT)
picked = filedialog.askdirectory(title="选择本地目录", initialdir=start)
if picked:
self.local.delete(0, "end")
self.local.insert(0, picked)
def selected_port(self):
label = self.port.get().strip()
if not label:
raise ValueError("请选择串口")
return getattr(self, "_port_map", {}).get(label, label.split()[0])
def flash_dir(self):
path = self.flash.get().strip().replace("\\", "/")
if path != nx.ROOT and path.endswith("/"):
path = path.rstrip("/")
nx.path_bytes(path, allow_root=True)
return path
def on_info(self):
self._start("查询", self._job_info)
def on_list(self):
self._start("列目录", self._job_list)
def on_upload(self):
self._start("批量写入", self._job_upload)
def on_format(self):
if not messagebox.askokcancel(
"格式化 NAND",
"将清空外置 NAND 上的全部文件。\n新盘第一次使用需要格式化;已有表情文件的盘不要点。"):
return
typed = simpledialog.askstring("确认格式化", "请输入 FORMAT /extflash")
if typed != "FORMAT /extflash":
self._append("格式化已取消")
return
self._start("格式化", self._job_format)
def on_stop(self):
self._stop.set()
self._append("停止:当前文件结束后不再继续")
def _start(self, title, job):
if self._busy:
return
try:
port = self.selected_port()
flash = self.flash_dir()
local = Path(self.local.get().strip() or str(LOCAL_DEFAULT))
except Exception as exc:
messagebox.showerror("参数错误", str(exc))
return
self._busy = True
self._stop.clear()
self.btn_info.configure(state="disabled")
self.btn_list.configure(state="disabled")
self.btn_put.configure(state="disabled")
self.btn_format.configure(state="disabled")
self.btn_stop.configure(state="normal")
self.status.configure(text=title + "…")
threading.Thread(target=self._run, args=(title, job, port, local, flash), daemon=True).start()
def _run(self, title, job, port, local, flash):
try:
if nx.serial is None:
raise ValueError("pyserial missing: py -3 -m pip install pyserial")
with nx.open_port(port) as serial_port:
client = nx.Client(serial_port)
client.connect()
job(client, local, flash)
self._post(lambda: self.status.configure(text=title + "完成"))
except Exception as exc:
self._append("错误: " + str(exc))
self._post(lambda: self.status.configure(text=title + "失败"))
finally:
self._post(self._idle)
def _job_info(self, client, local, flash):
page, pages, mounted, total, free = client.info()
self._append(
f"NAND {page * pages} bytes; mounted={mounted}; FAT={total} bytes, free={free}")
if not mounted:
self._append("未挂载:新盘需要先点「格式化」,再列目录或写入。")
def _job_format(self, client, local, flash):
self._append("正在格式化 NAND,可能需要一两分钟…")
nx.format_volume(client)
self._append("格式化完成并已重新挂载")
self._job_info(client, local, flash)
def _job_list(self, client, local, flash):
rows = []
for is_dir, name, size in client.listdir(flash):
if is_dir:
rows.append(f"DIR {name}")
else:
rows.append(f"FILE {name} {size}")
if not rows:
rows.append("(空目录)")
self._append(f"列目录 {flash} ({len(rows)} 项)")
self._post(lambda: self._show_listing(flash, rows))
def _job_upload(self, client, local, flash):
if not local.is_dir():
raise ValueError(f"本地目录不存在: {local}")
entries = list(nx.directory_entries(local))
files = [(path, parts) for path, parts, is_dir in entries if not is_dir]
dirs = [parts for _, parts, is_dir in entries if is_dir]
if not files and not dirs:
self._append(f"本地目录没有可上传的文件: {local}")
return
total = len(files)
self._post(lambda: self._set_progress(0, max(total, 1)))
if flash != nx.ROOT:
client.call(nx.MKDIR, nx.path_bytes(flash))
for parts in dirs:
if self._stop.is_set():
self._append("已停止")
return
client.call(nx.MKDIR, nx.path_bytes(join_flash(flash, *parts)))
done = 0
failed = 0
for path, parts in files:
if self._stop.is_set():
self._append("已停止")
break
dest = join_flash(flash, *parts)
self._append(f"写入 {path.name} -> {dest}")
try:
client.upload(path, dest, log=self._append)
done += 1
except Exception as exc:
failed += 1
self._append(f"失败 {path.name}: {exc}")
n = done + failed
self._post(lambda n=n, total=total: self._set_progress(n, total))
self._append(f"批量结束:成功 {done}/{total}" + (f",失败 {failed}" if failed else ""))
try:
self._job_list(client, local, flash)
except Exception as exc:
self._append("写入后列目录失败: " + str(exc))
def _set_progress(self, value, maximum):
self.progress.configure(maximum=max(maximum, 1), value=value)
self.status.configure(text=f"{value}/{maximum}")
def _show_listing(self, flash, rows):
self.listing.delete(0, "end")
self.listing.insert("end", flash)
for row in rows:
self.listing.insert("end", " " + row)
def _idle(self):
self._busy = False
self.btn_info.configure(state="normal")
self.btn_list.configure(state="normal")
self.btn_put.configure(state="normal")
self.btn_format.configure(state="normal")
self.btn_stop.configure(state="disabled")
def _append(self, line):
self._jobs.put(line)
def _post(self, fn):
self._jobs.put(fn)
def _pump(self):
try:
while True:
item = self._jobs.get_nowait()
if callable(item):
item()
else:
self.log.configure(state="normal")
self.log.insert("end", item + "\n")
self.log.see("end")
self.log.configure(state="disabled")
except queue.Empty:
pass
self.after(80, self._pump)
def main():
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
try:
App().mainloop()
except Exception as exc:
try:
messagebox.showerror("NAND 传输", str(exc))
except Exception:
raise
if __name__ == "__main__":
main()