翼度科技»论坛 编程开发 python 查看内容

Python pymodbus类库使用学习总结

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
实践环境

Python 3.9.13
https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe
pymodbus-3.6.8-py3-none-any.whl
https://files.pythonhosted.org/packages/35/19/a9d16f74548d6750acf6604fa74c2cd165b5bc955fe021bf5e1fa04acf14/pymodbus-3.6.8-py3-none-any.whl
pyserial-3.5
备注:如果不安装该模块,采用串口通信时(下述代码中 comm = 'serial'时的通信),会报错NameError: name 'serial' is not defined
  1. pip install pyserial
复制代码
Virtual Serial Port Driver 6.9
链接:https://pan.baidu.com/s/15pCeYyOZWSsbEr_rU1e5hg?pwd=vspd
提取码:vspd
代码实践

修改pymodbus\logging.py

路径: PYTHON_HOME\Lib\site-packages\pymodbus\logging.py
修改源码是为了更方便使用类库自带日志打印器
  1. # ...略
  2. class Log:
  3.     """Class to hide logging complexity.
  4.     :meta private:
  5.     """
  6.     # _logger = logging.getLogger(__name__) # commented by shouke
  7.     _logger = logging.getLogger('logger') # added by shouke # 方便在不同模块获取日志打印器
  8.     @classmethod
  9.     def apply_logging_config(cls, level, log_file_name):
  10.         """Apply basic logging configuration."""
  11.         if level == logging.NOTSET:
  12.             level = cls._logger.getEffectiveLevel()
  13.         if isinstance(level, str):
  14.             level = level.upper()
  15.         log_stream_handler = logging.StreamHandler()
  16.         log_formatter = logging.Formatter(
  17.             "%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s"
  18.         )
  19.         log_stream_handler.setFormatter(log_formatter)
  20.         ##################### commented by shouke #################
  21.         # cls._logger.addHandler(log_stream_handler)
  22.         # if log_file_name:
  23.         #     log_file_handler = logging.FileHandler(log_file_name)
  24.         #     log_file_handler.setFormatter(log_formatter)
  25.         #     cls._logger.addHandler(log_file_handler)
  26.         ##########################################################
  27.         ##################### added by shouke #################
  28.         if not cls._logger.handlers: # 如果不加这个判断,在不同模块都执行pymodbus_apply_logging_config函数时,会导致同一条日志被重复打印
  29.             cls._logger.addHandler(log_stream_handler)
  30.             if log_file_name:
  31.                 log_file_handler = logging.FileHandler(log_file_name)
  32.                 log_file_handler.setFormatter(log_formatter)
  33.                 cls._logger.addHandler(log_file_handler)
  34.         ##########################################################
  35.         cls.setLevel(level)
  36. # ...略
  37.         
  38. pymodbus_apply_logging_config(level=logging.INFO) # 统一日志打印器配置
复制代码
异步服务器和异步客户端实现

异步服务器代码实现

