Python pymodbus類庫使用學習總結

授客發表於2024-08-04

實踐環境

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

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

修改原始碼是為了更方便使用類庫自帶日誌列印器

# ...略
class Log:
    """Class to hide logging complexity.

    :meta private:
    """

    # _logger = logging.getLogger(__name__) # commented by shouke
    _logger = logging.getLogger('logger') # added by shouke # 方便在不同模組獲取日誌列印器

    @classmethod
    def apply_logging_config(cls, level, log_file_name):
        """Apply basic logging configuration."""
        if level == logging.NOTSET:
            level = cls._logger.getEffectiveLevel()
        if isinstance(level, str):
            level = level.upper()
        log_stream_handler = logging.StreamHandler()
        log_formatter = logging.Formatter(
            "%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s"
        )

        log_stream_handler.setFormatter(log_formatter)
        ##################### commented by shouke #################
        # cls._logger.addHandler(log_stream_handler)
        # if log_file_name:
        #     log_file_handler = logging.FileHandler(log_file_name)
        #     log_file_handler.setFormatter(log_formatter)
        #     cls._logger.addHandler(log_file_handler)
        ##########################################################
        ##################### added by shouke #################
        if not cls._logger.handlers: # 如果不加這個判斷,在不同模組都執行pymodbus_apply_logging_config函式時,會導致同一條日誌被重複列印
            cls._logger.addHandler(log_stream_handler)
            if log_file_name:
                log_file_handler = logging.FileHandler(log_file_name)
                log_file_handler.setFormatter(log_formatter)
                cls._logger.addHandler(log_file_handler)
        ##########################################################
        cls.setLevel(level)

# ...略
        
pymodbus_apply_logging_config(level=logging.INFO) # 統一日誌列印器配置

非同步伺服器和非同步客戶端實現

非同步伺服器程式碼實現

server_async.py

#!/usr/bin/env python3
'''Pymodbus 非同步伺服器示例

多執行緒非同步伺服器的一個示例。
'''

import asyncio

import logging
### 不修改 PYTHON_HOME\Lib\site-packages\pymodbus\logging.py原始碼的基礎上,獲取日誌列印器相關方法及說明:
# 方法1
# _logger = logging.getLogger(__name__) # 採用該_logger列印的日誌看不到
# logging.basicConfig(level=logging.DEBUG) # 不註釋上行程式碼的基礎上新增這行程式碼,日誌能列印出來,但是日誌所在模組不為當前模組

# 方法2
# _logger = logging.getLogger(__file__)
# logging.basicConfig(level=logging.INFO) #  新增這行程式碼,確保日誌能列印出來,但是列印出來的日誌所在模組不為當前模組

# 方法3:
# from pymodbus.logging import Log,pymodbus_apply_logging_config
# pymodbus_apply_logging_config(level=logging.INFO) # 如果缺少這行程式碼,下面async_helper中的日誌列印將無法在控制檯輸出
# _logger = Log._logger

# 修改 PYTHON_HOME\Lib\site-packages\pymodbus\logging.py原始碼的基礎上,獲取日誌列印器相關方法:
_logger = logging.getLogger('logger')
# _logger.setLevel(logging.INFO)


from pymodbus import __version__ as pymodbus_version
from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusServerContext,
    ModbusSlaveContext,
    ModbusSparseDataBlock,
)
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.server import (
    StartAsyncSerialServer,
    StartAsyncTcpServer,
    StartAsyncTlsServer,
    StartAsyncUdpServer,
)


# hook函式
def server_request_tracer(request, *_addr):
    """跟蹤請求

    所有伺服器請求在被處理之前都經過此過濾器
    """
    _logger.info(f'---> REQUEST: {request}') # 輸出,類似這樣: ---> REQUEST: ReadBitRequest(0,1)

def server_response_manipulator( response):
    """操縱響應

    所有伺服器響應在傳送之前都透過此過濾器
    過濾器返回:
    - 響應,原始或修改後的
    - 跳過編碼,發出是否對響應進行編碼的訊號
    """
    _logger.info(f'---> RESPONSE: {response}') # 輸出,類似這樣:---> RESPONSE: ReadCoilsResponse(1)
    # response.should_respond = False # 如果讓該行程式碼生效,則客戶端收不到伺服器響應
    return response, False


