299 lines
8.1 KiB
Python
299 lines
8.1 KiB
Python
"""
|
|
串口通信协议实现
|
|
支持两台设备之间可靠的数据传输
|
|
|
|
协议帧格式:
|
|
+------+------+--------+------+--------+------+
|
|
| HEAD | LEN | CMD | DATA | CRC | TAIL |
|
|
+------+------+--------+------+--------+------+
|
|
| 0xAA | 2B | 1B | N字节 | 2B | 0x55 |
|
|
+------+------+--------+------+--------+------+
|
|
|
|
HEAD: 帧头标识 (1字节, 0xAA)
|
|
LEN: 数据长度 (2字节, 大端序, 包含CMD+DATA)
|
|
CMD: 命令字 (1字节)
|
|
DATA: 数据内容 (N字节)
|
|
CRC: CRC16校验 (2字节, 大端序)
|
|
TAIL: 帧尾标识 (1字节, 0x55)
|
|
"""
|
|
|
|
import serial
|
|
import struct
|
|
from enum import IntEnum
|
|
from typing import Optional, Callable
|
|
import threading
|
|
|
|
|
|
class Command(IntEnum):
|
|
"""命令类型定义"""
|
|
HEARTBEAT = 0x01 # 心跳包
|
|
DATA_QUERY = 0x02 # 数据查询
|
|
DATA_RESPONSE = 0x03 # 数据响应
|
|
CONTROL = 0x04 # 控制命令
|
|
ACK = 0x05 # 应答
|
|
NACK = 0x06 # 否定应答
|
|
|
|
|
|
class SerialProtocol:
|
|
"""串口协议类"""
|
|
|
|
# 协议常量
|
|
FRAME_HEAD = 0xAA
|
|
FRAME_TAIL = 0x55
|
|
# 最小帧长度: HEAD(1) + LEN(2) + CMD(1) + CRC(2) + TAIL(1)
|
|
MIN_FRAME_LEN = 7
|
|
|
|
def __init__(self, port: str, baudrate: int = 115200,
|
|
timeout: float = 1.0):
|
|
"""
|
|
初始化串口协议
|
|
|
|
Args:
|
|
port: 串口名称 (如 'COM1', '/dev/ttyUSB0')
|
|
baudrate: 波特率
|
|
timeout: 超时时间(秒)
|
|
"""
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.timeout = timeout
|
|
self.serial = None
|
|
self.running = False
|
|
self.receive_thread = None
|
|
self.receive_callback: Optional[Callable] = None
|
|
|
|
def open(self) -> bool:
|
|
"""打开串口"""
|
|
try:
|
|
self.serial = serial.Serial(
|
|
port=self.port,
|
|
baudrate=self.baudrate,
|
|
bytesize=serial.EIGHTBITS,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
timeout=self.timeout
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
print(f"打开串口失败: {e}")
|
|
return False
|
|
|
|
def close(self):
|
|
"""关闭串口"""
|
|
self.stop_receive()
|
|
if self.serial and self.serial.is_open:
|
|
self.serial.close()
|
|
|
|
@staticmethod
|
|
def calc_crc16(data: bytes) -> int:
|
|
"""
|
|
计算CRC16校验值 (CRC-16/MODBUS)
|
|
|
|
Args:
|
|
data: 需要计算校验的数据
|
|
|
|
Returns:
|
|
CRC16校验值
|
|
"""
|
|
crc = 0xFFFF
|
|
for byte in data:
|
|
crc ^= byte
|
|
for _ in range(8):
|
|
if crc & 0x0001:
|
|
crc = (crc >> 1) ^ 0xA001
|
|
else:
|
|
crc >>= 1
|
|
return crc
|
|
|
|
def build_frame(self, cmd: int, data: bytes = b'') -> bytes:
|
|
"""
|
|
构建数据帧
|
|
|
|
Args:
|
|
cmd: 命令字
|
|
data: 数据内容
|
|
|
|
Returns:
|
|
完整的数据帧
|
|
"""
|
|
# 计算长度 (CMD + DATA)
|
|
length = 1 + len(data)
|
|
|
|
# 构建帧体 (不含CRC和TAIL)
|
|
frame_body = (struct.pack('>BHB', self.FRAME_HEAD, length, cmd)
|
|
+ data)
|
|
|
|
# 计算CRC (对HEAD+LEN+CMD+DATA进行校验)
|
|
crc = self.calc_crc16(frame_body)
|
|
|
|
# 添加CRC和TAIL
|
|
frame = frame_body + struct.pack('>HB', crc, self.FRAME_TAIL)
|
|
|
|
return frame
|
|
|
|
def parse_frame(self, frame: bytes) -> Optional[dict]:
|
|
"""
|
|
解析数据帧
|
|
|
|
Args:
|
|
frame: 接收到的数据帧
|
|
|
|
Returns:
|
|
解析结果字典 {'cmd': 命令字, 'data': 数据}, 失败返回None
|
|
"""
|
|
if len(frame) < self.MIN_FRAME_LEN:
|
|
return None
|
|
|
|
# 检查帧头和帧尾
|
|
if frame[0] != self.FRAME_HEAD or frame[-1] != self.FRAME_TAIL:
|
|
return None
|
|
|
|
try:
|
|
# 解析长度
|
|
length = struct.unpack('>H', frame[1:3])[0]
|
|
|
|
# 检查帧长度是否匹配
|
|
# HEAD + LEN + (CMD+DATA) + CRC + TAIL
|
|
expected_len = 1 + 2 + length + 2 + 1
|
|
if len(frame) != expected_len:
|
|
return None
|
|
|
|
# 解析命令字
|
|
cmd = frame[3]
|
|
|
|
# 提取数据
|
|
data = frame[4:4+length-1]
|
|
|
|
# 提取CRC
|
|
received_crc = struct.unpack('>H', frame[-3:-1])[0]
|
|
|
|
# 计算CRC并校验
|
|
calc_crc = self.calc_crc16(frame[:-3])
|
|
if received_crc != calc_crc:
|
|
print(f"CRC校验失败: 接收={received_crc:04X}, "
|
|
f"计算={calc_crc:04X}")
|
|
return None
|
|
|
|
return {'cmd': cmd, 'data': data}
|
|
|
|
except Exception as e:
|
|
print(f"解析帧失败: {e}")
|
|
return None
|
|
|
|
def send_frame(self, cmd: int, data: bytes = b'') -> bool:
|
|
"""
|
|
发送数据帧
|
|
|
|
Args:
|
|
cmd: 命令字
|
|
data: 数据内容
|
|
|
|
Returns:
|
|
发送是否成功
|
|
"""
|
|
if not self.serial or not self.serial.is_open:
|
|
print("串口未打开")
|
|
return False
|
|
|
|
try:
|
|
frame = self.build_frame(cmd, data)
|
|
self.serial.write(frame)
|
|
return True
|
|
except Exception as e:
|
|
print(f"发送失败: {e}")
|
|
return False
|
|
|
|
def receive_frame(self) -> Optional[dict]:
|
|
"""
|
|
接收一帧数据 (阻塞式)
|
|
|
|
Returns:
|
|
解析结果字典或None
|
|
"""
|
|
if not self.serial or not self.serial.is_open:
|
|
return None
|
|
|
|
try:
|
|
# 等待帧头
|
|
while True:
|
|
byte = self.serial.read(1)
|
|
if not byte:
|
|
return None
|
|
if byte[0] == self.FRAME_HEAD:
|
|
break
|
|
|
|
# 读取长度字段
|
|
len_bytes = self.serial.read(2)
|
|
if len(len_bytes) != 2:
|
|
return None
|
|
|
|
length = struct.unpack('>H', len_bytes)[0]
|
|
|
|
# 读取剩余数据: CMD + DATA + CRC + TAIL
|
|
remaining = self.serial.read(length + 3)
|
|
if len(remaining) != length + 3:
|
|
return None
|
|
|
|
# 重组完整帧
|
|
frame = bytes([self.FRAME_HEAD]) + len_bytes + remaining
|
|
|
|
# 解析帧
|
|
return self.parse_frame(frame)
|
|
|
|
except Exception as e:
|
|
print(f"接收失败: {e}")
|
|
return None
|
|
|
|
def start_receive(self, callback: Callable[[int, bytes], None]):
|
|
"""
|
|
启动接收线程
|
|
|
|
Args:
|
|
callback: 回调函数 callback(cmd, data)
|
|
"""
|
|
if self.running:
|
|
return
|
|
|
|
self.receive_callback = callback
|
|
self.running = True
|
|
self.receive_thread = threading.Thread(
|
|
target=self._receive_loop, daemon=True)
|
|
self.receive_thread.start()
|
|
|
|
def stop_receive(self):
|
|
"""停止接收线程"""
|
|
self.running = False
|
|
if self.receive_thread:
|
|
self.receive_thread.join(timeout=2.0)
|
|
|
|
def _receive_loop(self):
|
|
"""接收循环"""
|
|
while self.running:
|
|
result = self.receive_frame()
|
|
if result and self.receive_callback:
|
|
try:
|
|
self.receive_callback(result['cmd'], result['data'])
|
|
except Exception as e:
|
|
print(f"回调函数执行失败: {e}")
|
|
|
|
def send_heartbeat(self) -> bool:
|
|
"""发送心跳包"""
|
|
return self.send_frame(Command.HEARTBEAT)
|
|
|
|
def send_data(self, data: bytes) -> bool:
|
|
"""发送数据"""
|
|
return self.send_frame(Command.DATA_RESPONSE, data)
|
|
|
|
def send_control(self, control_code: int,
|
|
params: bytes = b'') -> bool:
|
|
"""发送控制命令"""
|
|
data = bytes([control_code]) + params
|
|
return self.send_frame(Command.CONTROL, data)
|
|
|
|
def send_ack(self) -> bool:
|
|
"""发送应答"""
|
|
return self.send_frame(Command.ACK)
|
|
|
|
def send_nack(self) -> bool:
|
|
"""发送否定应答"""
|
|
return self.send_frame(Command.NACK)
|