實踐環境
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)
相關說明
-
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) # 報錯
-
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 函式來新增新的資料塊
-
pymodbus.datastore.ModbusSlaveContext
用於建立每種資料訪問都儲存在一個塊中的一個modbus資料模型。該類可用來模擬 Modbus 從裝置上下文。可以在這個上下文中新增多個不同型別的資料塊,模擬一個完整的 Modbus 從裝置。
例子:
ModbusSlaveContext( di=datablock(), # 輸入離散量(Discrete Inputs) co=datablock(), # 輸出線圈 (Coils) hr=datablock(), # 保持暫存器(Holding Register) ir=datablock(), # 輸入暫存器(Input Register) )
-
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從機IDkwargs
可選引數,實驗性引數
-
def write_coils( self, address: int, values: list[bool] | bool, slave: int = 0, **kwargs: Any)
寫多個線圈(對應功能碼
0x0F
)address
要寫入資料的起始地址values
要寫入的布林值列表、或者單個布林值slave
可選引數,Modbus從機IDkwargs
可選引數,實驗性引數
-
def write_register(self, address: int, value: int, slave: int = 0, **kwargs: Any)
寫單個暫存器(功能碼
0x06
)address
要寫入資料的起始地址value
要寫入的整數slave
可選引數,Modbus從機IDkwargs
可選引數,實驗性引數
-
def write_registers( self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any)
寫多個暫存器(功能碼
0x10
)address
要寫入資料的起始地址values
要寫入的整數列表、或者單個整數slave
可選引數,Modbus從機IDkwargs
可選引數,實驗性引數
-
為伺服器設定初始化 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