class Args:
    comm = 'tcp' # 通訊模式,可選值 tcp、udp serial、tls
    comm_defaults = {
        'tcp': ['socket', 5020],
        # 'tcp': ['rtu', 5020], # 如果採用ModbusRTU協議 則使用這個
        'udp': ['socket', 5020],
        # 'serial': ['rtu', '/dev/ptyp0'], # Linux
        'serial': ['rtu', 'COM1']  # Windows(本例中COM1和COM2是成對的)
        'tls': ['tls', 5020]
    }

    framer = comm_defaults[comm][0] # 幀型別
    port = comm_defaults[comm][1] # 站點埠
    host = ''
    baudrate = 9600 # 序列裝置波特率 即每秒傳輸的位數

    # 連續的、無間隙順序儲存資料塊(暫存器塊)
    datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)  # pylint: disable=unnecessary-lambda-assignment

    # 連續的,或者可能有間隙的稀疏、不規則儲存資料塊
    # datablock = lambda : ModbusSparseDataBlock({0x00: 0, 0x05: 1})  # pylint: disable=unnecessary-lambda-assignment

    # 工廠模式
    # datablock = lambda : ModbusSequentialDataBlock.create()  # pylint: disable=unnecessary-lambda-assignment,unnecessary-lambda

    # 如果從節點數量不為0
    #伺服器使用伺服器上下文,該上下文允許伺服器,以針對不同的從裝置ID使用不同的從站上下文(slave context)進行響應。
    # 預設情況下,它將為提供的每個slave ID返回相同的上下文(廣播模式)。
    # 但是,可以透過將single標識設定為False並且提供slave ID到上下文對映的字典來覆蓋這種行為:
    # 從機上下文也可以按zero_mode初始化,這意味著到地址(0-7)的請求將對映到地址(0-7)。
    # 預設值為False,因此地址(0-7)將對映到(1-8):
    # context = {
    #     0x01: ModbusSlaveContext( # 0x01為從裝置、從機地址
    #         di=datablock(), # 輸入離散量
    #         co=datablock(), # 輸出線圈
    #         hr=datablock(), # 保持暫存器
    #         ir=datablock(), # 輸入暫存器
    #     ),
    #     0x02: ModbusSlaveContext(
    #         di=datablock(),
    #         co=datablock(),
    #         hr=datablock(),
    #         ir=datablock(),
    #     ),
    #     0x03: ModbusSlaveContext(
    #         di=datablock(),
    #         co=datablock(),
    #         hr=datablock(),
    #         ir=datablock(),
    #         zero_mode=True,
    #     ),
    #     }
    # single = False

    #如果從節點數量為0
    context = ModbusSlaveContext(
        di=datablock(), co=datablock(), hr=datablock(), ir=datablock()
    )
    single = True

    # 構建資料儲存
    context = ModbusServerContext(slaves=context,
                                  single=single
                                  )

    # ----------------------------------------------------------------------- #
    # 初始化伺服器資訊
    # 不對屬性欄位做任何設定,則欄位值預設為空字串
    # ----------------------------------------------------------------------- #
    identity = ModbusDeviceIdentification(
        info_name={
            "VendorName": "Pymodbus",
            "ProductCode": "PM",
            "VendorUrl": "https://github.com/pymodbus-dev/pymodbus/",
            "ProductName": "Pymodbus Server",
            "ModelName": "Pymodbus Server",
            "MajorMinorRevision": pymodbus_version,
        }
    )

    # sslctx=sslctx,  # 用於TLS的SSLContext (預設為None, 自動建立)
    cert_file_path = './certificates/pymodbus.crt' # 用於TLS 證書檔案路徑  (如果sslctx為None時使用該檔案)
    key_file_path = './certificates/pymodbus.key'  # 用於TLS 私鑰檔案路徑 (如果sslctx為None時使用該檔案)


