Node-OPCUA 入门(2)-创建一个简单的opcua客户端

这个例子中,我们创建一个OPCUA 客户端来监视服务器上的一个变量。服务器已在上一个教程中创建。

注意:这个示例使用的是旧的回调方法,它已经被async/await方法所取代,后边我们会使用typescript重写。

1.0 下面为客户端创建一个项目:

bash 复制代码
$ mkdir sample_client
$ cd sample_client
$ npm init                      # creates a package.json
$ npm install node-opcua --save
$ npm install async --save

2.0 创建并编辑示例文件sample_client.js

2.1 引入需要的组件
javascript 复制代码
const { OPCUAClient, makeBrowsePath, AttributeIds, resolveNodeId, TimestampsToReturn} = require("node-opcua");
const async = require("async");
2.2 客户端实例化

要连接到服务器,客户端必须指定服务器的确切URL,包括主机名、端口和路径。

javascript 复制代码
// const endpointUrl = "opc.tcp://<hostname>:4334/UA/MyLittleServer";
const endpointUrl = "opc.tcp://" + require("os").hostname() + ":4334/UA/MyLittleServer";

其中应替换为运行服务器的机器的计算机名或完全合格的域名。UA/MyLittleServer是由服务器定义的端点的路径,也必须被该服务器上的现有端点路径替换。

bash 复制代码
const client = OPCUAClient.create({
    endpointMustExist: false
});

注意,在默认情况下,endpointUrl必须匹配服务器公开的url,这意味着不能用主机名别名或直接的ip地址替换。要放松这个限制,可以在创建OPCUA 客户端时使用endpointMustExist: false选项。

下面,添加一些帮助程序来诊断连接问题。通过添加一个对"backoff"事件进行处理的程序,可以了解连接的进度。当客户端连接到服务器失败时,将引发"backoff"事件,并指示稍后将重试。

javascript 复制代码
client.on("backoff", (retry, delay) =>
    console.log(
"正在努力连接到 ", 
endpointUrl,
 ": 重试 =", 
retry,
 "下一次重试 ",
 delay / 1000,
 "秒")
);
2.3 设置一系列异步操作

我们先创建一个框架,使用占位符替代实际功能,设置客户端整个生命周期的工作过程。async.series函数将按照任务定义的顺序执行所有任务,因此我们可以假设在创建会话之前已经建立了连接。所有任务完成后,客户端将断开连接。

javascript 复制代码
let the_session, the_subscription;

async.series([
    // 第1步 : 连接到
    function(callback)  {
        _"连接"
    },
    // 第2步 : 创建会话
    function(callback) {
        _"创建会话"
    },
    // 第3步 : 浏览
    function(callback) {
       _"浏览根文件夹"
    },
    // 第4步 : 读取变量
    function(callback) {
       _"通过字面量参数读取变量"
    },
    function(callback) {
       _"通过变量参数读取变量"
    },

    // 第5步: 配置一个订阅,添加一个间隔10秒的监视项
    function(callback) {
       _"配置一个订阅"
    },
    function(callback) {
       _"添加监视项"
    },
    function(callback) {
        // 等待10秒
        setTimeout(()=>callback(), 10*1000);
    },
    // 停止会话
    function(callback) {
        _"停止会话";
    },
    // 关闭会话
    function(callback) {
        _"关闭会话"
    }
],
function(err) {
    if (err) {
        console.log(" failure ",err);
    } else {
        console.log("成功!");
    }
    client.disconnect(function(){});
}) ;

2.3.1 连接

javascript 复制代码
client.connect(
endpointUrl, function (err) {
    if (err) {
       console.log(" 无法连接到端点 :", endpointUrl);
         } else {
         console.log("连接成功 !");
            }
          callback(err);
        });
** 2.3.2 创建会话**
javascript 复制代码
client.createSession(function (err, session) {
    if (err) {
         return callback(err);
             }
    the_session = session;
    callback();
     });
2.3.3 浏览根文件夹

我们可以浏览RootFolder来接收它所有子节点的列表。通过browseResult的引用对象,我们能够访问所有属性。让我们打印所有节点的browseName。

javascript 复制代码
  the_session.browse("RootFolder", function (err, browseResult) {
       if (!err) {
         console.log("浏览根文件夹: ");
         for (let reference of browseResult.references) {
         console.log(reference.browseName.toString(), reference.nodeId.toString());
                }
                 }
     callback(err);
                    });
2.3.4 读取变量

可以通过read函数直接使用变量的nodeId访问变量的值。

javascript 复制代码
 the_session.read({ nodeId: "ns=1;s=free_memory", attributeId: AttributeIds.Value }, (err, dataValue) => {
                if (!err) {
                    console.log(" 空闲内存 % = ", dataValue.toString());
                }
                callback(err);
            });

