Skip to content

Commit 5491801

Browse files
authored
Merge pull request #1368 from danielpeintner/issue-1367-opcua-href
Parse nodeId from href
2 parents 9b6c86c + b912d32 commit 5491801

7 files changed

Lines changed: 139 additions & 92 deletions

File tree

package-lock.json

Lines changed: 21 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/binding-opcua/README.md

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ const thingDescription = {
2828
type: "number",
2929
forms: [
3030
{
31-
href: "opc.tcp://opcuademo.sterfive.com:26543", // endpoint,
31+
href: "opc.tcp://opcuademo.sterfive.com:26543?id=ns=1;s=PumpSpeed",
3232
op: ["readproperty", "observeproperty"],
33-
"opcua:nodeId": "ns=1;s=PumpSpeed",
3433
},
3534
],
3635
},
@@ -64,60 +63,23 @@ import { thingDescription } from "./demo-opcua-thing-description";
6463

6564
### Run the Example App
6665

67-
The `examples/src/opcua` folder contains a set of typescript demo that shows you
66+
The `examples/src/bindings/opcua` folder contains a set of typescript demo that shows you
6867
how to define a thing description containing OPCUA Variables and methods.
6968

7069
- `demo-opcua1.ts` shows how to define and read an OPC-UA variable in WoT.
7170
- `demo-opcua2.ts` shows how to subscribe to an OPC-UA variable in WoT.
7271
- `opcua-coffee-machine-demo.ts` demonstrates how to define and invoke OPCUA methods as WoT actions.
7372

74-
### Form extensions
73+
### Format for href
7574

76-
#### href
77-
78-
the `href` property must contains a OPCUA endpoint url in the form `opc.tcp://MACHINE:PORT/Application`
75+
The `href` must contain an OPCUA endpoint url in the form of `opc.tcp://<address>:<port>/?id=<nodeId>`
7976
such as for instance:
80-
`opc.tcp://opcuademo.sterfive.com:26543` or `opc.tcp://localhost:48010`
81-
82-
#### opcua:nodeId
83-
84-
The form must contain an `opcua:nodeId` property that describes the nodeId of the OPCUA Variable to read/write/subscribe or the nodeId of the OPCUA Object related to the action.
85-
86-
The `opcua:nodeId` can have 2 forms:
87-
88-
- a **NodeId** as a string, such as `"ns=1;i=1234"` , for instance:
89-
90-
```javascript
91-
"opcua:nodeId": "ns=1;s=\"Machine\".\"Component\""
92-
```
93-
94-
- or **browsePath**: The browse path will be converted into the corresponding nodeId at runtime when first encountered.
95-
96-
```
97-
"opcua:nodeId": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine" },
98-
```
77+
`opc.tcp://opcuademo.sterfive.com:26543?id=ns=1;s=PumpSpeed`
9978

100-
### opcua:method
79+
`<nodeId>` has the following expectations:
10180

102-
for example:
103-
104-
```typescript
105-
const thingDescription = {
106-
// ...
107-
actions: {
108-
brewCoffee: {
109-
forms: [
110-
{
111-
href: "opc.tcp://opcuademo.sterfive.com:26543",
112-
op: ["invokeaction"],
113-
"opcua:nodeId": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine" },
114-
"opcua:method": { root: "i=84", path: "/Objects/2:DeviceSet/1:CoffeeMachine/2:MethodSet/9:Start" },
115-
},
116-
],
117-
},
118-
},
119-
};
120-
```
81+
- any hash character (`#`) must be URL encoded (`%23`)
82+
- any ampersand character (`&`) must be URL encoded (`%26`)
12183

12284
### defining a property
12385

@@ -133,9 +95,8 @@ const thingDescription = {
13395
type: "number",
13496
forms: [
13597
{
136-
href: "opc.tcp://opcuademo.sterfive.com:26543",
98+
href: "opc.tcp://opcuademo.sterfive.com:26543?id=ns=1;s=Temperature",
13799
op: ["readproperty", "observeproperty"],
138-
"opcua:nodeId": "ns=1;s=Temperature",
139100
},
140101
],
141102
},

