变频器集成以及增加点动控制(0209)

This commit is contained in:
2026-02-09 11:36:37 +08:00
parent 88dfc53b9d
commit d6ad01274a
45 changed files with 7161 additions and 1578 deletions

183
opc/debug_nodes.py Normal file
View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
节点访问诊断脚本
帮助诊断OPC UA节点访问问题
"""
import sys
import time
# 添加项目路径
sys.path.insert(0, r"d:\f-work\three_control_system\Feeding1129\opc")
from opcua_client_test import OPCUAClientTest
def diagnose_node_access():
"""诊断节点访问问题"""
print("=" * 60)
print("OPC UA 节点访问诊断")
print("=" * 60)
# 创建客户端
client = OPCUAClientTest()
try:
# 连接到服务器
print("\n[1] 连接到OPC UA服务器...")
if not client.connect():
print("连接失败!")
return
print("连接成功!")
# 获取对象节点
objects = client.client.get_objects_node()
print(f"\n对象节点: {objects}")
print(f"对象节点ID: {objects.nodeid}")
# 浏览所有子节点
print("\n[2] 浏览对象节点下的所有子节点...")
try:
children = objects.get_children()
print(f"子节点数量: {len(children)}")
for i, child in enumerate(children):
try:
browse_name = child.get_browse_name()
node_id = child.nodeid
print(f" {i+1}. {browse_name} -> {node_id}")
except Exception as e:
print(f" {i+1}. (获取名称失败: {e})")
except Exception as e:
print(f"浏览子节点失败: {e}")
# 尝试不同的路径格式
print("\n[3] 尝试不同的路径格式访问节点...")
path_formats = [
# 格式1: 使用命名空间前缀
"2:upper/2:upper_weight",
"2:lower/2:lower_weight",
# 格式2: 原始字符串(避免转义问题)
r"2:upper/2:upper_weight",
r"2:lower/2:lower_weight",
# 格式3: 使用列表格式
["2:upper", "2:upper_weight"],
["2:lower", "2:lower_weight"],
# 格式4: 不带命名空间前缀
"upper/upper_weight",
"lower/lower_weight",
["upper", "upper_weight"],
["lower", "lower_weight"],
]
for path in path_formats:
try:
if isinstance(path, list):
node = objects.get_child(path)
else:
node = objects.get_child(path)
browse_name = node.get_browse_name()
value = node.get_value()
print(f" ✓ 成功: {path}")
print(f" 节点: {node}")
print(f" 名称: {browse_name}")
print(f" 值: {value}")
print()
# 找到一个有效的就继续尝试其他格式
break
except Exception as e:
print(f" ✗ 失败: {path}")
print(f" 错误: {e}")
print()
# 尝试方法1先获取设备对象再获取变量
print("\n[4] 方法1: 先获取设备对象,再获取变量...")
try:
upper_device = objects.get_child("2:upper")
print(f" 上料斗设备: {upper_device}")
upper_weight = upper_device.get_child("2:upper_weight")
value = upper_weight.get_value()
print(f" ✓ 成功获取上料斗重量: {value}")
except Exception as e:
print(f" ✗ 失败: {e}")
# 尝试方法2直接使用完整路径
print("\n[5] 方法2: 直接使用完整路径...")
try:
# 注意:这里使用原始字符串避免转义问题
upper_weight = objects.get_child(r"2:upper/2:upper_weight")
value = upper_weight.get_value()
print(f" ✓ 成功获取上料斗重量: {value}")
except Exception as e:
print(f" ✗ 失败: {e}")
# 尝试方法3使用get_children遍历
print("\n[6] 方法3: 使用get_children遍历查找...")
try:
for child in objects.get_children():
try:
browse_name = str(child.get_browse_name())
if "upper" in browse_name.lower():
print(f" 发现上料斗相关节点: {browse_name} -> {child.nodeid}")
# 尝试获取该节点的子节点
for sub_child in child.get_children():
try:
sub_browse_name = str(sub_child.get_browse_name())
if "weight" in sub_browse_name.lower():
value = sub_child.get_value()
print(f" └─ {sub_browse_name}: {value}")
except Exception as e:
print(f" └─ 获取{sub_browse_name}失败: {e}")
except Exception as e:
print(f" 处理节点失败: {e}")
except Exception as e:
print(f" ✗ 遍历失败: {e}")
# 尝试写入操作
print("\n[7] 尝试写入操作...")
try:
# 找到有效的节点路径
upper_device = objects.get_child("2:upper")
upper_weight = upper_device.get_child("2:upper_weight")
# 写入测试值
test_value = 123.45
upper_weight.set_value(test_value)
print(f" ✓ 成功写入: {test_value}")
# 读取验证
read_value = upper_weight.get_value()
print(f" ✓ 读取验证: {read_value}")
except Exception as e:
print(f" ✗ 写入失败: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"\n诊断过程出错: {e}")
import traceback
traceback.print_exc()
finally:
try:
client.disconnect()
print("\n[8] 已断开连接")
except:
pass
print("\n" + "=" * 60)
print("诊断完成")
print("=" * 60)
if __name__ == "__main__":
diagnose_node_access()

274
opc/opcua_client_feed.py Normal file
View File

@ -0,0 +1,274 @@
#!/usr/bin/env python3
from opcua import Client, ua
import time
from datetime import datetime
import configparser
from threading import Thread
# class OpcuaUiSignal:
# value_changed = Signal(str, str, object)
# opc_disconnected = Signal(str) # OPC服务断开信号参数断开原因
# opc_reconnected = Signal() # OPC重连成功信号
# opc_log = Signal(str) # OPC运行日志信号参数日志信息
# Opcua回调处理器
class SubscriptionHandler:
def __init__(self):
self.node_id_to_name = {}
# self.opc_signal = opc_signal
def datachange_notification(self, node, val, data):
try:
node_id = node.nodeid.to_string()
var_name = self.node_id_to_name.get(node_id)
# self.opc_signal.value_changed.emit(node_id, var_name, val)
except Exception as e:
err_msg = f"opcua解析值变化事件失败: {e}"
# self.opc_signal.opc_log.emit(err_msg)
class OpcuaClientFeed(Thread):
def __init__(self, parent=None):
super().__init__(parent)
self.server_url = ""
self.client = None
self.connected = False
self.subscription = None
self.monitored_items = []
self.is_running = True # 线程运行标志位
self.node_id_mapping = {} # node_id 和 可读变量名的映射表
self.is_reconnect_tip_sent = False # 重连失败提示是否已发送
# self.opc_signal = OpcuaUiSignal()
# self.handler = SubscriptionHandler(self.opc_signal)
self.handler = SubscriptionHandler()
self.target_var_paths = []
# 参数
self.heartbeat_interval = None # 心跳检测间隔
self.reconnect_interval = None # 首次/掉线重连间隔
self.sub_interval = None # 订阅间隔 (单位:ms)
def stop_run(self):
"""停止线程+断开连接"""
self.is_running = False
self.disconnect()
self.wait()
print("opcua客户端线程已退出")
def connect(self):
"""连接到OPC服务器"""
try:
self.client.connect()
self.connected = True
msg = f"成功连接到OPCUA服务器: {self.server_url}"
print(msg)
# self.opc_signal.opc_log.emit(msg)
self.is_reconnect_tip_sent = False
return True
except Exception as e:
self.connected = False
err_msg = f"连接OPCUA服务器失败: {e}"
print(err_msg)
if not self.is_reconnect_tip_sent:
# self.opc_signal.opc_log.emit(err_msg)
# 标记为已发送后续不重复在UI上显示
self.is_reconnect_tip_sent = True
return False
def disconnect(self):
"""断开连接"""
self.connected = False
try:
if self.monitored_items:
for item in self.monitored_items:
try:
self.subscription.unsubscribe(item)
except Exception:
pass
self.monitored_items.clear()
if self.subscription:
try:
self.subscription.delete()
except Exception:
pass
self.subscription = None
if self.client:
try:
self.client.disconnect()
except Exception:
pass
self.node_id_mapping.clear()
if hasattr(self, 'handler') and self.handler:
self.handler.node_id_to_name = {}
except Exception as e:
print(f"opcua断开连接异常: {e}")
def build_node_id_mapping(self):
"""根据object_name+var_name路径获取nodeid,建立映射表"""
if not self.connected:
return False
try:
# self.opc_signal.opc_log.emit("开始构建nodeid映射表...")
objects_node = self.client.get_objects_node()
self.handler.node_id_to_name = self.node_id_mapping
for var_name, path_list in self.target_var_paths:
target_node = objects_node.get_child(path_list)
node_id = target_node.nodeid.to_string()
self.node_id_mapping[node_id] = var_name
# self.opc_signal.opc_log.emit("nodeid映射表构建成功")
return True
except Exception as e:
err_msg = f"构建{var_name}映射表失败: {e}"
print(err_msg)
# self.opc_signal.opc_log.emit(err_msg)
return False
def create_multi_subscription(self, interval=None):
"""订阅多个变量(基于映射表的nodeid)"""
if not self.connected:
return
if not self.node_id_mapping and not self.build_node_id_mapping():
return
try:
interval = int(interval) if interval else self.sub_interval
self.subscription = self.client.create_subscription(interval, self.handler)
# self.opc_signal.opc_log.emit(f"opcua订阅创建成功(间隔:{interval}ms)")
for node_id, var_name in self.node_id_mapping.items():
var_node = self.client.get_node(node_id)
monitored_item = self.subscription.subscribe_data_change(var_node)
self.monitored_items.append(monitored_item)
# self.opc_signal.opc_log.emit(f"已订阅变量: {var_name} (nodeid: {node_id})")
print(f"已订阅变量: {var_name} (nodeid: {node_id})")
except Exception as e:
err_msg = f"创建批量订阅失败: {e}"
print(err_msg)
# self.opc_signal.opc_log.emit(err_msg)
def read_opc_config(self, cfg_path = "config/opc_config.ini"):
"""读取OPC配置文件, 初始化所有参数和节点列表"""
try:
cfg = configparser.ConfigParser()
cfg.read(cfg_path, encoding="utf-8")
# 1. 读取服务器基础配置
self.server_url = cfg.get("OPC_SERVER_CONFIG", "server_url")
self.heartbeat_interval = cfg.getint("OPC_SERVER_CONFIG", "heartbeat_interval")
self.reconnect_interval = cfg.getint("OPC_SERVER_CONFIG", "reconnect_interval")
self.sub_interval = cfg.getint("OPC_SERVER_CONFIG", "sub_interval")
# 2. 读取OPC节点配置
node_section = cfg["OPC_NODE_LIST"]
for readable_name, node_path_str in node_section.items():
node_path_list = node_path_str.split(",")
self.target_var_paths.append( (readable_name, node_path_list) )
# print("target_var_paths", self.target_var_paths)
except Exception as e:
print(f"读取配置文件失败: {e},使用默认配置启动!")
self.server_url = "opc.tcp://localhost:4840/zjsh_feed/server/"
self.heartbeat_interval = 4
self.reconnect_interval = 2
self.sub_interval = 500
self.target_var_paths = [
("upper_weight", ["2:upper", "2:upper_weight"]),
("lower_weight", ["2:lower", "2:lower_weight"])
]
# 参数合法性检验
self.heartbeat_interval = self.heartbeat_interval if isinstance(self.heartbeat_interval, int) and self.heartbeat_interval >=1 else 4
self.reconnect_interval = self.reconnect_interval if isinstance(self.reconnect_interval, int) and self.reconnect_interval >=1 else 2
self.sub_interval = self.sub_interval if isinstance(self.sub_interval, int) and self.sub_interval >=100 else 500
def write_value_by_name(self, var_readable_name, value):
"""
根据变量可读名称写入值(主要用于修改方量, 方量的类型为 Double类型)
:param var_readable_name: 变量可读名称(如"upper_weight")
:param value: 要写入的值
"""
if not self.connected:
# self.opc_signal.opc_log.emit(f"{var_readable_name}写入失败: OPC服务未连接")
return
target_node_id = None
for node_id, name in self.node_id_mapping.items():
if name == var_readable_name:
target_node_id = node_id
break
if not target_node_id:
# self.opc_signal.opc_log.emit(f"写入失败:未找到变量名 {var_readable_name} 对应的nodeid")
return
try:
target_node = self.client.get_node(target_node_id)
# variant = ua.Variant(float(value), ua.VariantType.Double)
target_node.set_value(value)
# self.opc_signal.opc_log.emit(f"写入成功:{var_readable_name} = {value}")
except Exception as e:
err_msg = f"opcua写入值失败: {e}"
print(err_msg)
# self.opc_signal.opc_log.emit(err_msg)
# ===== 心跳检测函数 =====
def _heartbeat_check(self):
"""心跳检测: 判断opc服务是否存活"""
try:
self.client.get_node("i=2258").get_value()
return True
except Exception as e:
err_msg = f"心跳检测失败, OPCUA服务已断开 {e}"
print(err_msg)
# self.opc_signal.opc_log.emit(err_msg)
return False
# ===== 掉线重连函数 =====
def _auto_reconnect(self):
"""掉线后自动重连+重建映射+恢复订阅"""
# self.opc_signal.opc_disconnected.emit("OPC服务掉线, 开始自动重连...")
try:
self.disconnect()
except Exception as e:
print(f"_auto_reconnect: 断开旧连接时出现异常: {e}")
while self.is_running:
# self.opc_signal.opc_log.emit(f"重试连接OPC服务器: {self.server_url}")
if self.connect():
self.build_node_id_mapping()
self.create_multi_subscription()
# self.opc_signal.opc_reconnected.emit()
# self.opc_signal.opc_log.emit("OPCUA服务器重连成功, 所有订阅已恢复正常")
print("OPCUA服务器重连成功, 所有订阅已恢复正常")
break
time.sleep(self.reconnect_interval)
def _init_connect_with_retry(self):
"""连接opc服务器"""
# self.opc_signal.opc_log.emit("OPC客户端初始化, 开始连接服务器...")
print("OPC客户端初始化, 开始连接服务器...")
while self.is_running:
if self.connect():
self.build_node_id_mapping()
self.create_multi_subscription()
break
# self.opc_signal.opc_log.emit(f"连接OPCUA服务器失败, {self.reconnect_interval}秒后重试...")
time.sleep(self.reconnect_interval)
def run(self) -> None:
"""opcua客户端线程主函数"""
self.read_opc_config() # 读取配置文件
self.client = Client(self.server_url) # 初始化opc客户端
# 连接opc服务器
self._init_connect_with_retry()
while self.is_running:
if self.connected:
if not self._heartbeat_check():
self.connected = False
self._auto_reconnect()
else:
self._auto_reconnect()
time.sleep(self.heartbeat_interval)
if __name__ == "__main__":
opcua_client = OpcuaClientFeed()
opcua_client.run()
opcua_client.write_value_by_name("upper_weight", 100.0)

View File

@ -17,24 +17,36 @@ class SubHandler:
def __init__(self):
self.data_changes = {}
self.change_count = 0
# 缓存节点名称,避免在回调中频繁查询
self.node_names = {}
def datachange_notification(self, node, val, data):
"""
数据变化时的回调函数
注意:此函数在订阅线程中调用,必须快速返回,避免耗时操作
"""
self.change_count += 1
node_name = node.get_display_name().Text
# 从缓存获取节点名称,避免发起网络请求
node_id = str(node)
if node_id in self.node_names:
node_name = self.node_names[node_id]
else:
# 如果缓存中没有尝试从节点ID中提取名称备用方案
node_name = node_id
# 存储数据变化
self.data_changes[node_name] = {
'value': val,
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
'node_id': str(node)
'node_id': node_id
}
print(f"🔔 数据变化 #{self.change_count}")
print(f" 节点: {node_name}")
print(f" 数值: {val}")
print(f" 时间: {self.data_changes[node_name]['timestamp']}")
print(f" 节点ID: {node}")
print("-" * 50)
class OPCUAClientSubscription:
@ -95,6 +107,23 @@ class OPCUAClientSubscription:
upper_weight_node = upper_device.get_child("2:upper_weight")
lower_weight_node = lower_device.get_child("2:lower_weight")
# 【关键优化】在订阅前预获取并缓存节点名称,避免回调中发起网络请求
try:
upper_name = upper_weight_node.get_display_name().Text
except Exception:
upper_name = "upper_weight"
try:
lower_name = lower_weight_node.get_display_name().Text
except Exception:
lower_name = "lower_weight"
# 缓存节点名称
self.handler.node_names[str(upper_weight_node)] = upper_name
self.handler.node_names[str(lower_weight_node)] = lower_name
print(f"📋 已缓存节点名称: {upper_name}, {lower_name}")
# 开始监控
upper_handle = self.subscription.subscribe_data_change(upper_weight_node)
lower_handle = self.subscription.subscribe_data_change(lower_weight_node)
@ -109,6 +138,8 @@ class OPCUAClientSubscription:
except Exception as e:
print(f"❌ 设置订阅失败: {e}")
import traceback
traceback.print_exc()
return False
def get_current_values(self):
@ -149,7 +180,7 @@ class OPCUAClientSubscription:
# 每5秒显示一次统计信息
if current_time - last_stats_time >= 5:
elapsed = current_time - start_time
changes_per_minute = (self.handler.change_count / elapsed) * 60
changes_per_minute = (self.handler.change_count / elapsed) * 60 if elapsed > 0 else 0
print(f"\n📈 统计信息 (运行时间: {elapsed:.1f}s)")
print(f" 总变化次数: {self.handler.change_count}")
@ -186,6 +217,8 @@ def main():
except Exception as e:
print(f"❌ 客户端运行错误: {e}")
import traceback
traceback.print_exc()
finally:
client.disconnect()
@ -198,6 +231,6 @@ if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n👋 用户中断程序")
sys.exit(0)
except Exception as e:
print(f"❌ 客户端运行错误: {e}")
sys.exit(1)

View File

@ -52,20 +52,235 @@ class OPCUAClientTest:
objects = self.client.get_objects_node()
print(f"对象节点: {objects}")
upper_device = objects.get_child("2:upper")
print(f"\n上料斗对象: {upper_device}")
lower_device = objects.get_child("2:lower")
print(f"下料斗对象: {lower_device}")
print("\n=== 当前对象属性===")
self.read_object_properties(upper_device, lower_device)
# 浏览Objects下的所有子节点
print("\n=== Objects 节点下的子节点 ===")
for child in objects.get_children():
browse_name = child.get_browse_name()
print(f" 节点: {browse_name} (nodeId: {child.nodeid})")
# 如果是上料斗或下料斗对象,继续浏览它们的子节点
if "upper" in str(browse_name).lower() or "lower" in str(browse_name).lower():
try:
for sub_child in child.get_children():
sub_browse_name = sub_child.get_browse_name()
print(f" └─ {sub_browse_name} (nodeId: {sub_child.nodeid})")
except:
pass
# 尝试获取设备对象
print("\n=== 尝试获取设备对象 ===")
try:
upper_device = objects.get_child("2:upper")
print(f"上料斗对象: {upper_device}")
except Exception as e:
print(f"获取上料斗对象失败: {e}")
# 尝试其他可能的路径
try:
upper_device = objects.get_child(["2:upper"])
print(f"上料斗对象(列表方式): {upper_device}")
except Exception as e2:
print(f" 也无法通过列表方式获取: {e2}")
try:
lower_device = objects.get_child("2:lower")
print(f"下料斗对象: {lower_device}")
except Exception as e:
print(f"获取下料斗对象失败: {e}")
except Exception as e:
print(f"浏览节点时出错: {e}")
def get_node_path(self, obj_path: str) -> str:
"""
获取节点路径 - 尝试多种格式
Args:
obj_path: 对象名称(如 "upper", "upper_weight"
Returns:
str: 节点路径,如果找不到返回 None
"""
if not self.connected:
return None
try:
objects = self.client.get_objects_node()
# 尝试多种节点路径格式
path_formats = [
f"2:{obj_path}",
f"2:upper/2:{obj_path}",
f"2:lower/2:{obj_path}",
f"ns=2;{obj_path}",
obj_path
]
for path in path_formats:
try:
node = objects.get_child(path)
print(f" 找到节点: {path} -> {node}")
return path
except:
continue
return None
except Exception as e:
print(f"查找节点路径时出错: {e}")
return None
def write_data(self, node_path: str, value, data_type: str = "auto") -> bool:
"""
向OPC UA节点写入数据
Args:
node_path: 节点路径(如 "2:upper/2:upper_weight"
value: 要写入的值
data_type: 数据类型("int", "float", "bool", "string", "auto"
Returns:
bool: 写入成功返回True失败返回False
"""
if not self.connected:
print("请先连接到服务器")
return False
try:
# 获取对象节点
objects = self.client.get_objects_node()
# 尝试多种方式获取节点
node = None
error_msg = None
# 方式1: 直接使用路径
try:
node = objects.get_child(node_path)
except Exception as e:
error_msg = e
# 方式2: 分解路径
if node is None and "/" in node_path:
try:
parts = node_path.split("/")
node = objects
for part in parts:
node = node.get_child(part)
except:
pass
# 方式3: 尝试用数字索引
if node is None:
try:
node = objects.get_child([node_path])
except:
pass
if node is None:
print(f"写入数据失败 {node_path}: 找不到节点")
print(f" 详细错误: {error_msg}")
print(f" 提示: 请先运行 browse_nodes() 方法查看可用的节点路径")
return False
# 根据数据类型转换值
if data_type == "int":
value = int(value)
elif data_type == "float":
value = float(value)
elif data_type == "bool":
value = bool(value)
elif data_type == "string":
value = str(value)
# "auto" 模式下自动推断类型
# 写入数据
node.set_value(value)
# 获取节点名称用于显示
try:
node_name = node.get_browse_name()
except:
node_name = node_path
print(f"✓ 成功写入 {node_name}: {value}")
return True
except Exception as e:
print(f"✗ 写入数据失败 {node_path}: {e}")
return False
def write_weights_directly(self, upper_value, lower_value) -> bool:
"""
直接写入上下料斗重量(自动检测节点路径)
Args:
upper_value: 上料斗重量值
lower_value: 下料斗重量值
Returns:
bool: 写入成功返回True失败返回False
"""
if not self.connected:
print("请先连接到服务器")
return False
success = True
try:
objects = self.client.get_objects_node()
# 查找上料斗重量节点
upper_weight_node = None
lower_weight_node = None
# 遍历Objects下的所有节点
for child in objects.get_children():
browse_name = str(child.get_browse_name())
print(browse_name)
if "upper" in browse_name.lower():
upper_weight_node = child
print(f"找到上料斗重量节点: {browse_name}")
break
for child in objects.get_children():
browse_name = str(child.get_browse_name())
if "lower" in browse_name.lower():
lower_weight_node = child
print(f"找到下料斗重量节点: {browse_name}")
break
# 写入上料斗重量
if upper_weight_node:
try:
upper_weight_node.set_value(upper_value)
print(f"✓ 成功写入上料斗重量: {upper_value}")
except Exception as e:
print(f"✗ 写入上料斗重量失败: {e}")
success = False
else:
print("✗ 未找到上料斗重量节点")
success = False
# 写入下料斗重量
if lower_weight_node:
try:
lower_weight_node.set_value(lower_value)
print(f"✓ 成功写入下料斗重量: {lower_value}")
except Exception as e:
print(f"✗ 写入下料斗重量失败: {e}")
success = False
else:
print("✗ 未找到下料斗重量节点")
success = False
return success
except Exception as e:
print(f"写入重量数据时出错: {e}")
return False
def read_object_properties(self, upper_device, lower_device):
"""读取重量数值"""
"""读取重量数值需要外部传入device对象"""
try:
# 读取重量
upper_weight = upper_device.get_child("2:upper_weight").get_value()
@ -76,7 +291,79 @@ class OPCUAClientTest:
except Exception as e:
print(f"读取数据时出错: {e}")
def read_weights(self) -> tuple:
"""
直接读取上料斗和下料斗重量无需先获取device对象
Returns:
tuple: (上料斗重量, 下料斗重量),读取失败返回 (None, None)
"""
if not self.connected:
print("请先连接到服务器")
return None, None
try:
# 直接获取节点并读取数据
objects = self.client.get_objects_node()
# 使用列表格式访问节点freeopcua推荐的方式
upper_weight = objects.get_child(["2:upper", "2:upper_weight"]).get_value()
lower_weight = objects.get_child(["2:lower", "2:lower_weight"]).get_value()
print(f"上料斗重量: {upper_weight}")
print(f"下料斗重量: {lower_weight}")
return upper_weight, lower_weight
except Exception as e:
print(f"读取重量数据时出错: {e}")
return None, None
def write_multiple_values(self, values_dict: dict) -> dict:
"""
批量写入多个节点
Args:
values_dict: 字典key为节点路径value为要写入的值
Returns:
dict: 写入结果key为节点路径value为成功/失败状态
"""
results = {}
for node_path, value in values_dict.items():
results[node_path] = self.write_data(node_path, value)
return results
def write_test_data(self):
"""测试写入各种类型的数据"""
if not self.connected:
print("请先连接到服务器")
return
print("\n=== 测试写入数据 ===")
# 测试写入目标重量
self.write_data("2:upper/2:target_weight", 150.5, "float")
# 测试写入开关量
self.write_data("2:upper/2:valve_on", True, "bool")
# 测试写入整数
self.write_data("2:upper/2:cycle_count", 10, "int")
# 测试批量写入
values = {
"2:upper/2:target_weight": 200.0,
"2:lower/2:target_weight": 100.0,
}
results = self.write_multiple_values(values)
print("\n批量写入结果:")
for path, success in results.items():
status = "✓ 成功" if success else "✗ 失败"
print(f" {path}: {status}")
def monitor_data(self, duration=30):
"""监控数据变化"""
if not self.connected:
@ -97,7 +384,7 @@ class OPCUAClientTest:
start_time = time.time()
while time.time() - start_time < duration:
print(f"\n--- {time.strftime('%H:%M:%S')} ---")
self.read_sensor_values(upper_device, lower_device)
self.read_object_properties(upper_device, lower_device)
time.sleep(5) # 每5秒读取一次
except KeyboardInterrupt:
@ -117,21 +404,61 @@ def main():
if not client.connect():
return
# 浏览节点结构
# 浏览节点结构(首先发现实际节点结构)
print("\n" + "="*60)
print("步骤1: 浏览服务器节点结构")
print("="*60)
client.browse_nodes()
# 监控数据变化
client.monitor_data(duration=30)
# 尝试使用新方法写入数据
print("\n" + "="*60)
print("步骤2: 使用动态节点查找方法写入数据")
print("="*60)
# 测试写入数据
# client.write_test_data()
# 方法1: 使用write_weights_directly自动查找节点
print("\n尝试方法1: write_weights_directly (自动查找节点)")
# client.write_data("2:upper/2:upper_weight", 180, "int")
# client.write_data("2:lower/2:lower_weight", 120, "int")
values = {
"2:upper/2:upper_weight": 200,
"2:lower/2:lower_weight": 100,
}
client.write_multiple_values(values)
# success1 = client.write_weights_directly(150, 120)
time.sleep(2)
# 继续监控
print("\n继续监控数据...")
client.monitor_data(duration=15)
# if not success1:
# # 方法2: 尝试可能的替代路径
# print("\n尝试方法2: 尝试其他节点路径格式")
# # 列出可能的节点路径格式
# possible_paths = [
# "2:upper_weight",
# "2:lower_weight",
# "2:upper/upper_weight",
# "2:lower/lower_weight",
# "ns=2;upper_weight",
# "ns=2;lower_weight"
# ]
# for path in possible_paths:
# print(f" 尝试写入: {path}")
# client.write_data(path, 150.5, "float")
# time.sleep(0.5)
print("\n" + "="*60)
print("步骤3: 读取验证数据")
print("="*60)
upper, lower = client.read_weights()
print(f"读取结果 - 上料斗: {upper}, 下料斗: {lower}")
except KeyboardInterrupt:
print("\n客户端被用户中断")
except Exception as e:
print(f"客户端运行错误: {e}")
import traceback
traceback.print_exc()
finally:
# 断开连接
client.disconnect()

237
opc/opcua_server_test.py Normal file
View File

@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""
简单的OPC UA服务器示例
用于工业自动化数据通信
"""
from opcua import Server, ua
import time
import random
import threading
from datetime import datetime
# from core.system import SystemState
# from config.ini_manager import ini_manager
class SimpleOPCUAServer:
# def __init__(self, state, endpoint=ini_manager.opcua_endpoint, name="Feed_Server"):
def __init__(self, endpoint='', name="Feed_Server"):
"""
初始化OPC UA服务器
Args:
endpoint: 服务器端点地址
name: 服务器名称
"""
self.server = Server()
self.server.set_endpoint(endpoint)
self.server.set_server_name(name)
# self.state = state
# 设置服务器命名空间
self.namespace = self.server.register_namespace("Feed_Control_System")
# 获取对象节点
self.objects = self.server.get_objects_node()
# 创建自定义对象
self.create_object_structure()
# 运行标志
self.running = False
# 订阅和监控项
self.subscription = None
self.monitored_items = []
# 记录上次值用于检测变化
self._last_values = {}
def create_object_structure(self):
"""创建OPC UA对象结构"""
# 创建上料斗对象
self.upper = self.objects.add_object(self.namespace, "upper")
self.lower=self.objects.add_object(self.namespace, "lower")
self.sys=self.objects.add_object(self.namespace, "sys")
self.mould=self.objects.add_object(self.namespace, "mould")
self.pd=self.objects.add_object(self.namespace, "pd")
# 创建变量
self.create_variables()
def create_variables(self):
"""创建OPC UA变量"""
# 创建变量时显式指定数据类型和初始值
#上料斗
self.upper_weight = self.upper.add_variable(self.namespace, "upper_weight", ua.Variant(0.0, ua.VariantType.Float))
self.upper_is_arch = self.upper.add_variable(self.namespace, "upper_is_arch", ua.Variant(False, ua.VariantType.Boolean))
self.upper_door_closed = self.upper.add_variable(self.namespace, "upper_door_closed", ua.Variant(False, ua.VariantType.Boolean))
self.upper_volume = self.upper.add_variable(self.namespace, "upper_volume", ua.Variant(0.0, ua.VariantType.Float))
self.upper_door_position = self.upper.add_variable(self.namespace, "upper_door_position", ua.Variant(0, ua.VariantType.Int16))
#下料斗
self.lower_weight = self.lower.add_variable(self.namespace, "lower_weight", ua.Variant(0.0, ua.VariantType.Float))
self.lower_is_arch = self.lower.add_variable(self.namespace, "lower_is_arch", ua.Variant(False, ua.VariantType.Boolean))
#模具车
self.mould_finish_weight = self.mould.add_variable(self.namespace, "mould_finish_weight", ua.Variant(0.0, ua.VariantType.Float))
self.mould_need_weight = self.mould.add_variable(self.namespace, "mould_need_weight", ua.Variant(0.0, ua.VariantType.Float))
self.mould_frequency = self.mould.add_variable(self.namespace, "mould_frequency", ua.Variant(230, ua.VariantType.Int32))
self.mould_vibrate_status = self.mould.add_variable(self.namespace, "mould_vibrate_status", ua.Variant(False, ua.VariantType.Boolean))
self.feed_status = self.mould.add_variable(self.namespace, "feed_status", ua.Variant(0, ua.VariantType.Int16))
self.pd_data=self.pd.add_variable(self.namespace, "pd_data", ua.Variant("", ua.VariantType.String))
# 在创建变量后立即设置可写权限(不需要等待服务器启动)
self.upper_weight.set_writable(True)
self.lower_weight.set_writable(True)
self.upper_is_arch.set_writable(True)
self.upper_door_closed.set_writable(True)
self.upper_volume.set_writable(True)
self.upper_door_position.set_writable(True)
self.lower_is_arch.set_writable(True)
self.mould_finish_weight.set_writable(True)
self.mould_need_weight.set_writable(True)
self.mould_frequency.set_writable(True)
self.mould_vibrate_status.set_writable(True)
self.feed_status.set_writable(True)
self.pd_data.set_writable(True)
print("[变量创建] 变量创建完成AccessLevel权限已设置")
# 验证并打印当前的AccessLevel属性
# try:
# al = self.upper_weight.get_attribute(ua.AttributeIds.AccessLevel)
# ual = self.upper_weight.get_attribute(ua.AttributeIds.UserAccessLevel)
# print(f"[变量创建] upper_weight AccessLevel: {al.Value.Value}, UserAccessLevel: {ual.Value.Value}")
# al2 = self.lower_weight.get_attribute(ua.AttributeIds.AccessLevel)
# ual2 = self.lower_weight.get_attribute(ua.AttributeIds.UserAccessLevel)
# print(f"[变量创建] lower_weight AccessLevel: {al2.Value.Value}, UserAccessLevel: {ual2.Value.Value}")
# except Exception as e:
# print(f"[变量创建] 获取权限属性失败: {e}")
def setup_variable_permissions(self):
"""设置变量权限 - 在服务器启动后调用"""
try:
# 重新设置变量为可写,确保权限生效
self.upper_weight.set_writable(True)
self.lower_weight.set_writable(True)
print("[权限设置] 变量权限已重新设置")
# 验证权限
try:
al = self.upper_weight.get_attribute(ua.AttributeIds.AccessLevel)
ual = self.upper_weight.get_attribute(ua.AttributeIds.UserAccessLevel)
print(f"[权限设置] upper_weight AccessLevel: {al.Value.Value}, UserAccessLevel: {ual.Value.Value}")
except Exception as e:
print(f"[权限设置] 验证失败: {e}")
except Exception as e:
print(f"[权限设置] 设置权限失败: {e}")
print("[权限设置] 尝试强制设置...")
# def setup_state_listeners(self):
# """设置状态监听器 - 事件驱动更新"""
# if hasattr(self.state, 'state_updated'):
# self.state.state_updated.connect(self.on_state_changed)
# print("状态监听器已设置 - 事件驱动模式")
# def on_state_changed(self, property_name, value):
# """状态变化时的回调函数"""
# try:
# # 根据属性名更新对应的OPC UA变量
# if property_name == "upper_weight":
# self.upper_weight.set_value(value)
# elif property_name == "lower_weight":
# self.lower_weight.set_value(value)
# # 可以在这里添加更多状态映射
# print(f"状态更新: {property_name} = {value}")
# except Exception as e:
# print(f"状态更新错误: {e}")
def start(self):
"""启动服务器"""
try:
self.server.start()
self.running = True
print(f"服务器端点: opc.tcp://0.0.0.0:4840/freeopcua/server/")
print("=" * 60)
# 【关键修复】在设置监听器之前,先设置变量权限
# 这确保 AccessLevel 属性在客户端写入前已正确设置
# self.setup_variable_permissions()
print("=" * 60)
# 设置客户端写入监听器
# self.setup_write_listeners()
print("=" * 60)
# 初始化当前值
# if self.state:
# self.upper_weight.set_value(self.state._upper_weight)
# self.lower_weight.set_value(self.state._lower_weight)
# print("已同步初始状态值")
# 设置状态监听器 - 关键步骤!
# self.setup_state_listeners()
# # 只有在没有状态系统时才使用模拟线程
# if not self.state:
# print("使用模拟数据模式")
# self.simulation_thread = threading.Thread(target=self.simulate_data)
# self.simulation_thread.daemon = True
# self.simulation_thread.start()
except Exception as e:
print(f"启动服务器失败: {e}")
def stop(self):
"""停止服务器"""
# 移除监听器
# self.remove_write_listeners()
self.running = False
self.server.stop()
print("OPC UA服务器已停止")
# 断开状态监听器
# if hasattr(self.state, 'state_updated'):
# try:
# self.state.state_updated.disconnect(self.on_state_changed)
# except:
# pass
def main():
"""主函数"""
# 创建系统状态实例
# state = SystemState()
# 创建并启动服务器
server = SimpleOPCUAServer(
endpoint="opc.tcp://0.0.0.0:4840/freeopcua/server/",
name="工业自动化 OPC UA 服务器"
)
try:
server.start()
print("服务器正在运行,按 Ctrl+C 停止...")
# 保持服务器运行
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n正在停止服务器...")
server.stop()
except Exception as e:
print(f"服务器运行错误: {e}")
server.stop()
if __name__ == "__main__":
main()