AKA-00
辰龙AI教育机器人
新闻
- 2026-03-03 添加对zl-zp10s舵机的支持
- 2026-02-12 小车模拟器可用了
- 2026-02-06 AKA-00 小车添加了对 MG996R 舵机的支持,目前支持 STS3215 和 MG996R
- 2026-01-30 opi5p 开发板可以驱动 AKA-00 小车了
开始使用
配置指南
使用文档
开发资料
项目介绍
AKA-00 是一个面向教学的低成本AI机器人,通过提供简单的平台实现多种算法的训练和仿真。
核心能力
| 能力 | 说明 |
|---|---|
| 机械臂控制 | ZL-ZP10S 串口舵机(也支持 STS3215),夹爪带闭环判"夹住" |
| 底盘运动 | TT 马达差速控制,经 ESP32-C3 底盘板闭环,可按距离/角度走精确到位 |
| 视觉推理 | 板载 NPU 跑 .cvimodel 模型;单帧检测接口 + 可编排的 Lua 流程脚本 |
| 远程控制 | Web 界面 + HTTP API + WebSocket 摇杆 |
技术架构
AKA-00
├── cpp/
│ ├── capp/ # aka-capp 服务:HTTP/WS 服务 + 接口路由
│ ├── csrc/ # 平台与硬件层:摄像头、板载屏、串口、舵机、电机、推理
│ └── board/ # 板上部署内容(config.toml、init.sh、demo/)
├── frontend/ # React 前端源码
├── static/ # 前端构建产物(打包时收进部署目录)
└── tests/ # 板测工具与测试脚本
各目录职责与"想改什么改哪里"见代码结构。
硬件平台
| 组件 | 型号 |
|---|---|
| 主控芯片 | (算能 CV1812,1 TOPS NPU) |
| 机械臂 | ZL-ZP10S / STS3215 |
| 电机 | TT 马达 ×2(接 ESP32-C3 底盘板闭环) |
| 摄像头 | USB 免驱摄像头(UVC / MJPEG) |
| 板载屏 | ST7796S 320x480(仅带屏版) |
快速开始
本文档帮助你快速开始让 AKA-00 跑起来。
1. 组装
参考 硬件接线 完成机械臂、电机、摄像头的连接。
2. 通电
- 连接电源,等待控制板指示灯亮起
- 等待 60 秒,网络模块启动
- 连接机器人热点(格式:
chenlong-robot-xxxxx) - 浏览器访问
192.168.4.1,进入控制界面 - 之后可以通过手机上的遥控器控制小车
3. 部署(首次/更新)
项目以单文件 aka-00-server 分发,拷贝到控制板执行:
# 打包(在开发机上;改了前端要先 cd frontend && npm run build)
make -C cpp ota # → cpp/dist/aka-00-server
# 拷贝到控制板(-O:板载 sshd 不认新版 SFTP 协议)
scp -O cpp/dist/aka-00-server root@<robot>:/root/
# 首次部署:解包 + 热点 + 开机自启
ssh root@<robot> '/root/aka-00-server --init'
更新部署(保留 config.toml、证书、demo 卡片等现场数据):
scp -O cpp/dist/aka-00-server root@<robot>:/root/
ssh root@<robot> 'setsid /root/aka-00-server --update >/root/ota.log 2>&1 < /dev/null'
- 别放
/tmp:板上/tmp是 53MB 的内存盘,20MB 的固件放那儿会把内存打爆。- 想让本次带的默认配置连
config.toml一起覆盖,用AKA_OTA_RESET_CONFIG=1 ... --update。setsid是因为--update结尾是exec init.sh,会把服务挂在当前 ssh 会话上; 也可以升级完直接reboot(开机由/etc/init.d/S99webstart拉起)。
4. 使用
启动后通过以下方式控制:
- Web 界面: 访问
http://<机器人IP>/ - API: 使用
/api/control接口
下一步
硬件参数
主控板

| 参数 | 值 |
|---|---|
| 型号 | LicheeRV Nano |
| CPU | 算能 SG2002 大核:1GHz RISC-V C906 / ARM A53 二选一; 小核:700MHz RISC-V C906; |
| NPU | 1 TOPS (INT8),支持 BF16 |
机械臂舵机和控制板
控制板
微雪UART串口通信控制板

使用的舵机
| 参数 | 值 |
|---|---|
| 型号 | ZL-ZP10S |
| 通信 | 串口 UART |
| 设备 | /dev/ttyS2(主控 UART2) |
| 波特率 | 115200 |
支持的舵机
当前 C++ 实现里编了驱动的两种(config.toml 的 [arm] backend 选):
- ZL-ZP10S(
backend = "zp10s",默认) - STS3215(
backend = "sts3215")
底盘控制板(ESP32-C3)

使用的电机
| 参数 | 值 |
|---|---|
| 型号 | TT 马达 ×2(带编码器) |
| 控制方式 | ESP32-C3 底盘板闭环 PID,主控经 UART 下发/回读 |
| 通信 | /dev/ttyS1(主控 UART1),115200 |
| 编码器 | 4680 脉冲/轮圈 |
| 固件 | esp32_base_control/base_control.ino(独立工程,不在本仓库) |
主控不再直连电机 PWM/GPIO —— 旧文档里那张「PWM Chip 4 / Channel 0,1,2,3」的 接线表已经不适用,那套是 Python 版用 GPIO 直驱的做法。 角度/距离闭环(
CMD_MOVE_DISTANCE)和限速都在 ESP32 固件里做,主机只发目标值、 读回 rpm 与状态。
板载屏
| 参数 | 值 |
|---|---|
| 型号 | ST7796S |
| 分辨率 | 320x480(RGB565) |
| 接口 | SPI,只写(无 MISO / 无触摸引出),内核暴露为 /dev/fb0 |
| 接线 | 见硬件接线的「板载屏(SPI)」 |
| 配置 | config.toml 的 [display](开关 / 全屏半屏 / 方向 / 帧率) |
只有带屏版会驱动它(make 默认,AKA_WITH_SCREEN=1);不带屏版(make noscreen)
把整个显示栈编译期裁掉,二进制更小且完全不碰 /dev/fb0,[display] 配置被忽略。
硬件接线
接线示意
主控接口图

本项目使用了以下接口:
- 底盘控制(UART1)
- A18:UART1 RX
- A19:UART1 TX
- 机械臂舵机(UART2)
- A28:UART2 TX
- A29:UART2 RX
- 板载屏(SPI)
- SCK:SPI 时钟
- SDO:SPI 数据输出(接屏的 SDI)
- CS:SPI 片选
- IO5:LCD_RS(数据/命令选择)
- IO6:LCD_RST(复位)
- VBUS 5V
- GND
底盘控制板接口图

- VM:电机供电
- NC:置空
- GND:接地
- A、BO1、2:接电机
- A、BIN1、2:控制信号输入
- STBY:SLEEP控制,底电平有效
机械臂控制板接口图

- D:数据总线
- V:舵机供电正级
- G:舵机接地
- DC+:主控供电正级
- DC-:主控供电负极
- TX:控制输入
- RX:控制接收
- GND:接地
- A UART:UART总线控制模式
- B USB:USB总线控制模式
控制电路连线图
接线前请确保断电操作。
机械臂 UART
- 机械臂舵机控制板接主控 UART2(A28 / A29),设备节点
/dev/ttyS2 - 波特率:115200
底盘 UART
- 底盘控制板(ESP32-C3)接主控 UART1(A18 / A19),设备节点
/dev/ttyS1 - 波特率:115200
- 主控不再直连电机 PWM:A16~A19 原本那四路 PWM 现在不用了(A18/A19 复用成 UART1), 电机驱动与编码器都归 ESP32 底盘板管
板载屏(SPI)
板载屏(ST7796S,320x480)走 SPI,接法如下(左=屏的排针,右=主控):
| 屏(ST7796S) | 主控 | 说明 |
|---|---|---|
| VCC | VDD | 供电 |
| GND | GND | 地 |
| LCD_CS | CS | SPI 片选 |
| LCD_RST | IO6 | 复位 |
| LCD_RS | IO5 | 数据/命令选择(DC) |
| SDI | SDO | SPI 数据:屏的输入接主控的输出(MOSI) |
| SCK | SCK | SPI 时钟 |
- 屏是只写设备(没有 MISO / 触摸没有引出),capp 不直接操作 SPI 引脚,只往
内核暴露出来的
/dev/fb0写画面。 - SPI 时钟在设备树里(节点
st7796s@0的spi-max-frequency):出厂 4MHz 只有 ~420KB/s(约 2fps),本板实测稳定上限 20MHz(~2MB/s),24MHz 起白屏。 杜邦线转接会明显拉低能跑的频率。 - 屏的参数(开关、全屏/半屏、方向、帧率)都在
config.toml的[display]里配, 没有 HTTP 接口;调参依据见cpp/README.md的「板载屏显示」一节。 - 排查接线用
./tools/screen_test(colors看通道序/方向、bench看写屏带宽)。
接口一览
| 用途 | 主控引脚 | 设备节点 | 配置 |
|---|---|---|---|
| 底盘(ESP32-C3) | A18 / A19(UART1) | /dev/ttyS1 | [motor] |
| 机械臂舵机 | A28 / A29(UART2) | /dev/ttyS2 | [arm] |
| 板载屏 | SCK / SDO / CS / IO5 / IO6 | /dev/fb0 | [display] |
引脚复用(哪些脚要做 UART)见开发板说明。
更多原理图
硬件原理图位于 hardware/ 目录:
LicheeRV_Nano-70418_Schematic.pdf- 主控板原理图sg2000_trm_cn.pdf- SG2000 技术参考手册众灵舵机使用手册-250508.pdf- 舵机使用说明
第一次连接
如何连接机器人
第一步,连接机器人自身的热点,用于配置机器人
- 确保机器人已连接到电源,等待机器人控制板灯亮
- 等待20秒到30秒,此时控制板正在启动网络模块
- 开发者打开电脑/手机,进入wifi连接,找到控制板的热点并连接,例如
chenlong-robot-02
第二步,让机器人连接到开发者的WiFi网络
- 连接热点之后,打开浏览器,输入
192.168.4.1(不同的机器人可能不同,已实际为准),即可进入机器人的遥控界面 - 在配置页面中,刷新网络,找到开发者需要的WiFi网络,点击连接,如有密码需要输入密码

- 连接成功后,网页会显示当前连接的WiFi网络名称以及为机器人分配的IP

第三步,ssh登录机器人的控制板
- 第二步进行完之后,开发者需要记下为机器人分配的IP,并且让自己的电脑和机器人连接到同一个WiFi网络

