OpenPLC中端点实时监控功能的实现——Pymodbus

Openplc中利用pymodbus库来实现对软PLC 输入输出的监控,并在【OpenPLC_v3\webserver】文件夹的monitoring.py文件中定义了有关的接口的实现,这篇博客对pymodbus和OpenPLC中monitoring.py进行简单的介绍,以方便大家借助OpenPLC提供的接口进一步开发调试、变量监控等功能。

1 Pymodbus的基本使用流程介绍

在PLC(硬或软)上启用Modbus服务的过程涉及硬件和软件的配置,具体步骤和方法会依赖于使用的PLC品牌和型号。

对于硬PLC,需要通过相应的PLC编程软件来配置Modbus协议的工作模式(如Modbus TCP或RTU),设置相关的寄存器和通信参数,然后将配置下载到PLC上。

对于软PLC(安装了OpenPLC runtime的普通嵌入式设备),在安装OpenPLC runtime时,已经搭载了modbus server的有关服务(在本机端口502上),可以通过Openplc webserver提供的有关接口直接启用。

在PLC上启动了Modbus服务后,我们可以使用pymodbus和该Modbus服务通信,从而获取数据,pymodbus的介绍如下:

pymodbus是一个支持 Modbus 协议的库,支持 Modbus RTU、Modbus TCP 等多种通信模式。以下是如何使用 pymodbus 库来监控 PLC 输入输出的基本步骤:

连接到 PLC

根据你的 PLC 类型(Modbus RTU 或 Modbus TCP),你需要选择不同的连接方式。

1. Modbus TCP 连接

Modbus TCP 是基于以太网的,可以通过 IP 地址和端口来连接 PLC,例如

from pymodbus.client.sync import ModbusTcpClient

# 连接到PLC(假设PLC的IP地址是192.168.1.100,端口是502)
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

2. Modbus RTU 连接

Modbus RTU 是基于串口的通信,需要通过串口设备来连接 PLC。

from pymodbus.client.sync import ModbusSerialClient as ModbusClient

# 连接到PLC(假设PLC的串口设备是'/dev/ttyUSB0',波特率是9600)
client = ModbusClient(method='rtu', port='/dev/ttyUSB0', baudrate=9600)
client.connect()

读取输入输出数据

Modbus 协议有不同类型的寄存器,其中最常见的类型包括:

  • Coils (离散输出):通常用于控制开关等输出设备。
  • Discrete Inputs (离散输入):读取开关状态等离散输入。
  • Holding Registers (保持寄存器):通常用于读写设备的参数。
  • Input Registers (输入寄存器):读取设备传感器等输入数据。

可以使用 pymodbus 提供的不同方法来读取这些寄存器,这里仅以读取离散输入为例:

# 读取离散输入(假设你要读取地址为0到9的离散输入)
result = client.read_discrete_inputs(0, 10)
if result.isError():
  print("Error reading discrete inputs")
else:
  print(result.bits) # 打印离散输入的值(True/False)

写入数据

除了读取数据,pymodbus 也支持写入数据

# 写入一个保持寄存器的值(假设写入地址为0的寄存器,值为123)
client.write_register(0, 123)

关闭连接

完成所有操作后,需要关闭与 PLC 的连接:

client.close()

2 OpenPLC中关于monitoring.py的介绍

OpenPLC中使用Modbus TCP 链接,有关的使用接口都定义在【OpenPLC_v3\webserver】文件夹的monitoring.py文件中,各个接口的介绍如下:

 2.1 class debug_var

记录上传的.st文件中定义所有的输入输出变量的name, location, type, value等信息

利用parse_st()函数可以解析提供的.st文件,提取变量信息,并根据条件将这些变量添加到debug_vars中

利用cleanup函数可以清空 debug_vars 列表中的内容,释放已存储的调试变量

2.2 class modbus_monitor