async def run_async_server():
    """Run server."""

    txt = f'### start ASYNC server, listening on {Args.port} - {Args.comm}'
    _logger.info(txt)

    if Args.comm == 'tcp':
        address = (Args.host if Args.host else '', Args.port if Args.port else None)
        server = await StartAsyncTcpServer(
            context=Args.context,  # 資料儲存
            identity=Args.identity,  # 伺服器標識
            # TBD host=
            # TBD port=
            address=address,  # 監聽地址
            # custom_functions=[],  # 允許自定義處理函式
            framer=Args.framer,  # 使用的幀策略
            # ignore_missing_slaves=True,  # 忽略對缺失的slave的請求
            # broadcast_enable=False,  # 是否允許廣播 將 slave id 0視為廣播地址
            # timeout=1  # 等待請求完成的時間 # waiting time for request to complete

            # 可選引數,實現hook功能
            request_tracer=server_request_tracer,
            response_manipulator=server_response_manipulator
        )
    elif Args.comm == 'udp':
        address = (
            Args.host if Args.host else "127.0.0.1",
            Args.port if Args.port else None,
        )
        server = await StartAsyncUdpServer(
            context=Args.context,
            identity=Args.identity,
            address=address,
            # custom_functions=[],
            framer=Args.framer,
            # ignore_missing_slaves=True,
            # broadcast_enable=False,
            # timeout=1
        )
    elif Args.comm == 'serial':
        # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600
        #             PTY,link=/tmp/ttyp0,raw,echo=0,ospeed=9600
        server = await StartAsyncSerialServer(
            context=Args.context,
            identity=Args.identity,
            # timeout=1,  # waiting time for request to complete
            port=Args.port,  # 串列埠
            # custom_functions=[],
            framer=Args.framer,
            # stopbits=1,  # 要使用的停止位數(The number of stop bits to use)
            # bytesize=8,  # 序列化訊息位元組大小(The bytesize of the serial messages)
            # parity="N",  # 使用哪種奇偶校驗
            baudrate=Args.baudrate,  # 用於序列裝置的波特率
            # handle_local_echo=False,  # 處理USB-to-RS485介面卡的本地echo(Handle local echo of the USB-to-RS485 adaptor)
            # ignore_missing_slaves=True,  # ignore request to a missing slave
            # broadcast_enable=False,  #
            # strict=True,  # 使用嚴格的計時,針對Modbus RTU 為t1.5(use strict timing, t1.5 for Modbus RTU)
        )
    elif Args.comm == 'tls':
        address = (Args.host if Args.host else '', Args.port if Args.port else None)
        server = await StartAsyncTlsServer(
            context=Args.context,  # Data storage
            host='localhost',  # 定義用於連線的tcp地址
            # port=port,  # tcp監聽埠
            identity=Args.identity,  # server identify
            # custom_functions=[],  # allow custom handling
            address=address,
            framer=Args.framer,
            certfile=Args.cert_file_path,  # The cert file path for TLS (used if sslctx is None)
            # sslctx=sslctx,  # The SSLContext to use for TLS (default None and auto create)
            keyfile=Args.key_file_path,  # The key file path for TLS (used if sslctx is None)
            # password="none",  # 用於解密私鑰檔案的密碼
            # ignore_missing_slaves=True,
            # broadcast_enable=False,
            # timeout=1
        )
    return server


async def async_helper():
    _logger.info("Starting server...")
    await run_async_server()


if __name__ == "__main__":
    asyncio.run(async_helper(), debug=True)

非同步客戶端程式碼實現

client_async.py

#!/usr/bin/env python3

'''Pymodbus非同步客戶端示例
'''
import asyncio
import logging

import pymodbus.client as modbusClient
from pymodbus import ModbusException

_logger = logging.getLogger('logger')

class Args:
    comm = 'tcp' # 通訊模式,可選值 tcp、udp serial、tls
    comm_defaults = {
        'tcp': ['socket', 5020],
        # 'tcp': ['rtu', 5020], # 如果採用ModbusRTU協議 則使用這個
        'udp': ['socket', 5020],
        # 'serial': ['rtu', '/dev/ptyp0'], # Linux
        'serial': ['rtu', 'COM2']  # Windows(本例中COM1和COM2是用Virtual Serial Port Driver 6.9軟體成對新增的虛擬埠)
        'tls': ['tls', 5020]
    }

    framer = comm_defaults[comm][0] # 幀型別
    port = comm_defaults[comm][1] # 站點埠
    host = '127.0.0.1' # 服務端地址
    baudrate = 9600 # 序列裝置波特率

    timeout = 10 # 客戶端訪問伺服器超時時間(該引數僅用於客戶端(slave節點)),float型

    # sslctx=sslctx,  # 用於TLS的SSLContext (預設為None, 自動建立)
    cert_file_path = './certificates/pymodbus.crt' # 用於TLS 證書檔案路徑  (如果sslctx為None時使用該檔案)
    key_file_path = './certificates/pymodbus.key'  # 用於TLS 私鑰檔案路徑 (如果sslctx為None時使用該檔案)