server_async.py
  1. #!/usr/bin/env python3
  2. '''Pymodbus 异步服务器示例
  3. 多线程异步服务器的一个示例。
  4. '''
  5. import asyncio
  6. import logging
  7. ### 不修改 PYTHON_HOME\Lib\site-packages\pymodbus\logging.py源码的基础上,获取日志打印器相关方法及说明:
  8. # 方法1
  9. # _logger = logging.getLogger(__name__) # 采用该_logger打印的日志看不到
  10. # logging.basicConfig(level=logging.DEBUG) # 不注释上行代码的基础上添加这行代码,日志能打印出来,但是日志所在模块不为当前模块
  11. # 方法2
  12. # _logger = logging.getLogger(__file__)
  13. # logging.basicConfig(level=logging.INFO) #  添加这行代码,确保日志能打印出来,但是打印出来的日志所在模块不为当前模块
  14. # 方法3:
  15. # from pymodbus.logging import Log,pymodbus_apply_logging_config
  16. # pymodbus_apply_logging_config(level=logging.INFO) # 如果缺少这行代码,下面async_helper中的日志打印将无法在控制台输出
  17. # _logger = Log._logger
  18. # 修改 PYTHON_HOME\Lib\site-packages\pymodbus\logging.py源码的基础上,获取日志打印器相关方法:
  19. _logger = logging.getLogger('logger')
  20. # _logger.setLevel(logging.INFO)
  21. from pymodbus import __version__ as pymodbus_version
  22. from pymodbus.datastore import (
  23.     ModbusSequentialDataBlock,
  24.     ModbusServerContext,
  25.     ModbusSlaveContext,
  26.     ModbusSparseDataBlock,
  27. )
  28. from pymodbus.device import ModbusDeviceIdentification
  29. from pymodbus.server import (
  30.     StartAsyncSerialServer,
  31.     StartAsyncTcpServer,
  32.     StartAsyncTlsServer,
  33.     StartAsyncUdpServer,
  34. )
  35. # hook函数
  36. def server_request_tracer(request, *_addr):
  37.     """跟踪请求
  38.     所有服务器请求在被处理之前都经过此过滤器
  39.     """
  40.     _logger.info(f'---> REQUEST: {request}') # 输出,类似这样: ---> REQUEST: ReadBitRequest(0,1)
  41. def server_response_manipulator( response):
  42.     """操纵响应
  43.     所有服务器响应在发送之前都通过此过滤器
  44.     过滤器返回:
  45.     - 响应,原始或修改后的
  46.     - 跳过编码,发出是否对响应进行编码的信号
  47.     """
  48.     _logger.info(f'---> RESPONSE: {response}') # 输出,类似这样:---> RESPONSE: ReadCoilsResponse(1)
  49.     # response.should_respond = False # 如果让该行代码生效,则客户端收不到服务器响应
  50.     return response, False
  51. class Args:
  52.     comm = 'tcp' # 通讯模式,可选值 tcp、udp serial、tls
  53.     comm_defaults = {
  54.         'tcp': ['socket', 5020],
  55.         # 'tcp': ['rtu', 5020], # 如果采用ModbusRTU协议 则使用这个
  56.         'udp': ['socket', 5020],
  57.         # 'serial': ['rtu', '/dev/ptyp0'], # Linux
  58.         'serial': ['rtu', 'COM1']  # Windows(本例中COM1和COM2是成对的)
  59.         'tls': ['tls', 5020]
  60.     }
  61.     framer = comm_defaults[comm][0] # 帧类型
  62.     port = comm_defaults[comm][1] # 站点端口
  63.     host = ''
  64.     baudrate = 9600 # 串行设备波特率 即每秒传输的位数
  65.     # 连续的、无间隙顺序存储数据块(寄存器块)
  66.     datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)  # pylint: disable=unnecessary-lambda-assignment
  67.     # 连续的,或者可能有间隙的稀疏、不规则存储数据块
  68.     # datablock = lambda : ModbusSparseDataBlock({0x00: 0, 0x05: 1})  # pylint: disable=unnecessary-lambda-assignment
  69.     # 工厂模式
  70.     # datablock = lambda : ModbusSequentialDataBlock.create()  # pylint: disable=unnecessary-lambda-assignment,unnecessary-lambda
  71.     # 如果从节点数量不为0
  72.     #服务器使用服务器上下文,该上下文允许服务器,以针对不同的从设备ID使用不同的从站上下文(slave context)进行响应。
  73.     # 默认情况下,它将为提供的每个slave ID返回相同的上下文(广播模式)。
  74.     # 但是,可以通过将single标识设置为False并且提供slave ID到上下文映射的字典来覆盖这种行为:
  75.     # 从机上下文也可以按zero_mode初始化,这意味着到地址(0-7)的请求将映射到地址(0-7)。
  76.     # 默认值为False,因此地址(0-7)将映射到(1-8):
  77.     # context = {
  78.     #     0x01: ModbusSlaveContext( # 0x01为从设备、从机地址
  79.     #         di=datablock(), # 输入离散量
  80.     #         co=datablock(), # 输出线圈
  81.     #         hr=datablock(), # 保持寄存器
  82.     #         ir=datablock(), # 输入寄存器
  83.     #     ),
  84.     #     0x02: ModbusSlaveContext(
  85.     #         di=datablock(),
  86.     #         co=datablock(),
  87.     #         hr=datablock(),
  88.     #         ir=datablock(),
  89.     #     ),
  90.     #     0x03: ModbusSlaveContext(
  91.     #         di=datablock(),
  92.     #         co=datablock(),
  93.     #         hr=datablock(),
  94.     #         ir=datablock(),
  95.     #         zero_mode=True,
  96.     #     ),
  97.     #     }
  98.     # single = False
  99.     #如果从节点数量为0
  100.     context = ModbusSlaveContext(
  101.         di=datablock(), co=datablock(), hr=datablock(), ir=datablock()
  102.     )
  103.     single = True
  104.     # 构建数据存储
  105.     context = ModbusServerContext(slaves=context,
  106.                                   single=single
  107.                                   )
  108.     # ----------------------------------------------------------------------- #
  109.     # 初始化服务器信息
  110.     # 不对属性字段做任何设置,则字段值默认为空字符串
  111.     # ----------------------------------------------------------------------- #
  112.     identity = ModbusDeviceIdentification(
  113.         info_name={
  114.             "VendorName": "Pymodbus",
  115.             "ProductCode": "PM",
  116.             "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
  117.             "ProductName": "Pymodbus Server",
  118.             "ModelName": "Pymodbus Server",
  119.             "MajorMinorRevision": pymodbus_version,
  120.         }
  121.     )
  122.     # sslctx=sslctx,  # 用于TLS的SSLContext (默认为None, 自动创建)
  123.     cert_file_path = './certificates/pymodbus.crt' # 用于TLS 证书文件路径  (如果sslctx为None时使用该文件)
  124.     key_file_path = './certificates/pymodbus.key'  # 用于TLS 私钥文件路径 (如果sslctx为None时使用该文件)
  125. async def run_async_server():
  126.     """Run server."""
  127.     txt = f'### start ASYNC server, listening on {Args.port} - {Args.comm}'
  128.     _logger.info(txt)
  129.     if Args.comm == 'tcp':
  130.         address = (Args.host if Args.host else '', Args.port if Args.port else None)
  131.         server = await StartAsyncTcpServer(
  132.             context=Args.context,  # 数据存储
  133.             identity=Args.identity,  # 服务器标识
  134.             # TBD host=
  135.             # TBD port=
  136.             address=address,  # 监听地址
  137.             # custom_functions=[],  # 允许自定义处理函数
  138.             framer=Args.framer,  # 使用的帧策略
  139.             # ignore_missing_slaves=True,  # 忽略对缺失的slave的请求
  140.             # broadcast_enable=False,  # 是否允许广播 将 slave id 0视为广播地址
  141.             # timeout=1  # 等待请求完成的时间 # waiting time for request to complete
  142.             # 可选参数,实现hook功能
  143.             request_tracer=server_request_tracer,
  144.             response_manipulator=server_response_manipulator
  145.         )
  146.     elif Args.comm == 'udp':
  147.         address = (
  148.             Args.host if Args.host else "127.0.0.1",
  149.             Args.port if Args.port else None,
  150.         )
  151.         server = await StartAsyncUdpServer(
  152.             context=Args.context,
  153.             identity=Args.identity,
  154.             address=address,
  155.             # custom_functions=[],
  156.             framer=Args.framer,
  157.             # ignore_missing_slaves=True,
  158.             # broadcast_enable=False,
  159.             # timeout=1
  160.         )
  161.     elif Args.comm == 'serial':
  162.         # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600
  163.         #             PTY,link=/tmp/ttyp0,raw,echo=0,ospeed=9600
  164.         server = await StartAsyncSerialServer(
  165.             context=Args.context,
  166.             identity=Args.identity,
  167.             # timeout=1,  # waiting time for request to complete
  168.             port=Args.port,  # 串口
  169.             # custom_functions=[],
  170.             framer=Args.framer,
  171.             # stopbits=1,  # 要使用的停止位数(The number of stop bits to use)
  172.             # bytesize=8,  # 序列化消息字节大小(The bytesize of the serial messages)
  173.             # parity="N",  # 使用哪种奇偶校验
  174.             baudrate=Args.baudrate,  # 用于串行设备的波特率
  175.             # handle_local_echo=False,  # 处理USB-to-RS485适配器的本地echo(Handle local echo of the USB-to-RS485 adaptor)
  176.             # ignore_missing_slaves=True,  # ignore request to a missing slave
  177.             # broadcast_enable=False,  #
  178.             # strict=True,  # 使用严格的计时,针对Modbus RTU 为t1.5(use strict timing, t1.5 for Modbus RTU)
  179.         )
  180.     elif Args.comm == 'tls':
  181.         address = (Args.host if Args.host else '', Args.port if Args.port else None)
  182.         server = await StartAsyncTlsServer(
  183.             context=Args.context,  # Data storage
  184.             host='localhost',  # 定义用于连接的tcp地址
  185.             # port=port,  # tcp监听端口
  186.             identity=Args.identity,  # server identify
  187.             # custom_functions=[],  # allow custom handling
  188.             address=address,
  189.             framer=Args.framer,
  190.             certfile=Args.cert_file_path,  # The cert file path for TLS (used if sslctx is None)
  191.             # sslctx=sslctx,  # The SSLContext to use for TLS (default None and auto create)
  192.             keyfile=Args.key_file_path,  # The key file path for TLS (used if sslctx is None)
  193.             # password="none",  # 用于解密私钥文件的密码
  194.             # ignore_missing_slaves=True,
  195.             # broadcast_enable=False,
  196.             # timeout=1
  197.         )
  198.     return server
  199. async def async_helper():
  200.     _logger.info("Starting server...")
  201.     await run_async_server()
  202. if __name__ == "__main__":
  203.     asyncio.run(async_helper(), debug=True)
