这个方法的思路和主要代码由GPT生成。
需要安装 “node-red-contrib-opcua” 包来使用 OPC UA Browser。
在这个示例流程中,我们使用了以下节点:
- Inject 节点(用来触发流程开始):设置为每个流程启动时触发。
- OPC UA Browser 节点(用于获取目录信息):配置 OPC UA 服务器连接和目录路径。
- Function 节点(用于处理目录信息):用来解析 OPC UA Browser 返回的目录信息,并将标签名存储到一个数组中。
- Delay 节点(用于延迟发送标签组名):将标签组名发送到 OPC UA Browser 之前添加一些延迟。
- Debug 节点(用于调试输出):将标签名和标签组名输出到调试面板。
根据上边的流程截图创建流程,
Set browse address 按照图片样式设置一个需要办理访问的目录。
以下是 Function 节点 Decode & filter 的代码(用于解析目录信息并存储标签名):
//这段代码根据GPT生成的代码修改得到的。
// 定义两个空数组,用于存储不同类型的nodeId
var X_nodeIds = flow.get('X_nodeIds1') || [];
var nodeIds = flow.get('nodeIds1') || [];
// 获取消息中的节点数组
var items = msg.payload;
// 遍历节点数组中的每一个节点
for (var i = 0; i < items.length; i++) {
var item = items[i];
var ref = item.item;
var nodeClass = ref.nodeClass;
var typeDef = ref.typeDefinition;
var bname = ref.browseName;
var ns = bname.namespaceIndex;
var name = bname.name;
var nodeId = ref.nodeId;
var NodeId_str = nodeId.toString();
// 检查并跳过不满足条件的nodeId
if (NodeId_str.includes('._') || NodeId_str.match(/^.*=_/) || NodeId_str.match('.V4') || NodeId_str.match('.@')) {
continue;
}
// 根据不同的条件创建并发送消息
if (ns === 2) {
if (nodeClass === 1) { // 条件一:命名空间索引为2且nodeClass为1的节点
var newmsgX = { topic: nodeId, payload: "", actiontype: 'browse' };
// 发送第一个类型的消息
node.send([null, newmsgX]);
X_nodeIds.push(nodeId);
} else if (nodeClass === 2) { // 条件二:命名空间索引为2且nodeClass为2的节点
var newmsgY = { topic: nodeId, payload: "", actiontype: 'browse' };
// 发送第二个类型的消息
node.send([newmsgY, null]);
nodeIds.push(nodeId);
}
}
}
// 将更新后的数组存储回flow上下文
flow.set('X_nodeIds1', X_nodeIds);
flow.set('nodeIds1', nodeIds);
// 不返回任何消息,直接通过send方法发送
return null;
请根据实际情况修改 OPC UA Browser 节点和 Delay 节点的配置,部署这个流程后。每次运行流程时,它都会遍历整个目录,将标签名保存在数组中,并将标签组名发送回 OPC UA Browser 节点进行下一次遍历。
以下是 Function 节点 (清除所有 flow 范围内的变量) 的代码(用于清除所有 flow 范围内的变量),这段代码由GPT生成:
// 清除所有 flow 范围内的变量
var keys = flow.keys(); // 获取所有存储的 keys
for (var i = 0; i < keys.length; i++) { // 遍历所有 keys
flow.set(keys[i], undefined); // 将每个 key 对应的值设置为 undefined
}
// 验证是否清除成功
var clearedKeys = flow.keys(); // 再次获取所有存储的 keys
if (clearedKeys.length === 0) {
node.warn("Flow context successfully cleared."); // 输出成功信息
msg.payload = "Flow context successfully cleared."; // 设置 msg.payload 为成功信息
} else {
node.warn("Failed to clear flow context. Remaining keys: " + clearedKeys); // 输出失败信息和剩余的 keys
msg.payload = "Failed to clear flow context. Remaining keys: " + clearedKeys; // 设置 msg.payload 为失败信息和剩余的 keys
}
return msg; // 返回 msg
以下是 Function 节点 (Show Final Node IDs) 的代码(使用Debug输出显示所有标签名),这段代码由GPT生成:
// 获取 flow 上下文中的 nodeIds 数组
var finalNodeIds = flow.get('nodeIds1') || [];
// 将最终的 nodeIds 数组赋值给 msg.payload
msg.payload = finalNodeIds;
// 返回 msg 以显示最终的 nodeIds 数组
// return msg;
//这段代码本来到 return msg; 就结束了,但是当 数组长度超过1000时,Debug显示不出超过 1000 的元素,又增加了下边的代码改为一次最多输出1000个元素,临时解决了问题。
// // 获取要发送的数组
// const finalNodeIds = msg.payload;
// 设置每次发送的元素数量
const chunkSize = 1000;
// 初始化起始索引和结束索引
let startIndex = 0;
let endIndex = chunkSize;
// 循环发送所有元素
while (startIndex < finalNodeIds.length) {
// 获取当前批次的元素
const chunk = finalNodeIds.slice(startIndex, endIndex);
// 创建新的消息,并将当前批次的元素作为有效载荷发送
node.send({
payload: chunk
});
// 更新起始索引和结束索引,指向下一个批次
startIndex += chunkSize;
endIndex += chunkSize;
}
最后,这是这个流程导出的JSON,可以直接在Node-red中尝试导入。
[
{
"id": "28c02a8fd3c52bbf",
"type": "OpcUa-Browser",
"z": "bc19e3cc51a9a215",
"endpoint": "bfd4fb1d3827c759",
"item": "",
"datatype": "",
"topic": "",
"items": [],
"name": "",
"x": 530,
"y": 60,
"wires": [
[
"02fb5a0d042755b9",
"33625a091b088e5a"
]
]
},
{
"id": "e2cf37bc11c2ee19",
"type": "inject",
"z": "bc19e3cc51a9a215",
"name": "Browse",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "",
"topic": "",
"payload": "",
"payloadType": "str",
"x": 100,
"y": 60,
"wires": [
[
"c209a0a4da4b3a2e",
"f736f71918e2182c"
]
]
},
{
"id": "c209a0a4da4b3a2e",
"type": "function",
"z": "bc19e3cc51a9a215",
"name": "Set browse address",
"func": "// msg.topic='ns=2;s=DL_S1.D1';\nmsg.topic='ns=0;i=85';\nmsg.actiontype='browse';\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 280,
"y": 60,
"wires": [
[
"28c02a8fd3c52bbf"
]
]
},
{
"id": "b9012b8c7084df40",
"type": "debug",
"z": "bc19e3cc51a9a215",
"name": "目录debug 54",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 820,
"y": 260,
"wires": []
},
{
"id": "1bbfe2859a040f28",
"type": "delay",
"z": "bc19e3cc51a9a215",
"name": "",
"pauseType": "rate",
"timeout": "10",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "5",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 680,
"y": 180,
"wires": [
[
"28c02a8fd3c52bbf",
"b9012b8c7084df40"
]
]
},
{
"id": "02fb5a0d042755b9",
"type": "debug",
"z": "bc19e3cc51a9a215",
"name": "debug 55",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 820,
"y": 60,
"wires": []
},
{
"id": "724b8fae9ad64826",
"type": "debug",
"z": "bc19e3cc51a9a215",
"name": "变量debug 56",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 840,
"y": 100,
"wires": []
},
{
"id": "33625a091b088e5a",
"type": "delay",
"z": "bc19e3cc51a9a215",
"name": "",
"pauseType": "rate",
"timeout": "10",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 310,
"y": 180,
"wires": [
[
"e204381af2d34de2"
]
]
},
{
"id": "e204381af2d34de2",
"type": "function",
"z": "bc19e3cc51a9a215",
"name": "Decode & filter",
"func": "// 定义两个空数组,用于存储不同类型的nodeId\nvar X_nodeIds = flow.get('X_nodeIds1') || [];\nvar nodeIds = flow.get('nodeIds1') || [];\n\n// 获取消息中的节点数组\nvar items = msg.payload;\n\n// 遍历节点数组中的每一个节点\nfor (var i = 0; i < items.length; i++) {\n var item = items[i];\n var ref = item.item;\n var nodeClass = ref.nodeClass;\n var typeDef = ref.typeDefinition;\n var bname = ref.browseName;\n var ns = bname.namespaceIndex;\n var name = bname.name;\n var nodeId = ref.nodeId;\n var NodeId_str = nodeId.toString();\n\n // 检查并跳过不满足条件的nodeId\n if (NodeId_str.includes('._') || NodeId_str.match(/^.*=_/) || NodeId_str.match('.V4') || NodeId_str.match('.@')) {\n continue;\n }\n\n // 根据不同的条件创建并发送消息\n if (ns === 2) {\n if (nodeClass === 1) { // 条件一:命名空间索引为2且nodeClass为1的节点\n var newmsgX = { topic: nodeId, payload: \"\", actiontype: 'browse' };\n // 发送第一个类型的消息\n node.send([null, newmsgX]);\n X_nodeIds.push(nodeId);\n } else if (nodeClass === 2) { // 条件二:命名空间索引为2且nodeClass为2的节点\n var newmsgY = { topic: nodeId, payload: \"\", actiontype: 'browse' };\n // 发送第二个类型的消息\n node.send([newmsgY, null]);\n nodeIds.push(nodeId);\n }\n }\n}\n\n// 将更新后的数组存储回flow上下文\nflow.set('X_nodeIds1', X_nodeIds);\nflow.set('nodeIds1', nodeIds);\n\n// 不返回任何消息,直接通过send方法发送\nreturn null;",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 180,
"wires": [
[
"724b8fae9ad64826"
],
[
"1bbfe2859a040f28"
]
]
},
{
"id": "a14762e716eb88fd",
"type": "inject",
"z": "bc19e3cc51a9a215",
"name": "Show Final IDs",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 320,
"wires": [
[
"e656c50f0dcef8aa"
]
]
},
{
"id": "e656c50f0dcef8aa",
"type": "function",
"z": "bc19e3cc51a9a215",
"name": "Show Final Node IDs",
"func": "// 获取 flow 上下文中的 nodeIds 数组\nvar finalNodeIds = flow.get('nodeIds1') || [];\n// 将最终的 nodeIds 数组赋值给 msg.payload\nmsg.payload = finalNodeIds;\n// 返回 msg 以显示最终的 nodeIds 数组\n// return msg;\n\n\n// // 获取要发送的数组\n// const finalNodeIds = msg.payload;\n\n // 设置每次发送的元素数量\n const chunkSize = 1000;\n\n // 初始化起始索引和结束索引\n let startIndex = 0;\n let endIndex = chunkSize;\n\n // 循环发送所有元素\n while (startIndex < finalNodeIds.length) {\n // 获取当前批次的元素\n const chunk = finalNodeIds.slice(startIndex, endIndex);\n\n // 创建新的消息,并将当前批次的元素作为有效载荷发送\n node.send({\n payload: chunk\n });\n\n // 更新起始索引和结束索引,指向下一个批次\n startIndex += chunkSize;\n endIndex += chunkSize;\n }\n\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 320,
"wires": [
[
"3fbf36f8f6fdfc80"
]
]
},
{
"id": "3fbf36f8f6fdfc80",
"type": "debug",
"z": "bc19e3cc51a9a215",
"name": "变量地址",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 700,
"y": 320,
"wires": []
},
{
"id": "f736f71918e2182c",
"type": "function",
"z": "bc19e3cc51a9a215",
"name": "清除所有 flow 范围内的变量",
"func": "// 清除所有 flow 范围内的变量\nvar keys = flow.keys(); // 获取所有存储的 keys\nfor (var i = 0; i < keys.length; i++) { // 遍历所有 keys\n flow.set(keys[i], undefined); // 将每个 key 对应的值设置为 undefined\n}\n\n// 验证是否清除成功\nvar clearedKeys = flow.keys(); // 再次获取所有存储的 keys\nif (clearedKeys.length === 0) {\n node.warn(\"Flow context successfully cleared.\"); // 输出成功信息\n msg.payload = \"Flow context successfully cleared.\"; // 设置 msg.payload 为成功信息\n} else {\n node.warn(\"Failed to clear flow context. Remaining keys: \" + clearedKeys); // 输出失败信息和剩余的 keys\n msg.payload = \"Failed to clear flow context. Remaining keys: \" + clearedKeys; // 设置 msg.payload 为失败信息和剩余的 keys\n}\n\nreturn msg; // 返回 msg",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 360,
"y": 280,
"wires": [
[]
]
},
{
"id": "b7e1628c12f76bda",
"type": "inject",
"z": "bc19e3cc51a9a215",
"name": "清理",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 90,
"y": 280,
"wires": [
[
"f736f71918e2182c"
]
]
},
{
"id": "bfd4fb1d3827c759",
"type": "OpcUa-Endpoint",
"endpoint": "opc.tcp://SJ-KEPSer-01:49320",
"secpol": "None",
"secmode": "None",
"none": true,
"login": false,
"usercert": false,
"usercertificate": "",
"userprivatekey": ""
}
]
有一个问题没有解决,就是第一次点击部署,程序就开始运行,但是会出想非常多的重复标签,等程序运行完成后再次点击inject节点Browse,这次运行的结果是根预期的一样。
不知道是哪里的问题