async def run_async_client(modbus_calls=None):
    """Run sync client."""

    _logger.info("### Create client object")
    if Args.comm == "tcp":
        client = modbusClient.AsyncModbusTcpClient(
            Args.host,
            port=Args.port,  # on which port
            # Common optional parameters:
            framer=Args.framer,
            timeout=Args.timeout,
            retries=3,
            reconnect_delay=1,
            reconnect_delay_max=10,
            #    retry_on_empty=False,
            # TCP setup parameters
            #    source_address=("localhost", 0),
        )
    elif Args.comm == "udp":
        client = modbusClient.AsyncModbusUdpClient(
            Args.host,
            port=Args.port,
            # Common optional parameters:
            framer=Args.framer,
            timeout=Args.timeout,
            #    retries=3,
            #    retry_on_empty=False,
            # UDP setup parameters
            #    source_address=None,
        )
    elif Args.comm == "serial":
        client = modbusClient.AsyncModbusSerialClient(
            Args.port,
            # Common optional parameters:
            #    framer=ModbusRtuFramer,
            timeout=Args.timeout,
            #    retries=3,
            #    retry_on_empty=False,
            # Serial setup parameters
            baudrate=Args.baudrate,
            #    bytesize=8,
            #    parity="N",
            #    stopbits=1,
            #    handle_local_echo=False,
            #    strict=True,
        )
    elif Args.comm == "tls":
        client = modbusClient.AsyncModbusTlsClient(
            Args.host,
            port=Args.port,
            # Common optional parameters:
            framer=Args.framer,
            timeout=Args.timeout,
            #    retries=3,
            #    retry_on_empty=False,
            # TLS setup parameters
            sslctx=modbusClient.AsyncModbusTlsClient.generate_ssl(
                certfile=Args.cert_file_path,
                keyfile=Args.key_file_path,
            #    password="none",
            ),
            server_hostname="localhost",
        )

    _logger.info("### Client starting")
    await client.connect()
    assert client.connected
    if modbus_calls:
        await modbus_calls(client)
    client.close()
    _logger.info("### End of Program")


async def run_a_few_calls(client):
    
    try:
        # 讀取線圈狀態
        rr = await client.read_coils(0, 1, slave=0) # 從 0x00 地址開始讀取1個線圈
        print(rr.bits) # 輸出:[True, False, False, False, False, False, False, False]
        # assert len(rr.bits) == 8
        print(rr) # 輸出:ReadCoilsResponse(8)

        # 讀輸入離散量
        rr = await client.read_holding_registers(0, 1, slave=0) # 從 0x00 地址開始讀取1個線圈
        print(rr.registers) # 輸出:[17]

        rr = await client.read_holding_registers(4, 2, slave=0) # 從0x04 地址開始讀取2個線圈
        # assert rr.registers[0] == 17
        # assert rr.registers[1] == 17
        print(rr.registers) # 輸出:[17, 17]

        # 讀保持暫存器
        rr = await client.read_holding_registers(5, 4) # 從 0x05 地址開始讀取4個線圈
        print(rr.registers) # 輸出:[17, 17, 17, 17]

        # 讀輸入暫存器
        rr = await client.read_input_registers(0x0F, 3, slave=0) # 從 0x0F 地址開始讀取4個線圈
        print(rr.registers) # 輸出:[17, 17, 17]
        rr = await client.read_input_registers(15, 3, slave=0) # 從 0x0F 地址開始讀取4個線圈
        print(rr) # 輸出:ReadInputRegistersResponse (3)
        print(rr.registers) # 輸出:[17, 17, 17]

        # 寫單個線圈
        rr = await client.write_coil(9, False, slave=0) # 將布林值False寫入 0x09 地址
        print(rr) # 輸出:WriteCoilResponse(9) => False
        rr = await client.read_coils(9, 1, slave=0)
        print(rr.bits) # 輸出:[False, False, False, False, False, False, False, False]

        # 寫多個線圈
        await client.write_coils(10, False, slave=0) # 將布林值False寫入 0x0A 地址
        rr = await client.read_coils(10, 1, slave=0)
        print(rr.bits) # 輸出:[False, False, False, False, False, False, False, False]
        rr = await client.read_coils(11, 1, slave=0)
        print(rr.bits) # 輸出:[True, False, False, False, False, False, False, False]

        await client.write_coils(10, [False, False], slave=0) # 將布林值False寫入 0x0A 0x0B 地址
        rr = await client.read_coils(11, 1, slave=0)
        print(rr.bits) # 輸出:[False, False, False, False, False, False, False, False]

        # 寫單個保持暫存器
        await client.write_register(12, 0x0F, slave=0) # 將0x0F寫入 0x0C 地址
        rr = await client.read_input_registers(12, 3, slave=0)
        print(rr.registers) # 輸出:[17, 17, 17]
        rr = await client.read_holding_registers(12, 4)
        print(rr.registers) # 輸出:[15, 17, 17, 17]

        # 寫多個保持暫存器
        await client.write_registers(13, 0x0F, slave=0) # 將0x0F寫入 0x0D 地址
        rr = await client.read_holding_registers(13, 2)
        print(rr.registers) # 輸出:[15, 17]

        await client.write_registers(13, [0x0F, 0x0E], slave=0) # 將0x0F寫入 0x0D,0x0E 地址
        rr = await client.read_holding_registers(13, 2)
        print(rr.registers) # 輸出:[15, 14]
    except ModbusException:
        pass


