node-opcua:读写操作与连接问题解决方案

在之前的项目中,我使用了 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 库的使用,并解决了一些常见的连接和数据读取写入问题。如果你在使用过程中遇到类似的问题,希望本文能够为你提供一些帮助。

如果有任何问题或想要深入了解的内容,欢迎留言讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值