You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用pymodbus的read_holding_registers遇错误端口程序冻结,无法捕获异常求助

问题分析与解决

核心原因

连接蓝牙虚拟串口这类无实际Modbus设备响应的端口时,ModbusSerialClient.connect()可能返回成功(因为串口本身能被打开),但后续read_holding_registers()会卡在底层串口读写操作中——由于没有设备返回响应,且底层串口驱动未触发超时错误,导致程序冻结,常规ModbusException和通用异常无法捕获这种阻塞。

解决方案

1. 提前验证端口有效性

连接前先检查端口是否存在且可正常访问,避免无效连接:

import serial.tools.list_ports

def is_port_valid(port):
    ports = [p.device for p in serial.tools.list_ports.comports()]
    if port not in ports:
        return False
    try:
        ser = serial.Serial(port, baudrate=9600, timeout=1)
        ser.close()
        return True
    except serial.SerialException:
        return False

在read_holding_registers函数开头加入:

if not is_port_valid(port):
    logger.error(f"Port {port} is invalid or unavailable.")
    return None

2. 为读操作添加强制超时

用线程给读操作设置最大执行时间,避免无限阻塞:

import threading
import inspect
import ctypes

def terminate_thread(thread):
    if not thread.is_alive():
        return
    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), exc)
    if res > 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), None)

def read_with_timeout(client, start_addr, count, slave_id, timeout=2):
    result = None
    def target():
        nonlocal result
        result = client.read_holding_registers(address=start_addr, count=count, slave=slave_id)
    
    thread = threading.Thread(target=target)
    thread.start()
    thread.join(timeout)
    
    if thread.is_alive():
        terminate_thread(thread)
        logger.error("Read operation timed out.")
        return None
    return result

替换原代码中读操作的部分:

response = read_with_timeout(client, start_address, count, slave_id, timeout=2)
if response is None:
    return None

3. 捕获底层串口异常

导入serial.SerialException并添加捕获逻辑,处理串口层面的错误:

from serial import SerialException

# 在try-except块中新增
except SerialException as e:
    logger.error(f"Serial Port Error: {e}")
    return None

4. 启用严格Modbus协议模式

初始化客户端时添加strict=True,强制遵循协议规范,减少无响应时的阻塞概率:

client = ModbusSerialClient(
    port=port,
    baudrate=9600,
    stopbits=1,
    bytesize=8,
    parity='N',
    timeout=2,
    strict=True  # 启用严格模式
)

修改后的完整代码

from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from serial import SerialException
import serial.tools.list_ports
import logging
import threading
import inspect
import ctypes

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def is_port_valid(port):
    ports = [p.device for p in serial.tools.list_ports.comports()]
    if port not in ports:
        return False
    try:
        ser = serial.Serial(port, baudrate=9600, timeout=1)
        ser.close()
        return True
    except SerialException:
        return False

def terminate_thread(thread):
    if not thread.is_alive():
        return
    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), exc)
    if res > 1:
        ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), None)

def read_with_timeout(client, start_addr, count, slave_id, timeout=2):
    result = None
    def target():
        nonlocal result
        result = client.read_holding_registers(address=start_addr, count=count, slave=slave_id)
    
    thread = threading.Thread(target=target)
    thread.start()
    thread.join(timeout)
    
    if thread.is_alive():
        terminate_thread(thread)
        logger.error("Read operation timed out.")
        return None
    return result

def read_holding_registers(port, slave_id, start_address, count):
    if not is_port_valid(port):
        logger.error(f"Port {port} is invalid or unavailable.")
        return None

    client = ModbusSerialClient(
        port=port,
        baudrate=9600,
        stopbits=1,
        bytesize=8,
        parity='N',
        timeout=2,
        strict=True
    )

    try:
        if not client.connect():
            logger.error("Failed to connect to the device.")
            return None

        response = read_with_timeout(client, start_address, count, slave_id, timeout=2)
        if response is None:
            return None

        if response.isError():
            logger.error(f"Modbus Error: {response}")
            return None

        logger.info(f"Successfully read {len(response.registers)} registers.")
        return response.registers

    except ModbusException as e:
        logger.error(f"Modbus Exception: {e}")
        return None
    except SerialException as e:
        logger.error(f"Serial Port Error: {e}")
        return None
    except Exception as e:
        logger.error(f"Unexpected Error: {e}")
        return None
    finally:
        client.close()

if __name__ == "__main__":
    PORT = 'COM7'
    SLAVE_ID = 1
    START_ADDR = 0
    REG_COUNT = 10

    print(f"Reading {REG_COUNT} Holding Registers starting from address {START_ADDR}...")
    registers = read_holding_registers(PORT, SLAVE_ID, START_ADDR, REG_COUNT)

    if registers is not None:
        for i, reg in enumerate(registers):
            print(f"Register {START_ADDR + i}: {reg} (0x{reg:04X})")
    else:
        print("Failed to read registers. Check connection and parameters.")

内容的提问来源于stack exchange,提问作者Kamsky Paul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 17:37:31