بنقرة واحدة
upy-generate
第四步——业务代码生成。读取 scaffold 阶段的 project-manifest.json,下载驱动、生成 DI 架构的业务代码、Mock 层、单元测试。触发:upy-scaffold 完成后自动进入。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
第四步——业务代码生成。读取 scaffold 阶段的 project-manifest.json,下载驱动、生成 DI 架构的业务代码、Mock 层、单元测试。触发:upy-scaffold 完成后自动进入。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Analyze MicroPythonOS App ideas directly or when invoked by mpos-plan-app. Use to turn natural-language MPOS App requests into requirements, default app identity, manifest draft, Activity/Service plan, MPOS/LVGL API plan, dependency risk, test/deploy plan, mandatory MicroPythonOS resource links, and a JSON handoff before code generation.
Deploy or preview a MicroPythonOS app on desktop, web, device copy, MPK install, or installer/flash guidance paths. Use when Codex needs to launch a confirmed app for manual preview, copy it to a board with mpremote, validate an MPK on-device, or route firmware install and erase to install.micropythonos.com. Does not own app generation, static lint, packaging, or default smoke testing.
MicroPythonOS 基础开发知识库。提供代码架构、App/MPK 约束、LVGL 编程约定、MPY API reference、官方 docs 专题 reference、AGENTS 本地强约束。mpos-plan-app / mpos-analyze-app / mpos-prepare-deps / mpos-gen-app / mpos-test-app / mpos-package-app / mpos-deploy-app / mpos-publish-app 均依赖此 skill。
Generate, update, and repeatedly repair MicroPythonOS App code after requirements are confirmed. Use after mpos-analyze-app and optionally mpos-prepare-deps to create or modify an internal_filesystem/apps package directory with root MANIFEST.JSON, root icon_64x64.png, assets/*.py entrypoints/dependencies, dependency adapters, and validation results. Always defaults to a two-phase flow: first produce a generation plan and ask for confirmation, then write files only after explicit user confirmation. Supports repeated calls for user feature changes and test-failure repair loops. Does not analyze vague requirements, prepare external dependencies, package MPK files, deploy devices, flash firmware, publish to upystore, or rebuild lvgl_micropython.
Package and validate a single MicroPythonOS App as an MPK release artifact. Use when Codex needs to create a .mpk, validate an MPOS App manifest/icon/package structure, emit one app_index_entry.json fragment, run optional temporary install validation, or prepare AppStore/upystore publishing artifacts without uploading.
Orchestrate a MicroPythonOS App workflow across analyze, dependency preparation, generation, testing, packaging, deployment, and upystore publishing. Use when Codex needs to start from a natural-language app request, continue or resume an interrupted MPOS app task, decide the next mpos-* skill, maintain per-app plan_state.json and activity_log.jsonl under tmp/mpos-plan-app, handle user requirement changes with invalidation confirmation, or run the default path through mpos-publish-app. Does not implement code, download dependencies, test, package, deploy, flash, or upload directly.
| name | upy-generate |
| description | 第四步——业务代码生成。读取 scaffold 阶段的 project-manifest.json,下载驱动、生成 DI 架构的业务代码、Mock 层、单元测试。触发:upy-scaffold 完成后自动进入。 |
本项目生成的代码遵循 以单元测试为核心的嵌入式开发 方法论:
main.py(DI 装配入口)外,所有代码文件不 import machine。驱动通过工厂函数注入,任务函数通过参数接收驱动对象。import Mock 运行测试,无需任何硬件。test/pc/ 目录下的单元测试运行在 CPython,不依赖 MicroPython 运行时。lib/ 下的基础库(logger、time_helper 等)提供 CPython fallback 以支持此能力。给定 project-manifest.json(phase: scaffold),完成驱动下载、任务代码生成、DI 装配、测试生成。代码理解和生成由 LLM 直接完成,不再依赖正则脚本。
python --version
python -c "import requests; print('requests OK')"
python -c "import flake8; print('flake8 OK')"
python -c "import pylint; print('pylint OK')"
无外部依赖(Python 3 标准库 + requests + flake8 + pylint)。
python G:/MicroPython_Skills/upy-generate/scripts/download_drivers.py --project-dir {project_dir}
脚本从 upypi / GitHub 拉取驱动 .py 文件到 firmware/lib/,同时尝试拉取 README.md 和 code/main.py 作为参考材料:
| 文件 | 命名 | 用途 |
|---|---|---|
| 驱动 .py | lib/<driver>.py | 驱动源码,理解 API |
| README.md (upypi) | lib/<name>_README.md | 接线图、API 说明、使用流程 |
| code/main.py (upypi) | lib/<name>_example.py | 可运行示例,展示真实调用模式 |
| README.md (GitHub) | lib/<name>_README.md | 仓库文档 |
upypi / GitHub 源文件可能包含 \r\r\n 等非标准换行符,会导致 CPython 解析失败。下载后扫描 lib/*.py,若 compile 失败则自动修复:
cd {project_dir} && python -c "
import os, sys
for fn in os.listdir('firmware/lib'):
if not fn.endswith('.py'): continue
path = os.path.join('firmware/lib', fn)
try:
with open(path, 'rb') as f: data = f.read()
compile(data.decode('utf-8'), path, 'exec')
except (SyntaxError, IndentationError, UnicodeDecodeError):
# Normalize line endings and remove blank lines between backslash continuations
text = data.decode('utf-8', errors='ignore')
text = text.replace('\r\n', '\n').replace('\r', '\n')
lines = text.splitlines(keepends=True)
result = []; i = 0
while i < len(lines):
line = lines[i]; result.append(line)
if line.rstrip('\n').endswith('\\\\'):
i += 1
while i < len(lines) and lines[i].strip('\n').strip() == '': i += 1
else: i += 1
text = ''.join(result)
with open(path, 'w', encoding='utf-8') as f: f.write(text)
print(f'[FIX] {fn} line endings normalized')
"
---
## Phase 2: LLM 理解驱动 → 生成工厂 + Mock
**对 manifest.devices 中的每个 device,逐一执行:**
### 2A: 阅读理解(Mock API 的唯一来源)
**Mock 的方法签名、参数、返回值类型必须从下载到 `firmware/lib/` 的驱动源码中直接提取 —— 不依赖任何外部的 api_ref 字符串或预置数据库。**
读取以下文件(如果存在):
firmware/lib/.py ← 驱动源码(Mock API 的权威来源) firmware/lib/_README.md ← 使用文档 firmware/lib/_example.py ← 示例代码
从源码中理解:
- **主驱动类是哪个**:如果有多个类(如 `SSD1306` 基类 + `SSD1306_I2C` 子类),选 I2C/SPI 子类;没有子类则选第一个非 Exception 类
- **__init__ 参数**:区分 I2C 地址、Pin 脚号、配置参数
- **公开方法(Mock 覆盖目标)**:所有不以 `_` 开头的方法,逐一记录方法签名(参数名、默认值)和返回值类型。这些将直接决定 Mock 类的方法列表和默认返回值
- **是否是自建 I2C 的驱动**:`self.i2c = I2C(...)` → 需要修改驱动以支持外部传入 i2c
example.py 可以告诉你真实的调用方式:
```python
# 从 example 知道:BMP280(i2c=i2c, address=addr)、read_compensated_data() 返回 (temp, press, hum)
如果驱动在 __init__ 中自己创建 I2C(如 SHT30 的 self.i2c = I2C(scl=Pin(...), sda=Pin(...))),直接修改 lib/<driver>.py:
def __init__(self, ...) 末尾加 , i2c=Noneself.i2c = I2C(...) 包在 if i2c is not None: self.i2c = i2c else: ... 里self.i2c_addr 等后续赋值留在 if/else 外面(两种分支都需要)drivers/<name>_driver/__init__.py工厂文件是 DI 链条的第一环,导出 create_<name> 和 scan_<name>_i2c 两个函数,行为必须一致可靠。
参考来源:下载的 lib/<name>_example.py(即驱动包的 code/main.py)展示了真实的 I2C 地址、通信速率配置和调用方式,LLM 必须阅读该文件后再生成工厂代码。
# -*- coding: utf-8 -*-
# @Generated : upy-generate
# @File : drivers/<name>_driver/__init__.py
# @License : MIT
try:
from lib.<module> import <DriverClass>
except ImportError:
<DriverClass> = None
# Default I2C address from driver source / example.py
_<NAME>_DEFAULT_ADDR = <default_addr>
def create_<name>(i2c, address=None):
"""Create <DriverClass> instance. Returns driver object or None on failure."""
if i2c is None:
return None
try:
obj = <DriverClass>(i2c=i2c, address=address if address is not None else _<NAME>_DEFAULT_ADDR)
return obj
except Exception as e:
from lib.logger import warning
warning('Driver <NAME> init failed: {}'.format(e))
print('[DRIVER] <NAME> init failed:', e)
return None
def scan_<name>_i2c(i2c, address=None):
"""Scan I2C bus for <NAME>. Returns True if device found at address.
Does NOT create a driver instance — only checks if the I2C address
responds on the bus. Used by upy-deploy-test for hardware connectivity check.
NOTE: Some sensors (e.g. GT911) have software-configurable addresses
that cannot be detected via passive scan. For these, consult the
downloaded example.py — scan may return False until the device is
initialized, or require a different detection strategy.
"""
if i2c is None:
return False
addr = address if address is not None else _<NAME>_DEFAULT_ADDR
return addr in i2c.scan()
i2c, address=None,扫描函数独立于驱动实例pin_num,内部创建 Pin(pin_num, Pin.OUT)。GPIO 器件没有现成驱动,需要 LLM 从头写合理的封装(on/off/toggle/value),Mock 同样覆盖这些方法。GPIO 器件不需要 scan 函数spi, cs_pin,内部创建 Pin(cs_pin, Pin.OUT)i2c, address=Nonedrivers/<name>_driver/mock.pyMock 的方法列表和签名直接从 Phase 2A 阅读驱动源码的结果中提取,覆盖驱动所有公开方法,默认返回值与驱动返回类型匹配:
# -*- coding: utf-8 -*-
# @Generated : upy-generate
# @File : drivers/<name>_driver/mock.py
# @License : MIT
class Mock<Name>:
"""Mock <NAME> — same API as real driver, returns canned data."""
def __init__(self, **kwargs):
self._<method1> = kwargs.get('<method1>', <default>)
...
def <method1>(self):
return self._<method1>
...
默认值规则:
0True(<典型值>),如 (25.0, 60.0)self._xxx,方法体直接 pass如果 manifest.scaffold_mode 为 async:
默认规则:驱动本身不改(保持同步,async 化在封装层完成),工厂函数返回同步驱动对象不变。task 函数形态和 main.py 入口由 scaffold 骨架阶段决定,generate 阶段沿用。
例外 —— 驱动中存在阻塞式方法时必须修改驱动源码:
I2S 外设等硬件阻塞操作:查阅 MicroPython asyncio 官方文档确认是否有异步替代 API。若有(如 I2S.readinto() 的异步版本),在工厂封装层包装;若没有,需在驱动中增加非阻塞轮询路径。
time.sleep() / time.sleep_ms() 等延时:驱动中如果有延时等待(如传感器上电稳定等待、复位后延时),必须修改:
time.sleep(n) 替换为 await asyncio.sleep(n) 或 await asyncio.sleep_ms(n)async def轮询等待 I2C 响应等忙等待:改为 await asyncio.sleep_ms() 让步给事件循环。
修改驱动后,更新对应 Mock 的方法签名(async def 匹配),task 协程中用 await 调用。
读取 manifest.requirements.description(用户需求描述)和 manifest.devices 列表 → 生成 task 文件。
约束(LLM 必须在生成代码中满足):
纯函数 + DI:task 是纯 Python 函数,不 import machine,所有硬件通过参数传入驱动对象
独立异常处理:每个传感器/器件的读写操作独立 try/except,一个挂了不影响其他
print() 与 lib.logger 强制双写:install_rotating() 会 monkey-patch 所有 logger 输出(包括 debug()/info()/warning()/error())重定向到 /log/run_*.log 文件,REPL 完全看不到。因此 print() 是 REPL 唯一可见通道,必须严格遵守以下规则:
必须双写(logger + print 同时存在)的场景:
debug(msg) + print(msg)debug(msg) + print(msg)info(msg) + print(msg)warning/error(msg) + print(msg)warning/info(msg) + print(msg)不可双写(只用一处)的场景:
debug()print()日志消息需带模块标识前缀(如 [sensor]、[display]、[alarm]),具体格式由 LLM 自主决定
温度键冲突处理:当同时有温湿度传感器和气压传感器时,气压传感器的温度写入 pressure_temp,避免覆盖主温度
无硬编码阈值:所有阈值、间隔从 conf.py 导入
装饰器:timer 模式用 @timed_function,async 模式用 @timed_coro
task 日志插入点(LLM 必须在对应位置插入,级别和具体措辞可自主调整):
| 位置 | 级别 | 双写 | 内容要求 |
|---|---|---|---|
| 传感器读取成功 | debug | 必须 | 含传感器名 + 实际读值。debug(msg) + print(msg) 缺一不可 |
| 传感器读取失败 | warning | 必须 | 含传感器名 + 异常信息。warning(msg) + print(msg) |
| 报警触发 | warning | 必须 | 含参数名 + 当前值 + 阈值。warning(msg) + print(msg) |
| 报警恢复 | info | 必须 | 含参数名 + 当前值。info(msg) + print(msg) |
| 显示更新 | debug | 必须 | 含当前显示内容摘要。debug(msg) + print(msg) |
| 显示更新失败 | warning | 必须 | 含异常信息。warning(msg) + print(msg) |
| 未预期 Exception | error/exception | 必须 | 含上下文(哪个 task、在处理什么数据)。logger + print 双写 |
| 纯内部状态(GC 统计、计数器递增等) | debug | 可选 | 用户不关心的内部细节,logger 记录即可,可不 print |
LLM 自主决定:
读取 scaffold 生成的 firmware/conf.py,按项目需求补充业务常量。
约束:
LOG_DIR = "/log"
LOG_MAX_FILES = 4
LOG_LINES_PER_FILE = 150
日志消息原则(LLM 生成所有日志消息时遵守):
[sensor]、[alarm]),方便在日志文件中 grep/搜索debug=开发调试细节(可默认关闭)、info=关键流程节点、warning=可恢复异常、error/critical=不可恢复错误LLM 自主决定: 根据 manifest.requirements.description 推断需要的常量名和值(告警阈值、校准值、显示间隔等),日志消息中的时间戳格式和模块标识前缀。
读取所有工厂文件 + task 文件 + conf.py → 生成 main.py。
约束:
import time; time.sleep(3) # Boot delay: allow mpremote to reconnect after reset
这确保 deploy/autofix 的持久会话在 main.py 输出第一行日志之前就已连接,不会遗漏任何启动输出。
machine.I2C/Pin → 工厂 create_xxx() → 驱动对象 → task 函数参数,完整串联from lib.logger import install_rotating, getLogger, info, warning, error, setLevel, DEBUG, INFO
from conf import LOG_DIR, LOG_MAX_FILES, LOG_LINES_PER_FILE, LOG_LEVEL
# 安装轮转日志 —— 所有 logger 输出写入 /log/run_*.log
# fmt 参数由 LLM 自主决定,不指定则使用默认 "%(levelname)s:%(name)s:%(message)s"
install_rotating(LOG_DIR, max_files=LOG_MAX_FILES, lines_per_file=LOG_LINES_PER_FILE)
# 激活 conf.LOG_LEVEL —— install_rotating() 后 logger 输出只进文件,
# setLevel() 控制哪些级别真正写入(DEBUG 全量 / INFO 跳过 debug)
if LOG_LEVEL == 'DEBUG':
setLevel(DEBUG)
else:
setLevel(INFO)
_log = getLogger("main")
补一句 print() 输出固件名称+版本到 REPL,再 info(...) 写入日志。
为什么 print() + 轮转日志两者都要:
print() → REPL 实时输出,mpremote 连接时即时可见/log/run_*.log,REPL 断开后仍可通过 mpremote fs 读取回溯G:\MicroPython_Claude_Assistant\device\main.py(第 13-24 行)scan_<name>_i2c(i2c),结果用 info/warning + print 双写main.py 日志插入点(LLM 必须在对应位置插入,级别和具体措辞可自主调整):
| 位置 | 级别 | 内容要求 |
|---|---|---|
| 固件启动 | info | 项目名 + 版本号 + 板名 |
| 轮转日志安装 | debug/info | LOG_DIR 路径 |
| I2C 总线初始化 | info | bus id + SCL/SDA pin 号 |
| 各驱动创建成功 | info | 驱动名 + I2C 地址 |
| 各驱动创建失败 | warning | 驱动名 + 异常信息 |
| I2C 扫描结果 | info/warning | 器件名 + found/missing |
| 调度器/事件循环启动 | info | tick_ms 或等效参数 |
| 调度器主循环异常 | error | 致命错误,含 traceback |
LLM 自主决定: import 组织、初始化顺序、install_rotating(fmt=...) 参数、启动日志具体措辞、GPIO/SPI 器件的创建方式。
test/pc/test_<task>.py约束(LLM 必须在生成代码中满足):
from drivers.<name>_driver.mock import Mock<Name>sys.path.insert(0, 'firmware') 确保能在 CPython 下导入 firmware/ 下的模块LLM 自主决定: 测试类命名、Mock 参数配置、断言内容和数量、是否扩展更多边界用例。
test/device/test_smoke.py只测硬件可用性,不测业务逻辑。
只用 MicroPython unittest 支持的 assert 方法(assertTrue、assertEqual、assertIsNotNone、assertRaises),禁止使用 assertIsInstance、assertIn、assertNotIn、assertGreater 等 CPython 扩展。
from machine import I2C, Pin
import unittest
from drivers.<name>_driver import scan_<name>_i2c
class TestSmoke(unittest.TestCase):
def test_i2c_bus_scan(self):
i2c = I2C(<bus_id>, scl=Pin(<scl>), sda=Pin(<sda>))
devices = i2c.scan()
self.assertTrue(len(devices) > 0, "No I2C devices found on bus")
def test_<name>_present(self):
i2c = I2C(<bus_id>, scl=Pin(<scl>), sda=Pin(<sda>))
self.assertTrue(scan_<name>_i2c(i2c),
"<NAME> not found at expected address")
LLM 自主决定: 为每个 I2C 器件生成对应的 test_<name>_present 方法,添加 GPIO/SPI 器件的硬件测试。Pin 脚号从 manifest 读取。
PC 端测试(test/pc/)会遇到 MicroPython 特有模块在 CPython 不存在的问题。生成代码时应遵守架构约束(除 main.py 外不 import machine),但如果 lib/ 下的文件(如 logger、time_helper)使用了 micropython.const、time.ticks_ms 等 MPY 专有 API,需要确认它们有 CPython fallback。如果发现缺失,顺手补上。
cd {project_dir} && python -m flake8 firmware/ --extend-exclude=firmware/lib --max-line-length=120 2>&1
项目目录下需存在 .pylintrc,忽略 MicroPython 特有模块的 import-error:
[MASTER]
ignore-paths=^firmware/lib/.*\.py$
ignore-patterns=test_
[MESSAGES CONTROL]
disable=
import-error, # machine/micropython 等 MPY 模块在 CPython 不存在
no-member, # MPY 动态注入成员 pylint 无法推断
no-name-in-module,
c-extension-no-member
[TYPECHECK]
ignored-modules=
machine,micropython,pyb,esp,esp32,espnow,rp2,mimxrt,zephyr,wipy,stm,
neopixel,network,bluetooth,framebuf,uctypes,cryptolib,deflate,btree,
vfs,openamp,lcd160cr,WM8960,
uasyncio,uselect,utime,ujson,uos,ustruct,ure,uzlib,uhashlib,
ubinascii,ucollections,urandom,uerrno,uheapq,uselect,ussl,usocket
[FORMAT]
max-line-length=120
cd {project_dir} && python -m pylint firmware/ --rcfile=.pylintrc 2>&1
有错直接修,修完重新验证,直到 pass。
这是关键拦截点:CPython 有大量内置模块(logging、collections、typing、pathlib 等),MicroPython 没有。PC 端测试通过不代表设备上能运行。
扫描 firmware/ 下所有 .py 文件(排除 firmware/lib/),提取顶层 import X 和 from X import Y 中的 X,逐一检查是否在已知 MicroPython 可用模块白名单中:
MicroPython 内置 + stdlib 白名单(63 个):
sys, os, time, machine, micropython, gc, math, cmath, struct, json,
binascii, collections, errno, hashlib, io, platform, random, re, select,
socket, ssl, array, network, bluetooth, framebuf, uctypes, cryptolib,
deflate, btree, vfs, openamp, lcd160cr, neopixel, esp, esp32, espnow,
rp2, mimxrt, zephyr, wipy, stm, uasyncio, uarray, ubinascii, ucollections,
ucryptolib, uctypes, uerrno, uhashlib, uheapq, uio, ujson, uos, uplatform,
urandom, ure, uselect, usocket, ussl, ustruct, utime, uzlib, threading
LLM 执行步骤:
firmware/lib/ 外部驱动):cd {project_dir} && find firmware -name "*.py" -not -path "firmware/lib/*" | sort
逐个文件读取,提取顶层 import X 和 from X import Y 中的模块名 X(忽略 . 开头的相对导入)
对每个 X 做三层判断:
X 在白名单中?
→ 跳过(MPY 原生支持)
X 不在白名单,但 firmware/ 下存在 X.py 或 X/__init__.py?
→ 错误:应改为相对导入。示例:
文件 firmware/tasks/my_task.py 中有 import logging
→ firmware/lib/logger/logging.py 存在
→ 修复为 from lib.logger import logging 或相对路径导入
X 不在白名单,firmware/ 下也不存在?
→ 警告:MicroPython 可能无此模块。LLM 判断是否需要替代方案
常见陷阱:
import typing → MicroPython 不支持,删除类型注解导入
import pathlib → 改用 os.path / os.mkdir 等
import dataclasses → 改用普通类
import collections → 只用 list/dict(MPY 有 ucollections 但功能受限)
import logging → 用本项目的 lib.logger.logging
发现可自动修复的导入错误(同项目内有对应 .py 文件)→ 直接改,改完标注 [FIX]
发现不可自动修复的(无替代模块)→ 打印 warning,记录到审查清单,Phase 8 最终审查时处理
此检查在 flake8/pylint 之后执行,因为 pylint 已配置 ignored-modules 白名单跳过 MPY 模块的 import-error,但那是为了 CPython lint 通过——本检查是反过来,在 CPython 能通过但 MPY 不通过的情况做拦截。
这是关键拦截点:Phase 4 补充 conf.py 时可能加入常量,但 Phase 3 已生成的 task 文件未必引用。结果是配置项定义了但无人使用——"看起来有配置,实际不生效"。
LLM 执行步骤:
conf.py 中所有模块级 UPPER_CASE = value 常量名(排除 __ 开头的 Python 内置变量):cd {project_dir} && python -c "
import re, sys
with open('firmware/conf.py', 'r', encoding='utf-8') as f:
text = f.read()
# Match module-level CONSTANT = value
consts = set(re.findall(r'^([A-Z][A-Z_0-9]+)\s*=', text, re.MULTILINE))
print('\n'.join(sorted(consts)))
"
firmware/ 下所有 .py 文件(排除 firmware/lib/ 外部驱动和 firmware/conf.py 自身),检查是否被引用:引用形式:
import conf → 后续 conf.CONSTANT_NAME ✓
from conf import X → 直接使用 X ✓
from conf import (X, Y) → X, Y 被使用 ✓
死配置 (定义于 conf.py 但未被任何 firmware/*.py 引用):
BUZZER_ALARM_DURATION_MS → 无文件使用
BUZZER_ALARM_INTERVAL_MS → 无文件使用
死配置的语义是否明确需要?
├─ 是(如 BUZZER_ALARM_DURATION_MS 表示"蜂鸣器应脉冲而非长响")
│ → 修 task 文件:在对应逻辑中引入此常量,让配置生效
│
└─ 否(如某个 LLM 自行发明的、无实际用途的常量)
→ 从 conf.py 中删除,保持配置干净
典型陷阱(LLM 必须注意):
LOG_DIR、SAMPLE_INTERVAL_MS 等)不会被误判——它们一定被 main.py 或模板文件引用conf.py 中仅被 test/ 目录引用的常量不算死配置——测试也算使用main.py DI 装配阶段使用,不在 task 文件中出现——这正常Phase 2-7 完成后,LLM 执行最终审查,逐项核验:
firmware/lib/*.py 驱动源码,确认工厂 __init__.py 的 import 和构造函数调用与真实驱动一致manifest.requirements.description,确认每个需求点都有对应的 task 处理manifest.output 中列出的输出器件(display/buzzer/led)都有对应的 task 和 main.py 装配test/pc/test_*.py 是否覆盖正常 + 异常(传感器报错)+ 边界(传感器为 None)三种情况test/device/test_smoke.py 是否只用 MPY unittest 允许的 assert 方法firmware/lib/ 下的外部驱动是否有 CPython 特有的绝对导入(如 import typing、import logging)——这些在设备上会 ImportErrorcd {project_dir} && python -c "
import json, os
from datetime import datetime, timezone
path = 'project-manifest.json'
with open(path, 'r', encoding='utf-8') as f:
m = json.load(f)
m['phase'] = 'generate'
m['generate'] = m.get('generate', {})
m['generate']['generated_at'] = datetime.now(timezone.utc).isoformat()
with open(path, 'w', encoding='utf-8') as f:
json.dump(m, f, ensure_ascii=False, indent=2)
print('[OK] manifest phase → generate')
"
---
## 参考文档
生成代码时遇到 API 不确定的情况,查阅以下权威文档:
| 文档 | URL | 用途 |
|------|-----|------|
| MicroPython 官方文档 | https://docs.micropython.org/en/latest/index.html | 所有 MPY 标准库 API 参考 |
| asyncio 协程 | https://docs.micropython.org/en/latest/library/asyncio.html | async 模式:`create_task`、`sleep_ms`、`gather`、`Event`、`Queue` 等 |
| _thread 多线程 | https://docs.micropython.org/en/latest/library/_thread.html | _thread 模式:`start_new_thread`、`allocate_lock`、`exit` 等 |
| unittest 源码 | https://github.com/micropython/micropython-lib/blob/master/python-stdlib/unittest | MPY unittest 支持的 assert 方法参考(仅 `assertTrue`/`assertEqual`/`assertIsNotNone`/`assertRaises`) |
---
## 架构约束(生成代码必须遵守)
- **鸭子类型 DI**:task 函数接收对象,不关心是真实驱动还是 Mock
- **除 main.py 外不 import machine**:所有 .py 文件不直接 import machine.Pin/I2C/SPI/UART
- **除 lib/ 外不使用 typing 泛型**:类型注解只用 MPY 内置类型
- **独立 try/except**:每个 task_tick 独立异常处理,一个传感器挂了不影响其他
- **阈值在 conf.py**:不硬编码数值
- **Mock 方法签名与真实驱动一致**:通过阅读驱动源码和 example.py 确认
- **所有 I2C 驱动工厂必须导出 `scan_<name>_i2c(i2c, address)` 函数**:不依赖驱动实例,仅扫描 I2C 总线检查地址是否存在,为后续调试/部署 skill 提供硬件连通性判断依据。软件可配置地址的器件(如 GT911)需查阅 example.py 确定检测策略
- **日志写入设备文件系统**:main.py 启动时安装轮转日志(`lib.logger.install_rotating`)。关键流程节点 `print()` + `lib.logger` 双写(REPL 即时可见 + 设备端持久化);纯调试细节可只用 `print()`。即使 REPL 断开,日志仍可通过 mpremote 读取设备端文件
---
## 与其他 skill 的关系
- ← `upy-scaffold`:输入骨架 + manifest
- → `upy-deploy-test`:上传代码到设备并验证
- → `upy-wiring`:引脚分配表 → 接线图
- → `upy-diagram`:代码结构 → 架构图
---
## 强约束
- **可以修改 lib/ 下的驱动源码**(仅限:①添加 DI 支持如 i2c=None 参数 ②修复非标准换行符影响 CPython 编译)
- **不生成全局 firmware/mock/ 目录**
- **async 模式下驱动默认不改**,但如果驱动存在阻塞式方法(`time.sleep`、I2S 阻塞操作等),必须修改为异步版本并查阅 asyncio 官方文档确认 API;异步化也可在封装层处理
- **生成结束自动 flake8 + pylint 验证 + PC 测试运行**,不通过不结束
- **lib/ 下文件需保证 CPython 兼容**:`micropython.const`、`time.ticks_ms` 等 MPY 专有 API 要有 fallback(logger、time_helper 等 scaffold 文件可能遗漏)
- **设备端测试只用 MPY unittest assert 子集**:`assertTrue`、`assertEqual`、`assertIsNotNone`、`assertRaises`,禁止 `assertIn`/`assertIsInstance` 等