241 lines
8.2 KiB
Python
241 lines
8.2 KiB
Python
import serial
|
||
import serial.tools.list_ports
|
||
import threading
|
||
import time
|
||
from typing import Optional, Dict, Any
|
||
|
||
class SerialManager:
|
||
"""
|
||
串口管理器 - 负责所有串口通信功能
|
||
采用单例模式确保串口只被创建一次
|
||
"""
|
||
|
||
_instance = None
|
||
_initialized = False
|
||
|
||
def __new__(cls, *args, **kwargs):
|
||
"""单例模式实现"""
|
||
if cls._instance is None:
|
||
cls._instance = super(SerialManager, cls).__new__(cls)
|
||
return cls._instance
|
||
|
||
def __init__(self, port: str = None, baudrate: int = 9600,
|
||
timeout: float = 1.0, write_timeout: float = 1.0):
|
||
"""
|
||
初始化串口管理器
|
||
|
||
Args:
|
||
port: 串口端口,如 "COM1" 或 "/dev/ttyUSB0"
|
||
baudrate: 波特率,默认9600
|
||
timeout: 读超时时间(秒)
|
||
write_timeout: 写超时时间(秒)
|
||
"""
|
||
if self._initialized:
|
||
return
|
||
|
||
self.port = port
|
||
self.baudrate = baudrate
|
||
self.timeout = timeout
|
||
self.write_timeout = write_timeout
|
||
|
||
# 串口对象和连接状态
|
||
self.serial_obj = None
|
||
self.is_connected = False
|
||
|
||
# 线程安全锁
|
||
self.serial_lock = threading.Lock()
|
||
|
||
# 初始化串口连接
|
||
self._init_serial_connection()
|
||
|
||
self._initialized = True
|
||
|
||
def _init_serial_connection(self):
|
||
"""初始化串口连接"""
|
||
if not self.port:
|
||
self._auto_detect_port()
|
||
|
||
if not self.port:
|
||
print("⚠ 未找到可用串口")
|
||
return
|
||
|
||
try:
|
||
self.serial_obj = serial.Serial(
|
||
port=self.port,
|
||
baudrate=self.baudrate,
|
||
bytesize=8,
|
||
parity='N',
|
||
stopbits=1,
|
||
timeout=self.timeout,
|
||
write_timeout=self.write_timeout
|
||
)
|
||
|
||
if self.serial_obj.is_open:
|
||
self.is_connected = True
|
||
print(f"✓ 串口连接成功: {self.port} ({self.baudrate}bps)")
|
||
else:
|
||
print(f"⚠ 串口打开失败: {self.port}")
|
||
|
||
except Exception as e:
|
||
print(f"⚠ 串口连接失败: {e}")
|
||
self.is_connected = False
|
||
|
||
def _auto_detect_port(self):
|
||
"""自动检测可用串口"""
|
||
try:
|
||
available_ports = list(serial.tools.list_ports.comports())
|
||
if available_ports:
|
||
print(f"检测到可用串口: {[port.device for port in available_ports]}")
|
||
|
||
import platform
|
||
system = platform.system()
|
||
|
||
if system == "Windows":
|
||
# Windows系统优先选择COM3、COM4等
|
||
for port in available_ports:
|
||
if port.device in ["COM3", "COM4", "COM5", "COM6"]:
|
||
self.port = port.device
|
||
break
|
||
else:
|
||
self.port = available_ports[0].device
|
||
else:
|
||
# Linux系统优先选择USB串口
|
||
for port in available_ports:
|
||
if "USB" in port.description or "ttyUSB" in port.device:
|
||
self.port = port.device
|
||
break
|
||
else:
|
||
self.port = available_ports[0].device
|
||
except Exception as e:
|
||
print(f"串口检测失败: {e}")
|
||
|
||
def _modbus_crc16(self, data: bytes) -> bytes:
|
||
"""计算Modbus CRC16校验码"""
|
||
crc = 0xFFFF
|
||
for byte in data:
|
||
crc ^= byte
|
||
for _ in range(8):
|
||
if crc & 0x0001:
|
||
crc = (crc >> 1) ^ 0xA001
|
||
else:
|
||
crc >>= 1
|
||
return bytes([crc & 0xFF, (crc >> 8) & 0xFF])
|
||
|
||
def send_modbus_command(self, unit: int, function_code: int,
|
||
data: bytes, expected_response_len: int = 8) -> Dict[str, Any]:
|
||
"""
|
||
发送Modbus命令
|
||
|
||
Args:
|
||
unit: 从站地址
|
||
function_code: 功能码
|
||
data: 数据字节
|
||
expected_response_len: 期望响应长度
|
||
|
||
Returns:
|
||
包含操作结果的字典
|
||
"""
|
||
if not self.is_connected or not self.serial_obj:
|
||
return {"success": False, "error": "串口未连接"}
|
||
|
||
with self.serial_lock:
|
||
try:
|
||
# 构建请求帧
|
||
request_frame = bytes([unit & 0xFF, function_code & 0xFF]) + data
|
||
crc = self._modbus_crc16(request_frame)
|
||
full_request = request_frame + crc
|
||
|
||
# 清空缓冲区
|
||
self.serial_obj.reset_input_buffer()
|
||
|
||
# 发送请求
|
||
self.serial_obj.write(full_request)
|
||
self.serial_obj.flush()
|
||
|
||
print(f"📤 发送Modbus命令: {full_request.hex(' ').upper()}")
|
||
|
||
# 读取响应
|
||
time.sleep(0.1) # 等待设备响应
|
||
response = self.serial_obj.read(expected_response_len)
|
||
|
||
if len(response) < expected_response_len:
|
||
return {"success": False, "error": "响应超时或长度不足", "raw": response}
|
||
|
||
# CRC校验
|
||
if len(response) >= 4:
|
||
response_data = response[:-2]
|
||
response_crc = response[-2:]
|
||
calculated_crc = self._modbus_crc16(response_data)
|
||
|
||
if response_crc != calculated_crc:
|
||
return {"success": False, "error": "CRC校验失败", "raw": response}
|
||
|
||
return {
|
||
"success": True,
|
||
"raw": response,
|
||
"raw_hex": response.hex(' ').upper()
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": f"串口通信异常: {str(e)}"}
|
||
|
||
def write_single_coil(self, unit: int, coil_addr: int, value: bool) -> bool:
|
||
"""
|
||
写单个线圈(功能码0x05)
|
||
|
||
Args:
|
||
unit: 从站地址
|
||
coil_addr: 线圈地址
|
||
value: 线圈值(True/False)
|
||
|
||
Returns:
|
||
操作是否成功
|
||
"""
|
||
data = bytes([
|
||
(coil_addr >> 8) & 0xFF, # 地址高字节
|
||
coil_addr & 0xFF, # 地址低字节
|
||
0xFF if value else 0x00, # 值高字节
|
||
0x00 # 值低字节
|
||
])
|
||
|
||
result = self.send_modbus_command(unit, 0x05, data, 8)
|
||
return result["success"]
|
||
|
||
def read_holding_registers(self, unit: int, start_addr: int, quantity: int) -> Dict[str, Any]:
|
||
"""
|
||
读取保持寄存器(功能码0x03)
|
||
|
||
Args:
|
||
unit: 从站地址
|
||
start_addr: 起始地址
|
||
quantity: 读取数量
|
||
|
||
Returns:
|
||
包含寄存器数据的字典
|
||
"""
|
||
data = bytes([
|
||
(start_addr >> 8) & 0xFF, # 起始地址高字节
|
||
start_addr & 0xFF, # 起始地址低字节
|
||
(quantity >> 8) & 0xFF, # 数量高字节
|
||
quantity & 0xFF # 数量低字节
|
||
])
|
||
|
||
return self.send_modbus_command(unit, 0x03, data, 5 + 2 * quantity)
|
||
|
||
def close(self):
|
||
"""关闭串口连接"""
|
||
if self.serial_obj and self.is_connected:
|
||
try:
|
||
self.serial_obj.close()
|
||
self.is_connected = False
|
||
print("✓ 串口连接已关闭")
|
||
except Exception as e:
|
||
print(f"⚠ 关闭串口失败: {e}")
|
||
|
||
def get_status(self) -> Dict[str, Any]:
|
||
"""获取串口状态"""
|
||
return {
|
||
"is_connected": self.is_connected,
|
||
"port": self.port,
|
||
"baudrate": self.baudrate
|
||
} |