初始提交卡包 USB 工程
纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user