VWED_server/tests/test_modbus_server.py

241 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()