无人船项目总结

主要涉及 frp 内网穿透部署、树莓派串口设置以及 ros2 下的部署过程。

Posted by R on 2023-11-02

比赛涉及到用 ros 去控制无人船运动,完成传感器采集等任务,从编程的角度讲难度不高,但有很多内容以前从来没做过。包括 ros 也是第一次实际运用,因此值得拿出来记录一下,有很多不足的地方需要去改进,也有很多空间可以得到进一步的开发,放在记录最后进行总结。

frp 内网穿透

一点唠叨

整体构思是先打通通讯,再去细致化的部署具体的功能。通讯要实现远程通讯的目的,可以通过天线信号进行收发,但是要实现这种效果首先天线需要对准,很有可能出现断连的情况。其次在部署过程中还要学习很多东西,包括通讯协议等,这些内容复杂且没有一个比较理想的效果,因此我排除掉这种选择。

如果利用网络传输,那么这些问题就更容易解决,网络传输保证了你的协议不需要自己去研究,也更加稳定(无人船的范围是有信号覆盖的)。但是,无论是远程控制树莓派还是通过 ros 进行分布式通信,都需要在同一网络下及局域网下进行,这极大的限制了无人船的范围。解决这一问题的办法就是内网穿透。

这个概念我之前已经有过了解,当时想着有没有办法远程操控公司配好的 ubuntu 电脑,或是自己研究个树莓派的小东西放在家里,即使在学校也能远程操控。然后搜集了很多资料,就找到了内网穿透这个东西,当时的了解是内网穿透需要公网 ip,需要你自己去联系运营商要,还要在你已有一个自己的宽带及路由器的情况下。

这对于学生来说是很麻烦的一件事情,再加上这个想法并没有很急切很成熟,很多相关的概念也含糊不清,于是就放弃了对于内网穿透的进一步调研。直到这次比赛项目再次走到这个概念面前,我觉得可以试一试,如果这个能解决,那么比赛之后的一些功能都很好去做,对于以后很多方面的编程部署都要有很大的好处。

简单来说,这解锁了一片空间,给了你更大的自由性,掌握内网穿透的步骤,可以在任何时间地点去对相隔万里外的一个设备或服务器去作出操作,而不用局限于局域网的限制(开发板远程开发很突出的一个痛点)。

从概念讲起

首先,什么是 frp,什么又是内网穿透?

frp 是一个内网穿透的工具,这里需要讲一下,公网就是指可以在互联网上直接抵达的地址,无论是淘宝、百度或者 csdn,它们的地址都是可以直接抵达的。如果你有一个公网地址,那意味着你不但可以访问别人,别人也可以访问你。

而内网是不可以直接上网的,属于私有 IP(这里面具体的原理我也不是很清楚,需要学习网络编程才能详细了解),我们平时通过移动、电信之类的运营商上网,路由器分出来的 ip 都是私有的局域网 IP。但是为什么私有的也可以上网?那是因为运营商买了公有 IP,把这些公有 IP 分出来给用户使用,一片区域的用户使用的基本上是同一个 IP。

因此,公网是无法直接访问内网的设备的,因为从公网到内网的这个过程是运营商在管理的,绕过运营商,从公网直接到内网设备的这个过程,就叫内网穿透。

frp 是开源的内网穿透工具,可以让外部用户通过公网访问内网中的服务,而不需要暴露内网的IP地址和端口号。FRP的主要原理是通过一个中转服务器,在公网与内网之间建立一个反向代理通道,将公网用户的请求转发到内网中的服务。

也就是说 frp 充当了中间人的角色,外部用户要访问内网的设备,只需要给 frp 发送请求,frp 则通过端口映射等使得两者建立联系。

FRP 支持多种协议,包括 TCP、UDP、HTTP 和 HTTPS 等。它可以在不同的操作系统上运行,包括 Windows、Linux、Mac OS 等。FRP 有两个主要组件:服务器端和客户端。服务器端需要在公网上运行,用来转发公网用户的请求;客户端需要在内网中运行,用来将内网服务映射到公网上。

简单来说,处于内网的客户端通过 frp 的端口映射摇身一变,变成了公网 IP 地址和端口号,这样谁都可以通过互联网访问到客户端了。

