m5-onboard

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

M5Stack Onboarding

M5Stack 设备初始化

This skill automates the full cold-start workflow for an M5Stack ESP32 device: detect on USB, identify model, flash UIFlow 2.0, and push a MicroPython app bundle onto
/flash/
so the device boots into user software. The apps we ship (Claude Buddy, Snake, Hello) talk over BLE or USB. The workflow runs on macOS, Linux, and Windows; the skill was developed against an M5Stack Basic v2.6 (CH9102 bridge, ESP32-D0WDQ6-V3, 16 MB flash) and generalized to cover the rest of the Core family, with the Cardputer-Adv (ESP32-S3, native USB) as the current default target.
本技能可自动化完成M5Stack ESP32设备的完整冷启动工作流:USB检测、型号识别、烧录UIFlow 2.0固件,并将MicroPython应用包推送至
/flash/
目录,使设备启动后直接运行用户软件。我们提供的应用(Claude Buddy、贪吃蛇、Hello)通过BLE或USB通信。该工作流支持macOS、Linux和Windows系统;技能基于M5Stack Basic v2.6(CH9102桥接芯片、ESP32-D0WDQ6-V3、16MB闪存)开发,并适配Core系列其他型号,当前默认目标设备为Cardputer-Adv(ESP32-S3、原生USB)。

Where the scripts live

脚本存放位置

