first commit
This commit is contained in:
298
ck/serial_protocol.py
Normal file
298
ck/serial_protocol.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
串口通信协议实现
|
||||
支持两台设备之间可靠的数据传输
|
||||
|
||||
协议帧格式:
|
||||
+------+------+--------+------+--------+------+
|
||||
| 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)
|
||||
Reference in New Issue
Block a user