复制代码
异步客户端代码实现

client_async.py
  1. #!/usr/bin/env python3
  2. '''Pymodbus异步客户端示例
  3. '''
  4. import asyncio
  5. import logging
  6. import pymodbus.client as modbusClient
  7. from pymodbus import ModbusException
  8. _logger = logging.getLogger('logger')
  9. class Args:
  10.     comm = 'tcp' # 通讯模式,可选值 tcp、udp serial、tls
  11.     comm_defaults = {
  12.         'tcp': ['socket', 5020],
  13.         # 'tcp': ['rtu', 5020], # 如果采用ModbusRTU协议 则使用这个
  14.         'udp': ['socket', 5020],
  15.         # 'serial': ['rtu', '/dev/ptyp0'], # Linux
  16.         'serial': ['rtu', 'COM2']  # Windows(本例中COM1和COM2是用Virtual Serial Port Driver 6.9软件成对添加的虚拟端口)
  17.         'tls': ['tls', 5020]
  18.     }
  19.     framer = comm_defaults[comm][0] # 帧类型
  20.     port = comm_defaults[comm][1] # 站点端口
  21.     host = '127.0.0.1' # 服务端地址
  22.     baudrate = 9600 # 串行设备波特率
  23.     timeout = 10 # 客户端访问服务器超时时间(该参数仅用于客户端(slave节点)),float型
  24.     # sslctx=sslctx,  # 用于TLS的SSLContext (默认为None, 自动创建)
  25.     cert_file_path = './certificates/pymodbus.crt' # 用于TLS 证书文件路径  (如果sslctx为None时使用该文件)
  26.     key_file_path = './certificates/pymodbus.key'  # 用于TLS 私钥文件路径 (如果sslctx为None时使用该文件)
  27. async def run_async_client(modbus_calls=None):
  28.     """Run sync client."""
  29.     _logger.info("### Create client object")
  30.     if Args.comm == "tcp":
  31.         client = modbusClient.AsyncModbusTcpClient(
  32.             Args.host,
  33.             port=Args.port,  # on which port
  34.             # Common optional parameters:
  35.             framer=Args.framer,
  36.             timeout=Args.timeout,
  37.             retries=3,
  38.             reconnect_delay=1,
  39.             reconnect_delay_max=10,
  40.             #    retry_on_empty=False,
  41.             # TCP setup parameters
  42.             #    source_address=("localhost", 0),
  43.         )
  44.     elif Args.comm == "udp":
  45.         client = modbusClient.AsyncModbusUdpClient(
  46.             Args.host,
  47.             port=Args.port,
  48.             # Common optional parameters:
  49.             framer=Args.framer,
  50.             timeout=Args.timeout,
  51.             #    retries=3,
  52.             #    retry_on_empty=False,
  53.             # UDP setup parameters
  54.             #    source_address=None,
  55.         )
  56.     elif Args.comm == "serial":
  57.         client = modbusClient.AsyncModbusSerialClient(
  58.             Args.port,
  59.             # Common optional parameters:
  60.             #    framer=ModbusRtuFramer,
  61.             timeout=Args.timeout,
  62.             #    retries=3,
  63.             #    retry_on_empty=False,
  64.             # Serial setup parameters
  65.             baudrate=Args.baudrate,
  66.             #    bytesize=8,
  67.             #    parity="N",
  68.             #    stopbits=1,
  69.             #    handle_local_echo=False,
  70.             #    strict=True,
  71.         )
  72.     elif Args.comm == "tls":
  73.         client = modbusClient.AsyncModbusTlsClient(
  74.             Args.host,
  75.             port=Args.port,
  76.             # Common optional parameters:
  77.             framer=Args.framer,
  78.             timeout=Args.timeout,
  79.             #    retries=3,
  80.             #    retry_on_empty=False,
  81.             # TLS setup parameters
  82.             sslctx=modbusClient.AsyncModbusTlsClient.generate_ssl(
  83.                 certfile=Args.cert_file_path,
  84.                 keyfile=Args.key_file_path,
  85.             #    password="none",
  86.             ),
  87.             server_hostname="localhost",
  88.         )
  89.     _logger.info("### Client starting")
  90.     await client.connect()
  91.     assert client.connected
  92.     if modbus_calls:
  93.         await modbus_calls(client)
  94.     client.close()
  95.     _logger.info("### End of Program")
  96. async def run_a_few_calls(client):
  97.    
  98.     try:
  99.         # 读取线圈状态
  100.         rr = await client.read_coils(0, 1, slave=0) # 从 0x00 地址开始读取1个线圈
  101.         print(rr.bits) # 输出:[True, False, False, False, False, False, False, False]
  102.         # assert len(rr.bits) == 8
  103.         print(rr) # 输出:ReadCoilsResponse(8)
  104.         # 读输入离散量
  105.         rr = await client.read_holding_registers(0, 1, slave=0) # 从 0x00 地址开始读取1个线圈
  106.         print(rr.registers) # 输出:[17]
  107.         rr = await client.read_holding_registers(4, 2, slave=0) # 从0x04 地址开始读取2个线圈
  108.         # assert rr.registers[0] == 17
  109.         # assert rr.registers[1] == 17
  110.         print(rr.registers) # 输出:[17, 17]
  111.         # 读保持寄存器
  112.         rr = await client.read_holding_registers(5, 4) # 从 0x05 地址开始读取4个线圈
  113.         print(rr.registers) # 输出:[17, 17, 17, 17]
  114.         # 读输入寄存器
  115.         rr = await client.read_input_registers(0x0F, 3, slave=0) # 从 0x0F 地址开始读取4个线圈
  116.         print(rr.registers) # 输出:[17, 17, 17]
  117.         rr = await client.read_input_registers(15, 3, slave=0) # 从 0x0F 地址开始读取4个线圈
  118.         print(rr) # 输出:ReadInputRegistersResponse (3)
  119.         print(rr.registers) # 输出:[17, 17, 17]
  120.         # 写单个线圈
  121.         rr = await client.write_coil(9, False, slave=0) # 将布尔值False写入 0x09 地址
  122.         print(rr) # 输出:WriteCoilResponse(9) => False
  123.         rr = await client.read_coils(9, 1, slave=0)
  124.         print(rr.bits) # 输出:[False, False, False, False, False, False, False, False]
  125.         # 写多个线圈
  126.         await client.write_coils(10, False, slave=0) # 将布尔值False写入 0x0A 地址
  127.         rr = await client.read_coils(10, 1, slave=0)
  128.         print(rr.bits) # 输出:[False, False, False, False, False, False, False, False]
  129.         rr = await client.read_coils(11, 1, slave=0)
  130.         print(rr.bits) # 输出:[True, False, False, False, False, False, False, False]
  131.         await client.write_coils(10, [False, False], slave=0) # 将布尔值False写入 0x0A 0x0B 地址
  132.         rr = await client.read_coils(11, 1, slave=0)
  133.         print(rr.bits) # 输出:[False, False, False, False, False, False, False, False]
  134.         # 写单个保持寄存器
  135.         await client.write_register(12, 0x0F, slave=0) # 将0x0F写入 0x0C 地址
  136.         rr = await client.read_input_registers(12, 3, slave=0)
  137.         print(rr.registers) # 输出:[17, 17, 17]
  138.         rr = await client.read_holding_registers(12, 4)
  139.         print(rr.registers) # 输出:[15, 17, 17, 17]
  140.         # 写多个保持寄存器
  141.         await client.write_registers(13, 0x0F, slave=0) # 将0x0F写入 0x0D 地址
  142.         rr = await client.read_holding_registers(13, 2)
  143.         print(rr.registers) # 输出:[15, 17]
  144.         await client.write_registers(13, [0x0F, 0x0E], slave=0) # 将0x0F写入 0x0D,0x0E 地址
  145.         rr = await client.read_holding_registers(13, 2)
  146.         print(rr.registers) # 输出:[15, 14]
  147.     except ModbusException:
  148.         pass
  149. async def main():
  150.     await run_async_client(modbus_calls=run_a_few_calls)
  151. if __name__ == "__main__":
  152.     asyncio.run(main(), debug=True)
