本次我们实现用手机来控制小车的移动。效果如下:
控制小车移动是通过一个网页实现的,这样通过手机或者电脑上的浏览器就能控制小车的移动。
网页上部是监控区域,可以看到小车摄像头的实时画面。
网页中部是控制区域,有两个摇杆,左摇杆控制小车左右转向,右摇杆控制小车前进和后退。
网页下部是功能选择,用于后面扩展功能,Autonomous Mode用于在手动驾驶和自动驾驶模式间切换,Record Video用于录制小车行进的视频,后面可以使用视频和控制输入来训练自动驾驶模型。
本次我们实现的功能虽然看起来不太复杂,但部分基础代码对以后的扩展是非常重要的,因此有必要设计一个良好的架构。
在真实的汽车上有一个通信总线 - CAN,Controller Area Network,控制器局域网。汽车上需要通信、交换数据的部件都连接到该总线上。它可以实现点对点、一对多、广播的通信方式。CAN有行业的国际标准。
在我们的项目里用消息总线(message bus)模拟CAN,各组件通过发布/订阅(publish/subscribe)的方式进行通信。redis、kafka、rabbitmq、zeromq等都能实现所需功能,其中redis和zeromq比较轻量,在对比了redis和zeromq的性能后,我选择了zeromq作为mycar的消息中间件。
组件是指小车上实现特定功能,可以独立运行的部件。mycar项目的组件可能有:运动控制、摄像头、蓝牙手柄、激光雷达、IMU等。
在这篇文章里,我们需要实现的功能模块是:
运动控制,模块名:actuator摄像头,模块名:camera网页控制,模块名:web_controller各具体组件的UML类图如下:
Car 代表一辆小车,是项目的主类,负责加载配置文件和启动各组件(Component)。 创建该类需要传入配置文件和小车的运行时间。
Component 是小车组件的抽象接口,组件主要的行为方法是run、on_message和publish_message。start和shutdown方法在小车启动和停止时会自动调用。on_message和publish_message用来收发消息。run方法在start返回True时有作用,用于在单独线程执行组件长时间运行的业务逻辑,例子是摄像头不断产生图像数据。
CAN和ZmqCAN 代表消息总线的抽象类和zeromq实现,它也是一种组件Component。主要方法是向指定的channel发送消息和订阅消息,分别是publish(channel, message)和subscribe(channels, listener)。
PWMSteering和PWMThrottle 在actuator模块,分别是控制小车方向和速度的类。依赖PCA9685类来发出PWM信号。
Camera 是小车的摄像头类,用于产生实时图像。
WebController WebController组件用于接收页面输出,发送控制消息到消息总线。
下面分章节详细介绍一些重要的类和组件。
PWM,是指脉宽调制Pulse Width Modulation,遥控车一般使用PWM来控制转向和速度,所以2通道的遥控器一路输出到转向舵机,一路输出到电子调速器ESC。
I2C,IC间的通信协议,Inter-Integrated Circuit,包含一条双向串行数据线SDA,一条串行时钟线SCL。
PCA9685,是一个通用的16路舵机控制板,有16个输出端口,我们需要用到其中2个。
我们不需要了解如何产生I2C和PWM信号,在我们的项目里,由软件控制jetson nano的GPIO口产生I2C信号给PCA9685,PCA9685再生成PWM信号去驱动小车运动。
接线方式上一篇文章有讲到。
这个类使用了adafruit公司的python包adafruit-circuitpython-servokit来控制舵机角度与ESC速度。它已经封装好了ServoKit这个类。
使用上一讲的接线方式,ServoKit的使用方法如下:
servokit = ServoKit(channels=16, address=0x40) # 控制PCA9685第一个口的舵机的转动角度 servokit.servo[0].angle = 135 # 角度范围 0 ~ 180度 # 控制PCA9685第二个口的ESC的速度 servokit.continuous_servo[1].throttle = 0.3 # 速度的范围是 -1 ~ 1,大于0表示向前移动,如果输入负数代表后退,注意从前进切换到后退要输入两次同样的负数这两个类实现了Component接口,调用PCA9685类来实现功能。
PWMSteering 支持定义正前方的前进角度(straight_angle),以防小车有偏差;也支持自定义可转的角度范围(full_left_angle, full_right_angle)。默认的是小车舵机支持的90度的转向范围。
PWMThrottle 支持自定义控制速度的范围(min_throttle, max_throttle),如(-0.3, 0.3),会把(-1, 1)的值映射到这个范围内,因为小车速度较快,调小点可以更方便控制小车,以防撞坏。
Camera这个类比较简单,它使用cv2.VideoCapture()来捕捉摄像头图像。
如果连接的是CSI摄像头,那么使用gstreamer pipline + Nvidia的nvarguscamerasrc,参数可以参考官方文档。
如果是在其他平台上做测试,可以使用cv2.VideoCapture(0)来使用默认的摄像头设备。
WebController类用Flask来启动了一个本地web server,端口为8080。
实时图像展示部分使用的是返回帧图片的HTTP报文的方式,使用<img>HTML标签就可以让浏览器会不断加载新的帧。帧的格式为:
--frame Content-Type: image/jpeg [图像字符串]这种方式实现比较简单。
摇杆部分使用的是一个开源的js库做的,其实用一个摇杆就可以实现方向和速度的调节的,这里我选择分开来控制。
两个功能控制选项目前还没有用到,先忽略。
最后是小车运行的配置文件,用来配置小车需要加载的所有组件Component,一个例子如下:
components: zmq_can: # 模块名 server_mode: True # 创建实例的参数 actuator: # 该模块有两个类 PWMSteering: # 第一个类 subscription: ['web_steering'] # 接收来自这个channel的消息 channel: 0 PWMThrottle: # 第二个类 subscription: ['web_throttle'] channel: 1 min_throttle: -0.3 max_throttle: 0.3 camera: #该模块只有一个类 publication: ['cam/image'] # 把图像发送到这个channel device: '/device/video0' web_controller: subscription: ['cam/image'] publication: ['web_steering', 'web_throttle', 'web_record', 'web_autonomous'] # 发送消息到多个channel 顶层,使用components作为键,代表是小车的组件次层缩进,以组件的模块(文件)作为键,如果该模块只有一个类,那么第三层缩进可以直接写该类的创建时传入的参数,如camera最后层缩进,配置组件类创建时传入的参数这两个属性是每个Component都有的属性,默认为空([]),分别代表组件要订阅的消息和要发出的消息。
介绍完项目的代码后,我们看看怎么跑起来。
代码已开源到github,首先clone一下:
git clone https://github.com/evan-wu/mycar.git --branch blog-3 --single-branch注意jetson nano的Jet Pack已经自带了python3和编译好的opencv,切勿自己手动安装opencv。安装以下4个依赖就可以了:
pip3 instal pyyaml adafruit-circuitpython-servokit flask pyzmq用浏览器(手机浏览器)访问: http://<jetson nano ip>:8080 就可以了。
欢迎github/blog点赞、留言、讨论!
后续预告:手柄控制小车移动,实现PID寻线小车算法…