在之前的项目中,我使用了 open62541 来实现基于 C++ 的 OPC UA 协议通信。除了配置稍显麻烦外,整体的使用体验还是相当不错的。然而,随着项目需求的变化,通信协议切换到 Node.js。而这里我只需要npm安装相关的库(如 node-opcua
),我原本以为这将大大简化开发流程。
一开始确实如此,很顺利的实现了读取写入等基础操作,但是在我测试的时候发现这里遇到了一个连接bug,如果你在协议外面统一进行重连,那么恭喜你,你也会遇到这个问题,opcua通过外部重连的时候并不像我写的modbus啊,s7协议啊,可以直接重连,而是会直接卡住,似乎是node-opcua里面没有设置连接超时,导致你创建连接的时候他就会一直处于连接中....
一、连接
OPC UA 协议的连接参数需要设置得比较多,特别是它支持用户名密码验证。在我的项目中,我并没有启用这些身份验证功能,所以配置相对简单。不过,值得注意的是,node-opcua
确实提供了内部的重连机制。我记得可以通过设置一些重连参数来让它自己尝试重新连接,但我并没有尝试过。
const endpointUrl = "opc.tcp://localhost:4840"; // 根据你的服务端地址进行修改
const options = {
securityMode: "None", // 安全模式:'None', 'Sign', 'SignAndEncrypt'
securityPolicy: "None", // 安全策略:'None', 'Basic256', 'Basic128Rsa15' 等
};
const client = OPCUAClient.create(options});
如果你选择在外部统一进行重连,就会遇到我提到的问题。这个问题并没有什么特别好的解决办法,唯一的应对方式是添加一个超时判断。通过设置连接超时,比如当连接时间超过一定限制时,主动返回 false
,来避免程序卡住。
async connectWithTimeout(timeout = 5000) {
return new Promise((resolve, reject) => {
// 创建一个定时器来检测超时
const timeoutId = setTimeout(() => {
reject(new Error("Connection timeout exceeded"));
}, timeout);
this.client.connect(this.options.endpoint, (err) => {
// 清除超时定时器
clearTimeout(timeoutId);
if (err) {
reject(err);
} else {
resolve("Connected successfully");
}
});
});
}
这种方法解决了连接过程中的超时问题,但同样的超时问题也出现在创建会话。因此,建议将超时判断写在外部进行统一管理。
二、读取数据
在 node-opcua
中,读取数据并不是直接使用 client
来读取,而是通过 会话 来读取。以下是读取数据的示例代码:
// 创建会话
const session = await client.createSession();
async function readData(session) {
try {
// 设置要读取的节点ID (根据实际的服务器配置)
const nodeId = "ns=1;s=3"; // 替换为实际的节点ID
// 读取节点数据
const dataValue = await session.read({
nodeId: nodeId,
attributeId: AttributeIds.Value
});
// 打印返回的值
console.log("读取的值:", dataValue.value.value);
} catch (err) {
console.error("读取数据失败: ", err);
}
}
在 node-opcua
中,节点的 NodeId
通常由以下三部分组成:
IdentifierType:这指定了标识符的类型。它可以是以下几种之一:
-
NameSpaceIndex:表示节点标识符所属的命名空间的索引。每个命名空间在 OPC UA 中都有一个唯一的索引,通常用于区分不同的地址空间,一般是整数,这里用缩写ns表示
-
IdentifierType:这指定了标识符的类型,他有四种类型:
-
Numeric
:标识符是一个数字(通常表示预定义的节点),用i表示; -
String
:标识符是一个字符串(通常用于引用自定义节点),用s表示; -
GUID
:标识符是一个全局唯一标识符(GUID),用g表示; -
Opaque
:标识符是一个“透明”类型,通常用于专用的应用程序或设备,用o表示。
-
-
Identifier:这是实际的标识符值,根据不同的类型填写具体的值。
例如,NodeId
为 "ns=1;s=3"
,意味着节点的命名空间索引是 1
,标识符类型是 String
,标识符值是 "3"
。
三、写入数据
写入数据也是一样的,要用会话去写入,代码如下:
async function writeStringData(session) {
try {
const nodeId = "ns=1;s=9"; // 替换为实际的节点ID
const stringToWrite = "Hello OPC UA"; // 要写入的字符串
const dataValue = await session.read({
nodeId: nodeId,
attributeId: AttributeIds.Value
});
const dataTypeId = dataValue.value.dataType;
const dataTypeName = DataType[dataTypeId] ? DataType[dataTypeId] : "未知类型";
if(dataTypeName === "String"){
const type = DataType.String;
}
const statusCode = await session.write({
nodeId: nodeId,
attributeId: AttributeIds.Value,
value: {
value: {
dataType: DataType.String,
value: stringToWrite
}
}
});
console.log("写入状态:", statusCode._name);
} catch (err) {
console.error("写入字符串数据失败: ", err);
}
}
在写入数据时,必须明确指定数据类型。为了确保类型正确,我们可以先读取节点的数据类型,然后根据读取到的类型来进行相应的写入。为了方便使用,我整理了 OPC UA
支持的基础数据类型,如下:
// * OPC UA 基础数据类型(数字枚举)
// * 参考 OPC UA 规范 1.04
const DataType = {
// 基础类型
Boolean: 1, // 布尔值 true/false
SByte: 2, // 有符号8位整数 (-128~127)
Byte: 3, // 无符号8位整数 (0~255)
Int16: 4, // 有符号16位整数 (-32,768~32,767)
UInt16: 5, // 无符号16位整数 (0~65,535)
Int32: 6, // 有符号32位整数 (-2,147,483,648~2,147,483,647)
UInt32: 7, // 无符号32位整数 (0~4,294,967,295)
Int64: 8, // 有符号64位整数
UInt64: 9, // 无符号64位整数
Float: 10, // 单精度浮点数 (IEEE 754)
Double: 11, // 双精度浮点数 (IEEE 754)
String: 12, // UTF-8字符串
DateTime: 13, // 日期时间(UTC)
Guid: 14, // 全局唯一标识符 (16字节)
ByteString: 15, // 二进制数据(字节数组)
XmlElement: 16, // XML元素
NodeId: 17, // 节点标识符
ExpandedNodeId: 18, // 扩展节点标识符
// 服务相关类型
StatusCode: 19, // 操作状态码
QualifiedName: 20, // 带命名空间的名称
LocalizedText: 21, // 本地化文本
ExtensionObject: 22, // 扩展对象
DataValue: 23, // 数据值(含时间戳、状态等)
Variant: 24, // 可变类型容器
DiagnosticInfo: 25, // 诊断信息
// 复合类型
Structure: 256, // 结构体
Enumeration: 257, // 枚举
Number: 258, // 抽象数值类型
Integer: 259, // 抽象整数类型
UInteger: 260 // 抽象无符号整数
};
四、断开连接
断开连接的直接disconnect就可以了,但是要记得把会话也关闭。
// 关闭会话
await session.close();
console.log("会话已关闭!");
// 断开连接
await client.disconnect();
console.log("已断开与服务器的连接!");
四、测试
由于没有找到官方的 OPC UA 服务端,我使用了 open62541
库自己编写了一个 OPC UA 服务端进行测试。服务端的实现随机生成一些数据,以模拟实际的 OPC UA 服务器。
通过这次的迁移,主要是学习 node-opcua
库的使用,并解决了一些常见的连接和数据读取写入问题。如果你在使用过程中遇到类似的问题,希望本文能够为你提供一些帮助。
如果有任何问题或想要深入了解的内容,欢迎留言讨论!