【车联网原型系统|三】树莓派设计+模拟基站程序
物联网原型系统导航
【车联网原型系统|一】项目介绍+需求分析+概要设计 https://blog.csdn.net/weixin_46291251/article/details/125807297
【车联网原型系统|二】数据库+应用层协议设计 https://blog.csdn.net/weixin_46291251/article/details/125808107
【车联网原型系统|三】树莓派设计+模拟基站程序 https://blog.csdn.net/weixin_46291251/article/details/125808229
【车联网原型系统|四】adhoc组网+frp内网穿透 https://blog.csdn.net/weixin_46291251/article/details/125808621
【车联网原型系统|五】前后端分离 https://blog.csdn.net/weixin_46291251/article/details/125808674
【车联网原型系统|六】效果展示 https://blog.csdn.net/weixin_46291251/article/details/125808845
【源码下载】 https://download.csdn.net/download/weixin_46291251/86227197
文章目录
【车联网原型系统|三】树莓派设计+模拟基站程序
树莓派(小车)设计
概述
小车主要包括以下几个模块:
数据传输模块、主控模块、传感器、避障模块、蜂鸣器模块和其他模块(日志、配置等),
下面是整个项目的结构图。
小车端的程序采用MVC的思想进行设计,由于不涉及数据库的操作,dao层主要是底层数据的获取(从GPIO引脚),controller层是主控模块的控制类,通过调用dao层的程序对上层提供服务(由于服务比较简单,没有设计service层),实体类有报文、指令、数据等。
实体类
报文
_src_id = "127.0.0.1"
_des_id = "127.0.0.1"
pac_type = 0
ctl_type = 0
_timestamp = "2022-05-13 18:22:35"
'''数据字段content具体是data还是instruction取决于_pac_type和_ctl_type'''
content = None
# _data = data()
# _instruction = instruction
'''一些常量'''
PI_TO_PC = 1 # 基站->树莓派
PC_TO_PI = 2 # 树莓派->基站
PI_TO_PI = 3 # 树莓派->树莓派
CAR_MOVE = 0 # 控制小车运动
CAR_STOP = 1 # 停止小车运动
BACK_HOME = 2 # 返回出发点
SENSOR_UP = 3 # 开启传感器
SENSOR_DOWN = 4 # 关闭传感器
ROUTER = 5 # 转发报文
INIT_CONF = 6 # 初始化 / 修改配置信息
指令
"""ins_type表示指令的种类"""
ins_type = -1
direction = [] # 四个按钮的状态
timestamp = -1 # 指令创建的时间戳,解析报文的时候直接用packet内的即可(主要是run内入栈使用)
# duration = -1 # 指令执行时长
speed = 50
diff_speed = 20 # 转向时 两侧轮胎的差速
sensor_list = []
# todo:
""" """
CAR_MOVE = 0 # 控制小车运动
CAR_STOP = 1 # 停止小车运动
BACK_HOME = 2 # 返回出发点
SENSOR_UP = 3 # 开启传感器
SENSOR_DOWN = 4 # 关闭传感器
ROUTER = 5 # 转发报文
SIG_INIT_STOP = 6 # ctrl-c手动停止
DISCONECT_STOP = 7 # 断开
数据
日志
日志模块用于输出小车相应其他的模块的执行记录。
避障模块
这个模块主要由三个部分组成:
1)红外 2)超声波 3)舵机
红外传感器由左右两个组成,调整灵敏度使得大约在20cm左右的时候开始避障。
超声波和舵机两个硬件组合使用,用来判断前、左、右三个方向的距离。
红外部分
这部分主要是判断左右是否过近
超声波dao
超声波主要是根据超声波和发射和接收时间差来计算距离
舵机dao
舵机根据指定等角度来移动
超声波舵机组合控制器
这部分主要是调用超声波和舵机两个硬件,获取指定方向上的距离。
蜂鸣器模块
dao
首先是dao层:
根据给定的频率、间隔等调用蜂鸣器硬件来发声
Controller
Controller层封装了几种格式化的声音,比如网络连接、网络断开等。
环境传感器
主要是从指定的引脚获取数据,由于树莓派的GPIO没有ADC能力,所以需要使用拓展版的ADC模块将读取到的模拟信号转换成数字信号并且返回给上层调用者
。
移动模块
移动模块主要是三个功能:移前后移动、原地左右转弯,前后的同时左右转弯、停止、一键返航等功能。
moveDao
moveDao主要接收底层指令(左右侧的速度,时间),根据指令直接对小车进行控制。
左右两侧分开控制,左侧两个轮胎为一组,和右边独立开,方向和速度都独立控制。
moveController
通过更顶层的指令对小车进行控制,完成不同的运动方式。
接收的指令是(按键状态,时状态间,速度,转弯差速等)
1)按键状态是一个四元组,分别对应前后左右四个方向对应按键的按下状态
2)允许0-4个按键被按下。
3)速度指的是小车正常允许的速度。
4)差速指的是转弯时,左右侧轮胎的差速。
通信模块
通信模块用于与后(基站)端进行通信,互相传输数据。共包含四个方面的功能:接收信息、发送信息、安全通信和转发信息。
接收
当小车接收到服务器的数据包时,需要对数据包进行解析。解析出数据包的类型,再调用相应的模块进行处理。
发送
小车需要实时发送运动轨迹信息和传感器检测到的环境数据
这两种数据都是定时自动发送的,环境数据在一个定时周期内采集一次,路径数据在每次调用移动模块时进行记录,并随着环境数据一起发送给后端,发送成功之后就清楚历史轨迹数据。
安全
由于系统中存在报文转发的情况,报文使用明文传播就会有泄漏的风险,所以要对报文进行加密处理。在本系统中,报文的所有数据都使用DES算法进行加密,因为DES加密可以在很大程度上保证安全性,并且性能好,加密、解密速度块。
具体而言,每个树莓派都绑定了一个长为64比特的密钥,在树莓派和后端服务器中,都存储了该密钥,即密钥进行了物理上的分发。
主控模块
主程序用来调用其他模块
主要分为以下三个部分
1)运动线程
线程循环接收来自基站的报文,并且从报文中提取出指令。然后根据不同的指令用不同的模块完成任务。具体的,这里需要调用moveController来指导小车的运动,sensorController来开关传感器。
2)反馈数据线程
这个线程根据指定的时间间隔定时执行任务,每个周期获取一次环境数据,填充到报文内的环境数据字段。然后将当前周期内小车的实际运行轨迹填充到轨迹字段。然后将报文打包发送给基站。
3)避障线程
这个线程循环调用红外和超声波传感器,获取前方距离和左右是否安全,如果前方距离过近,那么再让舵机转头测量左右方向的距离,然后让小车移动到安全的方向。
模拟基站程序(测试用)
import socket
from pi.config import *
import threading
import json
from pynput.keyboard import Listener
import time
# 监测按键:https://blog.csdn.net/qq_44741568/article/details/97895880
"""
这里模拟一下基站,发送指令,接收数据
ADSW四个按键充当方向键进行操作
"""
_dir = [False, False, False, False] # 前后左右四个按钮
new_sock = socket.socket
def on_press(key):
global _dir
_dir_old = _dir.copy()
if "'w'" == str(key):
print("\r按下--前进", end="")
_dir[0] = True
if "'s'" == str(key):
print("\r按下--后退", end="")
_dir[1] = True
if "'a'" == str(key):
print("\r按下--左转", end="")
_dir[2] = True
if "'d'" == str(key):
print("\r按下--右转", end="")
_dir[3] = True
if _dir != _dir_old:
my_send(_dir)
def on_release(key):
global _dir
_dir_old = _dir.copy()
if "'w'" == str(key):
print("松开--前进")
_dir[0] = False
if "'s'" == str(key):
print("松开--后退")
_dir[1] = False
if "'a'" == str(key):
print("松开--左转")
_dir[2] = False
if "'d'" == str(key):
print("松开--右转")
_dir[3] = False
if _dir != _dir_old:
my_send(_dir)
def my_send(dir_list):
print("send", dir_list)
packet = {
"src_id": "192.168.34.53",
"dst_id": "192.168.34.53",
"pac_type": 2,
"ctl_type": 0,
"timestamp": "2022-05-13 18:22:35",
"content": {
"direction": [False, False, False, False],
"speed": 70,
"diff_speed": 20,
}
}
packet["content"]["direction"] = dir_list
global new_sock
new_sock.send((json.dumps(packet)).encode('utf-8'))
def service_thread_send(_sock: socket.socket):
"""
收到新的连接,先判断该连接的类型,然后交给对应的函数处理
:param _sock: 收发报文对应的socket
:return:
"""
with Listener(on_press=on_press, on_release=on_release) as listener:
listener.join()
def service_thread_recv(_sock: socket.socket):
while True:
data_bin = _sock.recv(1024)
if data_bin == b"": # 断开
print("断开连接")
_sock.close()
return
print("收到数据:", json.loads(data_bin.decode("utf-8")))
if __name__ == '__main__':
s_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建套接字
s_sock.bind((addr_BaseStation_1, port_BaseStation_1))
s_sock.listen(5) # socket的”排队个数“为5 !!!不是最大连接数
while True:
print("listening: ", s_sock)
new_sock, adress = s_sock.accept() # 这条命令是阻塞的
print("连接成功")
threading.Thread(target=service_thread_recv, args=(new_sock,)).start() # 收到新的请求就直接投入新的线程并且开始运行
threading.Thread(target=service_thread_send, args=(new_sock,)).start() # 收到新的请求就直接投入新的线程并且开始运行