本文定位 :给参赛学生 + AI 编程助手(ESP-Claw Web / 任意大模型)直接使用的【任务书与代码骨架】——把本文连同 §二引脚表整段喂给 AI,即可生成可烧录运行的完整代码。交叉引用(只引用不复述) :结构、绳路、参数初值与验收依据 02-纪念桥主体详案 §3;AI 管线状态机、提示词与日志格式见 05-图生文软件路径与提示词模板;Claw 辅助编程话术、预存脚本清单与现场降级见 06-分工-硬件程序交ESP-Claw-人工接线调机;规则与总降级表以 01-比赛执行手册 为准。编号说明 :01/02 旧文内"代码见 03、图生文见 04、Claw 见 05"的编号已顺延,实际文件为本文件 04 、05、06。本文即全套方案的"机构代码"正式篇。
一、软件架构总览#
1.1 两主控分工(一张图定死)#
┌──────────────────────── M10 = AI 主控(Debian / Python)──────────────────────┐
│ B1 按钮(外部,P0) ──触发──▶ 图生文管线(05 §一:抓帧→API→日志01~06→改写上屏) │
│ USB 摄像头 640×480 │ 2.8" 屏(状态字+短文)│ 喇叭(可选播报)│ WiFi→多模态API │
│ 与 K10:仅可选串口/人肉握手:TRIG / STATE / ESTOP(不通不阻塞演示) │
└──────────────────────────────────┬───────────────────────────────────────────┘
│ USB 串口(可选,05 §1.4 人肉握手兜底)
┌──────────────────────── K10 = 机构主控(ESP32-S3 / MicroPython)─────────────┐
│ S1 360°连续舵机 ←PWM(GPIO12)─ 绕线柱绞盘 → 绳 → 定滑轮 → 献花台(行程120mm) │
│ B2 按钮(GPIO11):IDLE 短按=启动一轮循环;运动中按=急停 │
│ 状态机:IDLE → UP → HOLD → DOWN → IDLE + ESTOP(急停,见 §1.2) │
│ 供电:舵机独立 5V(充电宝/笔记本 USB)与主控共地;机械限位兜底 │
└──────────────────────────────────────────────────────────────────────────────┘
LED 长明灯=CR2032 独立供电(不进任何代码);直流电机=仅电池盒闸刀通断(备用,不进主程序)
分工铁律 :M10 只管"看图说话",K10 只管"升降急停",两边互不 import、互不阻塞 ;唯一耦合是可选的一行文本握手(§二)。AI 侧状态机(待机→拍摄中→生成中→请改写→已上屏)与日志目录结构全部执行 05 ,本文不复述。
1.2 机构侧状态机(K10,安全相关,禁止 AI 改名/删转移)#
B2 短按(上升沿) N_UP 到时 / 或 B2 急停
┌──────────────────────────▶ UP ─────────────────────────┐
│ (S1=UP占空比, 计时2.6s) │ B2 按下
│ ▼
IDLE ◀──────── N_DOWN 到时 ─────── DOWN ◀── HOLD(停1.0s)─┤
(停止7.5%) (S1=DOWN占空比,计时2.6s) (到位停留) │
▲ │
└──────────── B2 释放后复位 ◀────── ESTOP ◀──────────────┘
(S1=立即7.5% 中停;任何运动状态按下 B2 即入)
状态 S1 输出 持续条件 出口 IDLE 停止占空比 7.5% 等 B2 B2 上升沿 → UP UP 上行占空比 85% 计时 N_UP_S=2.6s 到时→HOLD;B2→ESTOP HOLD 停止 7.5% 计时 HOLD_S=1.0s 到时→DOWN;B2→ESTOP DOWN 下行占空比 15% 计时 N_DOWN_S=2.6s 到时→IDLE;B2→ESTOP ESTOP 立即停止 7.5% 等 B2 释放 释放→IDLE(需重新按才启动)
要点:①运动中的计时循环每 20ms 轮询一次 B2 ,急停响应目标 ≤100ms(实测记入 §六);②行程靠计时 控制,机械限位(02 §3.3)永远兜底,软件永不过限位继续用力;③一轮循环 = UP→HOLD→DOWN→IDLE,演示时可连按多次。
1.3 与 AI 侧的时序关系#
两台主控各自独立跑自己的循环,机构演示与图生文可并行 (06 §一分工总表)。若接了串口握手:K10 进入 UP 时发 STATE,UP,M10 可在收到 STATE,IDLE(一轮完成)后才允许下一次 B1 触发——这是优化项不是依赖 ,握手失败按 05"人肉握手"照常演示。
1.4 启动与运行方式(赛前定死主备,现场不试第三种)#
步骤 动作 归属 1 舵机独立 5V 上电 → K10 上电 → init_servo() 打停止占空比,观察 300ms 归位无飞车 06 阶段 A4 2 运行机构程序:主 =Web/Console 执行 scripts/bridge_servo.py(或 .lua),备 =Mind+ 点一下开始 06 §四"主 1 条+备 1 条" 3 M10 单独冷启动:m10_trigger.py 进入"待机"状态字,摄像头自检 05 §四彩排 4 两边各自跑通后,才接可选串口线;接不上就宣布人肉握手,不再花时间 05 §1.4
启动顺序的原则:机构先安全(先中位后动作),AI 先独立(不依赖机构状态) 。任何一边没起来,另一边照常演示。
二、引脚与接口契约表#
2.1 K10(机构主控)#
接口 信号 引脚常量 方向/模式 接线与约束 3Pin Gravity I/O #1 S1 舵机信号 SERVO_PIN = 12(GPIO12)输出,PWM 50Hz 与本文 §二引脚契约 同一约定 ;信号线只进此口,严禁接 5V 3Pin Gravity I/O #2 B2 升降循环/急停 BTN_PIN = 11(GPIO11)输入,内部上拉,低电平有效 对应 params.lua 的 button2.pin=11;消抖 30ms;丝印标 升降/急停 USB Type-C 可选串口 ↔ M10 CDC 虚拟串口 双向文本行,HANDSHAKE=False 默认关 只传 TRIG/STATE/ESTOP 一行指令;不通=人肉握手(05 §1.4) 2Pin 电池口 K10 自供电 — — 与舵机 5V 分开 ,只共地
2.2 M10(AI 主控)#
接口 信号 引脚常量 说明 扩展板数字口 P0 B1 AI 触发 B1_PIN = "P0"按下→跑一次 05 图生文管线;丝印标 AI触发 USB-A USB 摄像头 640×480 — 抓帧与日志按 05 §1.1 2.8" 屏 状态字 + 40–80 字短文 — 状态字用 05 §1.4 的五个词,不自造 USB(对 K10) 可选串口 /dev/ttyACM0 类115200 或 CDC 自协商;发送 TRIG,接收 STATE,* WiFi 多模态 API — 断网降级只执行 05 L1–L3 板载 A 键 05 中"按钮②确认"的落点 — 外接按钮只有 2 个 ;确认/下一段用板载 A 键或自动定稿(联调二选一,TODO)
2.3 电气与按钮总契约(代码之外,先对线再上电)#
项 契约 按钮总数 仅 2 个外部按钮 :B1→M10(AI),B2→K10(机构)。禁止再加第三个按钮逻辑舵机供电 独立 5V(充电宝/笔记本 USB);与 K10 逻辑地单点共地 ;共地断=舵机不转(06 口诀第 1 条) LED 长明灯 CR2032 独立,代码不管 直流电机 仅电池盒+闸刀通断,不经 PWM、不进主程序 (备用件) 机械限位 上=绳结碰滑轮(+120mm),下=桥面垫块;软件只做计时
2.4 给 AI 编程助手的"禁止改动约定"(贴进对话框顶部)#
引脚按 §2.1/§2.2 已记忆值写死 ,不得自行换脚、不得新增引脚;除非用户出示新接线表。参数表 PARAMS 的字段名与结构禁止改动 ,只允许修改数值(N_UP_S / N_DOWN_S / HOLD_S / UP_DUTY_PCT / DOWN_DUTY_PCT)。状态机五个状态名与转移关系禁止增删改 (含急停路径);B2 双语义(空闲=启动、运动中=急停)不得弱化。不得引入新第三方依赖、不得联网、不得改 05 的 prompt/日志目录结构。 输出文件路径必须按 §五约定;保留全部 TODO 注释供学生联调填写。 不得删除消抖、急停轮询、异常即停(try/finally → stop())这三段安全代码。
三、K10 机构控制代码骨架#
3.1 主程序(MicroPython,完整可运行风格 · scripts/bridge_servo.py)#
python 复制
# -*- coding: utf-8 -*-
"""
scripts/bridge_servo.py — K10 机构主控:舵机绞盘升降献花台(纪念桥 1:100)
契约:04 §二引脚表;参数初值:02 §3.5(联调后填死,现场不再改结构)
运行:Mind+ / MicroPython 烧录,或 ESP-Claw Web/Console 执行(06 §四)
状态机:IDLE → UP → HOLD → DOWN → IDLE + ESTOP(04 §1.2)
"""
import sys
import time
from machine import Pin, PWM
# ---------- 参数表:字段/结构禁止改动,只许改数值(TODO 联调填死) ----------
PARAMS = {
"UP_DUTY_PCT": 85.0, # S1 上行占空比 %(360°连续舵机)
"DOWN_DUTY_PCT": 15.0, # S1 下行占空比 %
"STOP_DUTY_PCT": 7.5, # S1 停止(中位)占空比 %
"N_UP_S": 2.6, # 上行计时行程 s ≈ 120mm(02 §3.4 运动学)
"N_DOWN_S": 2.6, # 下行计时行程 s
"HOLD_S": 1.0, # 到位停留 s
"SERVO_PIN": 12, # S1 → 3Pin I/O #1(= 本文 §二契约 servo1.pin)
"BTN_PIN": 11, # B2 → 3Pin I/O #2(低电平有效,内部上拉)
"DEBOUNCE_MS": 30, # B2 消抖
"POLL_MS": 20, # 运动中急停轮询周期(响应 ≈ 消抖+轮询 ≤100ms)
"HANDSHAKE": False, # TODO: 接妥 M10 USB 串口后改 True;默认人肉握手
}
BAUD = 115200
# ---------- 状态名(禁止改动) ----------
ST_IDLE, ST_UP, ST_HOLD, ST_DOWN, ST_ESTOP = "IDLE", "UP", "HOLD", "DOWN", "ESTOP"
state = ST_IDLE
cycle_no = 0
btn_raw = True # 原始电平(上拉,未按下=True)
btn_stable = True
btn_last_change = 0
btn_just_pressed = False # 消抖后的上升沿(按下沿)置位,主循环消费
# ---------- 可选串口握手(04 §1.3;默认关闭,不影响单机演示) ----------
try:
import uselect
except ImportError:
uselect = None
ser_sel = None
def serial_init():
"""TODO: 与 M10 的 USB CDC 串口就绪后返回选择器;失败返回 None=人肉握手。"""
global ser_sel
if not PARAMS["HANDSHAKE"] or uselect is None:
return None
try:
ser_sel = uselect.select([sys.stdin], [], [], 0)[0]
except Exception:
ser_sel = None
return ser_sel
def serial_poll():
"""非阻塞读一行指令:TRIG=等效启动,ESTOP=急停,?=回报状态。"""
global state
if ser_sel is None:
return
try:
if uselect.select([sys.stdin], [], [], 0)[0]:
line = sys.stdin.readline().strip()
if line == "TRIG" and state == ST_IDLE:
state = ST_UP # 与 B2 同权的远程启动(可选)
elif line == "ESTOP" and state != ST_IDLE:
state = ST_ESTOP
elif line == "?":
sys.stdout.write("STATE,%s,CYCLE,%d\n" % (state, cycle_no))
except Exception:
pass # 串口异常不阻塞机构:按人肉握手降级(05/06)
def serial_send(msg):
if ser_sel is not None:
try:
sys.stdout.write(msg + "\n")
except Exception:
pass
# ---------- 舵机底层 ----------
servo = None
def init_servo():
"""S1 初始化:50Hz PWM,先打停止占空比再交控制权,防止上电飞车。"""
global servo
servo = PWM(Pin(PARAMS["SERVO_PIN"]), freq=50)
set_duty_pct(PARAMS["STOP_DUTY_PCT"])
time.sleep_ms(300) # TODO 联调:舵机上电归位等待,可微调
print("[INIT] servo GPIO%d stop=%.1f%%" % (PARAMS["SERVO_PIN"], PARAMS["STOP_DUTY_PCT"]))
def set_duty_pct(pct):
"""占空比% → PWM。TODO: 若联调发现脉宽映射不符(02 §3.5),只改参数表数值。"""
if servo is None:
return
servo.duty_u16(int(pct / 100.0 * 65535))
def stop():
"""立即中停(状态机所有出口最终都走这里)。"""
set_duty_pct(PARAMS["STOP_DUTY_PCT"])
# ---------- B2 轮询(消抖 + 边沿) ----------
def btn_scan():
"""每轮主循环调用:更新消抖状态,产出 just_pressed 边沿。"""
global btn_raw, btn_stable, btn_last_change, btn_just_pressed
now = time.ticks_ms()
level = btn_pin.value() # 低电平=按下
if level != btn_raw:
btn_raw = level
btn_last_change = now
if btn_raw != btn_stable and time.ticks_diff(now, btn_last_change) >= PARAMS["DEBOUNCE_MS"]:
if btn_stable and not btn_raw: # 原来未按、现在按住 → 下降沿(按下)
btn_just_pressed = True
btn_stable = btn_raw
def btn_clear_edge():
global btn_just_pressed
btn_just_pressed = False
def btn_pressed():
"""当前是否按住(急停判定用,含消抖后的稳定电平)。"""
return not btn_stable
# ---------- 计时行程(内嵌急停轮询) ----------
def _run_for(duty_pct, seconds, tag):
"""按占空比运行 seconds 秒;期间每 POLL_MS 查一次 B2。
返回 True=正常到时;False=被急停(调用方负责进 ESTOP)。"""
set_duty_pct(duty_pct)
print("[RUN] %s duty=%.1f%% t=%.1fs" % (tag, duty_pct, seconds))
t0 = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), t0) < int(seconds * 1000):
btn_scan()
serial_poll()
if btn_pressed() or state == ST_ESTOP:
return False
time.sleep_ms(PARAMS["POLL_MS"])
return True
def up():
"""上行:S1=UP占空比,计时 N_UP_S 秒(行程120mm,02 §3.4)。"""
return _run_for(PARAMS["UP_DUTY_PCT"], PARAMS["N_UP_S"], "UP")
def down():
"""下行:S1=DOWN占空比,计时 N_DOWN_S 秒。"""
return _run_for(PARAMS["DOWN_DUTY_PCT"], PARAMS["N_DOWN_S"], "DOWN")
def hold():
"""到位停留:中停 HOLD_S 秒(同样可被急停打断)。"""
stop()
t0 = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), t0) < int(PARAMS["HOLD_S"] * 1000):
btn_scan()
serial_poll()
if btn_pressed() or state == ST_ESTOP:
return False
time.sleep_ms(PARAMS["POLL_MS"])
return True
def estop():
"""急停动作:立即中停 + 状态落 ESTOP + 对外通报。"""
stop()
serial_send("STATE,ESTOP")
print("[ESTOP] cycle=%d" % cycle_no)
# ---------- 主状态机 ----------
btn_pin = None
def main():
global state, cycle_no, btn_pin
btn_pin = Pin(PARAMS["BTN_PIN"], Pin.IN, Pin.PULL_UP)
init_servo()
serial_init()
print("[IDLE] 等待 B2 启动循环;运动中按 B2=急停")
serial_send("STATE,IDLE")
while True:
btn_scan()
serial_poll()
if state == ST_IDLE:
if btn_just_pressed:
btn_clear_edge()
cycle_no += 1
state = ST_UP
serial_send("STATE,UP,CYCLE,%d" % cycle_no)
elif state == ST_UP:
if not up(): # 急停打断
estop(); state = ST_ESTOP; continue
state = ST_HOLD
elif state == ST_HOLD:
if not hold():
estop(); state = ST_ESTOP; continue
state = ST_DOWN
elif state == ST_DOWN:
if not down():
estop(); state = ST_ESTOP; continue
stop()
state = ST_IDLE
serial_send("STATE,IDLE,CYCLE,%d" % cycle_no)
print("[DONE] cycle=%d 全程完成" % cycle_no)
elif state == ST_ESTOP:
if not btn_pressed(): # B2 松开才允许回到 IDLE(防连击误启动)
stop()
state = ST_IDLE
serial_send("STATE,IDLE")
print("[IDLE] 急停解除,等待重新启动")
time.sleep_ms(10)
if __name__ == "__main__":
try:
main()
except Exception as e:
stop() # 任何异常先中停(安全第一)
print("[FATAL]", e)
raise
给 AI 的填空点(只留 TODO,其余锁死) :上电归位等待 300ms、占空比→脉宽换算、HANDSHAKE 开关、握手协议字段。其余(状态名、消抖、急停轮询、try/finally 中停)不得重写 。
3.3 与 02 §3.5 参数表的对应(联调后"填死"的唯一通道)#
02 §3.5 参数表字段 本文 PARAMS 字段 联调动作 S1 上行占空比 85% UP_DUTY_PCT实测速度偏快/偏慢时改此数值 S1 下行占空比 15% DOWN_DUTY_PCT同上;若方向反了与上行对调数值 S1 停止占空比 7.5% STOP_DUTY_PCT仅当上电不归中时微调 N 上行/下行时间 2.6s N_UP_S / N_DOWN_S用 §六 50 次计时的均值回填 到位延时 1.0s HOLD_S展示需要更久时改,急停仍可打断
联调闭环 :跑 test_50cycles.py → 把均值抄回 PARAMS → 再跑 10 次确认 → 才在 02 §3.5"11/14 实测"列签字填死。现场只执行、不再改 (06 §三现场只许改参数的口径,与本文一致)。
3.2 Lua 移植对照(ESP-Claw 预存脚本 · scripts/bridge_servo.lua)#
按 06 §四"程序赛前写死、现场只跑"的口径,同一状态机可移植为 Lua(Claw Web/Console 执行)。骨架如下,参数仍集中在 params.lua,现场只改那一个文件 :
lua 复制
-- scripts/bridge_servo.lua(Claw 预存;参数引用本文 §二契约与参数表,勿改结构)
local P = require("params") -- {servo1={pin=12,...}, button2={pin=11}}
local UP,DOWN,STOP = 85,15,7.5 -- TODO 联调后从 params 读并填死
local N,HOLD_MS = 2600,1000
local st,idle,up,hold,down,estop = "IDLE","IDLE","UP","HOLD","DOWN","ESTOP"
local function stop() hw.pwm(servo_pin, STOP) end
local function btn() return hw.gpio_read(btn_pin)==0 end -- 低电平按下
-- 主循环:IDLE 按下→UP;UP/HOLD/DOWN 每 20ms 查按钮,按下→estop()→等释放→IDLE
-- TODO: 用 Claw 的 hwmap 核对 pin=12/11 占用后 run;结构与 Python 版一致
二选一原则 :赛前定死"MicroPython 主跑 + Lua 热备"或反之(06 §四"主 1 条 + 备 1 条"),现场不试第三种。
四、M10 触发侧代码骨架(只写触发与显示接口)#
管线内部(抓帧、prompt、重试、日志 01~06、L0–L3 降级)全部在 05 ,本节不复述、不复制。本文件假定 05 形态 A 已提供 ai_pipeline.run_pipeline()。
python 复制
# -*- coding: utf-8 -*-
"""scripts/m10_trigger.py — M10:B1 触发 → 05 图生文管线 → 屏显。
只含触发/显示/可选握手;prompt 与日志逻辑一律见 05,不在此实现。"""
import time
# TODO: 按 05 §1.2 实际形态改导入名;期望返回:
# {"ok": bool, "text": str, "log_dir": str, "status": str}
# status 取值必须来自 05 §1.4 状态字: 待机/拍摄中/生成中/请改写/已上屏
from ai_pipeline import run_pipeline
B1_PIN = "P0" # §2.2 契约,禁止改脚
K10_SERIAL = None # TODO: 接妥 USB 串口后填端口,如 "/dev/ttyACM0"
STATUS_WORDS = ["待机", "拍摄中", "生成中", "请改写", "已上屏"] # 引自 05,只引用
def show(state, text="", log_tag=""):
"""屏显状态字 + 短文 + 日志角标(05 §1.1 要求角落打 `日志: AI-XXX`)。
TODO: 用 M10 实际显示库(如 unihiker/screen)实现;text 只显示定稿 40–80 字。"""
assert state in STATUS_WORDS, "状态字必须取自 05 §1.4"
print("[屏显] %s | %s | %s" % (state, text[:20], log_tag))
def read_b1():
"""读 B1:按下=True。TODO: 按 M10 扩展库读 P0,低/高电平有效联调填死。"""
raise NotImplementedError("TODO: 读 P0 数字口")
def notify_k10(msg):
"""可选握手:向 K10 发 TRIG 等。失败静默——按 05 人肉握手,不阻塞 AI 管线。"""
if K10_SERIAL is None:
return
try:
import serial
with serial.Serial(K10_SERIAL, 115200, timeout=0.1) as s:
s.write((msg + "\n").encode())
except Exception:
pass
def main():
show("待机")
last = False
while True:
now = read_b1() # TODO: 消抖可复用 04 §三写法
if now and not last: # B1 下降沿触发一次
notify_k10("TRIG") # 可选:让 K10 同步启动升降
result = run_pipeline() # ← 05 的管线函数,本文不展开
if result and result.get("ok"):
show("已上屏", result.get("text", ""), result.get("log_dir", ""))
else:
show("待机") # 失败走 05 L1–L3,不在本文件处理
last = now
time.sleep(0.05)
if __name__ == "__main__":
main()
边界 :05 中"按钮②确认/下一段"在本硬件契约下用 M10 板载 A 键 或"学生改写后自动定稿"替代(§2.2 TODO,联调定死一种)——外接按钮永远只有 B1/B2 两个。
五、给 AI 编程助手的任务书模板(直接粘贴)#
任务书正文(≤10 条,学生只需填〔〕):
目标 :为行空板 K10(ESP32-S3,MicroPython)生成机构控制程序 scripts/bridge_servo.py,驱动 360°连续舵机 S1 完成"上升 2.6s→停 1s→下降 2.6s→待机"循环,B2 按钮启动,运动中按 B2 急停。引脚按已记忆契约写 :S1=GPIO12、B2=GPIO11(低电平有效内部上拉);M10 侧 B1=P0。禁止自行换脚 。参数表 PARAMS 只许改数值、禁止改字段名与结构 ;本次允许调整的只有 N_UP_S / N_DOWN_S / HOLD_S / UP_DUTY_PCT / DOWN_DUTY_PCT,其余字段原样保留。状态机锁死 :状态名仅 IDLE/UP/HOLD/DOWN/ESTOP,转移与急停路径按任务书附图,不得增删。按占空比计时行程实现 init_servo / up / down / stop 四个函数,计时循环内每 20ms 轮询 B2 ,消抖 30ms。 任何异常必须先 stop() 中停再抛出;保留 HANDSHAKE 可选串口开关(默认 False)。 输出路径 :MicroPython 版 → scripts/bridge_servo.py;Claw Lua 版 → scripts/bridge_servo.lua(参数对齐 06 的 params.lua);M10 触发版 → scripts/m10_trigger.py。禁止改名、禁止输出到其他目录 。中文注释,保留全部 TODO 标记;不引入第三方库、不联网、不触碰 05 的 prompt/日志结构。 〔联调补充:本次只把 N 改为 ____ s,占空比改为 ____%〕——除该行外不接受其他修改指令。
生成后验证清单(学生逐项打勾,全绿才算可用):
语法检查:python -m py_compile scripts/bridge_servo.py(或 mpy-cross)零报错 引脚/参数表 diff:与 §二、PARAMS 结构逐字段一致,无新增字段 上电自检:S1 打停止占空比后无抖动、无飞车 (06 阶段 A4 单路点动) 单次循环:按 B1…按 B2 → UP 2.6s → 停 1s → DOWN 2.6s → IDLE,秒表误差 ≤0.5s 急停 10 次:运动中按 B2,≤100ms 内肉眼即停 ,松开后需再按才重启 连续 50 次跑通(§六记录表),无过热、无卡绳 拔掉串口线:单机仍可完整演示(握手必须是可选项)
六、联调测试脚本(11/14 全链路联调用,验收口径对齐 02 §3.7)#
6.1 测试脚本骨架(scripts/test_50cycles.py,K10 上运行)#
python 复制
# -*- coding: utf-8 -*-
"""50 次循环计时 + 到位误差 + 急停响应记录。
依赖:与 bridge_servo.py 同目录;每 10 次输出一行汇总(02 §3.7 要求)。"""
import time
from bridge_servo import (init_servo, up, down, hold, stop, estop,
btn_scan, btn_pressed, btn_clear_edge, PARAMS)
MM_PER_STROKE = 120.0 # 行程 mm(02 §3.1)
N = 50
rows = [] # (序号, 上行s, 下行s, 到位误差mm, 温升备注)
def timed(fn):
t0 = time.ticks_ms()
ok = fn()
return ok, time.ticks_diff(time.ticks_ms(), t0) / 1000.0
def one_cycle(i):
"""跑一轮并计时;误差按"实测时长 / 名义时长 × 行程"折算(TODO 联调改用直尺实测)。"""
ok1, tu = timed(up)
if not ok1:
return None
hold()
ok2, td = timed(down)
stop()
if not ok2:
return None
err_mm = abs(tu - PARAMS["N_UP_S"]) / PARAMS["N_UP_S"] * MM_PER_STROKE
return (i, round(tu, 2), round(td, 2), round(err_mm, 1))
def estop_latency_test(times=10):
"""急停响应:按住 B2 起到 stop() 止,目标 ≤100ms。"""
results = []
for _ in range(times):
# TODO: 联调时人工在 UP 段按下 B2,脚本测 ticks 差;此处留接口
pass
return results
if __name__ == "__main__":
init_servo()
print("idx,up_s,down_s,err_mm")
for i in range(1, N + 1):
r = one_cycle(i)
if r is None:
print("ABORT at", i, "(急停或异常,先排查再续测)")
break
rows.append(r)
print("%d,%.2f,%.2f,%.1f" % r)
if i % 10 == 0: # 每 10 次:记录温升 + 汇总
avg = sum(x[3] for x in rows[-10:]) / 10
print("# 第%d-%d 次 平均误差 %.1f mm | TODO手测舵机温度" % (i - 9, i, avg))
time.sleep(30) # 02 §3.6:防过热间歇
stop()
print("# 完成 %d/%d 次" % (len(rows), N))
6.2 记录表模板(打印后手填,联调当天进档案)#
序号 上行实测 s 下行实测 s 到位误差 mm 急停响应 ms 舵机温升(每10次) 备注(卡绳/打滑) 1–10 实测≤100 11–20 21–30 31–40 41–50 判定 目标 2.6±0.5s 同左 ±5mm 内(02 §3.1) ≤100ms 无烫手 无
急停专项:另记 10 次急停——从按下到肉眼停住的延迟、松开后是否需重新按才启动(必须为"是")。50 次全绿 + 急停 10 次全绿,才允许在 02 §3.5 参数表"11/14 实测"列填死数值。
6.3 联调当天执行顺序(对齐 01"11/14 上午唯一联调窗口")#
空载 5 次 :只看绳路与方向,确认 UP/DUTY 数值没被改坏(02 §3.7 第 1 条)。带载 50 次 :上表逐行填,每 10 次停 30s 测温升;出现卡绳立即中止,先修机械再续测。急停 10 次 + 断电手拉 3 次 (手拉属机械降级,不进代码,02 §3.7 第 3/4 条)。全绿后回填参数表、截图进档案;之后禁止再改架构 ,只允许微调数值(01 联调铁律)。 把最终 PARAMS 打印一份贴在桥体底座内侧,现场对照,防止被 AI 对话误改。
七、常见故障代码级排查表#
症状 先看代码哪里 / 打印什么 处理(人工硬件在前,代码在后,06 口诀) 舵机完全不转 打印 [INIT] 是否出现;servo.duty_u16 是否执行 先查共地→独立 5V→信号线是否插错排针;hwmap/引脚核对 GPIO12;最后才怀疑程序 方向反转 (上行变下行)PARAMS 中 UP_DUTY_PCT 与 DOWN_DUTY_PCT 数值只交换这两个数值 (或对调绕线方向),禁止改字段名;记录进 §6.2 上电抖动/飞车 init_servo() 是否先打 STOP 再 sleep_ms(300);占空比换算确认停止占空比 7.5% 实测有效;TODO 处微调归位等待;仍飞车=查 5V 是否被信号污染 计时漂移 (到位误差 >5mm)[RUN] 打印的 t 与秒表对比;N_UP_S 是否被 AI 改过只调 N_UP_S/N_DOWN_S 数值;机械限位兜底;每次演示前手动归零一次(02 §3.6) B2 失灵 (按了没反应)btn_scan() 读值是否翻转;消抖 30ms 是否吞掉短按万用表测 GPIO11 电平;查上拉与接线;确认丝印脚位=11;长按 >30ms 再测 急停不灵 _run_for 循环里是否仍有 btn_pressed() 判定禁止删除急停轮询;POLL_MS 不得 >50ms;查是否状态被 AI 改成阻塞 sleep(N)(违规,退回骨架 ) 串口不通 HANDSHAKE 是否为 False(预期!);serial_poll 有无异常默认人肉握手即可演示;要启用时查端口号与共地;06:Claw 10 分钟不顺直接降级 循环跑到一半停在 DOWN [FATAL] 打印的异常;是否堵转过流try/except 已中停;查卡绳/配重(02 §3.6);恢复后从 IDLE 重新启动
八、备份与回退#
层级 内容 动作 U 盘备份(赛前必做) scripts/bridge_servo.py、bridge_servo.lua、m10_trigger.py、test_50cycles.py、params.lua 原值截图、Mind+ 工程包、本文件打印稿11/14 晚按 01 打包清单入"程序袋";现场机器故障 30 分钟内可换机重烧(06 §二) Claw 不可用 Web/LLM 挂 按 06 §六顺序降级:Console 跑预存脚本 → Mind+/手写同功能工程 → 换备用主控按引脚表重接 → 纯机械手拉演示;聊天写程序不计入通过标准 K10 整板故障 机构主控死 热备第二块 M10 接机构 IO(01 §七),引脚按 §二标签重接;程序改用同一 PARAMS 结构 舵机/绞盘故障 机构不动 断电后手拉绳尾 缓降(02 §3.3 降级)→ 静态展示;答辩讲设计意图,不影响结构分 图生文不受影响 M10 管线与 K10/Claw 完全解耦 05 L0–L3 独立执行;AI日志/ 与 Claw日志/ 分开(05 §五);机构降级时 AI 照常出完整一次生成
回退口诀 :程序问题回 U 盘,Claw 问题回 Console/Mind+,机构问题回手拉,AI 问题回 05 降级表——四条线互不拖累。
换机恢复三步(现场照做,不讨论) :① U 盘拷入 scripts/ 全目录,核对 PARAMS 与打印稿一致;② 按 §二引脚表重新压线,丝印对脚位(06"标签与脚本引脚一致");③ 先跑 test_50cycles.py 的 5 次抽测,绿了再进 50 次正式记录。恢复过程超过 30 分钟 → 改走机械手拉 + 静态展示,保结构分与 AI 分。
附:本文与其他文档的一句话索引#
需要 打开 绳路/参数初值/50 次验收口径 02 §3图生文状态字、prompt、日志 01–06、断网 L0–L3 05(只执行不复述)Claw 话术、params.lua、现场 60–70% 时间给接线 06(只引用不复述)比赛日程与总降级 01