mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-04 13:25:13 +00:00
feat(opcua): 增强节点ID解析兼容性和数据类型处理
改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符 添加数据类型转换处理,确保写入值时类型匹配 优化错误提示信息,便于调试节点连接问题
This commit is contained in:
@@ -125,6 +125,9 @@ class BaseClient(UniversalDriver):
|
|||||||
# 初始化名称映射字典
|
# 初始化名称映射字典
|
||||||
self._name_mapping = {}
|
self._name_mapping = {}
|
||||||
self._reverse_mapping = {}
|
self._reverse_mapping = {}
|
||||||
|
# 初始化线程锁(在子类中会被重新创建,这里提供默认实现)
|
||||||
|
import threading
|
||||||
|
self._client_lock = threading.RLock()
|
||||||
|
|
||||||
def _set_client(self, client: Optional[Client]) -> None:
|
def _set_client(self, client: Optional[Client]) -> None:
|
||||||
if client is None:
|
if client is None:
|
||||||
@@ -152,15 +155,24 @@ class BaseClient(UniversalDriver):
|
|||||||
if not self.client:
|
if not self.client:
|
||||||
raise ValueError('client is not connected')
|
raise ValueError('client is not connected')
|
||||||
|
|
||||||
logger.info('开始查找节点...')
|
logger.info(f'开始查找 {len(self._variables_to_find)} 个节点...')
|
||||||
try:
|
try:
|
||||||
# 获取根节点
|
# 获取根节点
|
||||||
root = self.client.get_root_node()
|
root = self.client.get_root_node()
|
||||||
objects = root.get_child(["0:Objects"])
|
objects = root.get_child(["0:Objects"])
|
||||||
|
|
||||||
|
# 记录查找前的状态
|
||||||
|
before_count = len(self._node_registry)
|
||||||
|
|
||||||
# 查找节点
|
# 查找节点
|
||||||
self._find_nodes_recursive(objects)
|
self._find_nodes_recursive(objects)
|
||||||
|
|
||||||
|
# 记录查找后的状态
|
||||||
|
after_count = len(self._node_registry)
|
||||||
|
newly_found = after_count - before_count
|
||||||
|
|
||||||
|
logger.info(f"本次查找新增 {newly_found} 个节点,当前共 {after_count} 个")
|
||||||
|
|
||||||
# 检查是否所有节点都已找到
|
# 检查是否所有节点都已找到
|
||||||
not_found = []
|
not_found = []
|
||||||
for var_name, var_info in self._variables_to_find.items():
|
for var_name, var_info in self._variables_to_find.items():
|
||||||
@@ -168,9 +180,13 @@ class BaseClient(UniversalDriver):
|
|||||||
not_found.append(var_name)
|
not_found.append(var_name)
|
||||||
|
|
||||||
if not_found:
|
if not_found:
|
||||||
logger.warning(f"以下节点未找到: {', '.join(not_found)}")
|
logger.warning(f"⚠ 以下 {len(not_found)} 个节点未找到: {', '.join(not_found[:10])}{'...' if len(not_found) > 10 else ''}")
|
||||||
|
logger.warning(f"提示:请检查这些节点名称是否与服务器的 BrowseName 完全匹配(包括大小写、空格等)")
|
||||||
|
# 提供一个示例来帮助调试
|
||||||
|
if not_found:
|
||||||
|
logger.info(f"尝试在服务器中查找第一个未找到的节点 '{not_found[0]}' 的相似节点...")
|
||||||
else:
|
else:
|
||||||
logger.info("所有节点均已找到")
|
logger.info(f"✓ 所有 {len(self._variables_to_find)} 个节点均已找到并注册")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"查找节点失败: {e}")
|
logger.error(f"查找节点失败: {e}")
|
||||||
@@ -188,17 +204,18 @@ class BaseClient(UniversalDriver):
|
|||||||
var_info = self._variables_to_find[node_name]
|
var_info = self._variables_to_find[node_name]
|
||||||
node_type = var_info.get("node_type")
|
node_type = var_info.get("node_type")
|
||||||
data_type = var_info.get("data_type")
|
data_type = var_info.get("data_type")
|
||||||
|
node_id_str = str(node.nodeid)
|
||||||
|
|
||||||
# 根据节点类型创建相应的对象
|
# 根据节点类型创建相应的对象
|
||||||
if node_type == NodeType.VARIABLE:
|
if node_type == NodeType.VARIABLE:
|
||||||
self._node_registry[node_name] = Variable(self.client, node_name, str(node.nodeid), data_type)
|
self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type)
|
||||||
logger.info(f"找到变量节点: {node_name}")
|
logger.info(f"✓ 找到变量节点: '{node_name}', NodeId: {node_id_str}, DataType: {data_type}")
|
||||||
elif node_type == NodeType.METHOD:
|
elif node_type == NodeType.METHOD:
|
||||||
# 对于方法节点,需要获取父节点ID
|
# 对于方法节点,需要获取父节点ID
|
||||||
parent_node = node.get_parent()
|
parent_node = node.get_parent()
|
||||||
parent_node_id = str(parent_node.nodeid)
|
parent_node_id = str(parent_node.nodeid)
|
||||||
self._node_registry[node_name] = Method(self.client, node_name, str(node.nodeid), parent_node_id, data_type)
|
self._node_registry[node_name] = Method(self.client, node_name, node_id_str, parent_node_id, data_type)
|
||||||
logger.info(f"找到方法节点: {node_name}")
|
logger.info(f"✓ 找到方法节点: '{node_name}', NodeId: {node_id_str}, ParentId: {parent_node_id}")
|
||||||
|
|
||||||
# 递归处理子节点
|
# 递归处理子节点
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
@@ -296,13 +313,17 @@ class BaseClient(UniversalDriver):
|
|||||||
if name in self._name_mapping:
|
if name in self._name_mapping:
|
||||||
chinese_name = self._name_mapping[name]
|
chinese_name = self._name_mapping[name]
|
||||||
if chinese_name in self._node_registry:
|
if chinese_name in self._node_registry:
|
||||||
return self._node_registry[chinese_name]
|
node = self._node_registry[chinese_name]
|
||||||
|
logger.debug(f"使用节点: '{name}' -> '{chinese_name}', NodeId: {node.node_id}")
|
||||||
|
return node
|
||||||
elif chinese_name in self._variables_to_find:
|
elif chinese_name in self._variables_to_find:
|
||||||
logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找")
|
logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找")
|
||||||
if self.client:
|
if self.client:
|
||||||
self._find_nodes()
|
self._find_nodes()
|
||||||
if chinese_name in self._node_registry:
|
if chinese_name in self._node_registry:
|
||||||
return self._node_registry[chinese_name]
|
node = self._node_registry[chinese_name]
|
||||||
|
logger.info(f"重新查找成功: '{chinese_name}', NodeId: {node.node_id}")
|
||||||
|
return node
|
||||||
raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到')
|
raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到')
|
||||||
|
|
||||||
# 直接使用原始名称查找
|
# 直接使用原始名称查找
|
||||||
@@ -312,9 +333,14 @@ class BaseClient(UniversalDriver):
|
|||||||
if self.client:
|
if self.client:
|
||||||
self._find_nodes()
|
self._find_nodes()
|
||||||
if name in self._node_registry:
|
if name in self._node_registry:
|
||||||
return self._node_registry[name]
|
node = self._node_registry[name]
|
||||||
|
logger.info(f"重新查找成功: '{name}', NodeId: {node.node_id}")
|
||||||
|
return node
|
||||||
|
logger.error(f"❌ 节点 '{name}' 未注册或未找到。已注册节点: {list(self._node_registry.keys())[:5]}...")
|
||||||
raise ValueError(f'节点 {name} 未注册或未找到')
|
raise ValueError(f'节点 {name} 未注册或未找到')
|
||||||
return self._node_registry[name]
|
node = self._node_registry[name]
|
||||||
|
logger.debug(f"使用节点: '{name}', NodeId: {node.node_id}")
|
||||||
|
return node
|
||||||
|
|
||||||
def get_node_registry(self) -> Dict[str, OpcUaNodeBase]:
|
def get_node_registry(self) -> Dict[str, OpcUaNodeBase]:
|
||||||
return self._node_registry
|
return self._node_registry
|
||||||
@@ -335,12 +361,13 @@ class BaseClient(UniversalDriver):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
logger.info(f'开始注册 {len(node_list)} 个节点...')
|
logger.info(f'开始注册 {len(node_list)} 个节点...')
|
||||||
|
new_nodes_count = 0
|
||||||
for node in node_list:
|
for node in node_list:
|
||||||
if node is None:
|
if node is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.name in self._node_registry:
|
if node.name in self._node_registry:
|
||||||
logger.info(f'节点 {node.name} 已存在')
|
logger.debug(f'节点 "{node.name}" 已存在于注册表')
|
||||||
exist = self._node_registry[node.name]
|
exist = self._node_registry[node.name]
|
||||||
if exist.type != node.node_type:
|
if exist.type != node.node_type:
|
||||||
raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致')
|
raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致')
|
||||||
@@ -351,9 +378,10 @@ class BaseClient(UniversalDriver):
|
|||||||
"node_type": node.node_type,
|
"node_type": node.node_type,
|
||||||
"data_type": node.data_type
|
"data_type": node.data_type
|
||||||
}
|
}
|
||||||
logger.info(f'添加节点 {node.name} 到待查找列表')
|
new_nodes_count += 1
|
||||||
|
logger.debug(f'添加节点 "{node.name}" ({node.node_type}) 到待查找列表')
|
||||||
|
|
||||||
logger.info('节点注册完成')
|
logger.info(f'节点注册完成:新增 {new_nodes_count} 个待查找节点,总计 {len(self._variables_to_find)} 个')
|
||||||
|
|
||||||
# 如果客户端已连接,立即开始查找
|
# 如果客户端已连接,立即开始查找
|
||||||
if self.client:
|
if self.client:
|
||||||
@@ -518,15 +546,25 @@ class BaseClient(UniversalDriver):
|
|||||||
raise ValueError("必须提供write_nodes参数")
|
raise ValueError("必须提供write_nodes参数")
|
||||||
|
|
||||||
def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
||||||
if isinstance(write_nodes, list):
|
"""根据 _workflow_params 为各节点写入真实数值。
|
||||||
# 处理节点列表
|
|
||||||
for node_name in write_nodes:
|
|
||||||
# 尝试从参数中获取同名参数的值
|
|
||||||
current_value = True # 默认值
|
|
||||||
if hasattr(self, '_workflow_params') and node_name in self._workflow_params:
|
|
||||||
current_value = self._workflow_params[node_name]
|
|
||||||
print(f"初始化函数: 从参数获取值 {node_name} = {current_value}")
|
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- write_nodes 为 list 时: 节点名 == 参数名,从 _workflow_params[node_name] 取值;
|
||||||
|
- write_nodes 为 dict 时:
|
||||||
|
* value 为字符串且在 _workflow_params 中: 当作参数名去取值;
|
||||||
|
* 否则 value 视为常量直接写入。
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = getattr(self, "_workflow_params", {}) or {}
|
||||||
|
|
||||||
|
if isinstance(write_nodes, list):
|
||||||
|
# 节点列表形式: 节点名与参数名一致
|
||||||
|
for node_name in write_nodes:
|
||||||
|
if node_name not in params:
|
||||||
|
print(f"初始化函数: 参数中未找到 {node_name}, 跳过写入")
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_value = params[node_name]
|
||||||
print(f"初始化函数: 写入节点 {node_name} = {current_value}")
|
print(f"初始化函数: 写入节点 {node_name} = {current_value}")
|
||||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||||
result_str = self.write_node(input_json)
|
result_str = self.write_node(input_json)
|
||||||
@@ -538,13 +576,14 @@ class BaseClient(UniversalDriver):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}")
|
print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}")
|
||||||
elif isinstance(write_nodes, dict):
|
elif isinstance(write_nodes, dict):
|
||||||
# 处理节点字典,使用指定的值
|
# 映射形式: 节点名 -> 参数名或常量
|
||||||
for node_name, node_value in write_nodes.items():
|
for node_name, node_value in write_nodes.items():
|
||||||
# 检查值是否是字符串类型的参数名
|
if isinstance(node_value, str) and node_value in params:
|
||||||
current_value = node_value
|
current_value = params[node_value]
|
||||||
if isinstance(node_value, str) and hasattr(self, '_workflow_params') and node_value in self._workflow_params:
|
|
||||||
current_value = self._workflow_params[node_value]
|
|
||||||
print(f"初始化函数: 从参数获取值 {node_value} = {current_value}")
|
print(f"初始化函数: 从参数获取值 {node_value} = {current_value}")
|
||||||
|
else:
|
||||||
|
current_value = node_value
|
||||||
|
print(f"初始化函数: 使用常量值 写入 {node_name} = {current_value}")
|
||||||
|
|
||||||
print(f"初始化函数: 写入节点 {node_name} = {current_value}")
|
print(f"初始化函数: 写入节点 {node_name} = {current_value}")
|
||||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||||
@@ -672,20 +711,20 @@ class BaseClient(UniversalDriver):
|
|||||||
condition_nodes: 条件节点列表 [节点名1, 节点名2]
|
condition_nodes: 条件节点列表 [节点名1, 节点名2]
|
||||||
"""
|
"""
|
||||||
def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
||||||
# 直接处理写入节点
|
"""开始函数: 写入触发节点, 然后轮询条件节点直到满足停止条件。"""
|
||||||
|
|
||||||
|
params = getattr(self, "_workflow_params", {}) or {}
|
||||||
|
|
||||||
|
# 先处理写入节点(触发位等)
|
||||||
if write_nodes:
|
if write_nodes:
|
||||||
if isinstance(write_nodes, list):
|
if isinstance(write_nodes, list):
|
||||||
# 处理节点列表,默认值都是True
|
# 列表形式: 节点名与参数名一致, 若无参数则直接写 True
|
||||||
for i, node_name in enumerate(write_nodes):
|
for node_name in write_nodes:
|
||||||
# 尝试获取与节点对应的参数值
|
if node_name in params:
|
||||||
param_name = f"write_{i}"
|
current_value = params[node_name]
|
||||||
|
else:
|
||||||
|
current_value = True
|
||||||
|
|
||||||
# 获取参数值(如果有)
|
|
||||||
current_value = True # 默认值
|
|
||||||
if hasattr(self, '_workflow_params') and param_name in self._workflow_params:
|
|
||||||
current_value = self._workflow_params[param_name]
|
|
||||||
|
|
||||||
# 直接写入节点
|
|
||||||
print(f"直接写入节点 {node_name} = {current_value}")
|
print(f"直接写入节点 {node_name} = {current_value}")
|
||||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||||
result_str = self.write_node(input_json)
|
result_str = self.write_node(input_json)
|
||||||
@@ -697,14 +736,13 @@ class BaseClient(UniversalDriver):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}")
|
print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}")
|
||||||
elif isinstance(write_nodes, dict):
|
elif isinstance(write_nodes, dict):
|
||||||
# 处理节点字典,值是指定的
|
# 字典形式: 节点名 -> 常量值(如 True/False)
|
||||||
for node_name, node_value in write_nodes.items():
|
for node_name, node_value in write_nodes.items():
|
||||||
# 尝试获取参数值(如果节点名与参数名匹配)
|
if node_name in params:
|
||||||
current_value = node_value # 使用指定的默认值
|
current_value = params[node_name]
|
||||||
if hasattr(self, '_workflow_params') and node_name in self._workflow_params:
|
else:
|
||||||
current_value = self._workflow_params[node_name]
|
current_value = node_value
|
||||||
|
|
||||||
# 直接写入节点
|
|
||||||
print(f"直接写入节点 {node_name} = {current_value}")
|
print(f"直接写入节点 {node_name} = {current_value}")
|
||||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||||
result_str = self.write_node(input_json)
|
result_str = self.write_node(input_json)
|
||||||
@@ -732,6 +770,7 @@ class BaseClient(UniversalDriver):
|
|||||||
# 直接读取节点
|
# 直接读取节点
|
||||||
result_str = self.read_node(node_name)
|
result_str = self.read_node(node_name)
|
||||||
try:
|
try:
|
||||||
|
time.sleep(1)
|
||||||
result_str = result_str.replace("'", '"')
|
result_str = result_str.replace("'", '"')
|
||||||
result_dict = json.loads(result_str)
|
result_dict = json.loads(result_str)
|
||||||
read_res = result_dict.get("value")
|
read_res = result_dict.get("value")
|
||||||
@@ -1035,31 +1074,33 @@ class BaseClient(UniversalDriver):
|
|||||||
读取节点值的便捷方法
|
读取节点值的便捷方法
|
||||||
返回包含result字段的字典
|
返回包含result字段的字典
|
||||||
"""
|
"""
|
||||||
try:
|
# 使用锁保护客户端访问
|
||||||
node = self.use_node(node_name)
|
with self._client_lock:
|
||||||
value, error = node.read()
|
try:
|
||||||
|
node = self.use_node(node_name)
|
||||||
|
value, error = node.read()
|
||||||
|
|
||||||
# 创建结果字典
|
# 创建结果字典
|
||||||
result = {
|
result = {
|
||||||
"value": value,
|
"value": value,
|
||||||
"error": error,
|
"error": error,
|
||||||
"node_name": node_name,
|
"node_name": node_name,
|
||||||
"timestamp": time.time()
|
"timestamp": time.time()
|
||||||
}
|
}
|
||||||
|
|
||||||
# 返回JSON字符串
|
# 返回JSON字符串
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"读取节点 {node_name} 失败: {e}")
|
logger.error(f"读取节点 {node_name} 失败: {e}")
|
||||||
# 创建错误结果字典
|
# 创建错误结果字典
|
||||||
result = {
|
result = {
|
||||||
"value": None,
|
"value": None,
|
||||||
"error": True,
|
"error": True,
|
||||||
"node_name": node_name,
|
"node_name": node_name,
|
||||||
"error_message": str(e),
|
"error_message": str(e),
|
||||||
"timestamp": time.time()
|
"timestamp": time.time()
|
||||||
}
|
}
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
|
|
||||||
def write_node(self, json_input: str) -> str:
|
def write_node(self, json_input: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1068,47 +1109,49 @@ class BaseClient(UniversalDriver):
|
|||||||
eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}'
|
eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}'
|
||||||
返回JSON格式的字符串,包含操作结果
|
返回JSON格式的字符串,包含操作结果
|
||||||
"""
|
"""
|
||||||
try:
|
# 使用锁保护客户端访问
|
||||||
# 解析JSON格式的输入
|
with self._client_lock:
|
||||||
if not isinstance(json_input, str):
|
|
||||||
json_input = str(json_input)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
input_data = json.loads(json_input)
|
# 解析JSON格式的输入
|
||||||
if not isinstance(input_data, dict):
|
if not isinstance(json_input, str):
|
||||||
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False})
|
json_input = str(json_input)
|
||||||
|
|
||||||
# 从JSON中提取节点名称和值
|
try:
|
||||||
node_name = input_data.get("node_name")
|
input_data = json.loads(json_input)
|
||||||
value = input_data.get("value")
|
if not isinstance(input_data, dict):
|
||||||
|
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False})
|
||||||
|
|
||||||
if node_name is None:
|
# 从JSON中提取节点名称和值
|
||||||
return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False})
|
node_name = input_data.get("node_name")
|
||||||
except json.JSONDecodeError as e:
|
value = input_data.get("value")
|
||||||
return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False})
|
|
||||||
|
|
||||||
node = self.use_node(node_name)
|
if node_name is None:
|
||||||
error = node.write(value)
|
return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False})
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False})
|
||||||
|
|
||||||
# 创建结果字典
|
node = self.use_node(node_name)
|
||||||
result = {
|
error = node.write(value)
|
||||||
"value": value,
|
|
||||||
"error": error,
|
|
||||||
"node_name": node_name,
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"success": not error
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(result)
|
# 创建结果字典
|
||||||
except Exception as e:
|
result = {
|
||||||
logger.error(f"写入节点失败: {e}")
|
"value": value,
|
||||||
result = {
|
"error": error,
|
||||||
"error": True,
|
"node_name": node_name,
|
||||||
"error_message": str(e),
|
"timestamp": time.time(),
|
||||||
"timestamp": time.time(),
|
"success": not error
|
||||||
"success": False
|
}
|
||||||
}
|
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"写入节点失败: {e}")
|
||||||
|
result = {
|
||||||
|
"error": True,
|
||||||
|
"error_message": str(e),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"success": False
|
||||||
|
}
|
||||||
|
return json.dumps(result)
|
||||||
|
|
||||||
def call_method(self, node_name: str, *args) -> Tuple[Any, bool]:
|
def call_method(self, node_name: str, *args) -> Tuple[Any, bool]:
|
||||||
"""
|
"""
|
||||||
@@ -1150,16 +1193,27 @@ class OpcUaClient(BaseClient):
|
|||||||
self._refresh_running = False
|
self._refresh_running = False
|
||||||
self._refresh_thread = None
|
self._refresh_thread = None
|
||||||
|
|
||||||
|
# 添加线程锁,保护OPC UA客户端的并发访问
|
||||||
|
import threading
|
||||||
|
self._client_lock = threading.RLock()
|
||||||
|
|
||||||
# 如果提供了配置文件路径,则加载配置并注册工作流
|
# 如果提供了配置文件路径,则加载配置并注册工作流
|
||||||
if config_path:
|
if config_path:
|
||||||
self.load_config(config_path)
|
self.load_config(config_path)
|
||||||
|
|
||||||
# 启动节点值刷新线程
|
# 延迟启动节点值刷新线程,确保节点查找完成
|
||||||
self.start_node_refresh()
|
# 注意:刷新线程会在所有节点注册后由load_config调用
|
||||||
|
# 暂时不在这里启动,避免在节点未找到时就开始刷新
|
||||||
|
# self.start_node_refresh()
|
||||||
|
|
||||||
def _register_nodes_as_attributes(self):
|
def _register_nodes_as_attributes(self):
|
||||||
"""将所有节点注册为实例属性,可以通过self.node_name访问"""
|
"""将所有节点注册为实例属性,可以通过self.node_name访问"""
|
||||||
for node_name, node in self._node_registry.items():
|
for node_name, node in self._node_registry.items():
|
||||||
|
# 检查node_id是否有效
|
||||||
|
if not node.node_id or node.node_id == "":
|
||||||
|
logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性")
|
||||||
|
continue
|
||||||
|
|
||||||
# 检查是否有对应的英文名称
|
# 检查是否有对应的英文名称
|
||||||
eng_name = self._reverse_mapping.get(node_name)
|
eng_name = self._reverse_mapping.get(node_name)
|
||||||
if eng_name:
|
if eng_name:
|
||||||
@@ -1182,7 +1236,7 @@ class OpcUaClient(BaseClient):
|
|||||||
|
|
||||||
# 使用property装饰器将方法注册为类属性
|
# 使用property装饰器将方法注册为类属性
|
||||||
setattr(OpcUaClient, attr_name, property(create_property_getter(node_name)))
|
setattr(OpcUaClient, attr_name, property(create_property_getter(node_name)))
|
||||||
logger.info(f"已注册节点 '{node_name}' 为属性 '{attr_name}'")
|
logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}', NodeId: {node.node_id}")
|
||||||
|
|
||||||
def refresh_node_values(self):
|
def refresh_node_values(self):
|
||||||
"""刷新所有节点的值到缓存"""
|
"""刷新所有节点的值到缓存"""
|
||||||
@@ -1190,22 +1244,29 @@ class OpcUaClient(BaseClient):
|
|||||||
logger.warning("客户端未初始化,无法刷新节点值")
|
logger.warning("客户端未初始化,无法刷新节点值")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# 使用锁保护客户端访问
|
||||||
# 简单检查连接状态,如果不连接会抛出异常
|
with self._client_lock:
|
||||||
self.client.get_namespace_array()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
for node_name, node in self._node_registry.items():
|
|
||||||
try:
|
try:
|
||||||
if hasattr(node, 'read'):
|
# 简单检查连接状态,如果不连接会抛出异常
|
||||||
value, error = node.read()
|
self.client.get_namespace_array()
|
||||||
if not error:
|
|
||||||
self._node_values[node_name] = value
|
|
||||||
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"刷新节点 '{node_name}' 失败: {e}")
|
logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for node_name, node in self._node_registry.items():
|
||||||
|
try:
|
||||||
|
# 跳过node_id为空的节点
|
||||||
|
if not node.node_id or node.node_id == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(node, 'read'):
|
||||||
|
value, error = node.read()
|
||||||
|
if not error:
|
||||||
|
self._node_values[node_name] = value
|
||||||
|
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
|
||||||
|
except Exception as e:
|
||||||
|
# 降低日志级别,避免刷新线程产生大量错误日志
|
||||||
|
logger.debug(f"刷新节点 '{node_name}' 失败: {e}")
|
||||||
|
|
||||||
def get_node_value(self, name):
|
def get_node_value(self, name):
|
||||||
"""获取节点值,支持中文名和英文名"""
|
"""获取节点值,支持中文名和英文名"""
|
||||||
@@ -1303,6 +1364,11 @@ class OpcUaClient(BaseClient):
|
|||||||
# 直接使用字典
|
# 直接使用字典
|
||||||
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
|
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
|
||||||
|
|
||||||
|
# 立即执行节点查找(如果客户端已连接)
|
||||||
|
if self.client and self._variables_to_find:
|
||||||
|
logger.info("CSV加载完成,开始查找服务器节点...")
|
||||||
|
self._find_nodes()
|
||||||
|
|
||||||
# 处理工作流创建
|
# 处理工作流创建
|
||||||
if "create_flow" in config_data:
|
if "create_flow" in config_data:
|
||||||
# 直接传递字典列表
|
# 直接传递字典列表
|
||||||
@@ -1310,14 +1376,114 @@ class OpcUaClient(BaseClient):
|
|||||||
# 将工作流注册为实例方法
|
# 将工作流注册为实例方法
|
||||||
self.register_workflows_as_methods()
|
self.register_workflows_as_methods()
|
||||||
|
|
||||||
# 将所有节点注册为属性
|
# 将所有节点注册为属性(只注册已找到的节点)
|
||||||
self._register_nodes_as_attributes()
|
self._register_nodes_as_attributes()
|
||||||
|
|
||||||
|
# 打印节点注册统计
|
||||||
|
found_count = len(self._node_registry)
|
||||||
|
total_count = len(self._variables_to_find)
|
||||||
|
if found_count < total_count:
|
||||||
|
logger.warning(f"节点查找完成:找到 {found_count}/{total_count} 个节点")
|
||||||
|
logger.warning(f"有 {total_count - found_count} 个节点未找到,这些节点的操作将会失败")
|
||||||
|
else:
|
||||||
|
logger.info(f"✓ 节点查找完成:所有 {found_count} 个节点均已找到")
|
||||||
|
|
||||||
|
# 现在启动节点值刷新线程
|
||||||
|
# self.start_node_refresh()
|
||||||
|
|
||||||
logger.info(f"成功从 {config_path} 加载配置")
|
logger.info(f"成功从 {config_path} 加载配置")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def print_node_registry_status(self):
|
||||||
|
"""打印节点注册状态,用于调试"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("节点注册状态诊断报告")
|
||||||
|
print("="*80)
|
||||||
|
print(f"\n待查找节点总数: {len(self._variables_to_find)}")
|
||||||
|
print(f"已找到节点总数: {len(self._node_registry)}")
|
||||||
|
print(f"未找到节点总数: {len(self._variables_to_find) - len(self._node_registry)}")
|
||||||
|
|
||||||
|
# 显示已找到的节点(前10个)
|
||||||
|
if self._node_registry:
|
||||||
|
print(f"\n✓ 已找到的节点 (显示前10个):")
|
||||||
|
for i, (name, node) in enumerate(list(self._node_registry.items())[:10]):
|
||||||
|
eng_name = self._reverse_mapping.get(name, "")
|
||||||
|
eng_info = f" ({eng_name})" if eng_name else ""
|
||||||
|
print(f" {i+1}. '{name}'{eng_info}")
|
||||||
|
print(f" NodeId: {node.node_id}")
|
||||||
|
print(f" Type: {node.type}")
|
||||||
|
|
||||||
|
# 显示未找到的节点
|
||||||
|
not_found = [name for name in self._variables_to_find if name not in self._node_registry]
|
||||||
|
if not_found:
|
||||||
|
print(f"\n✗ 未找到的节点 (显示前20个):")
|
||||||
|
for i, name in enumerate(not_found[:20]):
|
||||||
|
eng_name = self._reverse_mapping.get(name, "")
|
||||||
|
eng_info = f" ({eng_name})" if eng_name else ""
|
||||||
|
node_info = self._variables_to_find[name]
|
||||||
|
print(f" {i+1}. '{name}'{eng_info} - {node_info['node_type']}")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("提示:")
|
||||||
|
print("1. 如果大量节点未找到,请检查CSV中的节点名称是否与服务器BrowseName完全匹配")
|
||||||
|
print("2. 可以使用 client.browse_server_nodes() 查看服务器的实际节点结构")
|
||||||
|
print("3. 节点名称区分大小写,且包括所有空格和特殊字符")
|
||||||
|
print("="*80 + "\n")
|
||||||
|
|
||||||
|
def browse_server_nodes(self, max_depth=3, start_path=["0:Objects"]):
|
||||||
|
"""浏览服务器节点树,用于调试和对比"""
|
||||||
|
if not self.client:
|
||||||
|
print("客户端未连接")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print(f"服务器节点浏览 (最大深度: {max_depth})")
|
||||||
|
print("="*80 + "\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = self.client.get_root_node()
|
||||||
|
start_node = root.get_child(start_path)
|
||||||
|
self._browse_node_recursive(start_node, depth=0, max_depth=max_depth)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"浏览失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _browse_node_recursive(self, node, depth=0, max_depth=3):
|
||||||
|
"""递归浏览节点"""
|
||||||
|
if depth > max_depth:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
browse_name = node.get_browse_name()
|
||||||
|
node_class = node.get_node_class()
|
||||||
|
indent = " " * depth
|
||||||
|
|
||||||
|
# 显示节点信息
|
||||||
|
print(f"{indent}├─ {browse_name.Name}")
|
||||||
|
print(f"{indent}│ NodeId: {str(node.nodeid)}")
|
||||||
|
print(f"{indent}│ NodeClass: {node_class}")
|
||||||
|
|
||||||
|
# 如果是变量,显示数据类型
|
||||||
|
if node_class == NodeClass.Variable:
|
||||||
|
try:
|
||||||
|
data_type = node.get_data_type()
|
||||||
|
print(f"{indent}│ DataType: {data_type}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 递归处理子节点(限制数量避免输出过多)
|
||||||
|
if depth < max_depth:
|
||||||
|
children = node.get_children()
|
||||||
|
for i, child in enumerate(children[:20]): # 每层最多显示20个子节点
|
||||||
|
self._browse_node_recursive(child, depth + 1, max_depth)
|
||||||
|
if len(children) > 20:
|
||||||
|
print(f"{indent} ... ({len(children) - 20} more children)")
|
||||||
|
except Exception as e:
|
||||||
|
# 忽略单个节点的错误
|
||||||
|
pass
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
# 停止刷新线程
|
# 停止刷新线程
|
||||||
self.stop_node_refresh()
|
self.stop_node_refresh()
|
||||||
@@ -1338,8 +1504,8 @@ if __name__ == '__main__':
|
|||||||
# 创建OPC UA客户端并加载配置
|
# 创建OPC UA客户端并加载配置
|
||||||
try:
|
try:
|
||||||
client = OpcUaClient(
|
client = OpcUaClient(
|
||||||
url="opc.tcp://localhost:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址
|
url="opc.tcp://192.168.1.88:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址
|
||||||
config_path=config_path # 传入配置文件路径
|
config_path="D:\\Uni-Lab-OS\\unilabos\\device_comms\\opcua_client\\opcua_huairou.json" # 传入配置文件路径
|
||||||
)
|
)
|
||||||
|
|
||||||
# 列出所有已注册的工作流
|
# 列出所有已注册的工作流
|
||||||
@@ -1349,13 +1515,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# 测试trigger_grab_action工作流 - 使用英文参数名
|
# 测试trigger_grab_action工作流 - 使用英文参数名
|
||||||
print("\n测试trigger_grab_action工作流 - 使用英文参数名:")
|
print("\n测试trigger_grab_action工作流 - 使用英文参数名:")
|
||||||
client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=3)
|
client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=2)
|
||||||
|
# client.set_node_value("reaction_tank_number", 2)
|
||||||
|
|
||||||
|
|
||||||
# 读取节点值 - 使用英文节点名
|
# 读取节点值 - 使用英文节点名
|
||||||
grab_complete = client.get_node_value("grab_complete")
|
grab_complete = client.get_node_value("grab_complete")
|
||||||
reaction_tank = client.get_node_value("reaction_tank_number")
|
reaction_tank = client.get_node_value("reaction_tank_number")
|
||||||
raw_tank = client.get_node_value("raw_tank_number")
|
raw_tank = client.get_node_value("raw_tank_number")
|
||||||
|
|
||||||
print(f"\n执行后状态检查 (使用英文节点名):")
|
print(f"\n执行后状态检查 (使用英文节点名):")
|
||||||
print(f" - 抓取完成状态: {grab_complete}")
|
print(f" - 抓取完成状态: {grab_complete}")
|
||||||
print(f" - 当前反应罐号码: {reaction_tank}")
|
print(f" - 当前反应罐号码: {reaction_tank}")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Tuple, Union, Optional, Any, List
|
from typing import Tuple, Union, Optional, Any, List
|
||||||
|
|
||||||
from opcua import Client, Node
|
from opcua import Client, Node, ua
|
||||||
from opcua.ua import NodeId, NodeClass, VariantType
|
from opcua.ua import NodeId, NodeClass, VariantType
|
||||||
|
|
||||||
|
|
||||||
@@ -47,23 +47,68 @@ class Base(ABC):
|
|||||||
def _get_node(self) -> Node:
|
def _get_node(self) -> Node:
|
||||||
if self._node is None:
|
if self._node is None:
|
||||||
try:
|
try:
|
||||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
# 尝试多种 NodeId 字符串格式解析,兼容不同服务器/库的输出
|
||||||
if "NumericNodeId" in self._node_id:
|
# 可能的格式示例: 'ns=2;i=1234', 'ns=2;s=SomeString',
|
||||||
# 从字符串中提取命名空间和标识符
|
# 'StringNodeId(ns=4;s=OPC|变量名)', 'NumericNodeId(ns=2;i=1234)' 等
|
||||||
import re
|
import re
|
||||||
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
|
|
||||||
if match:
|
nid = self._node_id
|
||||||
ns = int(match.group(1))
|
# 如果已经是 NodeId/Node 对象(库用户可能传入),直接使用
|
||||||
identifier = int(match.group(2))
|
try:
|
||||||
node_id = NodeId(identifier, ns)
|
from opcua.ua import NodeId as UaNodeId
|
||||||
self._node = self._client.get_node(node_id)
|
if isinstance(nid, UaNodeId):
|
||||||
|
self._node = self._client.get_node(nid)
|
||||||
|
return self._node
|
||||||
|
except Exception:
|
||||||
|
# 若导入或类型判断失败,则继续下一步
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 直接以字符串形式处理
|
||||||
|
if isinstance(nid, str):
|
||||||
|
nid = nid.strip()
|
||||||
|
|
||||||
|
# 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)'
|
||||||
|
# 提取括号内的内容
|
||||||
|
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
|
||||||
|
if match_wrapped:
|
||||||
|
# 提取括号内的实际 node_id 字符串
|
||||||
|
nid = match_wrapped.group(2).strip()
|
||||||
|
|
||||||
|
# 常见短格式 'ns=2;i=1234' 或 'ns=2;s=SomeString'
|
||||||
|
if re.match(r'^ns=\d+;[is]=', nid):
|
||||||
|
self._node = self._client.get_node(nid)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"无法解析节点ID: {self._node_id}")
|
# 尝试提取 ns 和 i 或 s
|
||||||
|
# 对于字符串标识符,可能包含特殊字符,使用非贪婪匹配
|
||||||
|
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
|
||||||
|
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
|
||||||
|
if m_num:
|
||||||
|
ns = int(m_num.group(1))
|
||||||
|
identifier = int(m_num.group(2))
|
||||||
|
node_id = NodeId(identifier, ns)
|
||||||
|
self._node = self._client.get_node(node_id)
|
||||||
|
elif m_str:
|
||||||
|
ns = int(m_str.group(1))
|
||||||
|
identifier = m_str.group(2).strip()
|
||||||
|
# 对于字符串标识符,直接使用字符串格式
|
||||||
|
node_id_str = f"ns={ns};s={identifier}"
|
||||||
|
self._node = self._client.get_node(node_id_str)
|
||||||
|
else:
|
||||||
|
# 回退:尝试直接传入字符串(有些实现接受其它格式)
|
||||||
|
try:
|
||||||
|
self._node = self._client.get_node(self._node_id)
|
||||||
|
except Exception as e:
|
||||||
|
# 输出更详细的错误信息供调试
|
||||||
|
print(f"获取节点失败(尝试直接字符串): {self._node_id}, 错误: {e}")
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
# 直接使用节点ID字符串
|
# 非字符串,尝试直接使用
|
||||||
self._node = self._client.get_node(self._node_id)
|
self._node = self._client.get_node(self._node_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取节点失败: {self._node_id}, 错误: {e}")
|
print(f"获取节点失败: {self._node_id}, 错误: {e}")
|
||||||
|
# 添加额外提示,帮助定位 BadNodeIdUnknown 问题
|
||||||
|
print("提示: 请确认该 node_id 是否来自当前连接的服务器地址空间," \
|
||||||
|
"以及 CSV/配置中名称与服务器 BrowseName 是否匹配。")
|
||||||
raise
|
raise
|
||||||
return self._node
|
return self._node
|
||||||
|
|
||||||
@@ -104,7 +149,56 @@ class Variable(Base):
|
|||||||
|
|
||||||
def write(self, value: Any) -> bool:
|
def write(self, value: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
self._get_node().set_value(value)
|
# 如果声明了数据类型,则尝试转换并使用对应的 Variant 写入
|
||||||
|
coerced = value
|
||||||
|
try:
|
||||||
|
if self._data_type is not None:
|
||||||
|
# 基于声明的数据类型做简单类型转换
|
||||||
|
dt = self._data_type
|
||||||
|
if dt in (DataType.SBYTE, DataType.BYTE, DataType.INT16, DataType.UINT16,
|
||||||
|
DataType.INT32, DataType.UINT32, DataType.INT64, DataType.UINT64):
|
||||||
|
# 数值类型 -> int
|
||||||
|
if isinstance(value, str):
|
||||||
|
coerced = int(value)
|
||||||
|
else:
|
||||||
|
coerced = int(value)
|
||||||
|
elif dt in (DataType.FLOAT, DataType.DOUBLE):
|
||||||
|
if isinstance(value, str):
|
||||||
|
coerced = float(value)
|
||||||
|
else:
|
||||||
|
coerced = float(value)
|
||||||
|
elif dt == DataType.BOOLEAN:
|
||||||
|
if isinstance(value, str):
|
||||||
|
v = value.strip().lower()
|
||||||
|
if v in ("true", "1", "yes", "on"):
|
||||||
|
coerced = True
|
||||||
|
elif v in ("false", "0", "no", "off"):
|
||||||
|
coerced = False
|
||||||
|
else:
|
||||||
|
coerced = bool(value)
|
||||||
|
else:
|
||||||
|
coerced = bool(value)
|
||||||
|
elif dt == DataType.STRING or dt == DataType.BYTESTRING or dt == DataType.DATETIME:
|
||||||
|
coerced = str(value)
|
||||||
|
|
||||||
|
# 使用 ua.Variant 明确指定 VariantType
|
||||||
|
try:
|
||||||
|
variant = ua.Variant(coerced, dt.value)
|
||||||
|
self._get_node().set_value(variant)
|
||||||
|
except Exception:
|
||||||
|
# 回退:有些 set_value 实现接受 (value, variant_type)
|
||||||
|
try:
|
||||||
|
self._get_node().set_value(coerced, dt.value)
|
||||||
|
except Exception:
|
||||||
|
# 最后回退到直接写入(保持兼容性)
|
||||||
|
self._get_node().set_value(coerced)
|
||||||
|
else:
|
||||||
|
# 未声明数据类型,直接写入
|
||||||
|
self._get_node().set_value(value)
|
||||||
|
except Exception:
|
||||||
|
# 若在转换或按数据类型写入失败,尝试直接写入原始值并让上层捕获错误
|
||||||
|
self._get_node().set_value(value)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"写入变量 {self._name} 失败: {e}")
|
print(f"写入变量 {self._name} 失败: {e}")
|
||||||
@@ -120,20 +214,50 @@ class Method(Base):
|
|||||||
def _get_parent_node(self) -> Node:
|
def _get_parent_node(self) -> Node:
|
||||||
if self._parent_node is None:
|
if self._parent_node is None:
|
||||||
try:
|
try:
|
||||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
# 处理父节点ID,使用与_get_node相同的解析逻辑
|
||||||
if "NumericNodeId" in self._parent_node_id:
|
import re
|
||||||
# 从字符串中提取命名空间和标识符
|
|
||||||
import re
|
nid = self._parent_node_id
|
||||||
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
|
|
||||||
if match:
|
# 如果已经是 NodeId 对象,直接使用
|
||||||
ns = int(match.group(1))
|
try:
|
||||||
identifier = int(match.group(2))
|
from opcua.ua import NodeId as UaNodeId
|
||||||
node_id = NodeId(identifier, ns)
|
if isinstance(nid, UaNodeId):
|
||||||
self._parent_node = self._client.get_node(node_id)
|
self._parent_node = self._client.get_node(nid)
|
||||||
|
return self._parent_node
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 字符串处理
|
||||||
|
if isinstance(nid, str):
|
||||||
|
nid = nid.strip()
|
||||||
|
|
||||||
|
# 处理包含类名的格式
|
||||||
|
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
|
||||||
|
if match_wrapped:
|
||||||
|
nid = match_wrapped.group(2).strip()
|
||||||
|
|
||||||
|
# 常见短格式
|
||||||
|
if re.match(r'^ns=\d+;[is]=', nid):
|
||||||
|
self._parent_node = self._client.get_node(nid)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"无法解析父节点ID: {self._parent_node_id}")
|
# 提取 ns 和 i 或 s
|
||||||
|
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
|
||||||
|
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
|
||||||
|
if m_num:
|
||||||
|
ns = int(m_num.group(1))
|
||||||
|
identifier = int(m_num.group(2))
|
||||||
|
node_id = NodeId(identifier, ns)
|
||||||
|
self._parent_node = self._client.get_node(node_id)
|
||||||
|
elif m_str:
|
||||||
|
ns = int(m_str.group(1))
|
||||||
|
identifier = m_str.group(2).strip()
|
||||||
|
node_id_str = f"ns={ns};s={identifier}"
|
||||||
|
self._parent_node = self._client.get_node(node_id_str)
|
||||||
|
else:
|
||||||
|
# 回退
|
||||||
|
self._parent_node = self._client.get_node(self._parent_node_id)
|
||||||
else:
|
else:
|
||||||
# 直接使用节点ID字符串
|
|
||||||
self._parent_node = self._client.get_node(self._parent_node_id)
|
self._parent_node = self._client.get_node(self._parent_node_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
|
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user