2.控制手柄的ROS驱动代码
0x00 驱动代码概述
在第一篇文章中介绍了如何使用常见的硬件来搭建一个控制手柄,我们使用Arduino为核心控制板,获取各传感器数据。然后将数据组装成自定义的数据帧格式,通过串口将数据帧发送出来。这样就完成了控制手柄固件代码,但是这些数据帧我们并不能直接将其用来控制ROS下小车移动。我们需要解析数据帧内容,然后将其再次封装成ROS下控制小车移动的消息格式,最终要通过话题发送出来才可以。本篇文章将指导如何实现手柄的ROS驱动代码,首先我们需要来认识下整个控制手柄的数据流程示意图,通过该图就能对手柄的代码实现理解更为透彻:
0x01 创建驱动软件包
在这里由于我们增加了dynamic_reconfigure功能,这样就可以动态的更新节点参数了。这样就使得我们在创建软件包时新增了一个依赖项dynamic_reconfigure,完整的创建软件包命令如下:
catkin_create_pkg ros_handle_python rospy geometry_msgs dynamic_reconfigure
下面我们首先来看下整体代码的结构,这样就方便大家后面知道每个代码文件放置在什么路径下了:
在该软件包中,需要注意的是CMakeLists.txt文件,该文件需要确保有下图所示的部分才行,不然编译会出错的:
对于package.xml文件不用做修改,保持默认状态即可,下面来详细介绍每个源码文件的内容。
0x02 arduino通信代码
我们首先来介绍驱动部分的底层通信部分代码,由于控制手柄的主控板是arduino。那么我们的上层驱动程序就需要可以连接arduino的串口,并可以从串口读取到发送过来的数据。这部分代码是非常重要的,后续的部分解析的数据帧都是从这里获取的。我们首先来看下arduino_driver.py的完整代码,后面再对该代码进行解释说明:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
"""
Copyright: 2016-2018 ROS小课堂 www.corvin.cn
Author: corvin
Description:
A Python driver for the Arduino microcontroller
History:
20181024: initial this source code file.
"""
import os
import time
import sys, traceback
from serial.serialutil import SerialException
from serial import Serial
class Arduino:
''' Configuration Arduino Board Parameters
'''
def __init__(self, port="/dev/ttyACM0", baudrate=57600, timeout=0.5):
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.interCharTimeout = timeout/30.
# connect arduino board
def connect(self):
try:
self.port = Serial(port=self.port, baudrate=self.baudrate, timeout=self.timeout)
# give the firmware time to wake up.
time.sleep(1)
except SerialException:
print "Serial Exception:"
print sys.exc_info()
print "Traceback follows:"
traceback.print_exc(file=sys.stdout)
print "Cannot connect to Arduino!"
os._exit(2)
def close(self):
''' Close the serial port.
'''
self.port.close()
def recv(self, timeout=0.5):
timeout = min(timeout, self.timeout)
''' This command should not be used on its own: it is called by the execute commands
below in a thread safe manner. Note: we use read() instead of readline() since
readline() tends to return garbage characters from the Arduino
'''
c = ''
value = ''
attempts = 0
while c != '\r':
c = self.port.read(1)
value += c
attempts += 1
if attempts * self.interCharTimeout > timeout:
return None
value = value.strip('\r')
return value
def recv_int_array(self):
try:
values = self.recv(self.timeout).split()
return map(int, values)
except:
return []
""" Basic test for connectivity """
if __name__ == "__main__":
portName = "/dev/ttyACM0"
baudRate = 57600
myArduino = Arduino(port=portName, baudrate=baudRate, timeout=0.5)
print "Ready to connect arduino serial port..."
myArduino.connect()
print "Ready to read data ..."
time.sleep(1)
Data = []
Data = myArduino.recv_int_array()
print Data
myArduino.close()
print "Now disconnect arduino serial port..."
下面来对代码进行简要解析说明:
-
init()函数:为了初始化类的成员变量,这里提供一些默认的参数,包括默认的arduino端口/dev/ttyACM0,波特率57600,连接的超时时间等。
-
connect()函数:用于根据指定的连接参数来连接arduino串口,如果连接串口失败的话,会输出错误提示信息。
-
close()函数:用于断开当前的串口连接。
-
recv()函数:从串口中一个字符一个字符的读取内容,直到遇到换行符则认为读完一帧数据,将该一帧数据返回。
-
recv_int_array()函数:将调用recv()函数返回的数据帧给分割,然后存储到列表中,然后返回。
-
最后的主函数,就是为了测试一下与arduino通信是否正常。测试流程为首先连接串口,从中读取数据并打印出来,最后断开连接。
在进行后面的数据帧解析前,我们需要先确保该源码文件中提供的各函数功能可以正常工作。下面可以先测试下,如下图所示,如果可以从串口中读取到数据,则说明该代码是正常的:
0x03 动态更新节点参数代码
为了方便及时更新节点运行参数,我就新增了这个dynamic_reconfigure功能。这里介绍要新增这个功能需要编写的代码,首先来看下需要在cfg文件夹中新增的配置文件的代码handle_params.cfg:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
"""
Copyright: 2016-2018 ROS小课堂 www.corvin.cn
Author: corvin
Description:
实现dynamic_reconfigure功能的需要的配置文件源码.
History:
20181116: initial this comment.
"""
from dynamic_reconfigure.parameter_generator_catkin import *
PACKAGE = "ros_handle_python"
gen = ParameterGenerator()
gen.add("cmd_topic_name", str_t, 0, "set cmd topic name", "/turtle1/cmd_vel")
gen.add("cmd_public_rate", int_t, 0, "send cmd rate", 20, 0, 20)
gen.add("angular_z", double_t, 0, "set angular_z param", 1.0, 0, 2.0)
gen.add("linear_x", double_t, 0, "set linear_x param", 1.0, 0, 2.0)
exit(gen.generate(PACKAGE, "handle_dynamic_reconfig", "handle"))
下面来对该代码进行简要解析说明:
-
gen = ParameterGenerator():创建一个动态参数生成器,下面就可以往该生成器中添加可以动态配置的参数了。
-
gen.add():第一个参数表示可以动态配置的参数名,字符串格式;第二个参数表明需要配置的参数类型,这里包括str_t(字符串)、int_t(整型)、double_t(浮点型)、bool_t(布尔型);第三个参数为要传入参数动态配置回调函数中的掩码,在回调函数中会修改所有参数的掩码,表示参数已经进行了修改,该值一般设置为0;第四个参数表示对该参数的说明描述信息。第五个参数表示参数设置的默认值;第六个参数表示参数可以设置的最小值;最后一个参数表示可以参数可以设置的最大值;
-
exit(gen.generate(PACKAGE,"handle_dynamic_reconfig", "handle")):用于生成所有C++和Python相关使用的头文件,并退出该程序。第一个参数表明当前软件包名称;第二个参数是动态参数更新的节点名;第三个参数是生成头文件使用的前缀,一般情况下是和配置文件名相同的(不带后面的.cfg后缀),当然也可以不一样。这样最终生成handleConfig.h这样的头文件,这样就可以在后面的代码中直接引用该头文件了。
-
注意:需要给该文件加上执行权限,使用chmod +x handle_params.cfg命令增加执行权限。
然后来看一下动态更新参数服务端的代码dynamic_server.py,完整代码如下:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
Copyright: 2016-2018 ROS小课堂 www.corvin.cn
Author: corvin
Description:
用于手柄可以使用dynamic_reconfigure,这样就可以动态的调整手柄的参数.
History:
20181115: initial this comment.
'''
import rospy
from dynamic_reconfigure.server import Server
from ros_handle_python.cfg import handleConfig
def callback(config, level):
rospy.loginfo("Dynamic_reconfig_server:topic_name:{cmd_topic_name},public_rate:{cmd_public_rate},angular_z:{angular_z},linear_x:{linear_x}".format(**config))
return config
if __name__ == "__main__":
rospy.init_node("handle_dynamic_reconfig", anonymous=True)
srv = Server(handleConfig, callback)
rospy.spin()
该代码就是动态更新节点的服务端代码,只有该节点运行起来后才可以动态的更新节点参数。
0x04 主节点代码
下面来介绍主节点代码,该代码用来连接arduino串口,获取数据并解析数据帧。然后将获取的数据重新封装成ROS下控制移动的消息格式,然后将其发送到指定的话题中。下面来看下handle_node.py完整的代码:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
Copyright: 2016-2018 ROS小课堂 www.corvin.cn
Author: corvin
Description:
ROS解析代码,用来通过串口来获取从arduino发来的数据.然后组装成控制移动的twist命令,
通过话题发布出来.
History:
20181024: initial this code.
'''
import os
import rospy
import dynamic_reconfigure.client
from arduino_driver import Arduino
from geometry_msgs.msg import Twist
from serial.serialutil import SerialException
class HandleROS():
def __init__(self):
rospy.init_node('ros_handle_node', log_level=rospy.INFO)
client = dynamic_reconfigure.client.Client("handle_dynamic_reconfig",timeout=30,config_callback=self.callback)
# Get the name in case it is set in the launch file
self.name = rospy.get_name()
rospy.on_shutdown(self.shutdown)
self.getConfigParam()
# Connect arduino board
self.controller = Arduino(self.port, self.baud, self.timeout)
self.controller.connect()
rospy.loginfo("Connected to handle on port " + self.port + " at " +
str(self.baud) + " baud!" +" cmd_topic name:" + self.cmd_topic)
while not rospy.is_shutdown():
dynamic_param = client.get_configuration(timeout=3)
self.dynamicReconfig(dynamic_param)
self.cmd_vel_pub = rospy.Publisher(self.cmd_topic, Twist, queue_size=self.cmd_delay)
loop_rate = rospy.Rate(self.rate)
Data = []
Data = self.controller.recv_int_array()
if len(Data) == 5:
map(float, Data)
#rospy.loginfo("lock_status:" + str(Data[0])+" handle_x:" + str(Data[1]) + " handle_y:"+str(Data[2])
# + " handle_z:"+str(Data[3])+" rotate_percent:"+str(Data[4]))
if (Data[0] == 1):
linear_x, angular_z = self.parseSerialData(Data[1], Data[2], Data[3], Data[4])
self.sendCmdTopic(linear_x, angular_z)
loop_rate.sleep()
def dynamicReconfig(self, paramList):
self.cmd_topic = paramList['cmd_topic_name']
self.rate = paramList['cmd_public_rate']
self.angular_z = paramList['angular_z']
self.linear_x = paramList['linear_x']
def callback(self, config):
rospy.loginfo("{cmd_topic_name},{cmd_public_rate},{angular_z},{linear_x}".format(**config))
def parseSerialData(self, handle_x, handle_y, handle_z, rotate_percent):
if handle_z == 1:
linear_x = self.linear_x
linear_x *= (rotate_percent/100.0)
self.cmd_linear_x = linear_x
else:
angular_z = self.angular_z
angular_z *= (rotate_percent/100.0)
self.cmd_angular_z = angular_z
# config angular_z
angular_z = self.cmd_angular_z
if handle_x > 700:
angular_z *= -1
elif handle_x < 300:
angular_z *= 1
else:
angular_z = 0.0
# config linear x
linear_x = self.cmd_linear_x
if handle_y > 700:
linear_x *= 1
elif handle_y < 300:
linear_x *= -1
else:
linear_x = 0.0
#rospy.loginfo("Send linear_x: " + str(linear_x)+" angular_z:"+str(angular_z))
return linear_x, angular_z
def sendCmdTopic(self, linear_x, angular_z):
'''Send cmd to topic
'''
cmd_msg = Twist()
cmd_msg.linear.x = linear_x
cmd_msg.linear.y = 0
cmd_msg.linear.z = 0
cmd_msg.angular.x = 0
cmd_msg.angular.y = 0
cmd_msg.angular.z = angular_z
self.cmd_vel_pub.publish(cmd_msg)
def getConfigParam(self):
'''Get parameter from config file
'''
self.port = rospy.get_param("~port", "/dev/ttyACM0")
self.baud = int(rospy.get_param("~baud", 57600))
self.timeout = rospy.get_param("~timeout", 0.7)
self.cmd_topic = rospy.get_param("~cmd_topic", "/cmd_vel")
self.angular_z = rospy.get_param("~angular_z", 1.0)
self.linear_x = rospy.get_param("~linear_x", 1.0)
self.rate = int(rospy.get_param("~public_rate", 20))
self.cmd_delay = int(rospy.get_param("~cmd_delay", 1))
self.cmd_linear_x = 1.0
self.cmd_angular_z = 1.0
def shutdown(self):
'''Shutdown the handle
'''
rospy.logwarn("Shutting down ros handle node...")
try:
self.cmd_vel_pub.publish(Twist())
rospy.sleep(2)
except:
pass
try:
self.controller.close()
except:
pass
finally:
rospy.logwarn("Serial port closed")
os._exit(0)
if __name__ == '__main__':
try:
myHandle = HandleROS()
except SerialException:
rospy.logerr("Serial exception trying to open port !")
os._exit(1)
下面来对该代码进行简要解析说明:
-
init()函数:该函数为类的初始化函数,包括初始化dynamic_reconfigure客户端、从launch文件获取配置参数、连接控制手柄的arduino串口。当连接好串口后,开始不断的检测dynamic_reconfigure中参数有没有动态更新,如果更新的话就取最新参数值。使用最新参数值来初始化发布者对象和发布频率对象。然后从串口中取出数据帧,将其解析判断后重新组装成Twist消息,最后通过发布者将该控制移动的命令发布出去。
-
dynamicReconfig()函数:从dynamic_reconfigure客户端返回的列表中解析出各参数值。
-
callback()回调函数:该回调函数是dynamic_reconfigure客户端数据更新时自动调用的函数,这里的代码只是打印下最新的参数。
-
parseSerialData()函数:根据串口数据的摇杆数据值和旋转编码器值来确定线性速度x轴方向速度、角速度z轴旋转速度,这里需要注意的是只有在按下摇杆,即摇杆的hanle_z反馈为0时,旋转旋钮控制的是角速度的大小。在默认状态handle_z没有按下,反馈的是1时,控制的是线性速度x轴方向速度。另外就是如何来确定往前后走和左右转向,为了防止过于灵敏,我就设置了一个区间,超过该区间才认为控制有效。如下图所示:
-
sendCmdTopic()函数:将计算得到的linear_x和angular_z填写到Twist消息中,然后通过发布者发布到指定话题中。
-
getConfigParam()函数:从launch文件中获取的配置参数。我这里的代码可以使用两种方式来修改控制手柄的参数,一个就是launch文件加载的配置参数,另外一个就是dynamic_reconfigure可以动态更新配置参数。
-
shutdown()函数:当节点停止运行时执行的函数,这里是向话题发送一个空移动命令,为了让小车现在可以停止移动。然后断开连接的串口。
-
最后的main函数:就是初始化一个HandleROS()类对象,这样就会直接调用init()函数了,节点就开始不断运行了。
最后来看一下launch文件ros_handle.launch,通过该文件就可以来使手柄节点正常启动了:
<!-- Copyright: 2016-2018 ROS小课堂 www.corvin.cn Author: corvin Description: 启动ROS控制手柄的launch文件,用来解析下位机arduino的各传感器数据,然后组装 并发布控制命令. History: 20181024: init this launch file. 20181105: add dynamic server node. --> <launch> <!--startup ros control handle dynamic server node --> <node name="handle_dynamic_reconfig" pkg="ros_handle_python" type="dynamic_server.py" output="screen" /> <!-- startup contorl handle client node --> <node name="handle_python_node" pkg="ros_handle_python" type="handle_node.py" output="screen"> <rosparam file="$(find ros_handle_python)/cfg/handle_params.yaml" command="load"/> </node> </launch>
同时来看下在启动handle_python_node节点时加载的配置文件handle_params.yaml是如何编写的:
# Copyright: 2016-2018 ROS小课堂 www.corvin.cn # Author: corvin # Description: # 该文件为手柄的配置参数,包括:端口号,波特率,话题发布频率等. # 下面来依次介绍下各参数的意义: # port:arduino板的连接端口号. # baud:连接arduino的波特率,这个是在arduino代码中设置的. # timeout:读取数据时超时时间. # public_rate:发布话题的频率. # cmd_topic:发布控制移动的话题名称. # angular_z:发送角速度的最大值. # linear_x:发送线速度的最大值. # send_cmd_delay:话题中命令执行的延迟,最小需要设置为1,不能为0. # 最大无上限,一般不超过10.这里默认设置为1,即以最快速度执行话题中命令. # History: # 20181024: initial this config file. port: /dev/ttyACM0 baud: 57600 timeout: 0.5 public_rate: 20 cmd_topic: /turtle1/cmd_vel angular_z: 1.0 linear_x: 1.0 cmd_delay: 1
到这里手柄的ROS驱动代码就全介绍完了。
0x05 运行测试
在介绍完所有代码后,接下来就可以来编译和运行测试了。编译命令就是直接在工作区根目录下执行catkin_make命令即可,测试也很简单,我们只有运行launch文件即可。运行launch文件的命令如下:
roslaunch ros_handle_python ros_handle.launch
当运行该launch文件后,通过rqt_graph来查看当前的节点和话题通信图来确认节点启动是否正常:
通过该图发现,最左边是handle_dynamic_reconfig节点,该节点负责动态更新控制手柄的配置参数。该节点与handle_python_node通信是通过parameter_descriptions和parameter_updates这两个话题,一个是用来介绍更新的动态参数,一个是更新的参数内容。最终发布的话题是/turtle1/cmd_vel,这个话题中消息就是控制手柄发布出来的控制移动命令。
从最终的话题我们就得知,要想测试手柄是否正常工作就需要订阅该话题测试。这里就需要启动ROS下的turtlesim来看看手柄是否可以控制其移动:
查看下此时的节点话题图:
在控制小乌龟四处移动过程中,可以将各速度数据可视化显示。这里使用rqt_plot来查看数据:
在Topic中输入/turtle1/pose即可查看到所有的数据,默认显示的数据较多。这里我们可以先查看angular_velocity和linear_velocity是否正常,这里还需要调整显示的坐标轴量程。当我们控制小乌龟往前走时,可以发现linear_velocity一直在增大,通过旋钮可以控制该速度的大小。速度反馈正常。
下面来测试dynamic_reconfigure动态更新参数是否正常,我们在启动了launch文件后。只需要在终端中输入以下命令就可以开启动态更新节点:
rosrun rqt_reconfigure rqt_reconfigure
我们可以在不中断节点运行的情况下来更新发布控制命令的话题,如下动图所示:
0x06 References
[1].corvin_zhang 1.使用dynamic_reconfigure实现节点参数动态更新. https://www.corvin.cn/651.html
[2].ROS Wiki. Setting up Dynamic Reconfigure for a Node (python).
http://wiki.ros.org/dynamic_reconfigure/Tutorials/SettingUpDynamicReconfigureForANode%28python%29
[3].ROS Wiki. Using the Dynamic Reconfigure Python Client.
http://wiki.ros.org/dynamic_reconfigure/Tutorials/UsingTheDynamicReconfigurePythonClient
[4].ROS API. Package dynamic_reconfigure.
0x07 Feedback
大家在按照教程操作过程中有任何问题,可以直接在文章末尾给我留言,或者关注ROS小课堂的官方微信公众号,在公众号中给我发消息反馈问题也行。我基本上每天都会处理公众号中的留言!当然如果你要是顺便给ROS小课堂打个赏,我也会感激不尽的,打赏30块还会被邀请进ROS小课堂的微信群,与更多志同道合的小伙伴一起学习和交流!
[wshop_reward]