安装与使用

首先提前声明,frp 的服务端需要部署在有公网 ip 的服务器上,要么是云服务器,要么是有你自己的宽带公网 ip 服务器(ip 需要和运营商申请)。在这里我选择了云服务器,如果对网速没有太大要求,只是想实现一些命令的传输(如 ssh 控制),那么云服务器只需要选择轻量级的即可,价格相对便宜并且学生有免费试用期。

我是在腾讯云进行申请了一个月免费的轻量级云服务器,规格如下所示,安装 Ubuntu 22.04 系统。

1

申请到以后会有对应的公网 ip 地址,可以直接通过 putty、xshell 等远程访问,密码设置等都有对应引导项,此处不再赘述。

服务端安装部署

远程登陆到云服务器,运行如下命令:

1
wget https://github.com/fatedier/frp/releases/download/v0.42.0/frp_0.42.0_linux_amd64.tar.gz

需要注意的是,下载版本客户端服务端需要统一,确定好版本一致。

下载完成后在服务器上使用如下命令进行解压:

1
tar -zxvf frp_0.42.0_linux_amd64.tar.gz

进入解压目录,无桌面操作可以通过命令 ll 来查询目录下的文件,有四个文件需要注意,分别是 frpc\frpc.ini\frps\frps.ini,对应的是客户端部署文件以及服务端部署文件。

此时我们部署的是服务端,可以将 frpc、frpc.ini 删掉,整个配置过程只需修改 frps.ini。

编辑 frps.ini,添加以下内容:

1
2
3
4
5
6
7
8
[common]
bind_port = 7000 # 必用:frp的默认监听端口,也可以自行修改,对应防火墙端口需要开放。

# 选用:frp监测后台配置,三个选项结合使用
dashboard_port = 7500 # 监测frp状态的端口
token = ***** # 用于客户端和服务端连接的口令
dashboard_user = *** # 登录frp监测网页的用户名
dashboard_pwd = ******* # 登录frp监测网页的密码

保存以后在该目录下运行 frps 服务端:

1
./frps -c frps.ini

同时需要注意在云服务器防火墙配置中开启对应的端口。

2

这里我的7000和7500端口就是为 frp 而打开的。运行输出如下内容表面运行正常。

3

此时访问云服务器的 ip 地址加端口号(**:7500)即可看到端口监控界面,此后各种数据的收发状况都可在此看到。

4

此时服务端仍属于前台工作,若关闭终端或远程窗口,frps 便会停止运行,因此使用 nohup 命令将其运行在后台。

1
nohup ./frps -c frps.ini &

进入后台后若要关闭 frp 需要先找到 frp 进程:

1
ps -aux|grep frp| grep -v grep

确定进程号后用 kill 命令关闭对应进程。

也可以使用 systemctl 来对 frps 进行控制,首先创建 frps.service:

1
sudo vim /lib/systemd/system/frps.service

在 frps.service 里添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=frps service
After=network.target syslog.target
Wants=network.target

[Service]
Type=simple
#启动服务的命令(此处写你的frps的实际安装目录)
ExecStart=/root/frp/frps -c /root/frp/frps.ini

[Install]
WantedBy=multi-user.target

注意”ExecStart=”之后的路径要填写你的frps安装路径(本文以安装到/root/frp/为例)

然后即可使用 systemctl 命令来控制 frps:

1
2
3
4
5
6
7
8
9
10
# 设置开机自启动
sudo systemctl enable frps
# 启动
sudo systemctl start frps
# 关闭
sudo systemctl stop frps
# 重启
sudo systemctl restart frps
# 查看状态
sudo systemctl status frps

至此,整个服务端部署流程便完成了。

客户端安装部署

客户端可以是 Windows、Linux 或者 macos,此处出于项目部署在树莓派上,介绍的是 Linux 下的配置过程,其他系统也是大同小异。

首先和此前一样的步骤,下载、解压,注意版本号一定要保持一致,如果由于外网的缘故无法连接到 github,建议在电脑上下下来再用优盘拷过去进行解压。

