使用Reactor的Pymodbus Modbus RTU客户端回调未触发问题求助
解决Pymodbus异步RTU客户端回调未触发且Transport为Null的问题
我仔细看了你的代码和问题日志,核心问题出在Modbus协议初始化逻辑错误,以及对RTU串口参数的混淆使用,导致Transport无法正确绑定,最终异步请求的回调完全无法触发。咱们一步步拆解问题并修复:
问题根源分析
- 协议初始化方式错误:你在
connectionMade方法里直接调用ModbusClientProtocol.__init__和ModbusRtuFramer.__init__,这完全破坏了Twisted协议与Transport的绑定逻辑,直接导致self.transport为Null,异步请求发不出去,回调自然不会触发。 - RTU参数混淆:Modbus RTU是串口通信,不需要IP地址,你把
deviceIP和串口路径(比如/dev/ttyUSB0)拼在一起的操作完全不符合RTU的使用逻辑。 - 未定义参数遗漏:
read方法里直接使用startingAddress和registerCount,但这两个变量没有从配置中赋值,后续会引发运行时错误。 - Reactor线程风险:单独开线程启动Reactor的方式容易导致事件循环阻塞,影响异步回调的执行。
修正后的完整代码
下面是调整后的可运行代码,我标注了所有关键修改点:
import time from twisted.internet import reactor, serialport, protocol from pymodbus.client.asynchronous import ModbusClientProtocol, ModbusClientMixin from pymodbus.transaction import ModbusRtuFramer from pymodbus.factory import ClientDecoder import logging # 配置日志输出 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def readDevices(modbusRTUDevice): serial_port = modbusRTUDevice["serial_port"] logger.info("Connecting to Modbus RTU device at port {0}".format(serial_port)) modbusClientFactory = CustomModbusClientFactory() modbusClientFactory.modbusDevice = modbusRTUDevice # 初始化串口客户端:参数顺序为协议、串口路径、Reactor、串口参数 serialport.SerialPort( modbusClientFactory.buildProtocol(), serial_port, reactor, baudrate=9600, parity='E', bytesize=8, stopbits=1, timeout=0.2 ) reactor.run() class CustomModbusClientFactory(protocol.ClientFactory, ModbusClientMixin): modbusDevice = {} def buildProtocol(self, addr=None): # 直接初始化自定义协议,传入Framer和串口参数 modbusClientProtocol = CustomModbusClientProtocol( framer=ModbusRtuFramer(ClientDecoder()), baudrate=9600, parity='E', bytesize=8, stopbits=1, timeout=0.2, retryOnEmpty=True, retries=3 ) modbusClientProtocol.factory = self modbusClientProtocol.modbusDevice = self.modbusDevice return modbusClientProtocol def clientConnectionLost(self, connector, reason): logger.info("Connection lost, reconnecting...") connector.connect() def clientConnectionFailed(self, connector, reason): logger.error("Connection failed: {0}".format(reason)) reactor.stop() class CustomModbusClientProtocol(ModbusClientProtocol): def __init__(self, framer, **kwargs): # 用super()正确初始化父类,保证协议与Transport绑定 super().__init__(framer, **kwargs) self.modbusDevice = {} def connectionMade(self): super().connectionMade() serial_port = self.modbusDevice["serial_port"] logger.info("Modbus RTU device connected at port {0}".format(serial_port)) # 从配置中读取寄存器参数 self.startingAddress = self.modbusDevice["starting_address"] self.registerCount = self.modbusDevice["register_count"] self.read() def read(self): serial_port = self.modbusDevice["serial_port"] slaveAddress = self.modbusDevice["slaveAddress"] logger.info("Reading holding registers of Modbus RTU device at port {0}...".format(serial_port)) deferred = self.read_holding_registers(self.startingAddress, self.registerCount, unit=slaveAddress) deferred.addCallbacks(self.requestFetched, self.requestNotFetched) def requestNotFetched(self, error): logger.error("Error reading registers of Modbus RTU device : {0}".format(error)) # 用Reactor的延迟调用替代sleep,避免阻塞事件循环 reactor.callLater(0.5, self.read) def requestFetched(self, response): logger.info("Successfully fetched registers! Response: {0}".format(response.registers)) # 在这里添加你的数据处理逻辑 # Do some other stuff here # 定时重复读取 reactor.callLater(1, self.read) # 示例调用 if __name__ == "__main__": # RTU设备配置:移除无用的IP字段,只保留串口路径、从站地址、寄存器参数 rtu_device = { "serial_port": "/dev/ttyUSB0", # Windows下替换为"COM3"这类串口名 "slaveAddress": 1, "starting_address": 0, "register_count": 10 } readDevices(rtu_device)
关键修改说明
- 协议初始化修正:在
CustomModbusClientProtocol的构造函数中用super()正确调用父类初始化,保证Framer和Transport的绑定关系。 - RTU参数清理:删除了无用的
ip字段,改用serial_port表示串口路径,符合RTU串口通信的要求。 - 连接状态管理:在工厂类中添加了断线重连和连接失败处理逻辑,提升稳定性。
- 异步友好的重试:把
sleep(0.5)换成reactor.callLater,避免阻塞Twisted的事件循环。 - 参数补全:在
connectionMade中初始化寄存器的起始地址和数量,确保读取逻辑能正常执行。
额外检查项
- 串口权限:Linux下需要将用户加入
dialout组(sudo usermod -aG dialout your_username),否则无法访问串口。 - 参数匹配:确保串口参数(波特率、奇偶校验、数据位、停止位)和设备端完全一致,这是RTU通信最常见的坑。
- 串口占用:Windows下要确认没有其他程序占用目标串口,否则会连接失败。
内容的提问来源于stack exchange,提问作者OO7