复制代码
相关说明


  • pymodbus.datastore.ModbusSequentialDataBlock
    在 Modbus 协议中,数据通常被组织成多个数据块,而每个数据块包含一定数量的数据寄存器、者线圈或者离散量。该类是一个用于创建顺序排列的Modbus顺序数据存储数据块的类。
    例如:
    1. ModbusSequentialDataBlock(0x00, [17] * 100) # 创建了一个从地址 0x00 开始,包含 100(即包含100个地址) 个初始值为 17 的数据块
    复制代码
    实践时发现,此时通过read_coils读取线圈,读取线圈起始地址不能超过99,否则服务端会报错Exception Response(129, 1, IllegalAddress),
    1. client.read_coils(98, 1, slave=0) # 可正常读取
    2. client.read_coils(99, 1, slave=0) # 报错
    复制代码
    修改下服务端数据块起始地址
    1. ModbusSequentialDataBlock(0x01, [17] * 100) # 创建了一个从地址 0x01 开始
    复制代码
    实践时发现,此时通过read_coils读取线圈,读取线圈起始地址不能超过100,否则服务端会报错Exception Response(129, 1, IllegalAddress),
    1. client.read_coils(99, 1, slave=0) # 可正常读取
    2. client.read_coils(100, 1, slave=0) # 报错
    复制代码
  • pymodbus.datastore.ModbusSparseDataBlock
    用于创建稀疏数据块的类。该类允许创建包含不连续地址的数据块(可随机访问)。具体来说,可以在数据块中指定特定地址的数据,而无需为数据块的每个地址都分配内存。这种方式可以有效地节省内存空间,尤其是在处理大量数据时。
    例如:
    1. sparse = ModbusSparseDataBlock({10: [3, 5, 6, 8], 30: 1, 40: [0]*20})
    复制代码
    创建一个拥有3个地址的数据块。
    一个地址从0x10开始,长度为4(即包含4个地址),初始值分别为3,5,6,8,一个地址从0x30开始,长度为1,初始值为10,一个地址从0x40开始,长度为20,初始为0
    1. sparse = ModbusSparseDataBlock([10]*100)
    复制代码
    创建从地址0x00开始,长度为100,初始值为10的数据块
    1. sparse = ModbusSparseDataBlock() # 创建空的数据块
    2. sparse.setValues(0, [10]*10)  # 添加从地址0x00开始,长度为10,值为10的数据块
    3. sparse.setValues(30, [20]*5) # 添加从地址0x30开始,长度为5, 值为20的数据块
    4. # 注意,除非执行类__init__初始化函数时,将 mutable 属性设置为True(默认值),否则无法使用 setValues 函数来添加新的数据块
    复制代码
  • pymodbus.datastore.ModbusSlaveContext
    用于创建每种数据访问都存储在一个块中的一个modbus数据模型。该类可用来模拟 Modbus 从设备上下文。可以在这个上下文中添加多个不同类型的数据块,模拟一个完整的 Modbus 从设备。
    例子:
    1. ModbusSlaveContext(
    2.                 di=datablock(), # 输入离散量(Discrete Inputs)
    3.                 co=datablock(), # 输出线圈 (Coils)
    4.                 hr=datablock(), # 保持寄存器(Holding Register)
    5.                 ir=datablock(), # 输入寄存器(Input Register)
    6.             )
    复制代码
  • pymodbus.datastore.ModbusServerContext
    这表示从上下文的主集合,用于创建一个服务器上下文,并将从站上下文添加到服务器上下文中。
    如果初始化时,属性single被设置为True,它将被视为单个上下文(所有的从设备共享相同的 Modbus 地址空间,没有独立的地址范围),因此每个slave ID都返回相同的上下文。通过分析源码可知,当single被设置为True时,会创建一个从设备上下文,设备地址默认为 0,
    如果single设置为False,它将被解释为从站上下文的集合从属上下文(每个从设备都有独立的 Modbus 地址空间,它们的地址范围是相互独立的)
  • pymodbus.client.mixin.ModbusClientMixin

    • def read_coils(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)
      读线圈(功能码 0x01)

      • address  要读取数据的起始地址
      • count  可选参数,要读取的线圈数量(针对read_coils,发现count设置大于1的数和设置为1是一样的效果)
      • slave 可选参数,Modbus从机ID(实践发现,服务端构建服务器实例时,如果single设置为True时, 这里的slave只要不超出合法值范围,可以随便填,但是如果single设置为False,则必须填写正确的从机ID)
      • kwargs可选参数,实验性参数
      异常抛出ModbusException,下同,不再赘述

    • def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)
      读输入离散量(对应功能码 0x02)
      参数说明参考read_coils
    • def read_holding_registers(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)
      读保持寄存器(对应功能码 0x03)
      参数说明参考read_coils
    • def read_input_registers(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)
      读输入寄存器(对应功能码 0x04)
      参数说明参考read_coils
    • def write_coil(self, address: int, value: bool, slave: int = 0, **kwargs: Any)
      写单个线圈(对应功能码 0x05)

      • address  要写入数据的起始地址
      • value  要写入的布尔值
      • slave 可选参数,Modbus从机ID
      • kwargs可选参数,实验性参数

    • def  write_coils( self, address: int, values: list[bool] | bool, slave: int = 0, **kwargs: Any)
      写多个线圈(对应功能码 0x0F)

      • address  要写入数据的起始地址
      • values  要写入的布尔值列表、或者单个布尔值
      • slave 可选参数,Modbus从机ID
      • kwargs可选参数,实验性参数

    • def write_register(self, address: int, value: int, slave: int = 0, **kwargs: Any)
      写单个寄存器(功能码 0x06)

      • address  要写入数据的起始地址
      • value  要写入的整数
      • slave 可选参数,Modbus从机ID
      • kwargs可选参数,实验性参数

    • def write_registers( self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any)
      写多个寄存器(功能码 0x10)

      • address  要写入数据的起始地址
      • values  要写入的整数列表、或者单个整数
      • slave 可选参数,Modbus从机ID
      • kwargs可选参数,实验性参数




