一键导入
upy-autofix
第六步——编排协调层。读取设备日志,解析错误,分级决策后委托上游 skill 修复(generate/select-hw/analyze),最多 3 次尝试。触发:upy-deploy 运行失败后自动进入。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
第六步——编排协调层。读取设备日志,解析错误,分级决策后委托上游 skill 修复(generate/select-hw/analyze),最多 3 次尝试。触发:upy-deploy 运行失败后自动进入。
用 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-autofix |
| description | 第六步——编排协调层。读取设备日志,解析错误,分级决策后委托上游 skill 修复(generate/select-hw/analyze),最多 3 次尝试。触发:upy-deploy 运行失败后自动进入。 |
编排协调层,不是独立修复机。 核心逻辑:triage.py 采集结构化数据 → LLM 读取 JSON + 原始日志 → 分级决策 → 委托上游 skill 修复 → 验证。
脚本只做数据采集 + git 管理 + 硬件信号驱动,不做修复决策。所有判断由 LLM 完成。
新增:硬件信号验证能力 — 软件修复 2 次无效后,autofix 可以主动驱动外设(LED 闪烁/蜂鸣器响/传感器读数/显示器填色),通过自检或用户反馈判定硬件是否正常,避免在坏硬件上无限循环修代码。
upy-deploy Phase 6 判定 FAILdeploy_logs/ 目录下有设备端原始日志文件(run_*.log)triage.py 可用(本 skill 自带的脚本)python G:/MicroPython_Skills/upy-autofix/scripts/triage.py \
--log-dir {deploy_logs路径} \
--port {COM} \
--attempt 1
输出 JSON 到 stdout,LLM 捕获并解析。JSON 结构见 triage.py 文件头部注释。
每个字段都有默认值——脚本已做 try/except,不会因日志格式异常而崩溃。warnings 字段列出所有降级情况。
LLM 同时读取两个信息源:
| 来源 | 作用 | 何时读 |
|---|---|---|
| triage.py JSON | 快速定位:错误类型、P 级别、I2C 状态、attempt 计数 | 每次都读 |
| deploy_logs/*.log 原始日志 | 深度理解:完整 traceback、print 时序、上下文 | JSON 不足以判断时 |
研判顺序:
先看 JSON 的 i2c_ok 字段
false → 硬件问题,跳 Step 5(输出排查指引),不修代码true 或 null(无 I2C 设备)→ 软件问题,继续看 JSON 的 p_level + error_type
unknown → 读原始日志,LLM 独立判断错误类型后走对应路径判断是否需要硬件信号验证(Step 2.5)——任一满足即触发:
attempt >= 2 且同一 error_type 连续出现 → 软件修复无效,怀疑硬件error_type = "NoOutput" 或 "unknown" → 代码跑了但没产出,怀疑沉默外设error_type = "OSError_19" 或 "OSError_110" → 外设通信失败,可能是接线/供电当 Step 2 触发条件命中时,不直接进入软件修复。先做硬件诊断。
LLM 通读 firmware/ 全部 .py 源码 + project-manifest.json:
识别对象:
├── machine.Pin(x) → GPIO 引脚, 外设角色(从变量名/上下文推断)
├── machine.PWM(Pin(x)) → PWM 输出 (LED/蜂鸣器/舵机)
├── machine.I2C(...) → I2C 总线 + 从设备地址
├── machine.SPI(...) → SPI 总线 + CS 引脚
├── machine.UART(...) → UART 总线 + 波特率
├── machine.ADC(Pin(x)) → 模拟输入
└── from xxx import Xxx → 驱动类实例化, __init__ 参数模式
然后按附录 A 的模板,为每个外设生成 sanity_config.json:
生成原则:
python G:/MicroPython_Skills/upy-autofix/scripts/hardware_sanity.py \
--config {project_dir}/sanity_config.json \
--port {COM}
输出 JSON 到 stdout,LLM 捕获。
读 JSON:
├── 全部 PASS → 硬件正常,问题在代码逻辑 → 继续 Step 3 委托修复
│
├── 特定外设 FAIL (I2C 传感器 scan 失败 / 读 WHO_AM_I 不匹配)
│ → 输出该外设定向排查指引 (接线/供电/地址冲突)
│ → 不继续修代码
│
├── 特定外设 FAIL (用户反馈型: LED不亮/蜂鸣器不响)
│ → 输出该外设定向排查指引
│ → 不继续修代码
│
├── 板载 LED 也 FAIL → MCU 供电/复位/USB 线问题
│ → 输出基础排查指引 (换USB线/换供电口/检查EN引脚)
│
└── pending_feedback == true
→ AskUserQuestion 逐条询问(每条一问)
→ 收集回答后重新判定
hardware_sanity.py 对 user_feedback 模式的测试会在结果中标记 _pending_question。LLM 读取后:
对每条 pending:
AskUserQuestion(
question: result._pending_question,
header: "硬件诊断",
options: ["是,正常", "否,没有反应"]
)
用户回答后:
"是" → 该外设 status = "pass"
"否" → 该外设 status = "fail"
反馈汇总后再判定一次:
最多让用户回答 3 个问题。如果有 4+ 个反馈型外设,优先测"最可能故障"的那个(根据 triage error_type 指向)。
LLM 使用 Skill 工具调用上游 skill,打包 error context:
委托 upy-generate 时传入:
委托 upy-select-hw 时传入:
委托 upy-analyze 时传入:
每次修复后的验证路径:
修复完成
↓
可选:Skill("upy-simulate") PC 端快速验证(2-3s,省去串口烧录延迟)
↓
Skill("upy-deploy") 重新烧录运行
↓
再次运行 triage.py(--attempt N+1)
↓
读 JSON:
├─ status="pass" → 成功,输出 PASS 摘要
└─ status="fail" → 回到 Step 2 重新研判(可能升级回退层级)
升级规则:同一策略连续失败 → 向上一级回退(P0 直接改 → P0 委托 generate → P1 委托 select-hw → 需求层面 analyze)。
当 i2c_ok: false(已尝试 software I2C + 降速均无效)时,LLM 直接输出以下中文指引,不修代码:
I2C 总线扫描不到设备,已尝试 software I2C 和低速模式,均无响应。这是硬件连接问题,请按以下顺序排查:
1. 接线检查:
- SDA/SCL/VCC/GND 每根线用万用表通断档确认导通
- VCC 接 3.3V(不是 5V!)
- GND 必须与 MCU 共地
2. 供电检查:
- 模块电源指示灯是否亮?
- VCC 引脚电压是否为 3.3V ± 0.1V?
3. 上拉电阻:
- SDA/SCL 各需 4.7kΩ 上拉到 3.3V
- 部分模块自带,部分没有——检查你的模块
4. 传感器本身:
- 是否发热异常?
- 替换另一块同型号传感器测试
5. 冲突检查:
- 拔掉其他外设,只接这一个传感器测试
排查完成后发送"重新部署"重试。
python G:/MicroPython_Skills/upy-autofix/scripts/triage.py --rollback --log-dir {deploy_logs路径}
然后 LLM 输出中文卡点报告:
自动修复 3 次未成功。
错误类型:{error_type}
3 次尝试:
1. {strategy1} → {result1}
2. {strategy2} → {result2}
3. {strategy3} → {result3}
git 已回滚到修复前状态。
建议手动排查方向:{具体建议}
triage.py 自动将每次修复的历史写入 logs/error_report.json(追加模式),包含:时间戳、MCU 型号、错误类型、traceback、每次尝试的策略和结果、使用的 skill 版本。
LLM 在 3 次全败时额外补充 llm_analysis 字段(根因分析 + 知识盲区标记)。
upy-deploy FAIL
↓
upy-autofix (本 skill)
├── triage.py → 采集数据
├── LLM 研判
├── [新增] hardware_sanity.py → 硬件信号驱动验证
├── 委托 → upy-generate(代码修复)
├── 委托 → upy-select-hw(引脚/地址修复)
├── 委托 → upy-analyze(需求重新分析)
├── 可选验证 → upy-simulate(PC 快速验证)
├── 重新部署 → upy-deploy
└── 失败回流 → CI/CD 反馈到各 skill SKILL.md
upy-deploy:接收 FAIL 判定 + deploy_logs/ 日志upy-generate:委托代码修复upy-select-hw:委托引脚/地址重新分配upy-analyze:委托需求重新分析upy-simulate:可选 PC 验证upy-deploy:修复后重新烧录attempt >= 2 连续同错误 / NoOutput / OSError_19/110 — 不浪费用户时间--snapshot)error_type: "unknown",此时 LLM 必须从原始日志独立判断error_report.json,驱动 CI/CD 持续改进mpremote fs cp 和 resume exec 在 Windows 上进入 raw REPL 模式(发送 Ctrl+C),会杀死正在运行的 main.py。而且被杀死的进程可能未 flush 日志文件,读到空文件会误判"无日志输出"。正确做法:让 main.py 运行到自然结束或崩溃 → 软复位后(程序已停)再 fs cp 抓日志。如需在运行时检查设备状态,用 hardware_sanity.py 的 I2C 扫描(独立于 main.py 进程)LLM 读取 firmware/ 源码识别外设类型后,按以下模板生成 sanity_config.json 中的 test code。
模板结构:
{
"id": "peripheral_name_sanity",
"category": "i2c_sensor|spi_sensor|uart|gpio_out|display|adc|dac|input",
"mode": "self_verify|user_feedback",
"label": "中文名 (引脚/地址)",
"code": "完整 MicroPython 代码, mpremote exec 执行",
"pass_pattern": "SCAN_OK|CHIP_OK|FRAME_OK|ADC_OK|DAC_OK|TEST_DONE",
"fail_pattern": "SCAN_FAIL|CHIP_FAIL|FRAME_FAIL",
"value_key": "TEMP|VOLT|RAW",
"value_range": [min, max],
"question": "仅 user_feedback 模式: AskUserQuestion 的问题",
"timeout_ms": 10000
}
识别信号: __init__(i2c, address=0x??) 或 I2C(0, scl=Pin(x), sda=Pin(y))
自检步骤: i2c.scan() 确认地址 → (可选) 读 WHO_AM_I/CHIP_ID → 读一次数据 → 物理合理范围判定
通用模板:
from machine import I2C, Pin
i2c = I2C({bus_id}, scl=Pin({scl}), sda=Pin({sda}), freq=400000)
addrs = [hex(a) for a in i2c.scan()]
if '{expected_addr}' in addrs:
print('SCAN_OK')
else:
print('SCAN_FAIL:' + str(addrs))
# 读一次数据
from {driver_module} import {DriverClass}
s = {DriverClass}(i2c, address={addr_int})
try:
r = s.{read_method}()
print('VALUE:' + str(r))
except Exception as e:
print('READ_ERR:' + str(e))
示例 — BMP280 (地址 0x76):
from machine import I2C, Pin
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
addrs = [hex(a) for a in i2c.scan()]
if '0x76' in addrs:
print('SCAN_OK')
else:
print('SCAN_FAIL:' + str(addrs))
from bmp280_float import BMP280
s = BMP280(i2c, address=0x76)
data = s.read_compensated_data()
print('TEMP:' + str(data[0]) + ',' + 'PRESS:' + str(data[1]))
pass_pattern: "SCAN_OK", value_key: "TEMP", value_range: [-40, 85]
识别信号: __init__(spi, cs) 或 SPI(1, sck=Pin(x), mosi=Pin(y), miso=Pin(z))
自检步骤: 读 WHO_AM_I / CHIP_ID / DEVICE_ID 寄存器 → 与 datasheet 预期值比对
通用模板:
from machine import SPI, Pin
spi = SPI({bus_id}, sck=Pin({sck}), mosi=Pin({mosi}), miso=Pin({miso}))
cs = Pin({cs}, Pin.OUT)
cs.value(1)
# 读 WHO_AM_I 寄存器
cs.value(0)
spi.write(bytearray([0x80 | {who_am_i_reg}]))
resp = spi.read(1)
cs.value(1)
if resp[0] == {expected_id}:
print('CHIP_OK:' + hex(resp[0]))
else:
print('CHIP_FAIL: expected ' + hex({expected_id}) + ' got ' + hex(resp[0]))
pass_pattern: "CHIP_OK", fail_pattern: "CHIP_FAIL"
示例 — ADXL345:
from machine import SPI, Pin
spi = SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
cs = Pin(9, Pin.OUT); cs.value(1)
cs.value(0)
spi.write(bytearray([0x80])) # DEVID register
resp = spi.read(1)
cs.value(1)
if resp[0] == 0xE5:
print('CHIP_OK')
else:
print('CHIP_FAIL: got ' + hex(resp[0]))
pass_pattern: "CHIP_OK", fail_pattern: "CHIP_FAIL"
识别信号: __init__(uart) 或 UART(1, baudrate=9600, tx=Pin(x), rx=Pin(y))
自检步骤: 发查询命令 → 等响应 → 超时判定
通用模板:
from machine import UART, Pin
import time
u = UART({uart_id}, baudrate={baud}, tx=Pin({tx}), rx=Pin({rx}), timeout=2000)
u.write(b'{query_cmd}')
time.sleep(0.5)
resp = u.read()
if resp and len(resp) > 0:
print('FRAME_OK:' + str(resp[:32]))
else:
print('FRAME_FAIL: no response')
pass_pattern: "FRAME_OK", fail_pattern: "FRAME_FAIL"
示例 — PMS7003 (主动模式,等数据帧):
from machine import UART, Pin
import time
u = UART(2, baudrate=9600, tx=Pin(17), rx=Pin(16), timeout=5000)
t0 = time.time()
while time.time() - t0 < 5:
if u.any():
d = u.read()
if d and len(d) >= 2 and d[0] == 0x42 and d[1] == 0x4D:
print('FRAME_OK: start bytes valid')
break
else:
print('FRAME_FAIL: no valid frame in 5s')
pass_pattern: "FRAME_OK", fail_pattern: "FRAME_FAIL"
示例 — SIM800/SIM7600 (AT 指令):
from machine import UART, Pin
import time
u = UART(2, baudrate=115200, tx=Pin(17), rx=Pin(16), timeout=3000)
u.write(b'AT\r\n')
time.sleep(1)
resp = u.read()
if resp and b'OK' in resp:
print('FRAME_OK: AT response')
else:
print('FRAME_FAIL: ' + str(resp))
识别信号: __init__(pin: int) → Pin(x, Pin.OUT) 或 PWM(Pin(x))
测试步骤: PWM/电平周期性变化 ×3 → 问用户
通用模板:
from machine import Pin, PWM
import time
p = PWM(Pin({pin}), freq=1000)
for i in range(3):
p.duty_u16(32768); time.sleep(0.3)
p.duty_u16(0); time.sleep(0.3)
p.deinit()
print('TEST_DONE')
pass_pattern: "TEST_DONE", question: "{label} {动作描述}?(y/n)"
变体 — LED:
from machine import Pin, PWM
import time
p = PWM(Pin({pin}), freq=1000)
for _ in range(3):
for d in range(0, 65535, 16384): # 渐亮
p.duty_u16(d); time.sleep(0.05)
p.duty_u16(0); time.sleep(0.2)
p.deinit()
print('TEST_DONE')
question: "LED({pin}引脚) 闪烁了3次吗?"
变体 — 蜂鸣器:
from machine import Pin, PWM
import time
p = PWM(Pin({pin}), freq=1000)
for f in [440, 660, 880]: # A4, E5, A5
p.freq(f); p.duty_u16(32768)
time.sleep(0.25)
p.duty_u16(0)
time.sleep(0.1)
p.deinit()
print('TEST_DONE')
question: "蜂鸣器响了3声吗?"
变体 — 继电器:
from machine import Pin
import time
p = Pin({pin}, Pin.OUT)
for _ in range(3):
p.value(1); time.sleep(0.3)
p.value(0); time.sleep(0.3)
print('TEST_DONE')
question: "听到继电器咔哒声了吗?"
识别信号: __init__(i2c/spi, ...) + .fill() / .show() 方法
测试步骤: 全屏交替填充 → 问用户
通用模板:
from machine import {bus_type}, Pin
{init_code}
# 全屏闪烁
display.fill(1); display.show()
import time; time.sleep(0.5)
display.fill(0); display.show()
time.sleep(0.5)
display.fill(1); display.show()
print('TEST_DONE')
question: "{label} 屏幕闪烁了吗?"
示例 — SSD1306 (I2C):
from machine import I2C, Pin
from ssd1306 import SSD1306
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
display = SSD1306(128, 64, i2c)
display.fill(1); display.show()
import time; time.sleep(0.5)
display.fill(0); display.show()
time.sleep(0.5)
display.fill(1); display.show()
print('TEST_DONE')
示例 — ST7789 (SPI, 彩色):
from machine import SPI, Pin
from st7789 import ST7789
spi = SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
dc = Pin(8, Pin.OUT); cs = Pin(9, Pin.OUT); rst = Pin(7, Pin.OUT)
display = ST7789(spi, 240, 240, dc=dc, cs=cs, rst=rst)
import time
for color in [0xF800, 0x07E0, 0x001F]: # 红→绿→蓝
display.fill(color); time.sleep(0.5)
print('TEST_DONE')
question: "屏幕显示红→绿→蓝了吗?"
识别信号: __init__(i2c, address=0x??) + read() 含 channel/gain 参数
自检步骤: 读悬空通道 vs 读 VCC 或固定电压 → 差值判定
通用模板:
from machine import I2C, Pin
i2c = I2C({bus_id}, scl=Pin({scl}), sda=Pin({sda}))
from {driver_module} import {DriverClass}
adc = {DriverClass}(i2c, address={addr})
import time
r1 = adc.read(channel1=0)
time.sleep(0.1)
r2 = adc.read(channel1=0)
diff = abs(r1 - r2)
if diff < 50:
# 读数太稳定可能是悬空读噪声 或 短路
# 尝试读另一个通道
r3 = adc.read(channel1=3)
if abs(r1 - r3) > 100:
print('ADC_OK: ch0=' + str(r1) + ', ch3=' + str(r3))
else:
print('ADC_FAIL: all channels same ~' + str(r1))
else:
print('ADC_OK: fluctuation ' + str(diff))
pass_pattern: "ADC_OK", fail_pattern: "ADC_FAIL"
识别信号: __init__(i2c, address=0x??) + write() + read() (有读回)
自检步骤: write(中值) → read() → 对比
通用模板:
from machine import I2C, Pin
i2c = I2C({bus_id}, scl=Pin({scl}), sda=Pin({sda}))
from {driver_module} import {DriverClass}
dac = {DriverClass}(i2c, address={addr})
dac.write(2048)
import time; time.sleep(0.05)
state = dac.read()
if state and len(state) >= 2:
val = (state[0] << 4) | (state[1] >> 4)
if abs(val - 2048) < 100:
print('DAC_OK: wrote=2048, read=' + str(val))
else:
print('DAC_FAIL: wrote=2048, read=' + str(val))
else:
print('DAC_FAIL: no response')
pass_pattern: "DAC_OK", fail_pattern: "DAC_FAIL"
变体 — 无 read() 的 DAC (如部分 MCP4725 实现):
不测值回读,改为 write(0) → write(2048) → write(0) 接万用表测电压,此时降级为 user_feedback 模式。question: "DAC 输出引脚电压在 write(0) 和 write(2048) 之间有变化吗?"
识别信号: __init__(pin, ...) + 含 callback/idle_state/debounce
测试步骤: 读初值 → 打印 → 等待 → 读终值
通用模板:
from machine import Pin
import time
p = Pin({pin}, Pin.IN, Pin.PULL_UP)
print('INIT_VAL:' + str(p.value()))
time.sleep(6) # 给用户操作时间
print('FINAL_VAL:' + str(p.value()))
question: "请在6秒内{操作描述}(按按钮/旋转编码器/在PIR前移动),REPL 输出的 INIT_VAL 和 FINAL_VAL 值变化了吗?"
示例 — 按钮:
from machine import Pin
import time
p = Pin(5, Pin.IN, Pin.PULL_UP)
print('INIT:' + str(p.value()))
time.sleep(6)
print('FINAL:' + str(p.value()))
print('TEST_DONE')
question: "请在6秒内按下按钮,REPL 中 INIT 和 FINAL 的值变化了吗?"
何时用: 任何 sanity check 的第一步。如果这个失败,不用测其他外设。
from machine import Pin
import time
led = Pin({pin}, Pin.OUT)
for _ in range(4):
led.value(1); time.sleep(0.2)
led.value(0); time.sleep(0.2)
print('LED_OK')
pass_pattern: "LED_OK", question: "板载LED闪烁了吗?"
注意:板载 LED 虽属 GPIO 输出,但它是 MCU 供电/复位/烧录成功的最基本标志。如果 LED_OK 都不打印 → 设备根本没进入 REPL。
LLM 读 firmware/ 源码:
│
├── 找到 Pin(x, Pin.OUT) / PWM(Pin(x)) 且变量名含 "led"
│ → A9: 板载 LED 基础测试 (优先级最高,放 config.tests[0])
│
├── 找到 I2C(0, scl=Pin(x), sda=Pin(y))
│ └── 遍历所有 I2C 驱动类实例
│ → A1: I2C 传感器 (self_verify)
│
├── 找到 SPI(1, sck=Pin(x), ...)
│ └── 遍历所有 SPI 驱动类实例
│ → A2: SPI 传感器 (self_verify)
│
├── 找到 UART(1, baudrate=..., tx=Pin(x), rx=Pin(y))
│ └── 遍历所有 UART 驱动类实例
│ → A3: UART 外设 (self_verify 或 user_feedback)
│
├── 找到 Pin(x, Pin.OUT) / PWM(Pin(x)) 且变量名含 "buzzer"/"relay"/"motor"
│ → A4: GPIO 输出 (user_feedback)
│
├── 找到 I2C/SPI 驱动 含 .fill() / .show() 方法
│ → A5: 显示器 (user_feedback)
│
├── 找到 I2C 驱动 含 .read() + channel/gain 参数
│ → A6: ADC (self_verify)
│
├── 找到 I2C 驱动 含 .write() + .read()
│ → A7: DAC (self_verify)
│
└── 找到 Pin(x, Pin.IN) 含 callback/idle_state
→ A8: 输入器件 (semi_auto)
测试顺序:板载 LED → I2C 传感器 → SPI 传感器 → UART → ADC/DAC → 显示器 → GPIO 输出 → 输入器件。前一个 FAIL 且是基础级别的(LED / 供电相关),后面的不跑了。