此处我们需要关注的则变成了 frpc、frpc.ini 两个文件,对 frpc.ini 进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[common]
# frp路由配置
server_addr = *.*.*.* # 服务端机器的ip
server_port = 7000 # 服务端frp监听端口
token = ****** # 连接口令
# 使用tls加密,tls是一种加密协议,可以避免信息被过滤掉
tls_enable = true

[ssh] # 穿透标签
type = tcp # 连接类型
local_ip = 127.0.0.1 # 客服端ip
local_port = 22 # 客户端端口
remote_port = 4071 # 服务端端口,可以自定义,但是注意防火墙开放对应端口

[http]
type = tcp
local_ip = 127.0.0.1
local_port = 407
remote_port = 4072
custom_domains = 43.143.177.241

如果只打算通过 ssh 访问的话,http 是可以不配置的,这里需要注意 remote_port 是指远程端口,需要在云服务器防火墙设置中打开(前面有提到),local_port 是本地端口,需要在本地即树莓派中打开。

配置完成后可以和前面的服务端流程一样设置开机自启动以及 systemctl 指令控制。

相关报错及对应解决办法

连接失败的问题一般是出在端口上,先确定防火墙规则是否正确添加。如果添加正确仍然无法连接,问题可能出在客户端也就是树莓派上,对应的本地端口可能没有打开(一般情况下22端口是不会有这种问题的),具体调试可参考以下:

  • 方式一:

    开启防火墙:

    1
    systemctl start firewalld

    开放指定端口:

    1
    2
    3
    4
    5
    firewall-cmd --zone=public --add-port=1935/tcp --permanent
    命令含义:
    --zone #作用域
    --add-port=1935/tcp #添加端口,格式为:端口/通讯协议
    --permanent #永久生效,没有此参数重启后失效

    重启防火墙:

    1
    firewall-cmd --reload

    查看端口号:

    1
    2
    netstat -ntlp   //查看当前所有tcp端口
    netstat -ntulp |grep 1935 //查看所有1935端口使用情况·
  • 方式二:

    直接开放对应端口:

    1
    /sbin/iptables -I INPUT -p tcp --dport 8080 -j ACCEPT

首次连接成功,后面突然无法连接的情况是由于校园网封禁了云服务器的端口,大概是这么个意思,只要用热点或者连其他网就可以解决了,如何让校园网不去封禁云服务器,这点就不太清楚了。

树莓派串口通讯

串口通讯包括硬件串口和 USB 串口的配置,在此次项目中分别对应了单片机通讯和传感器 arduino 板的通讯,在这部分一概并述。

硬件串口通讯

串口通讯的基本原理我搞的并不是很清楚,暂时没有深入研究的打算,所以就不去详细描述。在这里只需要明白,按正确的方式连接后,树莓派可以通过串口线发送信息,单片机也同样可以反馈信息。

GPIO扩展口定义

树莓派上的引脚有着三种编码方式,分别有 Board 编码、BCM 编码、wiringPi 编码,其中 Board 编码对应的是实际物理插槽,BCM 编码基本和 GPIO 的名字对应,wiringPi 编码则是wiringPi 库所使用的引脚编码方式。

1

另外,在进行串口通信时,两个设备间的 TXD、RXD 要交错连接。

串口设置

树莓派包含两个串口,这里指的应该是两类,但是具体还是不太清楚,先按两个来讲。

一种是硬件串口(/dev/ttyAMA0),硬件串口是由硬件结构实现的,有单独的波特率时钟源,可靠,一般优先选择这个使用。

另一种是mini串口(/dev/ttyS0),mini串口时钟源是由CPU内核时钟提供,波特率受到内核时钟的影响,不稳定。(自己认为可以理解成软串口,但是应该和 arduino 的软硬串口不是一回事)

使用串口通信自然是需要稳定的进行通信,这就需要修改串口的映射关系。

串口映射关系修改

serial0 是 GPIO 引脚对应的串口,srial1 是蓝牙对应的串口,默认未启用 serial0。首先使用 ls -l /dev/serial* 查看当前的映射关系:

2

