@@ -19,13 +19,14 @@ import { expect } from "chai";
1919import { Servient , createLoggers } from "@node-wot/core" ;
2020import { InteractionOptions } from "wot-typescript-definitions" ;
2121
22- import { OPCUAServer } from "node-opcua" ;
22+ import { DataType , makeBrowsePath , OPCUAServer , StatusCodes , UAVariable } from "node-opcua" ;
2323
2424import { OPCUAClientFactory } from "../src" ;
2525import { startServer } from "./fixture/basic-opcua-server" ;
26+ import { opcuaVariableSchemaType } from "../src/opcua-data-schemas" ;
2627const endpoint = "opc.tcp://localhost:7890" ;
2728
28- const { debug, info } = createLoggers ( "binding-opcua" , "full-opcua-thing-test" ) ;
29+ const { debug, info, error } = createLoggers ( "binding-opcua" , "full-opcua-thing-test" ) ;
2930
3031const thingDescription : WoT . ThingDescription = {
3132 "@context" : "https://www.w3.org/2019/wot/td/v1" ,
@@ -49,7 +50,14 @@ const thingDescription: WoT.ThingDescription = {
4950 observable : true ,
5051 readOnly : true ,
5152 unit : "°C" ,
52- type : "number" ,
53+ oneOf : [
54+ {
55+ type : "number" ,
56+ description : "A simple number" ,
57+ } ,
58+ opcuaVariableSchemaType . dataValue ,
59+ opcuaVariableSchemaType . variant ,
60+ ] ,
5361 "opcua:nodeId" : { root : "i=84" , path : "/Objects/1:MySensor/2:ParameterSet/1:Temperature" } ,
5462 // Don't specify type here as it could be multi form: type: [ "object", "number" ],
5563 forms : [
@@ -59,6 +67,7 @@ const thingDescription: WoT.ThingDescription = {
5967 op : [ "readproperty" , "observeproperty" ] ,
6068 "opcua:nodeId" : { root : "i=84" , path : "/Objects/1:MySensor/2:ParameterSet/1:Temperature" } ,
6169 contentType : "application/json" ,
70+ type : "number" ,
6271 } ,
6372 {
6473 href : "/" , // endpoint,
@@ -78,6 +87,13 @@ const thingDescription: WoT.ThingDescription = {
7887 "opcua:nodeId" : { root : "i=84" , path : "/Objects/1:MySensor/2:ParameterSet/1:Temperature" } ,
7988 contentType : "application/opcua+json;type=DataValue" ,
8089 } ,
90+ {
91+ href : "/" , // endpoint,
92+ op : [ "readproperty" , "observeproperty" ] ,
93+ "opcua:nodeId" : { root : "i=84" , path : "/Objects/1:MySensor/2:ParameterSet/1:Temperature" } ,
94+ contentType : "application/octet-stream" , // equivalent to Variant
95+ type : "number" ,
96+ } ,
8197 ] ,
8298 } ,
8399 // Enriched value like provided by OPCUA
@@ -323,11 +339,34 @@ const thingDescription: WoT.ThingDescription = {
323339describe ( "Full OPCUA Thing Test" , ( ) => {
324340 let opcuaServer : OPCUAServer ;
325341 let endpoint : string ;
342+
343+ function setTemperature ( value : number , sourceTimestamp : Date ) {
344+ const browsePath = makeBrowsePath ( "ObjectsFolder" , "/1:MySensor/2:ParameterSet/1:Temperature" ) ;
345+ const browsePathResult = opcuaServer . engine . addressSpace ?. browsePath ( browsePath ) ;
346+ if ( ! browsePathResult || browsePathResult . statusCode !== StatusCodes . Good ) {
347+ error ( "Cannot find Temperature node" ) ;
348+ return ;
349+ }
350+ const nodeId = browsePathResult . targets ! [ 0 ] . targetId ! ;
351+ const uaTemperature = opcuaServer . engine . addressSpace ?. findNode ( nodeId ) as UAVariable ;
352+ if ( uaTemperature ) {
353+ uaTemperature . setValueFromSource (
354+ {
355+ dataType : DataType . Double ,
356+ value,
357+ } ,
358+ StatusCodes . Good ,
359+ sourceTimestamp
360+ ) ;
361+ }
362+ }
363+
326364 before ( async ( ) => {
327365 opcuaServer = await startServer ( ) ;
328366 endpoint = opcuaServer . getEndpointUrl ( ) ;
329367 debug ( `endpoint = ${ endpoint } ` ) ;
330368
369+ setTemperature ( 25 , new Date ( "2022-01-01T12:00:00Z" ) ) ;
331370 // adjust TD to endpoint
332371 thingDescription . base = endpoint ;
333372 ( thingDescription . opcua as unknown as { endpoint : string } ) . endpoint = endpoint ;
@@ -649,4 +688,80 @@ describe("Full OPCUA Thing Test", () => {
649688 await servient . shutdown ( ) ;
650689 }
651690 } ) ;
691+
692+ // Please refer to the description of this codec on how to decode and encode plain register
693+ // values to/from JavaScript objects (See `OctetstreamCodec`).
694+ // **Note** `array` and `object` schema are not supported.
695+ [
696+ // Var
697+ { property : "temperature" , contentType : "application/octet-stream" , expectedValue : 25 } ,
698+ { property : "temperature" , contentType : "application/octet-stream;byteSeq=BIG_ENDIAN" , expectedValue : 25 } ,
699+ { property : "temperature" , contentType : "application/octet-stream;byteSeq=LITTLE_ENDIAN" , expectedValue : 25 } ,
700+ {
701+ property : "temperature" ,
702+ contentType : "application/octet-stream;byteSeq=BIG_ENDIAN_BYTE_SWAP" ,
703+ expectedValue : 25 ,
704+ } ,
705+ {
706+ property : "temperature" ,
707+ contentType : "application/octet-stream;byteSeq=LITTLE_ENDIAN_BYTE_SWAP" ,
708+ expectedValue : 25 ,
709+ } ,
710+ { property : "temperature" , contentType : "application/json" , expectedValue : 25 } ,
711+
712+ // DataValue
713+ {
714+ property : "temperature" ,
715+ contentType : "application/opcua+json;type=DataValue" ,
716+ expectedValue : {
717+ SourceTimestamp : new Date ( "2022-01-01T12:00:00Z" ) ,
718+ Value : {
719+ Type : 11 ,
720+ Body : 25 ,
721+ } ,
722+ } ,
723+ } ,
724+ {
725+ property : "temperature" ,
726+ contentType : "application/opcua+json;type=Variant" ,
727+ expectedValue : {
728+ Type : 11 ,
729+ Body : 25 ,
730+ } ,
731+ } ,
732+ ] . map ( ( { contentType, property, expectedValue } , index ) => {
733+ it ( `CONTENT-TYPE-${ index } - should work with this encoding format- contentType=${ contentType } ` , async ( ) => {
734+ setTemperature ( 25 , new Date ( "2022-01-01T12:00:00Z" ) ) ;
735+
736+ const { thing, servient } = await makeThing ( ) ;
737+
738+ const propertyForm = thing . getThingDescription ( ) . properties ?. [ property ] . forms ;
739+ if ( ! propertyForm ) {
740+ expect . fail ( `no forms for ${ property } ` ) ;
741+ }
742+
743+ // find exact match of contentType first
744+ let formIndex = propertyForm . findIndex ( ( form ) => form . contentType === contentType ) ;
745+ if ( formIndex === undefined || formIndex < 0 ) {
746+ const mainCodec = contentType . split ( ";" ) [ 0 ] ;
747+ // fallback to main codec match
748+ formIndex = propertyForm . findIndex ( ( form ) => form . contentType === mainCodec ) ;
749+ if ( formIndex === undefined || formIndex < 0 ) {
750+ debug ( propertyForm . map ( ( f ) => f . contentType ) . join ( "," ) ) ;
751+ expect . fail ( `Cannot find form with contentType ${ contentType } ` ) ;
752+ }
753+ }
754+
755+ debug ( `Using form index ${ formIndex } with contentType ${ contentType } ` ) ;
756+
757+ try {
758+ const content = await thing . readProperty ( property , { formIndex } ) ;
759+ const value = await content . value ( ) ;
760+ debug ( `${ property } value is: ${ value } ` ) ;
761+ expect ( value ) . to . eql ( expectedValue ) ;
762+ } finally {
763+ await servient . shutdown ( ) ;
764+ }
765+ } ) ;
766+ } ) ;
652767} ) ;
0 commit comments