first commit
This commit is contained in:
25
ck/.gitignore
vendored
Normal file
25
ck/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Test
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
292
ck/README.md
Normal file
292
ck/README.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# 串口通信协议
|
||||
|
||||
一个完整的Python串口通信协议实现,支持两台设备之间可靠的数据传输,可传输结构化数据(用户名、线路名称、站点编号等)。
|
||||
|
||||
## 特性
|
||||
|
||||
- ✅ **完整的协议帧结构** - 包含帧头、长度、命令、数据、CRC校验、帧尾
|
||||
- ✅ **CRC16校验** - 使用CRC-16/MODBUS算法确保数据完整性
|
||||
- ✅ **异步接收** - 多线程接收,不阻塞主程序
|
||||
- ✅ **多种命令类型** - 心跳、数据传输、控制命令、应答等
|
||||
- ✅ **结构化数据** - 支持JSON格式的站点数据传输(用户名、线路名称、站点编号)
|
||||
- ✅ **易于使用** - 简洁的API,开箱即用
|
||||
|
||||
## 协议帧格式
|
||||
|
||||
```
|
||||
+------+------+--------+------+--------+------+
|
||||
| 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)
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 命令类型
|
||||
|
||||
| 命令 | 值 | 说明 |
|
||||
| ----------- | ---- | -------------- |
|
||||
| HEARTBEAT | 0x01 | 心跳包 |
|
||||
| DATA_QUERY | 0x02 | 数据查询 |
|
||||
| DATA_RESPONSE | 0x03 | 数据响应 |
|
||||
| CONTROL | 0x04 | 控制命令 |
|
||||
| ACK | 0x05 | 应答 |
|
||||
| NACK | 0x06 | 否定应答 |
|
||||
|
||||
## 站点数据模型
|
||||
|
||||
### StationData 类
|
||||
|
||||
用于传输站点信息的数据模型:
|
||||
|
||||
```python
|
||||
from data_models import StationData
|
||||
|
||||
# 创建站点数据
|
||||
station = StationData(
|
||||
username="张三", # 用户名
|
||||
line_name="1号线", # 线路名称
|
||||
station_no=5 # 第几站
|
||||
)
|
||||
|
||||
# 序列化为字节(用于发送)
|
||||
data_bytes = station.to_bytes()
|
||||
|
||||
# 从字节反序列化(接收后解析)
|
||||
restored = StationData.from_bytes(data_bytes)
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本示例
|
||||
|
||||
```python
|
||||
from serial_protocol import SerialProtocol, Command
|
||||
|
||||
# 创建协议实例
|
||||
device = SerialProtocol(port='COM1', baudrate=115200)
|
||||
|
||||
# 打开串口
|
||||
if device.open():
|
||||
# 发送数据
|
||||
device.send_data(b"Hello, World!")
|
||||
|
||||
# 发送心跳
|
||||
device.send_heartbeat()
|
||||
|
||||
# 发送控制命令
|
||||
device.send_control(0x10, b"\x01\x02")
|
||||
|
||||
# 关闭串口
|
||||
device.close()
|
||||
```
|
||||
|
||||
### 异步接收数据
|
||||
|
||||
```python
|
||||
def on_receive(cmd, data):
|
||||
print(f"收到命令: 0x{cmd:02X}, 数据: {data.hex()}")
|
||||
|
||||
device = SerialProtocol(port='COM1', baudrate=115200)
|
||||
device.open()
|
||||
|
||||
# 启动接收线程
|
||||
device.start_receive(on_receive)
|
||||
|
||||
# ... 主程序运行 ...
|
||||
|
||||
device.close()
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 设备A (发送端)
|
||||
|
||||
运行 `device_a.py`,设备A会定期发送:
|
||||
- 心跳包
|
||||
- **站点数据**(包含用户名、线路名称、站点编号)
|
||||
- **接收设备B的字典响应**
|
||||
|
||||
输出示例:
|
||||
```
|
||||
============================================================
|
||||
循环 1
|
||||
============================================================
|
||||
✓ 发送心跳包
|
||||
|
||||
准备发送站点数据:
|
||||
用户: 李四, 线路: 2号线, 站点: 第1站
|
||||
✓ 站点数据发送成功
|
||||
|
||||
[设备A] 收到数据:
|
||||
命令: 0x03 (DATA_RESPONSE)
|
||||
|
||||
📥 收到设备B的响应字典:
|
||||
1: "李四"
|
||||
2: "2号线"
|
||||
3: "1"
|
||||
4: "已接收"
|
||||
5: "设备B确认"
|
||||
```
|
||||
|
||||
运行命令:
|
||||
```bash
|
||||
python device_a.py
|
||||
```
|
||||
|
||||
### 设备B (接收端)
|
||||
|
||||
运行 `device_b.py`,设备B会:
|
||||
- 接收站点数据并解析显示
|
||||
- 自动应答心跳包
|
||||
- **返回统一格式的响应字典** `{1:"xxx", 2:"xxx", 3:"xxx", ...}`
|
||||
|
||||
输出示例:
|
||||
```
|
||||
============================================================
|
||||
[设备B] 收到数据
|
||||
============================================================
|
||||
命令类型: 0x03 (DATA_RESPONSE)
|
||||
|
||||
📍 站点数据详情:
|
||||
用户名称: 李四
|
||||
线路名称: 2号线
|
||||
站点编号: 第1站
|
||||
|
||||
📤 设备B统一响应格式:
|
||||
1: "李四"
|
||||
2: "2号线"
|
||||
3: "1"
|
||||
4: "已接收"
|
||||
5: "设备B确认"
|
||||
|
||||
<<< 已发送响应字典
|
||||
```
|
||||
|
||||
运行命令:
|
||||
```bash
|
||||
python device_b.py
|
||||
```
|
||||
|
||||
## 设备B统一响应格式
|
||||
|
||||
设备B接收到站点数据后,会返回统一格式的字典响应:
|
||||
|
||||
```python
|
||||
{
|
||||
1: "用户名",
|
||||
2: "线路名称",
|
||||
3: "站点编号",
|
||||
4: "已接收",
|
||||
5: "设备B确认"
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `1`: 接收到的用户名
|
||||
- `2`: 接收到的线路名称
|
||||
- `3`: 接收到的站点编号(字符串)
|
||||
- `4`: 固定值 "已接收"
|
||||
- `5`: 固定值 "设备B确认"
|
||||
|
||||
## 硬件连接
|
||||
|
||||
### 方案1: 使用两个USB转串口模块
|
||||
|
||||
```
|
||||
设备A (COM1) <---RX/TX交叉---> 设备B (COM2)
|
||||
TX ----------------------> RX
|
||||
RX <---------------------- TX
|
||||
GND <---------------------> GND
|
||||
```
|
||||
|
||||
### 方案2: 使用虚拟串口 (测试用)
|
||||
|
||||
**Windows**: 使用 com0com 或 Virtual Serial Port Driver
|
||||
**Linux**: 使用 socat
|
||||
|
||||
```bash
|
||||
# Linux创建虚拟串口对
|
||||
socat -d -d pty,raw,echo=0 pty,raw,echo=0
|
||||
# 会创建 /dev/pts/X 和 /dev/pts/Y
|
||||
```
|
||||
|
||||
## API文档
|
||||
|
||||
### SerialProtocol 类
|
||||
|
||||
#### 初始化
|
||||
|
||||
```python
|
||||
SerialProtocol(port: str, baudrate: int = 115200, timeout: float = 1.0)
|
||||
```
|
||||
|
||||
#### 方法
|
||||
|
||||
- `open() -> bool` - 打开串口
|
||||
- `close()` - 关闭串口
|
||||
- `send_frame(cmd: int, data: bytes) -> bool` - 发送数据帧
|
||||
- `receive_frame() -> Optional[dict]` - 接收数据帧(阻塞)
|
||||
- `start_receive(callback)` - 启动异步接收
|
||||
- `stop_receive()` - 停止异步接收
|
||||
- `send_heartbeat() -> bool` - 发送心跳包
|
||||
- `send_data(data: bytes) -> bool` - 发送数据
|
||||
- `send_control(code: int, params: bytes) -> bool` - 发送控制命令
|
||||
- `send_ack() -> bool` - 发送应答
|
||||
- `send_nack() -> bool` - 发送否定应答
|
||||
|
||||
#### 静态方法
|
||||
|
||||
- `calc_crc16(data: bytes) -> int` - 计算CRC16校验值
|
||||
- `build_frame(cmd: int, data: bytes) -> bytes` - 构建数据帧
|
||||
- `parse_frame(frame: bytes) -> Optional[dict]` - 解析数据帧
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何修改串口号?
|
||||
|
||||
编辑 `device_a.py` 和 `device_b.py`,修改 `port` 参数:
|
||||
- Windows: `'COM1'`, `'COM2'`, etc.
|
||||
- Linux: `'/dev/ttyUSB0'`, `'/dev/ttyS0'`, etc.
|
||||
|
||||
### 2. 如何修改波特率?
|
||||
|
||||
修改 `baudrate` 参数,常用值:9600, 19200, 38400, 57600, 115200
|
||||
|
||||
### 3. 数据长度限制?
|
||||
|
||||
理论最大65535字节,但建议单帧数据不超过1024字节以提高可靠性。
|
||||
|
||||
### 4. 如何处理超时?
|
||||
|
||||
设置 `timeout` 参数控制读取超时时间。
|
||||
|
||||
## 应用场景
|
||||
|
||||
- 🤖 机器人通信
|
||||
- 📡 传感器数据采集
|
||||
- 🎮 设备控制
|
||||
- 📊 工业自动化
|
||||
- 🔌 嵌入式系统互联
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 作者
|
||||
|
||||
Created with ❤️ for reliable serial communication
|
||||
82
ck/data_models.py
Normal file
82
ck/data_models.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
数据模型定义
|
||||
定义设备间传输的数据结构
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class StationData:
|
||||
"""站点数据模型"""
|
||||
username: str # 用户名
|
||||
line_name: str # 线路名称
|
||||
station_no: int # 第几站
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""转换为字节数据"""
|
||||
data_dict = asdict(self)
|
||||
json_str = json.dumps(data_dict, ensure_ascii=False)
|
||||
return json_str.encode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> Optional['StationData']:
|
||||
"""从字节数据解析"""
|
||||
try:
|
||||
json_str = data.decode('utf-8')
|
||||
data_dict = json.loads(json_str)
|
||||
return StationData(
|
||||
username=data_dict['username'],
|
||||
line_name=data_dict['line_name'],
|
||||
station_no=data_dict['station_no']
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"解析数据失败: {e}")
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
"""字符串表示"""
|
||||
return (f"用户: {self.username}, "
|
||||
f"线路: {self.line_name}, "
|
||||
f"站点: 第{self.station_no}站")
|
||||
|
||||
|
||||
class ResponseData:
|
||||
"""设备B统一响应数据格式 {1:"xxxx", 2:"xxxx", 3:"xxxx", ...}"""
|
||||
|
||||
@staticmethod
|
||||
def create_response(station_data: StationData) -> Dict[int, str]:
|
||||
"""
|
||||
根据站点数据创建响应字典
|
||||
|
||||
Args:
|
||||
station_data: 接收到的站点数据
|
||||
|
||||
Returns:
|
||||
格式化的响应字典 {1: "用户名", 2: "线路名", 3: "站点号", ...}
|
||||
"""
|
||||
return {
|
||||
1: station_data.username,
|
||||
2: station_data.line_name,
|
||||
3: str(station_data.station_no),
|
||||
4: "已接收",
|
||||
5: "设备B确认"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def to_bytes(response_dict: Dict[int, str]) -> bytes:
|
||||
"""将响应字典转换为字节数据"""
|
||||
json_str = json.dumps(response_dict, ensure_ascii=False)
|
||||
return json_str.encode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> Optional[Dict[int, Any]]:
|
||||
"""从字节数据解析响应字典"""
|
||||
try:
|
||||
json_str = data.decode('utf-8')
|
||||
return json.loads(json_str)
|
||||
except Exception as e:
|
||||
print(f"解析响应数据失败: {e}")
|
||||
return None
|
||||
103
ck/device_a.py
Normal file
103
ck/device_a.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
设备A示例 - 发送端
|
||||
模拟第一台设备,定期发送站点数据(用户名、线路名称、站点编号)
|
||||
"""
|
||||
|
||||
from serial_protocol import SerialProtocol, Command
|
||||
from data_models import StationData, ResponseData
|
||||
import time
|
||||
|
||||
|
||||
def on_receive(cmd: int, data: bytes):
|
||||
"""接收数据回调函数"""
|
||||
print("\n[设备A] 收到数据:")
|
||||
cmd_name = (Command(cmd).name if cmd in Command._value2member_map_
|
||||
else 'UNKNOWN')
|
||||
print(f" 命令: 0x{cmd:02X} ({cmd_name})")
|
||||
|
||||
if cmd == Command.ACK:
|
||||
print(" >>> 对方已确认接收")
|
||||
elif cmd == Command.DATA_RESPONSE:
|
||||
# 优先尝试解析字典响应
|
||||
response_dict = ResponseData.from_bytes(data)
|
||||
if response_dict and isinstance(response_dict, dict):
|
||||
print("\n 📥 收到设备B的响应字典:")
|
||||
for key, value in response_dict.items():
|
||||
print(f" {key}: \"{value}\"")
|
||||
else:
|
||||
# 尝试解析站点数据
|
||||
station_data = StationData.from_bytes(data)
|
||||
if station_data:
|
||||
print(f" >>> 对方发来站点数据: {station_data}")
|
||||
else:
|
||||
print(f" >>> 对方发来数据: "
|
||||
f"{data.decode('utf-8', errors='ignore')}")
|
||||
|
||||
|
||||
def main():
|
||||
# 创建串口协议实例
|
||||
# Windows: 'COM1', 'COM2', etc.
|
||||
# Linux: '/dev/ttyUSB0', '/dev/ttyS0', etc.
|
||||
device = SerialProtocol(port='COM1', baudrate=115200)
|
||||
|
||||
print("=" * 60)
|
||||
print("设备A - 发送端")
|
||||
print("=" * 60)
|
||||
|
||||
# 打开串口
|
||||
if not device.open():
|
||||
print("❌ 无法打开串口")
|
||||
return
|
||||
|
||||
print(f"✓ 串口已打开: {device.port} @ {device.baudrate}")
|
||||
|
||||
# 启动接收线程
|
||||
device.start_receive(on_receive)
|
||||
print("✓ 接收线程已启动")
|
||||
|
||||
try:
|
||||
# 模拟线路数据
|
||||
lines = ["1号线", "2号线", "3号线", "环线"]
|
||||
users = ["张三", "李四", "王五", "赵六"]
|
||||
|
||||
# 模拟设备运行
|
||||
counter = 0
|
||||
while True:
|
||||
counter += 1
|
||||
print(f"\n{'='*60}")
|
||||
print(f"循环 {counter}")
|
||||
print('='*60)
|
||||
|
||||
# 1. 发送心跳包
|
||||
if device.send_heartbeat():
|
||||
print("✓ 发送心跳包")
|
||||
else:
|
||||
print("✗ 发送心跳包失败")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# 2. 发送站点数据
|
||||
station_data = StationData(
|
||||
username=users[counter % len(users)],
|
||||
line_name=lines[counter % len(lines)],
|
||||
station_no=counter
|
||||
)
|
||||
print("\n准备发送站点数据:")
|
||||
print(f" {station_data}")
|
||||
|
||||
if device.send_data(station_data.to_bytes()):
|
||||
print("✓ 站点数据发送成功")
|
||||
else:
|
||||
print("✗ 站点数据发送失败")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n用户中断")
|
||||
finally:
|
||||
device.close()
|
||||
print("串口已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
117
ck/device_b.py
Normal file
117
ck/device_b.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
设备B示例 - 接收端
|
||||
模拟第二台设备,接收站点数据(用户名、线路名称、站点编号)并应答
|
||||
"""
|
||||
|
||||
from serial_protocol import SerialProtocol, Command
|
||||
from data_models import StationData, ResponseData
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
|
||||
def on_receive(cmd: int, data: bytes):
|
||||
"""接收数据回调函数"""
|
||||
print("\n" + "="*60)
|
||||
print("[设备B] 收到数据")
|
||||
print("="*60)
|
||||
cmd_name = (Command(cmd).name if cmd in Command._value2member_map_
|
||||
else 'UNKNOWN')
|
||||
print(f"命令类型: 0x{cmd:02X} ({cmd_name})")
|
||||
|
||||
# 根据不同命令进行处理
|
||||
if cmd == Command.HEARTBEAT:
|
||||
print(">>> 收到心跳包")
|
||||
# 可以回复ACK
|
||||
device.send_ack()
|
||||
print("<<< 已发送ACK应答\n")
|
||||
|
||||
elif cmd == Command.DATA_RESPONSE:
|
||||
# 尝试解析站点数据
|
||||
station_data = StationData.from_bytes(data)
|
||||
if station_data:
|
||||
print("\n📍 站点数据详情:")
|
||||
print(f" 用户名称: {station_data.username}")
|
||||
print(f" 线路名称: {station_data.line_name}")
|
||||
print(f" 站点编号: 第{station_data.station_no}站")
|
||||
|
||||
# 根据站点编号执行不同逻辑
|
||||
if station_data.station_no == 0:
|
||||
print("\n🚀 站点编号为0,启动 actions.py")
|
||||
# 启动 actions.py
|
||||
|
||||
subprocess.Popen(["python", "actions.py"], cwd="d:\\Projects\\cjgc_data")
|
||||
|
||||
if station_data.station_no > 0:
|
||||
print(f"\n🚀 站点编号为{station_data.station_no},启动 check_station.py")
|
||||
# 启动 check_station.py
|
||||
|
||||
subprocess.Popen(["python", "check_station.py"], cwd="d:\\Projects\\cjgc_data")
|
||||
|
||||
# 创建统一格式的响应字典 {1:"xxx", 2:"xxx", 3:"xxx", ...}
|
||||
response_dict = ResponseData.create_response(station_data)
|
||||
print("\n📤 设备B统一响应格式:")
|
||||
for key, value in response_dict.items():
|
||||
print(f" {key}: \"{value}\"")
|
||||
|
||||
# 发送响应
|
||||
device.send_data(ResponseData.to_bytes(response_dict))
|
||||
print("\n<<< 已发送响应字典\n")
|
||||
else:
|
||||
# 如果不是站点数据,按普通消息处理
|
||||
message = data.decode('utf-8', errors='ignore')
|
||||
print(f">>> 收到普通消息: {message}")
|
||||
device.send_ack()
|
||||
print("<<< 已发送ACK\n")
|
||||
|
||||
elif cmd == Command.CONTROL:
|
||||
if len(data) >= 1:
|
||||
control_code = data[0]
|
||||
params = data[1:]
|
||||
print(f">>> 收到控制命令: 0x{control_code:02X}, "
|
||||
f"参数: {params.hex()}")
|
||||
# 执行控制逻辑...
|
||||
device.send_ack()
|
||||
print("<<< 已发送ACK\n")
|
||||
|
||||
|
||||
# 全局变量,用于在回调中访问
|
||||
device = None
|
||||
|
||||
|
||||
def main():
|
||||
global device
|
||||
|
||||
# 创建串口协议实例
|
||||
# 注意: 设备B应该使用另一个串口,或者通过虚拟串口对连接
|
||||
# Windows: 'COM2', Linux: '/dev/ttyUSB1'
|
||||
device = SerialProtocol(port='COM2', baudrate=115200)
|
||||
|
||||
print("=" * 60)
|
||||
print("设备B - 接收端")
|
||||
print("=" * 60)
|
||||
|
||||
# 打开串口
|
||||
if not device.open():
|
||||
print("❌ 无法打开串口")
|
||||
return
|
||||
|
||||
print(f"✓ 串口已打开: {device.port} @ {device.baudrate}")
|
||||
|
||||
# 启动接收线程
|
||||
device.start_receive(on_receive)
|
||||
print("✓ 接收线程已启动,等待接收数据...")
|
||||
|
||||
try:
|
||||
# 保持运行
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n用户中断")
|
||||
finally:
|
||||
device.close()
|
||||
print("串口已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
ck/requirements.txt
Normal file
1
ck/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyserial>=3.5
|
||||
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