mothercup/tools/flash_package.py
evan.liu 57762e50be tools: self-contained portable production package
- tools/python-embed/: embedded Python 3.12.10 runtime with bleak/pyserial
  preinstalled (git-ignored, fetched from the huaweicloud mirror); bats
  resolve Python in order: python-embed -> .venv-ble -> system
- NSpyocd.exe + DFP pack copied into tools/ (git-ignored);
  flash_package.py prefers the in-tools copies
- make_release.bat/ps1: one-click zip to out/mothercup_tools_<ts>.zip
  (staged in TEMP to avoid self-inclusion; zip excluded from git)
- all bats converted to CRLF (LF-only breaks cmd block parsing and %~dp0)
- verified: zip extracted outside the repo runs ble_ota --uart end-to-end
  (PASS, 27 kB/s) and flash_package via the bundled NSpyocd
- docs: dev log section 49, AGENTS.md tools section + heap 18KB fix
2026-09-05 10:34:59 +08:00

120 lines
4.4 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
flash_package.py - erase the chip and program the merged package via
NS-LINK (NSpyocd). Streams the vendor tool's output with the vendor
banner replaced by the CAIIC one (the banner is baked into the
PyInstaller-packed NSpyocd.exe and cannot be edited).
usage: flash_package.bat [image]
[image] optional, default: tools/out/mothercup_ble_prod.hex
Connect NS-LINK to SWD (PA4/PA5) and the reset pin first, and CLOSE
Keil (it holds the probe).
"""
import os
import subprocess
import sys
# NSpyocd is a PyInstaller-packed pyocd: its banner is inside the
# compressed bundle, so we rebrand it on the fly. The console encoding
# varies (UTF-8 vs GBK), match the byte pattern in both.
BANNER_PATTERNS = [
("国民技术NSLINK上位机".encode("utf-8"), b"CAIIC NSLINK UMP"),
("国民技术NSLINK上位机".encode("gbk"), b"CAIIC NSLINK UMP"),
]
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TOOLS = os.path.dirname(os.path.abspath(__file__))
# Prefer the copies shipped inside tools/ (portable production package);
# fall back to the repo's nations-tec/ SDK layout (developer machine).
_local_pyocd = os.path.join(TOOLS, "NSpyocd", "NSpyocd.exe")
_local_pack = os.path.join(TOOLS, "N32WB03x_DFP.1.4.0.pack")
PYOCD = _local_pyocd if os.path.isfile(_local_pyocd) else \
os.path.join(ROOT, "nations-tec", "N32WB03x_SDK_V2.0.0",
"utilities", "dfu", "NSpyocd", "NSpyocd.exe")
# The builtin "n32wb031" target only maps 256KB of flash (fails at
# 0x01040000 on the dual-bank package); the DFP pack target
# "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
PACK = _local_pack if os.path.isfile(_local_pack) else \
os.path.join(ROOT, "nations-tec", "N32WB03x_DFP.1.4.0.pack")
TARGET = "n32wb031keq6_2"
def run_step(args):
"""Run one NSpyocd step, streaming output with the banner replaced.
Reads in small chunks (not line-buffered) so the tool's \\r-based
progress bar redraws live. Returns the process exit code."""
proc = subprocess.Popen([PYOCD] + args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
out = sys.stdout.buffer
keep = max(len(p) for p, _ in BANNER_PATTERNS) - 1
tail = b""
while True:
chunk = proc.stdout.read(256)
if not chunk:
break
buf = tail + chunk
# replace on the COMBINED buffer first (a pattern straddling the
# chunk boundary only exists contiguously here), then hold back
# the last maxpat-1 bytes so a pattern split by the next chunk
# is still seen whole
for old, new in BANNER_PATTERNS:
buf = buf.replace(old, new)
emit, tail = buf[:-keep], buf[-keep:]
out.write(emit)
out.flush()
for old, new in BANNER_PATTERNS:
tail = tail.replace(old, new)
out.write(tail)
out.flush()
return proc.wait()
def main():
image = sys.argv[1] if len(sys.argv) > 1 else \
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"out", "mothercup_ble_prod.hex")
if not os.path.isfile(image):
print("%s not found - run tools\\make_package.bat first" % image)
return 1
# -M under-reset: connect while holding the chip in reset
# -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
print("Erasing chip...", flush=True)
rc = run_step(["erase", "--chip", "--pack", PACK, "-M", "under-reset",
"-f", "1000000", "-t", TARGET])
if rc != 0:
return fail()
print("Programming %s ..." % image, flush=True)
# -O smart_flash=false: with the pack target the pre-program diff pass
# faults reading 0x01000000; programming+verify itself is fine.
rc = run_step(["load", "--pack", PACK, "-M", "under-reset",
"-f", "1000000", "-t", TARGET,
"-O", "smart_flash=false", image])
if rc != 0:
return fail()
print("Program Finish!")
return 0
def fail():
print()
print("*** FLASH FAILED ***")
print("Troubleshooting:")
print(" 1. Close Keil / any tool holding the probe, replug NS-LINK USB")
print(" 2. List probes: \"%s\" list" % PYOCD)
print(" 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin")
print(" 4. If several probes are attached, add -u <probe_uid> "
"to the commands above")
return 1
if __name__ == "__main__":
sys.exit(main())