为服务器设置初始化 payload实现

server_payload.py
  1. #!/usr/bin/env python3
  2. '''Pymodbus服务器Payload示例。
  3. 此示例展示如何使用builder初始化具复杂的内存layout的服务器
  4. '''
  5. import asyncio
  6. import logging
  7. _logger = logging.getLogger('logger')
  8. from pymodbus.constants import Endian
  9. from pymodbus.datastore import (
  10.     ModbusSequentialDataBlock,
  11.     ModbusServerContext,
  12.     ModbusSlaveContext
  13. )
  14. from pymodbus.server import StartAsyncTcpServer
  15. from pymodbus.payload import BinaryPayloadBuilder
  16. async def run_async_server():
  17.     """Run server."""
  18.     builder = BinaryPayloadBuilder(byteorder=Endian.LITTLE, wordorder=Endian.LITTLE)
  19.     builder.add_string("abcdefgh")
  20.     builder.add_bits([0, 1, 0, 1, 1, 0, 1, 0])
  21.     builder.add_8bit_int(-0x12)
  22.     builder.add_8bit_uint(0x12)
  23.     builder.add_16bit_int(-0x5678)
  24.     builder.add_16bit_uint(0x1234)
  25.     builder.add_32bit_int(-0x1234)
  26.     builder.add_32bit_uint(0x12345678)
  27.     builder.add_16bit_float(12.34)
  28.     builder.add_16bit_float(-12.34)
  29.     builder.add_32bit_float(22.34)
  30.     builder.add_32bit_float(-22.34)
  31.     builder.add_64bit_int(-0xDEADBEEF)
  32.     builder.add_64bit_uint(0x12345678DEADBEEF)
  33.     builder.add_64bit_uint(0xDEADBEEFDEADBEED)
  34.     builder.add_64bit_float(123.45)
  35.     builder.add_64bit_float(-123.45)
  36.     datablock = ModbusSequentialDataBlock(1, builder.to_registers())
  37.     context = ModbusSlaveContext(
  38.         di=datablock, co=datablock, hr=datablock, ir=datablock # 注意,datablock不能加括号()
  39.     )
  40.     single = True
  41.     # 构建数据存储
  42.     context = ModbusServerContext(slaves=context, single=single)
  43.     txt = f'### start ASYNC server, listening on 5020 - tcp'
  44.     _logger.info(txt)
  45.     address = ('', 5020)
  46.     server = await StartAsyncTcpServer(
  47.         context=context,  # 数据存储
  48.         # TBD host=
  49.         # TBD port=
  50.         address=address,  # 监听地址
  51.         # custom_functions=[],  # 允许自定义处理函数
  52.         framer='socket',  # 使用的帧策略
  53.         # ignore_missing_slaves=True,  # 忽略对缺失的slave的请求
  54.         # broadcast_enable=False,  # 是否允许广播 将 slave id 0视为广播地址
  55.         # timeout=1  # 等待请求完成的时间 # waiting time for request to complete
  56.     )
  57.     return server
  58. async def async_helper():
  59.     _logger.info("Starting server...")
  60.     await run_async_server()
  61. if __name__ == "__main__":
  62.     asyncio.run(async_helper(), debug=True)
