mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-14 03:35:12 +00:00
feat(opcua): 增强节点ID解析兼容性和数据类型处理
改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符 添加数据类型转换处理,确保写入值时类型匹配 优化错误提示信息,便于调试节点连接问题
This commit is contained in:
@@ -125,6 +125,9 @@ class BaseClient(UniversalDriver):
|
||||
# 初始化名称映射字典
|
||||
self._name_mapping = {}
|
||||
self._reverse_mapping = {}
|
||||
# 初始化线程锁(在子类中会被重新创建,这里提供默认实现)
|
||||
import threading
|
||||
self._client_lock = threading.RLock()
|
||||
|
||||
def _set_client(self, client: Optional[Client]) -> None:
|
||||
if client is None:
|
||||
@@ -152,15 +155,24 @@ class BaseClient(UniversalDriver):
|
||||
if not self.client:
|
||||
raise ValueError('client is not connected')
|
||||
|
||||
logger.info('开始查找节点...')
|
||||
logger.info(f'开始查找 {len(self._variables_to_find)} 个节点...')
|
||||
try:
|
||||
# 获取根节点
|
||||
root = self.client.get_root_node()
|
||||
objects = root.get_child(["0:Objects"])
|
||||
|
||||
# 记录查找前的状态
|
||||
before_count = len(self._node_registry)
|
||||
|
||||
# 查找节点
|
||||
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 = []
|
||||
for var_name, var_info in self._variables_to_find.items():
|
||||
@@ -168,9 +180,13 @@ class BaseClient(UniversalDriver):
|
||||
not_found.append(var_name)
|
||||
|
||||
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:
|
||||
logger.info("所有节点均已找到")
|
||||
logger.info(f"✓ 所有 {len(self._variables_to_find)} 个节点均已找到并注册")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查找节点失败: {e}")
|
||||
@@ -188,17 +204,18 @@ class BaseClient(UniversalDriver):
|
||||
var_info = self._variables_to_find[node_name]
|
||||
node_type = var_info.get("node_type")
|
||||
data_type = var_info.get("data_type")
|
||||
node_id_str = str(node.nodeid)
|
||||
|
||||
# 根据节点类型创建相应的对象
|
||||
if node_type == NodeType.VARIABLE:
|
||||
self._node_registry[node_name] = Variable(self.client, node_name, str(node.nodeid), data_type)
|
||||
logger.info(f"找到变量节点: {node_name}")
|
||||
self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type)
|
||||
logger.info(f"✓ 找到变量节点: '{node_name}', NodeId: {node_id_str}, DataType: {data_type}")
|
||||
elif node_type == NodeType.METHOD:
|
||||
# 对于方法节点,需要获取父节点ID
|
||||
parent_node = node.get_parent()
|
||||
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)
|
||||
logger.info(f"找到方法节点: {node_name}")
|
||||
self._node_registry[node_name] = Method(self.client, node_name, node_id_str, parent_node_id, data_type)
|
||||
logger.info(f"✓ 找到方法节点: '{node_name}', NodeId: {node_id_str}, ParentId: {parent_node_id}")
|
||||
|
||||
# 递归处理子节点
|
||||
for child in node.get_children():
|
||||
@@ -296,13 +313,17 @@ class BaseClient(UniversalDriver):
|
||||
if name in self._name_mapping:
|
||||
chinese_name = self._name_mapping[name]
|
||||
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:
|
||||
logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找")
|
||||
if self.client:
|
||||
self._find_nodes()
|
||||
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}) 未注册或未找到')
|
||||
|
||||
# 直接使用原始名称查找
|
||||
@@ -312,9 +333,14 @@ class BaseClient(UniversalDriver):
|
||||
if self.client:
|
||||
self._find_nodes()
|
||||
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} 未注册或未找到')
|
||||
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]:
|
||||
return self._node_registry
|
||||
@@ -335,12 +361,13 @@ class BaseClient(UniversalDriver):
|
||||
return self
|
||||
|
||||
logger.info(f'开始注册 {len(node_list)} 个节点...')
|
||||
new_nodes_count = 0
|
||||
for node in node_list:
|
||||
if node is None:
|
||||
continue
|
||||
|
||||
if node.name in self._node_registry:
|
||||
logger.info(f'节点 {node.name} 已存在')
|
||||
logger.debug(f'节点 "{node.name}" 已存在于注册表')
|
||||
exist = self._node_registry[node.name]
|
||||
if exist.type != node.node_type:
|
||||
raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致')
|
||||
@@ -351,9 +378,10 @@ class BaseClient(UniversalDriver):
|
||||
"node_type": node.node_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:
|
||||
@@ -518,15 +546,25 @@ class BaseClient(UniversalDriver):
|
||||
raise ValueError("必须提供write_nodes参数")
|
||||
|
||||
def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
||||
if isinstance(write_nodes, list):
|
||||
# 处理节点列表
|
||||
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}")
|
||||
"""根据 _workflow_params 为各节点写入真实数值。
|
||||
|
||||
约定:
|
||||
- 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}")
|
||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||
result_str = self.write_node(input_json)
|
||||
@@ -538,13 +576,14 @@ class BaseClient(UniversalDriver):
|
||||
except Exception as e:
|
||||
print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}")
|
||||
elif isinstance(write_nodes, dict):
|
||||
# 处理节点字典,使用指定的值
|
||||
# 映射形式: 节点名 -> 参数名或常量
|
||||
for node_name, node_value in write_nodes.items():
|
||||
# 检查值是否是字符串类型的参数名
|
||||
current_value = 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]
|
||||
if isinstance(node_value, str) and node_value in params:
|
||||
current_value = params[node_value]
|
||||
print(f"初始化函数: 从参数获取值 {node_value} = {current_value}")
|
||||
else:
|
||||
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})
|
||||
@@ -672,20 +711,20 @@ class BaseClient(UniversalDriver):
|
||||
condition_nodes: 条件节点列表 [节点名1, 节点名2]
|
||||
"""
|
||||
def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
|
||||
# 直接处理写入节点
|
||||
"""开始函数: 写入触发节点, 然后轮询条件节点直到满足停止条件。"""
|
||||
|
||||
params = getattr(self, "_workflow_params", {}) or {}
|
||||
|
||||
# 先处理写入节点(触发位等)
|
||||
if write_nodes:
|
||||
if isinstance(write_nodes, list):
|
||||
# 处理节点列表,默认值都是True
|
||||
for i, node_name in enumerate(write_nodes):
|
||||
# 尝试获取与节点对应的参数值
|
||||
param_name = f"write_{i}"
|
||||
# 列表形式: 节点名与参数名一致, 若无参数则直接写 True
|
||||
for node_name in write_nodes:
|
||||
if node_name in params:
|
||||
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}")
|
||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||
result_str = self.write_node(input_json)
|
||||
@@ -697,14 +736,13 @@ class BaseClient(UniversalDriver):
|
||||
except Exception as e:
|
||||
print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}")
|
||||
elif isinstance(write_nodes, dict):
|
||||
# 处理节点字典,值是指定的
|
||||
# 字典形式: 节点名 -> 常量值(如 True/False)
|
||||
for node_name, node_value in write_nodes.items():
|
||||
# 尝试获取参数值(如果节点名与参数名匹配)
|
||||
current_value = node_value # 使用指定的默认值
|
||||
if hasattr(self, '_workflow_params') and node_name in self._workflow_params:
|
||||
current_value = self._workflow_params[node_name]
|
||||
if node_name in params:
|
||||
current_value = params[node_name]
|
||||
else:
|
||||
current_value = node_value
|
||||
|
||||
# 直接写入节点
|
||||
print(f"直接写入节点 {node_name} = {current_value}")
|
||||
input_json = json.dumps({"node_name": node_name, "value": current_value})
|
||||
result_str = self.write_node(input_json)
|
||||
@@ -732,6 +770,7 @@ class BaseClient(UniversalDriver):
|
||||
# 直接读取节点
|
||||
result_str = self.read_node(node_name)
|
||||
try:
|
||||
time.sleep(1)
|
||||
result_str = result_str.replace("'", '"')
|
||||
result_dict = json.loads(result_str)
|
||||
read_res = result_dict.get("value")
|
||||
@@ -1035,31 +1074,33 @@ class BaseClient(UniversalDriver):
|
||||
读取节点值的便捷方法
|
||||
返回包含result字段的字典
|
||||
"""
|
||||
try:
|
||||
node = self.use_node(node_name)
|
||||
value, error = node.read()
|
||||
# 使用锁保护客户端访问
|
||||
with self._client_lock:
|
||||
try:
|
||||
node = self.use_node(node_name)
|
||||
value, error = node.read()
|
||||
|
||||
# 创建结果字典
|
||||
result = {
|
||||
"value": value,
|
||||
"error": error,
|
||||
"node_name": node_name,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
# 创建结果字典
|
||||
result = {
|
||||
"value": value,
|
||||
"error": error,
|
||||
"node_name": node_name,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# 返回JSON字符串
|
||||
return json.dumps(result)
|
||||
except Exception as e:
|
||||
logger.error(f"读取节点 {node_name} 失败: {e}")
|
||||
# 创建错误结果字典
|
||||
result = {
|
||||
"value": None,
|
||||
"error": True,
|
||||
"node_name": node_name,
|
||||
"error_message": str(e),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
return json.dumps(result)
|
||||
# 返回JSON字符串
|
||||
return json.dumps(result)
|
||||
except Exception as e:
|
||||
logger.error(f"读取节点 {node_name} 失败: {e}")
|
||||
# 创建错误结果字典
|
||||
result = {
|
||||
"value": None,
|
||||
"error": True,
|
||||
"node_name": node_name,
|
||||
"error_message": str(e),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
return json.dumps(result)
|
||||
|
||||
def write_node(self, json_input: str) -> str:
|
||||
"""
|
||||
@@ -1068,47 +1109,49 @@ class BaseClient(UniversalDriver):
|
||||
eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}'
|
||||
返回JSON格式的字符串,包含操作结果
|
||||
"""
|
||||
try:
|
||||
# 解析JSON格式的输入
|
||||
if not isinstance(json_input, str):
|
||||
json_input = str(json_input)
|
||||
|
||||
# 使用锁保护客户端访问
|
||||
with self._client_lock:
|
||||
try:
|
||||
input_data = json.loads(json_input)
|
||||
if not isinstance(input_data, dict):
|
||||
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False})
|
||||
# 解析JSON格式的输入
|
||||
if not isinstance(json_input, str):
|
||||
json_input = str(json_input)
|
||||
|
||||
# 从JSON中提取节点名称和值
|
||||
node_name = input_data.get("node_name")
|
||||
value = input_data.get("value")
|
||||
try:
|
||||
input_data = json.loads(json_input)
|
||||
if not isinstance(input_data, dict):
|
||||
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False})
|
||||
|
||||
if node_name is None:
|
||||
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})
|
||||
# 从JSON中提取节点名称和值
|
||||
node_name = input_data.get("node_name")
|
||||
value = input_data.get("value")
|
||||
|
||||
node = self.use_node(node_name)
|
||||
error = node.write(value)
|
||||
if node_name is None:
|
||||
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})
|
||||
|
||||
# 创建结果字典
|
||||
result = {
|
||||
"value": value,
|
||||
"error": error,
|
||||
"node_name": node_name,
|
||||
"timestamp": time.time(),
|
||||
"success": not error
|
||||
}
|
||||
node = self.use_node(node_name)
|
||||
error = node.write(value)
|
||||
|
||||
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)
|
||||
# 创建结果字典
|
||||
result = {
|
||||
"value": value,
|
||||
"error": error,
|
||||
"node_name": node_name,
|
||||
"timestamp": time.time(),
|
||||
"success": not error
|
||||
}
|
||||
|
||||
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]:
|
||||
"""
|
||||
@@ -1150,16 +1193,27 @@ class OpcUaClient(BaseClient):
|
||||
self._refresh_running = False
|
||||
self._refresh_thread = None
|
||||
|
||||
# 添加线程锁,保护OPC UA客户端的并发访问
|
||||
import threading
|
||||
self._client_lock = threading.RLock()
|
||||
|
||||
# 如果提供了配置文件路径,则加载配置并注册工作流
|
||||
if config_path:
|
||||
self.load_config(config_path)
|
||||
|
||||
# 启动节点值刷新线程
|
||||
self.start_node_refresh()
|
||||
# 延迟启动节点值刷新线程,确保节点查找完成
|
||||
# 注意:刷新线程会在所有节点注册后由load_config调用
|
||||
# 暂时不在这里启动,避免在节点未找到时就开始刷新
|
||||
# self.start_node_refresh()
|
||||
|
||||
def _register_nodes_as_attributes(self):
|
||||
"""将所有节点注册为实例属性,可以通过self.node_name访问"""
|
||||
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)
|
||||
if eng_name:
|
||||
@@ -1182,7 +1236,7 @@ class OpcUaClient(BaseClient):
|
||||
|
||||
# 使用property装饰器将方法注册为类属性
|
||||
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):
|
||||
"""刷新所有节点的值到缓存"""
|
||||
@@ -1190,22 +1244,29 @@ class OpcUaClient(BaseClient):
|
||||
logger.warning("客户端未初始化,无法刷新节点值")
|
||||
return
|
||||
|
||||
try:
|
||||
# 简单检查连接状态,如果不连接会抛出异常
|
||||
self.client.get_namespace_array()
|
||||
except Exception as e:
|
||||
logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
|
||||
return
|
||||
|
||||
for node_name, node in self._node_registry.items():
|
||||
# 使用锁保护客户端访问
|
||||
with self._client_lock:
|
||||
try:
|
||||
if hasattr(node, 'read'):
|
||||
value, error = node.read()
|
||||
if not error:
|
||||
self._node_values[node_name] = value
|
||||
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
|
||||
# 简单检查连接状态,如果不连接会抛出异常
|
||||
self.client.get_namespace_array()
|
||||
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):
|
||||
"""获取节点值,支持中文名和英文名"""
|
||||
@@ -1303,6 +1364,11 @@ class OpcUaClient(BaseClient):
|
||||
# 直接使用字典
|
||||
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:
|
||||
# 直接传递字典列表
|
||||
@@ -1310,14 +1376,114 @@ class OpcUaClient(BaseClient):
|
||||
# 将工作流注册为实例方法
|
||||
self.register_workflows_as_methods()
|
||||
|
||||
# 将所有节点注册为属性
|
||||
# 将所有节点注册为属性(只注册已找到的节点)
|
||||
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} 加载配置")
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
||||
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):
|
||||
# 停止刷新线程
|
||||
self.stop_node_refresh()
|
||||
@@ -1338,8 +1504,8 @@ if __name__ == '__main__':
|
||||
# 创建OPC UA客户端并加载配置
|
||||
try:
|
||||
client = OpcUaClient(
|
||||
url="opc.tcp://localhost:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址
|
||||
config_path=config_path # 传入配置文件路径
|
||||
url="opc.tcp://192.168.1.88:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址
|
||||
config_path="D:\\Uni-Lab-OS\\unilabos\\device_comms\\opcua_client\\opcua_huairou.json" # 传入配置文件路径
|
||||
)
|
||||
|
||||
# 列出所有已注册的工作流
|
||||
@@ -1349,13 +1515,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 测试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")
|
||||
reaction_tank = client.get_node_value("reaction_tank_number")
|
||||
raw_tank = client.get_node_value("raw_tank_number")
|
||||
|
||||
print(f"\n执行后状态检查 (使用英文节点名):")
|
||||
print(f" - 抓取完成状态: {grab_complete}")
|
||||
print(f" - 当前反应罐号码: {reaction_tank}")
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
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
|
||||
|
||||
|
||||
@@ -47,23 +47,68 @@ class Base(ABC):
|
||||
def _get_node(self) -> Node:
|
||||
if self._node is None:
|
||||
try:
|
||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
||||
if "NumericNodeId" in self._node_id:
|
||||
# 从字符串中提取命名空间和标识符
|
||||
import re
|
||||
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
|
||||
if match:
|
||||
ns = int(match.group(1))
|
||||
identifier = int(match.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._node = self._client.get_node(node_id)
|
||||
# 尝试多种 NodeId 字符串格式解析,兼容不同服务器/库的输出
|
||||
# 可能的格式示例: 'ns=2;i=1234', 'ns=2;s=SomeString',
|
||||
# 'StringNodeId(ns=4;s=OPC|变量名)', 'NumericNodeId(ns=2;i=1234)' 等
|
||||
import re
|
||||
|
||||
nid = self._node_id
|
||||
# 如果已经是 NodeId/Node 对象(库用户可能传入),直接使用
|
||||
try:
|
||||
from opcua.ua import NodeId as UaNodeId
|
||||
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:
|
||||
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:
|
||||
# 直接使用节点ID字符串
|
||||
# 非字符串,尝试直接使用
|
||||
self._node = self._client.get_node(self._node_id)
|
||||
except Exception as e:
|
||||
print(f"获取节点失败: {self._node_id}, 错误: {e}")
|
||||
# 添加额外提示,帮助定位 BadNodeIdUnknown 问题
|
||||
print("提示: 请确认该 node_id 是否来自当前连接的服务器地址空间," \
|
||||
"以及 CSV/配置中名称与服务器 BrowseName 是否匹配。")
|
||||
raise
|
||||
return self._node
|
||||
|
||||
@@ -104,7 +149,56 @@ class Variable(Base):
|
||||
|
||||
def write(self, value: Any) -> bool:
|
||||
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
|
||||
except Exception as e:
|
||||
print(f"写入变量 {self._name} 失败: {e}")
|
||||
@@ -120,20 +214,50 @@ class Method(Base):
|
||||
def _get_parent_node(self) -> Node:
|
||||
if self._parent_node is None:
|
||||
try:
|
||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
||||
if "NumericNodeId" in self._parent_node_id:
|
||||
# 从字符串中提取命名空间和标识符
|
||||
import re
|
||||
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
|
||||
if match:
|
||||
ns = int(match.group(1))
|
||||
identifier = int(match.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._parent_node = self._client.get_node(node_id)
|
||||
# 处理父节点ID,使用与_get_node相同的解析逻辑
|
||||
import re
|
||||
|
||||
nid = self._parent_node_id
|
||||
|
||||
# 如果已经是 NodeId 对象,直接使用
|
||||
try:
|
||||
from opcua.ua import NodeId as UaNodeId
|
||||
if isinstance(nid, UaNodeId):
|
||||
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:
|
||||
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:
|
||||
# 直接使用节点ID字符串
|
||||
self._parent_node = self._client.get_node(self._parent_node_id)
|
||||
except Exception as e:
|
||||
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
|
||||
|
||||
Reference in New Issue
Block a user