packages/binding-opcua/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"node-opcua-status-code": "2.139.0",
4444
"node-opcua-types": "2.143.0",
4545
"node-opcua-variant": "2.142.0",
46-
"rxjs": "5.5.11"
46+
"rxjs": "5.5.11",
47+
"url": "^0.11.4"
4748
},
4849
"scripts": {
4950
"build": "tsc -b",

packages/binding-opcua/src/opcua-protocol-client.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import { Subscription } from "rxjs/Subscription";
1717
import { promisify } from "util";
1818
import { Readable } from "stream";
19+
import { URL } from "url";
1920

2021
import { ProtocolClient, Content, ContentSerdes, Form, SecurityScheme, createLoggers } from "@node-wot/core";
2122

@@ -44,7 +45,6 @@ import { makeBrowsePath } from "node-opcua-service-translate-browse-path";
4445
import { StatusCodes } from "node-opcua-status-code";
4546

4647
import { schemaDataValue } from "./codec";
47-
import { FormElementProperty } from "wot-thing-description-types";
4848
import { opcuaJsonEncodeVariant } from "node-opcua-json";
4949
import { Argument, BrowseDescription, BrowseResult } from "node-opcua-types";
5050
import { isGoodish2, ReferenceTypeIds } from "node-opcua";
@@ -63,20 +63,12 @@ export type NodeIdLike2 = NodeIdLike & {
6363
};
6464

6565
export interface FormPartialNodeDescription {
66-
"opcua:nodeId": NodeIdLike | NodeByBrowsePath;
66+
/** @deprecated use href instead */
67+
"opcua:nodeId"?: NodeIdLike | NodeByBrowsePath;
6768
}
6869

6970
export interface OPCUAForm extends Form, FormPartialNodeDescription {}
7071

71-
export interface OPCUAFormElement extends FormElementProperty, FormPartialNodeDescription {}
72-
73-
export interface OPCUAFormInvoke extends OPCUAForm {
74-
"opcua:method": NodeIdLike | NodeByBrowsePath;
75-
}
76-
export interface OPCUAFormSubscribe extends OPCUAForm {
77-
"opcua:samplingInterval"?: number;
78-
}
79-
8072
interface OPCUAConnection {
8173
session: ClientSession;
8274
client: OPCUAClient;
@@ -260,23 +252,37 @@ export class OPCUAProtocolClient implements ProtocolClient {
260252
}
261253
}
262254

263-
private async _resolveNodeId(form: OPCUAForm): Promise<NodeId> {
264-
const fNodeId = form["opcua:nodeId"];
255+
static getNodeId(form: OPCUAForm): NodeIdLike | NodeByBrowsePath {
256+
let fNodeId;
257+
if (form.href != null && form.href !== "" && form.href !== "/") {
258+
// parse node id from href
259+
// Note: href needs to be absolute
260+
const url = new URL(form.href);
261+
if (url != null && url.search != null && url.search.startsWith("?")) {
262+
const searchParams = new URLSearchParams(url.search.substring(1));
263+
fNodeId = searchParams.get("id");
264+
}
265+
}
265266
if (fNodeId == null) {
266-
debug(`resolveNodeId: form = ${form}`);
267-
throw new Error("form must expose a 'opcua:nodeId'");
267+
// fallback to *old* way
268+
fNodeId = form["opcua:nodeId"];
269+
if (fNodeId == null) {
270+
debug(`resolveNodeId: form = ${form}`);
271+
throw new Error("form must expose nodeId via href or 'opcua:nodeId'");
272+
}
268273
}
274+
return fNodeId;
275+
}
276+
277+
private async _resolveNodeId(form: OPCUAForm): Promise<NodeId> {
278+
const fNodeId = OPCUAProtocolClient.getNodeId(form);
269279
return this._resolveNodeId2(form, fNodeId);
270280
}
271281

272282
/** extract the dataType of a variable */
273283
private async _predictDataType(form: OPCUAForm): Promise<DataType> {
274-
const fNodeId = form["opcua:nodeId"];
275-
if (fNodeId == null) {
276-
debug(`resolveNodeId: form = ${form}`);
277-
throw new Error("form must expose a 'opcua:nodeId'");
278-
}
279-
const nodeId = await this._resolveNodeId2(form, fNodeId);
284+
const nodeId = await this._resolveNodeId(form);
285+
280286
return await this._withSession<DataType>(form, async (session) => {
281287
const dataTypeOrNull = await getBuiltInDataType(session, nodeId);
282288
if (dataTypeOrNull !== undefined && dataTypeOrNull !== DataType.Null) {
@@ -286,14 +292,14 @@ export class OPCUAProtocolClient implements ProtocolClient {
286292
});
287293
}
288294

289-
private async _resolveMethodNodeId(form: OPCUAFormInvoke): Promise<NodeId> {
295+
private async _resolveMethodNodeId(form: OPCUAForm): Promise<NodeId> {
290296
// const objectNode = this._resolveNodeId(form);
291297
const fNodeId = form["opcua:method"];
292298
if (fNodeId == null) {
293299
debug(`resolveNodeId: form = ${form}`);
294-
throw new Error("form must expose a 'opcua:nodeId'");
300+
throw new Error("form must expose a 'opcua:method'");
295301
}
296-
return this._resolveNodeId2(form, fNodeId);
302+
return this._resolveNodeId2(form, fNodeId as NodeIdLike | NodeByBrowsePath);
297303
}
298304

299305
public async readResource(form: OPCUAForm): Promise<Content> {
@@ -328,7 +334,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
328334
}
329335
}
330336

331-
public async invokeResource(form: OPCUAFormInvoke, content: Content): Promise<Content> {
337+
public async invokeResource(form: OPCUAForm, content: Content): Promise<Content> {
332338
return await this._withSession(form, async (session) => {
333339
const objectId = await this._resolveNodeId(form);
334340
const methodId = await this._resolveMethodNodeId(form);
@@ -362,7 +368,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
362368
error?: (error: Error) => void,
363369
complete?: () => void
364370
): Promise<Subscription> {
365-
debug(`subscribeResource: form ${form["opcua:nodeId"]}`);
371+
debug(`subscribeResource: form ${OPCUAProtocolClient.getNodeId(form)}`);
366372

367373
return this._withSubscription<Subscription>(form, async (session, subscription) => {
368374
const nodeId = await this._resolveNodeId(form);
@@ -437,7 +443,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
437443
}
438444

439445
async unlinkResource(form: OPCUAForm): Promise<void> {
440-
debug(`unlinkResource: form ${form["opcua:nodeId"]}`);
446+
debug(`unlinkResource: form ${OPCUAProtocolClient.getNodeId(form)}`);
441447
this._withSubscription<void>(form, async (session, subscription) => {
442448
const nodeId = await this._resolveNodeId(form);
443449
await this._unmonitor(nodeId);
@@ -575,7 +581,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
575581

576582
private async _resolveInputArguments(
577583
session: IBasicSession,
578-
form: OPCUAFormInvoke,
584+
form: OPCUAForm,
579585
content: Content | undefined | null,
580586
argumentDefinition: ArgumentDefinition
581587
): Promise<VariantOptions[]> {
@@ -623,7 +629,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
623629

624630
private async _resolveOutputArguments(
625631
session: IBasicSession,
626-
form: OPCUAFormInvoke,
632+
form: OPCUAForm,
627633
argumentDefinition: ArgumentDefinition,
628634
outputVariants: Variant[]
629635
): Promise<Content> {

packages/binding-opcua/test/client-test.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ContentSerdes, createLoggers } from "@node-wot/core";
1919

2020
import { VariableIds, OPCUAServer } from "node-opcua";
2121

22-
import { OPCUAProtocolClient, OPCUAForm, OPCUAFormInvoke } from "../src/opcua-protocol-client";
22+
import { OPCUAProtocolClient, OPCUAForm } from "../src/opcua-protocol-client";
2323
import { OpcuaJSONCodec, schemaDataValue } from "../src/codec";
2424
import { startServer } from "./fixture/basic-opcua-server";
2525

@@ -92,7 +92,7 @@ describe("OPCUA Client", function () {
9292
expected: new Date("2022-01-31T10:45:00.000Z"),
9393
},
9494
].forEach(({ contentType, expected }, index) => {
95-
it(`Y1-${index} should read a topic with contentType= ${contentType}`, async () => {
95+
it(`Y1a-${index} should read a topic with contentType= ${contentType}`, async () => {
9696
const readForm: OPCUAForm = {
9797
href: endpoint,
9898
"opcua:nodeId": "ns=1;s=ManufacturingDate",
@@ -107,6 +107,28 @@ describe("OPCUA Client", function () {
107107
const codecSerDes = ContentSerdes.get();
108108
const dataValue = codecSerDes.contentToValue(content2, schemaDataValue) as Record<string, unknown>;
109109

110+
// (deal with always changing date )
111+
if (dataValue.SourceTimestamp != null) {
112+
expect(dataValue.SourceTimestamp).to.be.instanceOf(Date);
113+
dataValue.SourceTimestamp = "*";
114+
}
115+
debug(`${dataValue}`);
116+
expect(dataValue).to.eql(expected);
117+
});
118+
it(`Y1b-${index} should read a topic with contentType= ${contentType}`, async () => {
119+
const readForm: OPCUAForm = {
120+
href: endpoint + "?id=ns=1;s=ManufacturingDate",
121+
contentType,
122+
};
123+
124+
const content = await client.readResource(readForm);
125+
const content2 = { ...content, body: await content.toBuffer() };
126+
127+
debug(`readResource returned: ${content2.body.toString("ascii")}`);
128+
129+
const codecSerDes = ContentSerdes.get();
130+
const dataValue = codecSerDes.contentToValue(content2, schemaDataValue) as Record<string, unknown>;
131+
110132
// (deal with always changing date )
111133
if (dataValue.SourceTimestamp != null) {
112134
expect(dataValue.SourceTimestamp).to.be.instanceOf(Date);
@@ -164,7 +186,7 @@ describe("OPCUA Client", function () {
164186
required: ["TargetTemperature"],
165187
};
166188

167-
const form: OPCUAFormInvoke = {
189+
const form: OPCUAForm = {
168190
href: endpoint,
169191
"opcua:nodeId": { root: "i=84", path: "/Objects/1:MySensor" },
170192
"opcua:method": { root: "i=84", path: "/Objects/1:MySensor/2:MethodSet/1:SetTemperatureSetPoint" },

0 commit comments

Comments
 (0)