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 () {});
    }
);
相关推荐
阿蒙Amon3 小时前
TypeScript学习-第7章:泛型(Generic)
javascript·学习·typescript
睡美人的小仙女1273 小时前
Threejs加载环境贴图报错Bad File Format: bad initial token
开发语言·javascript·redis
fanruitian3 小时前
uniapp android开发 测试板本与发行版本
前端·javascript·uni-app
摘星编程4 小时前
React Native + OpenHarmony:Timeline垂直时间轴
javascript·react native·react.js
2501_944525545 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
jin1233225 小时前
React Native鸿蒙跨平台完成剧本杀组队详情页面,可以复用桌游、团建、赛事等各类组队详情页开发
javascript·react native·react.js·ecmascript·harmonyos
经年未远6 小时前
vue3中实现耳机和扬声器切换方案
javascript·学习·vue
刘一说6 小时前
Vue 组件不必要的重新渲染问题解析:为什么子组件总在“无故”刷新?
前端·javascript·vue.js
可触的未来,发芽的智生7 小时前
狂想:为AGI代称造字ta,《第三类智慧存在,神的赐名》
javascript·人工智能·python·神经网络·程序人生
徐同保7 小时前
React useRef 完全指南:在异步回调中访问最新的 props/state引言
前端·javascript·react.js