async def main():

    await run_async_client(modbus_calls=run_a_few_calls)


if __name__ == "__main__":
    asyncio.run(main(), debug=True)

相關說明

  1. pymodbus.datastore.ModbusSequentialDataBlock

    在 Modbus 協議中,資料通常被組織成多個資料塊,而每個資料塊包含一定數量的資料暫存器、者線圈或者離散量。該類是一個用於建立順序排列的Modbus順序資料儲存資料塊的類。

    例如:

    ModbusSequentialDataBlock(0x00, [17] * 100) # 建立了一個從地址 0x00 開始,包含 100(即包含100個地址) 個初始值為 17 的資料塊
    

    實踐時發現,此時透過read_coils讀取線圈,讀取線圈起始地址不能超過99,否則服務端會報錯Exception Response(129, 1, IllegalAddress)

    client.read_coils(98, 1, slave=0) # 可正常讀取
    client.read_coils(99, 1, slave=0) # 報錯
    

    修改下服務端資料塊起始地址

    ModbusSequentialDataBlock(0x01, [17] * 100) # 建立了一個從地址 0x01 開始
    

    實踐時發現,此時透過read_coils讀取線圈,讀取線圈起始地址不能超過100,否則服務端會報錯Exception Response(129, 1, IllegalAddress)

    client.read_coils(99, 1, slave=0) # 可正常讀取
    client.read_coils(100, 1, slave=0) # 報錯
    
  2. pymodbus.datastore.ModbusSparseDataBlock

    用於建立稀疏資料塊的類。該類允許建立包含不連續地址的資料塊(可隨機訪問)。具體來說,可以在資料塊中指定特定地址的資料,而無需為資料塊的每個地址都分配記憶體。這種方式可以有效地節省記憶體空間,尤其是在處理大量資料時。

    例如:

    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

    sparse = ModbusSparseDataBlock([10]*100)
    

    建立從地址0x00開始,長度為100,初始值為10的資料塊

    sparse = ModbusSparseDataBlock() # 建立空的資料塊
    sparse.setValues(0, [10]*10)  # 新增從地址0x00開始,長度為10,值為10的資料塊
    sparse.setValues(30, [20]*5) # 新增從地址0x30開始,長度為5, 值為20的資料塊
    # 注意,除非執行類__init__初始化函式時,將 mutable 屬性設定為True(預設值),否則無法使用 setValues 函式來新增新的資料塊
    
  3. pymodbus.datastore.ModbusSlaveContext

    用於建立每種資料訪問都儲存在一個塊中的一個modbus資料模型。該類可用來模擬 Modbus 從裝置上下文。可以在這個上下文中新增多個不同型別的資料塊,模擬一個完整的 Modbus 從裝置。

    例子:

     ModbusSlaveContext(
                    di=datablock(), # 輸入離散量(Discrete Inputs)
                    co=datablock(), # 輸出線圈 (Coils)
                    hr=datablock(), # 保持暫存器(Holding Register)
                    ir=datablock(), # 輸入暫存器(Input Register)
                )
    
  4. pymodbus.datastore.ModbusServerContext

    這表示從上下文的主集合,用於建立一個伺服器上下文,並將從站上下文新增到伺服器上下文中。

    如果初始化時,屬性single被設定為True,它將被視為單個上下文(所有的從裝置共享相同的 Modbus 地址空間,沒有獨立的地址範圍),因此每個slave ID都返回相同的上下文。透過分析原始碼可知,當single被設定為True時,會建立一個從裝置上下文,裝置地址預設為 0,

    如果single設定為False,它將被解釋為從站上下文的集合從屬上下文(每個從裝置都有獨立的 Modbus 地址空間,它們的地址範圍是相互獨立的)

  5. pymodbus.client.mixin.ModbusClientMixin

    1. 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,下同,不再贅述

    2. def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)

      讀輸入離散量(對應功能碼 0x02)

      引數說明參考read_coils

    3. def read_holding_registers(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)

      讀保持暫存器(對應功能碼 0x03)

      引數說明參考read_coils

    4. def read_input_registers(self, address: int, count: int = 1, slave: int = 0, **kwargs: Any)

      讀輸入暫存器(對應功能碼 0x04)

      引數說明參考read_coils

    5. def write_coil(self, address: int, value: bool, slave: int = 0, **kwargs: Any)

      寫單個線圈(對應功能碼 0x05)

      • address 要寫入資料的起始地址
      • value 要寫入的布林值
      • slave 可選引數,Modbus從機ID
      • kwargs可選引數,實驗性引數
    6. def write_coils( self, address: int, values: list[bool] | bool, slave: int = 0, **kwargs: Any)

      寫多個線圈(對應功能碼 0x0F)

      • address 要寫入資料的起始地址
      • values 要寫入的布林值列表、或者單個布林值
      • slave 可選引數,Modbus從機ID
      • kwargs可選引數,實驗性引數
    7. def write_register(self, address: int, value: int, slave: int = 0, **kwargs: Any)

      寫單個暫存器(功能碼 0x06)

      • address 要寫入資料的起始地址
      • value 要寫入的整數
      • slave 可選引數,Modbus從機ID
      • kwargs可選引數,實驗性引數
    8. 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