这表明蓝牙串口 serial1 使用硬件串口 ttyAMA0。

  • 树莓派系统(或 Ubuntu 安装 raspi-config)下操作:

    修改映射关系这里树莓派使用 raspi-config 比较方便,鉴于我所使用的不是树莓派系统而是 Ubuntu,因此在这里将两种方法都讲一讲以便参考。

    首先安装 raspi-config,前往官网下载最新版安装包 下载地址:点击连接,选择最新版deb格式安装包下载。

    提取下载链接,使用 wget 直接下载到树莓派中,或者也可以电脑上下载完传过去。

    1
    wget http://archive.raspberrypi.org/debian/pool/main/r/raspi-config/raspi-config_20200504_all.deb

    然后执行安装命令:

    1
    dpkg -i raspi-config_20200504_all.deb

    根据提示安装所需依赖:

    1
    apt install lua

    如果还有其他问题就缺啥装啥即可,依赖安装完成后重新执行如上安装命令。

    安装完成使用 sudo raspi-config 进入配置图形界面,选择 Interfacing Options,再点击 Serial 选项。

    第一个选项(would you like a login shell to be accessible over serial?)选择 NO,第二个选项(would you like the serial port hardware to be enabled?)选择 YES。

    保存后重启,查看映射关系会发现多了一个 gpio 串口 serial0 ,并且使用的是 ttyS0。

    3

    以上步骤相当于是开启了 GPIO 的串口功能,但是使用的还是软件串口,不是硬件串口。

    下面先禁用蓝牙功能:

    1
    sudo systemctl disable hciuart

    在 /boot/config.txt 文件末尾添加一行代码 dtoverlay=pi3-miniuart-bt。

    保存后重启查看映射关系,若调换成功如下所示:

    4

  • Ubuntu下操作:

    通过命令查看支持的串口数:

    1
    dtoverlay -a | grep uart

    配置串口:

    1
    sudo vim /boot/firmware/usercfg.txt

    添加如下内容,保存,重启:

    1
    2
    3
    4
    dtoverlay=uart2
    dtoverlay=uart3
    dtoverlay=uart4
    dtoverlay=uart5

    如果没有enable_uart = 1这行代码,也要加上,并且如果有enable_uart = 0这行代码,需要注释掉。

    输入指令 ls /dev/ttyAMA*,若出现以下内容说明配置成功:

    1
    /dev/ttyAMA0         /dev/ttyAMA1          /dev/ttyAMA2          /dev/ttyAMA3        /dev/ttyAMA4

    以上是开启串口的操作,改变映射关系还和上面树莓派系统下的操作相同。

禁用串口的控制台功能

前面已经对映射关系进行了修改,但是现在还不能用树莓派与电脑进行通信(和单片机好像可以),因为树莓派 gpio 口引出串口默认是用来做控制台使用的,即是为了用串口控制树莓派,而不是通信。所以我们要禁用此默认设置。

首先执行命令如下:

1
2
sudo systemctl stop serial-getty@ttyAMA0.service
sudo systemctl disable serial-getty@ttyAMA0.service

然后执行命令修改文件:

1
sudo nano /boot/cmdline.txt

并删除语句 console=serial0,115200。(没有则不需要)

测试检验

首先安装以下开发工具:

1
2
3
sudo apt-get install wiringpi
sudo apt-get install python-serial
sudo apt-get install minicom

c语言下使用 wiringPi 库, python语言下使用 serial 包,最后命令行使用 minicom 工具。

  • minicom 调试:

    使用 minicom -b 115200 -D /dev/ttyAMA0 进入串口调试界面,这里将一直等待接收,直到用户手动退出。退出时 ctrl+A,再按键 X 退出。
    minicom 调试界面默认是不显示用户输入,使用 cttl+A,再按 E 即可开启(会捕获换行)。

  • c 语言调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <wiringPi.h>
#include <wiringSerial.h>

int main()
{
int fd;
if(wiringPiSetup()<0) {
return 1;
}

//if((fd=serialOpen("/dev/ttyS0",115200))<0) { // gpio 使用mini串口
if((fd=serialOpen("/dev/ttyAMA0",115200))<0) { // gpio 使用硬件串口
return 1;
}

printf("serial test output ...\n");
serialPrintf(fd,"1234567890abcdef");

serialClose(fd);
return 0;
}

编译 gcc test.c -o test -lwiringPi,运行 sudo ./test

  • python 调试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*
