|
12 | 12 | * |
13 | 13 | * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 |
14 | 14 | ********************************************************************************/ |
15 | | - |
16 | | -import { ContentCodec, DataSchema, createLoggers } from "@node-wot/core"; |
17 | | -import { DataValue } from "node-opcua-data-value"; |
18 | | -import { DataType, Variant } from "node-opcua-variant"; |
19 | | -import Ajv from "ajv"; |
20 | | -import addFormats from "ajv-formats"; |
21 | | - |
22 | | -// see https://www.w3.org/Protocols/rfc1341/4_Content-Type.html |
23 | | -import { |
24 | | - opcuaJsonEncodeDataValue, |
25 | | - opcuaJsonDecodeDataValue, |
26 | | - opcuaJsonDecodeVariant, |
27 | | - DataValueJSON, |
28 | | - opcuaJsonEncodeVariant, |
29 | | -} from "node-opcua-json"; |
30 | | -import { BinaryStream } from "node-opcua-binary-stream"; |
31 | | -import { DataSchemaValue } from "wot-typescript-definitions"; |
32 | | - |
33 | | -const { debug } = createLoggers("binding-opcua", "codec"); |
34 | | - |
35 | | -// Strict mode has a lot of other checks and it prevents runtime unexpected problems |
36 | | -// TODO: in the future we should use the strict mode |
37 | | -const ajv = new Ajv({ strict: false }); |
38 | | -addFormats(ajv); |
39 | | -/** |
40 | | - * this schema, describe the node-opcua JSON format for a DataValue object |
41 | | - * |
42 | | - * const pojo = (new DataValue({})).toString(); |
43 | | - * |
44 | | - */ |
45 | | -export const schemaDataValue = { |
46 | | - type: ["object"], // "number", "integer", "string", "boolean", "array", "null"], |
47 | | - properties: { |
48 | | - serverPicoseconds: { type: "integer" }, |
49 | | - sourcePicoseconds: { type: "integer" }, |
50 | | - serverTimestamp: { type: "string", /* format: "date", */ nullable: true }, |
51 | | - sourceTimestamp: { type: "string", /* format: "date", */ nullable: true }, |
52 | | - statusCode: { |
53 | | - type: ["object"], |
54 | | - properties: { |
55 | | - value: { |
56 | | - type: "number", |
57 | | - }, |
58 | | - }, |
59 | | - }, |
60 | | - value: { |
61 | | - type: ["object"], |
62 | | - properties: { |
63 | | - dataType: { |
64 | | - type: ["string", "integer"], |
65 | | - }, |
66 | | - arrayType: { |
67 | | - type: ["string"], |
68 | | - }, |
69 | | - value: { |
70 | | - type: ["number", "integer", "string", "boolean", "array", "null", "object"], |
71 | | - }, |
72 | | - dimension: { |
73 | | - type: ["array"], |
74 | | - items: { type: "integer" }, |
75 | | - }, |
76 | | - additionalProperties: false, |
77 | | - }, |
78 | | - }, |
79 | | - }, |
80 | | - additionalProperties: true, |
81 | | -}; |
82 | | - |
83 | | -export const schemaVariantJSONNull = { |
84 | | - type: "null", |
85 | | - nullable: true, |
86 | | -}; |
87 | | - |
88 | | -export const schemaVariantJSON = { |
89 | | - type: "object", |
90 | | - properties: { |
91 | | - Type: { |
92 | | - type: ["number"], |
93 | | - }, |
94 | | - Body: { |
95 | | - type: ["number", "integer", "string", "boolean", "array", "null", "object"], |
96 | | - nullable: true, |
97 | | - }, |
98 | | - Dimensions: { |
99 | | - type: ["array"], |
100 | | - items: { type: "integer" }, |
101 | | - }, |
102 | | - }, |
103 | | - additionalProperties: false, |
104 | | - required: ["Type", "Body"], |
105 | | -}; |
106 | | - |
107 | | -export const schemaDataValueJSON1 = { |
108 | | - type: ["object"], // "number", "integer", "string", "boolean", "array", "null"], |
109 | | - properties: { |
110 | | - ServerPicoseconds: { type: "integer" }, |
111 | | - SourcePicoseconds: { type: "integer" }, |
112 | | - ServerTimestamp: { |
113 | | - type: "string" /*, format: "date" */, |
114 | | - }, |
115 | | - SourceTimestamp: { |
116 | | - type: "string" /*, format: "date" */, |
117 | | - }, |
118 | | - StatusCode: { |
119 | | - type: "integer", |
120 | | - minimum: 0, |
121 | | - }, |
122 | | - |
123 | | - Value: schemaVariantJSON, |
124 | | - Value1: { type: "number", nullable: true }, |
125 | | - |
126 | | - Value2: { |
127 | | - oneOf: [schemaVariantJSON, schemaVariantJSONNull], |
128 | | - }, |
129 | | - }, |
130 | | - |
131 | | - additionalProperties: false, |
132 | | - required: ["Value"], |
133 | | -}; |
134 | | -export const schemaDataValueJSON2 = { |
135 | | - properties: { |
136 | | - Value: { type: "null" }, |
137 | | - }, |
138 | | -}; |
139 | | -export const schemaDataValueJSON = { |
140 | | - oneOf: [schemaDataValueJSON2, schemaDataValueJSON1], |
141 | | -}; |
142 | | -export const schemaDataValueJSONValidate = ajv.compile(schemaDataValueJSON); |
143 | | -export const schemaDataValueValidate = ajv.compile(schemaDataValue); |
144 | | - |
145 | | -export function formatForNodeWoT(dataValue: DataValueJSON): DataValueJSON { |
146 | | - // remove unwanted/unneeded properties |
147 | | - delete dataValue.SourcePicoseconds; |
148 | | - delete dataValue.ServerPicoseconds; |
149 | | - delete dataValue.ServerTimestamp; |
150 | | - return dataValue; |
151 | | -} |
152 | | - |
153 | | -// application/json => is equivalent to application/opcua+json;type=Value |
154 | | - |
155 | | -export class OpcuaJSONCodec implements ContentCodec { |
156 | | - getMediaType(): string { |
157 | | - return "application/opcua+json"; |
158 | | - } |
159 | | - |
160 | | - bytesToValue(bytes: Buffer, schema: DataSchema, parameters?: { [key: string]: string }): DataSchemaValue { |
161 | | - const type = parameters?.type ?? "DataValue"; |
162 | | - let parsed = JSON.parse(bytes.toString()); |
163 | | - |
164 | | - const wantDataValue = parameters?.to === "DataValue" || false; |
165 | | - |
166 | | - switch (type) { |
167 | | - case "DataValue": { |
168 | | - const isValid = schemaDataValueJSONValidate(parsed); |
169 | | - if (!isValid) { |
170 | | - debug(`bytesToValue: parsed = ${parsed}`); |
171 | | - debug(`bytesToValue: ${schemaDataValueJSONValidate.errors}`); |
172 | | - throw new Error("Invalid JSON dataValue : " + JSON.stringify(parsed, null, " ")); |
173 | | - } |
174 | | - if (wantDataValue) { |
175 | | - return opcuaJsonDecodeDataValue(parsed); |
176 | | - } |
177 | | - return formatForNodeWoT(opcuaJsonEncodeDataValue(opcuaJsonDecodeDataValue(parsed), true)); |
178 | | - // return parsed; |
179 | | - } |
180 | | - case "Variant": { |
181 | | - if (wantDataValue) { |
182 | | - const dataValue = new DataValue({ value: opcuaJsonDecodeVariant(parsed) }); |
183 | | - return dataValue; |
184 | | - } |
185 | | - const v = opcuaJsonEncodeVariant(opcuaJsonDecodeVariant(parsed), true); |
186 | | - debug(`${v}`); |
187 | | - return v; |
188 | | - } |
189 | | - case "Value": { |
190 | | - if (wantDataValue) { |
191 | | - if (!parameters || !parameters.dataType) { |
192 | | - throw new Error("[OpcuaJSONCodec|bytesToValue]: unknown dataType for Value encoding" + type); |
193 | | - } |
194 | | - if (parameters.dataType === DataType[DataType.DateTime]) { |
195 | | - parsed = new Date(parsed); |
196 | | - } |
197 | | - const value = { |
198 | | - dataType: DataType[parameters.dataType as keyof typeof DataType], |
199 | | - value: parsed, |
200 | | - }; |
201 | | - return new DataValue({ value }); |
202 | | - } else { |
203 | | - if (parameters?.dataType === DataType[DataType.DateTime]) { |
204 | | - parsed = new Date(parsed); |
205 | | - } |
206 | | - return parsed; |
207 | | - } |
208 | | - } |
209 | | - default: |
210 | | - throw new Error("[OpcuaJSONCodec|bytesToValue]: Invalid type " + type); |
211 | | - } |
212 | | - } |
213 | | - |
214 | | - valueToBytes(value: unknown, _schema: DataSchema, parameters?: { [key: string]: string }): Buffer { |
215 | | - const type = parameters?.type ?? "DataValue"; |
216 | | - switch (type) { |
217 | | - case "DataValue": { |
218 | | - let dataValueJSON: DataValueJSON; |
219 | | - if (value instanceof DataValue) { |
220 | | - dataValueJSON = opcuaJsonEncodeDataValue(value, true); |
221 | | - } else if (value instanceof Variant) { |
222 | | - dataValueJSON = opcuaJsonEncodeDataValue(new DataValue({ value }), true); |
223 | | - } else if (typeof value === "string") { |
224 | | - dataValueJSON = JSON.parse(value) as DataValueJSON; |
225 | | - } else { |
226 | | - dataValueJSON = opcuaJsonEncodeDataValue(opcuaJsonDecodeDataValue(value as DataValueJSON), true); |
227 | | - } |
228 | | - dataValueJSON = formatForNodeWoT(dataValueJSON); |
229 | | - return Buffer.from(JSON.stringify(dataValueJSON), "ascii"); |
230 | | - } |
231 | | - case "Variant": { |
232 | | - if (value instanceof DataValue) { |
233 | | - value = opcuaJsonEncodeVariant(value.value, true); |
234 | | - } else if (value instanceof Variant) { |
235 | | - value = opcuaJsonEncodeVariant(value, true); |
236 | | - } else if (typeof value === "string") { |
237 | | - value = JSON.parse(value); |
238 | | - } |
239 | | - return Buffer.from(JSON.stringify(value), "ascii"); |
240 | | - } |
241 | | - case "Value": { |
242 | | - if (value === undefined) { |
243 | | - return Buffer.alloc(0); |
244 | | - } |
245 | | - if (value instanceof DataValue) { |
246 | | - value = opcuaJsonEncodeVariant(value.value, false); |
247 | | - } else if (value instanceof Variant) { |
248 | | - value = opcuaJsonEncodeVariant(value, false); |
249 | | - } |
250 | | - return Buffer.from(JSON.stringify(value), "ascii"); |
251 | | - } |
252 | | - default: |
253 | | - throw new Error("[OpcuaJSONCodec|valueToBytes]: Invalid type : " + type); |
254 | | - } |
255 | | - } |
256 | | -} |
257 | | -export const theOpcuaJSONCodec = new OpcuaJSONCodec(); |
258 | | - |
259 | | -export class OpcuaBinaryCodec implements ContentCodec { |
260 | | - getMediaType(): string { |
261 | | - return "application/opcua+octet-stream"; // see Ege |
262 | | - } |
263 | | - |
264 | | - bytesToValue(bytes: Buffer, schema: DataSchema, parameters?: { [key: string]: string }): DataValueJSON { |
265 | | - const binaryStream = new BinaryStream(bytes); |
266 | | - const dataValue = new DataValue(); |
267 | | - dataValue.decode(binaryStream); |
268 | | - return opcuaJsonEncodeDataValue(dataValue, true); |
269 | | - } |
270 | | - |
271 | | - valueToBytes( |
272 | | - dataValue: DataValueJSON | DataValue, |
273 | | - schema: DataSchema, |
274 | | - parameters?: { [key: string]: string } |
275 | | - ): Buffer { |
276 | | - dataValue = dataValue instanceof DataValue ? dataValue : opcuaJsonDecodeDataValue(dataValue); |
277 | | - |
278 | | - // remove unwanted properties |
279 | | - dataValue.serverPicoseconds = 0; |
280 | | - dataValue.sourcePicoseconds = 0; |
281 | | - dataValue.serverTimestamp = null; |
282 | | - |
283 | | - const size = dataValue.binaryStoreSize(); |
284 | | - const stream = new BinaryStream(size); |
285 | | - dataValue.encode(stream); |
286 | | - const body = stream.buffer; |
287 | | - return body; |
288 | | - } |
289 | | -} |
290 | | -export const theOpcuaBinaryCodec = new OpcuaBinaryCodec(); |
| 15 | +export * from "./codecs/opcua-binary-codec"; |
| 16 | +export * from "./codecs/opcua-json-codec"; |
| 17 | +export * from "./codecs/opcua-data-schemas"; |
0 commit comments