这个例子中,我们创建一个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 () {});
}
);