Browse Source

add full node-opcua nodeid class (inkl. guid part)

Martin Kunz 1 year ago
parent
commit
57af560e0d
4 changed files with 6378 additions and 35 deletions
  1. 326 33
      src/ua/NodeId.ts
  2. 2 2
      src/ua/UAReference.ts
  3. 28 0
      src/ua/guid.ts
  4. 6022 0
      src/ua/opcua_node_ids.ts

+ 326 - 33
src/ua/NodeId.ts

@@ -1,11 +1,103 @@
+/**
+ * @module node-opcua-nodeid
+ */
+
 import { assert } from "@/util/assert";
 import { assert } from "@/util/assert";
+import { emptyGuid, isValidGuid, type Guid, normalizeGuid } from "./guid";
+import { DataTypeIds, MethodIds, ObjectIds, ObjectTypeIds, ReferenceTypeIds, VariableIds, VariableTypeIds } from "./opcua_node_ids";
+
+
+/**
+ * `NodeIdType` an enumeration that specifies the possible types of a `NodeId` value.
+ */
+export enum NodeIdType {
+    /**
+     * @static
+     * @property NUMERIC
+     * @default 0x1
+     */
+    NUMERIC = 0x01,
+    /**
+     * @static
+     * @property STRING
+     * @default 0x2
+     */
+    STRING = 0x02,
+    /**
+     * @static
+     * @property GUID
+     * @default 0x3
+     */
+    GUID = 0x03,
+    /**
+     * @static
+     * @property BYTESTRING
+     * @default 0x4
+     */
+    BYTESTRING = 0x04
+}
+/*function defaultValue(identifierType: NodeIdType.BYTESTRING): null;
+function defaultValue(identifierType: NodeIdType.STRING): null;
+function defaultValue(identifierType: NodeIdType.NUMERIC): 0;
+function defaultValue(identifierType: NodeIdType.GUID): null;
+*/
+function defaultValue(identifierType: NodeIdType): string | number | Buffer {
+    switch (identifierType) {
+        case NodeIdType.GUID: return emptyGuid;
+        case NodeIdType.BYTESTRING: return null as any as Buffer;// Buffer.alloc(0);
+        case NodeIdType.STRING: return "";
+        case NodeIdType.NUMERIC: return 0;
+    }
+}
+
+export interface INodeIdNumeric extends NodeId {
+    identifierType: NodeIdType.NUMERIC;
+    value: number;
+}
+export interface INodeIdGuid extends NodeId {
+    identifierType: NodeIdType.GUID;
+    value: string;
+}
+export interface INodeIdByteString extends NodeId {
+    identifierType: NodeIdType.BYTESTRING;
+    value: Buffer;
+}
+export interface INodeIdString extends NodeId {
+    identifierType: NodeIdType.STRING;
+    value: string;
+}
+export type INodeId = INodeIdNumeric | INodeIdGuid | INodeIdString | INodeIdByteString;
+
 
 
+/**
+ * Construct a node ID
+ *
+ * @class NodeId
+ * @example
+ *
+ * ``` javascript
+ *    const nodeId = new NodeId(NodeIdType.NUMERIC,123,1);
+ *    ```
+ * @constructor
+ */
+export class NodeId {
+    public static NodeIdType = NodeIdType;
+    public static nullNodeId: NodeId;
+    public static resolveNodeId: (a: string | NodeId) => NodeId;
+    public static sameNodeId: (n1: NodeId, n2: NodeId) => boolean;
 
 
-export class NodeId  {
     public identifierType: NodeIdType;
     public identifierType: NodeIdType;
-    public value: number | string;
+    public value: number | string | Buffer | Guid;
     public namespace: number;
     public namespace: number;
-    constructor(identifierType?: NodeIdType | null, value?: number | string, namespace?: number) {
+
+    /**
+     * @param identifierType   - the nodeID type
+     * @param value            - the node id value. The type of Value depends on identifierType.
+     * @param namespace        - the index of the related namespace (optional , default value = 0 )
+     */
+    constructor(identifierType?: NodeIdType | null, value?: number | string | Buffer | Guid, namespace?: number) {
+
+
         if (identifierType === null || identifierType === undefined) {
         if (identifierType === null || identifierType === undefined) {
             this.identifierType = NodeIdType.NUMERIC;
             this.identifierType = NodeIdType.NUMERIC;
             this.value = 0;
             this.value = 0;
@@ -14,38 +106,129 @@ export class NodeId  {
         }
         }
 
 
         this.identifierType = identifierType;
         this.identifierType = identifierType;
-        this.value = value || this.defaultValue(identifierType);
+        this.value = value || defaultValue(identifierType);
         this.namespace = namespace || 0;
         this.namespace = namespace || 0;
 
 
         // namespace shall be a UInt16
         // namespace shall be a UInt16
         assert(this.namespace >= 0 && this.namespace <= 0xffff, "NodeId: invalid namespace value");
         assert(this.namespace >= 0 && this.namespace <= 0xffff, "NodeId: invalid namespace value");
         assert(this.identifierType !== NodeIdType.NUMERIC || (this.value !== null && this.value as number >= 0 && this.value as number <= 0xffffffff));
         assert(this.identifierType !== NodeIdType.NUMERIC || (this.value !== null && this.value as number >= 0 && this.value as number <= 0xffffffff));
+        assert(this.identifierType !== NodeIdType.GUID || isValidGuid(this.value as string), "NodeId: Guid is invalid");
         assert(this.identifierType !== NodeIdType.STRING || typeof this.value === "string", "cannot  empty string");
         assert(this.identifierType !== NodeIdType.STRING || typeof this.value === "string", "cannot  empty string");
-
-    }
-
-    defaultValue(identifierType: NodeIdType): string | number {
-        switch (identifierType) {
-            case NodeIdType.STRING: return "";
-            case NodeIdType.NUMERIC: return 0;
+        if (this.identifierType === NodeIdType.GUID) {
+            this.value = normalizeGuid(value as string);
         }
         }
     }
     }
-    
 
 
-    public toString(): string {
+    /**
+     * get the string representation of the nodeID.
+     *
+     * @method toString
+     * @example
+     *
+     *    ``` javascript
+     *    const nodeid = new NodeId(NodeIdType.NUMERIC, 123,1);
+     *    console.log(nodeid.toString());
+     *    ```
+     *
+     *    ```
+     *    >"ns=1;i=123"
+     *    ```
+     *
+     * @param [options.addressSpace] {AddressSpace}
+     * @return {String}
+     */
+    public toString(options?: { addressSpace?: any }): string {
+        const addressSpace = options ? options.addressSpace : null;
         let str;
         let str;
-        switch (this.identifierType) {
+        const _this = this as INodeId;
+        switch (_this.identifierType) {
             case NodeIdType.NUMERIC:
             case NodeIdType.NUMERIC:
-                str = "ns=" + this.namespace + ";i=" + this.value;
+                str = "ns=" + this.namespace + ";i=" + _this.value;
                 break;
                 break;
             case NodeIdType.STRING:
             case NodeIdType.STRING:
-                str = "ns=" + this.namespace + ";s=" + this.value;
+                str = "ns=" + this.namespace + ";s=" + _this.value;
+                break;
+            case NodeIdType.GUID:
+                str = "ns=" + this.namespace + ";g=" + normalizeGuid(_this.value);
+                break;
+            default:
+                assert(this.identifierType === NodeIdType.BYTESTRING, "invalid identifierType in NodeId : " + this.identifierType);
+                if (this.value) {
+                    str = "ns=" + this.namespace + ";b=" + (this.value as Buffer).toString("base64");
+                } else {
+                    str = "ns=" + this.namespace + ";b=<null>";
+                }
                 break;
                 break;
         }
         }
+
+        if (addressSpace) {
+            if (this.namespace === 0 && _this.identifierType === NodeIdType.NUMERIC) {
+                // find standard browse name
+                const name = reverse_map((this.value || 0).toString()) || "<undefined>";
+                str += " " + name;
+            } else if (addressSpace.findNode) {
+                // let use the provided address space to figure out the browseNode of this node.
+                // to make the message a little bit more useful.
+                const n = addressSpace.findNode(this);
+                str += " " + (n ? n.browseName.toString() : " (????)");
+            }
+        }
         return str;
         return str;
     }
     }
 
 
+    /**
+     * convert nodeId to a JSON string. same as {@link NodeId#toString }
+     */
+    public toJSON(): string {
+        return this.toString();
+    }
 
 
+    public displayText(): string {
+        if (this.namespace === 0 && this.identifierType === NodeIdType.NUMERIC) {
+            const name = reverse_map(this.value.toString());
+            if (name) {
+                return name + " (" + this.toString() + ")";
+            }
+        }
+        return this.toString();
+    }
+
+    /**
+     * returns true if the NodeId is null or empty
+     */
+    public isEmpty(): boolean {
+        const _this = this as INodeId;
+        switch (_this.identifierType) {
+            case NodeIdType.NUMERIC:
+                return _this.value === 0;
+            case NodeIdType.STRING:
+                return !_this.value;
+            case NodeIdType.GUID:
+                return !_this.value || _this.value === emptyGuid;
+            default:
+                return !_this.value || (_this.value as Buffer).length === 0;
+        }
+    }
+}
+
+NodeId.nullNodeId = new Proxy(
+    new NodeId(NodeIdType.NUMERIC, 0, 0),
+    {
+        get: (target: NodeId, prop: string) => {
+            return (target as any)[prop];
+        },
+        set: () => {
+            throw new Error("Cannot assign a value to constant NodeId.nullNodeId");
+        }
+    });
+
+
+export type NodeIdLike = string | NodeId | number;
+
+const regexNamespaceI = /ns=([0-9]+);i=([0-9]+)/;
+const regexNamespaceS = /ns=([0-9]+);s=(.*)/;
+const regexNamespaceB = /ns=([0-9]+);b=(.*)/;
+const regexNamespaceG = /ns=([0-9]+);g=([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})/;
 
 
 /**
 /**
  * Convert a value into a nodeId:
  * Convert a value into a nodeId:
@@ -62,9 +245,7 @@ export class NodeId  {
  * @param namespace {number}
  * @param namespace {number}
  */
  */
 // eslint-disable-next-line max-statements
 // eslint-disable-next-line max-statements
- static coerceNodeId(value: unknown, namespace?: number): NodeId {
-    const regexNamespaceI = /ns=([0-9]+);i=([0-9]+)/;
-    const regexNamespaceS = /ns=([0-9]+);s=(.*)/;
+export function coerceNodeId(value: unknown, namespace?: number): NodeId {
     let matches;
     let matches;
     let twoFirst;
     let twoFirst;
     if (value instanceof NodeId) {
     if (value instanceof NodeId) {
@@ -86,7 +267,16 @@ export class NodeId  {
         } else if (twoFirst === "s=") {
         } else if (twoFirst === "s=") {
             identifierType = NodeIdType.STRING;
             identifierType = NodeIdType.STRING;
             value = value.substring(2);
             value = value.substring(2);
-
+        } else if (twoFirst === "b=") {
+            identifierType = NodeIdType.BYTESTRING;
+            value = Buffer.from(value.substring(2), "base64");
+        } else if (twoFirst === "g=") {
+            identifierType = NodeIdType.GUID;
+            value = normalizeGuid(value.substring(2));
+            assert(isValidGuid(value as string));
+        } else if (isValidGuid(value)) {
+            identifierType = NodeIdType.GUID;
+            value = normalizeGuid(value);
         } else if ((matches = regexNamespaceI.exec(value)) !== null) {
         } else if ((matches = regexNamespaceI.exec(value)) !== null) {
             identifierType = NodeIdType.NUMERIC;
             identifierType = NodeIdType.NUMERIC;
             namespace = parseInt(matches[1], 10);
             namespace = parseInt(matches[1], 10);
@@ -95,9 +285,19 @@ export class NodeId  {
             identifierType = NodeIdType.STRING;
             identifierType = NodeIdType.STRING;
             namespace = parseInt(matches[1], 10);
             namespace = parseInt(matches[1], 10);
             value = matches[2];
             value = matches[2];
+        } else if ((matches = regexNamespaceB.exec(value)) !== null) {
+            identifierType = NodeIdType.BYTESTRING;
+            namespace = parseInt(matches[1], 10);
+            value = Buffer.from(matches[2], "base64");
+        } else if ((matches = regexNamespaceG.exec(value)) !== null) {
+            identifierType = NodeIdType.GUID;
+            namespace = parseInt(matches[1], 10);
+            value = normalizeGuid(matches[2]);
         } else {
         } else {
             throw new Error("String cannot be coerced to a nodeId : " + value);
             throw new Error("String cannot be coerced to a nodeId : " + value);
         }
         }
+    } else if (value instanceof Buffer) {
+        identifierType = NodeIdType.BYTESTRING;
     } else if (value instanceof Object) {
     } else if (value instanceof Object) {
         // it could be a Enum or a NodeId Like object
         // it could be a Enum or a NodeId Like object
         const tmp = value as any;
         const tmp = value as any;
@@ -108,20 +308,113 @@ export class NodeId  {
     }
     }
     return new NodeId(identifierType, value as any, namespace);
     return new NodeId(identifierType, value as any, namespace);
 }
 }
+
+const regEx1 = /^(s|g|b|i|ns)=/;
+/**
+ * construct a node Id from a value and a namespace.
+ * @class opcua
+ * @method makeNodeId
+ * @static
+ * @param {String|Buffer} value
+ * @param [namespace]=0 {Number} the node id namespace
+ * @return {NodeId}
+ */
+export function makeNodeId(value: string | Buffer | number, namespace?: number): NodeId {
+    value = value || 0;
+    namespace = namespace || 0;
+
+    let identifierType = NodeIdType.NUMERIC;
+    if (typeof value === "string") {
+        if (value.match(regEx1)) {
+            throw new Error("please use coerce NodeId instead");
+        }
+        //            1         2         3
+        //  012345678901234567890123456789012345
+        // "72962B91-FA75-4AE6-8D28-B404DC7DAF63"
+        if (isValidGuid(value)) {
+            identifierType = NodeIdType.GUID;
+            value = normalizeGuid(value);
+        } else {
+            identifierType = NodeIdType.STRING;
+        }
+    } else if (value instanceof Buffer) {
+        identifierType = NodeIdType.BYTESTRING;
+    }
+
+    const nodeId = new NodeId(identifierType, value, namespace);
+    return nodeId;
 }
 }
 
 
-export enum NodeIdType {
-    /**
-     * @static
-     * @property NUMERIC
-     * @default 0x1
-     */
-    NUMERIC = 0x01,
-    /**
-     * @static
-     * @property STRING
-     * @default 0x2
-     */
-    STRING = 0x02,
+// reverse maps
+let _nodeIdToNameIndex: any = {};
+let _nameToNodeIdIndex: any = {};
+
+const regName = /[a-zA-Z_].*/;
+
+(function build_standard_nodeid_indexes() {
+    function expand_map(directIndex: any) {
+        for (const name in directIndex) {
+            if (Object.prototype.hasOwnProperty.call(directIndex, name) && regName.exec(name) !== null) {
+                const value = directIndex[name];
+                _nodeIdToNameIndex[value] = name;
+                _nameToNodeIdIndex[name] = new NodeId(NodeIdType.NUMERIC, value, 0);
+            }
+        }
+    }
+
+    _nodeIdToNameIndex = {};
+    _nameToNodeIdIndex = {};
+    expand_map(ObjectIds);
+    expand_map(ObjectTypeIds);
+    expand_map(VariableIds);
+    expand_map(VariableTypeIds);
+    expand_map(MethodIds);
+    expand_map(ReferenceTypeIds);
+    expand_map(DataTypeIds);
+})();
+
+function reverse_map(nodeId: string) {
+    return _nodeIdToNameIndex[nodeId];
+}
+
+/**
+ * @class opcua
+ * @method resolveNodeId
+ * @static
+ * @param nodeIdOrString
+ * @return the nodeId
+ */
+export function resolveNodeId(nodeIdOrString: NodeIdLike): NodeId {
+    let nodeId;
+
+    const rawId = typeof nodeIdOrString === "string" ? _nameToNodeIdIndex[nodeIdOrString] : undefined;
+    if (rawId !== undefined) {
+        return rawId;
+    } else {
+        nodeId = coerceNodeId(nodeIdOrString);
+    }
+    return nodeId;
+}
+
+NodeId.resolveNodeId = resolveNodeId;
+
+export function sameNodeId(n1: NodeId, n2: NodeId): boolean {
+    if (n1.identifierType !== n2.identifierType) {
+        return false;
+    }
+    if (n1.namespace !== n2.namespace) {
+        return false;
+    }
+    switch (n1.identifierType) {
+        case NodeIdType.NUMERIC:
+        case NodeIdType.STRING:
+        case NodeIdType.GUID:
+            return n1.value === n2.value;
+        case NodeIdType.BYTESTRING:
+            return (n1.value as Buffer).toString("hex") === (n2.value as Buffer).toString("hex");
+        default:
+            throw new Error("Invalid identifier type");
+    }
 }
 }
+NodeId.sameNodeId = sameNodeId;
 
 

+ 2 - 2
src/ua/UAReference.ts

@@ -1,7 +1,7 @@
 import { XMLElem, type IToXML } from "@/util/XmlElem";
 import { XMLElem, type IToXML } from "@/util/XmlElem";
 import type { NamespaceTable } from "./NameSpaceTable";
 import type { NamespaceTable } from "./NameSpaceTable";
-import { NodeId } from "./NodeId";
 import { assert } from "@/util/assert";
 import { assert } from "@/util/assert";
+import { NodeId, coerceNodeId } from "./NodeId";
 
 
 export class UAReference implements IToXML{
 export class UAReference implements IToXML{
     constructor(public referenceType: string,
     constructor(public referenceType: string,
@@ -27,7 +27,7 @@ export class UAReference implements IToXML{
 
 
     static fromXML(uaReference: any): UAReference {
     static fromXML(uaReference: any): UAReference {
         return new UAReference(uaReference['@_ReferenceType'], 
         return new UAReference(uaReference['@_ReferenceType'], 
-                                NodeId.coerceNodeId(uaReference['#text']), 
+                                coerceNodeId(uaReference['#text']), 
                                 uaReference['@_IsForward']!="false");
                                 uaReference['@_IsForward']!="false");
     }
     }
 }
 }

+ 28 - 0
src/ua/guid.ts

@@ -0,0 +1,28 @@
+/***
+ * @module node-opcua-guid
+ */
+const regexGUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/;
+
+/**
+ * checks if provided string is a valid Guid
+ * a valid GUID has the form  XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX
+ * when X is a hexadecimal digit
+ *
+ * @method isValidGuid
+ * @param guid - the GUID to test for validaty
+ * @return  - true if the string is a valid GUID.
+ */
+export function isValidGuid(guid: string): boolean {
+    return regexGUID.test(guid);
+}
+
+
+//                                  1         2         3
+//                        012345678901234567890123456789012345
+export const emptyGuid = "00000000-0000-0000-0000-000000000000";
+
+export function normalizeGuid(guid: Guid |null | undefined): Guid {
+    return guid ? guid.toUpperCase() : emptyGuid;
+}
+
+export type Guid = string;

File diff suppressed because it is too large
+ 6022 - 0
src/ua/opcua_node_ids.ts