我们也可以构造一个带有nodeId和attributeId两个参数的nodeToRead对象,以告诉读取函数我们希望它做什么。前者告诉它确切的节点,后者告诉它我们想要获得哪个属性。在AttributeIds对象中枚举了SDK提供的可能值。每个字段都包含OPC-UA兼容的AttributeId,由OPC-UA标准定义。

javascript 复制代码
 const maxAge = 0;
 const nodeToRead = {
       nodeId: "ns=1;s=free_memory",
       attributeId: AttributeIds.Value
            };
the_session.read(nodeToRead, maxAge, function (err, dataValue) {
   if (!err) {
     console.log(" 空闲内存 % = ", dataValue.toString());
              }
     callback(err);
            });
2.3.5 配置一个订阅

OPC-UA允许订阅它的对象,而不是轮询。您将使用一个参数对象从the_session创建一个订阅。接下来,您将定义订阅结束的超时,并钩入几个订阅事件,如"started"。在定义实际的监视对象时,再次使用要监视的nodeId和attributeId。监视器对象再次允许与它的事件系统挂钩。

javascript 复制代码
const subscriptionOptions = {
                maxNotificationsPerPublish: 1000,
                publishingEnabled: true,
                requestedLifetimeCount: 100,
                requestedMaxKeepAliveCount: 10,
                requestedPublishingInterval: 1000
            };
            the_session.createSubscription2(subscriptionOptions, (err, subscription) => {
                if (err) {
                    return callback(err);
                }
                the_subscription = subscription;
                the_subscription
                    .on("started", () => {
                        console.log("订阅开始2秒 - subscriptionId=", the_subscription.subscriptionId);
                    })
                    .on("keepalive", function () {
                        console.log("订阅激活");
                    })
                    .on("terminated", function () {
                        console.log("终止");
                    });
                callback();
            });

添加一些监视项

javascript 复制代码
// 添加监视项
const itemToMonitor = {
    nodeId: resolveNodeId("ns=1;s=free_memory"),
    attributeId: AttributeIds.Value
          };
const monitoringParamaters = {
         samplingInterval: 100,
         discardOldest: true,
         queueSize: 10
            };
the_subscription.monitor(
    itemToMonitor,
    monitoringParamaters,
    TimestampsToReturn.Both,
    (err, monitoredItem) => {
        monitoredItem.on("changed", function (dataValue) {
            console.log(
                "监视项改变:  % 空闲内存 = ",
                 dataValue.value.value
                );
               }
            );
          callback();
            });
console.log("-------------------------------------");
2.3.6 停止订阅
javascript 复制代码
the_subscription.terminate(callback);
2.3.7 关闭会话
javascript 复制代码
the_session.close(function (err) {
  if (err) {
       console.log("关闭会话出错 ?");
            }
  callback();
     });
3.0 运行客户端

在命令使用下面的命令启动客户端(服务器已在这之前启动,并测试过了)

$ node sample_client.js

控制台打印下面的信息,客户端运行成功。

Plain 复制代码
PS E:\node-opcua-example\opcua-client> node .\sample_client.js
连接成功 !
浏览根文件夹: 
Objects ns=0;i=85
Types ns=0;i=86
Views ns=0;i=87
 空闲内存 % =  { /* DataValue */
   value: Variant(Scalar<Double>, value: 52.204908576305556)
   statusCode:      Good (0x00000000)
   serverTimestamp: 2022-09-09T03:28:42.395Z $ 873.800.000
   sourceTimestamp: 2022-09-09T03:28:42.395Z $ 760.300.000
}
 free mem % =  { /* DataValue */
   value: Variant(Scalar<Double>, value: 52.205079710763215)
   statusCode:      Good (0x00000000)
   serverTimestamp: 2022-09-09T03:28:42.399Z $ 315.000.000
   sourceTimestamp: 2022-09-09T03:28:42.399Z $ 215.500.000
}
终止
成功!
4.0 最后附上,示例sample_client.js文件的完整代码
javascript 复制代码
const { hostname } = require("os");
const async = require("async");
const { OPCUAClient, makeBrowsePath, AttributeIds, resolveNodeId, TimestampsToReturn } = require("node-opcua");

// const endpointUrl = "opc.tcp://<hostname>:4334/UA/MyLittleServer";
const endpointUrl = "opc.tcp://" + hostname() + ":4334/UA/MyLittleServer";
const client = OPCUAClient.create({
    endpointMustExist: false
});
client.on("backoff", (retry, delay) =>
    console.log("正在努力连接到 ", endpointUrl, ": 重试 =", retry, "下一次重试 ", delay / 1000, "秒")
);

let the_session, the_subscription;

