first commit

This commit is contained in:
2026-03-12 17:03:56 +08:00
commit aa4d4c7d7c
48 changed files with 10958 additions and 0 deletions

25
ck/.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
pyserial>=3.5

298
ck/serial_protocol.py Normal file
View 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)