复制代码
带有更新任务的服务器代码实现

server_updating.py
  1. #!/usr/bin/env python3
  2. '''带有更新任务的Pymodbus异步服务器示例。
  3. 异步服务器以及随服务器一起连续运行并更新值的任务示例
  4. '''
  5. import asyncio
  6. import logging
  7. _logger = logging.getLogger('logger')
  8. from pymodbus.datastore import (
  9.     ModbusSequentialDataBlock,
  10.     ModbusServerContext,
  11.     ModbusSlaveContext
  12. )
  13. from pymodbus.server import StartAsyncTcpServer
  14. async def updating_task(context):
  15.     '''更新服务器中的数据值
  16.     此任务伴随服务器持续运行,它将每两秒增加一些值
  17.     需要注意的是,getValues和setValues不是并发安全的
  18.     '''
  19.     # fc_as_hex = 3 # 功能码,例如3、0x03 表示读保持寄存器
  20.     fc_as_hex = 4
  21.     slave_id = 0x00 # 从节点ID
  22.     address = 0x00  # 数据读取起始地址
  23.     count = 6 # 需要获取的值的数量
  24.     values = context[slave_id].getValues(fc_as_hex, address, count=count)
  25.     # set values to zero
  26.     values = [0 for v in values]
  27.     context[slave_id].setValues(fc_as_hex, address, values)
  28.     txt = f'updating_task: started: initialised values: {values!s} at address {address!s}'
  29.     print(txt)
  30.     _logger.debug(txt)
  31.     # 循环递增
  32.     while True:
  33.         await asyncio.sleep(2)
  34.         values = context[slave_id].getValues(fc_as_hex, address, count=count)
  35.         values = [v + 1 for v in values]
  36.         context[slave_id].setValues(fc_as_hex, address, values)
  37.         txt = f'updating_task: incremented values: {values!s} at address {address!s}'
  38.         print(txt)
  39.         _logger.debug(txt)
  40. async def run_async_server(context):
  41.     """Run server."""
  42.     txt = f'### start ASYNC server, listening on 5020 - tcp'
  43.     _logger.info(txt)
  44.     address = ('', 5020)
  45.     server = await StartAsyncTcpServer(
  46.         context=context,  # 数据存储
  47.         # TBD host=
  48.         # TBD port=
  49.         address=address,  # 监听地址
  50.         # custom_functions=[],  # 允许自定义处理函数
  51.         framer='socket',  # 使用的帧策略
  52.         # ignore_missing_slaves=True,  # 忽略对缺失的slave的请求
  53.         # broadcast_enable=False,  # 是否允许广播 将 slave id 0视为广播地址
  54.         # timeout=1  # 等待请求完成的时间 # waiting time for request to complete
  55.     )
  56.     return server
  57. async def async_helper():
  58.     datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)  # pylint: disable=unnecessary-lambda-assignment
  59.     context = ModbusSlaveContext(
  60.         di=datablock(), co=datablock(), hr=datablock(), ir=datablock() # 注意,datablock不能加括号()
  61.     )
  62.     single = True
  63.     # 构建数据存储
  64.     context = ModbusServerContext(slaves=context, single=single)
  65.     task = asyncio.create_task(updating_task(context))
  66.     task.set_name("example updating task")
  67.     _logger.info("Starting server...")
  68.     await run_async_server(context)
  69.     task.cancel()
  70. if __name__ == "__main__":
  71.     asyncio.run(async_helper(), debug=True)
