自主避障 — 测试场、bang-bang 与比例控制¶
这一章讲什么: 给只会听命令的机器人装上第一个「自主行为」:看见墙,绕开墙。 顺序是:先造一间可控的测试场(自治行为的实验室伦理), 再写两代避障算法——固定距离(bang-bang):过线就转向,简单粗暴; 比例控制:离得越近转得越猛,动作平滑。 中间配了「行为数据可视化」,让你能一边跑一边看机器人「想」了什么。 这是机器人学最经典的第一课:从开关控制走向反馈控制。
1. 先看现象:为什么先建场子,再写代码¶
自主行为意味着你只按开始和停止,它自己动——这正是危险所在: 被测的机器人可能冲向楼梯、人或者硬墙1。书里的对策不是更小心,而是控制环境:
「控制环境让机器人测试更安全也更容易。我们可以刻意利用测试空间, 看行为在不同场景里怎么表现。」1
同时还有一个方法论理由:限缩变量——地板是木头还是地毯都会改变行驶; 先把环境固定住,第一批行为才调得动2。书里甚至给了一条传感器的老实话: 强光经镜子/窗户反射会骗过距离传感器,软、毛、透明、镜面的东西也一样1。
本章主线例子:机器人在泡沫板围成的场地里,先是「见墙就甩头」的莽汉, 后是「贴着墙丝滑转弯」的老司机——两代行为的差别,就是控制的差别。
2. 顶层全景:一个 Sense-Think-Act 循环,两代 Think¶
场地 C 形泡沫板测试场(墙高 ≥100 mm,白墙/黑墙)
传感 64 路距离 → 砍掉脚下 3 行 → 分左右两区 → 各取最小值
思考 v1 任一侧 < 300 mm?→ 对侧电机反转(bang-bang)
思考 v2 最近距离越小 → 转弯量 = 40 × (1/最近距离)(比例控制)
行动 发 motors/wheels;新数据回来,循环
旁观 fixed_distance_avoider/plot topic → 手机上的时间序列曲线
图说:v1 和 v2 的 Sense、Act 完全一样,只有 Think 不同——这正是分层的回报。
3. 核心原理之一:测试场工程——泡沫板的尺寸与接法¶
场地设计要回答四个问题:测什么(决定布局)、机器人多大(决定特征尺寸)、 多快(决定场地大小)、件与件怎么连接(决定结构)3。尺寸上书里给个诀窍: 内尺寸取公制整数倍(1.5 m、0.75 m)——好算、好建,而且后文建模时要把这些数写进代码4。 三种布局:A 大方形(开阔行为)、B 窄长道(长墙避障)、C 不规则形(长道 + 大空间 + 斜墙, 无旋转对称)——选 C,因为后面第 12 章的定位实验需要一个「不对称才能认出来」的场地4。
墙有两个硬指标:至少 100 mm 高(高过传感器,让它一定看得见)、纯色 (白或黑,传感器和后面前视觉都省事)5。材料推荐 10 mm 厚泡沫板(轻、好切; 纸板不耐磨,不推荐)6。
接法三种,照原书的图解接住:
| 接头 | 做法 | 用在哪 |
|---|---|---|
| A 插接 | 两板各开槽(槽宽 = 板厚,槽深 = 半高),互相咬合 | 外角;可拆,板子能重排成别的布局7 |
| B 胶带铰链 | 内侧贴宽胶带 | 内角;会晃,必须夹在 A 接头之间7 |
| C 直拼 | 板子不够长,端对端单面胶带 | 直墙加长;折起加强再翻回7 |
切割纪律:丁字尺定位、金属直尺当导向、美工刀反复轻划而不是一刀狠切 (撕纸 = 压力太大或该换新刀片);切槽要比切墙更细心8。安全只有一条,书里加粗: 任何时候,身体的任何部位不得在刀刃行进路线上8。
4. 核心原理之二:数据准备——看不见地板的传感器¶
上章的可视化页面暴露了一个问题:把机器人放地上,网格图最下面几行(0、1、2 行)是地板—— 不处理的话,机器人会永远试图「躲开地板」9。
准备管线照原书接住(这是本章所有思考的前提):
sensor_data = np.array(json.loads(message.payload))
grid = np.fliplr(sensor_data.reshape((8, 8))) # 折成 8×8,左右镜像(回忆第 8 章的镜像效应)
top_lines = grid[4:, :] # 只留上面 4 行,砍掉脚下 3 行
left_sensors = top_lines[:, :2] # 左区:前 2 列
right_sensors = top_lines[:, -2:] # 右区:后 2 列
left_distance = int(np.min(left_sensors)) # 每区取「最近一点」代表该区
right_distance = int(np.min(right_sensors))
三个决策值得点名:① 砍行——近地矮障碍会被牺牲掉(书里承认,这正是挑战题的引子); ② 取 min 不取均值——避障关心的是「最近的威胁」,均值会把危险稀释掉; ③ 分区成左右两块——「哪边更近」就是「往哪边转」的全部依据10。
5. 核心原理之三:v1 固定距离避障——bang-bang 控制¶
bang-bang 控制(阈值开关控制)是控制论里的正式名字:输出只有两个状态, 像砰砰开关一样跳变11。书里的实现是 sense-think-act 三段式的第一个完整样本:
self.speed = 0.8
self.threshold = 300 # 30 cm:撞上前留出的反应距离
# Think
left_motor_speed = self.speed
right_motor_speed = self.speed
if left_distance < self.threshold:
right_motor_speed = -self.speed # 左侧太近 → 右轮反转 → 向右甩头
elif right_distance < self.threshold:
left_motor_speed = -self.speed
# Act
publish_json(client, "motors/wheels", [left_motor_speed, right_motor_speed])
行为语义:默认直行;左边太近就把右轮打到反转,机器人立刻向右转离开威胁;
右边对称12。配上「行为自己遥控传感器」(start 时发布 start_ranging)、
all/stop 订阅、systemd 不自启(由网页 launcher 启停)——全套服务模式照旧13。
跑起来的样子:能躲墙,但动作是「冲—急转—冲—急转」的锯齿, 蹭墙预警:转太晚擦墙 → 调低速度或调高阈值14。书里还有一条物理提醒: 别撞硬墙——冲击会震坏传感器;测试场就是干这个的14。
行为可视化:给机器人的「心思」装窃听器¶
调参之前先装仪表:行为在专门的 debug topic
(fixed_distance_avoider/plot,内容是左右距离 + 时间戳(记录那一刻时刻的数字)发布内部状态,
网页订阅它画时间序列折线图15。这是书里反复用的「snoop(窃听)」模式——
复杂行为必须能旁观,图是调参的眼睛。
Chart.js 画时间轴要一个小适配:JavaScript 的时间按毫秒、Python 的 time.time()
按秒,推数据前 time * 1000**16。页面用 switch(topic) 同时伺候两张图
(距离网格图 + 行为曲线图)——同一套 MQTT 总线,数据想加就加。
6. 核心原理之四:v2 比例控制——把开关换成旋钮¶
bang-bang 的病:它只有「转 / 不转」两个档,动作必然生硬。 比例控制(proportional control)是机器人学的地基方法,书里原话: 「控制量不再到了阈值才跳变,而是随传感器读数连续变化——离得越近,转得越猛。」17
核心公式照原书接住:
距离在分母上:1 米远时 1/1000 很小,几乎不转;70 mm 处倒数曲线冲得很陡, curve 达到 1 = 原地打转18。原书图里那条曲线(70 mm–3000 mm)有两个设计细节: ① 图上 1 米线处车已以 0.7 的弯速开始转——远不等于不防; ② 曲线可以算出大于 1 的值,但应用到速度时被 np.clip 限在 ±1——公式可以暴冲,输出必须戴枷18。
代码(照原书的 Think 段):
self.speed = 0.6 # 基础速度降到六成——把余量让给转弯
self.curve_proportion = 40 # 增益:这个数决定「多敏感」
nearest_distance = min(self.left_distance, self.right_distance)
inverse = 1 / max(nearest_distance, 1) # max(…,1) 防除零
self.curve = self.curve_proportion * inverse
furthest_speed = np.clip(self.speed - self.curve, -1, 1) # 远侧减速
nearest_speed = np.clip(self.speed + self.curve, -1, 1) # 近侧加速(可过零反转)
if self.left_distance < self.right_distance:
left_motor_speed, right_motor_speed = nearest_speed, furthest_speed
else:
left_motor_speed, right_motor_speed = furthest_speed, nearest_speed
对照流程图有个诚实的出入:图里没有 if——比例部分确实无分支, 但「哪个电机是近侧」仍需一次判断,书里明说这点和流程图不完全一致19。 40 这个增益是本书给的第一颗「旋钮」:它决定 70 mm 这个「满转点」在哪; 数字大 = 敏感、远就开始躲;数字小 = 迟钝、贴脸才转18。
回到主线例子:窄道里的两代行为¶
场景:泡沫板搭一条窄道
v1 bang-bang:进道 → 一侧触阈值 → 急转 → 另一侧触阈值 → 急转回来……
在道里「弹」着走,像乒乓球
v2 比例: 进道 → 一侧略近 → 轻微转向 → 过一点头 → 回一点……
在道里「S 形游」着走,平滑但开始振荡
图说:振荡不是 bug,是比例控制的固有性格——「经典的比例超调行为」。
书里点破:v2 会转弯、会抖、会卡在角落;窄道里它开始振荡—— 「这是经典的比例超调(overshoot)行为」,治它要加别的项(积分、微分——后文第 11、13 章)。 哪个更好?看场景:覆盖大片房间的扫地式巡逻,bang-bang 够用; 贴墙巡线式的走廊巡航,比例式更贴20。
7. 作者的判断与证据¶
- 判断: 先场子后代码。这不是洁癖,是两个工程事实的合流: 机器人测试有物理风险(撞坏传感器)14;环境变量(地板材质、光照)和行为耦合, 不控变量就没法归因2。
- 判断: 传感器数据「取 min 分区」是面向任务的数据准备—— 避障只关心最近的威胁,这一刀切掉了 62/64 的数据量,换来决策的简单10。
- 事实层: 70 mm 满转点、40 的增益、0.6 的基础速度、300 mm 阈值—— 都是作者机器上的标定值,书里明说你的会不同18。
- 预告: Further reading 里指了两条路——更好的实时避障论文,以及 PID; 「读比例控制多半会撞上 PID 这个词,后面章节讲」21。
8. 边界与局限¶
- 看不见矮障碍和近地悬崖。 脚下三行被砍掉了——低障碍、楼梯沿全在盲区; 挑战题里让你「用脚下几行做防跌落」,但那是险活,书里限定只在泡沫场里试22。
- 振荡没有治。 本章止于「观察到超调」;微分项(第 15 章)和 PI 调参(第 11 章)才处理它。
- 没有对头行为。 正前方正中的墙,左右读数相等时 v2 的「近侧」判断会抖—— 本章的对策是物理的:正前方有墙就先后退再说?没有,书里机器人就是会卡角, 作者如实承认「might get stuck in corners」20。
- 增益 40 没有调参方法。 给数、试、看——本章的调参还是手艺,不是科学; 第 11 章才开始用图和公式驯它。
9. 可带走的¶
- 自主行为之前先控制环境:固定场地、固定光照、固定地板——先限变量,再谈调参2。
- 测试场规格:墙 ≥100 mm 高、纯色、泡沫板 10 mm;内尺寸用公制倍数54。
- 数据准备三决策:砍脚下行、每区取 min、分左右两区10。
- bang-bang:阈值 + 两侧开关动作;能躲、生硬、擦墙就调「速度↓或阈值↑」1214。
- 比例控制公式:转弯量 = 增益 × (1/最近距离);戴枷靠 np.clip,防除零靠 max(…,1)1819。
- 增益是旋钮:大 = 敏感,小 = 迟钝;它移动「满转点」的位置18。
- 振荡 = 比例控制的固有性格(超调),不是代码写错;治它要加项,不是调低增益了事20。
- 行为要留窃听口:专用 plot topic + 时间序列图;JS 毫秒、Python 秒,推图前 ×10001516。
- 选择算法看场景:大范围巡逻 bang-bang 够,贴墙巡航比例好20。
10. 原文地图¶
| 主题 | 原书位置 |
|---|---|
| 为什么控制环境 | text/82-fm-making-a-test-area-for-your-robot.txt:13(搜「Controlling the environment」) · text/82-fm-making-a-test-area-for-your-robot.txt:25(搜「limiting the variables」) |
| 场地设计与布局 C | text/82-fm-making-a-test-area-for-your-robot.txt:53(搜「simple metric multiples」) · text/82-fm-making-a-test-area-for-your-robot.txt:68(搜「no rotational symmetry」) · text/82-fm-making-a-test-area-for-your-robot.txt:76(搜「at least 100 mm high」) |
| 材料与接头 | text/82-fm-making-a-test-area-for-your-robot.txt:86(搜「foamboard」) · text/82-fm-making-a-test-area-for-your-robot.txt:117(搜「slot」) |
| 切割纪律 | text/82-fm-making-a-test-area-for-your-robot.txt:165(搜「in the path」) |
| 地板行问题 | text/83-fm-building-a-wall-avoiding-behavior.txt:76(搜「avoid the floor」) |
| v1 bang-bang | text/83-fm-building-a-wall-avoiding-behavior.txt:5(搜「bang-bang」) · text/83-fm-building-a-wall-avoiding-behavior.txt:100(搜「300 mm」) · text/83-fm-building-a-wall-avoiding-behavior.txt:106(搜「np.fliplr」) |
| 蹭墙与震传感器警告 | text/83-fm-building-a-wall-avoiding-behavior.txt:194(搜「jolt the sensor」) |
| 行为可视化与毫秒换算 | text/84-fm-visualizing-wall-avoider-data.txt:28(搜「snooping on this behavior」) · text/84-fm-visualizing-wall-avoider-data.txt:124(搜「multiply the time by 1,000」) |
| 比例控制公式与曲线 | text/85-fm-using-proportional-control.txt:16(搜「foundational in robotics」) · text/85-fm-using-proportional-control.txt:27(搜「1 / nearest_distance」) |
| 代码实现 | text/85-fm-using-proportional-control.txt:111(搜「prevented it from dividing by 0」) · text/85-fm-using-proportional-control.txt:114(搜「np.clip」) |
| 超调与两代行为对比 | text/86-fm-summary.txt:15(搜「classic proportional overshoot behavior」) · text/86-fm-summary.txt:16(搜「bounce instead」) |
-
出处:「Making a test area for your robot」第 13 段(
text/82-fm-making-a-test-area-for-your-robot.txt:13,搜「Controlling the environment」)。原文:自主行为下你只按开始停止;被测机器人可能冲向楼梯、人或硬墙;控制环境让测试更安全更容易。Note 警告:镜面/窗户反射的强光、软/毛/透明/镜面物体会迷惑距离传感器。 ↩↩↩ -
出处:「Making a test area for your robot」第 25 段(
text/82-fm-making-a-test-area-for-your-robot.txt:25,搜「limiting the variables」)。原文:木地板和地毯上行驶不同,地板是要控制的变量;Note:避障代码在任何场地都能跑,但跑第一个行为时限缩变量更容易。无平滑地面建议铺泡沫板。 ↩↩↩ -
出处:「Making a test area for your robot」第 42 段(
text/82-fm-making-a-test-area-for-your-robot.txt:42,搜「several factors」)。原文四问:测什么、机器人多大、多快、件怎么连接;还能否共享件减少制作和存储。 ↩ -
出处:「Making a test area for your robot」第 53 段(
text/82-fm-making-a-test-area-for-your-robot.txt:53,搜「simple metric multiples」)与第 60 段(搜「no rotational symmetry」)。原文:内尺寸取 1.5 m/0.75 m 这类公制倍数,有些测试要把这些尺寸给机器人;布局 C 无旋转对称,后文定位软件能用它做更有趣的实验;本书用 C 或类似形状。 ↩↩↩ -
出处:「Making a test area for your robot」第 76 段(
text/82-fm-making-a-test-area-for-your-robot.txt:76,搜「at least 100 mm high」)。原文:墙要能被传感器读到,至少 100 mm 高(高过距离传感器);白色或黑色等纯色对传感器和后续练习都友好。 ↩↩ -
出处:「Making a test area for your robot」第 86 段(
text/82-fm-making-a-test-area-for-your-robot.txt:86,搜「foamboard」)。原文:推荐泡沫板——轻、好切;买最小边超 500 mm、厚 10 mm 的;铅笔标记 + 丁字尺直角 + 锋利美工刀 + 金属直尺 + 切割垫;有合手的轻硬材料(胶合板、MDF)也行;不推荐纸板。 ↩ -
出处:「Making a test area for your robot」第 117 段(
text/82-fm-making-a-test-area-for-your-robot.txt:117,搜「slot」)。原文:三种接头——A:两板开槽互咬(槽宽 = 板厚、槽高 = 半高),适合外角,可拆改;A 接头还允许板子重排成布局 A/B,可平板收纳。B:内侧胶带铰链,适合内角,会晃须夹在 A 之间。C:胶带端对端直拼,适合墙加长。 ↩↩↩ -
出处:「Making a test area for your robot」第 165 段(
text/82-fm-making-a-test-area-for-your-robot.txt:165,搜「in the path」)。原文 Note(重要):任何时刻身体任何部位不得在刀刃路径上;切割区要够大、有垫、光线好、高度舒适;反复轻划而非强压;撕纸 = 太用力或刀片该换;槽的精度要求高于墙。 ↩↩ -
出处:「Building a wall-avoiding behavior」第 76 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:76,搜「avoid the floor」)。原文:距离绘图页里能看到底下几行(0、1、2 行)读到的是地板;必须剔掉这些行,否则机器人会一直试图避开地板。 ↩ -
出处:「Building a wall-avoiding behavior」第 106 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:106,搜「np.fliplr」)与同文件第 140 段(搜「np.min」)。原文:JSON → numpy 数组 → reshape(8,8) → fliplr 镜像;grid[4:,:] 只留上 4 行;左区取前 2 列、右区取后 2 列;各取 np.min 存最近距离;numpy.int64 要 int() 转换。 ↩↩↩ -
出处:「Building a wall-avoiding behavior」第 5 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:5,搜「bang-bang」)。原文:把阵列分左右两区,用 bang-bang 控制做固定距离避障——按阈值改变方向,每个电机两个状态;机器人要么转向要么直行,朝最先过近的一侧转开。 ↩ -
出处:「Building a wall-avoiding behavior」第 100 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:100,搜「300 mm」)。原文:speed=0.8、threshold=300(30 cm,留出反应距离);左侧 < 阈值 → 右电机反转;否则查右侧,对称处理;Act 段发 motors/wheels。 ↩↩ -
出处:「Building a wall-avoiding behavior」第 162 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:162,搜「start_ranging」)。原文:start() 连 MQTT、订阅 sensors/distance_mm、发布 start_ranging 让服务发数据、loop_forever;注册 systemd 不自启(False),由网页 launcher 启停。 ↩ -
出处:「Building a wall-avoiding behavior」第 194 段(
text/83-fm-building-a-wall-avoiding-behavior.txt:194,搜「jolt the sensor」)。原文排错:转太晚擦墙 → 降 speed 或升 threshold;警告:避免硬撞墙——冲击会震坏传感器,用测试场;低矮物在被砍掉的行里,检测不到。 ↩↩↩↩ -
出处:「Visualizing wall-avoider data」第 28 段(
text/84-fm-visualizing-wall-avoider-data.txt:28,搜「snooping on this behavior」)。原文:窃听行为数据;对复杂行为这样的窃听必不可少;图是调校的眼睛;debug topic fixed_distance_avoider/plot 发左右距离 + 时间码。 ↩↩ -
出处:「Visualizing wall-avoider data」第 124 段(
text/84-fm-visualizing-wall-avoider-data.txt:124,搜「multiply the time by 1,000」)。原文:update_data 推数据前 time×1000——JavaScript 用毫秒,Python 的 time.time() 给秒;图表 ~3-4 秒后出现;journalctl -u fixed_distance_avoider 查行为日志。 ↩↩ -
出处:「Using proportional control」第 16 段(
text/85-fm-using-proportional-control.txt:16,搜「foundational in robotics」)。原文:比例控制是机器人学的基础方法;不再到阈值才变,而是调整量与传感器成比例变化;障碍越近,转得越多。 ↩ -
出处:「Using proportional control」第 27 段(
text/85-fm-using-proportional-control.txt:27,搜「1 / nearest_distance」)。原文:curve = (1/nearest_distance) × proportional_factor;图覆盖 70–3000 mm;1 m 处弯速已 0.7;70 mm 处 curve=1 = 原地转;输出超 1 会被限;对数坐标因为近距离的变化更显著;改增益会移动满转点。 ↩↩↩↩↩↩ -
出处:「Using proportional control」第 111 段(
text/85-fm-using-proportional-control.txt:111,搜「prevented it from dividing by 0」)。原文:nearest = min(左右);inverse = 1/max(nearest,1) 防除零;curve = 40 × inverse;furthest/nearest = speed ∓ curve,np.clip 限 ±1;近侧判断有一次条件分支,与无分支的流程图不完全一致;speed 降为 0.6。 ↩↩ -
出处:「Summary」第 15 段(
text/86-fm-summary.txt:15,搜「classic proportional overshoot behavior」)与第 16 段(搜「bounce instead」)。原文:窄道里比例机会开始振荡——经典比例超调;后续学别的项来压它;固定避障会在道里「弹」,比例式会试着巡着墙走;会抖、会卡角;哪个好取决于场景(大片覆盖 vs 贴墙跟随)。 ↩↩↩↩ -
出处:「Challenges」第 16 段(
text/87-fm-challenges.txt:16,搜「Simple, Real-Time Obstacle Avoidance」)。原文 Further reading:推荐 Borenstein 的 Simple Real-Time Obstacle Avoidance 论文深入;比例避障会引向 PID 算法,后文覆盖。 ↩ -
出处:「Challenges」第 10 段(
text/87-fm-challenges.txt:10,搜「detect edges」)。原文挑战:被砍掉的底下几行能否用来检测边缘防跌落?只在泡沫场里非常小心地试;以及传感器偏高会漏掉贴地低物——怎么降低、数据准备怎么跟着变。 ↩