将tcp协议修改为opcua协议
This commit is contained in:
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"hopper_up_weight": "666666",
|
|
||||||
"hopper_down_weight": "5444444"
|
|
||||||
}
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
'''
|
|
||||||
# @Time : 2025/9/19 09:48
|
|
||||||
# @Author : reenrr
|
|
||||||
# @File : mock_server.py
|
|
||||||
'''
|
|
||||||
import socket
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
|
|
||||||
class TCPServerSimulator:
|
|
||||||
def __init__(self, host='127.0.0.1', port=8888, config_file='config.json'):
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.server_socket = None
|
|
||||||
self.is_running = False
|
|
||||||
self.client_sockets = []
|
|
||||||
self.config_file = config_file
|
|
||||||
|
|
||||||
# 初始状态为None
|
|
||||||
self.data_template = None
|
|
||||||
|
|
||||||
# 从配置文件中加载固定数据
|
|
||||||
self.load_config_data()
|
|
||||||
|
|
||||||
# 模拟数据模板
|
|
||||||
if self.data_template is None:
|
|
||||||
self.data_template = {
|
|
||||||
"hopper_up_weight": 0.0, # 上料斗重量
|
|
||||||
"hopper_down_weight": 0.0 # 下料斗重量
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_config_data(self):
|
|
||||||
"""从配置文件中加载固定数据"""
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.config_file):
|
|
||||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
||||||
self.data_template = json.load(f)
|
|
||||||
print(f"成功从 {self.config_file} 加载配置数据")
|
|
||||||
else:
|
|
||||||
print(f"配置文件 {self.config_file} 不存在")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"加载配置文件时发生错误:{e},将使用默认数据")
|
|
||||||
self.data_template = None
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""启动服务器"""
|
|
||||||
self.is_running = True
|
|
||||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self.server_socket.bind((self.host, self.port))
|
|
||||||
self.server_socket.listen(5)
|
|
||||||
print(f"服务器已启动,监听 {self.host}:{self.port}...")
|
|
||||||
|
|
||||||
# 启动接受连接的线程
|
|
||||||
accept_thread = threading.Thread(target=self.accept_connections, daemon=True)
|
|
||||||
accept_thread.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
while self.is_running:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n服务器正在关闭...")
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def accept_connections(self):
|
|
||||||
"""接受客户端连接"""
|
|
||||||
while self.is_running:
|
|
||||||
try:
|
|
||||||
client_socket, client_address = self.server_socket.accept()
|
|
||||||
self.client_sockets.append(client_socket)
|
|
||||||
print(f"客户端 {client_address} 已连接")
|
|
||||||
|
|
||||||
# 发送数据
|
|
||||||
data = self.generate_simulated_data()
|
|
||||||
self.send_data(client_socket, data)
|
|
||||||
print(f"已向客户端 {client_address} 发送数据:{data}")
|
|
||||||
|
|
||||||
# 启动一个线程监听客户端发送的指令
|
|
||||||
threading.Thread(
|
|
||||||
target=self.listen_client_commands,
|
|
||||||
args=(client_socket,client_address),
|
|
||||||
daemon=True
|
|
||||||
).start()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if self.is_running:
|
|
||||||
print(f"接受连接时发生错误: {e}")
|
|
||||||
break
|
|
||||||
|
|
||||||
def listen_client_commands(self, client_socket, client_address):
|
|
||||||
"""监听客户端发送的指令"""
|
|
||||||
while self.is_running and client_socket in self.client_sockets:
|
|
||||||
try:
|
|
||||||
# 接收客户端发送的指令
|
|
||||||
data = client_socket.recv(1024).decode('utf-8').strip()
|
|
||||||
if data:
|
|
||||||
print(f"客户端 {client_address} 发送指令: {data}")
|
|
||||||
else:
|
|
||||||
print(f"客户端 {client_address} 已断开连接")
|
|
||||||
self.client_sockets.remove(client_socket)
|
|
||||||
client_socket.close()
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
print(f"监听客户端 {client_address} 指令时发生错误: {e}")
|
|
||||||
self.client_sockets.remove(client_socket)
|
|
||||||
client_socket.close()
|
|
||||||
break
|
|
||||||
|
|
||||||
def generate_simulated_data(self):
|
|
||||||
"""生成模拟的状态数据"""
|
|
||||||
if self.data_template is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
data = self.data_template.copy()
|
|
||||||
data["timestamp"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def send_data(self, client_socket, data):
|
|
||||||
"""向客户端发送数据"""
|
|
||||||
try:
|
|
||||||
# 转换为JSON字符串并添加换行符作为结束标记
|
|
||||||
if data is None:
|
|
||||||
data_str = json.dumps(None) + "\n"
|
|
||||||
else:
|
|
||||||
data_str = json.dumps(data) + "\n"
|
|
||||||
client_socket.sendall(data_str.encode('utf-8'))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"向客户端 {client_socket.getpeername()} 发送数据时发生错误: {e}")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""停止服务器"""
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
# 关闭所有客户端连接
|
|
||||||
for sock in self.client_sockets:
|
|
||||||
try:
|
|
||||||
sock.close()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"关闭客户端连接时发生错误: {e}")
|
|
||||||
|
|
||||||
# 关闭服务器套接字
|
|
||||||
if self.server_socket:
|
|
||||||
try:
|
|
||||||
self.server_socket.close()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"关闭服务器套接字时发生错误: {e}")
|
|
||||||
|
|
||||||
print("服务器已关闭")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
server = TCPServerSimulator(host='127.0.0.1', port=8888)
|
|
||||||
server.start()
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from typing import List
|
from typing import List
|
||||||
from PySide6.QtCore import Qt, Signal, QEasingCurve, QUrl, QSize, QTimer, QEvent
|
from PySide6.QtCore import Qt, Signal, QEasingCurve, QUrl, QSize, QTimer, QEvent, QThread
|
||||||
from PySide6.QtGui import QIcon, QDesktopServices, QColor, QPalette, QBrush, QImage
|
from PySide6.QtGui import QIcon, QDesktopServices, QColor, QPalette, QBrush, QImage
|
||||||
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QVBoxLayout,
|
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QVBoxLayout,
|
||||||
QFrame, QWidget, QSpacerItem, QSizePolicy, QMainWindow, QPushButton)
|
QFrame, QWidget, QSpacerItem, QSizePolicy, QMainWindow, QPushButton)
|
||||||
@ -8,7 +8,7 @@ from PySide6.QtWidgets import (QApplication, QHBoxLayout, QVBoxLayout,
|
|||||||
from system_nav_bar import SystemNavBar
|
from system_nav_bar import SystemNavBar
|
||||||
from bottom_control_widget import BottomControlWidget
|
from bottom_control_widget import BottomControlWidget
|
||||||
from weight_dialog import WeightDetailsDialog
|
from weight_dialog import WeightDetailsDialog
|
||||||
from tcp_client import TcpClient
|
from opcua_client import OpcuaClient
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QWidget):
|
class MainWindow(QWidget):
|
||||||
@ -17,7 +17,7 @@ class MainWindow(QWidget):
|
|||||||
self.initWindow()
|
self.initWindow()
|
||||||
self.createSubWidgets() # 创建子部件
|
self.createSubWidgets() # 创建子部件
|
||||||
self.setupLayout() # 设置布局
|
self.setupLayout() # 设置布局
|
||||||
self.tcp_client = TcpClient(self) # 创建 TCP 客户端
|
self.init_opc_client() # 初始化OPC客户端
|
||||||
self.bind_signals() # 绑定信号槽
|
self.bind_signals() # 绑定信号槽
|
||||||
|
|
||||||
def initWindow(self):
|
def initWindow(self):
|
||||||
@ -92,21 +92,32 @@ class MainWindow(QWidget):
|
|||||||
self.close()
|
self.close()
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# OPC UA 客户端初始化
|
||||||
|
# -------------------
|
||||||
|
def init_opc_client(self):
|
||||||
|
"""初始化 OPC UA 客户端并启动线程(避免阻塞UI)"""
|
||||||
|
self.Opcua_client = OpcuaClient(self) # 创建 OPC UA 客户端实例
|
||||||
|
# 创建独立线程运行 OPC UA 客户端(避免阻塞UI线程)
|
||||||
|
self.opc_thread = QThread()
|
||||||
|
self.Opcua_client.moveToThread(self.opc_thread)
|
||||||
|
self.opc_thread.started.connect(self.Opcua_client.start_connect) # 线程启动后触发客户端连接
|
||||||
|
self.opc_thread.start() # 启动线程
|
||||||
|
|
||||||
# ---------------
|
# ---------------
|
||||||
# TCP更新重量信息
|
# OPC UA 信号绑定与数据更新
|
||||||
# ---------------
|
# ---------------
|
||||||
def bind_signals(self):
|
def bind_signals(self):
|
||||||
"""绑定TCP信号到弹窗更新函数"""
|
"""绑定TCP信号到弹窗更新函数"""
|
||||||
self.tcp_client.weight_updated.connect(self.update_weight_dialogs) # 绑定更新重量信息的信号槽
|
self.Opcua_client.weight_updated.connect(self.update_weight_dialogs) # 绑定更新重量信息的信号槽
|
||||||
# 连接状态信号:更新状态图标
|
# 连接状态信号:更新状态图标
|
||||||
self.tcp_client.connection_status_changed.connect(self.update_connection_status)
|
self.Opcua_client.connection_status_changed.connect(self.update_connection_status)
|
||||||
# 上料斗重量信息更新失败信号:更新状态图标
|
# 上料斗重量信息更新失败信号:更新状态图标
|
||||||
self.tcp_client.upper_weight_error.connect(self.upper_weight_status) # 重量信息更新失败信号
|
self.Opcua_client.upper_weight_error.connect(self.upper_weight_status) # 重量信息更新失败信号
|
||||||
# 下料斗重量信息更新失败信号:更新状态图标
|
# 下料斗重量信息更新失败信号:更新状态图标
|
||||||
self.tcp_client.down_weight_error.connect(self.down_weight_status) # 重量信息更新失败信号
|
self.Opcua_client.down_weight_error.connect(self.down_weight_status) # 重量信息更新失败信号
|
||||||
# 启动 TCP 连接
|
# 启动 TCP 连接
|
||||||
print(f"客户端启动,自动连接服务端{self.tcp_client.tcp_server_host}:{self.tcp_client.tcp_server_port}...")
|
print(f"客户端启动,自动连接服务端{self.Opcua_client.opc_server_url}")
|
||||||
self.tcp_client._connect_to_server()
|
|
||||||
|
|
||||||
def down_weight_status(self, is_error):
|
def down_weight_status(self, is_error):
|
||||||
"""发送上料斗错误信息,更新连接状态"""
|
"""发送上料斗错误信息,更新连接状态"""
|
||||||
@ -131,6 +142,14 @@ class MainWindow(QWidget):
|
|||||||
down_weight = data.get("hopper_down_weight", 0)
|
down_weight = data.get("hopper_down_weight", 0)
|
||||||
self.weight_dialog2.update_weight(down_weight)
|
self.weight_dialog2.update_weight(down_weight)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""窗口关闭时,停止 OPC UA 客户端和线程(避免资源泄露)"""
|
||||||
|
if hasattr(self, 'Opcua_client'):
|
||||||
|
self.Opcua_client.stop() # 停止客户端(断开连接、停止定时器)
|
||||||
|
if hasattr(self, 'opc_thread') and self.opc_thread.isRunning():
|
||||||
|
self.opc_thread.quit() # 退出线程
|
||||||
|
self.opc_thread.wait(2000) # 等待线程退出(最多2秒)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
267
opcua_client.py
Normal file
267
opcua_client.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
# @Time : 2025/11/17 18:51
|
||||||
|
# @Author : reenrr
|
||||||
|
# @File : opcua_client_test1111.py
|
||||||
|
'''
|
||||||
|
from PySide6.QtCore import QTimer, Slot, Signal, QObject, QThread, Qt
|
||||||
|
from opcua import Client, ua
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# -----------
|
||||||
|
# 参数配置
|
||||||
|
# -----------
|
||||||
|
OPC_SERVER_URL = "opc.tcp://localhost:4841/freeopcua/server/"
|
||||||
|
UPPER_WEIGHT_NODE_ID = "ns=2;s=upper_weight"
|
||||||
|
LOWER_WEIGHT_NODE_ID = "ns=2;s=lower_weight"
|
||||||
|
|
||||||
|
RECONNECT_INTERVAL = 2000 # 重连间隔(毫秒)
|
||||||
|
DATA_READ_INTERVAL = 2000 # 数据读取间隔(毫秒,与服务端更新频率一致)
|
||||||
|
|
||||||
|
class OpcuaClient(QObject):
|
||||||
|
# 客户端的信号接口
|
||||||
|
weight_updated = Signal(dict) # 重量更新信号(传递 {"hopper_up_weight": 值, "hopper_down_weight": 值})
|
||||||
|
connection_status_changed = Signal(bool) # 连接状态更新信号(True=连接,False=断开)
|
||||||
|
upper_weight_error = Signal(bool) # 上料斗重量异常信号(True=异常,False=正常)
|
||||||
|
down_weight_error = Signal(bool) # 下料斗重量异常信号(True=异常,False=正常)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.is_running = False # 系统运行状态标记
|
||||||
|
self.latest_weight_data = {"hopper_up_weight": 0, "hopper_down_weight": 0} # 缓存最新重量数据
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
# OPC UA 客户端核心配置
|
||||||
|
# ---------------
|
||||||
|
self.opc_client = None # OPC UA 客户端实例(延迟初始化)
|
||||||
|
self.opc_server_url = OPC_SERVER_URL
|
||||||
|
self.upper_weight_node = None # 上料斗重量节点对象
|
||||||
|
self.lower_weight_node = None # 下料斗重量节点对象
|
||||||
|
|
||||||
|
self.is_opc_connected = False # OPC UA 连接状态标记
|
||||||
|
self.has_connected_once = False # 至少连接成功一次标记
|
||||||
|
self.reconnect_count = 0 # 重连次数计数器
|
||||||
|
|
||||||
|
# 重连定时器(连接断开时触发)
|
||||||
|
self.reconnect_timer = QTimer(self)
|
||||||
|
self.reconnect_timer.setInterval(RECONNECT_INTERVAL)
|
||||||
|
self.reconnect_timer.timeout.connect(self.reconnect_to_server)
|
||||||
|
|
||||||
|
# 数据读取定时器(连接成功后触发,定时读取重量数据)
|
||||||
|
self.data_read_timer = QTimer(self)
|
||||||
|
self.data_read_timer.setInterval(DATA_READ_INTERVAL)
|
||||||
|
self.data_read_timer.timeout.connect(self._read_weight_data)
|
||||||
|
|
||||||
|
# 初始化 OPC UA 客户端
|
||||||
|
self._init_opc_client()
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# OPC UA 初始化相关函数
|
||||||
|
# ---------------------
|
||||||
|
def _init_opc_client(self):
|
||||||
|
"""初始化 OPC UA 客户端实例"""
|
||||||
|
try:
|
||||||
|
self.opc_client = Client(self.opc_server_url)
|
||||||
|
print("OPC UA 客户端初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OPC UA 客户端初始化失败:{e}")
|
||||||
|
self.opc_client = None
|
||||||
|
|
||||||
|
def start_connect(self):
|
||||||
|
"""启动连接(供外部调用,主动发起连接请求)"""
|
||||||
|
self._connect_to_server()
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# 连接管理相关函数
|
||||||
|
# ---------------------
|
||||||
|
def reconnect_to_server(self):
|
||||||
|
"""重连执行函数(与原 TCP 客户端一致)"""
|
||||||
|
if not self.is_opc_connected:
|
||||||
|
self.reconnect_count += 1
|
||||||
|
# 日志优化:每10次重连提示一次,避免日志刷屏
|
||||||
|
if self.reconnect_count % 10 == 0:
|
||||||
|
print(f"已连续重连{self.reconnect_count}次,仍未连接服务端,请检查服务端状态...")
|
||||||
|
else:
|
||||||
|
print(f"第{self.reconnect_count}次重连请求:{self.opc_server_url}")
|
||||||
|
self._connect_to_server()
|
||||||
|
|
||||||
|
def _connect_to_server(self):
|
||||||
|
"""主动发起 OPC UA 连接(核心连接逻辑)"""
|
||||||
|
if self.is_opc_connected or not self.opc_client:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 连接到 OPC UA 服务器
|
||||||
|
self.opc_client.connect()
|
||||||
|
print(f"OPC UA 连接成功:{self.opc_server_url}")
|
||||||
|
|
||||||
|
# 2. 获取重量节点对象(仅连接成功时获取一次)
|
||||||
|
self.upper_weight_node = self.opc_client.get_node(UPPER_WEIGHT_NODE_ID)
|
||||||
|
self.lower_weight_node = self.opc_client.get_node(LOWER_WEIGHT_NODE_ID)
|
||||||
|
|
||||||
|
# 3. 验证节点是否有效
|
||||||
|
self._verify_nodes()
|
||||||
|
|
||||||
|
# 4. 更新连接状态
|
||||||
|
self.is_opc_connected = True
|
||||||
|
self.has_connected_once = True
|
||||||
|
self.reconnect_count = 0
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
# 5. 发送信号(连接成功+错误状态重置)
|
||||||
|
self.connection_status_changed.emit(True)
|
||||||
|
self.upper_weight_error.emit(False)
|
||||||
|
self.down_weight_error.emit(False)
|
||||||
|
|
||||||
|
# 6. 启动数据读取定时器,停止重连定时器
|
||||||
|
self.data_read_timer.start()
|
||||||
|
self.reconnect_timer.stop()
|
||||||
|
|
||||||
|
# 7. 立即读取一次初始数据
|
||||||
|
self._read_weight_data()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OPC UA 连接失败:{e}")
|
||||||
|
self._handle_connect_failed()
|
||||||
|
|
||||||
|
def _reconnect_to_server(self):
|
||||||
|
"""重连执行函数"""
|
||||||
|
if self.is_opc_connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.reconnect_count += 1
|
||||||
|
# 日志优化:每10次重连提示一次,避免日志刷屏
|
||||||
|
if self.reconnect_count % 10 == 0:
|
||||||
|
print(f"已连续重连{self.reconnect_count}次,仍未连接服务端,请检查服务端状态...")
|
||||||
|
else:
|
||||||
|
print(f"第{self.reconnect_count}次重连请求:{self.opc_server_url}")
|
||||||
|
|
||||||
|
self._connect_to_server()
|
||||||
|
|
||||||
|
def _disconnect_from_server(self):
|
||||||
|
"""主动断开 OPC UA 连接"""
|
||||||
|
if self.opc_client and self.is_opc_connected:
|
||||||
|
try:
|
||||||
|
self.opc_client.disconnect()
|
||||||
|
print(f"OPC UA 连接断开:{self.opc_server_url}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OPC UA 断开连接失败:{e}")
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
self.is_opc_connected = False
|
||||||
|
self.is_running = False
|
||||||
|
self.upper_weight_node = None
|
||||||
|
self.lower_weight_node = None
|
||||||
|
|
||||||
|
# 停止数据读取定时器
|
||||||
|
self.data_read_timer.stop()
|
||||||
|
|
||||||
|
def _handle_connect_failed(self):
|
||||||
|
"""处理连接失败逻辑"""
|
||||||
|
self.is_opc_connected = False
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
# 发送连接断开信号
|
||||||
|
self.connection_status_changed.emit(False)
|
||||||
|
# 发送重量清零信号
|
||||||
|
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": ""})
|
||||||
|
# 标记重量异常
|
||||||
|
self.upper_weight_error.emit(True)
|
||||||
|
self.down_weight_error.emit(True)
|
||||||
|
|
||||||
|
# 启动重连定时器(首次连接失败或重连失败时)
|
||||||
|
if not self.reconnect_timer.isActive():
|
||||||
|
print(f"将在{RECONNECT_INTERVAL / 1000}秒后启动重连...")
|
||||||
|
self.reconnect_timer.start()
|
||||||
|
|
||||||
|
def _verify_nodes(self):
|
||||||
|
"""验证重量节点是否有效(避免节点ID错误)"""
|
||||||
|
try:
|
||||||
|
# 读取节点的基本信息,验证节点存在
|
||||||
|
upper_node_id = self.upper_weight_node.nodeid
|
||||||
|
lower_node_id = self.lower_weight_node.nodeid
|
||||||
|
print(f"节点验证成功:上料斗节点={upper_node_id}, 下料斗节点={lower_node_id}")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"节点验证失败(节点ID可能错误):{e}")
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# 数据读取与处理相关函数
|
||||||
|
# ---------------------
|
||||||
|
@Slot()
|
||||||
|
def _read_weight_data(self):
|
||||||
|
"""定时读取重量数据(核心业务逻辑)"""
|
||||||
|
if not self.is_opc_connected or not self.upper_weight_node or not self.lower_weight_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取 OPC UA 节点的重量值
|
||||||
|
upper_weight = self.upper_weight_node.get_value()
|
||||||
|
lower_weight = self.lower_weight_node.get_value()
|
||||||
|
|
||||||
|
# 数据有效性校验(过滤异常值)
|
||||||
|
upper_weight = round(float(upper_weight), 2) if self._is_valid_weight(upper_weight) else "error"
|
||||||
|
lower_weight = round(float(lower_weight), 2) if self._is_valid_weight(lower_weight) else "error"
|
||||||
|
|
||||||
|
# 构建重量数据字典(与原 TCP 客户端格式一致)
|
||||||
|
weight_data = {
|
||||||
|
"hopper_up_weight": upper_weight,
|
||||||
|
"hopper_down_weight": lower_weight
|
||||||
|
}
|
||||||
|
self.latest_weight_data = weight_data
|
||||||
|
|
||||||
|
# 错误状态判断(完全复用原 TCP 客户端逻辑)
|
||||||
|
if upper_weight == "error" and lower_weight == "error":
|
||||||
|
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": ""})
|
||||||
|
self.upper_weight_error.emit(True)
|
||||||
|
self.down_weight_error.emit(True)
|
||||||
|
elif upper_weight == "error":
|
||||||
|
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": lower_weight})
|
||||||
|
self.upper_weight_error.emit(True)
|
||||||
|
self.down_weight_error.emit(False)
|
||||||
|
elif lower_weight == "error":
|
||||||
|
self.weight_updated.emit({"hopper_up_weight": upper_weight, "hopper_down_weight": ""})
|
||||||
|
self.upper_weight_error.emit(False)
|
||||||
|
self.down_weight_error.emit(True)
|
||||||
|
else:
|
||||||
|
self.weight_updated.emit(weight_data)
|
||||||
|
self.upper_weight_error.emit(False)
|
||||||
|
self.down_weight_error.emit(False)
|
||||||
|
|
||||||
|
print(f"OPC UA 数据读取成功:上料斗={upper_weight} kg, 下料斗={lower_weight} kg")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OPC UA 数据读取失败:{e}")
|
||||||
|
# 读取失败时标记为异常
|
||||||
|
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": ""})
|
||||||
|
self.upper_weight_error.emit(True)
|
||||||
|
self.down_weight_error.emit(True)
|
||||||
|
# 读取失败可能是连接异常,触发重连
|
||||||
|
self._disconnect_from_server()
|
||||||
|
if not self.reconnect_timer.isActive():
|
||||||
|
self.reconnect_timer.start()
|
||||||
|
|
||||||
|
def _is_valid_weight(self, weight_value):
|
||||||
|
"""验证重量值是否有效"""
|
||||||
|
try:
|
||||||
|
weight = float(weight_value)
|
||||||
|
# 结合业务场景:重量值应大于0(模拟传感器有效范围)
|
||||||
|
return weight >= 0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# 外部接口函数(与原 TCP 客户端一致)
|
||||||
|
# ---------------------
|
||||||
|
def stop(self):
|
||||||
|
"""停止 OPC UA 客户端(断开连接、停止定时器)"""
|
||||||
|
self.reconnect_timer.stop()
|
||||||
|
self.data_read_timer.stop()
|
||||||
|
self._disconnect_from_server()
|
||||||
|
self.is_running = False
|
||||||
|
print("OPC UA 客户端已停止")
|
||||||
|
|
||||||
|
def get_current_time(self):
|
||||||
|
"""获取格式化的当前时间(保持原接口)"""
|
||||||
|
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
6
readme.md
Normal file
6
readme.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# 功能
|
||||||
|
使用opc_tcp通讯,读取服务端的上料斗和下料斗重量的数值,并且显示在界面上。当服务端断开,状态图标会切换,重量值清零,会一直重连服务端。
|
||||||
|
当服务端那边连接不上重量那边的客户端,上料斗或下料斗的值会变为"error",我这边客户端接收到"error"值时,会状态图标会切换,重量值清零,直到接收到正常值
|
||||||
|
# 使用教程
|
||||||
|
使用前需要修改的参数,opcua_client.py文件中,需要修改的地方有:
|
||||||
|

|
||||||
203
tcp_client.py
203
tcp_client.py
@ -1,203 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
'''
|
|
||||||
# @Time : 2025/11/13 11:05
|
|
||||||
# @Author : reenrr
|
|
||||||
# @File : tcp_client.py
|
|
||||||
'''
|
|
||||||
import json
|
|
||||||
from PySide6.QtCore import QTimer, Slot, Signal, QObject
|
|
||||||
from PySide6.QtNetwork import QTcpSocket, QAbstractSocket
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# -----------
|
|
||||||
# 参数配置
|
|
||||||
# -----------
|
|
||||||
tcp_server_host = "127.0.0.1"
|
|
||||||
tcp_server_port = 8888
|
|
||||||
|
|
||||||
RECONNECT_INTERVAL = 2000 # 重连间隔(毫秒)
|
|
||||||
|
|
||||||
class TcpClient(QObject):
|
|
||||||
# 信号,用于向主窗口/重量弹窗发送最新重量数据
|
|
||||||
weight_updated = Signal(dict) # 重量更新信号
|
|
||||||
connection_status_changed = Signal(bool) # 连接状态更新信号
|
|
||||||
upper_weight_error = Signal(bool) # 上料斗重量异常信号
|
|
||||||
down_weight_error = Signal(bool) # 下料斗重量异常信号
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.is_running = False # 系统运行状态标记
|
|
||||||
self.latest_json_data = {} # 缓存服务端发送的最新JSON数据
|
|
||||||
|
|
||||||
self.statusWidgets = [] # 存储api_field + 对应的value标签
|
|
||||||
|
|
||||||
# ---------------
|
|
||||||
# TCP客户端核心配置
|
|
||||||
# ---------------
|
|
||||||
self.tcp_socket = QTcpSocket(self) # TCP socket实例
|
|
||||||
self.tcp_server_host = tcp_server_host
|
|
||||||
self.tcp_server_port = tcp_server_port
|
|
||||||
self.is_tcp_connected = False # TCP连接状态标记
|
|
||||||
self.has_connected_once = False # 连接至服务器至少一次标记(区别首次连接和断开后重连)
|
|
||||||
self.reconnect_count = 0 # 重连次数计数器
|
|
||||||
|
|
||||||
# 重连定时器,每隔RECONNECT_INTERVAL毫秒重连一次
|
|
||||||
self.reconnect_timer = QTimer(self)
|
|
||||||
self.reconnect_timer.setInterval(RECONNECT_INTERVAL)
|
|
||||||
self.reconnect_timer.timeout.connect(self._reconnect_to_server) # 绑定重连函数
|
|
||||||
|
|
||||||
# 绑定TCP信号与槽(事件驱动)
|
|
||||||
self._bind_tcp_signals()
|
|
||||||
|
|
||||||
# ---------------------
|
|
||||||
# TCP连接相关函数
|
|
||||||
# ---------------------
|
|
||||||
def _connect_to_server(self):
|
|
||||||
"""主动发起连接(仅在未连接状态下有效"""
|
|
||||||
if not self.is_tcp_connected:
|
|
||||||
self.tcp_socket.abort() # 终止现有连接
|
|
||||||
self.tcp_socket.connectToHost(self.tcp_server_host, self.tcp_server_port)
|
|
||||||
print(f"发起连接请求:{self.tcp_server_host}:{self.tcp_server_port}")
|
|
||||||
|
|
||||||
def _reconnect_to_server(self):
|
|
||||||
"""重连执行函数:仅在未连接且未达最大次数时触发"""
|
|
||||||
if not self.is_tcp_connected:
|
|
||||||
self.reconnect_count += 1
|
|
||||||
# 日志优化:每10次重连提示一次,避免日志刷屏
|
|
||||||
if self.reconnect_count % 10 == 0:
|
|
||||||
print(f"已连续重连{self.reconnect_count}次,仍未连接服务端,请检查服务端状态...")
|
|
||||||
else:
|
|
||||||
print(f"第{self.reconnect_count}次重连请求:{self.tcp_server_host}:{self.tcp_server_port}")
|
|
||||||
self._connect_to_server()
|
|
||||||
|
|
||||||
def _bind_tcp_signals(self):
|
|
||||||
"""绑定TCP socket的核心信号(连接、断开、接收数据、错误)"""
|
|
||||||
# 连接成功信号
|
|
||||||
self.tcp_socket.connected.connect(self._on_tcp_connected)
|
|
||||||
# 断开连接信号
|
|
||||||
self.tcp_socket.disconnected.connect(self._on_tcp_disconnected)
|
|
||||||
# 接收数据信号(有新数据时触发)
|
|
||||||
self.tcp_socket.readyRead.connect(self._on_tcp_data_received)
|
|
||||||
# 错误信号(连接/通信出错时触发)
|
|
||||||
self.tcp_socket.errorOccurred.connect(self._on_tcp_error)
|
|
||||||
|
|
||||||
# ------------------
|
|
||||||
# TCP客户端核心功能
|
|
||||||
# ------------------
|
|
||||||
@Slot()
|
|
||||||
def _on_tcp_connected(self):
|
|
||||||
"""TCP连接成功回调"""
|
|
||||||
self.is_tcp_connected = True
|
|
||||||
self.has_connected_once = True
|
|
||||||
self.reconnect_timer.stop() # 停止重连定时器
|
|
||||||
self.reconnect_count = 0 # 重连计数器清零
|
|
||||||
self.is_running = True
|
|
||||||
print(f"TCP连接成功:{self.tcp_server_host}:{self.tcp_server_port}")
|
|
||||||
|
|
||||||
# 发送连接状态信号
|
|
||||||
self.connection_status_changed.emit(True)
|
|
||||||
# 重量错误状态信号(连接成功后恢复正常图标)
|
|
||||||
self.upper_weight_error.emit(False)
|
|
||||||
self.down_weight_error.emit(False)
|
|
||||||
# 连接成功后,向服务器发送“请求初始数据”指令
|
|
||||||
self._send_tcp_request("get_initial_data")
|
|
||||||
|
|
||||||
@Slot()
|
|
||||||
def _on_tcp_disconnected(self):
|
|
||||||
"""TCP连接断开回调"""
|
|
||||||
self.is_tcp_connected = False
|
|
||||||
self.is_running = False
|
|
||||||
print(f"TCP连接断开:{self.tcp_server_host}:{self.tcp_server_port}")
|
|
||||||
|
|
||||||
# 发送连接状态信号
|
|
||||||
self.connection_status_changed.emit(False)
|
|
||||||
|
|
||||||
# 发送重量清零信号
|
|
||||||
self.weight_updated.emit({"hopper_up_weight": 0, "hopper_down_weight": ""})
|
|
||||||
|
|
||||||
if not self.reconnect_timer.isActive(): # 未启动重连定时器时才启动
|
|
||||||
print(f"连接断开,将在{RECONNECT_INTERVAL / 1000}秒后启动重连...")
|
|
||||||
self.reconnect_timer.start()
|
|
||||||
|
|
||||||
@Slot()
|
|
||||||
def _on_tcp_data_received(self):
|
|
||||||
"""TCP数据接收回调(服务器发送数据时触发)"""
|
|
||||||
tcp_data = self.tcp_socket.readAll().data().decode("utf-8").strip()
|
|
||||||
print(f"TCP数据接收:{tcp_data}")
|
|
||||||
|
|
||||||
# 解析数据
|
|
||||||
try:
|
|
||||||
status_data = json.loads(tcp_data)
|
|
||||||
self.latest_json_data = status_data
|
|
||||||
|
|
||||||
# 错误检测
|
|
||||||
up_weight = status_data.get("hopper_up_weight")
|
|
||||||
down_weight = status_data.get("hopper_down_weight")
|
|
||||||
|
|
||||||
# 顺序不能变
|
|
||||||
if up_weight == "error" and down_weight == "error":
|
|
||||||
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": ""})
|
|
||||||
self.upper_weight_error.emit(True)
|
|
||||||
self.down_weight_error.emit(True)
|
|
||||||
elif up_weight == "error":
|
|
||||||
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": down_weight})
|
|
||||||
self.upper_weight_error.emit(True)
|
|
||||||
elif down_weight == "error":
|
|
||||||
self.weight_updated.emit({"hopper_up_weight": up_weight, "hopper_down_weight": ""})
|
|
||||||
self.down_weight_error.emit(True)
|
|
||||||
else:
|
|
||||||
# 防止打乱服务端连接时的状态图标变换
|
|
||||||
self.weight_updated.emit(status_data)
|
|
||||||
self.upper_weight_error.emit(False)
|
|
||||||
self.down_weight_error.emit(False)
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f"TCP数据解析失败(非JSON格式):{e}, 原始数据:{tcp_data}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"TCP数据处理异常:{e}")
|
|
||||||
|
|
||||||
@Slot(QAbstractSocket.SocketError)
|
|
||||||
def _on_tcp_error(self, error):
|
|
||||||
"""TCP错误回调"""
|
|
||||||
if not self.is_tcp_connected:
|
|
||||||
error_str = self.tcp_socket.errorString()
|
|
||||||
print(f"TCP错误:{error_str}")
|
|
||||||
self.is_tcp_connected = False
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
# 发送连接断开信号
|
|
||||||
self.connection_status_changed.emit(False)
|
|
||||||
# 错误时重置重量和错误状态
|
|
||||||
self.weight_updated.emit({"hopper_up_weight": "", "hopper_down_weight": ""})
|
|
||||||
self.weight_error_signal.emit(False)
|
|
||||||
|
|
||||||
# 首次连接失败时,启动重连定时器
|
|
||||||
if not self.reconnect_timer.isActive():
|
|
||||||
print(f"将在{RECONNECT_INTERVAL / 1000}秒后启动重连")
|
|
||||||
self.reconnect_timer.start()
|
|
||||||
|
|
||||||
def _send_tcp_request(self, request_cmd="get_status"):
|
|
||||||
"""向TCP服务器发送请求指令"""
|
|
||||||
if not self.is_tcp_connected:
|
|
||||||
print("TCP连接未建立,无法发送请求")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 构造请求数据
|
|
||||||
request_data = json.dumps({
|
|
||||||
"cmd": request_cmd,
|
|
||||||
"timestamp": self.get_current_time(),
|
|
||||||
"client_info": "布料系统客户端"
|
|
||||||
}) + "\n" # 增加换行符作为数据结束标识
|
|
||||||
|
|
||||||
# 发送请求数据
|
|
||||||
self.tcp_socket.write(request_data.encode("utf-8"))
|
|
||||||
print(f"TCP请求发送:{request_data.strip()}")
|
|
||||||
|
|
||||||
# ------------------
|
|
||||||
# 时间相关的通用方法
|
|
||||||
# ------------------
|
|
||||||
def get_current_time(self):
|
|
||||||
"""获取格式化的当前时间"""
|
|
||||||
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
@ -37,9 +37,11 @@ class WeightDetailsDialog(QDialog):
|
|||||||
self.check_label = None # 状态图标标签
|
self.check_label = None # 状态图标标签
|
||||||
|
|
||||||
self.is_running = False # 定时器是否运行
|
self.is_running = False # 定时器是否运行
|
||||||
self.latest_json_data = {}
|
|
||||||
self.statusWidgets = [] # 状态显示控件列表
|
self.statusWidgets = [] # 状态显示控件列表
|
||||||
|
|
||||||
|
# 存储当前重量值(用于处理空值/异常值)
|
||||||
|
self.current_weight = 0.0
|
||||||
|
|
||||||
self._init_ui(title)
|
self._init_ui(title)
|
||||||
|
|
||||||
# -----------------------
|
# -----------------------
|
||||||
@ -189,12 +191,21 @@ class WeightDetailsDialog(QDialog):
|
|||||||
def update_weight(self, weight_data):
|
def update_weight(self, weight_data):
|
||||||
"""根据TCP获取的数据更新重量显示"""
|
"""根据TCP获取的数据更新重量显示"""
|
||||||
# 更新6个数字标签(补零为6位,超过6位取后6位)
|
# 更新6个数字标签(补零为6位,超过6位取后6位)
|
||||||
try:
|
# 处理空值/异常值(客户端传递的 "" 或 "error")
|
||||||
weight_int = int(weight_data)
|
if weight_data == "" or weight_data == "error" or weight_data is None:
|
||||||
weight_str = f"{weight_int:06d}"[-6:]
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
weight_str = "000000"
|
weight_str = "000000"
|
||||||
|
self.current_weight = 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# 支持浮点数(如 648.06 → 保留2位小数后转为整数显示,避免丢失精度)
|
||||||
|
weight_int = int(float(weight_data))
|
||||||
|
self.current_weight = weight_int
|
||||||
|
weight_str = f"{weight_int:06d}"[-6:] # 超过6位取后6位
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
weight_str = "000000"
|
||||||
|
self.current_weight = 0
|
||||||
|
|
||||||
|
# 更新6个数字标签
|
||||||
for i in range(min(len(self.digit_labels), 6)):
|
for i in range(min(len(self.digit_labels), 6)):
|
||||||
self.digit_labels[i].setText(weight_str[i] if i < len(weight_str) else "0")
|
self.digit_labels[i].setText(weight_str[i] if i < len(weight_str) else "0")
|
||||||
|
|
||||||
@ -227,10 +238,15 @@ class WeightDetailsDialog(QDialog):
|
|||||||
# --------------------
|
# --------------------
|
||||||
def _clear_ui_info(self):
|
def _clear_ui_info(self):
|
||||||
"""清空管片ID和网格信息"""
|
"""清空管片ID和网格信息"""
|
||||||
if self.id_value_label:
|
# 修复:避免访问未定义的 self.id_value_label
|
||||||
|
if hasattr(self, 'id_value_label') and self.id_value_label:
|
||||||
self.id_value_label.setText("")
|
self.id_value_label.setText("")
|
||||||
for widget in self.statusWidgets:
|
for widget in self.statusWidgets:
|
||||||
widget['valueLabel'].setText("")
|
if 'valueLabel' in widget and widget['valueLabel']:
|
||||||
|
widget['valueLabel'].setText("")
|
||||||
|
# 清空重量显示
|
||||||
|
for label in self.digit_labels:
|
||||||
|
label.setText("0")
|
||||||
print("ℹ️ 界面信息已清空")
|
print("ℹ️ 界面信息已清空")
|
||||||
|
|
||||||
# 测试代码
|
# 测试代码
|
||||||
|
|||||||
Reference in New Issue
Block a user