复制代码
客户端访问验证
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import logging
  4. import pymodbus.client as modbusClient
  5. from pymodbus import ModbusException
  6. _logger = logging.getLogger('logger')
  7. async def run_async_client(modbus_calls=None):
  8.     """Run sync client."""
  9.     _logger.info("### Create client object")
  10.     client = modbusClient.AsyncModbusTcpClient(
  11.         '127.0.0.1',
  12.         port=5020,  # on which port
  13.         # Common optional parameters:
  14.         framer='socket', # 客户端访问服务器超时时间(该参数仅用于客户端(slave节点)),float型
  15.         timeout=10,
  16.         retries=3,
  17.         reconnect_delay=1,
  18.         reconnect_delay_max=10,
  19.         #    retry_on_empty=False,
  20.         # TCP setup parameters
  21.         #    source_address=("localhost", 0),
  22.     )
  23.     _logger.info("### Client starting")
  24.     await client.connect()
  25.     assert client.connected
  26.     if modbus_calls:
  27.         await modbus_calls(client)
  28.     client.close()
  29.     _logger.info("### End of Program")
  30. async def run_a_few_calls(client):
  31.     try:
  32.         # 读保持寄存器
  33.         # rr = await client.read_holding_registers(0, 4)
  34.         # print(rr.registers)
  35.         # 读输入寄存器
  36.         rr = await client.read_input_registers(0, 7, slave=0)
  37.         print(rr.registers)
  38.     except ModbusException:
  39.         pass
  40. async def main(cmdline=None):
  41.     """Combine setup and run."""
  42.     await run_async_client(modbus_calls=run_a_few_calls)
  43. if __name__ == "__main__":
  44.     asyncio.run(main(), debug=True)