- 开发者打开终端,输入
ssh root@[机器人分配的IP],即可登录机器人的控制板,密码为root - 登录成功后,即可在终端中操作机器人的控制板,输入
ping www.baidu.com或者curl www.baidu.com,用来检测控制板网络是否成功连接并且能够访问互联网
开发环境与部署
本机跑起来(不需要板子)
设备端是个普通的 C++ 程序,可以直接在开发机上编一份跑,配置、前端页面、demo 脚本 都用仓库里的实体文件:
# 1. 前端(改了前端才需要)
cd frontend && npm i && npm run build # 产物写到仓库根 static/
# 2. 设备端(host 版,不会覆盖交叉编译产物)
cd cpp/capp && make # → bin/aka-capp-dev
AKA_HOME=cpp/board ./cpp/capp/bin/aka-capp-dev
AKA_HOME 一定要指向 cpp/board,否则找不到 config.toml 和 static/。
串口和摄像头拆掉也能启动(对应模块退化成 mock 并打日志),所以只写接口、调页面时
不用接硬件。
前端热更新:
cd frontend && npm run dev
vite 的 dev server 把
/api、/ws代理到localhost:5000,而 C++ 版 capp 默认监听 80(config.toml的[web] port)。要联调,要么改frontend/vite.config.ts里的 proxy target,要么临时把 capp 的端口设成 5000。
打包
正式构建统一走 cpp/Makefile(在 orb 里直接跑;在 macOS 上会自动经 orb run 转发
交叉编译):
cd cpp
make # 一条龙:libjpeg/mbedtls/lua → csrc → capp → package → ota
make noscreen # 不带屏版本(整个显示栈编译期裁掉)
make clean # 清理构建产物
产物两样:cpp/dist/AKA-00/(部署目录)与 cpp/dist/aka-00-server(自解压安装器)。
前端不参与 make:打包时直接拿仓库根 static/ 里已有的产物,所以改了前端要先
cd frontend && npm run build(不带屏版是 npm run build:noscreen),拿错版本打包
脚本会告警。
部署到控制板
# 首次部署:解包 + 配热点 + 开机自启
scp -O cpp/dist/aka-00-server root@<板子IP>:/root/
ssh root@<板子IP> '/root/aka-00-server --init'
# 升级:保配置换包 + 重启服务
scp -O cpp/dist/aka-00-server root@<板子IP>:/root/
ssh root@<板子IP> 'setsid /root/aka-00-server --update >/root/ota.log 2>&1 < /dev/null'
scp加-O(板载 sshd 不认新版 SFTP 协议);固件别放/tmp(那是 53MB 的内存盘, 20MB 的固件会把内存打爆)。--update默认保留板上的config.toml、speed_config.json、arm_angles.json、cert.pem、key.pem,以及 demo 卡片(板上优先)和用户上传的模型(取并集)。 想让包里带的新配置生效:AKA_OTA_RESET_CONFIG=1 ... --update。- 换包用 staging + 目录改名,旧目录留成
$AKA_HOME.old作回滚点;只解包不重启用--extract。 --update结尾是exec init.sh,服务会挂在当前 ssh 会话上,所以要setsid, 或者升级完reboot(开机由/etc/init.d/S99webstart拉起)。
更完整的说明见仓库根 README.md 的「📦 打包」与「🚀 部署」两节。
初始化配置
这部分为机器人的初始化部分,都会在用户拿到设备前实现,如果用户需要自行初始化也可以按照本流程实现。
烧录镜像
从Releases下载最新镜像,通过烧入工具将镜像烧录到tf卡中,镜像中会自带一份项目文件。
连接主控
通过type-c接口可以将板子连接到电脑上
在win下在终端里输入ipconfig,找到一个新的以太网,例如 10.163.124.100。
之后可以使用ssh进行连接,ssh root@10.163.124.1
部署 aka-00-server
将 aka-00-server 拷贝到控制板,一条命令完成初始化:
# 1. 拷贝到控制板(-O:板载 sshd 不认新版 SFTP 协议;别放 /tmp,那是内存盘)
scp -O cpp/dist/aka-00-server root@<robot>:/root/
# 2. 一键初始化(解包 + AP 热点 + DHCP + 开机自启)
ssh root@<robot> '/root/aka-00-server --init'
--init 自动完成:
- 解包到
$AKA_HOME(默认/root/AKA-00) - 配置 AP 热点(SSID
chenlong-robot-<数字>,由 wlan0 的 MAC 推出,开放无密码, channel 6;要加密码就取消/etc/hostapd.conf里wpa=2那几行注释) - 配置 DHCP(
192.168.4.100~192.168.4.200,网关192.168.4.1) - 写入
S98apstart/S99webstart自启脚本 - 立即启动热点
之后每次开机由 /etc/init.d/S99webstart 拉起 init.sh(内部自愈循环,capp 崩了自动重启)。
手动更新:
scp -O cpp/dist/aka-00-server root@<robot>:/root/
ssh root@<robot> 'setsid /root/aka-00-server --update >/root/ota.log 2>&1 < /dev/null'
--update默认保留板上的config.toml、speed_config.json、arm_angles.json、cert.pem、key.pem与 demo 卡片/模型;想让包里带的新配置生效,加AKA_OTA_RESET_CONFIG=1。结尾是exec init.sh,会把服务挂在当前 ssh 会话上, 所以要setsid(或者升级完reboot)。
HTTPS 证书生成命令
通常不需要手工执行 ——
init.sh启动前会调https_init.sh自动生成 (缺cert.pem/key.pem时),端口默认 443。下面这条只在你想自己换/重置证书时用。
- 无交互生成自签名证书,有效期10年(3650天)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout key.pem -out cert.pem -days 3650 -nodes \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/OU=MyDept/CN=localhost"
网络配置
修改 /etc/wpa_supplicant.conf 文件
ctrl_interface=/var/run/wpa_supplicant
ap_scan=1
network={
ssid="wifi名"
psk="wifi密码"
priority=8
}
network={
ssid="#####"
psk="********"
priority=5
}
network={
key_mgmt=NONE
priority=1
}
镜像烧录
本文档介绍如何将系统镜像烧录到开发板。
准备工作
所需工具
- SD 卡(2GB或以上)
- SD 卡读卡器
- BalenaEtcher 或其他镜像烧录工具
下载镜像
从项目 releases 页面下载最新的系统镜像文件(.img 格式)。
镜像文件地址
烧录步骤
Windows 系统
- 插入 SD 卡
- 打开 BalenaEtcher
- 点击 "Flash from file",选择下载的镜像文件
- 点击 "Select target",选择 SD 卡设备
- 点击 "Flash" 开始烧录
- 等待烧录完成(约 5-10 分钟)
macOS / Linux 系统
使用 dd 命令烧录:
# 查看 SD 卡设备名称
diskutil list
# 卸载 SD 卡(假设设备为 /dev/disk2)
diskutil unmountDisk /dev/disk2
# 烧录镜像(注意:of 后面的设备名称不能包含 partition 号)
sudo dd if=./aka-image.img of=/dev/rdisk2 bs=4M status=progress
# 烧录完成后弹出 SD 卡
diskutil eject /dev/disk2
首次启动
- 烧录完成后,将 SD 卡插入开发板的 SD 卡槽
- 连接电源
- 等待系统启动(约 1-2 分钟)
- 通过串口或网络连接进行后续配置,详见初始化配置
常见问题
无法启动
- 检查电源是否正常供电
- 确认 SD 卡已正确插入
- 尝试重新烧录镜像
Web 界面
启动服务后,访问 http://<机器人IP>/ 进入 Web 控制界面。
功能区域
遥控器
通过方向键控制机器人运动:
- 前进/后退:前进/后退
- 左转/右转:左转/右转
夹爪控制
- 抓取:控制机械臂向下夹取夹爪闭合
- 释放:控制机械臂夹爪张开
WiFi 配置
访问 WiFi 配置页面,可重新设置机器人连接的 WiFi 网络。
详细步骤见 WiFi 配置
进入试验平台
可进入自带的实验平台进行实验
WebSocket 控制接口
底盘遥控这类高频操作走 WebSocket,比 HTTP 轮询延迟低得多。控制页的虚拟摇杆用的就是 这条通道。
连接
ws://<机器人IP>/ws/control # HTTPS 页面上用 wss://
协议格式
双方都发二进制帧,第一个字节是类型标记。
客户端 → 服务端
| 帧 | 说明 |
|---|---|
0xAA + int8 x + int8 y | 摇杆,3 字节;x 转向、y 油门,各 -128~127 |
0xDD + UTF-8 JSON | 低频命令(见下),文本帧发 JSON 也接受 |
摇杆换算:fy = y/127、fx = x/127,左 = (fy+fx)×100、右 = (fy-fx)×100,限幅
到 ±100 后下发。不带时长、不阻塞,所以松手要自己补一帧停止。
| 操作 | 字节 | 说明 |
|---|---|---|
| 前进 50 | AA 00 32 | X=0, Y=50 |
| 后退 30 | AA 00 E2 | X=0, Y=-30(补码) |
| 左转 25 | AA E7 00 | X=-25, Y=0 |
| 右转 25 | AA 19 00 | X=25, Y=0 |
| 停止 | AA 00 00 | X=0, Y=0 |
JSON 命令(0xDD + JSON):
| type | 说明 |
|---|---|
{"type":"ip"} | 查 IP,回 {"type":"ip","ip":"…"} |
{"type":"action","action":"stop","speed":50,"time":0} | 走和 HTTP /api/control 同一个动作入口(不阻塞,发完就走) |
{"type":"raw_command","cmd":"…"} | 透传固件原始指令(排障用) |
{"type":"arm_cmd","payload":{"command":"grab"}} | 夹爪开/合(只认 grab / release),不回复 |
{"type":"reinitialize"} | 重连底盘(清 PID/编码器,不掉线、不打断运行) |
其它 type 会被忽略(只记 debug 日志)。
服务端 → 客户端
| 帧 | 说明 |
|---|---|
0xBB + int16LE 左轮 + int16LE 右轮 | 轮速,单位 m/s × 1000;每 200ms 推一次 |
0xDD + UTF-8 JSON | 状态与应答。连上即推 {"type":"ip","ip":"…"},以及 {"type":"motor_status","motor":{…}}(此后仅在底盘连接状态变化时推) |
前端示例
const ws = new WebSocket("ws://192.168.4.1/ws/control");
ws.binaryType = "arraybuffer";
ws.send(new Uint8Array([0xAA, 0, 50])); // 前进
ws.send(new Uint8Array([0xAA, 0, 0])); // 停
ws.onmessage = (event) => {
const b = new DataView(event.data);
if (b.getUint8(0) === 0xBB) {
const left = b.getInt16(1, true) / 1000;
const right = b.getInt16(3, true) / 1000;
console.log(`左: ${left} m/s, 右: ${right} m/s`);
}
};
生命周期与断线
- 服务端每 4 秒 ping 一次;90 秒收不到任何字节才判死回收连接 —— 阈值给得宽是 因为手机 WiFi 省电模式会让上行停摆几十秒,秒级判死只会造成反复断开重连。
- ⚠️ 断开连接不会停车(这点和 Python 版相反):判死只用于回收僵尸连接,不再承担
安全停车职责。停车靠指令本身(摇杆发 0 或
action stop),以及 ESP32 固件侧的 心跳看门狗。 - 同一时刻只保持一个控制连接:多个连接同时发会互相抢(谁后发谁生效)。
API 文档
控制接口
GET /api/control?action=<action>&speed=<speed>&time=<time>&distance=<distance>&angle=<angle>
参数
| 参数 | 类型 | 必填 | 说明 | |------|------|------| | action | string | 是 | up / down / left / right / stop / grab / release | | speed | int | 否 | 电机百分比(1~100),默认 50 | | time | int | 否 | 持续时间(毫秒),无 distance/angle 时生效 | | distance | float | 否 | 移动距离(厘米 cm),up/down 有效 | | angle | float | 否 | 转动角度(度 °),left/right 有效 |
优先级:
distance/angle>time。传了 distance 或 angle 就忽略 time。所有"会动"的请求都只回一个
completed(true = 这次动作执行完了,失败时多一个error说明原因;细节看/api/motor/status或日志)。三个入口一致:?distance=/?angle=、?time=、以及POST /api/demo/init|run带"wait": true。返回时机:
请求 何时返回 返回 ?distance=/?angle=阻塞到 ESP32 固件闭环报结果(最多 30s) {"completed": …}?...&time=阻塞到动作做完并自动停车 {"completed": …}?action=up(不给 distance/time)立刻返回(持续运动,靠 ?action=stop停){status: success}?action=grab/release阻塞到夹爪那套序列做完(ZP10S 约 3.5s) {"completed": …}
grab/release不排队:上一段还没做完时,后来的请求直接回{"completed":false,"error":"夹爪正忙:上一段动作还没做完(这次没做,也没排队)"}(400), 不会攒成一串挨个执行(实测连点 5 次 → 只执行 1 次)。
distance/angle的结论来自固件(它自己闭环 + 回状态),不是主机猜的 —— 车被卡住会回aborted/timeout而不是completed。
speed是直接发给 ESP32 PID 控制器的目标百分比(setMotorSpeed(±100)→target_rpm = speed × 150 / 100)。100%对应约0.49 m/s,由PWM_RPM_MAX=150 RPM × 轮径62mm × π / 60推出。
距离运动示例
# 前进 30 厘米,速度 50%
curl "http://<ip>/api/control?action=up&distance=30&speed=50"
# 后退 15 厘米,速度 30%
curl "http://<ip>/api/control?action=down&distance=15&speed=30"
# 左转 90 度,速度 40%
curl "http://<ip>/api/control?action=left&angle=90&speed=40"
# 右转 45 度(用默认 speed=50)
curl "http://<ip>/api/control?action=right&angle=45"
时间运动示例
# 前进 2 秒,速度 50%
curl "http://<ip>/api/control?action=up&speed=50&time=2000"
# 停止
curl "http://<ip>/api/control?action=stop"
抓取
curl "http://<ip>/api/control?action=grab"
curl "http://<ip>/api/control?action=release"
speed 物理含义对照
speed 是 ESP32 PID 控制器的目标百分比。所有路径(摇杆 / 方向键 / REST+time / REST+distance)共用同一套物理含义:
| speed | 目标 RPM | 约合线速度 |
|---|---|---|
| 30 | 45 | 0.15 m/s |
| 50 | 75 | 0.24 m/s |
| 100 | 150 | 0.49 m/s |
电机直接控制
GET /api/motor/direct?left=<left>&right=<right>&duration=<duration>
| 参数 | 类型 | 说明 |
|---|---|---|
| left | int | 左轮 -100~100 |
| right | int | 右轮 -100~100 |
| duration | float | 持续时间(秒),0 为持续 |
# 全速前进
curl "http://<ip>/api/motor/direct?left=100&right=100"
# 原地右转
curl "http://<ip>/api/motor/direct?left=50&right=-50"
# 前进 1.5 秒
curl "http://<ip>/api/motor/direct?left=80&right=80&duration=1.5"
电机状态
GET /api/motor/status
{
"left_speed": 0.0,
"right_speed": 0.0,
"left_target": 50,
"right_target": 50,
"gripper_status": "stopped"
}
速度配置
GET /api/config/speed
POST /api/config/speed
{"forward_speed": 50, "turn_speed": 50}
摄像头
状态
GET /api/camera/status
{"camera_on": true}
打开 / 关闭
POST /api/camera/open
POST /api/camera/close
{"camera_on": true} // open 成功;打不开返回 500
{"camera_on": false} // close
摄像头是全局唯一的一份:屏显示、浏览器取流、单帧推理共用它。 关闭会同时熄屏(屏上显示待机图),对前端透明;打开后屏自动实时出图。
抓拍(单张图)
GET /api/camera/snapshot
{
"image": "<base64 JPEG>",
"width": 640,
"height": 360,
"format": "jpeg",
"m": 2671.82,
"c": -2.82
}
| 字段 | 说明 |
|---|---|
| image | 整帧图片的 base64(JPEG) |
| width / height | 图片像素尺寸 |
| format | 固定为 jpeg |
| m / c | 距离标定常数:D = m / P + c(P = 目标在画面中的像素尺寸,D = 距离)。配合检测框用,见距离标定 |
摄像头没开会自动打开(注意有副作用:
camera_on变成 true、板载屏开始实时出图),所以这里 拿到的是实时帧。打不开时(设备被占用/不存在)返回500+{"error":"camera not available"}。MJPEG 摄像头是原帧直通(不重新编码,quality 参数对它无效);只有 YUYV 摄像头才编码,quality=70。
视频流(MJPEG)
GET /api/camera/stream?fps=<fps>
| 参数 | 类型 | 说明 |
|---|---|---|
| fps | int | 发送帧率上限,默认 15,超出 1~30 会被夹到边界 |
响应为 multipart/x-mixed-replace; boundary=frame 的 MJPEG 流(浏览器 <img src> 直接用),持续到客户端断开。
默认直通摄像头原始 MJPEG 帧(服务端零解码零编码);只有配置了
[camera] stream_scale = true才会在服务端缩放到stream_width x stream_height后重编码下发 —— 那是拿 CPU 换 WiFi 带宽。有人在看流时,板载屏会自动降帧到
[display] fps_streaming(0 = 暂停显示)让出 CPU 给浏览器: 单核 SoC 上"全屏写屏 + 浏览器取流"会互相拖慢,所以默认浏览器优先。
底盘速度 / 综合状态
两个历史命名的接口,回的实际是底盘与电机状态(不是摄像头信息),保留是为了兼容前端:
GET /api/camera/speed
GET /api/camera/all_status?timestamp=<timestamp>
{
"left_speed": 0.0,
"right_speed": 0.0,
"left_target": 50,
"right_target": 50,
"gripper_status": "stopped",
"gripper_target": 0,
"timestamp_ms": 1730000000000
}
all_status 在此之上多三个字段:motor(电机连接状态)、image(base64 JPEG,quality=25)、
image_format;传给它的 timestamp 会原样回显。gripper_status 是夹爪运行状态,夹爪未连接时是 unknown。
这个接口不会打开摄像头(与
snapshot不同):摄像头关着时它读的是内存里缓存的最后一帧, 因此image依然是关闭前那一张、HTTP 依然 200,而camera_on为false。实测:关闭后连续两次 取图,图片字节完全相同。响应里没有帧时间戳,要判断实时性只能看camera_on。 从来没出过帧时image为null。
单帧推理(物体检测)
GET /api/detect?model=<模型名>
取当前摄像头的一帧跑一次模型,返回检测框的四个角。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| model | string | 是 | 模型名,对应 $AKA_HOME/demo/models/<模型名>.cvimodel(如 tennis、block)。只允许字母数字与 _ - .,不允许 / 与 .. |
| conf | float | 否 | 置信度阈值,默认 0.25。给的值会夹到 0.01~0.99 |
| iou | float | 否 | NMS 的 IoU 阈值,默认 0.45。管「两个框算不算同一个目标」(去重),不是置信度 |
摄像头没开会自动打开(与
/api/camera/snapshot行为一致);但刚打开时可能还没出帧, 这时返回{"ok":false,"error":"no frame"},隔一下重试即可。
conf管「这个框够不够可信」:调低减少漏检、调高压掉误检。iou管「两个框要不要合成一个」:同一个目标画出两个框就调低它, 挨着的两个目标被吃掉一个就调高它。两个参数给错值(不是数、或不在 0~1)返回 400,不会悄悄用默认值 —— 调参时最怕 「以为生效了其实没生效」。不给就是 0.25 / 0.45,与老版本行为完全一致。
响应
{
"ok": true,
"count": 1,
"boxes": [{"x1": 236.0, "y1": 88.5, "x2": 436.0, "y2": 283.5}]
}
| 字段 | 说明 |
|---|---|
| ok | 成功为 true |
| count | 框的个数;0 是正常结果(画面里没有目标) |
| boxes | 框列表,按分数降序、已完成类别内 NMS |
坐标是原图像素(采集分辨率,默认 640x360),与
GET /api/camera/snapshot返回的图 同一坐标系 —— 可以直接把框画到那张图上核对。只回框的四个角,不回类别/分数/耗时。
失败
一律 400 或 500 + {"ok":false,"error":"..."}:
| 情况 | HTTP | error 示例 |
|---|---|---|
| 没给 model | 400 | 缺少 model 参数(例:/api/detect?model=tennis) |
| model 名字非法 | 400 | model 名字非法(只允许字母数字与 _ - .):../etc/passwd |
| 模型不存在 / 加载失败 | 500 | 注册模型失败(CVI_NN_RegisterModel rc=…):/root/AKA-00/demo/models/xxx.cvimodel |
| 摄像头不可用 / 暂无帧 | 500 | camera not available / no frame |
示例
# 检测网球(模型 = $AKA_HOME/demo/models/tennis.cvimodel)
curl "http://<ip>/api/detect?model=tennis"
# 换另一颗模型
curl "http://<ip>/api/detect?model=block"
{"boxes":[{"x1":234,"x2":434,"y1":88.5,"y2":285.5}],"count":1,"ok":true}
说明
- 模型只有一个来源:部署目录下的
demo/models/(make package整目录照搬)。裸名字只在 库里查,不存在就报错,没有隐式回退。 - 接口是同步的:每个请求现场取帧 → 推理 → 返回。模型首次请求时加载,之后常驻;
只有
?model=变了才重新加载。 - TPU 是单实例:
/api/detect与流程脚本共用同一个检测器(各自串行), 但别在脚本跑的时候另起一个吃 TPU 的进程。 - 换自己的模型时对一下规格。本仓库
demo/models/tennis.cvimodel板上实测:输入640x480、YUV420_PLANAR、8 位量化;输出[1,5,6300,1]FP32、单类别 (6300 = 80×60 + 40×30 + 20×15,即三个 stride 的网格点数之和)。 输入尺寸与格式都从模型张量里读,不写配置 —— 模型吃什么就喂什么。
模型管理
给外部调用方(平台)用:把模型送进部署目录的 demo/models/ —— 也就是 /api/detect 唯一认的那个模型库。
上传模型(平台 → 小车,推荐)
POST /api/models/upload?name=<模型名>
Content-Type: application/octet-stream
(body = 模型文件的二进制内容)
# raw body:平台直接推文件(推荐)
curl --data-binary @tennis.cvimodel "http://<ip>/api/models/upload?name=tennis"
# multipart:浏览器 / form 客户端也行
curl -F "file=@tennis.cvimodel" "http://<ip>/api/models/upload?name=tennis"
| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| name | query | 是 | 模型名,落成 $AKA_HOME/demo/models/<name>.cvimodel。只允许字母数字与 _ - .,不允许 / 与 .. |
| 文件 | body | 是 | 模型二进制(raw body,或 multipart 里名为 file 的字段) |
{"ok": true, "name": "tennis", "path": "/root/AKA-00/demo/models/tennis.cvimodel", "size": 3540016}
同步接口:文件收完、校验通过、写盘换入之后才返回(3.5MB 的模型在内网上是一瞬间的事,不需要进度查询)。
为什么是"推"而不是"拉":小车在机器人的内网里(通常是热点/局域网),平台未必能被它反向访问; 这就是模型进入板子的唯一方式:由平台把文件推过来(不需要小车去访问平台, 也不需要板上有任何"模型商店/下载"的界面)。
同名覆盖,且覆盖即生效:/api/detect 每次请求都会 stat 模型文件,大小或 mtime 变了就重新加载
—— 换新版本不用重启 capp(代价是那一次请求多等一次模型加载)。
文件先落成
.part,校验通过后原子换入(rename)—— 传到一半、内容不对、中途断电都不会 破坏正在用的那颗模型。校验两道:文件头必须是
CviModel(挡住"上传了别的文件");大小上限 32MB (请求体是整块读进内存的,板上可用内存约 50MB;模型实际约 3.5MB)。校验只看文件头,所以「文件头对、内容是坏的」这种能被装上 —— 这时
/api/detect会明确报注册模型失败(CVI_NN_RegisterModel rc=…),重新传一个正确的即可,不需要别的清理动作。
| 失败 | HTTP | error 示例 |
|---|---|---|
| 没给 name | 400 | name 参数必填(例:?name=tennis) |
| name 非法 | 400 | name 非法(只允许字母数字与 _ - .):../etc/passwd |
| 内容不是 cvimodel | 400 | 不是 cvimodel(文件头不是 CviModel) |
| 请求体为空 | 400 | 请求体为空(把模型文件放进 body) |
| 超过 32MB | 413 | 文件过大:34603008 字节,上限 32MB |
删除模型
POST /api/models/delete
{"name": "tennis"}
{"ok": true, "name": "tennis", "cards": ["追网球接近"]}
| 失败 | HTTP | error 示例 |
|---|---|---|
| 没给 name | 400 | name 必填(要删的模型名,不带 .cvimodel) |
| name 非法 | 400 | 模型名非法(只允许字母数字与 _ - .):../tennis |
| 没有这个模型 | 400 | 没有这个模型:demo/models/tennis.cvimodel |
只删 demo/models/<名字>.cvimodel 这一个文件(Demo 页上模型标签右上角那个 ✕ 走的就是它)。
用它建过的卡不会跟着删:卡片配置不动,只是变成 ready=false、点开始报 模型文件缺失 ——
重传一个同名模型就原地复活。响应的 cards 是"正在用它的卡片名",界面拿它提示后果。
板上删掉包里自带的模型只到下次 OTA 为止:升级按文件名取并集,同名会用包里的版本 (见
cpp/scripts/build-ota.sh)。想让它彻底不来,得从cpp/board/demo/models/里去掉。
训练平台直传(浏览器 → 小车)
训练平台(yolotrain.chenlongrobot.com)训练完,浏览器把模型直传小车(同一局域网),
车端不做任何运行切换,只落盘 —— 后续验证人工做。与上面那个接口的区别:名字在表单里
(走 query 的旧接口是给 curl / 云端推模型用的),响应字段是 status/name/size。
模型落盘即可用:动作脚本是预定义的、与模型无关,不再给每个模型生成脚本 ——
传完要么在 Demo 页新建一张卡片(动作 × 这个模型),要么直接
POST /api/demo/init {"action":"grab","model":"orange"}。
POST /api/model/upload
Content-Type: multipart/form-data
file = <模型二进制,文件名固定 model.cvimodel> (必填)
name = <槽位名,如 orange> (必填)
curl -F "file=@model.cvimodel" -F "name=orange" "http://<ip>/api/model/upload"
成功:
{"status":"ok","name":"orange","size":12865136,
"path":"/root/AKA-00/demo/models/orange.cvimodel",
"script":"","script_created":false,"actions":["approach","grab"]}
(script / script_created 是为兼容训练平台那份契约保留的字段,现在恒为 "" / false;
actions 是当前可用的动作清单,方便平台侧提示"能用哪些动作"。都不属于必须消费的字段。)
| 失败 | HTTP | 响应 |
|---|---|---|
file 为空 / 后缀不是 .cvimodel | 400 | {"status":"error","message":"invalid file"} |
name 为空 / 含 /、.. 等 | 400 | {"status":"error","message":"invalid name"} |
| 文件头不是 CviModel / 超过 32MB | 400 / 413 | {"status":"error","message":"不是 cvimodel(文件头不是 CviModel)"} |
落盘与副作用:
- 模型 →
demo/models/<name>.cvimodel(同名覆盖,原子换入,坏包不会顶掉正在用的) - 脚本:不再生成。动作脚本是仓库里预定义的
demo/grab.lua/demo/approach.lua, 与模型无关;传完模型后要么在 Demo 页新建一张卡片(动作 × 这个模型),要么直接POST /api/demo/init {"action":"grab","model":"<名字>"}跑一下。 - CORS 与
OPTIONS预检由服务器统一处理(所有响应带Access-Control-Allow-Origin: *, 预检回 200),浏览器跨域直传不需要额外配置。
Demo(本地演示)
板上的 一张 demo 卡片 = 动作 × 模型:
- 动作是预定义的通用脚本(
demo/grab.lua追到就夹、demo/approach.lua只接近不夹, 你也可以再放一份demo/<动作>.lua加新动作)—— 与模型无关; - 模型是
demo/models/里的一颗.cvimodel; - 卡片由用户在 Demo 页新建(选动作、选模型、起个名字、填参数),名字随便起(中文也行),
配置存在
demo/configs/<卡片名>.json。
GET /api/demo/list → {"demos":[...], "actions":[...], "models":[...]}
POST /api/demo/init {"name":"追网球接近"} → 跑存下来的那张卡片
POST /api/demo/init {"action":"grab","model":"tennis"} → 直接跑,不用建卡
POST /api/demo/init {"name":"追网球接近"} → **默认等它跑完再返回**
POST /api/demo/stop → 停
GET /api/demo/list 一次给全三份数据(列表 + 可用的动作 + 可用的模型,新建表单直接用):
{
"demos": [
{"name":"追网球接近", "action":"approach", "model":"tennis",
"ready":true, "script":"approach", "path":"/root/AKA-00/demo/models/tennis.cvimodel",
"kind":"card", "error":""}
],
"actions": [{"id":"approach","name":"接近瞄准"}, {"id":"grab","name":"追到就夹"}],
"models": ["block", "tennis"]
}
动作的显示名来自脚本第一行的约定注释
-- name: 接近瞄准;没写就用文件名。ready=false表示动作脚本或模型文件缺了(卡片照样列出来,点开始会明确报错)。
卡片配置(一张卡片一份)
GET /api/demo/config?name=追网球接近
→ {"name":"追网球接近","action":"approach","model":"tennis",
"target_size":300,"speed":50,"turn_speed":25,"mode":"once"}
POST /api/demo/config {"name":"追网球接近","action":"approach","model":"tennis",
"target_size":300,"speed":30,"turn_speed":25,"mode":"loop"}
POST /api/demo/delete {"name":"追网球接近"}
| 字段 | 含义 |
|---|---|
| action | 动作脚本名(demo/<action>.lua),必填 |
| model | 模型名(demo/models/<model>.cvimodel),必填 |
| target_size | 目标框宽(原图像素)——框宽达到它就认为到位 |
| speed | 直线速度百分比(宿主还会再 clamp 到 ≤70) |
| turn_speed | 转弯速度百分比(同样 clamp 到 ≤70)—— 和直线分开:转弯要的占空比不同 |
| mode | 执行方式:once(默认,跑一遍就结束,最多 5 分钟)/ loop(跑完接着跑,直到你按停止,没有时长上限) |
POST 就是"新建或覆盖一张卡片":界面上的"新建"与"保存"走的是同一个接口 (改参数时要把
action/model一起回传,否则会当成新建)。改名 = 用新名字 POST 一份、 把旧的POST /api/demo/delete掉。这些值就是脚本里
params()读到的东西(另外宿主还会注入model,见下节)。卡片是用户在板上建的现场数据:OTA 升级时按"板上优先"保留 —— 同名卡片升级不会 覆盖你在界面上调好的参数(包里带的那些只在板上没有同名时才落地,当出厂预设)。
demo/*.lua(动作脚本)相反是仓库里的代码,升级按包里结算 —— 想调参就改卡片配置, 别改动作脚本,否则升级会丢。
临时组装一个 demo 直接跑(不建卡)
"模型 + 动作 + 那几个值"凑齐就是一次完整的 demo 请求,不用先建卡。刚传上来一个新 模型想立刻试、或者要把一条命令发给别人让他在板上按自己的参数跑一遍,都用这条:
curl -X POST http://<ip>/api/demo/init \
-H 'Content-Type: application/json' \
-d '{"action":"approach","model":"apple","target_size":320,"speed":30,"turn_speed":20,"mode":"once"}'
| 字段 | 含义 |
|---|---|
| action | 动作脚本名(demo/<action>.lua)—— 做什么 |
| model | 模型名(demo/models/<model>.cvimodel)—— 认什么 |
| target_size / speed / turn_speed | 与卡片里同名,缺省 300 / 25 / 25 |
| mode | once(默认,跑一遍)/ loop(跑完接着跑,直到 POST /api/demo/stop) |
效果与建一张卡再跑一样(宿主会把 model=apple 注入给动作脚本);区别是不落盘、
不会在 Demo 页留下卡片。想让它出现在页面上反复用,再按上面的卡片配置建成卡片。
跑完再返回(默认行为)
POST /api/demo/init / /api/demo/run 默认等这次跑完才返回,直接给完成标志:
curl -X POST -H 'Content-Type: application/json' \
-d '{"name":"追网球接近"}' http://<ip>/api/demo/init
# → {"completed": true}
# → {"completed": false, "error": "目标丢失(1520ms 没看到目标)"}
completed: false 时 error 说明原因(脚本 fail / 丢目标 / 被人的指令接管 /
timeout: 到最大执行时间(5 分钟))。过程中发生了什么看 /api/demo/status 的
state / message / round / notes。
执行一次(
mode: "once")有 5 分钟上限,到点宿主自己收工(停电机、状态落aborted、error写"到最大执行时间")—— 所以wait的请求最多 5 分钟必定有结论。 循环执行不受它管:loop本来就不该自己结束,等不到就去POST /api/demo/stop。唯一的例外是脚本卡在不调用任何原语的死循环里(宿主只在原语入口查打断): 那时请求会等到 310 秒回一条
timeout: 等了 310 秒还没跑完…,脚本仍在跑, 但POST /api/demo/stop会在它下一次调原语时生效。
想立刻返回(不等,自己轮询状态 —— 界面就是这么用的)就显式传 "wait": false,
此时响应是 {"status":"started", "name":…, "script":…}:
curl -X POST -H 'Content-Type: application/json' \
-d '{"name":"追网球接近","wait":false}' http://<ip>/api/demo/init
curl http://<ip>/api/demo/status # 边跑边看
mode=loop(循环执行)例外:它不会自己结束,所以默认不等(立刻回started, 否则等于把连接挂死);对它显式传"wait": true会被 400 拒掉 (loop 模式不会自己结束,wait 没有意义(要停就 POST /api/demo/stop))。要停就POST /api/demo/stop。
| 失败 | HTTP | 响应 |
|---|---|---|
| 卡片不存在 / 配置读不了 | 400 | 没有这张卡片(或配置读不了):demo/configs/xxx.json |
| 动作脚本不存在 | 400 | 动作脚本不存在:demo/approach.lua |
| 模型不存在 | 400 | 模型不存在:demo/models/apple.cvimodel |
| 名字非法(卡片名/动作名/模型名) | 400 | 各自说明原因(卡片名不能含 / \\ 与控制字符) |
流程脚本(Lua)
"看 → 对准 → 靠近 → 抓"这类流程天生要反复调参。写在 C++ 里,改一个数就得交叉编译 +
部署 + 重启(一轮几分钟);写在脚本里就是改一行存盘重跑。所以 capp 内置了一个 Lua 宿主:
原语在 C++(快、稳),流程在 $AKA_HOME/demo/*.lua(好改)。
POST /api/demo/run {"script":"grab",
"params":{"model":"tennis","target_size":300,"speed":20}}
→ {"ok":true,"state":"running","script":"grab","mode":"once"}
GET /api/demo/status
→ {"state":"running","script":"grab","model":"tennis","card":"追网球","message":"","calls":42,"action":"forward",
"notes":{"box_w":"212","offset":"-33"}}
POST /api/demo/stop
→ {"ok":true,"state":"aborted"}(立刻刹车,不等脚本配合)
| 字段 | 说明 |
|---|---|
| script | 动作名,读 $AKA_HOME/demo/<动作>.lua(grab / approach …)。只允许字母数字与 _ - . |
| params | 传给脚本的参数(脚本用 params() 读),任意扁平/嵌套表 |
| params.mode | once(默认)跑一遍就结束,最多 5 分钟(到点宿主收工)/ loop 跑完接着跑直到被停,没有时长上限 |
| state | 含义 |
|---|---|
idle | 没在跑 |
running | 正在跑 |
done | 脚本正常结束(message 是脚本的返回值) |
failed | 失败:脚本 fail()、推理/相机出错、脚本语法错、底盘掉线 |
aborted | 被停止:/api/demo/stop、人的运动指令接管、服务退出 |
脚本能用的原语(全部只有这些)
| 原语 | 说明 |
|---|---|
detect(model, opts?) | 取一帧跑一次推理;opts 可选 {conf=, iou=}(默认 0.25 / 0.45,同 /api/detect) → {frame_w=640, boxes={{x1,y1,x2,y2,w,h,cx,cy,area},...}};硬失败返回 nil, err("这一拍还没出帧"返回空列表,不是错误) |
forward(s) back(s) turn_left(s) turn_right(s) drive(l,r) | 驱动;s/l,r 是百分比,宿主一律 clamp 到 ±70 |
standby() brake() | 速度归零 / 刹车 |
sleep_ms(ms) | 等待(切段睡,随时可被打断) |
grab() release() | 夹爪(ZP10S 下是"伸下去→夹→抬起"约 3.5s 的整段序列) |
elapsed_ms() | 本脚本已跑的毫秒数 |
motor_connected() | 底盘是否真在线(掉线时驱动是空操作,脚本可据此提前收手) |
abort_requested() | 是否收到 stop(脚本可选择优雅收尾) |
note(k, v) | 往 /api/demo/status 的 notes 里发布一个可观测字段(调参用) |
log(fmt, ...) | 写日志(print 也是它) |
fail(msg) | 脚本主动判定失败 |
params() | 启动时传进来的参数表 |
数学/字符串/table 标准库可用;没有 io / os / package / coroutine / debug,也没有 pcall (见下)。
安全边界(宿主强制,脚本绕不过去)
这是会真开电机的功能,所以下面这些都不在脚本手里:
| 约束 | 由谁强制 |
|---|---|
| 速度上限 ±70% | 宿主 clamp 每个驱动原语的参数 |
| 执行方式 | mode:once 跑一遍,最多 5 分钟(到点宿主收工);loop 循环跑,没有总时长上限 —— 停不停由你按停止决定 |
| 被人的指令取代 | 脚本一驱动,宿主就记下指令代际号;摇杆//api/control 一进来代际号就变,脚本立刻被中断并交出控制权 |
| stop / 服务退出 / 底盘掉线 | 同上,立刻中断 |
| 内存 | Lua VM 用带预算的分配器(4MB),脚本狂建 table 也吃不光板子内存 |
| 脚本吞掉中断 | 不给 pcall/xpcall —— 脚本没法把宿主的打断 catch 住 |
| 退出时电机 | 宿主兜底刹车(脚本自己忘了停也一样) |
示例:demo/grab.lua(追到目标并抓起来)
curl -X POST http://<ip>/api/camera/open
curl "http://<ip>/api/detect?model=tennis" # 先看框多大,据此定 target_size
curl -X POST -H 'Content-Type: application/json' \
-d '{"script":"grab","params":{"model":"tennis","target_size":300,"speed":20,"mode":"once"}}' \
http://<ip>/api/demo/run
curl http://<ip>/api/demo/status # 边跑边看 action/notes
curl -X POST http://<ip>/api/demo/stop # 随时打断
判据("框宽 = 距离"这一条轴,参数与判据照搬隔壁仓库 aka0 那个预编译 demo,实机调过参): 取面积最大的框当目标,然后五选一 ——
| 情况 | 动作 |
|---|---|
| 框宽 > 目标 × 1.5(凑太近) | 后退一小段:脉冲 = 2.0ms/px × 超出量,夹在 300~700ms |
| 框宽 ≥ 目标 且 与夹爪位差 ≤ 25px | 到位:停稳 → 闭合夹爪(approach.lua 则只停不夹) |
| 偏离画面中心 > 80px | 大脉冲转向:脉冲 = 0.5ms/px × 偏离,夹在 300~400ms |
| 框宽 ≥ 目标 但没对准夹爪位 | 精调小脉冲转向(同上,上限 500ms,别转过头) |
| 框宽 < 目标 | 前进 600ms |
丢目标 1.5s 没找回就收工。三处脉冲的上下限都不是拍的:下限必须大于电机启动时间 (板上实测这块底盘 ~250ms 才转得起来),上限是"别转过头/退过头"(车尾没有眼睛)。
与那套 demo 的四处有意差异:① 用框宽像素判定(本项目口径)而不是框面积占比; ② 不做"抓前左转 3 次"的爪子偏置补偿(实测夹空再加);③ 丢目标即收工,不做没有超时的 原地找球;④ 凑太近会先退一小段(原来没有这一支,车几乎贴上去、夹爪反而够不着)。
夹爪(ZP10S)没有位置反馈,"夹到没有"无法确认 —— 脚本只能报告"抓取序列已执行完"。 另外 TPU 是单实例:跑动作脚本时别再并发调用
/api/detect(宿主内部串行,但会互相拖慢)。
WiFi
都作用在 wlan1 上(wlan0 是那台给用户连的 AP 热点,不动它)。
扫描网络
GET /api/wifi/scan
{"list": [{"ssid": "…", "id": "…", "signal": -52, "secured": true, "is_connected": false}],
"connected": "…"}
阻塞最多约 5 秒;排序是「已连接优先,其次信号从强到弱」,同名网络只留最强的那个。
连接
POST /api/wifi/connect
Content-Type: application/json
{"ssid": "WiFi名", "password": "密码"}
无密码时 password 传空字符串。阻塞约 8 秒等关联、再加最多 6 秒等 DHCP,成功回
{"ip": "…"}(还没拿到地址时回 "获取中...");失败回 408 +
{"error":"连接超时" | "连接失败,请检查密码或信号" | "未找到该网络"}。
连接成功会把 {ssid, password} 记进 /etc/aka-wifi.json(0600),capp 每次启动在
后台重放一次,所以不用每次开机都重连(只保留最后一个;rm 掉即"忘记网络")。
状态
GET /api/wifi/status # {"ssid": "…"|null, "ip": "…"}
GET /api/wifi/ip # {"ip": "…"}(没连上时回热点地址 192.168.4.1)
系统 IP
GET /api/system/ip # {"ip": "…"}
OTA 固件升级
当前版本
GET /api/ota/version # {"version": "v0.6.1", "updated": 1730000000, "service": "AKA-00"}
检查更新
GET /api/ota/check
去 config.toml 的 [ota] check_url 取版本信息,比语义版本号(退而比 updatedAt):
{"current_version": "…", "update_available": true, "latest_version": "…",
"hardware_desc": "…", "software_desc": "…", "url": "…"}
没有可用更新时回 404 {"status":"error","message":"未找到可用更新"}。
在线升级
POST /api/ota/upgrade
异步下载固件并安装,立刻回 {"status":"ok","task_id":"…"};已经是新版则回
{"status":"ok","message":"已是最新版本"}。固件与暂存都在 $AKA_HOME/.ota(不放 /tmp,
那是内存盘)。
直接上传固件升级(不联网)
POST /api/ota/update # multipart/form-data,文件部分就是固件
回 {"status":"ok","task_id":"…","message":"upload received, installing..."};没有文件回
400 {"status":"error","message":"no firmware file"}。
OTA 状态
GET /api/ota/status # 没有进行中的任务时 {"status":"idle"}
GET /api/ota/upgrade/progress?task_id=<id> # {"progress": 0-100, "status": "downloading|installing|done|error", "message": "…"}
系统信息
GET /api/system/info
{"ip": "192.168.4.1", "mac": "b8:27:eb:xx:xx:xx"}
GET /api/system/heartbeat
返回 CPU、内存、磁盘、运行时间等信息。
检测 · 识别 · Demo 运行 API
给调用方(平台、脚本、别人写的程序)的一份独立文档:从"看一眼画面"到"让小车自己追过去 把东西夹起来"这一整条链的 HTTP 接口。全部在板上实测过,坐标单位一律是原图像素。
想一次看全部接口(电机/机械臂/OTA/WiFi/系统)见 API 文档;这篇只讲 摄像头 → 推理 → 跑动作这条链,覆盖
/api/camera/*、/api/detect、/api/models/*与/api/demo/*。
一分钟上手
IP=<板子IP>
# ① 开摄像头(检测的前提;不开会回 "camera not available")
curl -X POST http://$IP/api/camera/open
# ② 看一眼识别结果(框的四个角,原图像素)
curl "http://$IP/api/detect?model=tennis"
# → {"ok":true,"count":1,"boxes":[{"x1":2,"x2":172,"y1":0,"y2":360}]}
# ③ 跑一次 demo:用 tennis 模型做"接近瞄准",框宽到 320px 就算到位
curl -X POST http://$IP/api/demo/init -H 'Content-Type: application/json' \
-d '{"action":"approach","model":"tennis","target_size":320,"speed":30,"turn_speed":25}'
# → {"completed":true} ← 默认等它跑完再返回(下面的"wait"一节)
③ 里没有建任何卡片:action + model + 参数凑齐就是一次完整请求。车会真的动。
1. 摄像头(检测的前提)
| 接口 | 说明 |
|---|---|
GET /api/camera/status | {"camera_on": true/false} |
POST /api/camera/open | 打开采集。成功 200 {"camera_on":true};打不开则 500 + {"camera_on":false}(原因见板子日志) |
POST /api/camera/close | 关闭采集(顺带停屏) |
GET /api/camera/snapshot | 单帧 JPEG,base64 塞在 JSON 里(见下) |
GET /api/camera/stream | MJPEG 流(multipart/x-mixed-replace),直接给 <img src="http://<ip>/api/camera/stream"> |
snapshot 的响应(实测 640x360 q70 约 46KB JSON):
{"image":"/9j/4AAhQVZ...", "format":"jpeg", "width":640, "height":360, "m":0.05, "c":-2.82}
m/c是像素→实际距离的标定系数(config.toml的[camera] calib_m/calib_c), 用来自算"这个框离我多远"。不需要就算着玩,detect不用它们。
分辨率/帧率/画质都在 config.toml 的 [camera](width/height/fps/jpeg_quality),
接口里不能改。stream 默认直通摄像头原始 MJPEG(服务端零解码零编码),所以带宽约
4~7 Mbps;嫌大就调低 jpeg_quality。
2. 检测 / 识别:GET /api/detect
"检测"和"识别"在这个系统里是同一件事:把一帧送进模型,拿回几个框。模型认什么由它自己 训练决定(网球、方块、橘子…),接口不管语义 —— 它只把框给你。
GET /api/detect?model=<模型名>[&conf=0.25][&iou=0.45]
| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| model | query | 是 | 模型名(= demo/models/<名字>.cvimodel 的文件名,不带后缀)。只允许字母数字与 _ - . |
| conf | query | 否 | 置信度阈值,(0,1);不给用 0.25 |
| iou | query | 否 | NMS 的 IoU 阈值,(0,1);不给用 0.45 |
成功(HTTP 200):
{"ok": true, "count": 1, "boxes": [{"x1": 2, "x2": 172, "y1": 0, "y2": 360}]}
| 字段 | 含义 |
|---|---|
| boxes[].x1 / y1 | 左上角(原图像素,浮点) |
| boxes[].x2 / y2 | 右下角 |
| count | 框数(可能 0 个 —— 那不是错误) |
框宽 =
x2 - x1,中心 =(x1+x2)/2, (y1+y2)/2—— REST 版只给四角,宽高/中心/面积 自己算。脚本里的detect()会多给w/h/cx/cy/area(见 §5)。坐标是原图(如 640x360),不是页面上
stream缩放后的尺寸 —— 两个尺寸不一致时 别拿标尺去量屏幕。
失败:
| 情况 | HTTP | 响应 |
|---|---|---|
| 没给 model | 400 | {"ok":false,"error":"缺少 model 参数(例:/api/detect?model=tennis)"} |
| model 名字非法 | 400 | {"ok":false,"error":"model 名字非法(只允许字母数字与 _ - .):../x"} |
| conf/iou 越界或不是数 | 400 | {"ok":false,"error":"conf / iou 要在 0~1 之间(如 ?conf=0.6&iou=0.3);不给就用默认 0.25 / 0.45"} |
| 摄像头不可用(没开、或设备打不开) | 500 | {"ok":false,"error":"camera not available"} |
| 摄像头开着但这一拍还没出帧 | 500 | {"ok":false,"error":"no frame"} |
| 模型文件不在或坏了 | 500 | {"ok":false,"error":"注册模型失败(CVI_NN_RegisterModel rc=…)"} 之类 |
要"连续看"就轮询这个接口(车上一帧推理约 100~340ms,取决于有没有同时写着屏); 要高帧率画面用
/api/camera/stream,它不跑模型。
conf给错值直接报错而不是悄悄用默认 —— 调参时最怕"以为生效了其实没生效"。
3. 模型("认什么")
模型就是 $AKA_HOME/demo/models/<名字>.cvimodel 一个文件,文件名去掉后缀就是 model 参数的值。
GET /api/demo/list 的 models 字段能拿到板上现有的清单:
curl http://$IP/api/demo/list
# → {"demos":[…], "actions":[{"id":"approach","name":"接近瞄准"},…], "models":["block","orange","tennis"]}
上传
| 接口 | 谁用 | 怎么发 |
|---|---|---|
POST /api/models/upload?name=<名字> | 平台 / curl 推文件 | 名字在 query,body 就是文件裸内容(也认 multipart 的 file 字段) |
POST /api/model/upload | 浏览器表单直传(CORS 已开) | multipart,两个字段 file + name |
curl --data-binary @orange.cvimodel "http://$IP/api/models/upload?name=orange"
# → {"ok":true,"name":"orange","path":"/root/AKA-00/demo/models/orange.cvimodel","size":12864632}
curl -F "file=@orange.cvimodel" -F "name=orange" "http://$IP/api/model/upload"
# → {"status":"ok","name":"orange","size":12864632,"path":"…"}
同名覆盖,且覆盖即生效(下一次 /api/detect 就用新模型,不用重启)。校验:文件头必须是
CviModel(挡"传错文件")、大小上限 32MB;先落 .part 再原子换入,传一半断了不会毁掉
正在用的那颗。
删除
curl -X POST http://$IP/api/models/delete -H 'Content-Type: application/json' -d '{"name":"orange"}'
# → {"ok":true,"name":"orange","cards":["追橘子"]} ← cards = 正在用它的卡片名
只删这一个文件。用它建过的卡片不会跟着删,只是变成 ready:false、点开始报
模型文件缺失 —— 重传一个同名模型就原地复活。删完记得 GET /api/demo/list 刷新清单。
4. Demo 运行
概念:一张卡片 = 动作 × 模型
- 动作 =
demo/<动作>.lua脚本(预定义、与模型无关:approach接近瞄准、grab追到就夹); - 模型 =
demo/models/<模型>.cvimodel; - 卡片 = 用户起的名字 + 上面两者 + 四个参数,存
demo/configs/<卡片名>.json。
卡片不是必需的:action + model + 参数 直接发就是一次完整请求(不落盘、页面不留卡片)。
卡片只是"存下来反复用"的壳。
跑一次:POST /api/demo/init
两种形状,二选一:
// ① 跑存下来的卡片(界面上点"开始"就是这条)
{"name": "追网球接近"}
// ② 临时组装一次(不用建卡)
{"action": "approach", "model": "tennis",
"target_size": 320, "speed": 30, "turn_speed": 25, "mode": "once"}
| 字段 | 说明 |
|---|---|
| name | 卡片名。给了它就以卡片为准,下面那些参数是"临时覆盖",不落盘 |
| action | 动作名(demo/<action>.lua)。没有 name 时必填 |
| model | 模型名。没有 name 时必填 |
| target_size | 目标框宽(原图像素):框宽达到它就认为到位。缺省 300 |
| speed | 直线速度百分比。缺省 25(宿主还会再 clamp 到 ≤70) |
| turn_speed | 转弯速度百分比。缺省 25 |
| mode | once(默认,跑一遍)/ loop(跑完接着跑,直到你按停止,没有时长上限) |
| wait | 见下节。默认 true |
响应:
// 默认(wait=true):等这次跑完再返回,只给一个完成标志
{"completed": true}
{"completed": false, "error": "目标丢失(1595ms 没看到目标)"} // 跑完了但没成功
{"completed": false, "error": "timeout: 到最大执行时间(5 分钟)"} // 到时限被宿主收工
// wait=false:立刻返回"起来了"
{"status":"started","name":"approach","script":"approach","action":"approach",
"model":"tennis","pid":682,"pgid":682,"completed":false}
wait 的语义(最容易被误解的一条):
- 它只决定这一个 HTTP 请求等不等,不是脚本的时限。
once默认等 → 一次请求拿到结论;loop默认不等(循环不会自己结束,等下去就是挂死 连接),显式传"wait": true直接 400。- 请求最多等 310 秒;而脚本自身在
once模式下最多跑 5 分钟(到点宿主收工、 状态落aborted、error写明"到最大执行时间")。所以正常情况是先由脚本收工、请求拿到 真实原因。 - 想自己轮询就传
"wait": false,然后看/api/demo/status。
失败响应:
| 情况 | HTTP | 响应 |
|---|---|---|
| 卡片不存在 | 400 | {"error":"没有这张卡片(或配置读不了):demo/configs/xxx.json"} |
| 卡片名非法 | 400 | {"error":"卡片名非法(不能含 / \\ 与控制字符,不能以 . 开头):../x"} |
| 动作脚本不存在 | 400 | {"error":"动作脚本不存在:demo/<动作>.lua"} |
| 模型文件不存在 | 400 | {"error":"模型不存在:demo/models/<模型>.cvimodel"} |
| 没给 name 也没给 action+model | 400 | {"error":"要么给 name(跑已建的卡片),要么给 action + model(直接跑)"} |
loop + "wait": true | 400 | {"error":"loop 模式不会自己结束,wait 没有意义(要停就 POST /api/demo/stop)"} |
| 已经有一个脚本在跑 | 409 | {"status":"already_running","pid":…,"name":"<在跑的卡片>","error":"已有脚本在跑(先 POST /api/demo/stop)"} |
一次只能跑一个:脚本驱动电机是独占的,第二个请求 409。要换就
stop再起。
最底层那条:POST /api/demo/run
{"script": "grab", "params": {"model": "tennis", "target_size": 300, "mode": "once"}}
和 init 的区别:不校验模型/卡片,params 原样交给脚本(调试、一次性用)。响应字段与
init 一致({ok,state,script,mode} / 等跑完时 {completed:…})。
看状态:GET /api/demo/status
{"state":"running","script":"approach","model":"tennis","card":"追网球接近","mode":"once",
"round":1,"calls":92,"action":"forward","message":"",
"notes":{"box_w":"298","offset":"28"}}
| 字段 | 含义 |
|---|---|
| state | idle(没在跑)/ running / done(正常结束)/ failed(脚本 fail、推理/相机错、底盘掉线…)/ aborted(被停:stop、人的指令接管、服务退出、或 once 到 5 分钟时限) |
| script | 正在跑的动作名 |
| model / card | 在追哪个模型 / 哪张卡片(直接 action+model 跑时 card 为空串) |
| mode / round | 执行方式 / 循环跑到第几轮 |
| message | 结束原因或脚本的返回值(done 时就是脚本 return 的那句话) |
| action | 最近一次电机原语(forward/turn_left/standby…),空 = 还没动过 |
| calls | 脚本调用原语的次数(心跳/活性) |
| notes | 脚本用 note(k,v) 发布的自定义观测值(如 box_w/offset)—— 调参就靠它 |
停:POST /api/demo/stop
立刻刹车、不等脚本配合;脚本会在下一次调原语时被中断,状态转 aborted。
没在跑时返回 {"status":"already_stopped"}(不报错)。
卡片管理
| 接口 | 说明 |
|---|---|
GET /api/demo/list | 卡片 + 动作 + 模型清单(新建卡片的下拉就用它)。卡片字段:name/action/model/ready/error/kind/script/path |
GET /api/demo/config?name=<卡片名> | 读一张卡的参数({name,action,model,target_size,speed,turn_speed,mode}) |
POST /api/demo/config | 新建或覆盖:{"name":…,"action":…,"model":…,"target_size":…,"speed":…,"turn_speed":…,"mode":…}。改参数时 action/model 也要回传,否则会被当成新建 |
POST /api/demo/delete | {"name":"<卡片名>"} —— 只删这张卡;动作脚本与模型文件不受影响 |
GET /api/demo/name | {name,action,model}:正在跑的是哪张卡(老接口,等价于 status 里的 card) |
ready:false 表示动作脚本或模型文件缺了 —— 卡片照样列出来(点开始会明确报错),不会凭空消失。
5. 动作脚本(要加新动作时)
动作就是 $AKA_HOME/demo/<动作名>.lua 一个文件,宿主内置 Lua 解释器跑它。
第一行的约定注释给显示名(界面上的"接近瞄准"就是这么来的):
-- name: 我的动作
local p = params() or {}
local model = p.model -- 宿主注入:卡片/请求里配的模型
local target = tonumber(p.target_size) or 300
while true do
local d, err = detect(model) -- 取一帧跑一次推理
if not d then fail("推理失败:" .. tostring(err)) end
local best = nil
for _, b in ipairs(d.boxes) do
if not best or b.area > best.area then best = b end
end
if not best then standby(); sleep_ms(50)
elseif best.x2 - best.x1 >= target then brake(); return "到位了"
else forward(p.speed or 20); sleep_ms(600); standby() end
end
能用的原语(全部只有这些)
| 原语 | 说明 |
|---|---|
detect(model, opts?) | 取一帧推理;opts 可给 {conf=, iou=}(默认 0.25 / 0.45)→ {frame_w=640, boxes={{x1,y1,x2,y2,w,h,cx,cy,area},…}};硬失败返回 nil, err("这一拍还没出帧"给空列表,不算错误) |
forward(s) back(s) turn_left(s) turn_right(s) drive(l,r) | 驱动,参数是百分比,宿主一律 clamp 到 ±70 |
standby() brake() | 速度归零 / 刹车 |
sleep_ms(ms) | 等待(切段睡,随时可被打断) |
grab() release() | 夹爪(ZP10S 上是"伸下去→夹→抬起"约 3.5s 的整段序列) |
elapsed_ms() | 本脚本已跑的毫秒数 |
motor_connected() | 底盘是否真在线(掉线时驱动是空操作) |
abort_requested() | 是否收到 stop(可选优雅收尾) |
note(k, v) | 往 /api/demo/status 的 notes 里发一个可观测值 |
log(fmt, …) / print | 写进 capp 日志 |
fail(msg) | 主动判定失败(状态 failed) |
params() | 启动时传进来的参数表 |
有 math/string/table 标准库;没有 io / os / package / coroutine / debug,也没有 pcall (脚本吞掉宿主的打断就糟了)。内存预算 4MB。
板上现成两个动作:
approach(-- name: 接近瞄准,只靠近不夹)、grab(-- name: 追到就夹)。 别改这两个文件 —— 它们是仓库里的代码、OTA 会按包覆盖;想调参请改卡片配置或另写一个 动作脚本(demo/<新名字>.lua,OTA 不会删你新加的)。
6. 安全边界(宿主强制,脚本和调用方都绕不过去)
| 约束 | 说明 |
|---|---|
| 速度上限 ±70% | 每个驱动原语的参数都被 clamp |
| 一次一个脚本 | 再发 init/run 会 409,先 stop |
once 最多 5 分钟 | 到点宿主收工(停电机、状态 aborted、error 写明原因) |
loop 没有时长上限 | 它本来就该一直跑,停不停由人决定 |
| 被人的指令取代 | 有人推摇杆 / 调 /api/control,脚本立刻被中断并交出控制权 |
| stop / 服务退出 / 底盘掉线 | 立刻中断(脚本收尾时宿主兜底刹车) |
| 内存 | Lua VM 4MB 预算 |
7. 一条龙例子(可直接复制)
IP=<板子IP>; MODEL=tennis
# 1) 传一颗模型(同名覆盖、覆盖即生效)
curl --data-binary @orange.cvimodel "http://$IP/api/models/upload?name=orange"
# 2) 开摄像头,看它在哪、框多大(据此定 target_size)
curl -X POST http://$IP/api/camera/open
curl "http://$IP/api/detect?model=$MODEL&conf=0.5"
# 3) 跑一次:不建卡,直接组装;默认等跑完
curl -X POST http://$IP/api/demo/init -H 'Content-Type: application/json' \
-d "{\"action\":\"approach\",\"model\":\"$MODEL\",\"target_size\":320,\"speed\":30,\"turn_speed\":25}"
# 4) 想边跑边看:wait=false + 轮询 status(notes 里有 box_w/offset,调参就看它)
curl -X POST http://$IP/api/demo/init -H 'Content-Type: application/json' \
-d "{\"action\":\"approach\",\"model\":\"$MODEL\",\"target_size\":320,\"wait\":false}"
while true; do curl -s http://$IP/api/demo/status; echo; sleep 1; done
# 5) 停
curl -X POST http://$IP/api/demo/stop
8. 常见坑(都是踩过的)
| 现象 | 原因 |
|---|---|
/api/detect 回 camera not available | 摄像头没开(先 POST /api/camera/open),或设备打不开 |
/api/detect 回 no frame | 摄像头开着、但这一拍还没出帧(刚开没多久);连续这样就是流卡住了 |
POST /api/camera/open 一直 500、但 snapshot 还能出图 | 采集流卡死、缓存里还有旧帧(两次 snapshot 字节完全相同就是在骗你)。重启 capp 恢复(实测 2026-09-21;根因待查:卡死后没法从 API 侧再拉起来) |
| 框的坐标对不上屏幕 | 坐标是原图像素;页面上的画面可能被缩放过 |
init 一直不返回 | 默认 wait=true,它在等脚本跑完(最多 310 秒)。想立刻拿控制权就 wait:false |
请求回 timeout: 等待 310 秒… 但车还在动 | 请求放弃等了,脚本还在跑 —— 补一个 POST /api/demo/stop |
loop 模式传了 wait:true 被 400 | 循环不会自己结束,等下去是挂死连接 |
第二个 init 回 409 | 已经有一个脚本在跑,先 stop |
| 删了模型,卡片还在但点开始报错 | 卡片不跟着删(故意的);重传同名模型即复活 |
传模型回 不是 cvimodel(文件头不是 CviModel) | 传错文件了 |
| 相机开着但屏没画面(带屏板) | 屏跟随摄像头:摄像头一开就自动起屏;/api/display/status 看 running |
底盘 · 电机控制 API(前端对接用)
面向写界面/客户端的人:怎么让小车动、怎么读它的实时状态、怎么让摇杆跟手。
覆盖 /api/control、/api/motor/*、/api/config/speed,以及 /ws/control 里与驱动有关的部分。
下面所有响应都是板上实测的。
本文只管"底盘怎么动"。推理与 demo 见 检测 · 识别 · Demo 运行 API, WebSocket 完整协议见 WebSocket 控制接口。
0. 四条路,先选对
| 你要做的事 | 用哪个 |
|---|---|
| 摇杆 / 连续控制(推荐) | WebSocket /ws/control 的 {"type":"action",…},顺带收轮速推送 |
| "往前走一段 / 转 90 度" | GET /api/control?action=…&time=/distance=/angle= |
| 精确控制左右轮(差速、自己算闭环) | GET /api/motor/direct?left=&right=&duration= |
| 显示速度 / 连接状态 / 夹爪 | GET /api/motor/status(轮询)或 WS 推送 |
| 界面上的"默认速度" | GET/POST /api/config/speed(后端只存不读,见 §4) |
1. GET /api/control —— 命名动作
GET /api/control?action=up&speed=40&time=500
| 参数 | 单位 | 说明 |
|---|---|---|
| action | — | up(前进)/ down(后退)/ left / right / stop;另有臂动作 grab / release |
| speed | % | 1~100(<1 抬到 1,>100 压到 100;不给默认 50) |
| time | 毫秒 | >0:阻塞到跑完、自动停车才返回;0/不给:立刻返回,车不停(要自己再发 stop) |
| distance | 厘米 | 仅 up/down。走这么远(ESP32 闭环),双向校验 |
| angle | 度 | 仅 left/right。转这么多度 |
响应只有完成标志(客户端不用为失败另写一套解析):
{"completed": true} // 200
{"completed": false, "error": "unsupported action: forward"} // 400,动作名写错
{"status": "error", "message": "angle 仅对 left/right 动作有效"} // 400,参数配错动作
speed的别名注意:没有forward,前进叫up。- 两种失败形状不同(实测):动作名不认识 =
{"completed":false,"error":"unsupported action: xxx"};distance/angle配错动作 ={"status":"error","message":"…"}(distance仅up/down、angle仅left/right)。都带 400。 distance/angle是阻塞的(等固件闭环回报),大距离能挂几秒~几十秒;客户端别设短超时, 服务端自己 30 秒兜底。grab/release是臂动作,服务端会等那套约 3.5 秒的序列做完再回completed。
2. GET /api/motor/direct —— 直接给左右轮速度
GET /api/motor/direct?left=25&right=-25&duration=2
| 参数 | 单位 | 说明 |
|---|---|---|
| left / right | % | ±100(负数 = 反转)。超范围静默截到 ±100,不报错 |
| duration | 秒 | >0:阻塞到时长结束、自动停车后才返回;0/不给:立刻返回,车不停 |
// duration > 0
{"status":"success","left":25,"right":-25,"duration":10,"completed":true,"mode":"completed",
"left_speed":0.1363,"right_speed":-0.0974} // 后两个是**实测**轮速 m/s
// duration = 0
{"status":"success","left":0,"right":0,"left_speed":0,"right_speed":0}
板上实测(2026-09-21):
| 命令 | 结果 |
|---|---|
L=R=25&duration=3 | 全程 0.05→0.10 m/s,3 秒后自动停 |
L=+25 R=-25&duration=10 | 原地转,全程保持到第 10 秒,到点滑行停 |
L=-30 R=+30&duration=1 | 反向(实测 left_speed 为负) |
left=150 | 实际按 100 跑(0.56 m/s),响应回显的仍是 150 |
两个要记住的点:
- 响应里的
left/right是你发的原值,不是生效值(超范围会被静默截断); 要生效值看left_speed/right_speed(实测)或/api/motor/status的*_target。- 不要指望服务端替你保活:
duration=10的请求会阻塞 10 秒,但期间不重发速度命令。 实测这块板子上单发一条能连续转 10 秒以上(代码注释里"只转 0.2~0.9s"的旧说法已不成立), 所以外部控制器 1 秒发一条足够;要更稳就自己定时重发。
3. GET /api/motor/status —— 前端显示用
{"left_speed":0.0649,"right_speed":0.0844,"left_target":150,"right_target":0,
"gripper_status":"unknown","gripper_target":0,
"motor":{"backend":"tt_pid","state":"connected","connected":true,"enabled":true,"attempts":0,"error":""}}
| 字段 | 含义 |
|---|---|
left_speed / right_speed | 实测轮速,m/s(有符号:负 = 反转)。启动瞬间是 0,等 0.2~0.5 秒再看 |
left_target / right_target | 最近一次目标速度(%),你发什么它就记什么(含超范围的原值) |
motor.state | connected / reconnecting / disabled(自动重连代理的状态) |
motor.connected | 底盘是否真在线。掉线时运动指令被丢弃(不再有 mock 兜底) |
motor.attempts / motor.error | 重连次数 / 最近一次错误(排障用) |
gripper_status / gripper_target | 夹爪状态(unknown = 还没读到)/ 目标位置 |
轮询建议:状态采集是 10Hz,界面用 200~500ms 轮询足够;要跟手就用 WS 推送(§5), 别用 50ms 轮询(单核板子会被拖慢)。
4. GET/POST /api/config/speed —— 界面上的"默认速度"
GET /api/config/speed → {"forward_speed":55,"turn_speed":40}
POST /api/config/speed body: {"forward_speed":55,"turn_speed":40}
存在 $AKA_HOME/speed_config.json。
后端不做任何运动决策时会用它:它只是"界面默认值"的存储。 现在的前端是
BaseControlPage拿forward_speed(并 clamp 到 ≤60)当摇杆的初始速度、SpeedConfigPage拿它当设置页的初值 —— 发指令时速度是显式带的(WS/HTTP 参数), 所以改了这里不会影响已经发出去的指令,也不会影响 demo 脚本(脚本的速度来自卡片参数)。
5. WebSocket /ws/control —— 摇杆 / 实时控制(推荐)
一条长连接,双向:发动作、收轮速。前端现有的摇杆就是这么做的。
发(JSON 文本帧):
{"type":"action","action":"up","speed":40,"time":300} // time 毫秒;0 = 不停,需要自己发 stop
{"type":"action","action":"stop"} // 急停
- 服务端回
{"type":"action","result":{…}}(result就是 §1 里那些字段)。 - 只认命名动作(
up/down/left/right/stop/grab/release)—— 没有直接给左右轮的通道; 要差速控制走 HTTP 的/api/motor/direct。
收(二进制帧):
| 首字节 | 内容 |
|---|---|
0xBB | 轮速推送:[0xBB, left_int16_LE, right_int16_LE],单位 mm/s,前端除 1000 得 m/s(≈10Hz) |
0xDD | JSON 文本帧(前面 1 字节是 0xDD):action 结果、motor_status(连接状态)、ip 等 |
摇杆交互的常见坑:拖动时按帧率发
action(例如 10Hz)就能跟手;松手必须发一次stop(time只保证"最长转多久",不保证松手就停)。
6. 三种"停车",别搞混
| 怎么发 | 底层动作 | 效果 |
|---|---|---|
/api/control?action=stop | brake() | 急停(钉住,轮子立刻不动) |
/api/motor/direct?left=0&right=0 | sleep() | 滑行停(会溜一点) |
带 time=/duration= 的命令到点 | sleep() | 同上,滑行停 |
界面上的"停止"按钮用 action=stop(急停)更符合直觉。
7. 谁优先:人的指令 > 脚本
- 任何运动指令(HTTP 或 WS)都会让正在跑的 demo 脚本被判
superseded: 被新的运动指令取代(人接管)并立刻中断、交出控制权 —— 设计如此, 摇杆一动脚本就停。 /api/control、/api/motor/direct不受"一次只能一个脚本"的 409 限制:随发随生效。- 底盘掉线(
motor.connected:false)时运动指令被丢弃 —— 界面上应该据此禁用摇杆并提示。
8. 常见坑(都实测过)
| 现象 | 原因 |
|---|---|
duration=0/time=0 发完车不停 | 这是"设定速度"语义,不是"走一段";要停就再发一次(急停用 action=stop) |
| 直驱能到 100%,但 demo 脚本最快只到 70% | 两条路的上限不同:/api/motor/direct 是 ±100,脚本/命名动作里宿主会把速度 clamp 到 ±70 |
响应里 left=150 以为生效了 150 | 超范围被静默截断到 100,响应回显原值;看 left_speed(实测)或 status.left_target |
发了速度但 left_speed 还是 0 | 轮速是实测值,启动有 0.2~0.5 秒延迟;也可能底盘掉线(看 motor.connected) |
| 代码注释说"单发一条只转 0.2~0.9s,要 80ms 重发" | 已过时:这块板子上单发能连续转 10 秒以上(§2 实测表) |
distance=30 只走了 3cm | 那是老 bug(cm/mm 差 10 倍),早已修;现在 distance 就是厘米 |
改了 /api/config/speed 车没变快 | 后端不读它(§4),它只是界面默认值的存储 |
解析 {completed,error} 时遇到 {status,message} | distance/angle 配错动作走的是后者(见 §1),错误处理两条都要覆盖 |
| 摇杆松手车还在走 | 松手要发 stop;time 只是上限 |
9. 前端最小示例
// ① 摇杆:按住时按帧率发,松手发 stop
const ws = new WebSocket(`ws://${location.host}/ws/control`);
ws.binaryType = "arraybuffer";
ws.onmessage = (e) => {
const dv = new DataView(e.data as ArrayBuffer);
if (dv.getUint8(0) === 0xBB) { // 轮速推送(mm/s)
const left = dv.getInt16(1, true) / 1000; // → m/s
const right = dv.getInt16(3, true) / 1000;
setWheelSpeed({left, right});
}
};
const drive = (action: string, speed: number) =>
ws.send(JSON.stringify({type: "action", action, speed, time: 300}));
const stop = () => ws.send(JSON.stringify({type: "action", action: "stop"}));
// ② 走一段 / 转个角度(阻塞到做完,别设短超时)
await fetch("/api/control?action=up&speed=40&distance=50"); // 前进 50 厘米
await fetch("/api/control?action=left&speed=35&angle=90"); // 左转 90 度
// ③ 差速直驱(自己算闭环)
await fetch("/api/motor/direct?left=30&right=15&duration=2"); // 2 秒后自动停
// ④ 显示
const st = await fetch("/api/motor/status").then(r => r.json());
// st.left_speed / st.right_speed(实测 m/s)、st.motor.connected(掉线提示)
距离标定
小车支持通过 YOLO 视觉检测 + 标定公式估算目标物体的真实距离。
标定公式
D = m / P + c
| 符号 | 含义 | 单位 |
|---|---|---|
| D | 目标物体的真实距离 | cm |
| P | 检测框在画面中的像素尺寸(取宽高中的较大值) | px |
| m | 标定乘数,由相机焦距和物体实际尺寸决定 | — |
| c | 标定偏移,修正系统误差 | — |
标定参数
标定参数 m 和 c 是 C++ 侧的编译期常量,在 cpp/csrc/include/csrc/config.hpp:
// 距离标定: D = m / P + c
double calib_m = 2671.82;
double calib_c = -2.82;
注意:它们不在
config.toml里,改了要重新交叉编译 + 部署(make -C cpp ota) 才生效 —— 不像相机/电机那些参数改配置文件重启就行。
如何标定:将目标物体(如网球)放在已知距离处,在画面中测量其像素尺寸
P,代入公式反算出适合自己场景的m和c值。
获取标定参数
小车的 /api/camera/snapshot 接口在返回图片的同时,会携带当前的标定参数:
GET /api/camera/snapshot
响应:
{
"image": "<base64 jpeg>",
"width": 640,
"height": 360,
"format": "jpeg",
"m": 2671.82,
"c": -2.82
}
| 字段 | 说明 |
|---|---|
| image | 640×360 的 JPEG 图片(采集分辨率,letterbox 等比缩放) |
| m | 标定乘数 |
| c | 标定偏移 |
YOLO 距离仪表盘
项目提供了电脑端仪表盘 tests/server_dashboard.py,可从浏览器直观查看检测结果和距离估算。
启动
python tests/server_dashboard.py --model tests/model/tennis.onnx --port 8080
浏览器打开 http://localhost:8080,输入小车 IP 地址(如 192.168.4.1),点击「开始」即可。
工作流程
小车 电脑端仪表盘
───── ─────────────
/api/camera/snapshot ←─── 拉取图片 + m, c
本地 YOLO 检测目标
计算 D = m / P + c
画框标注,显示距离
界面说明
| 区域 | 内容 |
|---|---|
| 距离显示 | 实时显示计算出的距离值(cm) |
| 标定参数 | 显示当前使用的 m、c 值 |
| P 值 | 显示检测框的像素尺寸 |
| 图片 | 640×360 画面,检测框以绿色矩形标注 |
参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
--model | tennis.onnx | YOLO ONNX 模型路径 |
--port | 8080 | 本机监听端口 |
如果没有 YOLO 模型或模型加载失败,仪表盘仍可运行,仅显示图片和 m/c 参数,不进行检测。
使用sg2002
SG2002 是一款面向 AIoT 领域的高性能、低功耗 SoC,内置多个处理器核心,集成 TPU、视频编解码器、丰富外设接口,适用于智能视觉、边缘计算等场景。
硬件架构
-
处理器
主处理器: RISCV C906 @ 1.0Ghz 和 ARM Cortex-A53 @ 1.0Ghz 协处理器: RISCV C906 @700Mhz
-
TPU
算力为 1TOPS(INT8),适用于AI推理计算
-
视频子系统
视频输出:支持 2L MIPI DSI 输出(分辨率 2880×1620@30fps),兼容 LVDS、BT.601/656/1120 等传统接口。
视频输入:支持 ISP(图像信号处理器),最高 5MP@30fps;支持 4L 或 2L+2L MIPI CSI 接口,兼容 DVP、Sub-LVDS、HisPI 等。
视频编解码:解码:H.264,支持 5MP@30fps。
编码:H.264/H.265,支持 5MP@30fps。
连接方式
-
串口连接
sudo apt install minicom # 安装minicom minicom -D /dev/ttyUSB0 -b 115200 # 连接串口,用户名:root,密码:root -
usb rndis 网口连接
ip a show # 查看网口信息.如果主机是10.245.118.100,则开发板是10.245.118.1。 ssh root@10.245.118.1 # 连接开发板,密码:root -
wifi 连接
# 假设分配的地址为192.168.1.2 ssh root@192.168.1.2 # 连接开发板,密码:root
配置 UART 串口
-
验证方法
# 开发板上验证外设串口(板载 busybox 没有 miniterm,用随包发布的板测工具) ./tools/tt_pid_test /dev/ttyS1 40 3000 # 底盘链路直测,默认 /dev/ttyS1 40 3000 ./tools/cam_probe # 摄像头出帧直测(跑之前先停 capp) # 主机上执行(利用minicom) minicom -D /dev/ttyUSB0 -b 115200 # 连接串口 -
uart0 默认开启,无需配置
-
uart1 默认开启,无需配置。但如果要同时使用uart1和uart2, 则需要进行配置。
devmem 0x03001070 32 0x2 # GPIOA 28 UART2 TX devmem 0x03001074 32 0x2 # GPIOA 29 UART2 RX devmem 0x03001068 32 0x6 # GPIOA 18 UART1 RX devmem 0x03001064 32 0x6 # GPIOA 19 UART1 TX -
uart3 引脚默认复用为SDIO。而SDIO被用于wifi连接。所以在有wifi连接的情况下,不能使用uart3。
devmem 0x030010D0 32 0x5 # GPIOP 18 UART3 CTS devmem 0x030010D4 32 0x5 # GPIOP 19 UART3 TX devmem 0x030010D8 32 0x5 # GPIOP 20 UART3 RX devmem 0x030010DC 32 0x5 # GPIOP 21 UART3 RTS
代码结构
板上跑的一切都在 cpp/ 里:编译成单个 riscv64 静态二进制 aka-capp,加上配置和前端
产物,一起打进 aka-00-server 安装器(见快速开始)。根目录其余
部分是 Python 版(历史实现)的遗留文件,不再维护。
目录
AKA-00/
├── cpp/
│ ├── capp/ # aka-capp 服务进程
│ │ ├── src/main.cpp # 入口:读配置 → 起服务
│ │ ├── src/http/ # 自研 HTTP 服务(socket + 线程 + 路由 + 报文)
│ │ ├── src/routes/ # 接口路由,一个域一个文件(motor/camera/arm/demo/…)
│ │ ├── src/services/ # 业务服务:控制、摄像头、demo、推理、WiFi、上报
│ │ ├── src/websocket.cpp # /ws/control 二进制通道
│ │ ├── src/script.cpp # Lua 流程脚本宿主
│ │ └── include/capp/ # 头文件(context.hpp 是全局上下文)
│ │
│ ├── csrc/ # 平台与硬件层(编成 libcsrc.a,不依赖 capp)
│ │ ├── src/camera/ # V4L2 采集 + JPEG 解码
│ │ ├── src/display/ # 板载 SPI 屏显示(仅带屏版编译)
│ │ ├── src/motor_pair.cpp # 差速运动学
│ │ ├── src/tt_pid.cpp # ESP32 底盘串口协议(闭环)
│ │ ├── src/gripper.cpp # 夹爪
│ │ ├── src/zp10s.cpp # ZL-ZP10S 舵机
│ │ ├── src/sts3215.cpp # STS3215 舵机
│ │ ├── src/yolo*.cpp # NPU 推理(.cvimodel)
│ │ ├── src/{config,state,serial,system_utils,angle_config}.cpp
│ │ ├── src/{http_client,https_client}.cpp # OTA / 状态上报的出栈请求
│ │ ├── tools/ # 板测工具:cam_probe / tt_pid_test / screen_test
│ │ └── include/csrc/ # 头文件
│ │
│ ├── board/ # 板上 $AKA_HOME/ 的镜像(实体文件,见下)
│ ├── scripts/ # 第三方库构建(libjpeg / mbedTLS / Lua)+ 安装器打包
│ └── third_party/ # 交叉编译产物(构建时生成,不进版本库)
│
├── frontend/ # React 前端源码,构建产物写到 ../static
├── static/ # 前端构建产物(打包时收进部署目录)
├── tests/ # 板测 demo 源码(demo_camera.c 等)与测试脚本
├── hardware/ # 原理图、舵机手册
└── docs/ # 本文档(mdBook)
板上目录($AKA_HOME)
部署到板子上的内容 = cpp/board/ 原样 + 三样构建产物(aka-capp、static/、
tools/)。所以"这个文件是哪来的"只有两种答案:要么在 cpp/board/ 里,要么是编出来的。
$AKA_HOME/
├── aka-capp # riscv64 静态二进制(构建产物)
├── static/ # 前端页面(构建产物)
├── tools/ # 板测工具(构建产物)
├── config.toml # 唯一配置文件:端口、串口、摄像头、屏、OTA 地址
├── init.sh / stop.sh # 启动(自愈循环,崩溃自动重启)/ 停止
├── init_ap_web.sh # 装 AP 热点 + 开机自启(一次性,无状态)
├── S97akaap # 首次开机自举 AP(带幂等锁)
├── https_init.sh # 缺证书时自动生成自签证书
├── demo/ # grab.lua / approach.lua(动作脚本)+ models/ + configs/
├── arm_angles.json # 机械臂标定角度(现场数据)
├── arm_angles_default.json
├── speed_config.json # 行驶速度(现场数据)
├── VERSION # 版本号(OTA 比对用)
└── start_img.jpg # 屏显示待机图
想改什么,改哪里
| 想改什么 | 改哪里 |
|---|---|
| 端口、串口、摄像头、屏、OTA 地址 | cpp/board/config.toml |
| 抓取/接近这类流程与调参 | 动作脚本 cpp/board/demo/*.lua;参数改卡片配置(界面上建的,存在 demo/configs/) |
| 开机、热点、自启 | cpp/board/init.sh、init_ap_web.sh、S97akaap |
| 某个接口的行为 | cpp/capp/src/routes/ 里对应域的文件 |
| 硬件时序、串口协议 | cpp/csrc/src/ 里对应模块 |
| 前端页面 | frontend/src/,改完 npm run build 再打包 |
别在板子上直接改文件:OTA 升级是整目录换包,只有
config.toml、speed_config.json、arm_angles.json、cert.pem、key.pem这几个文件,以及demo/configs/(板上优先)和demo/models/(取并集)会被保留,其余一律按包里的结算。想改什么就改仓库, 走make -C cpp ota重新打包部署。
调试方法
串口连接
# 安装 minicom
sudo apt install minicom
# 连接串口
minicom -D /dev/ttyUSB0 -b 115200
网络连接
USB RNDIS 网口
# 查看网口
ip a show
# 如果主机是 10.245.118.100,则开发板是 10.245.118.1
ssh root@10.245.118.1
WiFi SSH
ssh root@<机器人IP>
日志查看
capp 不写日志文件,输出直接走 stdout:开机由 /etc/init.d/S99webstart 拉起时打在
串口控制台上;手工启动想留一份就自己重定向:
AKA_HOME=$HOME/AKA-00 $HOME/AKA-00/init.sh > /root/aka.log 2>&1 &
tail -f /root/aka.log
日志详细程度由 config.toml 的 [logging] level 控制(默认 info)。
测试硬件
板子自带一套板测工具(部署目录的 tools/,make 打包时一并放进去):
./tools/tt_pid_test /dev/ttyS1 40 3000 # 底盘:直发速度看车动不动(绕过 Web 层)
./tools/cam_probe # 摄像头:验证能否出帧
./tools/screen_test # 板载屏:写屏带宽/颜色(仅带屏版)
这些工具会独占对应设备,跑之前先停 capp(
$AKA_HOME/stop.sh)。
网络诊断
# 检测网络连通性
ping www.baidu.com
# 检测外网访问
curl www.baidu.com
HTTPS 证书
不用手工生成:init.sh 启动前会调 https_init.sh,缺 cert.pem/key.pem 时自动
签一张(EC prime256v1,10 年),已经有的不会被覆盖。要换成 CA 签发的证书,把两个文件
放到 $AKA_HOME/ 即可;端口在 config.toml 的 [web] https_port(默认 443,
设 0 关闭 HTTPS)。手工重签的命令见初始化配置。
自定义镜像的制作
1. 基础镜像的选择
我们需要选择一个基础镜像,在它的基础上进行修改。从项目 releases 页面下载最新的系统镜像文件(.img 格式)。
镜像文件地址
2. 镜像的挂载
我们需要挂载镜像文件到本地,以便进行修改。具体步骤如下:
sudo losetup -fP /path/to/image.img --show # 把镜像对应到loop设备,返回loop设备的路径
# 假设返回的loop设备路径为/dev/loop0
sudo mount /dev/loop0p2 /mnt # 挂载loop设备的p2分区到/mnt目录
3. 镜像的修改
在挂载后,我们可以对镜像文件进行修改。具体步骤如下:
cd /mnt/root/ # 进入挂载目录
rm -rf AKA-00 # 删除旧版本
cp -r /path/to/AKA-00 . # 复制新的部署目录(= cpp/dist/AKA-00/)
用
make -C cpp package生成cpp/dist/AKA-00/,它就是板上$AKA_HOME(镜像里即/root/AKA-00/)该有的样子:cpp/board/的实体文件 + 三样构建产物 (aka-capp、static/、tools/)。前端有更新时先cd frontend && npm run build, 打包会自动把仓库根的static/收进去。
4. 镜像的卸载
在修改完成后,我们可以卸载镜像文件。
sudo umount /mnt # 卸载loop设备
sudo losetup -d /dev/loop0 # 卸载loop设备
5. 镜像的压缩
在修改完成后,我们可以压缩镜像文件,以便于传输。
xz -zk /path/to/image.img
AKA-00教育机器人测试文档 - 测试概述与策略
1. 测试概述
1.1 文档目的
本文档旨在为教育机器人(AKA-00教育机器人)提供从实验室到产品化过程中的标准测试流程指导。测试覆盖硬件功能、极限工况等多个方面,以验证机器人的功能完整性、稳定性和鲁棒性,确保产品交付质量。
1.2 测试范围
| 测试领域 | 测试内容 |
|---|---|
| 硬件功能测试 | 电机测试、电机控制电路模块测试、摄像头测试、舵机测试、舵机控制电路模块测试、电池测试、降压模块测试、UART通信测试、NPU功能测试(YOLO网球识别测试)、WiFi功能测试(SSH连接测试) |
| 极限工况测试 | 电机连续运行温度测试、舵机连续运行温度测试、高温/低温环境测试、阳光直射测试、长时间连续运行测试(40分钟) |
| 交互测试 | 手机APP遥控 |
| 系统集成测试 | 完整捡球流程、多球场景、自主模式切换 |
1.3 教育机器人基本信息
| 部件 | 描述 |
|---|---|
| 底盘 | 两轮驱动的四轮小车,电机驱动 |
| 机械臂 | 三关节结构,3个舵机分别控制2个关节和1个夹爪 |
| 摄像头 | USB接口,用于图像采集和物体识别 |
| 电池 | 7.5V电池,分出一路降压(7.5V给舵机,5V给控制板和电机) |
| 控制板 | sg2002芯片,Linux/StarryOS系统 |
| 通信接口 | USB控制摄像头,两路UART分别控制电机和舵机 |
1.4 测试环境
| 环境类型 | 描述 |
|---|---|
| 室内环境 | 实验室、体育馆等平坦地面,常温环境(25±5℃) |
| 室外环境 | 网球场、操场等实际使用场景 |
| 高温环境 | 40℃环境箱 |
| 低温环境 | 0℃环境箱 |
| 阳光直射环境 | 室外阳光直射条件 |
1.5 测试设备清单
| 设备名称 | 规格/型号 | 用途 |
|---|---|---|
| 教育机器人 | aka01b | 被测设备 |
| sg2002开发板 | Linux/StarryOS系统 | 主控单元 |
| USB摄像头 | - | 图像采集 |
| 机械臂(三关节) | 3舵机控制 | 网球拾取 |
| 手机APP | - | 遥控控制 |
| 网球 | 标准网球 | 测试目标 |
| 红桶/黑色网球袋 | - | 目标容器 |
| 万用表 | - | 电压测量 |
| 红外测温仪/热电偶 | - | 温度测量 |
| 计时器 | - | 时间记录 |
| 串口调试工具 | - | UART测试 |
| SSH客户端 | - | 远程连接测试 |
| 环境箱 | - | 高低温测试 |
1.6 参考文档
- 《0_测试需求.md》
2. 测试策略
2.1 测试方法
- 黑盒测试:验证功能是否符合预期,不关注内部实现
- 白盒测试:检查代码逻辑和算法正确性
- 集成测试:验证各模块协同工作能力
- 系统测试:验证整个系统在真实环境下的表现
- 极限测试:验证极端工况下的系统稳定性
2.2 测试分类
| 测试级别 | 测试内容 | 测试方法 |
|---|---|---|
| 单元测试 | 单个功能模块(电机、舵机、摄像头等) | 白盒测试 |
| 集成测试 | 模块间协作(UART通信等) | 黑盒测试 |
| 系统测试 | 完整系统功能(捡球流程等) | 黑盒测试 |
| 极限工况测试 | 极端环境条件下的稳定性 | 黑盒测试 |
| 验收测试 | 验证是否满足需求 | 用户验收 |
2.3 测试流程
- 测试准备:搭建测试环境、准备测试设备、明确测试参数(如安全温度上限)
- 单元测试执行:按照测试用例执行各硬件模块测试
- 集成测试执行:验证模块间协同工作能力
- 极限工况测试:在极端环境下验证系统稳定性(包括组装过程中的电机和舵机连续运行温度测试)
- 系统测试执行:验证完整功能流程
- 缺陷记录:记录测试中发现的问题
- 缺陷修复:开发人员修复问题
- 回归测试:验证修复效果
- 测试报告:汇总测试结果
2.4 测试优先级
| 优先级 | 测试类别 | 说明 |
|---|---|---|
| P0 | 硬件功能测试 | 确保核心硬件模块正常工作 |
| P1 | 极限工况测试 | 验证极端环境下的稳定性(如40分钟连续运行温度测试) |
| P2 | 交互测试 | 确保用户交互功能正常 |
| P3 | 系统集成测试 | 验证完整系统功能 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
AKA-00教育机器人测试文档 - 硬件测试
3. 硬件测试
3.1 电机测试与电机控制电路测试
3.1.1 测试目标
验证电机驱动功能和电机控制电路模块的工作状态,包括电机转动和控制芯片状态。
3.1.2 测试环境
- 底盘垫高,车轮离地
- 计时器
3.1.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-001 | 电机前进驱动 | 1. 启动机器人 2. 发送前进指令 3. 观察电机转动 | 电机正常转动 | ||
| HW-002 | 电机后退驱动 | 1. 启动机器人 2. 发送后退指令 3. 观察电机转动 | 电机正常转动 | ||
| HW-003 | 电机左转驱动 | 1. 启动机器人 2. 发送左转指令 3. 观察电机转动 | 电机正常转动 | ||
| HW-004 | 电机右转驱动 | 1. 启动机器人 2. 发送右转指令 3. 观察电机转动 | 电机正常转动 | ||
| HW-006 | 运动停止 | 1. 机器人运动中 2. 发送停止指令 | 电机立即停止转动 | ||
| HW-008 | 左电机独立控制 | 1. 单独驱动左电机 2. 观察右电机状态 3. 记录左电机转动情况 | 仅左电机转动,右电机保持静止 | ||
| HW-009 | 右电机独立控制 | 1. 单独驱动右电机 2. 观察左电机状态 3. 记录右电机转动情况 | 仅右电机转动,左电机保持静止 |
3.2 摄像头测试与控制板USB测试
3.2.1 测试目标
验证摄像头的图像采集功能是否正常。
3.2.2 测试环境
- 室内测试环境
3.2.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-011 | 摄像头图像采集 | 1. 启动摄像头 2. 抓取一张图片 3. 检查图像是否成功保存 | 成功抓取并保存一张图片 |
3.3 舵机测试与舵机控制电路测试
3.3.1 测试目标
验证舵机驱动功能和舵机控制电路模块的工作状态,包括3个舵机的独立控制。
3.3.2 测试环境
- 平坦地面
- 计时器
3.3.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-019 | 舵机1(关节1)控制 | 1. 发送舵机1转动指令 2. 观察舵机1运动 | 舵机1按指令转动 | ||
| HW-020 | 舵机2(关节2)控制 | 1. 发送舵机2转动指令 2. 观察舵机2运动 | 舵机2按指令转动 | ||
| HW-021 | 舵机3(夹爪)控制 | 1. 发送夹爪张开/闭合指令 2. 观察夹爪状态 | 夹爪正常张开和闭合 |
3.4 电池测试
3.4.1 测试目标
验证12V电池的输出电压和续航时间是否满足要求。
3.4.2 测试环境
- 室内/室外测试场地
- 满电状态的电池
- 万用表
- 计时器
3.4.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-026 | 电池输出电压测试 | 1. 电池满电状态 2. 测量电池输出电压 | 电池输出电压为7.5V±5% | ||
| HW-027 | 连续工作续航 | 1. 充满电 2. 启动自主捡球模式 3. 记录工作时间直到电量耗尽 | 续航时间≥[预期值]小时 |
3.5 降压模块测试
3.5.1 测试目标
验证降压模块的输出电压和输出电流是否正确(7.5V给舵机供电,5V给控制板和电机供电)。
3.5.2 测试环境
- 实验室测试环境
- 万用表
- 电流表
3.5.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-030 | 控制板供电降压模块测试 | 1. 连接电池到降压模块 2. 测量控制板供电输出电压 | 输出电压为5V±5% | ||
| HW-031 | 降压模块输出电压测试 | 1. 连接电池到降压模块 2. 测量各路输出电压 3. 记录测量值 | 舵机供电输出7.5V±5%,控制板供电输出5V±5% | ||
| HW-032 | 降压模块输出电流测试 | 1. 连接电池到降压模块 2. 测量各路输出电流 3. 记录测量值 | 舵机供电电流≤[预期值]A,控制板供电电流≤[预期值]A |
3.6 UART通信测试
3.6.1 测试目标
验证控制板两路UART接口的通信功能,分别用于控制电机和舵机。
3.6.2 测试环境
- 实验室测试环境
- 串口调试工具
- 电机控制板
- 舵机控制板
3.6.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-034 | UART1(电机控制)通信测试 | 1. 连接UART1到电机控制板 2. 发送电机控制指令 3. 检查通信状态和电机响应 | UART通信正常,电机按指令动作 | ||
| HW-035 | UART2(舵机控制)通信测试 | 1. 连接UART2到舵机控制板 2. 发送舵机控制指令 3. 检查通信状态和舵机响应 | UART通信正常,舵机按指令动作 |
3.7 NPU功能测试(含YOLO网球识别)
3.7.1 测试目标
验证控制板NPU(神经网络处理器)的YOLO网球识别功能。
3.7.2 测试环境
- 室内测试环境
- 网球
3.7.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-039 | NPU网球识别 | 1. 启动YOLO识别功能 2. 放置网球 3. 检查视频帧识别结果 | 能在视频帧里正确识别网球 |
3.8 WiFi功能测试
3.8.1 测试目标
验证控制板WiFi连接功能和SSH远程连接能力。
3.8.2 测试环境
- 室内测试环境
- WiFi网络环境
- SSH客户端
3.8.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| HW-043 | WiFi连接测试 | 1. 控制板接入WiFi网络 2. 检查连接状态 | 成功连接WiFi,获取IP地址 | ||
| HW-044 | SSH远程连接测试 | 1. 通过SSH客户端连接控制板 2. 执行命令验证连接 | SSH连接成功,命令执行正常 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
AKA-00教育机器人测试文档 - 极限工况测试
4. 极限工况测试
4.1 测试目标
验证机器人在极端环境条件下的安全运行能力,包括不同温度环境、阳光直射等工况下的长时间运行稳定性。
4.2 测试环境
- 高温测试环境(40℃)
- 低温测试环境(0℃)
- 室外阳光直射环境
- 温度测量设备
- 计时器
4.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| EX-007 | 电机连续运行温度测试 | 1. 安装完整底盘后 2. 控制电机连续最大速度转动40分钟 3. 测量电机控制芯片温度 | 电机控制芯片温度≤[实测安全温度]℃ | ||
| EX-008 | 舵机连续运行温度测试 | 1. 安装完整机械臂后 2. 控制舵机依次最大角度转动一次,执行抓取释放循环40分钟 3. 测量舵机和舵机控制芯片温度 | 舵机温度≤[实测安全温度]℃ 舵机控制芯片温度≤[实测安全温度]℃ | ||
| EX-001 | 高温环境连续运行测试 | 1. 在40℃环境下启动机器人 2. 持续高强度运动40分钟(一节课时间) 3. 测量电机控制芯片温度 | 电机控制芯片温度≤[实测安全温度]℃ | ||
| EX-002 | 低温环境连续运行测试 | 1. 在0℃环境下启动机器人 2. 持续高强度运动40分钟 3. 测量各关键部件温度 | 各部件工作正常,无异常低温问题 | ||
| EX-003 | 阳光直射环境测试 | 1. 在室外阳光直射环境下 2. 持续运行40分钟 3. 测量控制板和电池温度 | 控制板温度≤65℃,电池温度≤55℃ | ||
| EX-004 | 室内环境连续运行测试 | 1. 在室内常温环境下 2. 持续高强度运动40分钟 3. 测量电机控制芯片温度 | 电机控制芯片温度≤[实测安全温度]℃ | ||
| EX-005 | 室外环境连续运行测试 | 1. 在室外自然环境下 2. 持续高强度运动40分钟 3. 测量电机控制芯片温度 | 电机控制芯片温度≤[实测安全温度]℃ | ||
| EX-006 | 湿度环境测试 | 1. 在高湿度环境(85%RH)下 2. 持续运行2小时 3. 检查是否有结露或短路 | 无结露现象,电路工作正常 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
AKA-00教育机器人测试文档 - 交互测试
5. 交互测试
5.1 手机APP遥控测试
5.1.1 测试目标
验证手机APP遥控功能是否正常。
5.1.2 测试环境
- 手机安装控制APP
- 机器人处于遥控模式
5.1.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| INT-001 | APP连接 | 1. 打开手机APP 2. 搜索机器人 3. 点击连接 | 成功连接机器人 | ||
| INT-002 | APP断开连接 | 1. APP已连接机器人 2. 点击断开连接 | 成功断开连接 | ||
| INT-003 | 遥控前进 | 1. 连接APP 2. 点击前进按钮 3. 观察机器人运动 | 机器人前进 | ||
| INT-004 | 遥控后退 | 1. 连接APP 2. 点击后退按钮 3. 观察机器人运动 | 机器人后退 | ||
| INT-005 | 遥控左转 | 1. 连接APP 2. 点击左转按钮 3. 观察机器人运动 | 机器人左转 | ||
| INT-006 | 遥控右转 | 1. 连接APP 2. 点击右转按钮 3. 观察机器人运动 | 机器人右转 | ||
| INT-007 | 遥控停止 | 1. 机器人运动中 2. 点击停止按钮 3. 观察机器人状态 | 机器人停止 | ||
| INT-008 | 模式切换 | 1. 遥控模式下 2. 点击切换到自主模式 3. 观察机器人状态 | 成功切换到自主捡球模式 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
AKA-00教育机器人测试文档 - 系统集成测试
6. 系统集成测试
6.1 完整捡球流程测试
6.1.1 测试目标
验证机器人完整捡球流程的功能完整性,包括自主捡球模式的各项功能。
6.1.2 测试环境
- 室内/室外测试场地
- 放置网球和目标容器
6.1.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| SYS-001 | 启动自主模式 | 1. 启动机器人 2. 切换到自主捡球模式 3. 观察机器人行为 | 机器人开始搜索网球 | ||
| SYS-002 | 多球完整流程 | 1. 放置5个网球和1个目标容器 2. 启动自主捡球模式 3. 观察完整流程 | 机器人依次完成所有网球的捡取 | ||
| SYS-003 | 模式切换回遥控 | 1. 自主模式下 2. 切换到遥控模式 3. 观察机器人状态 | 成功切换到遥控模式 |
6.2 性能指标测试
6.2.1 测试目标
验证机器人的性能指标是否满足要求。
6.2.2 测试环境
- 室内/室外测试场地
6.2.3 测试用例
| 用例编号 | 测试场景 | 测试步骤 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|---|---|
| SYS-004 | 捡球效率测试 | 1. 放置10个网球 2. 启动自主模式 3. 记录捡球时间 | 捡球效率≥[预期值]个/分钟 | ||
| SYS-005 | 移动速度测试 | 1. 测量10米距离 2. 机器人从起点到终点 3. 记录时间 | 移动速度≥[预期值]米/秒 | ||
| SYS-006 | 续航时间测试 | 1. 充满电 2. 启动自主模式 3. 记录工作时间 | 续航时间≥[预期值]小时 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
AKA-00教育机器人测试文档 - 缺陷管理与测试报告
7. 缺陷管理
7.1 缺陷分类
| 级别 | 描述 | 示例 |
|---|---|---|
| 严重 | 导致系统崩溃或无法完成核心功能 | 机器人无法启动、无法识别网球 |
| 高 | 影响主要功能正常使用 | 捡球成功率低于50%、通信频繁失败 |
| 中 | 影响次要功能或用户体验 | 偶尔识别错误、APP响应延迟 |
| 低 | 轻微问题,不影响功能使用 | 界面显示问题、日志格式问题 |
7.2 缺陷记录模板
| 字段 | 描述 |
|---|---|
| 缺陷编号 | 唯一标识,如 DEF-001 |
| 缺陷标题 | 简要描述问题 |
| 缺陷级别 | 严重/高/中/低 |
| 测试用例 | 关联的测试用例编号 |
| 测试环境 | 测试时的环境信息 |
| 复现步骤 | 详细描述如何复现问题 |
| 预期结果 | 期望的正常行为 |
| 实际结果 | 实际观察到的行为 |
| 发现日期 | 问题发现日期 |
| 发现人 | 测试人员姓名 |
| 状态 | 待修复/修复中/已修复/已验证 |
| 修复人 | 开发人员姓名 |
| 修复日期 | 问题修复日期 |
8. 测试报告模板
8.1 测试摘要
| 项目 | 内容 |
|---|---|
| 测试名称 | AKA-00教育机器人测试 |
| 测试日期 | YYYY-MM-DD |
| 测试环境 | 室内/室外 |
| 测试人员 | 姓名 |
| 测试版本 | 机器人软件版本 |
8.2 测试结果统计
| 测试类别 | 用例总数 | 通过数 | 失败数 | 通过率 |
|---|---|---|---|---|
| 硬件测试 | ||||
| 极限工况测试 | ||||
| 交互测试 | ||||
| 系统集成测试 | ||||
| 总计 |
8.3 缺陷统计
| 缺陷级别 | 数量 |
|---|---|
| 严重 | |
| 高 | |
| 中 | |
| 低 | |
| 总计 |
8.4 测试结论
- 测试是否通过:□ 通过 □ 未通过
- 主要问题:
- 改进建议:
- 测试人员签名:
附录:测试用例状态说明
| 状态 | 说明 |
|---|---|
| □ 未执行 | 测试用例尚未执行 |
| □ 通过 | 测试用例执行通过 |
| □ 失败 | 测试用例执行失败 |
| □ 阻塞 | 测试用例因其他问题无法执行 |
文档版本:V1.0
创建日期:2026-06-11
文档状态:待审核
常见问题
连接问题
Q: 机器人热点无法连接?
- 确保机器人已通电且指示灯亮起
- 等待 60 秒让网络模块完全启动
- 确认电脑/手机 WiFi 已开启
Q: 无法 SSH 登录?
- 确认电脑和机器人在同一 WiFi 网络
- 检查 IP 地址是否正确
- 尝试使用串口连接调试
Q: WiFi 配置页面打不开?
浏览器访问 192.168.4.1,确认已连接机器人热点。
运行问题
Q: 启动失败,提示缺少依赖?
板上没有"依赖"可缺 —— 设备端是静态二进制,不依赖 Python 或任何库。启动失败看
init.sh 的输出(串口控制台),常见就两种:
aka-capp not found or not executable—— 包没解全或可执行位丢了,重跑一次./aka-00-server --update;- 端口被占(日志里
bind failed)—— 多半起了两个实例,ps看一眼init.sh与aka-capp。
如果是开发机上编译失败,那才是环境问题(缺 riscv64-musl 工具链): 见开发环境与部署。
Q: 机械臂不响应?
- 确认舵机控制板接在主控 UART2(
/dev/ttyS2,115200) - 确认舵机供电正常 —— 舵机是独立供电,别只靠主控
ls -l /dev/ttyS2看设备在不在,config.toml的[arm] port/backend是否对得上- 直接读一下角度:
curl http://<ip>/api/arm/angles
Q: 电机不转动?
底盘是 ESP32-C3 底盘板闭环控制的,主控并不直连电机 PWM,所以按链路查:
- 底盘板供电、串口接线(主控 UART1
/dev/ttyS1,115200) - 看
GET /api/motor/status的motor.connected/motor.state:是reconnecting就是串口没通 —— 服务会按退避自动重连,不用重启 - 用
./tools/tt_pid_test /dev/ttyS1 40 3000绕过 Web 层直测(先停 capp) - 配置为空也会导致行为不对:
config.toml丢了/0 字节时所有配置静默取默认值, 接口看着正常但车不动 ——init.sh启动时会对空配置告警,日志里搜config.toml
Q: 摄像头无法识别?
- 检查 USB 连接
- 确认设备文件
/dev/video0存在 - 测试摄像头:
ls -l /dev/video0
其他
Q: 如何查看机器人 IP?
GET /api/system/ip(热点模式下网关固定 192.168.4.1,接路由器后的 IP 也可以看
GET /api/wifi/status)。
Q: 如何开启 HTTPS?
不用手工做:cpp/board/https_init.sh 会在 capp 启动前自动生成自签证书
(EC prime256v1,10 年有效期;openssl 没编 EC 时退回 RSA-2048),缺一才生成 ——
已有的证书不会被覆盖。生成位置固定 $AKA_HOME/cert.pem / key.pem。
要手工换证书(比如换成 CA 签发的),把这两个文件放到 $AKA_HOME/ 即可,
端口在 config.toml 的 [web] https_port(默认 443)。
Q: 如何设置开机自启?
参考 机器人连接 中的开机自启配置。