This skill ships as part of the
cwc-makers
plugin for reference, but the executable scripts and the
buddy/
app bundle live in a local clone of https://github.com/moremas/build-with-claude (the
/maker-setup
command creates this clone). Run every
scripts/*.py
invocation below from inside that clone's
onboard/
directory so
--apps buddy
resolves to the sibling
buddy/device/
payload.
本技能作为
cwc-makers
插件的一部分发布,但可执行脚本和
buddy/
应用包存放在本地克隆的https://github.com/moremas/build-with-claude仓库中(`/maker-setup`命令会创建该克隆)。请在该克隆仓库的`onboard/`目录下运行以下所有`scripts/*.py`脚本,确保`--apps buddy
能正确解析到同级的
buddy/device/`目录中的文件。

When to use

使用场景

Use this when a user plugs in an M5Stack device and wants it provisioned. The decision tree:
  • Fresh/unknown device → run
    onboard.py --apps buddy
    end-to-end (detect → identify → flash → install apps). This is the default path.
  • Already-flashed device, user just wants apps installed/refreshed → run
    install_apps.py --src buddy
    (or any
    --src <path>
    to a directory of
    .py
    files).
  • Flashed device, something feels broken → run
    smoke_test.py
    (I2C + LCD + speaker + button check).
  • User wants to know what's on the bus / what the device can do
    smoke_test.py
    .
If multiple devices are plugged in, ask which port to target — don't guess. If the user is provisioning a device they previously worked with (e.g. "same thing as last time" or "another Buddy"), default to
--apps buddy
unless they say otherwise.
当用户接入M5Stack设备并需要配置时使用本技能,决策逻辑如下:
  • 全新/未知设备 → 端到端运行
    onboard.py --apps buddy
    (检测→识别→烧录→安装应用),这是默认流程。
  • 已烧录固件,仅需安装/更新应用 → 运行
    install_apps.py --src buddy
    (或使用
    --src <路径>
    指定包含
    .py
    文件的目录)。
  • 已烧录固件但出现故障 → 运行
    smoke_test.py
    (检测I2C、LCD、扬声器和按键)。
  • 用户想了解总线设备或设备功能 → 运行
    smoke_test.py
若接入多个设备,请询问用户目标端口,不要猜测。如果用户正在配置之前用过的设备(例如“和上次一样”或“另一台Buddy设备”),默认使用
--apps buddy
,除非用户另有说明。

Which variant to assume

默认设备型号

The rig this skill lives on provisions Cardputer-Adv boards overwhelmingly, so
onboard.py
now defaults to
--variant cardputer-adv
. In practice that means:
  • If the user says nothing about the model, go with the default. They're almost certainly holding a Cardputer-Adv.
  • If the user says "Cardputer" (no "Adv"), ask — the two models share a form factor but take different firmware images, and flashing the wrong one boot-loops the device.
  • If the user names any other board ("Core2", "CoreS3", "Basic", "Fire"), pass the matching
    --variant
    explicitly — the default won't apply.
  • The chip is ESP32-S3 either way, and
    detect.py
    won't be able to tell Cardputer from Cardputer-Adv before UIFlow is flashed (same native USB-JTAG VID, no pre-flash I2C probe). So this is a user-intent question, not a hardware-fingerprint one.
本技能所在环境主要配置Cardputer-Adv开发板,因此
onboard.py
现在默认使用
--variant cardputer-adv
。实际应用规则:
  • 若用户未指定型号,使用默认值,用户大概率手持Cardputer-Adv。
  • 若用户仅说“Cardputer”(不带“Adv”),请确认型号——两款设备外形相同,但需要不同的固件镜像,烧录错误会导致设备启动循环。
  • 若用户指定其他型号(如“Core2”、“CoreS3”、“Basic”、“Fire”),显式传递对应的
    --variant
    参数——默认值不适用。
  • 两款设备均搭载ESP32-S3芯片,在烧录UIFlow之前,
    detect.py
    无法区分Cardputer和Cardputer-Adv(原生USB-JTAG的VID相同,预烧录阶段无法通过I2C探测)。因此这是一个用户意图问题,而非硬件指纹识别问题。

The workflow

工作流

The main orchestrator is
scripts/onboard.py
. It drives the sub-scripts in order and handles the handoffs between them (waiting for reboots, capturing MAC, reporting progress). Prefer calling it directly over stitching the sub-scripts yourself unless the user asks for a partial run.
The default provisioning command (fresh Cardputer-Adv, install the buddy bundle):
python3 scripts/onboard.py --apps buddy
How to invoke this from Claude Code's Bash tool. Do NOT call
onboard.py
as a foreground Bash command. The Bash tool captures output and does not stream it back to the assistant until the command exits — and this command runs 2–3 minutes. That silence looks identical to a hang, and the assistant will usually give up before the button-dance prompt ever reaches the user. Instead, always run with
run_in_background: true
,
tee
to a log file, and then use the Monitor tool (or periodic
tail
via Read) to surface stage banners, heartbeats, and prompts to the user in real time.
2>&1
is not the fix — all progress already writes to stderr, which a terminal shows fine. The fix is streaming semantics, not redirection. The pattern that works:
undefined
主协调器为
scripts/onboard.py
,它按顺序调用子脚本并处理脚本间的衔接(等待重启、捕获MAC地址、报告进度)。除非用户要求执行部分流程,否则优先直接调用该脚本,而非自行拼接子脚本。
默认配置命令(全新Cardputer-Adv,安装Buddy应用包):
python3 scripts/onboard.py --apps buddy
如何通过Claude Code的Bash工具调用:请勿将
onboard.py
作为前台Bash命令调用。Bash工具会捕获输出,直到命令退出才会返回给助手——而该命令需要运行2-3分钟,这段静默期看起来和程序挂起无异,助手通常会在按钮操作提示到达用户前放弃等待。正确的做法是始终使用
run_in_background: true
,通过
tee
将输出写入日志文件,然后使用Monitor工具(或定期通过Read执行
tail
命令)实时向用户展示阶段提示、心跳信息和操作要求。
2>&1
无法解决问题——所有进度信息已写入stderr,终端可正常显示。解决方法是使用流式语义,而非重定向。可行的调用模式:
undefined

Launch (background, tee log):

启动(后台运行,输出写入日志):

python3 scripts/onboard.py --apps buddy 2>&1 | tee /tmp/m5-onboard.log
python3 scripts/onboard.py --apps buddy 2>&1 | tee /tmp/m5-onboard.log

Monitor (surfaces key events without drowning in byte-progress spam):

监控(仅显示关键事件,避免字节进度信息刷屏):

tail -f /tmp/m5-onboard.log | grep -E --line-buffered
"^====|heartbeat|Heads up|Enter download mode|download mode!|rebooted into UIFlow|Manual reset|DONE|ERROR|Error|Traceback|FAIL|failed|No USB|not detected|Attempt [0-9]|Device already in download|Download mode port|Post-flash port|Waiting for device"
undefined
tail -f /tmp/m5-onboard.log | grep -E --line-buffered \ "^====|heartbeat|Heads up|Enter download mode|download mode!|rebooted into UIFlow|Manual reset|DONE|ERROR|Error|Traceback|FAIL|failed|No USB|not detected|Attempt [0-9]|Device already in download|Download mode port|Post-flash port|Waiting for device"
undefined

Relaying physical steps to the user (REQUIRED)

向用户传达物理操作步骤(必填)

The flash stage cannot proceed without a manual button press on native-USB boards — there is no software path. When the monitored log shows
Enter download mode
(or the script appears to wait at the FLASH stage), you MUST stop and tell the user to do the following on the back of the Cardputer, in your own words, before continuing:
  1. Press and hold the G0 button
  2. While still holding G0, briefly press and release the RST button
  3. Keep holding G0 for about one more second, then release it
  4. The screen should go fully dark — that means download mode is active
If the device reboots into UIFlow instead of going dark, tell the user G0 was released too early and to try again holding it longer. Do not move on, retry the script, or attempt a software workaround until the user confirms the screen is dark — the flash will not start otherwise. The same applies to any later
Manual reset
prompt: relay the physical step and wait for the user.
Users running
onboard.py
directly in their own terminal (not via Claude Code) will see all output live — no changes needed there.
If
--port
is omitted,
detect.py
picks the most likely candidate across all three OSes: native-USB ESP32-S3 (
/dev/cu.usbmodem*
on macOS,
/dev/ttyACM*
on Linux,
COMx
on Windows), or a CH9102/CP210x UART bridge on older boards. Bluetooth-serial ports are filtered out. If multiple candidates are present, it asks.
The known apps name
buddy
resolves to the
buddy/device/
directory in this repo (custom launcher + Hello + Claude Buddy BLE client + Snake). Any other
--apps
value is treated as a filesystem path.
To skip re-flashing and just push (or refresh) the apps onto an already-provisioned device:
python3 scripts/install_apps.py --port <PORT> --src buddy
Where
<PORT>
is whatever
detect.py
printed on the last full run — for example
/dev/cu.usbmodem1101
,
/dev/ttyACM0
, or
COM3
.
对于原生USB开发板,烧录阶段必须手动按下按钮才能继续——没有软件触发的途径。当监控日志显示
Enter download mode
(或脚本在FLASH阶段停滞)时,必须暂停操作,用自己的语言告知用户在Cardputer的背面执行以下步骤,然后再继续:
  1. 按下并按住 G0按钮
  2. 保持按住G0的同时,短暂按下并松开RST按钮
  3. 继续按住G0约1秒,然后松开
  4. 屏幕应完全变黑——这表示已进入下载模式
如果设备重启进入UIFlow而非黑屏,说明用户提前松开了G0按钮,请告知用户再次尝试并按住更长时间。在用户确认屏幕变黑前,不要继续操作、重试脚本或尝试软件解决方法——否则烧录无法启动。后续出现
Manual reset
提示时,同样需要传达物理操作步骤并等待用户确认。
用户直接在自己的终端中运行
onboard.py
时,可实时看到所有输出——无需调整。
若省略
--port
参数,
detect.py
会在所有操作系统中选择最可能的端口:原生USB ESP32-S3(macOS为
/dev/cu.usbmodem*
,Linux为
/dev/ttyACM*
,Windows为
COMx
),或旧款开发板的CH9102/CP210x UART桥接端口。蓝牙串口会被过滤。若存在多个候选端口,脚本会询问用户。
已知应用名称
buddy
对应本仓库中的
buddy/device/
目录(自定义启动器+Hello+Claude Buddy BLE客户端+贪吃蛇)。其他
--apps
值会被视为文件系统路径。
若要跳过重新烧录,仅将应用推送(或更新)到已配置的设备:
python3 scripts/install_apps.py --port <PORT> --src buddy
其中
<PORT>
为上次完整运行时
detect.py
输出的端口——例如
/dev/cu.usbmodem1101
/dev/ttyACM0
COM3

Stages

阶段说明

  1. Detect (
    detect.py
    ) — enumerate serial ports, filter to USB-UART bridges (CH9102 vendor
    0x1A86
    , Silabs CP210x
    0x10C4
    , FTDI
    0x0403
    ) or the ESP32-S3 native USB-JTAG interface (
    0x303A
    ). Probe with esptool to confirm the chip. Port names differ per OS (
    /dev/cu.usbmodem*
    on macOS,
    /dev/ttyACM*
    /
    ttyUSB*
    on Linux,
    COMx
    on Windows) but pyserial abstracts that.
  2. Identify (
    detect.py
    ) — alongside port discovery,
    detect.py
    reads the factory-test partition signature and/or scans I2C once UIFlow is on, and cross-references
    references/hardware_signatures.md
    to suggest the right firmware variant (Basic-16MB, Core2, CoreS3, Cardputer-Adv, etc.). User-facing variant choice happens via
    onboard.py --variant
    ; there is no separate
    detect.py --identify
    flag.
  3. Fetch firmware (
    fetch_firmware.py
    ) — query the M5Burner manifest API and download the appropriate UIFlow 2.0 binary into the system temp dir. Cached between runs — safe to clear the cache anytime, it just re-downloads.
  4. Flash (
    flash.py
    ) —
    esptool write_flash 0x0 <image>
    at 460800 baud for UART bridges,
    --no-stub
    at 115200 baud for native-USB S3 devices. 921600 fails intermittently on the CH9102 bridge — do not increase it. Native-USB flash can intermittently throw
    Lost connection, retrying
    mid-erase; esptool recovers. The post-flash
    watchdog-reset
    teardown step can fail even when the flash itself succeeded —
    flash.py
    parses esptool's stdout, treats that specific failure pattern as non-fatal when
    Hash of data verified
    appeared, and
    onboard.py
    falls back to
    flash.native_reset()
    and then manual-RESET coaching if needed.
  5. Install apps (optional,
    install_apps.py
    ) — paste-mode REPL upload of every
    .py
    from a source directory into
    /flash/
    , then reboot via
    repl_reset
    (DTR/RTS is a no-op on native USB — don't reach for it). Source layout: root
    *.py
    /flash/
    ,
    apps/*.py
    /flash/apps/
    (UIFlow's stock launcher scans that). When the bundle ships a root
    main.py
    ,
    install_apps.py
    also sets NVS
    boot_option=2
    so UIFlow's own launcher doesn't run and our
    main.py
    takes over the boot flow — critical for BLE-using apps on ESP32-S3 (see gotchas below).
  6. Smoke test (optional,
    smoke_test.py
    ) — I2C scan, LCD test pattern, speaker beep, button read.
  1. 检测
    detect.py
    )——枚举串口,过滤出USB-UART桥接端口(CH9102厂商ID
    0x1A86
    、Silabs CP210x
    0x10C4
    、FTDI
    0x0403
    )或ESP32-S3原生USB-JTAG接口(
    0x303A
    )。通过esptool探测确认芯片型号。不同操作系统的端口名称不同(macOS为
    /dev/cu.usbmodem*
    ,Linux为
    /dev/ttyACM*
    /
    ttyUSB*
    ,Windows为
    COMx
    ),但pyserial会进行抽象处理。
  2. 识别
    detect.py
    )——在端口发现的同时,
    detect.py
    读取工厂测试分区签名,或在烧录UIFlow后扫描I2C,然后与
    references/hardware_signatures.md
    交叉比对,建议合适的固件版本(Basic-16MB、Core2、CoreS3、Cardputer-Adv等)。用户可通过
    onboard.py --variant
    选择型号;没有单独的
    detect.py --identify
    参数。
  3. 获取固件
    fetch_firmware.py
    )——查询M5Burner清单API,将合适的UIFlow 2.0二进制文件下载到系统临时目录。固件会在多次运行间缓存——清除缓存是安全的,下次运行会重新下载。
  4. 烧录
    flash.py
    )——对于UART桥接设备,使用
    esptool write_flash 0x0 <image>
    ,波特率为460800;对于原生USB S3设备,使用
    --no-stub
    ,波特率为115200。CH9102桥接设备在921600波特率下进行
    erase_flash
    会丢失同步(非理论问题,实际会失败)——请勿提高波特率。原生USB烧录过程中可能会间歇性出现
    Lost connection, retrying
    错误;esptool会自动恢复。烧录后的
    watchdog-reset
    清理步骤可能会失败,但只要出现
    Hash of data verified
    信息,
    flash.py
    会将该特定失败模式视为非致命,
    onboard.py
    会回退到
    flash.native_reset()
    ,若仍失败则指导用户手动重置。
  5. 安装应用(可选,
    install_apps.py
    )——通过粘贴模式REPL将源目录中的所有
    .py
    文件上传到
    /flash/
    ,然后通过
    repl_reset
    重启设备(原生USB设备上的DTR/RTS无效——请勿使用)。源目录结构:根目录下的
    *.py
    文件→
    /flash/
    apps/*.py
    /flash/apps/
    (UIFlow的默认启动器会扫描该目录)。如果应用包包含根目录下的
    main.py
    install_apps.py
    还会设置NVS的
    boot_option=2
    ,使UIFlow自带的启动器不再运行,而是由我们的
    main.py
    接管启动流程——这对ESP32-S3上使用BLE的应用至关重要(见下文注意事项)。
  6. 冒烟测试(可选,
    smoke_test.py
    )——I2C扫描、LCD测试图案、扬声器蜂鸣、按键读取。

Critical gotchas (baked into the scripts — do not second-guess)

关键注意事项(已集成到脚本中——请勿自行修改)

These are things the scripts already handle correctly but which you should not override if the user asks you to "just run esptool manually" or similar:
  • Native-USB ESP32-S3 boards (Cardputer, Cardputer-Adv, CoreS3) require a physical BtnG0+BtnRST dance to enter download mode. There is no software path. The chip has no DTR/RTS bridge, so nothing esptool or pyserial can do will put it into the ROM bootloader — the user has to hold GPIO0 low across a reset pulse with the hardware buttons. On Cardputer-Adv specifically both buttons (BtnG0 and BtnRST) are on the back of the device — small, flush-mounted, often easiest to press with a fingernail.
    onboard.py:_wait_for_download_port
    prompts for this at runtime during FLASH: press and HOLD BtnG0, briefly press BtnRST, release BtnRST first, keep holding BtnG0 for ~1 more second, release BtnG0, screen should be fully dark. If the device reboots back into UIFlow instead, BtnG0 was released too early — the coaching retries and tells the user to hold it longer. Do NOT try to automate this with
    esptool --before default_reset
    or pyserial's DTR/RTS; both are no-ops on native USB (the pins aren't wired to EN), and adding them just hides the real prompt.
  • Do not unplug the device during FLASH. Especially on native USB. A mid-flash disconnect leaves the internal flash in an inconsistent state. Mask ROM is usually reachable afterwards (press BtnG0 alone on the back, or do the full BtnG0+BtnRST dance), so the recovery is just to re-run
    m5-onboard go
    — it's idempotent and will re-enter download mode, re-flash, re-push apps. Don't panic and don't start opening the case; the mask ROM is in silicon and survives a corrupted flash as long as the USB PHY is intact.
  • Baud rate is 460800 on UART bridges, 115200 with
    --no-stub
    on native USB.
    Not 921600 on either. The CH9102 bridge loses sync on
    erase_flash
    at 921600 (not theoretical — it fails). Native USB's stub-baud-bump path produces "Lost connection" mid-flash; 115200 no-stub is counterintuitively faster end-to-end because it never fails.
  • NVS writes must use
    set_str
    , not
    set_blob
    (relevant to
    install_apps.py
    's
    boot_option
    setter).
    UIFlow's startup calls
    nvs.get_str()
    and ESP-IDF tags blob and string entries separately. A blob-tagged key returns
    ESP_ERR_NVS_NOT_FOUND
    to
    get_str
    , and the device boot-loops. If a prior attempt wrote a blob, call
    nvs.erase_key(name)
    before
    set_str
    .
  • REPL multi-line blocks need paste mode. Sending
    try:
    /
    except:
    line-by-line makes the REPL accumulate indentation forever. Use Ctrl-E to enter paste mode, send the block, Ctrl-D to execute.
    mpy_repl.py
    wraps this.
  • Hard reset is DTR=False, RTS=True, 100ms, RTS=False — but only on UART-bridge devices. On native-USB ESP32-S3 boards the DTR/RTS lines aren't wired to EN/GPIO0, so that pulse is a silent no-op. Use
    mpy_repl.repl_reset()
    (sends
    machine.reset()
    through the REPL) for post-install reboots on those devices —
    install_apps.py
    already does this. If you bypass
    install_apps.py
    and stitch your own flow, don't reach for DTR/RTS on a usbmodem port and expect a reboot; files will be on disk but the old code will still be running. That regression bit us once.
  • The idle heap-debug loop is normal. UIFlow 2.0 prints asyncio diagnostics while waiting at the pairing screen. Don't interpret it as a hang.
  • Cardputer-Adv (ESP32-S3) BLE peripherals require NVS
    boot_option=2
    + a custom
    main.py
    .
    UIFlow's default
    boot_option=1
    starts a background Flow-pairing BLE advertise that wedges the NimBLE controller — subsequent
    gap_advertise(adv_data=...)
    calls from user code hit OSError(-519) "Memory Capacity Exceeded" regardless of payload shape, and the device ends up advertising with empty AD fields that iOS and the desktop Claude Buddy app filter out. The bundle's
    main.py
    lives at
    /flash/
    and takes over the boot flow (showing a simple menu over
    /flash/apps/
    ), never touches BLE itself, and leaves the controller pristine for whichever app the user picks.
    install_apps.py
    now sets
    boot_option=2
    automatically when the bundle ships a root
    main.py
    — don't regress that behavior.
以下是脚本已正确处理的问题,但如果用户要求“直接手动运行esptool”等操作,请勿覆盖这些逻辑:
  • 原生USB ESP32-S3开发板(Cardputer、Cardputer-Adv、CoreS3)需要手动按下BtnG0+BtnRST进入下载模式:没有软件触发途径。芯片没有DTR/RTS桥接,因此esptool或pyserial无法将其置于ROM引导加载模式——用户必须通过硬件按钮在重置脉冲期间保持GPIO0低电平。对于Cardputer-Adv,两个按钮(BtnG0和BtnRST)都位于设备背面——体积小、嵌入式设计,通常用指甲按压最方便。
    onboard.py:_wait_for_download_port
    会在FLASH阶段运行时提示用户:按下并按住BtnG0,短暂按下BtnRST,先松开BtnRST,继续按住BtnG0约1秒,再松开BtnG0,屏幕应完全变黑。如果设备重启进入UIFlow而非黑屏,说明BtnG0松开过早——脚本会重试并告知用户按住更长时间。请勿尝试用
    esptool --before default_reset
    或pyserial的DTR/RTS自动化此操作;两者在原生USB设备上均无效(引脚未连接到EN),添加这些参数只会隐藏真实的操作提示。
  • 烧录过程中请勿拔下设备:尤其是原生USB设备。烧录中途断开连接会导致内部闪存状态不一致。之后通常仍可访问掩码ROM(按下背面的BtnG0,或执行完整的BtnG0+BtnRST操作),因此恢复方法只需重新运行
    m5-onboard go
    ——该操作是幂等的,会重新进入下载模式、重新烧录、重新推送应用。无需惊慌或拆开设备;掩码ROM集成在芯片中,只要USB PHY完好,即使闪存损坏也能保留。
  • UART桥接设备波特率为460800,原生USB设备使用
    --no-stub
    时波特率为115200
    :两者均不使用921600。CH9102桥接设备在921600波特率下执行
    erase_flash
    会丢失同步(实际会失败)。原生USB的stub波特率提升流程会在烧录中途出现“Lost connection”错误;115200无stub模式反而更快,因为不会失败。
  • NVS写入必须使用
    set_str
    ,而非
    set_blob
    (与
    install_apps.py
    boot_option
    设置相关):UIFlow启动时会调用
    nvs.get_str()
    ,ESP-IDF会分别标记blob和字符串条目。blob标记的键会在
    get_str()
    调用时返回
    ESP_ERR_NVS_NOT_FOUND
    ,导致设备启动循环。如果之前的尝试写入了blob,请在
    set_str
    前调用
    nvs.erase_key(name)
  • REPL多行代码块需要使用粘贴模式:逐行发送
    try:
    /
    except:
    会导致REPL无限累积缩进。使用Ctrl-E进入粘贴模式,发送代码块,然后按Ctrl-D执行。
    mpy_repl.py
    封装了此功能。
  • 硬重置为DTR=False、RTS=True、持续100ms、RTS=False——但仅适用于UART桥接设备:在原生USB ESP32-S3设备上,DTR/RTS引脚未连接到EN/GPIO0,因此该脉冲无效。对于这些设备,使用
    mpy_repl.repl_reset()
    (通过REPL发送
    machine.reset()
    )进行安装后重启——
    install_apps.py
    已实现此逻辑。如果绕过
    install_apps.py
    自行拼接流程,请勿在usbmodem端口使用DTR/RTS进行重启;文件会写入磁盘,但旧代码仍会运行。我们曾遇到过此问题。
  • 空闲堆调试循环是正常现象:UIFlow 2.0在配对屏幕等待时会打印asyncio诊断信息。请勿将其视为程序挂起。
  • Cardputer-Adv(ESP32-S3)BLE外设需要NVS
    boot_option=2
    +自定义
    main.py
    :UIFlow默认的
    boot_option=1
    会启动后台Flow配对BLE广播,导致NimBLE控制器阻塞——用户代码后续的
    gap_advertise(adv_data=...)
    调用会触发OSError(-519)“内存容量不足”错误,无论负载格式如何,设备最终会广播iOS和桌面Claude Buddy应用过滤的空AD字段。应用包中的
    main.py
    位于
    /flash/
    ,接管启动流程(通过
    /flash/apps/
    显示简单菜单),自身不涉及BLE操作,为用户选择的应用保留控制器的原始状态。
    install_apps.py
    现在会在应用包包含根目录
    main.py
    时自动设置
    boot_option=2
    ——请勿修改此行为。

After provisioning (what the user sees on the device)

配置完成后(用户在设备上看到的内容)

Once
m5-onboard go
finishes at the
DONE
banner, the device is ready to use on its own:
  • Power. Slide the switch on the right edge of the Cardputer-Adv to turn it on. Same switch turns it off. The board runs off its internal LiPo when unplugged; USB-C charges it.
  • Boot. A short boot log scrolls, then the launcher menu appears automatically. The menu lists every
    .py
    in
    /flash/apps/
    plus the top-level
    /flash/*.py
    entries.
  • Navigation. Arrow keys (or the keyboard's trackpoint-style cursor keys) scroll the menu; Enter launches the highlighted app; ESC returns to the launcher from inside an app.
  • Event WiFi auto-connect. The bundle's
    main.py
    connects to a hard-coded event WiFi (SSID
    cardputer
    ) on every boot and shows the result on the LCD before the launcher menu appears. Credentials live in
    buddy/device/wifi_event.py
    ; the connect is best-effort and the launcher always continues even if the connect fails. If you're using this bundle outside the event, edit
    wifi_event.py
    or remove the
    _connect_wifi_with_splash()
    call from
    main.py
    .
  • Claude Buddy over BLE. First time only: in Claude Desktop, Help → Troubleshooting → Enable Developer Tools (one-time, persists across launches). Then Developer menu → Hardware Buddy → Connect. BLE works regardless of the WiFi state — the link to Claude.app is local.
  • Getting back to UIFlow. The buddy bundle ships only a
    main.py
    at
    /flash/
    (no replacement
    boot.py
    ), so the stock UIFlow
    boot.py
    is never touched and there's no
    boot_uiflow.py
    backup to restore. Revert by removing our
    main.py
    from the device REPL:
    os.remove('/flash/main.py')
    followed by
    machine.reset()
    . UIFlow's stock launcher takes over on the next boot. To start completely fresh including the firmware, re-run the skill without
    --apps
    .
m5-onboard go
运行到
DONE
提示时,设备即可独立使用:
  • 电源:滑动Cardputer-Adv右侧边缘的开关开机,同样的开关用于关机。设备未接入USB时使用内置锂电池供电;USB-C接口用于充电。
  • 启动:简短的启动日志滚动后,会自动显示启动器菜单。菜单列出
    /flash/apps/
    中的所有
    .py
    文件以及
    /flash/
    根目录下的
    .py
    文件。
  • 导航:使用箭头键(或键盘的指点杆式光标键)滚动菜单;按Enter键启动高亮的应用;按ESC键从应用返回启动器。
  • 活动WiFi自动连接:应用包中的
    main.py
    会在每次启动时连接硬编码的活动WiFi(SSID为
    cardputer
    ),并在启动器菜单显示前在LCD上显示连接结果。凭证存放在
    buddy/device/wifi_event.py
    中;连接为尽力而为模式,即使连接失败,启动器仍会继续运行。如果在活动外使用此应用包,请编辑
    wifi_event.py
    或从
    main.py
    中移除
    _connect_wifi_with_splash()
    调用。
  • 通过BLE连接Claude Buddy:首次使用时,在Claude桌面端,进入帮助→故障排除→启用开发者工具(仅需一次,设置会在重启后保留)。然后进入开发者菜单→硬件Buddy→连接。BLE连接无需依赖WiFi状态——与Claude.app的连接是本地的。
  • 返回UIFlow:Buddy应用包仅在
    /flash/
    目录下包含
    main.py
    (未替换
    boot.py
    ),因此UIFlow默认的
    boot.py
    从未被修改,也没有可恢复的
    boot_uiflow.py
    备份。恢复方法是通过设备REPL删除我们的
    main.py
    os.remove('/flash/main.py')
    ,然后执行
    machine.reset()
    。下次启动时,UIFlow的默认启动器会接管。若要完全重置包括固件,请重新运行本技能且不添加
    --apps
    参数。

Files

文件说明

  • scripts/onboard.py
    — main orchestrator
  • scripts/detect.py
    — port discovery + chip ID
  • scripts/fetch_firmware.py
    — M5Burner API + download
  • scripts/flash.py
    — esptool wrapper
  • scripts/install_apps.py
    — push a directory of
    .py
    files into
    /flash/
    via paste-mode REPL; backs up
    boot.py
    as
    boot_uiflow.py
    before overwriting; also writes the
    boot_option
    NVS key when the bundle ships a root
    main.py
  • scripts/smoke_test.py
    — I2C + LCD + speaker + buttons
  • scripts/mpy_repl.py
    — shared serial/REPL helpers (paste mode, hard reset, boot-log capture)
  • references/hardware_signatures.md
    — chip + I2C fingerprints → model → firmware
  • references/uiflow2_nvs.md
    — NVS key reference with types and failure modes
  • scripts/onboard.py
    — 主协调器
  • scripts/detect.py
    — 端口发现+芯片ID识别
  • scripts/fetch_firmware.py
    — M5Burner API调用+固件下载
  • scripts/flash.py
    — esptool封装
  • scripts/install_apps.py
    — 通过粘贴模式REPL将目录中的
    .py
    文件推送至
    /flash/
    ;在覆盖前将
    boot.py
    备份为
    boot_uiflow.py
    ;当应用包包含根目录
    main.py
    时,写入
    boot_option
    NVS键
  • scripts/smoke_test.py
    — I2C+LCD+扬声器+按键检测
  • scripts/mpy_repl.py
    — 共享串口/REPL工具(粘贴模式、硬重置、启动日志捕获)
  • references/hardware_signatures.md
    — 芯片+I2C指纹→型号→固件映射
  • references/uiflow2_nvs.md
    — NVS键参考,包含类型和故障模式

Dependencies

依赖项

  • pyserial
    — vendored at
    onboard/scripts/vendor/serial/
    (pinned 3.5, BSD-3-Clause).
  • esptool
    — pip dependency, declared in
    requirements.txt
    . Importable check happens via
    importlib.util.find_spec("esptool")
    ; binary backstop search covers
    ~/Library/Python/*/bin/
    on macOS,
    ~/.local/bin/
    on Linux,
    %APPDATA%\Python\Python3XX\Scripts\
    on Windows.
