Files
dtm-py-all/modbus_server.py

435 lines
19 KiB
Python
Raw Normal View History

import array
import asyncio
import sys
import time
import datetime
import keyboard
import serial
import requests
import threading
import argparse
import re
import os
from pymodbus.exceptions import ModbusIOException
from pymodbus.server import StartTcpServer, StartAsyncTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from multiprocessing import Process
# 动态导入 ModbusPLC
from modbus_plc import get_modbus_plc_class
# 保持向后兼容的工厂函数
ModbusPLC = lambda port, config: get_modbus_plc_class()(port, config)
from mhi_plc import MitsubishiPLC
import utils
from utils import print_with_timestamp
from pymodbus.client import ModbusSerialClient as ModbusSerialClient
from pymodbus.exceptions import ModbusException, ConnectionException
def test_modbus_plc1():
# 创建Modbus RTU客户端
with ModbusSerialClient(method="rtu", port="com1", baudrate=19200, parity="E", timeout=1) as client:
if client.connect():
print("Modbus RTU Client Connected")
else:
print("Failed to connect to Modbus RTU Client")
result = client.write_coil(117, 0, 1)
utils.condition_print('write coil result', result.isError())
result = client.write_coil(118, 1, 1)
utils.condition_print('write coil result', result.isError())
client.close()
from pymodbus.datastore import ModbusSequentialDataBlock
class CustomDataBlock(ModbusSequentialDataBlock):
def __init__(self, data_type, plc_devices=None, plc_address=0x01, com_port="com1", plc_type="mhi", *args, **kwargs):
# print("CustomDataBlock init ", plc_address, com_port, plc_type)
if plc_devices is None:
plc_devices = []
self.com_port = com_port # PLC 通讯串口号
self.plc_address = plc_address # PLC的 站地址 如:1 3 4等
self.plc_type = plc_type # PLC 类型
self.data_type = data_type # 区分coil input_register holding_register
self.address = 0 # 这个是client 进行读写的寄存器地址
self.plc_devices = plc_devices # plc 设备的列表,每一台机器对应一个
# plc_devices 中元素中的device 是 MitsubishiPLC的实例用于和物理PLC的通讯
# 机器的COM口相同的话会共享相同的MitsubishiPLC的实例
self.plc_comm_lock = threading.Lock() # 具体访问MitsubishiPLC的保护锁
super().__init__(*args, **kwargs)
def getValues(self, address, count):
# plc_device_obj = [item for item in self.plc_devices if item["slave"] == self.plc_address] # 按照站地址进行匹配
plc_device_obj = [item for item in self.plc_devices
if item["slave"] == self.plc_address and item["com"] == self.com_port and item[
'type'] == self.plc_type] # 按照站地址进行匹配
plc_device = None
if len(plc_device_obj) and plc_device_obj[0].get('device'):
plc_device = plc_device_obj[0].get('device') # 得到访问物理PLC的MitsubishiPLC的实例
if self.data_type == 'coil':
# 处理线圈数据类型的逻辑
# print('read coils address=', address) # modbus client 要读的寄存器地址
if plc_device:
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
resp = plc_device.plc_read_bits(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('M', address), count)
if resp and resp.get('status') == 'success' and resp.get('data') and resp.get('length'):
return resp['data']
else:
return 0x00
# raise ModbusIOException("Modbus server read coil failed--PLC response error")
else:
if address == 18:
return [True, False, True]
else:
raise ModbusIOException("Modbus server read coil failed--PLC not connected")
pass
elif self.data_type == 'input_register':
# 处理输入寄存器数据类型的逻辑
default_value = [0x00 for _ in range(count)]
if plc_device:
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
resp = plc_device.plc_read_words(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('D', address), count)
if resp and resp.get('status') == 'success' and resp.get('data') and resp.get('length'):
return resp['data']
else:
# raise ModbusIOException("Modbus server read input register failed--PLC response error")
return default_value
else:
if address == 185:
return [88, 89, 100]
else:
raise ModbusIOException("Modbus server read input register failed--PLC not connected")
pass
elif self.data_type == 'holding_register':
# 处理保持寄存器数据类型的逻辑
if address == 10: # 10 作为特殊的寄存器用来返回PLC(物理)的连接状态
if plc_device:
# 直接返回缓存的值,无需实时读取
# return plc_device.get_connect_state(plc_device.conv_to_plc_address('D', 10), self.plc_address)
#print_with_timestamp(f"modbus server begin--0 get_connect_state_cache {plc_device.plc_port}")
connect_state = plc_device.get_connect_state_cache(plc_device.conv_to_plc_address('D', 10), self.plc_address)
#print_with_timestamp(f"modbus server end -- 1 get_connect_state_cache {plc_device.plc_port} ")
return connect_state
else:
return [0, 0, 0, 0, 0, 0, 0, 0, 0]
if plc_device:
default_value = [0x00 for _ in range(count)]
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
resp = plc_device.plc_read_words(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('D', address), count)
if resp and resp.get('status') == 'success' and resp.get('data') and resp.get('length'):
return resp.get('data')
else:
# raise ModbusIOException("Modbus server read holding register failed--PLC response error")
return default_value
else:
if address == 185:
return [88, 89, 100]
else:
raise ModbusIOException("Modbus server read holding register failed--PLC not connected")
pass
def setValues(self, address, values):
# print("CustomDataBlock setValues ", self.data_type, address, values)
# plc_device_obj = [item for item in self.plc_devices if item["slave"] == self.plc_address]
plc_device_obj = [item for item in self.plc_devices
if item["slave"] == self.plc_address and item["com"] == self.com_port and item[
'type'] == self.plc_type] # 按照站地址进行匹配
plc_device = None
if len(plc_device_obj) and plc_device_obj[0].get('device'):
plc_device = plc_device_obj[0].get('device')
# print("CustomDataBlock setValues plc_devices", self.plc_devices,plc_device) # cyx
if self.data_type == 'coil':
# 处理线圈数据类型的逻辑
if plc_device:
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
resp = plc_device.plc_write_bit(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('M', address), values[0])
if resp and resp.get('status') == 'success': # ACK
pass
else:
# raise ModbusIOException("Modbus server set coil failed--PLC response error")
pass
else:
# 如果读取不成功抛出一个Modbus异常
# raise ModbusIOException("Modbus server set coil failed--PLC not connected")
pass
elif self.data_type == 'input_register':
# 处理输入寄存器数据类型的逻辑
pass
elif self.data_type == 'holding_register':
# print("modbus server set holding register address=", address, len(values), values, self.plc_address) # cyx
if plc_device:
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
if len(values) == 1:
resp = plc_device.plc_write_word(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('D', address), values[0])
if resp and resp.get('status') == 'success': # ACK
pass
else:
raise ModbusIOException("Modbus server set holding register failed--PLC response error")
if len(values) > 1:
resp = plc_device.plc_write_words(self.plc_address, 0xFF,
plc_device.conv_to_plc_address('D', address), len(values),
values)
if resp and resp.get('status') == 'success': # ACK
pass
else:
raise ModbusIOException("Modbus server set holding register failed--PLC response error")
# 处理保持寄存器数据类型的逻辑
else:
# 如果读取不成功抛出一个Modbus异常
# raise ModbusIOException("Modbus server set holding register failed--PLC not connected")
pass
exit_flag = False
def getPlcConfig(serverUrl, params):
result = {}
query_string = params
response = requests.get(serverUrl + '/get_machine_config', params=query_string, json={})
if response.status_code == 200:
result = response.json()
else:
print('请求失败')
pass
return result
class ModbusServer(Process):
def __init__(self, host='127.0.0.1', port=5020, machines_config=None, ready_event=None, mock_mode=False):
super().__init__()
self.host = host
self.port = port
self.machines_config = machines_config or []
self.context = None
self.server = None
self.plc_devices = []
self.ready_event = ready_event # 用于通知主进程,服务器已启动
self.mock_mode = mock_mode # 添加模拟模式参数
def _create_plc_devices(self):
"""
Create PLC devices based on the machine configuration.
"""
try:
for config in self.machines_config:
if config.get('type') == 'mhi' and config.get('com').startswith("com") and config.get('plcAddress'):
device_matched = [
device_json['device']
for device_json in self.plc_devices
if device_json["com"] == config.get('com') and device_json['type'] == config.get('type')
]
device_have_created = device_matched[0] if device_matched else None
if device_have_created is None:
device_have_created = MitsubishiPLC(config.get('com'), config.get('comm_config'))
self.plc_devices.append({
'slave': int(config.get('plcAddress')),
'com': config.get('com'),
'device': device_have_created,
'type': config.get('type')
})
if config.get('type') == 'xinjie' and config.get('com').startswith("com") and config.get('plcAddress'):
device_matched = [
device_json['device']
for device_json in self.plc_devices
if device_json["com"] == config.get('com') and device_json['type'] == config.get('type')
]
device_have_created = device_matched[0] if device_matched else None
if device_have_created is None:
device_have_created = ModbusPLC(config.get('com'), config.get('comm_config'))
self.plc_devices.append({
'slave': int(config.get('plcAddress')),
'com': config.get('com'),
'device': device_have_created,
'type': config.get('type')
})
except Exception as error:
print(f"Error creating PLC devices: {error}")
def _create_context(self):
"""
Create Modbus context for each PLC device.
"""
stores = {}
for config in self.machines_config:
plc_address = int(config.get('plcAddress'))
coil_block = CustomDataBlock('coil', self.plc_devices, plc_address, config.get("com"), config.get("type"),
0, [False] * 0x10000)
holding_register_block = CustomDataBlock('holding_register', self.plc_devices, plc_address,
config.get("com"), config.get("type"), 0, [0] * 0x10000)
input_coil_block = CustomDataBlock('input_coil', self.plc_devices, plc_address, config.get("com"),
config.get("type"), 0, [False] * 0x10000)
input_register_block = CustomDataBlock('input_register', self.plc_devices, plc_address, config.get("com"),
config.get("type"), 0, [0] * 0x10000)
store = ModbusSlaveContext(
di=input_coil_block,
co=coil_block,
hr=holding_register_block,
ir=input_register_block,
zero_mode=True
)
tcp_unit_id = int(config.get('tcp_modbus_unit'))
stores[tcp_unit_id] = store
self.context = ModbusServerContext(slaves=stores, single=False)
async def _start_server(self):
"""
Start the Modbus TCP Server.
"""
self._create_context()
print(f"Starting Modbus server on {self.host}:{self.port}")
try:
self.server = await StartAsyncTcpServer(self.context, address=(self.host, self.port))
print(f"Modbus server started on {self.host}:{self.port}")
# 通知主进程服务器已启动c
if self.ready_event:
self.ready_event.set()
await self.server.serve_forever()
except Exception as e:
if self.ready_event:
self.ready_event.set()
print(f"Error starting Modbus server: {e}")
def run(self):
"""
Run the Modbus server in the Process.
"""
# 设置模拟模式
if self.mock_mode:
import modbus_plc
modbus_plc.MOCK_MODE = True
print("Modbus Server: 启用 PLC 模拟模式")
else:
print("Modbus Server: 使用真实 PLC 设备")
print("Modbus server begin start in a separate process")
# 创建 PLC 设备
self._create_plc_devices()
# 创建新的事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 启动 Modbus 服务器
loop.run_until_complete(self._start_server())
except Exception as e:
print(f"Failed to start Modbus server: {e}")
finally:
print("Modbus server shutting down.")
if self.server:
self.server.shutdown()
loop.close()
def stop(self):
"""
Stop the Modbus server gracefully.
"""
try:
for device_info in self.plc_devices:
device = device_info.get('device')
if hasattr(device, 'stop_polling'):
device.stop_polling()
if self.server:
self.server.shutdown()
print("Modbus server stopped gracefully.")
finally:
pass
modbus_server = None # 声明全局变量
exit_flag = False
def get_machine_configs(serverUrl='127.0.0.1:5050'):
global modbus_server, exit_flag
config_got = False
while not config_got:
try:
resp = getPlcConfig(serverUrl, {})
if resp.get('status') == 'success':
config_got = True
machine_config = resp.get('data', [])
modbus_server = ModbusServer(machine_config)
modbus_server.modbus_thread = threading.Thread(target=modbus_server.run_modbus_server)
modbus_server.modbus_thread.start()
except Exception as error:
pass
time.sleep(3)
pass
def main():
global modbus_server # 引用全局变量
global exit_flag
get_machine_configs_thread = threading.Thread(target=get_machine_configs, args=(args.serverUrl,))
get_machine_configs_thread.start()
def stop_server(): # 停止服务器的函数
global exit_flag
if modbus_server:
modbus_server.stop_modbus_server()
if modbus_server and modbus_server.modbus_thread:
modbus_server.modbus_thread.join()
get_machine_configs_thread.join()
exit_flag = True
try:
while not exit_flag:
if keyboard.is_pressed('z'):
# stop_server()
pass
time.sleep(0.1)
except KeyboardInterrupt:
print("用户强制退出Ctrl+C")
stop_server()
sys.exit(0)
if __name__ == "__main__":
# 创建解析器
parser = argparse.ArgumentParser(description="My modbus and plc Description")
# 添加参数
parser.add_argument('--serverUrl', default="http://127.0.0.1:5050", help="Server URL")
# 添加参数
parser.add_argument('--host', default="127.0.0.1", help="Seqrver URL")
# 添加参数
parser.add_argument('--port', default=5020, help="Server URL")
parser.add_argument('--mock', action='store_true', help="启用 PLC 模拟模式")
# 解析参数
args = parser.parse_args()
# 设置 MOCK_MODE
if hasattr(args, 'mock') and args.mock:
import modbus_plc
modbus_plc.MOCK_MODE = True
print("启用 PLC 模拟模式")
else:
print("使用真实 PLC 设备")
serverUrl = args.serverUrl
main()