介绍性机器人编程教程
已发表: 2022-03-11让我们面对现实吧,机器人很酷。 他们也将有一天统治世界,希望到那时他们会同情他们可怜的软肉创造者(又名机器人开发者),并帮助我们建立一个充满丰富的太空乌托邦。 我当然是在开玩笑,但只是在开玩笑。
为了对此事产生一些小的影响,我去年参加了自主机器人控制理论课程,最终我构建了一个基于 Python 的机器人模拟器,让我能够在一个简单、移动、可编程的机器人上练习控制理论.
在本文中,我将展示如何使用 Python 机器人框架开发控制软件,描述我为模拟机器人开发的控制方案,说明它如何与环境交互并实现目标,并讨论一些我在此过程中遇到的机器人编程的基本挑战。
为了学习本针对初学者的机器人编程教程,您应该具备两件事的基本知识:
- 数学——我们将使用一些三角函数和向量
- Python——因为 Python 是更流行的基本机器人编程语言之一——我们将使用基本的 Python 库和函数
这里显示的代码片段只是整个模拟器的一部分,它依赖于类和接口,因此为了直接阅读代码,您可能需要一些 Python 和面向对象编程的经验。
最后,帮助您更好地遵循本教程的可选主题是了解什么是状态机以及距离传感器和编码器如何工作。
可编程机器人的挑战:感知与现实,以及控制的脆弱性
所有机器人技术的基本挑战是:永远不可能知道环境的真实状态。 机器人控制软件只能根据传感器返回的测量值来猜测现实世界的状态。 它只能尝试通过产生控制信号来改变现实世界的状态。
因此,控制设计的第一步是提出现实世界的抽象,称为模型,用它来解释我们的传感器读数并做出决策。 只要现实世界按照模型的假设行事,我们就可以做出很好的猜测并施加控制。 然而,一旦现实世界偏离了这些假设,我们将不再能够做出正确的猜测,并且将失去控制。 通常,一旦失去控制,就永远无法恢复。 (除非一些仁慈的外力恢复它。)
这是机器人编程如此困难的关键原因之一。 我们经常在实验室中看到最新研究机器人的视频,它们在灵巧、导航或团队合作方面表现出色,我们很想问:“为什么不将其用于现实世界?” 好吧,下次你看到这样的视频,看看实验室环境的控制程度如何。 在大多数情况下,只要环境条件保持在其内部模型的狭窄范围内,这些机器人才能执行这些令人印象深刻的任务。 因此,机器人技术进步的一个关键是开发更复杂、灵活和健壮的模型——并且所述进步受到可用计算资源的限制。
[旁注:哲学家和心理学家都会注意到,生物也依赖于他们自己对感官所告诉他们的内部感知的依赖。 机器人技术的许多进步来自于观察生物并观察它们对意外刺激的反应。 想想看。 你的世界内部模型是什么? 它和蚂蚁不同,鱼不同? (希望如此。)然而,就像蚂蚁和鱼一样,它可能会过度简化世界的某些现实。 当您对世界的假设不正确时,您可能会失去对事物的控制。 有时我们称之为“危险”。 就像我们的小机器人在未知的宇宙中挣扎求生一样,我们所有人也是如此。 对于机器人专家来说,这是一个强有力的洞察力。]
可编程机器人模拟器
我构建的模拟器是用 Python 编写的,并且非常巧妙地称为Sobot Rimulator 。 你可以在 GitHub 上找到 v1.0.0。 它没有太多的花里胡哨,但它的构建是为了很好地完成一件事:提供移动机器人的准确模拟,并为有抱负的机器人专家提供一个简单的框架来练习机器人软件编程。 虽然拥有一个真正的机器人总是更好,但一个好的 Python 机器人模拟器更容易获得,并且是一个很好的起点。
在现实世界的机器人中,生成控制信号的软件(“控制器”)需要以非常高的速度运行并进行复杂的计算。 这会影响选择最好使用哪种机器人编程语言:通常,C++ 用于这些场景,但在更简单的机器人应用程序中,Python 是执行速度和易于开发和测试之间的一个很好的折衷方案。
我编写的软件模拟了一个名为 Khepera 的现实研究机器人,但它可以适应一系列具有不同尺寸和传感器的移动机器人。 由于我试图对模拟器进行编程,使其尽可能与真实机器人的功能相似,因此可以通过最少的重构将控制逻辑加载到真实的 Khepera 机器人中,并且它的性能与模拟机器人相同。 实现的具体功能参考 Khepera III,但它们可以很容易地适应新的 Khepera IV。
换句话说,对模拟机器人进行编程类似于对真实机器人进行编程。 如果模拟器要用于开发和评估不同的控制软件方法,这一点至关重要。
在本教程中,我将描述Sobot Rimulator v1.0.0 附带的机器人控制软件架构,并提供来自 Python 源代码的片段(为清晰起见稍作修改)。 但是,我鼓励您深入研究源代码并搞砸。 该模拟器已被分叉并用于控制不同的移动机器人,包括 iRobot 的 Roomba2。 同样,请随时 fork 项目并改进它。
机器人的控制逻辑受限于这些 Python 类/文件:
-
models/supervisor.py
这个类负责机器人周围的模拟世界和机器人本身之间的交互。 它进化了我们的机器人状态机并触发控制器来计算所需的行为。 -
models/supervisor_state_machine.py
这个类代表机器人可以处于的不同状态,这取决于它对传感器的解释。 -
models/controllers
目录中的文件——这些类在已知环境状态下实现机器人的不同行为。 具体来说,根据状态机选择特定的控制器。
目标
机器人和人一样,在生活中需要一个目标。 我们的软件控制这个机器人的目标非常简单:它将尝试前往预定的目标点。 这通常是任何移动机器人都应具备的基本功能,从自动驾驶汽车到机器人吸尘器。 目标的坐标在机器人被激活之前被编程到控制软件中,但可以从监督机器人运动的额外 Python 应用程序中生成。 例如,想象一下它通过多个航路点行驶。
然而,更复杂的是,机器人的环境可能布满了障碍物。 机器人在前往目标的途中不得与障碍物发生碰撞。 因此,如果机器人遇到障碍物,它必须找到自己的出路,以便它可以继续向目标前进。
可编程机器人
每个机器人都有不同的能力和控制问题。 让我们熟悉一下我们的模拟可编程机器人。
首先要注意的是,在本指南中,我们的机器人将是一个自主移动机器人。 这意味着它将在空间中自由移动,并且它将在自己的控制下这样做。 这与遥控机器人(非自主)或工厂机器人手臂(不可移动)形成鲜明对比。 我们的机器人必须自己弄清楚如何实现其目标并在其环境中生存。 对于新手机器人程序员来说,这被证明是一个令人惊讶的困难挑战。
控制输入:传感器
机器人可以通过多种不同的方式来监控其环境。 这些可以包括接近传感器、光传感器、保险杠、摄像头等任何东西。 此外,机器人可能会与外部传感器进行通信,这些传感器会为它们提供它们自己无法直接观察到的信息。
我们的参考机器人配备了九个红外传感器——较新的型号有八个红外和五个超声波接近传感器——布置在各个方向的“裙子”中。 面对机器人正面的传感器比背面的传感器多,因为对于机器人来说,知道它前面的东西通常比知道它后面的东西更重要。
除了接近传感器外,机器人还有一对跟踪车轮运动的车轮记号器。 这些使您可以跟踪每个轮子的旋转次数,一个轮子的一整圈向前转为 2,765 个滴答声。 朝相反方向倒数,减少而不是增加滴答计数。 您不必担心本教程中的具体数字,因为我们将编写的软件使用以米表示的行驶距离。 稍后我将向您展示如何使用简单的 Python 函数从刻度计算它。
控制输出:移动性
一些机器人用腿四处走动。 有些像球一样滚动。 有些甚至像蛇一样滑行。
我们的机器人是差动驱动机器人,这意味着它可以在两个轮子上滚动。 当两个轮子以相同的速度转动时,机器人沿直线移动。 当轮子以不同的速度移动时,机器人就会转动。 因此,控制这个机器人的运动归结为适当地控制这两个轮子的转动速度。
API
在 Sobot Rimulator 中,机器人“计算机”和(模拟的)物理世界之间的分离由文件robot_supervisor_interface.py
体现,该文件定义了与“真实机器人”传感器和电机交互的整个 API:
-
read_proximity_sensors()
以传感器的本机格式返回一个由九个值组成的数组 read_wheel_encoders()
返回一个包含两个值的数组,指示自开始以来的总滴答数set_wheel_drive_rates( v_l, v_r )
采用两个值(以弧度/秒为单位)并将车轮的左右速度设置为这两个值
该接口在内部使用一个机器人对象,该对象提供来自传感器的数据以及移动电机或车轮的可能性。 如果你想创建一个不同的机器人,你只需要提供一个不同的 Python 机器人类,它可以被相同的接口使用,其余的代码(控制器、主管和模拟器)将开箱即用!
模拟器
由于您会在现实世界中使用真正的机器人,而不会过多关注所涉及的物理定律,因此您可以忽略机器人的模拟方式,直接跳至控制器软件的编程方式,因为它们几乎相同在现实世界和模拟之间。 但如果你好奇,我会在这里简单介绍一下。
文件world.py
是一个代表模拟世界的 Python 类,里面有机器人和障碍物。 这个类中的 step 函数通过以下方式处理我们的简单世界:
- 将物理规则应用于机器人的运动
- 考虑与障碍物的碰撞
- 为机器人传感器提供新价值
最后,它调用负责执行机器人大脑软件的机器人主管。
step 函数在循环中执行,以便robot.step_motion()
使用监督者在上一个模拟步骤中计算的轮速移动机器人。
# step the simulation through one time interval def step( self ): dt = self.dt # step all the robots for robot in self.robots: # step robot motion robot.step_motion( dt ) # apply physics interactions self.physics.apply_physics() # NOTE: The supervisors must run last to ensure they are observing the "current" world # step all of the supervisors for supervisor in self.supervisors: supervisor.step( dt ) # increment world time self.world_time += dt
apply_physics()
函数在内部更新机器人接近传感器的值,以便主管能够估计当前模拟步骤的环境。 相同的概念适用于编码器。
一个简单的模型
首先,我们的机器人将有一个非常简单的模型。 它将对世界做出许多假设。 一些重要的包括:
- 地势总是平坦的
- 障碍永远不是圆的
- 车轮从不打滑
- 没有什么可以推动机器人
- 传感器永远不会出现故障或给出错误读数
- 当他们被告知时,车轮总是转动
尽管这些假设中的大多数在类似房屋的环境中都是合理的,但可能存在圆形障碍物。 我们的避障软件有一个简单的实现,并沿着障碍物的边界绕过它们。 我们将提示读者如何通过额外的检查来改进机器人的控制框架,以避免圆形障碍物。
控制回路
我们现在将进入控制软件的核心,并解释我们想要在机器人内部编程的行为。 可以在这个框架中添加额外的行为,你应该在阅读完之后尝试自己的想法! 基于行为的机器人软件是 20 多年前提出的,它仍然是移动机器人的强大工具。 例如,2007 年 DARPA 城市挑战赛中使用了一组行为,这是首个自动驾驶汽车竞赛!
机器人是一个动态系统。 机器人的状态、传感器的读数以及控制信号的影响是不断变化的。 控制事件发生的方式涉及以下三个步骤:
- 应用控制信号。
- 测量结果。
- 生成经过计算的新控制信号,使我们更接近目标。
这些步骤一遍又一遍地重复,直到我们实现目标。 我们每秒可以执行此操作的次数越多,我们对系统的控制就越精细。 Sobot Rimulator 机器人每秒重复这些步骤 20 次 (20 Hz),但许多机器人必须每秒执行数千或数百万次才能获得足够的控制。 请记住我们之前关于针对不同机器人系统和速度要求的不同机器人编程语言的介绍。
一般来说,每次我们的机器人使用其传感器进行测量时,它都会使用这些测量值来更新其对世界状态的内部估计——例如,与目标的距离。 它将这个状态与它想要的状态的参考值进行比较(对于距离,它希望它为零),并计算期望状态和实际状态之间的误差。 一旦知道了这些信息,生成新的控制信号就可以简化为最小化误差的问题,该误差最终将使机器人朝着目标移动。
一个绝妙的技巧:简化模型
为了控制我们想要编程的机器人,我们必须向左轮发送一个信号,告诉它转多快,并向右轮发送一个单独的信号,告诉它转多快。 我们称这些信号为 v L和v R 。 然而,不断地根据v L和v R来思考是非常麻烦的。 而不是问,“我们希望左轮转多快,我们希望右轮转多快?” 更自然地问,“我们希望机器人前进多快,我们希望它转动多快,或者改变它的航向?” 我们将这些参数称为速度v和角(旋转)速度ω (读作“omega”)。 事实证明,我们可以将整个模型建立在v和ω而不是v L和v R上,并且只有在我们确定了我们希望我们的编程机器人如何移动之后,才能将这两个值数学转换为我们需要的v L和v R实际控制机器人轮子。 这被称为控制的独轮车模型。
这是在supervisor.py
中实现最终转换的 Python 代码。 请注意,如果ω为 0,则两个车轮将以相同的速度转动:
# generate and send the correct commands to the robot def _send_robot_commands( self ): # ... v_l, v_r = self._uni_to_diff( v, omega ) self.robot.set_wheel_drive_rates( v_l, v_r ) def _uni_to_diff( self, v, omega ): # v = translational velocity (m/s) # omega = angular velocity (rad/s) R = self.robot_wheel_radius L = self.robot_wheel_base_length v_l = ( (2.0 * v) - (omega*L) ) / (2.0 * R) v_r = ( (2.0 * v) + (omega*L) ) / (2.0 * R) return v_l, v_r
估计状态:机器人,了解自己
使用它的传感器,机器人必须尝试估计环境的状态以及它自己的状态。 这些估计永远不会是完美的,但它们必须相当好,因为机器人将根据这些估计做出所有决定。 仅使用其接近传感器和车轮记号器,它必须尝试猜测以下内容:
- 障碍物的方向
- 与障碍物的距离
- 机器人的位置
- 机器人的航向
前两个属性由接近传感器读数确定,并且相当简单。 API 函数read_proximity_sensors()
返回一个包含九个值的数组,每个传感器一个值。 例如,我们提前知道第七个读数对应于指向机器人右侧 75 度的传感器。
因此,如果该值显示对应于 0.1 米距离的读数,我们就知道在 0.1 米外,向左 75 度处有障碍物。 如果没有障碍物,传感器将返回其最大范围 0.2 米的读数。 因此,如果我们在传感器 7 上读取 0.2 米,我们将假设该方向实际上没有障碍物。
由于红外传感器的工作方式(测量红外反射),它们返回的数字是检测到的实际距离的非线性变换。 因此,用于确定指示距离的 Python 函数必须将这些读数转换为米。 这是在supervisor.py
中完成的,如下所示:
# update the distances indicated by the proximity sensors def _update_proximity_sensor_distances( self ): self.proximity_sensor_distances = [ 0.02-( log(readval/3960.0) )/30.0 for readval in self.robot.read_proximity_sensors() ]
同样,在这个 Python 机器人框架中,我们有一个特定的传感器模型,而在现实世界中,传感器附带的软件应该提供类似的从非线性值到米的转换功能。
确定机器人的位置和航向(在机器人编程中统称为位姿)更具挑战性。 我们的机器人使用里程计来估计其姿势。 这就是车轮计时的用武之地。通过测量自控制循环的最后一次迭代以来每个车轮转动了多少,可以很好地估计机器人的姿势是如何变化的——但前提是变化很小。
这就是在现实世界的机器人中非常频繁地迭代控制回路很重要的原因之一,其中驱动车轮的电机可能并不完美。 如果我们等了太久才测量车轮的刻度,两个车轮都可以做很多事情,而且我们无法估计我们最终到达了哪里。
鉴于我们当前的软件模拟器,我们可以负担得起以 20 Hz 的频率运行里程计计算——与控制器的频率相同。 但是让一个单独的 Python 线程运行得更快以捕捉代码的较小移动可能是一个好主意。
下面是supervisor.py
中更新机器人姿态估计的完整里程计函数。 请注意,机器人的姿态由坐标x
和y
以及航向theta
组成,航向 theta 以正 X 轴的弧度为单位测量。 正x
指向东,正y
指向北。 因此,航向为0
表示机器人直接面向东方。 机器人始终假定其初始姿态为(0, 0), 0
。
# update the estimated position of the robot using it's wheel encoder readings def _update_odometry( self ): R = self.robot_wheel_radius N = float( self.wheel_encoder_ticks_per_revolution ) # read the wheel encoder values ticks_left, ticks_right = self.robot.read_wheel_encoders() # get the difference in ticks since the last iteration d_ticks_left = ticks_left - self.prev_ticks_left d_ticks_right = ticks_right - self.prev_ticks_right # estimate the wheel movements d_left_wheel = 2*pi*R*( d_ticks_left / N ) d_right_wheel = 2*pi*R*( d_ticks_right / N ) d_center = 0.5 * ( d_left_wheel + d_right_wheel ) # calculate the new pose prev_x, prev_y, prev_theta = self.estimated_pose.scalar_unpack() new_x = prev_x + ( d_center * cos( prev_theta ) ) new_y = prev_y + ( d_center * sin( prev_theta ) ) new_theta = prev_theta + ( ( d_right_wheel - d_left_wheel ) / self.robot_wheel_base_length ) # update the pose estimate with the new values self.estimated_pose.scalar_update( new_x, new_y, new_theta ) # save the current tick count for the next iteration self.prev_ticks_left = ticks_left self.prev_ticks_right = ticks_right
现在我们的机器人能够生成对现实世界的良好估计,让我们使用这些信息来实现我们的目标。
Python 机器人编程方法:Go-to-Goal 行为
在这个编程教程中,我们的小机器人存在的最高目的是达到目标。 那么我们如何让轮子转动才能到达那里呢? 让我们从稍微简化我们的世界观开始,并假设路上没有障碍。
然后,这变成了一项简单的任务,并且可以很容易地用 Python 进行编程。 如果我们在面对目标的同时前进,我们就会到达那里。 由于我们的里程计,我们知道我们当前的坐标和航向是什么。 我们也知道目标的坐标是什么,因为它们是预先编程的。 因此,使用一点线性代数,我们可以确定从我们的位置到目标的向量,如go_to_goal_controller.py
:
# return a go-to-goal heading vector in the robot's reference frame def calculate_gtg_heading_vector( self ): # get the inverse of the robot's pose robot_inv_pos, robot_inv_theta = self.supervisor.estimated_pose().inverse().vector_unpack() # calculate the goal vector in the robot's reference frame goal = self.supervisor.goal() goal = linalg.rotate_and_translate_vector( goal, robot_inv_theta, robot_inv_pos ) return goal
请注意,我们在机器人的参考系中获取到目标的向量,而不是在世界坐标中。 如果目标位于机器人参考系中的 X 轴上,则意味着它就在机器人的正前方。 因此,这个向量与 X 轴的角度是我们的航向和我们想要的航向之间的差异。 换句话说,这是我们当前状态与我们希望当前状态之间的误差。 因此,我们想要调整我们的转弯率ω ,以便我们的航向和目标之间的角度将变为 0。我们想要最小化误差:

