241 lines
9.2 KiB
Python
241 lines
9.2 KiB
Python
|
#!/usr/bin/env python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
"""
|
|||
|
Modbus TCP 模拟服务器
|
|||
|
用于测试在线脚本中的Modbus通信功能
|
|||
|
"""
|
|||
|
|
|||
|
import time
|
|||
|
import threading
|
|||
|
from pymodbus.server.startstop import StartTcpServer
|
|||
|
from pymodbus.device import ModbusDeviceIdentification
|
|||
|
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
|
|||
|
from pymodbus.datastore.context import ModbusServerContext
|
|||
|
import logging
|
|||
|
|
|||
|
# 配置日志
|
|||
|
logging.basicConfig(level=logging.INFO)
|
|||
|
logger = logging.getLogger(__name__)
|
|||
|
|
|||
|
|
|||
|
class ModbusTestServer:
|
|||
|
"""Modbus TCP 测试服务器"""
|
|||
|
|
|||
|
def __init__(self, host='localhost', port=5020):
|
|||
|
self.host = host
|
|||
|
self.port = port
|
|||
|
self.server_thread = None
|
|||
|
self.running = False
|
|||
|
|
|||
|
# 创建数据存储
|
|||
|
# 参数:(起始地址, 初始值列表)
|
|||
|
# 线圈 (0x区域) - 可读写的离散值
|
|||
|
coils = ModbusSequentialDataBlock(0, [False] * 100)
|
|||
|
|
|||
|
# 离散输入 (1x区域) - 只读的离散值
|
|||
|
discrete_inputs = ModbusSequentialDataBlock(0, [True, False, True, False] * 25)
|
|||
|
|
|||
|
# 输入寄存器 (3x区域) - 只读的16位值
|
|||
|
input_registers = ModbusSequentialDataBlock(0, [100, 200, 300, 400] * 25)
|
|||
|
|
|||
|
# 保持寄存器 (4x区域) - 可读写的16位值
|
|||
|
holding_registers = ModbusSequentialDataBlock(0, [1000, 2000, 3000, 4000] * 25)
|
|||
|
|
|||
|
# 创建从站上下文
|
|||
|
slave_context = ModbusSlaveContext(
|
|||
|
di=discrete_inputs, # 离散输入
|
|||
|
co=coils, # 线圈
|
|||
|
hr=holding_registers, # 保持寄存器
|
|||
|
ir=input_registers # 输入寄存器
|
|||
|
)
|
|||
|
|
|||
|
# 创建服务器上下文(支持从站ID 1-10)
|
|||
|
self.context = ModbusServerContext(slaves={
|
|||
|
1: slave_context, # 从站ID 1
|
|||
|
2: slave_context, # 从站ID 2 (共享同一个上下文,简化测试)
|
|||
|
}, single=False)
|
|||
|
|
|||
|
# 设备信息
|
|||
|
self.identity = ModbusDeviceIdentification()
|
|||
|
self.identity.VendorName = 'VWED Test'
|
|||
|
self.identity.ProductCode = 'VWED-MODBUS-SIM'
|
|||
|
self.identity.VendorUrl = 'https://github.com/vwed'
|
|||
|
self.identity.ProductName = 'VWED Modbus TCP Simulator'
|
|||
|
self.identity.ModelName = 'Test Server v1.0'
|
|||
|
self.identity.MajorMinorRevision = '1.0.0'
|
|||
|
|
|||
|
# 启动数据更新线程
|
|||
|
self.update_thread = None
|
|||
|
|
|||
|
def start_server(self):
|
|||
|
"""启动Modbus服务器"""
|
|||
|
if self.running:
|
|||
|
logger.warning("服务器已经在运行")
|
|||
|
return
|
|||
|
|
|||
|
logger.info(f"启动Modbus TCP服务器: {self.host}:{self.port}")
|
|||
|
|
|||
|
self.running = True
|
|||
|
|
|||
|
# 启动数据更新线程
|
|||
|
self.update_thread = threading.Thread(target=self._update_data_loop, daemon=True)
|
|||
|
self.update_thread.start()
|
|||
|
|
|||
|
# 启动服务器(在单独线程中)
|
|||
|
self.server_thread = threading.Thread(
|
|||
|
target=self._run_server,
|
|||
|
daemon=True
|
|||
|
)
|
|||
|
self.server_thread.start()
|
|||
|
|
|||
|
logger.info("Modbus TCP服务器启动成功")
|
|||
|
|
|||
|
def _run_server(self):
|
|||
|
"""运行服务器"""
|
|||
|
try:
|
|||
|
StartTcpServer(
|
|||
|
context=self.context,
|
|||
|
identity=self.identity,
|
|||
|
address=(self.host, self.port)
|
|||
|
)
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"服务器运行错误: {e}")
|
|||
|
self.running = False
|
|||
|
|
|||
|
def _update_data_loop(self):
|
|||
|
"""更新测试数据的循环"""
|
|||
|
counter = 0
|
|||
|
while self.running:
|
|||
|
try:
|
|||
|
# 更新输入寄存器数据(模拟传感器读数)
|
|||
|
for slave_id in [1, 2]:
|
|||
|
slave_context = self.context[slave_id]
|
|||
|
|
|||
|
# 更新输入寄存器(模拟变化的传感器数据)
|
|||
|
new_values = [
|
|||
|
100 + counter % 100, # 地址0: 100-199循环
|
|||
|
200 + (counter * 2) % 200, # 地址1: 200-399循环
|
|||
|
300 + (counter * 3) % 300, # 地址2: 300-599循环
|
|||
|
400 + (counter * 4) % 400, # 地址3: 400-799循环
|
|||
|
]
|
|||
|
|
|||
|
for i, value in enumerate(new_values):
|
|||
|
slave_context.setValues(3, i, [value]) # 3 = 输入寄存器
|
|||
|
|
|||
|
# 更新离散输入(模拟开关状态)
|
|||
|
discrete_values = [
|
|||
|
counter % 2 == 0, # 地址0: 每秒切换
|
|||
|
counter % 4 < 2, # 地址1: 每2秒切换
|
|||
|
counter % 6 < 3, # 地址2: 每3秒切换
|
|||
|
counter % 8 < 4, # 地址3: 每4秒切换
|
|||
|
]
|
|||
|
|
|||
|
for i, value in enumerate(discrete_values):
|
|||
|
slave_context.setValues(2, i, [value]) # 2 = 离散输入
|
|||
|
|
|||
|
counter += 1
|
|||
|
time.sleep(1) # 每秒更新一次
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"数据更新错误: {e}")
|
|||
|
time.sleep(1)
|
|||
|
|
|||
|
def stop_server(self):
|
|||
|
"""停止服务器"""
|
|||
|
logger.info("正在停止Modbus TCP服务器...")
|
|||
|
self.running = False
|
|||
|
|
|||
|
def get_server_info(self):
|
|||
|
"""获取服务器信息"""
|
|||
|
return {
|
|||
|
'host': self.host,
|
|||
|
'port': self.port,
|
|||
|
'running': self.running,
|
|||
|
'slaves': list(self.context.slaves()) if hasattr(self.context, 'slaves') and callable(self.context.slaves) else [1, 2],
|
|||
|
}
|
|||
|
|
|||
|
def set_coil_value(self, slave_id, address, value):
|
|||
|
"""设置线圈值(用于测试)"""
|
|||
|
try:
|
|||
|
slave_context = self.context[slave_id]
|
|||
|
slave_context.setValues(1, address, [bool(value)]) # 1 = 线圈
|
|||
|
logger.info(f"设置线圈 slave={slave_id}, addr={address}, value={value}")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"设置线圈失败: {e}")
|
|||
|
|
|||
|
def set_holding_register_value(self, slave_id, address, value):
|
|||
|
"""设置保持寄存器值(用于测试)"""
|
|||
|
try:
|
|||
|
slave_context = self.context[slave_id]
|
|||
|
slave_context.setValues(4, address, [int(value)]) # 4 = 保持寄存器
|
|||
|
logger.info(f"设置保持寄存器 slave={slave_id}, addr={address}, value={value}")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"设置保持寄存器失败: {e}")
|
|||
|
|
|||
|
def get_all_values(self, slave_id=1):
|
|||
|
"""获取所有值(用于调试)"""
|
|||
|
try:
|
|||
|
slave_context = self.context[slave_id]
|
|||
|
|
|||
|
# 读取各种类型的数据
|
|||
|
coils = slave_context.getValues(1, 0, 10) # 线圈
|
|||
|
discrete = slave_context.getValues(2, 0, 10) # 离散输入
|
|||
|
input_regs = slave_context.getValues(3, 0, 10) # 输入寄存器
|
|||
|
holding_regs = slave_context.getValues(4, 0, 10) # 保持寄存器
|
|||
|
|
|||
|
return {
|
|||
|
'coils': coils,
|
|||
|
'discrete_inputs': discrete,
|
|||
|
'input_registers': input_regs,
|
|||
|
'holding_registers': holding_regs
|
|||
|
}
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"获取数据失败: {e}")
|
|||
|
return None
|
|||
|
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数 - 启动测试服务器"""
|
|||
|
print("=" * 60)
|
|||
|
print("VWED Modbus TCP 测试服务器")
|
|||
|
print("=" * 60)
|
|||
|
|
|||
|
# 创建并启动服务器
|
|||
|
server = ModbusTestServer(host='localhost', port=5020)
|
|||
|
server.start_server()
|
|||
|
|
|||
|
print(f"服务器信息: {server.get_server_info()}")
|
|||
|
print("\n测试数据布局:")
|
|||
|
print("- 线圈 (0x区域): 地址0-99, 初始值为False")
|
|||
|
print("- 离散输入 (1x区域): 地址0-99, 动态变化的True/False")
|
|||
|
print("- 输入寄存器 (3x区域): 地址0-99, 动态变化的数值")
|
|||
|
print("- 保持寄存器 (4x区域): 地址0-99, 初始值1000,2000,3000,4000...")
|
|||
|
print("\n支持的从站ID: 1, 2")
|
|||
|
print(f"\n连接地址: {server.host}:{server.port}")
|
|||
|
print("\n测试示例:")
|
|||
|
print("python -c \"")
|
|||
|
print("from services.online_script.built_in_modules.modbus_module import VWEDModbusModule")
|
|||
|
print("m = VWEDModbusModule('test')")
|
|||
|
print("print(m.read_holding_register('localhost', 5020, 1, 0, 2))")
|
|||
|
print("\"")
|
|||
|
|
|||
|
try:
|
|||
|
while True:
|
|||
|
time.sleep(5)
|
|||
|
# 显示当前数据状态
|
|||
|
values = server.get_all_values(1)
|
|||
|
if values:
|
|||
|
print(f"\n[{time.strftime('%H:%M:%S')}] 从站1当前数据:")
|
|||
|
print(f" 输入寄存器[0-3]: {values['input_registers'][:4]}")
|
|||
|
print(f" 离散输入[0-3]: {values['discrete_inputs'][:4]}")
|
|||
|
print(f" 保持寄存器[0-3]: {values['holding_registers'][:4]}")
|
|||
|
|
|||
|
except KeyboardInterrupt:
|
|||
|
print("\n\n正在关闭服务器...")
|
|||
|
server.stop_server()
|
|||
|
print("服务器已关闭")
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|