#!/usr/bin/env python3
'''Pymodbus伺服器Payload示例。
此示例展示如何使用builder初始化具複雜的記憶體layout的伺服器

'''

import asyncio

import logging
_logger = logging.getLogger('logger')

from pymodbus.constants import Endian
from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusServerContext,
    ModbusSlaveContext
)
from pymodbus.server import StartAsyncTcpServer

from pymodbus.payload import BinaryPayloadBuilder

async def run_async_server():
    """Run server."""

    builder = BinaryPayloadBuilder(byteorder=Endian.LITTLE, wordorder=Endian.LITTLE)
    builder.add_string("abcdefgh")
    builder.add_bits([0, 1, 0, 1, 1, 0, 1, 0])
    builder.add_8bit_int(-0x12)
    builder.add_8bit_uint(0x12)
    builder.add_16bit_int(-0x5678)
    builder.add_16bit_uint(0x1234)
    builder.add_32bit_int(-0x1234)
    builder.add_32bit_uint(0x12345678)
    builder.add_16bit_float(12.34)
    builder.add_16bit_float(-12.34)
    builder.add_32bit_float(22.34)
    builder.add_32bit_float(-22.34)
    builder.add_64bit_int(-0xDEADBEEF)
    builder.add_64bit_uint(0x12345678DEADBEEF)
    builder.add_64bit_uint(0xDEADBEEFDEADBEED)
    builder.add_64bit_float(123.45)
    builder.add_64bit_float(-123.45)

    datablock = ModbusSequentialDataBlock(1, builder.to_registers())

    context = ModbusSlaveContext(
        di=datablock, co=datablock, hr=datablock, ir=datablock # 注意,datablock不能加括號()
    )
    single = True

    # 構建資料儲存
    context = ModbusServerContext(slaves=context, single=single)

    txt = f'### start ASYNC server, listening on 5020 - tcp'
    _logger.info(txt)

    address = ('', 5020)
    server = await StartAsyncTcpServer(
        context=context,  # 資料儲存
        # TBD host=
        # TBD port=
        address=address,  # 監聽地址
        # custom_functions=[],  # 允許自定義處理函式
        framer='socket',  # 使用的幀策略
        # ignore_missing_slaves=True,  # 忽略對缺失的slave的請求
        # broadcast_enable=False,  # 是否允許廣播 將 slave id 0視為廣播地址
        # timeout=1  # 等待請求完成的時間 # waiting time for request to complete
    )
    return server