# calculate the error terms theta_d = atan2( self.gtg_heading_vector[1], self.gtg_heading_vector[0] ) # calculate angular velocity omega = self.kP * theta_d
上述控制器 Python 实现片段中的self.kP
是一种控制增益。 它是一个系数,它决定了我们转弯的速度与我们离目标的距离成正比。 如果我们的航向错误是0
,那么转弯率也是0
。 在go_to_goal_controller.py
文件中的真正 Python 函数中,您会看到更多类似的增益,因为我们使用了 PID 控制器而不是简单的比例系数。
现在我们有了角速度ω ,我们如何确定前进速度v ? 一个好的一般经验法则是您可能本能地知道的:如果我们不转弯,我们可以全速前进,然后我们转弯越快,我们应该越慢。 这通常有助于我们保持系统稳定并在模型范围内运行。 因此, v是ω的函数。 在go_to_goal_controller.py
中,等式是:
# calculate translational velocity # velocity is v_max when omega is 0, # drops rapidly to zero as |omega| rises v = self.supervisor.v_max() / ( abs( omega ) + 1 )**0.5
详细说明这个公式的一个建议是考虑到我们通常在接近目标时放慢速度,以便以零速度到达它。 这个公式将如何变化? 它必须以某种方式将v_max()
替换为与距离成比例的东西。 好的,我们几乎完成了一个控制循环。 剩下要做的就是将这两个独轮车模型参数转换为不同的车轮速度,并将信号发送到车轮。 这是一个机器人在 go-to-goal 控制器下的轨迹示例,没有障碍物:
正如我们所看到的,目标向量是我们进行控制计算的有效参考。 它是“我们想去的地方”的内部表示。 正如我们将看到的,去到目标和其他行为之间的唯一主要区别是,有时朝着目标去是一个坏主意,所以我们必须计算一个不同的参考向量。
Python 机器人编程方法:避免障碍行为
当那个方向有障碍物时朝着目标前进就是一个很好的例子。 让我们尝试编写一个控制法则,让机器人避开它们,而不是一头扎进我们的方式。
为了简化场景,让我们现在完全忘记目标点,将以下目标作为我们的目标:当我们面前没有障碍物时,继续前进。 当遇到障碍物时,转身远离它,直到它不再在我们面前。
因此,当我们面前没有障碍物时,我们希望我们的参考向量简单地指向前方。 然后ω将为零, v将是最大速度。 然而,一旦我们用接近传感器检测到障碍物,我们希望参考矢量指向远离障碍物的任何方向。 这将导致ω向上射击以使我们远离障碍物,并导致v下降以确保我们不会在此过程中意外撞到障碍物。
生成所需参考向量的一种巧妙方法是将我们的九个邻近读数转换为向量,并进行加权求和。 当没有检测到障碍物时,向量将对称求和,从而产生一个根据需要指向正前方的参考向量。 但是,如果右侧的传感器检测到障碍物,它将对总和贡献一个较小的矢量,结果将是一个向左移动的参考矢量。
对于具有不同传感器位置的通用机器人,可以应用相同的想法,但当传感器在机器人前后对称时,可能需要改变重量和/或额外注意,因为加权和可能会变为零.
这是在avoid_obstacles_controller.py
中执行此操作的代码:
# sensor gains (weights) self.sensor_gains = [ 1.0+( (0.4*abs(p.theta)) / pi ) for p in supervisor.proximity_sensor_placements() ] # ... # return an obstacle avoidance vector in the robot's reference frame # also returns vectors to detected obstacles in the robot's reference frame def calculate_ao_heading_vector( self ): # initialize vector obstacle_vectors = [ [ 0.0, 0.0 ] ] * len( self.proximity_sensor_placements ) ao_heading_vector = [ 0.0, 0.0 ] # get the distances indicated by the robot's sensor readings sensor_distances = self.supervisor.proximity_sensor_distances() # calculate the position of detected obstacles and find an avoidance vector robot_pos, robot_theta = self.supervisor.estimated_pose().vector_unpack() for i in range( len( sensor_distances ) ): # calculate the position of the obstacle sensor_pos, sensor_theta = self.proximity_sensor_placements[i].vector_unpack() vector = [ sensor_distances[i], 0.0 ] vector = linalg.rotate_and_translate_vector( vector, sensor_theta, sensor_pos ) obstacle_vectors[i] = vector # store the obstacle vectors in the robot's reference frame # accumulate the heading vector within the robot's reference frame ao_heading_vector = linalg.add( ao_heading_vector, linalg.scale( vector, self.sensor_gains[i] ) ) return ao_heading_vector, obstacle_vectors
使用生成的ao_heading_vector
作为机器人尝试匹配的参考,以下是仅使用避免障碍控制器在模拟中运行机器人软件的结果,完全忽略目标点。 机器人漫无目的地四处弹跳,但它从不与障碍物相撞,甚至设法在一些非常狭窄的空间内航行:
Python机器人编程方法:混合自动机(行为状态机)
到目前为止,我们已经单独描述了两种行为——达到目标和避免障碍。 两者都发挥了令人钦佩的作用,但为了在充满障碍的环境中成功实现目标,我们需要将它们结合起来。
我们将开发的解决方案在于一类具有非常酷的混合自动机名称的机器。 混合自动机编程有几种不同的行为或模式,以及监督状态机。 监督状态机在离散时间(当目标实现或环境突然变化太大时)从一种模式切换到另一种模式,而每个行为都使用传感器和轮子对环境变化做出持续反应。 该解决方案被称为混合解决方案,因为它以离散和连续的方式发展。
我们的 Python 机器人框架在文件supervisor_state_machine.py
中实现了状态机。
配备我们两个方便的行为,一个简单的逻辑不言自明:当没有检测到障碍物时,使用 go-to-goal 行为。 当检测到障碍物时,切换到避免障碍物行为,直到不再检测到障碍物。
然而事实证明,这种逻辑会产生很多问题。 当这个系统遇到障碍物时,它倾向于做的是转身离开它,然后一旦它离开它,就立即转身再次撞上它。 结果是一个无限循环的快速切换,使机器人无用。 In the worst case, the robot may switch between behaviors with every iteration of the control loop—a state known as a Zeno condition .
There are multiple solutions to this problem, and readers that are looking for deeper knowledge should check, for example, the DAMN software architecture.
What we need for our simple simulated robot is an easier solution: One more behavior specialized with the task of getting around an obstacle and reaching the other side.
Python Robot Programming Methods: Follow-Wall Behavior
Here's the idea: When we encounter an obstacle, take the two sensor readings that are closest to the obstacle and use them to estimate the surface of the obstacle. Then, simply set our reference vector to be parallel to this surface. Keep following this wall until A) the obstacle is no longer between us and the goal, and B) we are closer to the goal than we were when we started. Then we can be certain we have navigated the obstacle properly.
With our limited information, we can't say for certain whether it will be faster to go around the obstacle to the left or to the right. To make up our minds, we select the direction that will move us closer to the goal immediately. To figure out which way that is, we need to know the reference vectors of the go-to-goal behavior and the avoid-obstacle behavior, as well as both of the possible follow-wall reference vectors. Here is an illustration of how the final decision is made (in this case, the robot will choose to go left):
Determining the follow-wall reference vectors turns out to be a bit more involved than either the avoid-obstacle or go-to-goal reference vectors. Take a look at the Python code in follow_wall_controller.py
to see how it's done.
Final Control Design
The final control design uses the follow-wall behavior for almost all encounters with obstacles. However, if the robot finds itself in a tight spot, dangerously close to a collision, it will switch to pure avoid-obstacles mode until it is a safer distance away, and then return to follow-wall. Once obstacles have been successfully negotiated, the robot switches to go-to-goal. Here is the final state diagram, which is programmed inside the supervisor_state_machine.py
:
Here is the robot successfully navigating a crowded environment using this control scheme:
An additional feature of the state machine that you can try to implement is a way to avoid circular obstacles by switching to go-to-goal as soon as possible instead of following the obstacle border until the end (which does not exist for circular objects!)
Tweak, Tweak, Tweak: Trial and Error
The control scheme that comes with Sobot Rimulator is very finely tuned. It took many hours of tweaking one little variable here, and another equation there, to get it to work in a way I was satisfied with. Robotics programming often involves a great deal of plain old trial-and-error. Robots are very complex and there are few shortcuts to getting them to behave optimally in a robot simulator environment…at least, not much short of outright machine learning, but that's a whole other can of worms.
I encourage you to play with the control variables in Sobot Rimulator and observe and attempt to interpret the results. Changes to the following all have profound effects on the simulated robot's behavior:
- The error gain
kP
in each controller - The sensor gains used by the avoid-obstacles controller
- The calculation of v as a function of ω in each controller
- The obstacle standoff distance used by the follow-wall controller
- The switching conditions used by
supervisor_state_machine.py
- Pretty much anything else
When Programmable Robots Fail
We've done a lot of work to get to this point, and this robot seems pretty clever. Yet, if you run Sobot Rimulator through several randomized maps, it won't be long before you find one that this robot can't deal with. Sometimes it drives itself directly into tight corners and collides. Sometimes it just oscillates back and forth endlessly on the wrong side of an obstacle. Occasionally it is legitimately imprisoned with no possible path to the goal. After all of our testing and tweaking, sometimes we must come to the conclusion that the model we are working with just isn't up to the job, and we have to change the design or add functionality.
In the mobile robot universe, our little robot's “brain” is on the simpler end of the spectrum. Many of the failure cases it encounters could be overcome by adding some more advanced software to the mix. More advanced robots make use of techniques such as mapping , to remember where it's been and avoid trying the same things over and over; heuristics , to generate acceptable decisions when there is no perfect decision to be found; and machine learning , to more perfectly tune the various control parameters governing the robot's behavior.
A Sample of What's to Come
Robots are already doing so much for us, and they are only going to be doing more in the future. While even basic robotics programming is a tough field of study requiring great patience, it is also a fascinating and immensely rewarding one.
In this tutorial, we learned how to develop reactive control software for a robot using the high-level programming language Python. But there are many more advanced concepts that can be learned and tested quickly with a Python robot framework similar to the one we prototyped here. I hope you will consider getting involved in the shaping of things to come!
Acknowledgement: I would like to thank Dr. Magnus Egerstedt and Jean-Pierre de la Croix of the Georgia Institute of Technology for teaching me all this stuff, and for their enthusiasm for my work on Sobot Rimulator.