复制代码
  1. class ModbusSlaveContext(ModbusBaseSlaveContext):
  2.     def getValues(self, fc_as_hex, address, count=1):
  3.         """Get `count` values from datastore.
  4.         :param fc_as_hex: The function we are working with
  5.         :param address: The starting address
  6.         :param count: The number of values to retrieve
  7.         :returns: The requested values from a:a+c
  8.         """
  9.         if not self.zero_mode:
  10.             address += 1
  11.         Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
  12.         return self.store[self.decode(fc_as_hex)].getValues(address, count)
  13.         
  14.     def setValues(self, fc_as_hex, address, values):
  15.         """Set the datastore with the supplied values.
  16.         :param fc_as_hex: The function we are working with
  17.         :param address: The starting address
  18.         :param values: The new values to be set
  19.         """
  20.         if not self.zero_mode:
  21.             address += 1
  22.         Log.debug("setValues[{}] address-{}: count-{}", fc_as_hex, address, len(values))
  23.         self.store[self.decode(fc_as_hex)].setValues(address, values)
复制代码
同步服务器和异步客户端实现

同步服务器代码实现

server_sync.py
  1. #!/usr/bin/env python3
  2. '''Pymodbus 同步服务器示例
  3. '''
  4. import logging
  5. from pymodbus.datastore import (
  6.     ModbusSequentialDataBlock,
  7.     ModbusServerContext,
  8.     ModbusSlaveContext
  9. )
  10. from pymodbus.server import StartTcpServer
  11. _logger = logging.getLogger('logger')
  12. def run_sync_server():
  13.     # 连续的、无间隙顺序存储数据块(寄存器块)
  14.     datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)
  15.     context = ModbusSlaveContext(
  16.         di=datablock(), co=datablock(), hr=datablock(), ir=datablock()
  17.     )
  18.     single = True
  19.     # 构建数据存储
  20.     context = ModbusServerContext(slaves=context,
  21.                                   single=single
  22.                                   )
  23.     txt = f'### start SYNC server'
  24.     _logger.info(txt)
  25.     address = ('', 5020)
  26.     server = StartTcpServer(
  27.         context=context,  # Data storage
  28.         # identity=identity,  # server identify
  29.         # TBD host=
  30.         # TBD port=
  31.         address=address,  # listen address
  32.         # custom_functions=[],  # allow custom handling
  33.         framer='socket',  # The framer strategy to use
  34.         # ignore_missing_slaves=True,  # ignore request to a missing slave
  35.         # broadcast_enable=False,  # treat slave_id 0 as broadcast address,
  36.         # timeout=1,  # waiting time for request to complete
  37.     )
  38.     return server
  39. def sync_helper():
  40.     server = run_sync_server()
  41.     server.shutdown()
  42. if __name__ == "__main__":
  43.     sync_helper()
复制代码
同步客户端代码实现

client_sync.py
  1. #!/usr/bin/env python3
  2. '''Pymodbus同步客户端示例
  3. '''
  4. import logging
  5. import pymodbus.client as modbusClient
  6. from pymodbus import ModbusException
  7. _logger = logging.getLogger('logger')
  8. def run_sync_client(modbus_calls=None):
  9.     """Run sync client."""
  10.     _logger.info("### Create client object")
  11.     client = modbusClient.ModbusTcpClient(
  12.         '127.0.0.1',
  13.         port=5020,  # on which port
  14.         # Common optional parameters:
  15.         framer='socket', # 客户端访问服务器超时时间(该参数仅用于客户端(slave节点)),float型
  16.         timeout=10,
  17.         retries=3,
  18.         reconnect_delay=1,
  19.         reconnect_delay_max=10,
  20.         #    retry_on_empty=False,
  21.         # TCP setup parameters
  22.         #    source_address=("localhost", 0),
  23.     )
  24.     _logger.info("### Client starting")
  25.     client.connect()
  26.     assert client.connected
  27.     if modbus_calls:
  28.         modbus_calls(client)
  29.     client.close()
  30.     _logger.info("### End of Program")
  31. def run_a_few_calls(client):
  32.     try:
  33.         # 读取线圈状态
  34.         rr = client.read_coils(0, 1, slave=1) # 从 0x00 地址开始读取1个线圈
  35.         print(rr.bits) # 输出:[True, False, False, False, False, False, False, False]
  36.         # assert len(rr.bits) == 8
  37.     except ModbusException:
  38.         pass
  39. def main():
  40.     run_sync_client(modbus_calls=run_a_few_calls)
  41. if __name__ == "__main__":
  42.     main()
复制代码
参考链接

https://pypi.org/project/pymodbus/
https://pymodbus.readthedocs.io/en/dev/index.html

来源:https://www.cnblogs.com/shouke/p/18342169
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具