async def async_helper():

    _logger.info("Starting server...")
    await run_async_server()


if __name__ == "__main__":
    asyncio.run(async_helper(), debug=True)

帶有更新任務的伺服器程式碼實現

server_updating.py

#!/usr/bin/env python3
'''帶有更新任務的Pymodbus非同步伺服器示例。

非同步伺服器以及隨伺服器一起連續執行並更新值的任務示例
'''

import asyncio

import logging
_logger = logging.getLogger('logger')

from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusServerContext,
    ModbusSlaveContext
)
from pymodbus.server import StartAsyncTcpServer


async def updating_task(context):
    '''更新伺服器中的資料值
    此任務伴隨伺服器持續執行,它將每兩秒增加一些值

    需要注意的是,getValues和setValues不是併發安全的
    '''

    # fc_as_hex = 3 # 功能碼,例如3、0x03 表示讀保持暫存器
    fc_as_hex = 4
    slave_id = 0x00 # 從節點ID
    address = 0x00  # 資料讀取起始地址
    count = 6 # 需要獲取的值的數量

    values = context[slave_id].getValues(fc_as_hex, address, count=count)

    # set values to zero
    values = [0 for v in values]
    context[slave_id].setValues(fc_as_hex, address, values)

    txt = f'updating_task: started: initialised values: {values!s} at address {address!s}'
    print(txt)
    _logger.debug(txt)

    # 迴圈遞增
    while True:
        await asyncio.sleep(2)

        values = context[slave_id].getValues(fc_as_hex, address, count=count)
        values = [v + 1 for v in values]
        context[slave_id].setValues(fc_as_hex, address, values)

        txt = f'updating_task: incremented values: {values!s} at address {address!s}'
        print(txt)
        _logger.debug(txt)


async def run_async_server(context):
    """Run server."""

    txt = f'### start ASYNC server, listening on 5020 - tcp'
    _logger.info(txt)

    address = ('', 5020)
    server = await StartAsyncTcpServer(
        context=context,  # 資料儲存
        # TBD host=
        # TBD port=
        address=address,  # 監聽地址
        # custom_functions=[],  # 允許自定義處理函式
        framer='socket',  # 使用的幀策略
        # ignore_missing_slaves=True,  # 忽略對缺失的slave的請求
        # broadcast_enable=False,  # 是否允許廣播 將 slave id 0視為廣播地址
        # timeout=1  # 等待請求完成的時間 # waiting time for request to complete
    )
    return server


async def async_helper():

    datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)  # pylint: disable=unnecessary-lambda-assignment

    context = ModbusSlaveContext(
        di=datablock(), co=datablock(), hr=datablock(), ir=datablock() # 注意,datablock不能加括號()
    )
    single = True

    # 構建資料儲存
    context = ModbusServerContext(slaves=context, single=single)

    task = asyncio.create_task(updating_task(context))
    task.set_name("example updating task")

    _logger.info("Starting server...")
    await run_async_server(context)

    task.cancel()


if __name__ == "__main__":
    asyncio.run(async_helper(), debug=True)

客戶端訪問驗證

#!/usr/bin/env python3

import asyncio
import logging

import pymodbus.client as modbusClient
from pymodbus import ModbusException

_logger = logging.getLogger('logger')

async def run_async_client(modbus_calls=None):
    """Run sync client."""

    _logger.info("### Create client object")
    client = modbusClient.AsyncModbusTcpClient(
        '127.0.0.1',
        port=5020,  # on which port
        # Common optional parameters:
        framer='socket', # 客戶端訪問伺服器超時時間(該引數僅用於客戶端(slave節點)),float型
        timeout=10,
        retries=3,
        reconnect_delay=1,
        reconnect_delay_max=10,
        #    retry_on_empty=False,
        # TCP setup parameters
        #    source_address=("localhost", 0),
    )

    _logger.info("### Client starting")
    await client.connect()
    assert client.connected
    if modbus_calls:
        await modbus_calls(client)
    client.close()
    _logger.info("### End of Program")


