730 lines
31 KiB
Python
730 lines
31 KiB
Python
|
|
import array
|
|||
|
|
import asyncio
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
|
|||
|
|
import keyboard
|
|||
|
|
import serial
|
|||
|
|
import struct
|
|||
|
|
import requests
|
|||
|
|
from queue import Queue
|
|||
|
|
import threading
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
from pymodbus.exceptions import ModbusIOException
|
|||
|
|
from pymodbus.pdu import ModbusRequest
|
|||
|
|
from pymodbus.server import StartTcpServer, StartAsyncTcpServer
|
|||
|
|
from pymodbus.device import ModbusDeviceIdentification
|
|||
|
|
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
|
|||
|
|
from pymodbus.client import ModbusSerialClient as ModbusClient
|
|||
|
|
|
|||
|
|
print_condition = False
|
|||
|
|
all_plc_recv = 0
|
|||
|
|
serverUrl = 'http://106.52.71.204:5050'
|
|||
|
|
# 定义颜色代码
|
|||
|
|
RED = '\033[31m'
|
|||
|
|
GREEN = '\033[32m'
|
|||
|
|
YELLOW = '\033[33m'
|
|||
|
|
BLUE = '\033[34m'
|
|||
|
|
MAGENTA = '\033[35m'
|
|||
|
|
CYAN = '\033[36m'
|
|||
|
|
WHITE = '\033[37m'
|
|||
|
|
RESET = '\033[m'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def bcd_coder_array(value=0, lenght=4):
|
|||
|
|
return int_to_bcd(value, lenght)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def int_to_bcd(number=0, length=5):
|
|||
|
|
bcd_array = []
|
|||
|
|
try:
|
|||
|
|
int_number = int(round(number)) # 使用round函数四舍五入,然后转成整数
|
|||
|
|
while int_number > 0:
|
|||
|
|
bcd = int_number % 100
|
|||
|
|
bcd_array.insert(0, (bcd // 10) << 4 | (bcd % 10))
|
|||
|
|
int_number = int_number // 100
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
while len(bcd_array) < length:
|
|||
|
|
bcd_array.insert(0, 0)
|
|||
|
|
|
|||
|
|
return bcd_array
|
|||
|
|
|
|||
|
|
|
|||
|
|
def int_to_ascii_array(number, length):
|
|||
|
|
# 将整数转换为ASCII码字符串
|
|||
|
|
ascii_str = str(number)
|
|||
|
|
# 获取ASCII码字符串的长度
|
|||
|
|
ascii_str_length = len(ascii_str)
|
|||
|
|
# 如果ASCII码字符串的长度小于指定长度,则在前面补0
|
|||
|
|
if ascii_str_length < length:
|
|||
|
|
ascii_str = '0' * (length - ascii_str_length) + ascii_str
|
|||
|
|
# 将ASCII码字符串转换为ASCII码数组
|
|||
|
|
ascii_array = [ord(c) for c in ascii_str]
|
|||
|
|
return ascii_array
|
|||
|
|
|
|||
|
|
|
|||
|
|
def str_to_ascii_array(str, length):
|
|||
|
|
# 获取ASCII码字符串的长度
|
|||
|
|
ascii_str_length = len(str)
|
|||
|
|
# 如果ASCII码字符串的长度小于指定长度,则在前面补0
|
|||
|
|
if ascii_str_length < length:
|
|||
|
|
ascii_str = '0' * (length - ascii_str_length) + str
|
|||
|
|
# 将ASCII码字符串转换为ASCII码数组
|
|||
|
|
ascii_array = [ord(c) for c in str]
|
|||
|
|
return ascii_array
|
|||
|
|
|
|||
|
|
|
|||
|
|
def hex_to_ascii_arr(hex_num, lenth):
|
|||
|
|
str_num = hex(hex_num)[2:].upper().rjust(lenth, '0')
|
|||
|
|
array = [ord(c) for c in str_num]
|
|||
|
|
return array
|
|||
|
|
|
|||
|
|
|
|||
|
|
def convert_ascii_to_num(ascii_val):
|
|||
|
|
if ord('0') <= ascii_val <= ord('9'):
|
|||
|
|
return ascii_val - ord('0')
|
|||
|
|
elif ord('A') <= ascii_val <= ord('F'):
|
|||
|
|
return ascii_val - ord('A') + 10
|
|||
|
|
elif ord('a') <= ascii_val <= ord('f'):
|
|||
|
|
return ascii_val - ord('a') + 10
|
|||
|
|
else:
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ascii_array_to_word(ascii_array):
|
|||
|
|
if len(ascii_array) != 4:
|
|||
|
|
return 0
|
|||
|
|
return convert_ascii_to_num(ascii_array[0]) * 0x1000 + \
|
|||
|
|
convert_ascii_to_num(ascii_array[1]) * 0x0100 + \
|
|||
|
|
convert_ascii_to_num(ascii_array[2]) * 0x010 + \
|
|||
|
|
convert_ascii_to_num(ascii_array[3])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def date_to_bcd(date_str): # 0:06:20.585233
|
|||
|
|
# condition_print("date=",date_str,type(date_str))
|
|||
|
|
# 移除日期中的"-"字符
|
|||
|
|
date_str = date_str.replace("-", "")
|
|||
|
|
# 按照两位数字进行分割
|
|||
|
|
date_parts = [date_str[i:i + 2] for i in range(0, len(date_str), 2)]
|
|||
|
|
# 将每个部分转换为十六进制数
|
|||
|
|
bcd_array = [int(part, 16) for part in date_parts]
|
|||
|
|
return bcd_array
|
|||
|
|
|
|||
|
|
|
|||
|
|
def time_to_bcd(time_str): # 10:06:20.585233
|
|||
|
|
time_str = time_str.split('.')[0] # 去除掉后面的毫秒
|
|||
|
|
time_parts = time_str.split(':') # 按":"分割
|
|||
|
|
bcd = [int(part) for part in time_parts] # 分别转为整数
|
|||
|
|
bcd = [int(str(i), 16) for i in bcd] # 分别转为16进制
|
|||
|
|
return bcd
|
|||
|
|
|
|||
|
|
|
|||
|
|
def conv_to_plc_address(type_char, value):
|
|||
|
|
# 检查type_char是否为'M'或'D',value是否为整数
|
|||
|
|
if type_char not in ('M', 'D') or not isinstance(value, int):
|
|||
|
|
raise ValueError("Type must be 'M' or 'D' and value must be an integer.")
|
|||
|
|
|
|||
|
|
# 使用str.format()方法格式化字符串
|
|||
|
|
formatted = "{type}{value:04d}".format(type=type_char, value=value)
|
|||
|
|
return formatted
|
|||
|
|
|
|||
|
|
|
|||
|
|
def condition_print(*args, **kwargs):
|
|||
|
|
global print_condition # 使用全局变量
|
|||
|
|
message = ' '.join(str(arg) for arg in args)
|
|||
|
|
if kwargs:
|
|||
|
|
message += ', ' + ', '.join("{}={}".format(key, value) for key, value in kwargs.items())
|
|||
|
|
|
|||
|
|
if print_condition:
|
|||
|
|
print(message)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MitsubishiPLC:
|
|||
|
|
def __init__(self, plc_port, comm_config):
|
|||
|
|
global print_condition, api_condition, all_plc_recv
|
|||
|
|
port_opened = False
|
|||
|
|
print("DevicePLC init", plc_port, comm_config) # cyx
|
|||
|
|
self.event_format_long = False
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
self.ser = serial.Serial(port=plc_port,
|
|||
|
|
baudrate=comm_config['baudrate'],
|
|||
|
|
parity=comm_config['parity'],
|
|||
|
|
bytesize=comm_config['bytesize'],
|
|||
|
|
stopbits=comm_config['stopbits'],
|
|||
|
|
timeout=1, rtscts=False)
|
|||
|
|
#condition_print(f"DevicePLC open {plc_port} success") # cyx
|
|||
|
|
print(f"DevicePLC open {plc_port} success") # cyx
|
|||
|
|
except:
|
|||
|
|
condition_print("\033[91m {}\033[00m".format("plc " + plc_port + " open error"))
|
|||
|
|
try:
|
|||
|
|
self.ser = serial.Serial(port="com1",
|
|||
|
|
baudrate=comm_config['baudrate'],
|
|||
|
|
parity=comm_config['parity'],
|
|||
|
|
bytesize=comm_config['bytesize'],
|
|||
|
|
stopbits=comm_config['stopbits'],
|
|||
|
|
timeout=2, rtscts=False)
|
|||
|
|
except:
|
|||
|
|
print("\033[91m {}\033[00m".format("plc " + "com1" + " open error"))
|
|||
|
|
else:
|
|||
|
|
port_opened = True
|
|||
|
|
print("\033[92m {}\033[00m".format("plc " + "com1" + " open success"))
|
|||
|
|
else:
|
|||
|
|
port_opened = True
|
|||
|
|
print("\033[92m {}\033[00m".format("plc " + plc_port + " open success--1"))
|
|||
|
|
finally:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if port_opened:
|
|||
|
|
self.link_down_timer = threading.Timer(5.0, self.link_down_timeout) # 创建一个5秒后执行的定时器
|
|||
|
|
# self.link_down_timer.start()
|
|||
|
|
self.link_down_event = threading.Event()
|
|||
|
|
|
|||
|
|
if self.port_is_opened():
|
|||
|
|
# self.recv_thread.start();
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def port_is_opened(self):
|
|||
|
|
if hasattr(self, 'ser') and self.ser:
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
return False
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def get_connect_state(self):
|
|||
|
|
return self.port_is_opened()
|
|||
|
|
|
|||
|
|
def link_down_timeout(self):
|
|||
|
|
self.link_down_event.set()
|
|||
|
|
self.link_down_timer = threading.Timer(5.0, self.link_down_timeout) # 创建一个5秒后执行的定时器
|
|||
|
|
self.link_down_timer.start()
|
|||
|
|
|
|||
|
|
def reset_link_down_timer(self):
|
|||
|
|
if self.link_down_timer:
|
|||
|
|
self.link_down_timer.cancel()
|
|||
|
|
self.link_down_timer = threading.Timer(5.0, self.link_down_timeout) # 创建一个5秒后执行的定时器
|
|||
|
|
self.link_down_timer.start()
|
|||
|
|
|
|||
|
|
def send_heartBit(self):
|
|||
|
|
if self.port_is_opened():
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def crc16_ccitt(self, data: bytearray): # CRC16/CCITT
|
|||
|
|
crcval = 0x0000
|
|||
|
|
for c in data:
|
|||
|
|
q = (crcval ^ c) & 0x0F
|
|||
|
|
crcval = (crcval >> 4) ^ (q * 0o10201)
|
|||
|
|
q = (crcval ^ (c >> 4)) & 0x0F
|
|||
|
|
crcval = (crcval >> 4) ^ (q * 0o10201)
|
|||
|
|
return crcval
|
|||
|
|
|
|||
|
|
def plc_read_frame(self, command, plc_no=0x01, pc_no=0xFF, address='D0160', len=0x02):
|
|||
|
|
frame = []
|
|||
|
|
frame.append(0x05)
|
|||
|
|
frame.extend(hex_to_ascii_arr(plc_no, 2))
|
|||
|
|
frame.extend(hex_to_ascii_arr(pc_no, 2))
|
|||
|
|
frame.extend([ord(c) for c in command]) # word read command
|
|||
|
|
frame.append(0x30)
|
|||
|
|
frame.extend(str_to_ascii_array(address, 5)) # eg.D0160
|
|||
|
|
frame.extend(int_to_ascii_array(len, 2))
|
|||
|
|
checksum = sum(frame[1:]) & 0xFF
|
|||
|
|
frame.extend(hex_to_ascii_arr(checksum, 2))
|
|||
|
|
# condition_print("frame=",frame,type(frame))
|
|||
|
|
condition_print('frame intent to read plc: [', ', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
return frame
|
|||
|
|
|
|||
|
|
def plc_write_frame(self, command, plc_no=0x01, pc_no=0xFF, address='D0160', date_len=0x02, data=[]):
|
|||
|
|
frame = [0x05]
|
|||
|
|
frame.extend(hex_to_ascii_arr(plc_no, 2))
|
|||
|
|
frame.extend(hex_to_ascii_arr(pc_no, 2))
|
|||
|
|
frame.extend([ord(c) for c in command]) # word write command bit write command
|
|||
|
|
frame.append(0x30)
|
|||
|
|
frame.extend(str_to_ascii_array(address, 5)) # eg.D0160
|
|||
|
|
frame.extend(int_to_ascii_array(date_len, 2))
|
|||
|
|
data_index = 0
|
|||
|
|
while date_len > 0:
|
|||
|
|
value = data[data_index]
|
|||
|
|
data_index = data_index + 1
|
|||
|
|
if command == 'BW':
|
|||
|
|
arr_len = 1
|
|||
|
|
else:
|
|||
|
|
arr_len = 4
|
|||
|
|
word_ascii_array = hex_to_ascii_arr(value, arr_len)
|
|||
|
|
# condition_print(word,'[', ', '.join('{:02x}'.format(x) for x in word_ascii_array), ']')
|
|||
|
|
frame.extend(word_ascii_array)
|
|||
|
|
date_len = date_len - 1
|
|||
|
|
checksum = sum(frame[1:]) & 0xFF
|
|||
|
|
frame.extend(hex_to_ascii_arr(checksum, 2))
|
|||
|
|
condition_print('frame intend to write plc: [', ', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
return frame
|
|||
|
|
|
|||
|
|
# Symbol name Description Code (hexadecimal)
|
|||
|
|
# STX Start of Text 02H
|
|||
|
|
# ETX End of Text 03H
|
|||
|
|
# EOT End of Transmission 04H
|
|||
|
|
# ENQ Enquiry 05H
|
|||
|
|
# ACK Acknowledge 06H
|
|||
|
|
# LF Line Feed 0AH
|
|||
|
|
# CL Clear 0CH
|
|||
|
|
# CR Carriage Return ODH
|
|||
|
|
# NAK Negative Acknowledge 15H
|
|||
|
|
def frame_is_valid(self, frame):
|
|||
|
|
# 检查第一个字节是否为控制代码
|
|||
|
|
control_code = frame[0]
|
|||
|
|
ETX: bool = False
|
|||
|
|
if control_code not in [0x05, 0x02, 0x03, 0x4, 0x06, 0x15]:
|
|||
|
|
condition_print("First byte is not a control code.")
|
|||
|
|
return False
|
|||
|
|
# 检查最后两个字节是否为校验和代码
|
|||
|
|
hex_string = ''.join(chr(char) for char in frame[-2:])
|
|||
|
|
# 然后,你可以使用int函数将这个16进制的字符串转换为一个整数,指定基数为16
|
|||
|
|
checksum = int(hex_string, 16)
|
|||
|
|
if frame[0] == 0x02 and frame[-3] != 0x03:
|
|||
|
|
return False
|
|||
|
|
if frame[0] == 0x06 or frame[0] == 0x15:
|
|||
|
|
return True
|
|||
|
|
if frame[-3] == 0x03:
|
|||
|
|
ETX = True
|
|||
|
|
end_position = -3 if ETX else -2
|
|||
|
|
checksum_calc = sum(frame[1:-2]) & 0xFF # 计算校验和并取第八位
|
|||
|
|
if checksum != checksum_calc:
|
|||
|
|
condition_print("Last two bytes do not match checksum.", checksum, checksum_calc, frame[-2] - 0x30,
|
|||
|
|
frame[-1] - 0x30)
|
|||
|
|
return False
|
|||
|
|
# 创建一个包含所有有效字符的字符串
|
|||
|
|
valid_chars = '0123456789abcdefABCDEF'
|
|||
|
|
|
|||
|
|
# 定义一个函数来检查bytes对象中的每个元素
|
|||
|
|
for i in frame[1:end_position]:
|
|||
|
|
char = chr(i)
|
|||
|
|
if char not in valid_chars:
|
|||
|
|
condition_print(f'{char} ({i}) is not a valid character.')
|
|||
|
|
# 在此处添加其他的逻辑处理代码,例如抛出异常
|
|||
|
|
# raise ValueError(f'{char} ({i}) is not a valid character.')
|
|||
|
|
return False
|
|||
|
|
else:
|
|||
|
|
# condition_print(f'{char} ({i}) is a valid character.')
|
|||
|
|
pass
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def plc_write(self, command='WW', plc_no=0x01, pc_no=0xFF, address='D0160', len=0x02, data=0x00000000):
|
|||
|
|
frame = self.plc_write_frame(command, plc_no, pc_no, address, len, data)
|
|||
|
|
timeout = 2 # 设置超时时间为2秒
|
|||
|
|
|
|||
|
|
if hasattr(self, 'ser') and self.ser:
|
|||
|
|
self.ser.write(frame)
|
|||
|
|
frame = self.ser.read(1);
|
|||
|
|
time.sleep(0.06)
|
|||
|
|
start_time = time.time() # 记录开始时间
|
|||
|
|
while self.ser.in_waiting:
|
|||
|
|
# 检查是否超时
|
|||
|
|
if time.time() - start_time > timeout:
|
|||
|
|
condition_print("serial com read Time's out!--", time)
|
|||
|
|
return {"status": "error", "msg": "serial com read Time's out!"}
|
|||
|
|
break
|
|||
|
|
frame += self.ser.read(self.ser.in_waiting) # 读取剩余的数据
|
|||
|
|
if self.frame_is_valid(frame) == False:
|
|||
|
|
condition_print("recv frame is not valid", '[', ', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
return {"status": "error", "msg": "recv frame is not valid"}
|
|||
|
|
else:
|
|||
|
|
condition_print("--write data to plc success", "resp", ' [',
|
|||
|
|
', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
# condition_print('recv pld frame: [', ', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
return {"status": "success", "data": frame}
|
|||
|
|
else:
|
|||
|
|
return {"status": "error", "msg": "serial com is not open!"}
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def plc_read(self, command, plc_no=0x01, pc_no=0xFF, address='D0160', len=0x01):
|
|||
|
|
frame = self.plc_read_frame(command, plc_no, pc_no, address, len)
|
|||
|
|
timeout = 2 # 设置超时时间为2秒
|
|||
|
|
if hasattr(self, 'ser') and self.ser:
|
|||
|
|
self.ser.write(frame)
|
|||
|
|
frame = self.ser.read(1);
|
|||
|
|
time.sleep(0.06)
|
|||
|
|
start_time = time.time() # 记录开始时间
|
|||
|
|
while self.ser.in_waiting:
|
|||
|
|
# 检查是否超时
|
|||
|
|
if time.time() - start_time > timeout:
|
|||
|
|
condition_print("serial com read Time's out!--", time)
|
|||
|
|
return {"status": "error", "msg": "serial com read Time's out!"}
|
|||
|
|
break
|
|||
|
|
frame += self.ser.read(self.ser.in_waiting) # 读取剩余的数据
|
|||
|
|
if self.frame_is_valid(frame) == False:
|
|||
|
|
condition_print("recv frame is not valid")
|
|||
|
|
return {"status": "error", "msg": "recv frame is not valid"}
|
|||
|
|
else:
|
|||
|
|
condition_print('--read data return: [', ', '.join('{:02x}'.format(x) for x in frame), ']')
|
|||
|
|
return {"status": "success", "data": frame}
|
|||
|
|
else:
|
|||
|
|
return {"status": "error", "msg": "serial com is not open!"}
|
|||
|
|
|
|||
|
|
def plc_read_words(self, plc_no=0x01, pc_no=0xFF, address='D0160', length=0x01):
|
|||
|
|
resp = {'status': 'error'}
|
|||
|
|
result = self.plc_read('WR', plc_no, pc_no, address, length)
|
|||
|
|
if result and 'status' in result and result['status'] == 'success' and 'data' in result:
|
|||
|
|
frame = result['data']
|
|||
|
|
data_len = (len(frame) - 8) / 4
|
|||
|
|
if data_len == length:
|
|||
|
|
value = [ascii_array_to_word(frame[5 + 4 * i:5 + 4 * (i + 1)]) for i in range(length)]
|
|||
|
|
resp = {'status': 'success', 'length': data_len, 'data': value}
|
|||
|
|
return resp
|
|||
|
|
|
|||
|
|
def plc_read_bits(self, plc_no=0x01, pc_no=0xFF, address='D0160', length=0x01):
|
|||
|
|
resp = {'status': 'error'}
|
|||
|
|
result = self.plc_read('BR', plc_no, pc_no, address, length)
|
|||
|
|
condition_print('plc_read_bits result', result) # cyx
|
|||
|
|
if 'status' in result and result['status'] == 'success' and 'data' in result:
|
|||
|
|
frame = result['data']
|
|||
|
|
data_len = (len(frame) - 8) / 1
|
|||
|
|
if data_len == length:
|
|||
|
|
value = [(frame[5 + 1 * i] - 0x30) for i in range(length)]
|
|||
|
|
resp = {'status': 'success', 'length': data_len, 'data': value}
|
|||
|
|
return resp
|
|||
|
|
|
|||
|
|
def plc_write_word(self, plc_no=0x01, pc_no=0xFF, address='D0160', data=0x0000):
|
|||
|
|
return self.plc_write('WW', plc_no, pc_no, address, 0x01, list([data]))
|
|||
|
|
|
|||
|
|
def plc_write_dword(self, plc_no=0x01, pc_no=0xFF, address='D0160', data=0x00000000):
|
|||
|
|
return self.plc_write('WW', plc_no, pc_no, address, 0x02, list([data >> 16, data & 0xFFFF]))
|
|||
|
|
|
|||
|
|
def plc_write_words(self, plc_no=0x01, pc_no=0xFF, start_addr='D0160', length=0x04, data=[]):
|
|||
|
|
return self.plc_write('WW', plc_no, pc_no, start_addr, length, data)
|
|||
|
|
|
|||
|
|
def plc_write_bits(self, plc_no=0x01, pc_no=0xFF, address='D0160', length=0x01, data=[]):
|
|||
|
|
return self.plc_write('BW', plc_no, pc_no, address, length, data)
|
|||
|
|
|
|||
|
|
|
|||
|
|
from pymodbus.datastore import ModbusSequentialDataBlock
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CustomDataBlock(ModbusSequentialDataBlock):
|
|||
|
|
def __init__(self, data_type, plc_devices=None, plc_address=0x01, *args, **kwargs):
|
|||
|
|
if plc_devices is None:
|
|||
|
|
plc_devices = []
|
|||
|
|
self.plc_address = plc_address # PLC的 站地址 如:1 3 4等
|
|||
|
|
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] # 按照站地址进行匹配
|
|||
|
|
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, 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:
|
|||
|
|
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':
|
|||
|
|
print('read input register address=', address)
|
|||
|
|
# 处理输入寄存器数据类型的逻辑
|
|||
|
|
if plc_device:
|
|||
|
|
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
|
|||
|
|
resp = plc_device.plc_read_words(self.plc_address, 0xFF, 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")
|
|||
|
|
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':
|
|||
|
|
# 处理保持寄存器数据类型的逻辑
|
|||
|
|
#print('read holding register address=', address,count)
|
|||
|
|
if address == 10: # 10 作为特殊的寄存器,用来返回PLC(物理)的连接状态
|
|||
|
|
if plc_device:
|
|||
|
|
return [plc_device.get_connect_state()]
|
|||
|
|
else:
|
|||
|
|
return [0]
|
|||
|
|
if plc_device:
|
|||
|
|
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
|
|||
|
|
resp = plc_device.plc_read_words(self.plc_address, 0xFF, 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 holding register failed--PLC response error")
|
|||
|
|
|
|||
|
|
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):
|
|||
|
|
plc_device_obj = [item for item in self.plc_devices if item["slave"] == self.plc_address]
|
|||
|
|
if len(plc_device_obj) and plc_device_obj[0].get('device'):
|
|||
|
|
plc_device = plc_device_obj[0].get('device')
|
|||
|
|
if self.data_type == 'coil':
|
|||
|
|
print("modbus server set coil values", address, values) # cyx
|
|||
|
|
# 处理线圈数据类型的逻辑
|
|||
|
|
if plc_device:
|
|||
|
|
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
|
|||
|
|
resp = plc_device.plc_write_bits(self.plc_address, 0xFF, conv_to_plc_address('M', address), 0x01,
|
|||
|
|
values)
|
|||
|
|
if resp and resp.get('status') == 'success' and resp.get('data') and resp['data'][0] == 0x06: # ACK
|
|||
|
|
pass
|
|||
|
|
else:
|
|||
|
|
raise ModbusIOException("Modbus server set coil failed--PLC response error")
|
|||
|
|
else:
|
|||
|
|
# 如果读取不成功,抛出一个Modbus异常
|
|||
|
|
raise ModbusIOException("Modbus server set coil failed--PLC not connected")
|
|||
|
|
elif self.data_type == 'input_register':
|
|||
|
|
# 处理输入寄存器数据类型的逻辑
|
|||
|
|
pass
|
|||
|
|
elif self.data_type == 'holding_register':
|
|||
|
|
print("modbus server set holding register address=", address, values, self.plc_address) # cyx
|
|||
|
|
if plc_device:
|
|||
|
|
with self.plc_comm_lock: # 保护访问资源,执行完缩进的代码块释放资源
|
|||
|
|
resp = plc_device.plc_write_word(self.plc_address, 0xFF, conv_to_plc_address('D', address),
|
|||
|
|
values[0])
|
|||
|
|
if resp and resp.get('status') == 'success' and resp.get('data') and resp['data'][0] == 0x06: # 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")
|
|||
|
|
|
|||
|
|
|
|||
|
|
exit_flag = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ModbusPlcServer:
|
|||
|
|
def __init__(self, host='127.0.0.1', port=5020, machines_config=None, timeout=3):
|
|||
|
|
if machines_config is None:
|
|||
|
|
machines_config = []
|
|||
|
|
self.server_task = None
|
|||
|
|
self.modbus_server = None
|
|||
|
|
self.host = host
|
|||
|
|
self.port = port
|
|||
|
|
self.is_running = True # 服务器运行状态的标志变量
|
|||
|
|
self.plc_devices = []
|
|||
|
|
self.machines_config = machines_config
|
|||
|
|
try:
|
|||
|
|
for config in machines_config:
|
|||
|
|
if config.get('type') == 'mhi' and config.get('com').startswith("com") and config.get('plcAddress'):
|
|||
|
|
# 使用列表推导式生成列表
|
|||
|
|
# 按照com 串口进行区分的,如果1个RS485上有几个PLC, 实际因为只用一个COM口,所以也就创建1个MitsubishiPLC
|
|||
|
|
# modbus client 访问时会传入站地址由于区分是哪个PLC,如:1 3 7 8等
|
|||
|
|
device_matched = [device_json['device'] for device_json in self.plc_devices if
|
|||
|
|
device_json["com"] == config.get('com')]
|
|||
|
|
# 判断列表是否包含元素,并据此取第一个元素或返回None
|
|||
|
|
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})
|
|||
|
|
|
|||
|
|
except Exception as error:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
async def run_server(self, ):
|
|||
|
|
global serverUrl
|
|||
|
|
"""
|
|||
|
|
block = ModbusSequentialDataBlock(0x00, [0] * 0xff)
|
|||
|
|
store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block)
|
|||
|
|
context = ModbusServerContext(slaves=store, single=True)
|
|||
|
|
|
|||
|
|
identity = ModbusDeviceIdentification()
|
|||
|
|
identity.VendorName = 'HYSZ Tech'
|
|||
|
|
identity.ProductCode = 'DTMGT-01'
|
|||
|
|
identity.VendorUrl = 'http://www.hysz.com'
|
|||
|
|
identity.ProductName = 'DTMGT PLC Server'
|
|||
|
|
identity.ModelName = 'DTMGT PLC Server'
|
|||
|
|
identity.MajorMinorRevision = '1.0'
|
|||
|
|
"""
|
|||
|
|
stores = {}
|
|||
|
|
# 创建 Modbus 数据块和上下文
|
|||
|
|
context = None
|
|||
|
|
for config in self.machines_config:
|
|||
|
|
coil_block = CustomDataBlock('coil', self.plc_devices, int(config.get('plcAddress')), 0, [False] * 0x100)
|
|||
|
|
holding_register_block = CustomDataBlock('holding_register', self.plc_devices,
|
|||
|
|
int(config.get('plcAddress')), 0, [0] * 0x100)
|
|||
|
|
input_coil_block = CustomDataBlock('input_colid', self.plc_devices, int(config.get('plcAddress')), 0,
|
|||
|
|
[False] * 0x100)
|
|||
|
|
input_register_block = CustomDataBlock('input_register', self.plc_devices, int(config.get('plcAddress')), 0,
|
|||
|
|
[0] * 0x100)
|
|||
|
|
store = ModbusSlaveContext(
|
|||
|
|
di=input_coil_block,
|
|||
|
|
co=coil_block,
|
|||
|
|
hr=holding_register_block,
|
|||
|
|
ir=input_register_block,
|
|||
|
|
zero_mode=True
|
|||
|
|
)
|
|||
|
|
stores[int(config.get('plcAddress'))] = store
|
|||
|
|
context = ModbusServerContext(slaves=stores, single=False) # 每个机器的modbus server 都有自己的上下文context (stores)
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
coil_block = CustomDataBlock('coil', self.plc_devices, 1, 0, [False] * 0x100)
|
|||
|
|
holding_register_block = CustomDataBlock('holding_register', self.plc_devices,
|
|||
|
|
1, 0, [0] * 0x100)
|
|||
|
|
input_coil_block = CustomDataBlock('input_colid', self.plc_devices, 1, 0,
|
|||
|
|
[False] * 0x100)
|
|||
|
|
input_register_block = CustomDataBlock('input_register', self.plc_devices, 1, 0,
|
|||
|
|
[0] * 0x100)
|
|||
|
|
store = ModbusSlaveContext(
|
|||
|
|
di=input_coil_block,
|
|||
|
|
co=coil_block,
|
|||
|
|
hr=holding_register_block,
|
|||
|
|
ir=input_register_block,
|
|||
|
|
zero_mode=True
|
|||
|
|
)
|
|||
|
|
context = ModbusServerContext(slaves=store, single=True)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# requests.post(serverUrl + '/modbusServerStarted', params={}, json={})
|
|||
|
|
print(f"Modbus server starting on {self.host}:{self.port}")
|
|||
|
|
try:
|
|||
|
|
self.modbus_server = await StartAsyncTcpServer(context=context, address=(self.host, self.port))
|
|||
|
|
print(f"Modbus server started on {self.host}:{self.port}")
|
|||
|
|
self.modbus_server.serve_forever()
|
|||
|
|
except Exception as error:
|
|||
|
|
print("Failed to start the server:", error)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def stop_server(self):
|
|||
|
|
global exit_flag
|
|||
|
|
if self.modbus_server:
|
|||
|
|
self.modbus_server.shutdown()
|
|||
|
|
self.modbus_server.serve_done.set_result(True)
|
|||
|
|
self.server_task.cancel() # 取消任务
|
|||
|
|
try:
|
|||
|
|
await self.server_task # 等待任务结束
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
finally:
|
|||
|
|
await self.modbus_server.wait_closed()
|
|||
|
|
exit_flag = True
|
|||
|
|
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
def __init__(self, machines_config):
|
|||
|
|
self.modbus_loop = None
|
|||
|
|
self.modbus_server = None
|
|||
|
|
self.machines_config = machines_config
|
|||
|
|
|
|||
|
|
def run_modbus_server(self):
|
|||
|
|
print("mhi_plc create modbus server")
|
|||
|
|
self.modbus_loop = asyncio.new_event_loop()
|
|||
|
|
asyncio.set_event_loop(self.modbus_loop)
|
|||
|
|
self.modbus_server = ModbusPlcServer('127.0.0.1', 5020, self.machines_config)
|
|||
|
|
self.modbus_loop.run_until_complete(self.modbus_server.run_server())
|
|||
|
|
print("mhi_plc create modbus over")
|
|||
|
|
|
|||
|
|
def stop_modbus_server(self):
|
|||
|
|
print("stop_modbus_server")
|
|||
|
|
try:
|
|||
|
|
if self.modbus_server:
|
|||
|
|
self.modbus_server.stop_server()
|
|||
|
|
print("modbus server stop ")
|
|||
|
|
if hasattr(self, 'modbus_loop') and self.modbus_loop.is_running():
|
|||
|
|
self.modbus_loop.call_soon_threadsafe(self.modbus_loop.stop)
|
|||
|
|
print("modbus server event loop exit")
|
|||
|
|
except Exception as error:
|
|||
|
|
print("stop_modbus_servererror:", error)
|
|||
|
|
|
|||
|
|
|
|||
|
|
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")
|
|||
|
|
|
|||
|
|
# 解析参数
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
serverUrl = args.serverUrl
|
|||
|
|
main()
|