提供利用pymodbus监控变量的接口,具体如下:

  • modbus_monitor():

    • 作用:定期从 Modbus 设备读取调试变量的数据,并更新变量的值。
    • 详细步骤
      • 遍历 debug_vars 列表中的每个 debug_var 对象。
      • 根据每个变量的位置(如 %IX, %QX, %IW 等)判断其类型,执行相应的 Modbus 读取操作。
      • 通过 mb_client 对象与 Modbus 设备通信,读取对应的寄存器或内存值。
      • 将读取到的值更新到 debug_data.value 中。
      • 如果 monitor_activeTrue,则每隔 0.5 秒递归调用modbus_monitor,实现持续监控,具体代码如下(感觉会导致出现pymodbus.exceptions.ConnectionException: Modbus Error: [Connection] ModbusTcpClient(127.0.0.1:502): Connection unexpectedly closed 0.000065 seconds into read of 8 bytes without response from unit before it closed connection):

 

        if (monitor_active == True):
        threading.Timer(0.5, modbus_monitor).start()

 

  • start_monitor(modbus_port_cfg):

    • 作用:启动 Modbus 监控,初始化 Modbus 客户端并开始读取数据。
    • 步骤
      • 判断是否已经在监控,如果没有,则初始化 ModbusTcpClient,并使用提供的端口配置连接到 Modbus 设备。
      • 设置 monitor_active = True,表示监控已启动。
      • 调用 modbus_monitor() 开始监控。
  • stop_monitor():

    • 作用:停止 Modbus 监控,关闭 Modbus 客户端连接。
    • 步骤
      • 判断当前是否正在监控,如果是,关闭 Modbus 客户端并将 monitor_active 设置为 False

3 OpenPLC中关于启动modbus服务的介绍

在【OpenPLC_v3\webserver】文件夹的webserver.py文件中,runtime利用以下代码启动modbus服务:

openplc_runtime.start_modbus(int(row[1]))

而start_modbus方法的具体实现为:

def start_modbus(self, port_num):
  return self._rpc(f'start_modbus({port_num})')

_rpc方法具体为:

def _rpc(self, msg, timeout=1000):
  data = ""
  if not self.runtime_status == "Running":
    return data
  try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('localhost', 43628))
    s.send(f'{msg}\n'.encode('utf-8'))
    data = s.recv(timeout).decode('utf-8')
    s.close()
    self.runtime_status = "Running"
  except socket.error as serr:
    print(f'Socket error during {msg}, is the runtime active?')
    self.runtime_status = "Stopped"
  return data

可以看出_rpc方法具体利用本机43628端口上运行的服务来启动modbus等一系列协议有关的服务,openplc中定义的modbus服务的端口是502,即port_num=502

 4 OpenPLC中关于查看debug变量的代码

在【OpenPLC_v3\webserver】文件夹的webserver.py文件中,runtime利用以下代码启动获取监控的点位数据,以下代码位于“

@app.route('/monitoring', methods=['GET', 'POST'])
def monitoring():
”函数中,是部分截取:
if modbus_enabled == True:
monitor.start_monitor(modbus_port_cfg)
data_index = 0
for debug_data in monitor.debug_vars:
return_str += '<tr style="height:60px" onclick="document.location=\'point-info?table_id=' + str(data_index) + '\'">'
return_str += '<td>' + debug_data.name + '</td><td>' + debug_data.type + '</td><td>' + debug_data.location + '</td><td>' + debug_data.forced + '</td><td valign="middle">'
if (debug_data.type == 'BOOL'):
if (debug_data.value == 0):
return_str += '<img src="/static/bool_false.png" alt="bool_false" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">FALSE</td>'
else:
return_str += '<img src="/static/bool_true.png" alt="bool_true" style="width:40px;height:40px;vertical-align:middle; margin-right:10px">TRUE</td>'
elif (debug_data.type == 'UINT'):
percentage = (debug_data.value*100)/65535
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'INT'):
percentage = ((debug_data.value + 32768)*100)/65535
debug_data.value = ctypes.c_short(debug_data.value).value
return_str += '<div class="w3-grey w3-round" style="height:40px"><div class="w3-container w3-blue w3-round" style="height:40px;width:' + str(int(percentage)) + '%"><p style="margin-top:10px">' + str(debug_data.value) + '</p></div></div></td>'
elif (debug_data.type == 'REAL') or (debug_data.type == 'LREAL'):
return_str += "{:10.4f}".format(debug_data.value)
else:
return_str += str(debug_data.value)
return_str += '</tr>'
data_index += 1
return_str += """
</table>
</div>
<input type='hidden' id='modbus_port_cfg' name='modbus_port_cfg' value='""" + str(modbus_port_cfg) + "'>"
return_str += pages.monitoring_tail
 注意:OpenPLC是前端不断主动get该路由函数,从而实现实时更新的,而不是建立了socketio从而与前端实时通信

 

posted @ 2024-12-17 15:32  碳酸钾K2CO3  阅读(299)  评论(0)    收藏  举报