async def run_a_few_calls(client):

    try:
        # 讀保持暫存器
        # rr = await client.read_holding_registers(0, 4) 
        # print(rr.registers) 

        # 讀輸入暫存器
        rr = await client.read_input_registers(0, 7, slave=0) 
        print(rr.registers)
    except ModbusException:
        pass


async def main(cmdline=None):
    """Combine setup and run."""

    await run_async_client(modbus_calls=run_a_few_calls)


if __name__ == "__main__":
    asyncio.run(main(), debug=True)
class ModbusSlaveContext(ModbusBaseSlaveContext):

    def getValues(self, fc_as_hex, address, count=1):
        """Get `count` values from datastore.

        :param fc_as_hex: The function we are working with
        :param address: The starting address
        :param count: The number of values to retrieve
        :returns: The requested values from a:a+c
        """
        if not self.zero_mode:
            address += 1
        Log.debug("getValues: fc-[{}] address-{}: count-{}", fc_as_hex, address, count)
        return self.store[self.decode(fc_as_hex)].getValues(address, count)
        

    def setValues(self, fc_as_hex, address, values):
        """Set the datastore with the supplied values.

        :param fc_as_hex: The function we are working with
        :param address: The starting address
        :param values: The new values to be set
        """
        if not self.zero_mode:
            address += 1
        Log.debug("setValues[{}] address-{}: count-{}", fc_as_hex, address, len(values))
        self.store[self.decode(fc_as_hex)].setValues(address, values)

同步伺服器和非同步客戶端實現

同步伺服器程式碼實現

server_sync.py

#!/usr/bin/env python3
'''Pymodbus 同步伺服器示例
'''

import logging

from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusServerContext,
    ModbusSlaveContext
)

from pymodbus.server import StartTcpServer

_logger = logging.getLogger('logger')


def run_sync_server():

    # 連續的、無間隙順序儲存資料塊(暫存器塊)
    datablock = lambda : ModbusSequentialDataBlock(0x01, [17] * 100)
    context = ModbusSlaveContext(
        di=datablock(), co=datablock(), hr=datablock(), ir=datablock()
    )
    single = True

    # 構建資料儲存
    context = ModbusServerContext(slaves=context,
                                  single=single
                                  )

    txt = f'### start SYNC server'
    _logger.info(txt)

    address = ('', 5020)
    server = StartTcpServer(
        context=context,  # Data storage
        # identity=identity,  # server identify
        # TBD host=
        # TBD port=
        address=address,  # listen address
        # custom_functions=[],  # allow custom handling
        framer='socket',  # The framer strategy to use
        # ignore_missing_slaves=True,  # ignore request to a missing slave
        # broadcast_enable=False,  # treat slave_id 0 as broadcast address,
        # timeout=1,  # waiting time for request to complete
    )
    return server


def sync_helper():

    server = run_sync_server()
    server.shutdown()


if __name__ == "__main__":
    sync_helper()

同步客戶端程式碼實現

client_sync.py

#!/usr/bin/env python3

'''Pymodbus同步客戶端示例
'''

import logging

import pymodbus.client as modbusClient
from pymodbus import ModbusException

_logger = logging.getLogger('logger')

def run_sync_client(modbus_calls=None):
    """Run sync client."""

    _logger.info("### Create client object")
    client = modbusClient.ModbusTcpClient(
        '127.0.0.1',
        port=5020,  # on which port
        # Common optional parameters:
        framer='socket', # 客戶端訪問伺服器超時時間(該引數僅用於客戶端(slave節點)),float型
        timeout=10,
        retries=3,
        reconnect_delay=1,
        reconnect_delay_max=10,
        #    retry_on_empty=False,
        # TCP setup parameters
        #    source_address=("localhost", 0),
    )

    _logger.info("### Client starting")
    client.connect()
    assert client.connected
    if modbus_calls:
        modbus_calls(client)
    client.close()
    _logger.info("### End of Program")


def run_a_few_calls(client):

    try:
        # 讀取線圈狀態
        rr = client.read_coils(0, 1, slave=1) # 從 0x00 地址開始讀取1個線圈
        print(rr.bits) # 輸出:[True, False, False, False, False, False, False, False]
        # assert len(rr.bits) == 8
    except ModbusException:
        pass


def main():

    run_sync_client(modbus_calls=run_a_few_calls)

if __name__ == "__main__":
    main()

參考連結

https://pypi.org/project/pymodbus/

https://pymodbus.readthedocs.io/en/dev/index.html