async.series(
    [
        // 第1步 : 连接
        function (callback) {
            client.connect(endpointUrl, function (err) {
                if (err) {
                    console.log(" 无法连接到端点 :", endpointUrl);
                } else {
                    console.log("连接成功 !");
                }
                callback(err);
            });
        },

        // 第2步 : 创建会话
        function (callback) {
            client.createSession(function (err, session) {
                if (err) {
                    return callback(err);
                }
                the_session = session;
                callback();
            });
        },

        // 第3步: 浏览
        function (callback) {
            the_session.browse("RootFolder", function (err, browseResult) {
                if (!err) {
                    console.log("浏览根文件夹: ");
                    for (let reference of browseResult.references) {
                        console.log(reference.browseName.toString(), reference.nodeId.toString());
                    }
                }
                callback(err);
            });
        },

        // 第4步 : 读取变量
        function (callback) {
            the_session.read({ nodeId: "ns=1;s=free_memory", attributeId: AttributeIds.Value }, (err, dataValue) => {
                if (!err) {
                    console.log(" 空闲内存 % = ", dataValue.toString());
                }
                callback(err);
            });
        },

        // 读取变量
        function (callback) {
            const maxAge = 0;
            const nodeToRead = {
                nodeId: "ns=1;s=free_memory",
                attributeId: AttributeIds.Value
            };

            the_session.read(nodeToRead, maxAge, function (err, dataValue) {
                if (!err) {
                    console.log(" 空闲内存 % = ", dataValue.toString());
                }
                callback(err);
            });
        },

        // 第5步: 设置订阅,添加周期10秒的监视项
        function (callback) {
            const subscriptionOptions = {
                maxNotificationsPerPublish: 1000,
                publishingEnabled: true,
                requestedLifetimeCount: 100,
                requestedMaxKeepAliveCount: 10,
                requestedPublishingInterval: 1000
            };
            the_session.createSubscription2(subscriptionOptions, (err, subscription) => {
                if (err) {
                    return callback(err);
                }

                the_subscription = subscription;

                the_subscription
                    .on("started", () => {
                        console.log("订阅开始2秒 - subscriptionId=", the_subscription.subscriptionId);
                    })
                    .on("keepalive", function () {
                        console.log("订阅激活");
                    })
                    .on("terminated", function () {
                        console.log("终止");
                    });
                callback();
            });
        },
        function (callback) {
            // 添加监视项
            const itemToMonitor = {
                nodeId: resolveNodeId("ns=1;s=free_memory"),
                attributeId: AttributeIds.Value
            };
            const monitoringParamaters = {
                samplingInterval: 100,
                discardOldest: true,
                queueSize: 10
            };

            the_subscription.monitor(itemToMonitor, monitoringParamaters, TimestampsToReturn.Both, (err, monitoredItem) => {
                monitoredItem.on("changed", function (dataValue) {
                    console.log("监视项改变:  % 空闲内存 = ", dataValue.value.value);
                });
                callback();
            });
            console.log("-------------------------------------");
        },
        function (callback) {
            // 等待一会儿 : 10 秒
            setTimeout(() => callback(), 10 * 1000);
        },
        // 终止订阅
        function (callback) {
            the_subscription.terminate(callback);
        },
        // 关闭会话
        function (callback) {
            the_session.close(function (err) {
                if (err) {
                    console.log("关闭会话出错 ?");
                }
                callback();
            });
        }
    ],
    function (err) {
        if (err) {
            console.log(" 错误 ", err);
        } else {
            console.log("成功!");
        }
        client.disconnect(function () {});
    }
);
相关推荐
长安牧笛2 小时前
开发课堂学生专注度分析程序,捕捉学生面部表情和动作,分析专注程度,帮助老师调整教学。
javascript
weixin_448119942 小时前
Datawhale Hello-Agents入门篇202512第2次作业
java·前端·javascript
BD_Marathon2 小时前
Vue3_事件渲染命令
开发语言·javascript·ecmascript
kaka-3332 小时前
微信小程序中使用 xlsx(xlsx.mini.min.js)实现 Excel 导入导出功能
javascript·微信小程序·excel
北冥有一鲲2 小时前
LangChain.js:Tool、Memory 与 Agent 的深度解析与实战
开发语言·javascript·langchain
霁月的小屋2 小时前
Vue响应式数据全解析:从Vue2到Vue3,ref与reactive的实战指南
前端·javascript·vue.js
小林rush3 小时前
uni-app跨分包自定义组件引用解决方案
前端·javascript·vue.js
亮子AI3 小时前
【Svelte】怎样实现一个图片上传功能?
开发语言·前端·javascript·svelte
心.c3 小时前
为什么在 Vue 3 中 uni.createCanvasContext 画不出图?
前端·javascript·vue.js