onboard.py
runs a preflight check at startup: if
esptool
(or, in the rare prune-vendor case,
pyserial
) is missing, it lists what's needed and asks the user whether to install now. On
Y
(or Enter) it runs
python -m pip install --user <missing>
in the current interpreter, then verifies. Inside a venv the
--user
flag is dropped so the install lands in the venv's site-packages. Non-interactive callers (piped stdin) get a manual-install hint instead of a prompt.
Python itself has to exist before this skill can do anything — you can't bootstrap an interpreter from inside one.
git
is not required — the
/maker-setup
command falls back to downloading the GitHub tarball with
curl
+
tar
(both pre-installed on macOS, Linux, and Windows 10+) when
git --version
fails. Claude's responsible for detecting Python and installing it if missing before running any
scripts/*.py
invocation. Detection is just running
python3 --version
/
python --version
— if it fails, Claude fetches Python with the host's native package manager before anything else.
Per-OS Python bootstrap (Claude's responsibility if missing):
  • Windows
    winget install -e --id Python.Python.3.13 --silent --accept-source-agreements --accept-package-agreements
    . Takes ~30 seconds, no UI, gets PATH right. If the current shell can't see
    python
    afterwards, tell the user to close and reopen the terminal (Windows updates PATH only on new shells).
  • macOS — Python 3 is usually pre-installed as
    /usr/bin/python3
    on any current macOS (shipped by Apple). If for some reason it isn't,
    brew install python@3.13
    via Homebrew is the go-to; if Homebrew itself is missing, offer to install it via
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    (but only if the user confirms — Homebrew is a larger commitment than winget).
  • Linux — use the distro package manager. Debian/Ubuntu:
    sudo apt-get update && sudo apt-get install -y python3 python3-pip
    . Fedora:
    sudo dnf install -y python3 python3-pip
    . Arch:
    sudo pacman -S --noconfirm python python-pip
    . You may need to sudo and should surface the password prompt to the user if needed.
pyserial — bundled with the skill:
A pinned
pyserial 3.5
ships under
scripts/vendor/
(BSD-3-Clause, Apache-compatible). Every script that imports
serial
calls
vendor_path.ensure_on_syspath()
before the first third-party import, which prepends
scripts/vendor/
to
sys.path
, so the vendored copy resolves regardless of whatever the user has system-wide. Net effect: port enumeration and REPL I/O work on a fresh clone with zero pip step. ~500 KB, pure-Python, same tree on macOS / Linux / Windows.
esptool — pip dependency, auto-installed on first run:
esptool
is GPLv2+ and is intentionally not vendored — keeping the repository cleanly Apache-2.0 means the GPL bits live in the user's pip-managed environment, not in the tree. The skill's preflight checks for an importable
esptool
and, if missing, prompts to install it (
python -m pip install --user esptool
--user
dropped inside a venv so it lands in site-packages). For subprocess calls we use
[sys.executable, "-m", "esptool", ...]
; the subprocess inherits user-site so the pip-installed module imports cleanly.
requirements.txt
declares this for explicit setup; the prompt path is the default for first-time attendees who haven't run pip yet.
Non-interactive callers (piped stdin, CI) skip the prompt and get a
python -m pip install --user esptool
hint instead.
Fallback if someone prunes
scripts/vendor/
:
The same preflight path also re-installs pyserial via pip if the vendor copy is gone. This handles the case where someone downloaded a source-only zip that excluded vendor, or manually trimmed the repo to save space.
USB driver — Windows-specific, only for older boards:
The CH9102 USB-UART driver is still a manual install on Windows — WCH doesn't publish a winget manifest. Only needed for UART-bridge boards (Basic, Fire, Core2, StickC). Native-USB ESP32-S3 boards (Cardputer, Cardputer-Adv, CoreS3) enumerate as composite USB-CDC devices using Windows' in-box drivers and need no extra install.
  • pyserial
    — 内置在
    onboard/scripts/vendor/serial/
    (固定版本3.5,BSD-3-Clause协议)。
  • esptool
    — pip依赖项,声明在
    requirements.txt
    中。通过
    importlib.util.find_spec("esptool")
    检查是否可导入;二进制后备搜索路径包括macOS的
    ~/Library/Python/*/bin/
    、Linux的
    ~/.local/bin/
    、Windows的
    %APPDATA%\\Python\\Python3XX\\Scripts\\
