一、简单概述
本人于今年夏天参加了电赛,在比赛封箱前无人机和地面站已经可以基本完成赛题要求,保底能上80分,但是非常可惜,在临场测试时出现纰漏,痛失省一,在无人机D题赛道仅拿了省二。从拿到赛题时的自信满满,在测试时的诧异焦急,到测试完后心里的五味杂陈,对于第一次参加电赛的我和我的队友来说,也算是一种经历。
如果做过23年的空地协同的赛题,那么今年的地面站以及与无人机之间的通讯对于大多数搞无人机赛题的队伍来说已经算是有一定的开发经验了。今年的无人机赛题相较于去年来说,在多机通讯方面有其延伸之处:无人机和地面站的数据传输量更多,无人机和地面站通讯联系更为紧密一体,这就对两者之间的通讯稳定性、可持续性要有更高的要求,并且地面站要有一定的人机交互显示功能。而对于无人机的任务要求方面,增加了对无人机运动精准度的需求,并且无人机飞行工作时间的增加,无人机电池续航问题就不得不考虑进去,所以对于无人机的任务控制代码有了更高的优化需求。
在本篇文章中,我会逐步讲解我们在赛时所做地面站的整体方案,将从 通讯环境需求、可触摸串口屏设计,串口屏与树莓派通讯建立,树莓派控制Arduino UNO开发板,开发板地面站(树莓派)与无人机(jetson nano nx)通讯建立这几个方面从赛题任务角度一 一带入。
下面是地面站的通讯结构示意图:
也许有些地方讲的不够详细或有出入,还请佬佬们多多指点;若是有问题或不理解的地方,可以到评论区沟通或与我私聊,觉得文章不错的还请多多点赞呐~
二、通讯环境需求
(一)硬件设备需求
所用设备如下:
1.树莓派4B
2. 淘晶驰串口触摸屏
3.Arduino UNO 开发板
4.发光二极管LED灯(三引脚PWM可控)
5.供电电源(5V即可)
(二)软件环境搭建
1.树莓派4B环境版本推荐与无人机linux系统版本一致,这里使用的是ubuntu20.04
该地面站的树莓派4B是从去年买的塔克AKM小车上拆卸下来的,所以烧录的系统是塔克官网Ubuntu20.04的系统,没作更改。但是我赛后才意识塔克的Ubuntu会对树莓派的开机启动时间进行锁定,拖延开机程序自启时间。所以我建议树莓派系统只需烧录与无人机的系统版本一致即可,并且把ROS系统给搭配好。
2.建议使用NoMachine远程桌面控制软件,对于树莓派等机载电脑的控制较为方便。
三、可触摸串口屏设计(本文章中所用型号为T1 TJC4827T143_011)
!!!注意:串口屏的设计要与树莓派以及无人机的通讯密切相关,设计时应时刻考虑该方法的合理可行。
串口屏中的各类事件代码若有疑惑的地方,可以查询淘晶驰串口屏官网:
http://wiki.tjc1688.com/download/usart_hmi.html
(一)串口屏入门操作
这一部分的内容就不详细讲解了,可以到官网或者B站上去自主学习,一些基本的操作要学会,大半天时间基本就可以熟练上手了。该项目主要涉及以下基本操作:
1.装好上位机软件,配置好电脑上的串口驱动,选择对应的屏幕型号并创建工程项目。
2.学会导入图片、字库,学会调用数字控件、文本控件、按钮控件和图片控件,对控件的一些属性要有了解,还要学会添加界面、下载工程到串口屏等基本操作。
3.对串口屏官网上的基本指令集和书写语法要有大概了解和认识。
(二)设计串口屏具体过程
项目已上传到百度网盘,可以下载查看
https://pan.baidu.com/s/1IbdpDcpMkfPL60ZgwwPy4Q?pwd=2inv
(1)明确赛题任务
在设计串口屏之前,再看一遍赛题对于地面站的功能要求:
将上述赛题的要求整理好之后主要分为下列几点:
1.无人机盘点货物过程中,货物坐标和编码信息可以实时正确显示在地面站,注意货物的坐标和编码不是一 一对应的。
2.每盘点的一个货物,无论是任务一还是任务二,地面站上的LED灯都可以亮闪一次。
3.无人机盘点完货物之后,可以根据编码实时查询到货物的坐标信息。
4.执行任务二时随机给到一个货物编号,地面站可以实时显示出该货物信息,并且可以地面站可以根据任务一保存下来的坐标i信息匹配出无人机的飞行路线显示图。
(2)根据任务需求和现有条件,设计串口屏
这里就不把设计过程一 一截图录屏再复现一遍,因为有太多重复性工作。这里会把完整工程分享给大家,并且会做详细讲解。
1)page0——货物信息显示(主界面)
在该页面中共使用了24个图片控件(p0~p23),25个数字控件(n0~n23,n24作为任务二的显示坐标),4个按钮控件(b0——任务1,b1——任务2,b24——轨迹查询,b25——物件查询)
1. page0页面中每个数字控件的属性必须为 全局属性
2.任务1和任务2的按钮每次按下都会触发按下事件,发送字符串给树莓派
任务1按下事件: prints "renwu1",0
任务2按下事件: prints "renwu2",0
3. b24按钮和b25按钮按下后会切换页面
b24按钮按下:page page1
b25按钮按下:page page2
4.在该页面的前初始化事件中添加下列代码,该代码会添加图库里的图片(一个红点)到任务二所抽取的货物坐标前
前初始化代码为:
if(n24.val>=1)
{
if(n24.val==n0.val)
{
pic 9,11,37
}
if(n24.val==n1.val)
{
pic 9,45,37
}
if(n24.val==n2.val)
{
pic 11,76,37
}
if(n24.val==n3.val)
{
pic 9,111,37
}
if(n24.val==n4.val)
{
pic 9,142,37
}
if(n24.val==n5.val)
{
pic 12,174,37
}
if(n24.val==n6.val)
{
pic 137,11,37
}
if(n24.val==n7.val)
{
pic 137,45,37
}
if(n24.val==n8.val)
{
pic 137,76,37
}
if(n24.val==n9.val)
{
pic 137,111,37
}
if(n24.val==n10.val)
{
pic 137,142,37
}
if(n24.val==n11.val)
{
pic 137,174,37
}
if(n24.val==n12.val)
{
pic 255,11,37
}
if(n24.val==n13.val)
{
pic 255,45,37
}
if(n24.val==n14.val)
{
pic 255,76,37
}
if(n24.val==n15.val)
{
pic 255,111,37
}
if(n24.val==n16.val)
{
pic 255,142,37
}
if(n24.val==n17.val)
{
pic 255,174,37
}
if(n24.val==n18.val)
{
pic 364,11,37
}
if(n24.val==n19.val)
{
pic 364,45,37
}
if(n24.val==n20.val)
{
pic 364,76,37
}
if(n24.val==n21.val)
{
pic 364,111,37
}
if(n24.val==n22.val)
{
pic 364,142,37
}
if(n24.val==n23.val)
{
pic 364,174,37
}
}
2)page1
1.在该页面中,按下b1按钮会在屏幕指定位置显示指定照片,显示什么照片取决于page0页面的n24.val值和n0~n23.val的值
page1页面b1按下事件代码:
a=page0.n24.val
if(a==page0.n0.val)
{
pic 170,15,33
}else if(a==page0.n1.val)
{
pic 170,15,33
}else if(a==page0.n2.val)
{
pic 170,15,33
}else if(a==page0.n3.val)
{
pic 170,15,33
}else if(a==page0.n4.val)
{
pic 170,15,33
}else if(a==page0.n5.val)
{
pic 170,15,33
}else if(a==page0.n6.val)
{
pic 170,15,34
}else if(a==page0.n7.val)
{
pic 170,15,34
}else if(a==page0.n8.val)
{
pic 170,15,34
}else if(a==page0.n9.val)
{
pic 170,15,34
}else if(a==page0.n10.val)
{
pic 170,15,34
}else if(a==page0.n11.val)
{
pic 170,15,34
}else if(a==page0.n12.val)
{
pic 170,15,35
}else if(a==page0.n13.val)
{
pic 170,15,35
}else if(a==page0.n14.val)
{
pic 170,15,35
}else if(a==page0.n15.val)
{
pic 170,15,35
}else if(a==page0.n16.val)
{
pic 170,15,35
}else if(a==page0.n17.val)
{
pic 170,15,35
}else if(a==page0.n18.val)
{
pic 170,15,36
}else if(a==page0.n18.val)
{
pic 170,15,36
}else if(a==page0.n18.val)
{
pic 170,15,36
}else if(a==page0.n19.val)
{
pic 170,15,36
}else if(a==page0.n20.val)
{
pic 170,15,36
}else if(a==page0.n21.val)
{
pic 170,15,36
}else if(a==page0.n22.val)
{
pic 170,15,36
}else if(a==page0.n23.val)
{
pic 170,15,36
}
2.按下b0后会切换到page0页面
page1页面b0按下事件代码:page page0
3)page2
1.前初始化事件:将page3页面的变量控件va0.val的值赋值给t0.txt
前初始化事件代码:cov page3.va0.val,t0.txt,0
2. 在page2页面中b1按钮按下后会将page3页面中的va1.val变量控件赋值为1,并且切换到page3页面。
page2页面b1按下事件代码: page3.va1.val=1
page page3
3. page2页面按下b0按钮后,会返回主界面page0。
page2页面b0按下事件代码:page page0
4.page2页面按下b2按钮后,会将t0.txt文本控件的数值赋值给变量控件va0.val,按钮弹起后,会根据变量控件va0.val和page0页面的数字控件的值进行对比,数值相同就给t1.txt赋值对应的货物坐标。
按下事件:covx t0.txt,va0.val,0,0
弹起事件:
if(va0.val==page0.n0.val)
{
t1.txt="A1"
}
if(va0.val==page0.n1.val)
{
t1.txt="A2"
}
if(va0.val==page0.n2.val)
{
t1.txt="A3"
}
if(va0.val==page0.n3.val)
{
t1.txt="A4"
}
if(va0.val==page0.n4.val)
{
t1.txt="A5"
}
if(va0.val==page0.n5.val)
{
t1.txt="A6"
}
if(va0.val==page0.n6.val)
{
t1.txt="B1"
}
if(va0.val==page0.n7.val)
{
t1.txt="B2"
}
if(va0.val==page0.n8.val)
{
t1.txt="B3"
}
if(va0.val==page0.n9.val)
{
t1.txt="B4"
}
if(va0.val==page0.n10.val)
{
t1.txt="B5"
}
if(va0.val==page0.n11.val)
{
t1.txt="B6"
}
if(va0.val==page0.n12.val)
{
t1.txt="C1"
}
if(va0.val==page0.n13.val)
{
t1.txt="C2"
}
if(va0.val==page0.n14.val)
{
t1.txt="C3"
}
if(va0.val==page0.n15.val)
{
t1.txt="C4"
}
if(va0.val==page0.n16.val)
{
t1.txt="C5"
}
if(va0.val==page0.n17.val)
{
t1.txt="C6"
}
if(va0.val==page0.n18.val)
{
t1.txt="D1"
}
if(va0.val==page0.n19.val)
{
t1.txt="D2"
}
if(va0.val==page0.n20.val)
{
t1.txt="D3"
}
if(va0.val==page0.n21.val)
{
t1.txt="D4"
}
if(va0.val==page0.n22.val)
{
t1.txt="D5"
}
if(va0.val==page0.n23.val)
{
t1.txt="D6"
}
4)page3
1.在该页面中b0~b9按钮依次有下列按下事件
t0.txt=t0.txt+"0"
t0.txt=t0.txt+"1"
t0.txt=t0.txt+"2"
t0.txt=t0.txt+"3"
t0.txt=t0.txt+"4"
t0.txt=t0.txt+"5"
t0.txt=t0.txt+"6"
t0.txt=t0.txt+"7"
t0.txt=t0.txt+"8"
t0.txt=t0.txt+"9"
2.该页面中的文本控件t1.txt按下后,会将t0.txt的值赋值给变量控件va0.val,并将该页面的t0.txt赋值给page2.t0.txt,并切换到page2页面。
按下事件:
cov t0.txt,va0.val,0
if(va1.val==1)
{
page2.t0.txt=t0.txt
page page2
}
至此,串口屏页面设计已经完成。其中串口屏与树莓派通讯的地方在于任务一和任务二命令的发送,以及树莓派向串口屏传输货物编码以及坐标信息,无人机发送给地面站(树莓派)的是一个字典,这个字典里会包含所需要的货物编码,其中none值会被转化为0,树莓派会对这个字典再做转换,然后再发送给串口屏。代码会在后面具体讲解。
data = {
'A1': None, 'A2': 12, 'A3': 8, 'A4': 3, 'A5': 1, 'A6': 17,
'B1': None, 'B2': 15, 'B3': 11, 'B4': 9, 'B5': 19, 'B6': 16,
'C1': None, 'C2': 14, 'C3': 7, 'C4': 0, 'C5': 2, 'C6': 13,
'D1': None, 'D2': 10, 'D3': 4, 'D4': 5, 'D5': 6, 'D6': 24
}
四、串口屏与树莓派通讯建立
因为无人机那边绝大多数代码的编译是通过python语言,所以地面站这边为了减少通讯代码的复杂度也是使用pytnon语言。但是官网上有关串口屏与上位机(树莓派)用python代码通讯的很少,以下代码是在其基础上做的添加和修改。
(一)串口屏和树莓派简单的通讯实现
(1)树莓派给串口屏发送数据
将串口屏通过简单的ttl转串口模块与树莓派进行连接,然后便可以进行简单的通讯代码测试了。
下述代码运行后,会将串口屏幕上的n0.val控件的值赋值为20
#树莓派向串口屏上的数字控件n0.val发送数值
import serial #导入模块
try:
portx="/dev/ttyCH341USB0" #树莓派上识别到的串口屏上的串口号
bps=115200 #波特率,应该与串口屏上设置的一致
timex=0.5
# 打开串口,并得到串口对象
ser=serial.Serial(portx,bps,timeout=timex)
print("串口详情参数:", ser)
# 写数据
result=ser.write("n0.val=20".encode("GB2312")) #字符格式与串口屏的字库一致,这里为GB2312
# 发送结束符
ser.write(bytes.fromhex('ff ff ff')) #每发送一次数据的格式固定为“控件赋值”+“结束符”!!
ser.close()#关闭串口
except Exception as e:
print("---异常---:",e)
1.查询树莓派串口可以使用终端命令: ls -l /dev/tty*
注意:串口有时被识别为ttyCH341USB,也有可能是ttyUSB,有时也可能被识别为ttyACM。
2.代码中设置的串口波特率应与串口屏幕上设置的一致。(一般设置为115200)
3.树莓派发送给串口屏的命令格式严格执行为:控制命令+结束符
(2)串口屏发送数据给树莓派
在串口屏上的按钮控件里的按下事件里写入prints 函数后,按下按键即可
#串口屏发送数据到树莓派上,可以用来当作一键启动的启动信号
import serial
# 替换为你的串口名称和波特率
serial_port = '/dev/ttyCH341USB0' # 例如,这可能是/dev/ttyUSB0, /dev/ttyAMA0等,取决于你的树莓派和串口触摸屏
baud_rate = 115200 # 确保这与你的串口触摸屏的设置相匹配
# 初始化串口连接
ser = serial.Serial(serial_port, baud_rate, timeout=1)
try:
while True:
if ser.in_waiting > 0:
# 读取串口中的数据(这里假设数据以换行符结束)
incoming_data = ser.readline().decode('GB2312').rstrip()
# 打印接收到的数据
print("Received:", incoming_data)
# 如果需要,可以在这里添加额外的逻辑来处理数据
# 例如,基于数据内容执行某些操作
except KeyboardInterrupt:
# 如果用户按下了Ctrl+C,则关闭串口连接并退出程序
print("Program stopped by user")
ser.close()
except serial.SerialException as e:
# 处理串口异常,例如串口无法打开
print(f"Serial port error: {e}")
ser.close()
以上代码就可以基本实现了树莓派和串口屏数据的传输,有关串口屏实时通过树莓派控制无人机并且完成数据传输的代码会在第六节里讲述。
五、树莓派控制Arduino UNO开发板
Arduino开发板需提前烧录好要控制LED灯的程序,再通过串口USB与树莓派连接,同时Arduino板连接好LED灯。当然也可使用其他开发板,但arduino最为简单快捷。
Arduino uno开发板烧录的程序:
int ledPin = 13; // 定义LED连接的引脚
void setup() {
pinMode(ledPin, OUTPUT); // 设置引脚模式为输出
Serial.begin(9600); // 初始化串口通讯,波特率为9600
}
void loop() {
if (Serial.available() > 0) { // 检查串口是否有数据可读
int incomingByte = Serial.read(); // 读取串口输入的数据
if (incomingByte == '1') { // 检查收到的数据是否是数字1
digitalWrite(ledPin, HIGH); // 点亮LED
delay(1000); // 等待一秒
digitalWrite(ledPin, LOW); // 关闭LED
}
}
}
六、地面站(树莓派)与无人机(jetson nano nx)通讯建立
(一)多机通讯的环境配置
本项目中多机通讯的实现是基于同一局域网下的主从机配置,将主机设为ROS_master(节点管理者),从机设置为普通的robot。于是就可以利用树莓派的无线WiFi功能,可将地面站设置为主机,无人机设置为从机。具体的环境配置可以见下面的文档链接。
文档中还会讲到关于开机自启的功能,后面编译好的代码可以整体融合在一起,放到开机自启文件中。
https://pan.baidu.com/s/1hYf-q9wdaO1EorbEiozEJQ?pwd=9kvg 提取码: 9kvg
(二)地面站控制无人机任务1和任务2
(1)地面站端发送任务一或任务二的指令给无人机
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
import serial
def serial_read_loop():
serial_port = '/dev/ttyCH341USB0'
baud_rate = 115200
ser = serial.Serial(serial_port, baud_rate, timeout=1)
# 话题名称为'ren'
pub = rospy.Publisher('ren', String, queue_size=10)
rospy.init_node('serial_to_ros_bridge', anonymous=True)
rate = rospy.Rate(10) # 10hz
while not rospy.is_shutdown():
if ser.in_waiting > 0:
incoming_data = ser.readline().decode('GB2312').rstrip()
print("Received:", incoming_data)
# 根据接收到的数据发布不同的信息
if incoming_data == "renwu1":
pub.publish("1") # 发布消息'1'
elif incoming_data == "renwu2":
pub.publish("2") # 发布消息'2'
rate.sleep()
ser.close()
if __name__ == '__main__':
try:
serial_read_loop()
except rospy.ROSInterruptException:
pass
except serial.SerialException as e:
print(f"Serial port error: {e}")
请你用python代码根据上述代码代创建一个话题“ren”的订阅者,用来接收上述代码发送的信息,并将接收到的消息打印出来
上述代码创建并发布了一个名为“ren”的话题,该话题会根据从串口屏得来的指令来向无人机发送“1”或“2”,无人机在接收到消息之后即可执行对应任务。在这里需要注意的是串口的波特率和串口号的识别。
(2)模拟无人机端订阅“ren”话题
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
def callback(data):
# 打印从话题'ren'接收到的消息
rospy.loginfo("Received message on 'ren' topic: {}".format(data.data))
def ren_subscriber():
# 初始化ROS节点
rospy.init_node('ren_subscriber', anonymous=True)
# 创建一个订阅者,订阅'ren'话题,并指定回调函数
subscriber = rospy.Subscriber('ren', String, callback)
# 使节点持续运行,直到ROS被关闭或Ctrl+C被按下
rospy.spin()
if __name__ == '__main__':
try:
ren_subscriber()
except rospy.ROSInterruptException:
pass
(三)无人机传输货物编号给地面站
(1)模拟无人机实时发送货物信息
无人机发送的货物信息的格式是一个字典,该字典在无数据的情况下默认数据为“None”,在摄像头识别编号后会实时更新字典里的数据,这里仅作数据模拟。
#模拟无人机端发送数据
#!/usr/bin/env python
import rospy
from std_msgs.msg import String
import json
import time
def publisher():
rospy.init_node('mission_publisher', anonymous=True)
pub = rospy.Publisher('mission', String, queue_size=10)
rate = rospy.Rate(5) # 5hz的频率发布
data = {
'A1': None, 'A2': 12, 'A3': 8, 'A4': 3, 'A5': 1, 'A6': 17,
'B1': None, 'B2': 15, 'B3': 11, 'B4': 9, 'B5': 19, 'B6': 16,
'C1': None, 'C2': 14, 'C3': 7, 'C4': 0, 'C5': 2, 'C6': 13,
'D1': None, 'D2': 10, 'D3': 4, 'D4': 5, 'D5': 6, 'D6': 24
}
while not rospy.is_shutdown():
# 模拟数据更新
for key in data:
if data[key] is None:
data[key] = 0 # 将None转换为0
else:
data[key] = 1 - data[key] # 切换值
# 构造JSON字符串
mission_data = json.dumps(data)
rospy.loginfo(mission_data)
pub.publish(mission_data)
rate.sleep()
if __name__ == '__main__':
try:
publisher()
except rospy.ROSInterruptException:
pass
(2)地面站接收无人机数据并且更新在串口屏上
地面站需要对无人机传送来的数组进行调整,将字典里的数据转换为一个个
import rospy
from std_msgs.msg import String
import serial
import time
import json
def callback(data):
# 解析JSON字符串数据到字典
try:
data_dict = json.loads(data.data)
except ValueError as e:
rospy.logerr("Error parsing data: %s", e)
return
# 打印到终端
rospy.loginfo(rospy.get_caller_id() + " I heard %s", data.data)
# 发送数据到串口
send_to_serial(data_dict)
def send_to_serial(data_dict):
ser = serial.Serial("/dev/ttyCH341USB0", 115200, timeout=0.5)
end_marker = b'\xff\xff\xff'
keys = ['A3', 'A2', 'A1', 'A4', 'A5', 'A6', 'B4', 'B5', 'B6', 'B3', 'B2', 'B1', 'C3', 'C2', 'C1', 'C4', 'C5', 'C6', 'D4', 'D5', 'D6', 'D1', 'D2', 'D3']
for i, key in enumerate(keys):
if key in data_dict:
val = data_dict[key]
ser.write(("n{}.val={}".format(i, val)).encode("GB2312"))
ser.write(end_marker)
ser.close()
def listener():
rospy.init_node('mission_listener', anonymous=True)
rospy.Subscriber("mission", String, callback)
rospy.spin()
if __name__ == '__main__':
listener()