python實(shí)現(xiàn)ModBusTCP協(xié)議的server是一件簡(jiǎn)單的事情,只要通過pymodbus、pyModbusTCP等模塊都可以實(shí)現(xiàn),本文采用pymodbus。
相關(guān)文章見:python實(shí)現(xiàn)ModBusTCP協(xié)議的client
一、了解pymodbus的Server
1、pymodbus.server的模塊
pymodbus.server中的模塊,能夠用于用于實(shí)現(xiàn) Modbus 協(xié)議的服務(wù)器端。以下是每個(gè)模塊的功能介紹:
(1)ModbusSerialServer: 這個(gè)模塊提供了一個(gè)基于串口的 Modbus 服務(wù)器。它允許通過串口與 Modbus 客戶端通信。
(2)ModbusTcpServer: 這個(gè)模塊提供了一個(gè)基于 TCP/IP 的 Modbus 服務(wù)器。它通過 TCP/IP 網(wǎng)絡(luò)接口與 Modbus 客戶端通信。
(3)ModbusTlsServer: 這個(gè)模塊提供了一個(gè)基于 TLS 加密的 Modbus 服務(wù)器。它通過安全的 TLS 連接與 Modbus 客戶端通信,確保通信的安全性。
(4)ModbusUdpServer: 這個(gè)模塊提供了一個(gè)基于 UDP 的 Modbus 服務(wù)器。與 TCP 不同,UDP 是一種面向無連接的協(xié)議,適用于某些特定的網(wǎng)絡(luò)環(huán)境。
(5)ServerAsyncStop: 這個(gè)類用于異步服務(wù)器的停止信號(hào)。通過發(fā)送這個(gè)信號(hào),可以優(yōu)雅地停止異步服務(wù)器的運(yùn)行。
(6)ServerStop: 這個(gè)類用于同步服務(wù)器的停止信號(hào)。與 ServerAsyncStop
類似,但用于同步服務(wù)器。
(7)StartAsyncSerialServer: 這個(gè)函數(shù)用于啟動(dòng)一個(gè)異步的基于串口的 Modbus 服務(wù)器。
(8)StartAsyncTcpServer: 這個(gè)函數(shù)用于啟動(dòng)一個(gè)異步的基于 TCP/IP 的 Modbus 服務(wù)器。
(9)StartAsyncTlsServer: 這個(gè)函數(shù)用于啟動(dòng)一個(gè)異步的基于 TLS 加密的 Modbus 服務(wù)器。
(10)StartAsyncUdpServer: 這個(gè)函數(shù)用于啟動(dòng)一個(gè)異步的基于 UDP 的 Modbus 服務(wù)器。
(11)StartSerialServer: 這個(gè)函數(shù)用于啟動(dòng)一個(gè)同步的基于串口的 Modbus 服務(wù)器。
(12)StartTcpServer: 這個(gè)函數(shù)期望用于啟動(dòng)一個(gè)同步的基于 TCP/IP 的 Modbus 服務(wù)器。(備注:源碼其實(shí)是異步的)
(13)StartTlsServer: 這個(gè)函數(shù)期望用于啟動(dòng)一個(gè)同步的基于 TLS 加密的 Modbus 服務(wù)器。(備注:源碼其實(shí)是異步的)
(14)StartUdpServer: 這個(gè)函數(shù)期望用于啟動(dòng)一個(gè)同步的基于 UDP 的 Modbus 服務(wù)器。(備注:源碼其實(shí)是異步的)
這些模塊和函數(shù)提供了多種不同類型的 Modbus 服務(wù)器的實(shí)現(xiàn)方式,可以根據(jù)具體的需求選擇合適的模塊和函數(shù)來創(chuàng)建和啟動(dòng) Modbus 服務(wù)器。
2、模塊的選用
StartAsyncTcpServer
和 StartTcpServer
是 pymodbus
庫中用于啟動(dòng) Modbus TCP 服務(wù)器的兩種不同的方法,其主要區(qū)別在于同步(Synchronous)和異步(Asynchronous)執(zhí)行方式。
(1)StartAsyncTcpServer(異步方式):
StartAsyncTcpServer
是一個(gè)異步函數(shù),它使用 Python 的 asyncio
模塊來實(shí)現(xiàn)異步的 Modbus TCP 服務(wù)器。在異步編程中,事件循環(huán)(event loop)可以處理多個(gè)任務(wù),使得程序在等待某些耗時(shí)操作(比如 I/O 操作)時(shí)不會(huì)被阻塞。這意味著它可以同時(shí)處理多個(gè)客戶端連接,提高了服務(wù)器的并發(fā)性能。
該函數(shù)的調(diào)用方式是異步的,需要在異步上下文(比如異步函數(shù)內(nèi)部或者異步腳本中)使用。
(2)StartTcpServer(同步方式):
StartTcpServer
是一個(gè)同步函數(shù),它使用 Python 的標(biāo)準(zhǔn)同步執(zhí)行方式,會(huì)阻塞當(dāng)前線程。在同步模式下,每次只能處理一個(gè)客戶端連接。這意味著服務(wù)器只能順序處理客戶端請(qǐng)求,一個(gè)接一個(gè)地處理。
該函數(shù)的調(diào)用方式是同步的,可以在任何地方調(diào)用。
總之,選擇哪種方式取決于你的應(yīng)用場(chǎng)景。如果需要處理大量并發(fā)連接并提高性能,可以選擇異步方式;如果簡(jiǎn)單的同步處理能夠滿足需求,可以選擇同步方式。
本文采用StartAsyncTcpServer,所以他涉及到的模塊是StartTcpServer,源碼如下:
def StartTcpServer(**kwargs): # pylint: disable=invalid-name
"""Start and run a serial modbus server."""
return asyncio.run(StartAsyncTcpServer(**kwargs))
如果要停止異步,需要調(diào)用ServerAsyncStop模塊。
二、一個(gè)Demo
1、服務(wù)端讀寫自身的保持寄存器的示例
import asyncio
import threading
import time
import json
from pymodbus.server import StartTcpServer, ServerAsyncStop
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
if __name__ == "__main__":
# Modbus TCP服務(wù)器的IP地址和端口號(hào)
server_ip = "192.168.1.188"
port = 502
station = 1
# 創(chuàng)建一個(gè)數(shù)據(jù)存儲(chǔ)區(qū),用于存儲(chǔ)從客戶端讀取的數(shù)據(jù)
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [0] * 100)
)
# 創(chuàng)建一個(gè)服務(wù)器上下文,用于處理客戶端的請(qǐng)求
context = ModbusServerContext(slaves=store, single=True)
# 啟動(dòng)ModBusTCP服務(wù)器
# 創(chuàng)建并啟動(dòng)線程(啟動(dòng)異步服務(wù)器)
# StartTcpServer(context=context, address=(server_ip, port))
modbus_server_thread = threading.Thread(target=StartTcpServer, kwargs=({"context":context, "address":(server_ip, port)}))
modbus_server_thread.start()
# 設(shè)置保持寄存器的0地址的值為s
# 定義函數(shù)參數(shù)
fc_as_hex = 0x03 # 功能碼,例如讀保持寄存器是"0x03"
write_address = 0 # 起始地址
read_address = 10
read_count = 1
values = [115] # 觸發(fā)指令s 要設(shè)置的多個(gè)值列表,如[10, 20, 30]
# 調(diào)用函數(shù),設(shè)置0地址為觸發(fā)指令s
store.setValues(fc_as_hex, write_address, values)
# 獲取保持寄存器的值并打印
hr_values = store.getValues(3, read_address, count=read_count)
print("Hold Register Values:", hr_values)
assert hr_values == [0], print("測(cè)試失敗!")
time.sleep(10)
try:
print("停止服務(wù)器")
asyncio.run(ServerAsyncStop()) # 停止服務(wù)器
except:
pass
2、實(shí)際協(xié)議幀解析
上一章節(jié)我們說到協(xié)議幀的格式,現(xiàn)在我們用上一章節(jié)的內(nèi)容解析一下實(shí)際協(xié)議幀的情況。
(1)一個(gè)完整的收發(fā)協(xié)議幀
[2023-11-02 10:45:31-315]NET001-發(fā)送:[00 23 00 00 00 0b] 01 10 00 00 00 02 04 73 00 00 00 [2023-11-02 10:45:31-317]NET001-接收:[00 23 00 00 00 06] 01 10 00 00 00 02
(2)發(fā)送
這條消息的結(jié)構(gòu)可以分解為:
- [00 23 00 00 00 0b]: 幀頭,pymodbus自動(dòng)封裝好,不用自己寫,表示消息的起始標(biāo)志和長度。依次包含事務(wù)標(biāo)識(shí)符(2字節(jié))、協(xié)議標(biāo)識(shí)符(2字節(jié))、長度字段(2字節(jié))。
- 01: 請(qǐng)求目標(biāo)站號(hào)。
- 10: 命令碼(功能碼),這里表示寫入多個(gè)寄存器。
- 00 00: 請(qǐng)求的寄存器起始地址。
- 00 02: 請(qǐng)求的寄存器數(shù)量。
- 04: 請(qǐng)求數(shù)據(jù)的字節(jié)數(shù)。
- 73 00 00 00: 具體的請(qǐng)求數(shù)據(jù),以十六進(jìn)制表示。
(3)接收
這條消息的結(jié)構(gòu)可以分解為:
- [00 23 00 00 00 06]: 幀頭,表示消息的起始標(biāo)志和長度。
- 01: 請(qǐng)求目標(biāo)站號(hào)。
- 10: 命令碼(功能碼),這里表示寫入多個(gè)寄存器。
- 00 00: 請(qǐng)求的寄存器起始地址。
- 00 02: 請(qǐng)求的寄存器數(shù)量。