1. 概要
- 了解现场网感知与控制的基本方法
- 掌握基于RS232的协议设计与通信方法在此基础上实现对现场设备状态数据的采集、对设备的控制。
- 针对快递柜系统设计实现一个对现场快递柜状态数据采集、显示、参数设置、抽屉打开、保鲜控制等功能软件系统。
2.成果展示
-
抽屉控制
-
参数设置
-
温度感知
相关说明
- 使用python+QT实现
- 总体设计参考这位博主的博客传送门
- 开发过程中遇到许多小问题,这里就不一一列举啦(遇到问题可以私信我讨论)
- UI文件我这里就不转成python代码发出来了(因为很长又没什么意义,需要的可以私信我)
- 废话不多说,直接上代码
20230706更新
源代码我放在阿里云盘了点我
主程序
control.py
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from send import *
from receive import *
from data_struct import *
from PySide2.QtUiTools import QUiLoader
from PySide2.QtGui import QIcon, QColor
import time # 时间模块
import threading # 线程模块
import serial # 串口模块
import ctypes
import inspect
import datetime
import pyqtgraph as pg
serial_port = 38400
port = 'COM2'
# 串口配置
com = serial.Serial(port, serial_port)
class MySignal(QObject):
status_text_print = Signal(QTextBrowser, str)
def async_raise(tid, exc_type):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exc_type):
exc_type = type(exc_type)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exc_type))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread): # 强制关闭线程
async_raise(thread.ident, SystemExit)
def status_show(temp_current, temp_msg):
temp_current.setText(temp_msg)
def compressor_btn_off_clicked():
print('压缩机关闭....')
send_compressor_control_frame(com, False)
def thread_up(com):
receive_data(com)
def receive(): # 接收数据线程
receive_thread = threading.Thread(target=lambda: thread_up(com))
receive_thread.setDaemon(True)
receive_thread.start()
return receive_thread
class Software:
def __init__(self):
# 从文件中加载UI定义
loader = QUiLoader()
loader.registerCustomWidget(pg.PlotWidget)
self.ui = QUiLoader().load('control.ui')
# 设置窗口icon
self.ui.setWindowIcon(QIcon('icon.ico'))
# 设置不可用
self.set_enable(False)
self.ms = MySignal()
self.ms.status_text_print.connect(status_show)
# 线程
self.thread_flash_ui = None
self.thread_receive_data = None
# 保存接收到的温度数据
self.temp_now_list = []
# 时间线
self.time_line_list = []
# 窗口渲染
self.plot_widget = self.ui.widget
self.plot_widget.setBackground('#172C4B')
self.plot_widget.setLabel('left', '温度℃')
self.plot_widget.setLabel('bottom', '时间/s')
# 按钮绑定函数
self.ui.sys_on_btn.clicked.connect(self.sys_btn_on_clicked)
self.ui.sys_off_btn.clicked.connect(self.sys_btn_off_clicked)
self.ui.compressor_on_btn.clicked.connect(self.compressor_btn_on_clicked)
self.ui.compressor_off_btn.clicked.connect(compressor_btn_off_clicked)
self.ui.box_btn1.clicked.connect(lambda: self.box_btn_on_clicked(1))
self.ui.box_btn2.clicked.connect(lambda: self.box_btn_on_clicked(2))
self.ui.box_btn3.clicked.connect(lambda: self.box_btn_on_clicked(3))
self.ui.box_btn4.clicked.connect(lambda: self.box_btn_on_clicked(4))
self.ui.box_btn5.clicked.connect(lambda: self.box_btn_on_clicked(5))
self.ui.box_btn6.clicked.connect(lambda: self.box_btn_on_clicked(6))
self.ui.box_btn7.clicked.connect(lambda: self.box_btn_on_clicked(7))
self.ui.box_btn8.clicked.connect(lambda: self.box_btn_on_clicked(8))
self.ui.box_btn9.clicked.connect(lambda: self.box_btn_on_clicked(9))
self.ui.box_btn10.clicked.connect(lambda: self.box_btn_on_clicked(10))
self.ui.submit_btn.clicked.connect(self.submit_btn_clicked)
def sys_btn_on_clicked(self):
print('系统开启....')
self.thread_flash_ui = self.flash_ui()
self.thread_receive_data = receive()
self.ui.sys_status_show.setText('运行中')
self.ui.compressor_status_show.setText('关闭')
self.set_enable(True)
self.ui.sys_on_btn.setEnabled(False)
# 如果串口未开启,则开启串口
if not com.isOpen():
com.open()
def sys_btn_off_clicked(self):
# 判断界面上的按钮是否为开启状态,如果是,则发送关闭帧
# 串口关闭
self.set_enable(False)
self.ui.sys_on_btn.setEnabled(True)
# 关闭线程
stop_thread(self.thread_flash_ui)
stop_thread(self.thread_receive_data)
self.ui.sys_status_show.setText('关闭')
self.ui.compressor_status_show.setText('停止')
self.ui.current_temp_show.setText('None')
send_compressor_control_frame(com, False)
def compressor_btn_on_clicked(self):
print('压缩机开启....')
# 2.1 发送压缩机控制帧
send_compressor_control_frame(com, True)
# 压缩机开启按钮不可用
self.ui.compressor_on_btn.setEnabled(False)
def box_btn_on_clicked(self, num):
# 点击按钮之后,将按钮上的文本修改成“开启”,如果原来是“开启”,则修改成“关闭”
btn = self.ui.groupBox_box.findChild(QPushButton, 'box_btn{}'.format(num))
if btn.text() == '开启':
btn.setText('关闭')
else:
btn.setText('开启')
# 获取所有按钮上的内容,存放到列表中
mylist = []
for btn in self.ui.groupBox_box.children():
if isinstance(btn, QPushButton) and btn.text() != '确定':
mylist.append(btn.text())
# 如果是开启状态,则替换成1,否则替换成0
if btn.text() == '开启':
mylist[-1] = 1
else:
mylist[-1] = 0
print(f"抽屉状态{mylist}")
# 3. 发送开锁帧
send_unlock_frame(com, mylist)
def submit_btn_clicked(self):
# 设置默认控制面板数据
# 遍历所有文本框,如果无输入,则使用默认值
for text in self.ui.groupBox_sys.children():
# 如果按钮是QTextEdit类型
if isinstance(text, QTextEdit):
# 如果文本框有输入,则使用输入值
if text.toPlainText():
# 如果是设置温度,则将输入值转换成浮点型
if text.objectName() == 'set_temp':
data_control_set['set_temp'] = float(text.toPlainText())
# 如果是设备代码,则将输入值字节化
elif text.objectName() == 'dev_code':
data_control_set['dev_code'] = int(text.toPlainText(), 16).to_bytes(5, 'big')
# print(text.toPlainText().encode('utf-8'))
else:
data_control_set[text.objectName()] = int(text.toPlainText())
# print('设置参数:{}'.format(data_control_set))
# 4. 发送设置温度帧
send_set_temperature_frame(com, data_control_set['set_temp'])
# print(f"设置的温度是{data_control_set['set_temp']},类型是{type(data_control_set['set_temp'])}")
# 5. 发送设置参数帧
send_set_parameter_frame(com, data_control_set)
# 6. 发送温度控制偏差
send_set_temperature_control_deviation_frame(com, data_control_set['temp_control'])
# 7. 发送设置设备地址帧
send_set_device_address_frame(com, data_control_set['dev_code'])
# 控件可用性全局设置
def set_enable(self, enable):
# if enable:
# self.ui.box_submit_btn.setEnabled(enable)
self.ui.submit_btn.setEnabled(enable)
self.ui.compressor_on_btn.setEnabled(enable)
self.ui.compressor_off_btn.setEnabled(enable)
self.ui.sys_off_btn.setEnabled(enable)
for btn in self.ui.groupBox_box.children():
# 如果按钮是QPushButton类型
if isinstance(btn, QPushButton) and btn.text() != '确定':
btn.setEnabled(enable)
btn.setText('关闭')
for text in self.ui.groupBox_sys.children():
# 如果按钮是QTextEdit类型
if isinstance(text, QTextEdit):
text.setEnabled(enable)
text.setText('')
# 强制关闭线程
def flash_ui(self): # 界面刷新线程
flash_thread = threading.Thread(target=self.flash)
flash_thread.setDaemon(True)
flash_thread.start()
return flash_thread
def flash(self):
while True:
# 实时温度数据
self.temp_now_list.append(float(get_temp(control_table_status['current_temp'])))
self.time_line_list.append(datetime.datetime.now().strftime('%H:%M:%S'))
self.ms.status_text_print.emit(self.ui.current_temp_show, get_temp(control_table_status['current_temp']))
if control_table_status['compressor_status'] == 0x00:
self.ms.status_text_print.emit(self.ui.compressor_status_show, '停止')
elif control_table_status['compressor_status'] == 0x01:
self.ms.status_text_print.emit(self.ui.compressor_status_show, '预启动')
elif control_table_status['compressor_status'] == 0x02:
self.ms.status_text_print.emit(self.ui.compressor_status_show, '运行')
elif control_table_status['compressor_status'] == 0x03:
self.ms.status_text_print.emit(self.ui.compressor_status_show, '故障')
time.sleep(1)
self.drawing_on_panel() # 在界面上画图1s更新一次
# print(self.temp_now_list)
# print(self.time_line_list)
def drawing_on_panel(self):
if len(self.temp_now_list) > 13:
# 保留最近40个数据
self.temp_now_list = self.temp_now_list[-13:] # 浮点类型
self.time_line_list = self.time_line_list[-13:] # 字符串类型
y = self.temp_now_list
xax = self.plot_widget.getAxis('bottom') # 坐标轴x
ticks = [list(zip(range(len(self.time_line_list)), self.time_line_list))]
xax.setTicks(ticks)
pen = pg.mkPen({'color': (155, 200, 160), 'width': 4}) # 画笔设置
self.plot_widget.plot(y, clear=True, pen=pen, symbol='o', symbolBrush=QColor(113, 148, 116)) # 画图
app = QApplication([]) # 创建QApplication对象
status = Software() # 创建软件状态对象
status.ui.show() # 显示窗口
app.exec_() # 运行程序
数据帧发送
send.py
# 数据封装成帧
# 数据帧格式:
# 信息头(2B)+帧长(1B)+帧号(1B)[取值:1~255]+设备地址(1B)[1~120,0为上位机地址)]+功能号(1B)+数据(NB)+CRC校验(2B)+帧尾(2B)
# 帧头固定:0xFFFF
# 帧尾固定:0xFFF7
# 固定部分10B,可变部分为数据部分,帧长度为整个数据帧的长度
from struct import *
import serial
frame_num = 0 # type: int # 定义全局变量帧号
# todo: 1.发送查询帧(10byte)
def send_query_frame(com, addr=0x01):
res = com.write(query_frame(addr))
return res
# todo: 2.启停压缩机控制帧(11byte)
def send_compressor_control_frame(com, compressor_control_data, addr=0x01):
res = com.write(compressor_control_frame(addr, compressor_control_data))
return res
# todo: 3.开锁帧(12byte)
def send_unlock_frame(com, box_control_data, addr=0x01):
res = com.write(box_control_frame(addr, box_control_data))
return res
# todo: 4.设置温度帧(11byte)
def send_set_temperature_frame(com, set_temperature_data, addr=0x01):
print(f"我是设置温度方法{set_temperature_data}")
res = com.write(set_temperature_frame(addr, set_temperature_data))
return res
# todo: 5.设置参数帧(28byte)
def send_set_parameter_frame(com, data_control_set, addr=0x01):
res = com.write(set_parameter_frame(addr, data_control_set))
return res
# todo: 6.设置温度控制偏差帧(11byte)
def send_set_temperature_control_deviation_frame(com, temp_control_deviation, addr=0x01):
res = com.write(set_temperature_control_deviation_frame(addr, temp_control_deviation))
return res
# todo: 7.设置设备地址帧(16byte)
def send_set_device_address_frame(com, dev_code_data, addr=0x01):
res = com.write(set_device_address_frame(addr, dev_code_data))
return res
# 封装成帧
def packet_data_to_frame(addr, func_num, data): # type: (int, int, bytes) -> bytes
"""
:param addr: 16进制的设备地址,范围:OX1~OX78,类型为int
:param func_num: 功能码
:param data: bytes类型的数据
:return: 返回一个bytes类型的数据帧
"""
global frame_num
# print(f"设备地址{pack('B', addr)}")
frame_head = pack('2B', 255, 255) # 0xFFFF 帧头
frame_length = pack('B', len(data) + 10) # 帧长
num = pack('B', frame_num) # 帧号
address = pack('B', addr) # 设备地址
fc_num = pack('B', func_num) # 功能码
crc_check_bytes = frame_length + num + address + fc_num + data
crc_frame = pack('H', crc_check(crc_check_bytes)) # CRC校验位
frame_tail = pack('2B', 255, 247) # 0xFFF7 帧尾
# 封装
frame_packet = frame_head + frame_length + num + address + fc_num + data + crc_frame + frame_tail
frame_num = (frame_num + 1) % 256
return frame_packet
# 1. 查询帧(10Byte)
def query_frame(addr):
"""
:param addr: 16进制的设备地址,范围:OX1~OX78,类型为int
:return: 返回一个bytes类型的查询帧
"""
data = packet_data_to_frame(addr, 1, b'')
print(f"查询帧发送:{data},长度为:{len(data)}")
return data
# 2.启停压缩机控制帧(11Byte)
def compressor_control_frame(addr, compressor_control_data):
"""
:param addr: 16进制的设备地址,范围:OX1~OX78,类型为int
:param compressor_control_data: bool类型,True为启动,False为停止
:return:
"""
if compressor_control_data:
control_byte = pack('B', 1)
else:
control_byte = pack('B', 0)
data = packet_data_to_frame(addr, 2, control_byte)
# print(len(data))
print(f"启停压缩机控制帧发送:{data},长度为:{len(data)}")
return data
# 3.开锁帧(12Byte)
def box_control_frame(addr, box_control_data):
"""
:param addr:16进制的设备地址,范围:OX1~OX78
:param box_control_data: 抽屉控制数据列表[]
:return:返回一个12bytes的数据帧
"""
byte1 = 0
byte2 = 0
# 字节处理,方法:将box_control_data的前八位取出,将这8位作为一个整体,转成10进制
for i in range(8):
byte1 += box_control_data[i] * 2 ** i
for i in [8, 9]:
byte2 += box_control_data[i] * 2 ** (i - 8)
# 数据帧封装
# print(byte1.to_bytes(1, 'big') + byte2.to_bytes(1, 'big'))
data = packet_data_to_frame(addr, 3, byte1.to_bytes(1, 'big') + byte2.to_bytes(1, 'big'))
print(f"开锁帧发送:{data},长度为:{len(data)}")
# print(f"封装好的开锁帧:{data}")
return data
# 4. 设置温度帧(11Byte)
def set_temperature_frame(addr, set_temperature_data):
"""
:param addr: 16进制的设备地址,范围:OX01~OX78
:param set_temperature_data: 温度数据类型为int
:return: 返回一个11bytes的数据帧
"""
temp = temp_convert(set_temperature_data)
data = packet_data_to_frame(addr, 4, temp.to_bytes(1, 'big'))
# 将res转成bytes类型
print(f"设置温度帧发送:{data},长度为:{len(data)}")
return data
# 5. 设置参数帧(28Byte)
def set_parameter_frame(addr, set_parameter_data):
"""
:param addr: 16进制的设备地址,范围:OX01~OX78
:param set_parameter_data: 参数数据列表,类型为字典
:return: 返回一个28bytes的数据帧
"""
# 参数数据列表
dev_id = set_parameter_data['dev_code'] # 设备ID
# 判断dev_id是否是字符串
if isinstance(dev_id, str):
dev_id = int(dev_id, 16).to_bytes(5, 'big')
dev_addr = set_parameter_data['dev_addr'].to_bytes(1, 'big') # 设备地址
time_gap = set_parameter_data['time_gap'].to_bytes(1, 'big') # 时间间隔
compressor_delay = set_parameter_data['compressor_delay'].to_bytes(1, 'big') # 压缩机延时
set_temp = temp_convert((set_parameter_data['set_temp'])).to_bytes(1, 'big') # 设定温度
temp_control = set_parameter_data['temp_control'].to_bytes(1, 'big') # 温度控制
res = dev_id + dev_addr + b'\x00' + time_gap + compressor_delay + b'\x00\x00' + set_temp + temp_control \
+ b'\xFF\xFF\xFF\xFF\x00'
print(len(res), res)
data = packet_data_to_frame(addr, 5, res)
print(f"设置参数帧发送:{data},长度为:{len(data)}")
return data
# 6. 设置温度控制偏差帧(11Byte)
def set_temperature_control_deviation_frame(addr, set_temperature_control_deviation_data):
"""
:param addr: 16进制的设备地址,范围:OX01~OX78
:param set_temperature_control_deviation_data: int类型的数据,温控偏差值
:return:返回一个11bytes的数据帧
"""
data = packet_data_to_frame(addr, 6, set_temperature_control_deviation_data.to_bytes(1, 'big'))
print(f"设置温度控制偏差帧发送:{data},长度为:{len(data)}")
return data
# 7. 设置设备地址帧(16Byte)
def set_device_address_frame(addr, dev_code_data):
"""
:param addr: 设备地址
:param dev_code_data: 设备代码bytes类型
:return: 返回一个16bytes的数据帧
"""
# 如果dev_code_data是字符串,则转成bytes类型
if isinstance(dev_code_data, str):
dev_code_data = int(dev_code_data, 16).to_bytes(5, 'big')
data = packet_data_to_frame(addr, 7, dev_code_data + addr.to_bytes(1, 'big'))
print(f"设置设备地址帧发送:{data},长度为:{len(data)}")
return data
# crc16算法
def crc_check(data_bytes):
"""
:param data_bytes: bytes类型的字符串
:return: 返回一个int类型的校验值
"""
crc_16 = 0xFFFF
for pos in data_bytes:
# print(pos)
crc_16 ^= pos # 每一个数据与CRC寄存器进行异或
for i in range(len(data_bytes)): # 循环计算每一个数据
if (crc_16 & 1) != 0: # 判断最后一位是否为1
crc_16 >>= 1 # 右移一位
crc_16 ^= 0xA001 # 异或A001
else:
crc_16 >>= 1 # 右移一位
return eval(hex(((crc_16 & 0xff) << 8) + (crc_16 >> 8))) # 返回校验值
# 温度值处理
def temp_convert(set_temperature_data):
# 将set_temperature_data转成浮点数
set_temperature_data = float(set_temperature_data)
s = str(set_temperature_data).split('.')
add_bit1 = '0' # 整数部分的符号位7th
add_bit2 = '0' # 小数部分的符号位0th
if int(s[0]) < 0:
add_bit1 = '1'
num1 = abs(int(s[0])) # 整数部分,取绝对值
num2 = int(s[1]) # 小数部分
if num2 >= 5:
add_bit2 = '1'
# print(f"整数部分符号位{add_bit2}, 小数部分符号位{add_bit1}")
# 将num1转成2进制
res = bin(num1)
print(res)
res_list = list(res)
res_list.insert(2, add_bit1)
res_list.insert(len(res_list) + 1, add_bit2)
for i in range(10 - len(res_list)): # 补齐10位
res_list.insert(3, '0')
res = ''.join(res_list)
print(res_list)
res = int(res, 2)
return res
数据帧接收
receive.py
# 数据接收
import serial
from send import send_compressor_control_frame
from data_struct import *
# 定义状态帧字典
def receive_data(com): # 接收数据线程
while True:
# if soft_status.ui.sys_on_btn.isEnabled():
com_data = com.read(80)
res = receive_frame(com_data)
for r in res:
if r[2] == 0x2C:
control_table_attributes['dev_code'] = r[6:11]
control_table_attributes['addr'] = r[11]
control_table_attributes['time_gap'] = r[13]
control_table_attributes['compressor_delay'] = r[14]
control_table_attributes['temp_set'] = r[17]
control_table_attributes['temp_control'] = r[18]
print(f"control_table_attributes{control_table_attributes}")
control_table_status['dev_code'] = r[24:29]
control_table_status['sys_status'] = r[29]
control_table_status['compressor_status'] = r[31]
control_table_status['temp_set'] = r[32]
control_table_status['current_temp'] = r[33]
control_table_status['box_status'] = r[36:38]
print(f"control_table_status:{control_table_status}")
temp_now = get_temp(control_table_status['current_temp'])
print(f"当前温度:{temp_now}")
compressor_control(temp_now, com)
def get_temp(temp_now):
if temp_now > 128:
temp_convert = '{:0>8}'.format(str(bin(temp_now))[3:])
temp_convert = str(int(temp_convert[1:7], 2) + 1 / 2 * int(temp_convert[7], 2))
temp_convert = '-' + temp_convert
return temp_convert
else:
temp_convert = '{:0>8}'.format(str(bin(temp_now))[2:])
# print(temp_convert)
temp_convert = str(int(temp_convert[1:7], 2) + 1 / 2 * int(temp_convert[7], 2))
return temp_convert
# 压缩机控制
def compressor_control(temp_now, com):
# if soft_status.ui.compressor_btn_on_clicked.isEnabled():
# 如果温度低于当前值
# check_num = control_table_attributes['compressor_delay']
flag_temp1 = float(get_temp(control_table_status['temp_set'])) - control_table_attributes['temp_control'] + 1
flag_temp2 = float(get_temp(control_table_status['temp_set'])) + control_table_attributes['temp_control'] - 1
if float(temp_now) <= flag_temp1:
send_compressor_control_frame(com, False)
# print("关闭压缩机")
# 如果温度高于当前值
if float(temp_now) >= flag_temp2:
send_compressor_control_frame(com, True)
# print("打开压缩机")
# else:
# send_compressor_control_frame(com, False)
def receive_frame(hex_datas): # 匹配帧头和帧尾,获取匹配成功的帧
res_frame = []
for i in range(0, len(hex_datas) - 1):
if hex_datas[i] == 0xFF and hex_datas[i + 1] == 0xFF:
for j in range(i, len(hex_datas) - 1):
if hex_datas[j] == 0xFF and hex_datas[j + 1] == 0xF7:
result = hex_datas[i:j + 2]
if len(result) == 14 or len(result) == 44:
res_frame.append(result)
return res_frame # 返回匹配成功的帧(列表)
相关数据结构
data_struct.py
data_control_set = {
'dev_code': '0xFFFFFFFFFF', # type: bytes
'dev_addr': 0x01,
'time_gap': 0x01,
'compressor_delay': 0x1e,
'set_temp': 4.0,
'temp_control': 0x02,
}
control_table_status = {
'dev_code': b'\xFF\xFF\xFF\xFF\xFF', # 设备ID比特流
'sys_status': 0x00, # 二位十六进制数表示一个比特
'compressor_status': 0x00,
'temp_set': 0x00,
'current_temp': 0x00,
'box_status': b'\x00\x00',
}
control_table_attributes = {
'dev_code': b'\xFF\xFF\xFF\xFF\xFF',
'addr': 0x01,
'time_gap': 0x01,
'compressor_delay': 0x1E,
'temp_set': 0x00,
'temp_control': 0x02,
}