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() |