onboard.py
在启动时会运行预检:如果
esptool
(或在极少数移除vendor目录的情况下
pyserial
)缺失,会列出所需依赖并询问用户是否立即安装。用户输入
Y
(或按Enter)后,会在当前解释器中运行
python -m pip install --user <缺失依赖>
,然后验证安装。在虚拟环境中,会省略
--user
参数,使安装包进入虚拟环境的site-packages。非交互式调用者(管道输入)会收到手动安装提示,而非交互式询问。
运行本技能前必须已安装Python——无法在Python内部引导解释器。无需git——当
git --version
失败时,
/maker-setup
命令会回退到使用
curl
+
tar
下载GitHub压缩包(macOS、Linux和Windows 10+均预装了这两个工具)。Claude负责在运行任何
scripts/*.py
之前检测Python是否缺失,若缺失则进行安装。检测方法为运行
python3 --version
/
python --version
——若失败,Claude会通过主机的原生包管理器获取Python。
各操作系统Python引导(若缺失,由Claude负责):
  • Windows
    winget install -e --id Python.Python.3.13 --silent --accept-source-agreements --accept-package-agreements
    。耗时约30秒,无UI,自动配置PATH。如果当前shell无法找到
    python
    ,请告知用户关闭并重新打开终端(Windows仅在新shell中更新PATH)。
  • macOS — 当前所有macOS版本通常预装了Python 3(路径为
    /usr/bin/python3
    ,由Apple提供)。若未安装,可通过Homebrew安装
    brew install python@3.13
    ;若未安装Homebrew,可提供安装命令
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    (但需用户确认——Homebrew比winget更复杂)。
  • Linux — 使用发行版包管理器。Debian/Ubuntu:
    sudo apt-get update && sudo apt-get install -y python3 python3-pip
    。Fedora:
    sudo dnf install -y python3 python3-pip
    。Arch:
    sudo pacman -S --noconfirm python python-pip
    。可能需要sudo权限,若需要请向用户显示密码提示。
pyserial — 随技能内置:
固定版本的
pyserial 3.5
存放在
scripts/vendor/
目录下(BSD-3-Clause协议,与Apache协议兼容)。所有导入
serial
的脚本会在首次导入第三方库前调用
vendor_path.ensure_on_syspath()
,将
scripts/vendor/
添加到
sys.path
开头,因此无论用户系统中安装了什么版本,都会使用内置的pyserial。效果:克隆仓库后无需执行pip步骤即可正常进行端口枚举和REPL I/O操作。体积约500 KB,纯Python实现,在macOS/Linux/Windows上完全一致。
esptool — pip依赖项,首次运行时自动安装:
esptool
采用GPLv2+协议,因此未内置——为保持仓库为Apache-2.0协议,GPL代码存放在用户的pip管理环境中,而非仓库内。技能的预检会检查是否可导入
esptool
,若缺失则提示安装(
python -m pip install --user esptool
——在虚拟环境中会省略
--user
参数,使安装包进入site-packages)。子进程调用使用
[sys.executable, "-m", "esptool", ...]
;子进程会继承用户站点,因此pip安装的模块可正常导入。
requirements.txt
中声明了该依赖,提示安装路径是首次使用且未运行过pip的用户的默认选择。
非交互式调用者(管道输入、CI)会跳过提示,直接显示
python -m pip install --user esptool
的提示信息。
若有人删除了
scripts/vendor/
目录的后备方案:
同样的预检流程会在vendor目录缺失时通过pip重新安装pyserial。这适用于用户下载了不含vendor目录的源码压缩包,或手动修剪仓库以节省空间的情况。
USB驱动 — Windows专属,仅适用于旧款开发板:
CH9102 USB-UART驱动在Windows上仍需手动安装——WCH未发布winget清单。仅适用于UART桥接开发板(Basic、Fire、Core2、StickC)。原生USB ESP32-S3开发板(Cardputer、Cardputer-Adv、CoreS3)会枚举为复合USB-CDC设备,使用Windows内置驱动,无需额外安装。

Platform notes

平台说明

The skill runs on macOS, Linux, and Windows. Non-obvious bits:
  • Port naming. pyserial abstracts the lookup but what the user sees looks different per OS. Pass whichever form
    detect.py
    reports:
    • macOS:
      /dev/cu.usbmodem1101
      (native USB) or
      /dev/cu.usbserial-XXXX
      (CH9102)
    • Linux:
      /dev/ttyACM0
      (native USB) or
      /dev/ttyUSB0
      (UART bridge)
    • Windows:
      COM3
      ,
      COM4
      , etc. (Device Manager → Ports if unsure)
  • Linux permissions — read this before blaming hardware. On most distros, accessing
    /dev/ttyUSB*
    /
    /dev/ttyACM*
    without sudo requires group membership (
    dialout
    on Debian/Ubuntu/Arch,
    uucp
    on Fedora). Symptom:
    detect.py
    finds the port, but the flash step fails with
    Permission denied
    or
    Could not open port
    . Fix once, long-term:
    bash
    sudo usermod -aG dialout $USER
    # log out / log back in — group change only takes effect for new sessions
    sudo python3 scripts/onboard.py ...
    works as a one-off but adding the group membership is strictly better because pyserial's port-open in user mode succeeds cleanly from then on.
  • Windows PATH gotchas. Python's
    pip install --user esptool
    lands the executable in
    %APPDATA%\Python\Python3XX\Scripts\
    . If that directory isn't on PATH,
    pip
    prints a warning and nothing else picks up the install.
    detect.py
    looks there directly as a backstop, so the skill still works even without PATH fixed. But if you're invoking esptool outside the skill (or hitting "esptool not found" errors from other tools), either:
    • Re-run the Python installer and tick "Add Python to PATH" (the install's default), OR
    • Add
      %APPDATA%\Python\Python3XX\Scripts
      to PATH via System Properties → Environment Variables, OR
    • Use
      python -m esptool ...
      which always works regardless of PATH.
  • Windows Store Python. Newer Windows 11 machines may have Python pre-installed via Microsoft Store. It works but has quirky PATH behavior (lives under
    %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.*\
    ).
    detect.py
    checks that location too. If you have the choice, the
    winget install Python.Python.3.13
    version is more predictable.
  • Bundle path resolution.
    install_apps.py
    's
    --src buddy
    shorthand resolves in this order:
    1. $M5_BUDDY_DIR
      if set — explicit override, always wins. Useful when you want to point at a fork or a customized bundle that isn't in this clone.
    2. The
      buddy/device/
      directory inside this repo, found via
      os.path.realpath(__file__)
      walking up from
      install_apps.py
      . Works for any clone location, including symlinked skill installs at
      ~/.claude/skills/m5-onboard/
      .
    3. ~/Downloads/m5stack/buddy/device
      .
    4. ~/Desktop/m5stack/buddy/device
      .
    Most installs hit (2). Set
    M5_BUDDY_DIR
    only for the unusual case of pointing at a bundle outside this clone:
    export M5_BUDDY_DIR=/path/to/buddy/device
    (Unix) or
    $env:M5_BUDDY_DIR="C:\path\to\buddy\device"
    (PowerShell).
  • Firmware cache. Downloaded firmware lands at
    ~/.cache/m5-onboard/
    (or
    $XDG_CACHE_HOME/m5-onboard/
    ), created at mode 0700 if missing. Cache files are MD5-verified at write time and re-verified on hit. Clearing the cache is safe; the next run re-downloads.
本技能支持macOS、Linux和Windows系统。以下是非显而易见的细节:
  • 端口命名:pyserial会抽象端口查找,但用户看到的端口名称因操作系统而异。传递
    detect.py
    报告的端口格式即可:
    • macOS:
      /dev/cu.usbmodem1101
      (原生USB)或
      /dev/cu.usbserial-XXXX
      (CH9102)
    • Linux:
      /dev/ttyACM0
      (原生USB)或
      /dev/ttyUSB0
      (UART桥接)
    • Windows:
      COM3
      COM4
      等(不确定时可查看设备管理器→端口)
  • Linux权限 — 先检查权限再怀疑硬件:在大多数发行版中,无需sudo访问
    /dev/ttyUSB*
    /
    /dev/ttyACM*
    需要用户属于特定组(Debian/Ubuntu/Arch为
    dialout
    ,Fedora为
    uucp
    )。症状:
    detect.py
    找到端口,但烧录步骤失败,提示
    Permission denied
    Could not open port
    。长期解决方案:
    bash
    sudo usermod -aG dialout $USER
    # 注销并重新登录——组变更仅对新会话生效
    临时解决方案是
    sudo python3 scripts/onboard.py ...
    ,但添加组成员身份更好,因为之后pyserial可在用户模式下正常打开端口。
  • Windows PATH问题:Python的
    pip install --user esptool
    会将可执行文件安装到
    %APPDATA%\\Python\\Python3XX\\Scripts\\
    。如果该目录不在PATH中,
    pip
    会打印警告,但其他工具无法找到安装包。
    detect.py
    会直接检查该目录,因此即使PATH未配置,技能仍可正常运行。但如果在技能外调用esptool(或其他工具提示“esptool not found”错误),可选择以下方法:
    • 重新运行Python安装程序,勾选“Add Python to PATH”(安装程序默认选项),或
    • 通过系统属性→环境变量将
      %APPDATA%\\Python\\Python3XX\\Scripts
      添加到PATH,或
    • 使用
      python -m esptool ...
      ,该方法不受PATH影响,始终有效。
  • Windows Store Python:较新的Windows 11机器可能通过Microsoft Store预装了Python。它可以正常工作,但PATH行为特殊(位于
    %LOCALAPPDATA%\\Packages\\PythonSoftwareFoundation.Python.*\\
    )。
    detect.py
    也会检查该位置。如果有选择,
    winget install Python.Python.3.13
    版本更可预测。
  • 应用包路径解析
    install_apps.py
    --src buddy
    简写会按以下顺序解析:
    1. 若设置了
      $M5_BUDDY_DIR
      ——显式覆盖,优先级最高。适用于指向不在本克隆仓库中的分支或自定义应用包的情况。
    2. 本仓库中的
      buddy/device/
      目录,通过
      os.path.realpath(__file__)
      install_apps.py
      向上遍历找到。适用于任何克隆位置,包括符号链接安装的技能(如
      ~/.claude/skills/m5-onboard/
      )。
    3. ~/Downloads/m5stack/buddy/device
    4. ~/Desktop/m5stack/buddy/device
    大多数安装会匹配第2种情况。仅在指向本克隆仓库外的应用包的特殊情况下设置
    M5_BUDDY_DIR
    :Unix系统使用
    export M5_BUDDY_DIR=/path/to/buddy/device
    ,PowerShell使用
    $env:M5_BUDDY_DIR="C:\\path\\to\\buddy\\device"
  • 固件缓存:下载的固件存放在
    ~/.cache/m5-onboard/
    (或
    $XDG_CACHE_HOME/m5-onboard/
    ),若目录不存在则创建,权限为0700。缓存文件在写入时会进行MD5验证,命中缓存时也会重新验证。清除缓存是安全的;下次运行会重新下载。",