import serial
import time

ser = serial.Serial("/dev/ttyAMA0",115200)

if not ser.isOpen():
print("open failed")
else:
print("open success: ")
print(ser)

try:
while True:
count = ser.inWaiting()
if count > 0:
recv = ser.read(count)
print("recv: " + recv)
ser.write(recv)
sleep(0.05)
except KeyboardInterrupt:
if ser != None:
ser.close()

一些要点

最基础的发送数据的方式:

1
2
3
4
import serial
ser = serial.Serial("/dev/ttyUSB0", 115200)
ser.flushInput() # 清除缓存
ser.write("LightningMaster\r\n".encode()) # 发送数据 \r\n可以实现换行 encode()默认是'utf-8'

发送中文:

1
2
3
4
import serial
ser = serial.Serial("/dev/ttyUSB0", 115200)
ser.flushInput() # 清除缓存
ser.write("你好\r\n".encode('gb2312')) # 发送数据 \r\n可以实现换行

发送十六进制:

1
2
3
4
5
6
import serial
import struct
ser = serial.Serial("/dev/ttyUSB0", 115200)
ser.flushInput() # 清除缓存
pack = struct.pack('BBBB', 0xaa, 6, 7, 0x55) # 将数据打包 格式是unsigned char
ser.write(pack)

接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
ser.read(num) # 读取收到的num个字节的数据
ser.inWaiting() # 可以获取还未读出的数据
'''
import serial
import struct
import time
ser = serial.Serial("/dev/ttyUSB0", 115200)
ser.flushInput() # 清除缓存
while True:
count = ser.inWaiting() # 获取还有多少字符未读
if count != 0:
data = ser.read(count) # 读取数据存到data中
print(data) # 打印接受到的数据
time.sleep(0.1) # 系统等待

如下内容可以把 b’’ 去掉:

1
print(data.decode('utf-8'))

接收中文需要使用下面的代码:

1
print(data.decode('gb2312'))

USB 串口通讯

USB 串口通讯理论上来说是插上就能发送的,但是大概是由于系统问题,搭载 Ubuntu 的树莓派在刚插上 USB 串口时无法使用,为此进行了很多查阅,大致得出了结论:是 brltty 占用了 USB 的设备号,至于 brltty,貌似是什么驱动盲文显示器程序,总之运行 sudo apt remove brltty 删掉就可以。

lsusb 可以查看连在本机的 USB 设备列表,如果这里有 CH-340 那就证明系统可以正确识别,不用担心是驱动的问题。

然后通过输入命令 ls /dev/tty* 以及插拔判断哪个串口号,如果没有变化,就是前面的占用问题。

注意先在 minicom 里面进行调试,调试好再去程序里验证,因为程序有可能会出错,进而导致对错误原因的误判。

如果发现只能发送不能接收,那是权限的问题,更改对应串口权限如下指令:

1
sudo chmod 777 /dev/ttyUSB0

结语

至此,关于内网穿透及串口配置的全部内容就记录完毕,当然过程中可能还有其他的问题没有涉及,绝大多数内容都是在部署过程查得资料的一个总体汇总,并非原创。

过程中有许多问题是我无法解决的,都需要查询相关指令进行解决,大部分的时间也浪费在这上面,但是我觉得如果能够对操作系统和通信编程有更深入的学习,这些问题解决起来就要快很多。

知道问题发生的原因或者可能的解决方向远比知道具体某个问题对应的指令是什么重要,只能说要学的很多,现在还只是照猫画虎。

关于 ros2 的部分算是自己写的,但是程序很简便,并且没有很成熟,就暂时不放在记录里,等到以后熟练掌握以后在对 ros2 进行记录。

文章参考来源(非全部)

树莓派4B安装CH340驱动

ubuntu22.04的 brltty 导致 USB 转串口连接失败

树莓派固定ttyUSB*端口

树莓派使用USB串口通信 CH340

树莓派串口通信 USB串口通信

腾讯云frp连接失败

阿里云服务器实现frp内网穿透

使用frp配置内网穿透

使用frp配置内网访问(穿透)教程(超详细,简单)

Raspberry树莓派+服务器+frp实现内网穿透进行远程实时视频