diff --git a/README.md b/README.md index 7b3d9a0..e926a44 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,6 @@ PLC.connect("192.168.1.1", 0).then(async () => { #### Reading Tags -**NOTE:** Currently, the `Tag` Class only supports *Atomic* datatypes (SINT, INT, DINT, REAL, BOOL). Not to worry, support for STRING, ARRAY, and UDTs are in the plans and coming soon! =] - Reading Tags `Individually`... ```javascript const { Controller, Tag } = require("ethernet-ip"); @@ -125,7 +123,6 @@ const barTag = new Tag("arrayTag[0]"); // Array Element const bazTag = new Tag("arrayTag[0,1,2]"); // Multi Dim Array Element const quxTag = new Tag("integerTag.0"); // SINT, INT, or DINT Bit const quuxTag = new Tag("udtTag.Member1"); // UDT Tag Atomic Member -const quuzTag = new Tag("boolArray[0]", null, BIT_STRING); // bool array tag MUST have the data type "BIT_STRING" passed in ``` Reading Tags as a `Group`... @@ -237,6 +234,188 @@ PLC.forEach(tag => { }); ``` +### User Defined Types + +User Defined Types must have a Template. Templates are managed by the controller. +Create a new template and add it to the controller. The template's name can be passed in as the type when creating a Tag. +```javascript +const { Controller, Tag, Template, CIP } = require("ethernet-ip"); +const { Types} = CIP.DataTypes; + +const PLC = new Controller(); + +// add template to controller with name and type definition +// the type definition is an object where the key is the member name +// and the value is the member type +PLC.addTemplate({ + name: "udt1", + definition: { + member1: Types.DINT; + member2: Types.DINT; + } +}); + +// create +const fooTag = new Tag("tag", null, "udt1"); + +PLC.connect("192.168.1.1", 0).then(async () => { + + // udt tags must be read before use + await PLC.readTag(fooTag); + + console.log(fooTag.value.member1); + console.log(fooTag.value.member2); + + fooTag.value.member1 = 5; + fooTag.value.member2 = 10; + + await PLC.writeTag(fooTag); + +}); +``` + +Specify arrays by setting a member to an object with a `type` and `length`. +```javascript +const { Controller, Tag, Template, CIP } = require("ethernet-ip"); +const { Types} = CIP.DataTypes; + +const PLC = new Controller(); + +// member 2 is an array of DINT with length 2 +PLC.addTemplate({ + name: "udt1", + definition: { + member1: Types.DINT, + member2: { type: Types.DINT, length: 2 }, + } +}); + +const fooTag = new Tag("tag", null, "udt1"); + +PLC.connect("192.168.1.1", 0).then(async () => { + + // udt tags must be read before use + await PLC.readTag(fooTag); + + console.log(fooTag.value.member1); + console.log(fooTag.value.member2[0]); + console.log(fooTag.value.member2[1]); + + fooTag.value.member1 = 5; + fooTag.value.member2[0] = 10; + fooTag.value.member2[1] = 20; + + await PLC.writeTag(fooTag); +}); +``` + +Nest UDTs by specifying a UDT name as a type. The child UDT template *MUST* be added before the parent UDT template. +```javascript +const { Controller, Tag, Template, CIP } = require("ethernet-ip"); +const { Types} = CIP.DataTypes; + +const PLC = new Controller(); + +// this template MUST be added first +PLC.addTemplate({ + name: "udt1", + definition: { + member1: Types.DINT, + member2: { type: Types.DINT, length: 2 }, + } +}); + +// this template references "udt1" and must be added AFTER "udt1" +PLC.addTemplate({ + name: "udt2", + definition: { + nestedUdt: "udt1", + anotherMember: Types.REAL, + } +}); + +const fooTag = new Tag("tag", null, "udt2"); + +PLC.connect("192.168.1.1", 0).then(async () => { + + // udt tags must be read before use + await PLC.readTag(fooTag); + + console.log(fooTag.value.nestedUdt.member1); + console.log(fooTag.value.nestedUdt.member2[0]); + console.log(fooTag.value.nestedUdt.member2[1]); + console.log(fooTag.value.anotherMember); + + fooTag.value.nestedUdt.member1 = 5; + fooTag.value.nestedUdt.member2[0] = 10; + fooTag.value.nestedUdt.member2[1] = 20; + fooTag.value.anotherMember = 40; + + await PLC.writeTag(fooTag); +}); +``` + +### Strings + +Strings can either be specified with their LEN and DATA members or by passing in a "string_length" value. +All templates with a STRING signature will have `getString()` and `setString(value)` functions on the +string member to allow for converstion between strings and the `LEN` and `DATA` members. +```javascript +const { Controller, Tag, Template, CIP } = require("ethernet-ip"); +const { Types} = CIP.DataTypes; + +const PLC = new Controller(); + +// create a string with LEN and DATA members +PLC.addTemplate({ + name: "String10", + definition: { + LEN: Types.DINT; + DATA: { type: Types.DINT, length: 10 }; + } +}); + +// create a string by passing in string_length +PLC.addTemplate({ + name: "AnotherString", + string_length: 12 +}); + +const fooTag = new Tag("tag1", null, "STRING"); // predefined 82 char string +const barTag = new Tag("tag2", null, "String10"); // user defined 10 char string +const bazTag = new Tag("tag3", null, "AnotherString"); // user defined 12 char string + +PLC.connect("192.168.1.1", 0).then(async () => { + + // udt tags must be read before use + await PLC.readTag(fooTag); + await PLC.readTag(barTag); + await PLC.readTag(baxTag); + + // access LEN, DATA, or getString() + console.log(fooTag.value.LEN, fooTag.value.DATA, fooTag.value.getString()); + console.log(barTag.value.LEN, barTag.value.DATA, barTag.value.getString()); + console.log(bazTag.value.LEN, bazTag.value.DATA, bazTag.value.getString()); + + // set LEN and DATA + fooTag.value.LEN = 8; + fooTag.value.DATA[0] = 110; + fooTag.value.DATA[1] = 101; + fooTag.value.DATA[2] = 119; + fooTag.value.DATA[3] = 32; + fooTag.value.DATA[4] = 116; + fooTag.value.DATA[5] = 101; + fooTag.value.DATA[6] = 120; + fooTag.value.DATA[7] = 116; + + // or use the setString(value) function + barTag.value.setString("new text"); + + await PLC.writeTag(fooTag); + await PLC.writeTag(barTag); +}); +``` + ## Demos - **Monitor Tags for Changes Demo** diff --git a/manuals/Data Access.pdf b/manuals/Data Access.pdf index da105b1..a4aee04 100644 Binary files a/manuals/Data Access.pdf and b/manuals/Data Access.pdf differ diff --git a/manuals/TypeEncode_CIPRW.pdf b/manuals/TypeEncode_CIPRW.pdf new file mode 100644 index 0000000..6fbcd6d Binary files /dev/null and b/manuals/TypeEncode_CIPRW.pdf differ diff --git a/src/controller/controller.spec.js b/src/controller/controller.spec.js index f7fde4e..4a83fdd 100644 --- a/src/controller/controller.spec.js +++ b/src/controller/controller.spec.js @@ -50,4 +50,17 @@ describe("Controller Class", () => { expect(plc.time).toMatchSnapshot(); }); }); + + describe("Add Templates Method", () => { + it("Should add templates", () => { + const plc = new Controller(); + + expect(plc.templates).not.toHaveProperty("udt"); + + plc.addTemplate({name: "udt"}); + + expect(plc.templates).toHaveProperty("udt"); + + }); + }); }); diff --git a/src/controller/index.js b/src/controller/index.js index dc0a08d..7e01f20 100644 --- a/src/controller/index.js +++ b/src/controller/index.js @@ -1,6 +1,8 @@ const { ENIP, CIP } = require("../enip"); const dateFormat = require("dateformat"); const TagGroup = require("../tag-group"); +const Template = require("../template"); +const TemplateMap = require("../template/atomics"); const { delay, promiseTimeout } = require("../utilities"); const Queue = require("task-easy"); @@ -33,7 +35,8 @@ class Controller extends ENIP { }, subs: new TagGroup(compare), scanning: false, - scan_rate: 200 //ms + scan_rate: 200, //ms + templates: TemplateMap() }; this.workers = { @@ -85,6 +88,17 @@ class Controller extends ENIP { return this.state.controller; } + /** + * Gets the Controller Templates Object + * + * @readonly + * @memberof Controller + * @returns {object} + */ + get templates() { + return this.state.templates; + } + /** * Fetches the last timestamp retrieved from the controller * in human readable form @@ -453,6 +467,16 @@ class Controller extends ENIP { forEach(callback) { this.state.subs.forEach(callback); } + + /** + * Adds new Template to Controller Templates + * + * @param {object} template + * @memberof Controller + */ + addTemplate(template){ + new Template(template).addToTemplates(this.state.templates); + } // endregion // region Private Methods @@ -474,12 +498,13 @@ class Controller extends ENIP { * @memberof Controller */ async _readTag(tag, size = null) { + tag.controller = this; + const MR = tag.generateReadMessageRequest(size); this.write_cip(MR); const readTagErr = new Error(`TIMEOUT occurred while writing Reading Tag: ${tag.name}.`); - // Wait for Response const data = await promiseTimeout( new Promise((resolve, reject) => { @@ -507,6 +532,8 @@ class Controller extends ENIP { * @memberof Controller */ async _writeTag(tag, value = null, size = 0x01) { + tag.controller = this; + const MR = tag.generateWriteMessageRequest(value, size); this.write_cip(MR); @@ -522,6 +549,7 @@ class Controller extends ENIP { if (err) reject(err); tag.unstageWriteRequest(); + resolve(data); }); @@ -549,6 +577,8 @@ class Controller extends ENIP { * @memberof Controller */ async _readTagGroup(group) { + group.setController(this); + const messages = group.generateReadMessageRequests(); const readTagGroupErr = new Error("TIMEOUT occurred while writing Reading Tag Group."); @@ -585,6 +615,8 @@ class Controller extends ENIP { * @memberof Controller */ async _writeTagGroup(group) { + group.setController(this); + const messages = group.generateWriteMessageRequests(); const writeTagGroupErr = new Error("TIMEOUT occurred while writing Reading Tag Group."); diff --git a/src/enip/cip/data-types/index.js b/src/enip/cip/data-types/index.js index 1595dfa..7150fb5 100644 --- a/src/enip/cip/data-types/index.js +++ b/src/enip/cip/data-types/index.js @@ -28,7 +28,7 @@ const Types = { EPATH: 0xdc, ENGUNIT: 0xdd, STRINGI: 0xde, - STRUCT: 0xa002 + STRUCT: 0x02a0 }; /** diff --git a/src/enip/cip/epath/segments/data/index.js b/src/enip/cip/epath/segments/data/index.js index ee5be93..56408aa 100644 --- a/src/enip/cip/epath/segments/data/index.js +++ b/src/enip/cip/epath/segments/data/index.js @@ -21,20 +21,20 @@ const build = (data, ANSI = true) => { throw new Error("Data Segment Data Must be a String or Buffer"); // Build Element Segment If Int - if (data % 1 === 0) return elementBuild(parseInt(data)); + if (data % 1 === 0) return _elementBuild(parseInt(data)); // Build symbolic segment by default - return symbolicBuild(data, ANSI); + return _symbolicBuild(data, ANSI); }; /** * Builds EPATH Symbolic Segment * * @param {string|buffer} data - * @param {boolean} [ANSI=true] Declare if ANSI Extended or Simple + * @param {boolean} ANSI Declare if ANSI Extended or Simple * @returns {buffer} */ -const symbolicBuild = (data, ANSI = true) => { +const _symbolicBuild = (data, ANSI) => { // Initialize Buffer let buf = Buffer.alloc(2); @@ -59,7 +59,7 @@ const symbolicBuild = (data, ANSI = true) => { * @param {string} data * @returns {buffer} */ -const elementBuild = data => { +const _elementBuild = data => { // Get Element Length - Data Access 2 - IOI Segments - Element Segments let type; let dataBuf; diff --git a/src/index.js b/src/index.js index ba6947b..15e6495 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ const Controller = require("./controller"); const Tag = require("./tag"); const TagGroup = require("./tag-group"); +const Template = require("./template"); const EthernetIP = require("./enip"); +const Types = require("./enip/cip/data-types"); // ok? const util = require("./utilities"); -module.exports = { Controller, Tag, TagGroup, EthernetIP, util }; +module.exports = { Controller, Tag, TagGroup, Template, EthernetIP, Types, util }; diff --git a/src/tag-group/__snapshots__/tag-group.spec.js.snap b/src/tag-group/__snapshots__/tag-group.spec.js.snap index ea7105a..cbb152c 100644 --- a/src/tag-group/__snapshots__/tag-group.spec.js.snap +++ b/src/tag-group/__snapshots__/tag-group.spec.js.snap @@ -187,4 +187,1024 @@ Array [ ] `; +exports[`Tag Class Generate Read Requests Method Generates Appropriate Output On Large Data 1`] = ` +Array [ + Object { + "data": Object { + "data": Array [ + 10, + 2, + 32, + 2, + 36, + 1, + 5, + 0, + 12, + 0, + 80, + 0, + 148, + 0, + 216, + 0, + 28, + 1, + 76, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 49, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 1, + 0, + 76, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 50, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 1, + 0, + 76, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 51, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 1, + 0, + 76, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 52, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 1, + 0, + 76, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 53, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 1, + 0, + ], + "type": "Buffer", + }, + "tag_ids": Array [ + "c00907cdbbe21da8405cb9619cf3ae03", + "21b6c868bb5d41ad7191d325eaaae487", + "29a77620e08c83b78ac32e2d4da75375", + "153cca324d6a853b129e62bed1c979fe", + "c620cebc92b9f3957fe6866747acba5a", + ], + }, +] +`; + exports[`Tag Class Generate Write Requests Method Generates Appropriate Output 1`] = `Array []`; + +exports[`Tag Class Generate Write Requests Method Generates Appropriate Output 2`] = ` +Array [ + Object { + "data": Object { + "data": Array [ + 10, + 2, + 32, + 2, + 36, + 1, + 5, + 0, + 12, + 0, + 48, + 0, + 84, + 0, + 120, + 0, + 156, + 0, + 77, + 13, + 145, + 12, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 112, + 114, + 111, + 103, + 145, + 9, + 104, + 101, + 108, + 108, + 111, + 84, + 97, + 103, + 49, + 0, + 196, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 77, + 13, + 145, + 12, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 112, + 114, + 111, + 103, + 145, + 9, + 104, + 101, + 108, + 108, + 111, + 84, + 97, + 103, + 50, + 0, + 196, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 77, + 13, + 145, + 12, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 112, + 114, + 111, + 103, + 145, + 9, + 104, + 101, + 108, + 108, + 111, + 84, + 97, + 103, + 51, + 0, + 196, + 0, + 1, + 0, + 2, + 0, + 0, + 0, + 77, + 13, + 145, + 12, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 112, + 114, + 111, + 103, + 145, + 9, + 104, + 101, + 108, + 108, + 111, + 84, + 97, + 103, + 52, + 0, + 196, + 0, + 1, + 0, + 3, + 0, + 0, + 0, + 77, + 13, + 145, + 12, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 112, + 114, + 111, + 103, + 145, + 9, + 104, + 101, + 108, + 108, + 111, + 84, + 97, + 103, + 53, + 0, + 196, + 0, + 1, + 0, + 4, + 0, + 0, + 0, + ], + "type": "Buffer", + }, + "tag_ids": Array [ + "f933545900d94fa18e26bf9495c807a5", + "6a094b0c9172a58a127de4f4e3d07b4d", + "b76fbe023a62902b18acbd368c5dec70", + "a7c5544db81347948c6c062509b7664d", + "a44f360e207bf89c9f2a379caeb459a4", + ], + }, +] +`; + +exports[`Tag Class Generate Write Requests Method Generates Appropriate Output On Large Tag Data 1`] = `Array []`; + +exports[`Tag Class Generate Write Requests Method Generates Appropriate Output On Large Tag Data 2`] = ` +Array [ + Object { + "data": Object { + "data": Array [ + 10, + 2, + 32, + 2, + 36, + 1, + 4, + 0, + 10, + 0, + 84, + 0, + 158, + 0, + 232, + 0, + 77, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 49, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 196, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 77, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 50, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 196, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 77, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 51, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 196, + 0, + 1, + 0, + 2, + 0, + 0, + 0, + 77, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 52, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 196, + 0, + 1, + 0, + 3, + 0, + 0, + 0, + ], + "type": "Buffer", + }, + "tag_ids": Array [ + "c00907cdbbe21da8405cb9619cf3ae03", + "21b6c868bb5d41ad7191d325eaaae487", + "29a77620e08c83b78ac32e2d4da75375", + "153cca324d6a853b129e62bed1c979fe", + ], + }, + Object { + "data": Object { + "data": Array [ + 10, + 2, + 32, + 2, + 36, + 1, + 1, + 0, + 4, + 0, + 77, + 32, + 145, + 20, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 58, + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 95, + 78, + 97, + 109, + 101, + 145, + 9, + 84, + 97, + 103, + 95, + 78, + 97, + 109, + 101, + 53, + 0, + 145, + 11, + 77, + 101, + 109, + 98, + 101, + 114, + 95, + 78, + 97, + 109, + 101, + 0, + 145, + 14, + 65, + 110, + 111, + 116, + 104, + 101, + 114, + 95, + 77, + 101, + 109, + 98, + 101, + 114, + 196, + 0, + 1, + 0, + 4, + 0, + 0, + 0, + ], + "type": "Buffer", + }, + "tag_ids": Array [ + "c620cebc92b9f3957fe6866747acba5a", + ], + }, +] +`; diff --git a/src/tag-group/index.js b/src/tag-group/index.js index afdb640..f5088b7 100644 --- a/src/tag-group/index.js +++ b/src/tag-group/index.js @@ -28,7 +28,7 @@ class TagGroup extends EventEmitter { * @memberof TagGroup */ get length() { - return Object.keys(this.tags).length; + return Object.keys(this.state.tags).length; } // endregion @@ -239,6 +239,16 @@ class TagGroup extends EventEmitter { this.state.tags[id].unstageWriteRequest(); } } + + /** + * Set all Tag Controllers + * + * @param {object} controller + * @memberof TagGroup + */ + setController(controller) { + this.forEach(tag => tag.controller = controller); + } // endregion // region Private Methods diff --git a/src/tag-group/tag-group.spec.js b/src/tag-group/tag-group.spec.js index eaa1bc1..0c56d1a 100644 --- a/src/tag-group/tag-group.spec.js +++ b/src/tag-group/tag-group.spec.js @@ -1,5 +1,6 @@ const TagGroup = require("./index"); const Tag = require("../tag"); +const Controller = require("../controller"); const { Types } = require("../enip/cip/data-types"); describe("Tag Class", () => { @@ -21,6 +22,24 @@ describe("Tag Class", () => { expect(group.generateReadMessageRequests()).toMatchSnapshot(); }); + + it("Generates Appropriate Output On Large Data", () => { + const tag1 = new Tag("Program:Program_Name.Tag_Name1.Member_Name.Another_Member", null, Types.DINT); + const tag2 = new Tag("Program:Program_Name.Tag_Name2.Member_Name.Another_Member", null, Types.DINT); + const tag3 = new Tag("Program:Program_Name.Tag_Name3.Member_Name.Another_Member", null, Types.DINT); + const tag4 = new Tag("Program:Program_Name.Tag_Name4.Member_Name.Another_Member", null, Types.DINT); + const tag5 = new Tag("Program:Program_Name.Tag_Name5.Member_Name.Another_Member", null, Types.DINT); + + const group = new TagGroup(); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + expect(group.generateReadMessageRequests()).toMatchSnapshot(); + }); }); describe("Generate Write Requests Method", () => { @@ -40,6 +59,232 @@ describe("Tag Class", () => { group.add(tag5); expect(group.generateWriteMessageRequests()).toMatchSnapshot(); + + tag1.value = 0; + tag2.value = 1; + tag3.value = 2; + tag4.value = 3; + tag5.value = 4; + + group.setController(new Controller()); + + expect(group.generateWriteMessageRequests()).toMatchSnapshot(); + }); + + it("Generates Appropriate Output On Large Tag Data", () => { + const tag1 = new Tag("Program:Program_Name.Tag_Name1.Member_Name.Another_Member", null, Types.DINT); + const tag2 = new Tag("Program:Program_Name.Tag_Name2.Member_Name.Another_Member", null, Types.DINT); + const tag3 = new Tag("Program:Program_Name.Tag_Name3.Member_Name.Another_Member", null, Types.DINT); + const tag4 = new Tag("Program:Program_Name.Tag_Name4.Member_Name.Another_Member", null, Types.DINT); + const tag5 = new Tag("Program:Program_Name.Tag_Name5.Member_Name.Another_Member", null, Types.DINT); + + const group = new TagGroup(); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + expect(group.generateWriteMessageRequests()).toMatchSnapshot(); + + tag1.value = 0; + tag2.value = 1; + tag3.value = 2; + tag4.value = 3; + tag5.value = 4; + + group.setController(new Controller()); + + expect(group.generateWriteMessageRequests()).toMatchSnapshot(); + }); + }); + + describe("Length Property", () => { + it("returns correct length property", () => { + const tag1 = new Tag("tag1"); + const tag2 = new Tag("tag2"); + const tag3 = new Tag("tag3"); + const tag4 = new Tag("tag4"); + const tag5 = new Tag("tag5"); + + const group = new TagGroup(); + + expect(group).toHaveLength(0); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + expect(group).toHaveLength(5); + }); + }); + + describe("Add Tag Method", () => { + it("adds tags correctly", () => { + const tag1 = new Tag("tag"); + + const group = new TagGroup(); + + expect(group).toHaveLength(0); + + group.add(tag1); + + expect(group).toHaveLength(1); + }); + + it("doesn't add tags more than once", () => { + const tag1 = new Tag("tag"); + + const group = new TagGroup(); + + expect(group).toHaveLength(0); + + group.add(tag1); + group.add(tag1); + + expect(group).toHaveLength(1); + }); + }); + + describe("Remove Tag Method", () => { + it("removes tags correctly", () => { + const tag1 = new Tag("tag"); + + const group = new TagGroup(); + + expect(group).toHaveLength(0); + + group.add(tag1); + + expect(group).toHaveLength(1); + + group.remove(tag1); + + expect(group).toHaveLength(0); + }); + + it("doesn't remove tags that do not exist", () => { + const tag1 = new Tag("tag1"); + const tag2 = new Tag("tag2"); + + const group = new TagGroup(); + + expect(group).toHaveLength(0); + + group.add(tag1); + + expect(group).toHaveLength(1); + + group.remove(tag2); + + expect(group).toHaveLength(1); + }); + }); + + describe("Parse Read Messgae Request Method", () => { + it("reads all tag message data", () => { + const tag1 = new Tag("tag1", null, Types.DINT); + const tag2 = new Tag("tag2", null, Types.DINT); + const tag3 = new Tag("tag3", null, Types.DINT); + const tag4 = new Tag("tag4", null, Types.DINT); + const tag5 = new Tag("tag5", null, Types.DINT); + + const group = new TagGroup(); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + group.setController(new Controller()); + + const ids = []; + const responses = []; + let value = 0; + group.forEach(tag => { + ids.push(tag.instance_id); + responses.push({data: Buffer.from(`c4000${value}000000`,"hex")}); + value++; + }); + + group.parseReadMessageResponses(responses,ids); + + expect(tag1.value).toEqual(0); + expect(tag2.value).toEqual(1); + expect(tag3.value).toEqual(2); + expect(tag4.value).toEqual(3); + expect(tag5.value).toEqual(4); + }); + }); + + describe("Parse Write Messgae Request Method", () => { + it("unstages wrtie on all tags", () => { + const tag1 = new Tag("tag1"); + const tag2 = new Tag("tag2"); + const tag3 = new Tag("tag3"); + const tag4 = new Tag("tag4"); + const tag5 = new Tag("tag5"); + + const group = new TagGroup(); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + tag1.value = 0; + tag2.value = 0; + tag3.value = 0; + tag4.value = 0; + tag5.value = 0; + + expect(tag1.write_ready).toBeTruthy(); + expect(tag2.write_ready).toBeTruthy(); + expect(tag3.write_ready).toBeTruthy(); + expect(tag4.write_ready).toBeTruthy(); + expect(tag5.write_ready).toBeTruthy(); + + const ids = []; + group.forEach(tag => ids.push(tag.instance_id)); + + group.parseWriteMessageRequests(null,ids); + + expect(tag1.write_ready).toBeFalsy(); + expect(tag2.write_ready).toBeFalsy(); + expect(tag3.write_ready).toBeFalsy(); + expect(tag4.write_ready).toBeFalsy(); + expect(tag5.write_ready).toBeFalsy(); + }); + }); + + describe("Set Controller Method", () => { + it("sets controller on all tags", () => { + const tag1 = new Tag("tag1"); + const tag2 = new Tag("tag2"); + const tag3 = new Tag("tag3"); + const tag4 = new Tag("tag4"); + const tag5 = new Tag("tag5"); + + const group = new TagGroup(); + + group.add(tag1); + group.add(tag2); + group.add(tag3); + group.add(tag4); + group.add(tag5); + + const plc = new Controller(); + + group.setController(plc); + + group.forEach(tag => { + expect(tag.controller).toMatchObject(plc); + }); }); }); }); diff --git a/src/tag/__snapshots__/tag.spec.js.snap b/src/tag/__snapshots__/tag.spec.js.snap index 844220e..9e43d91 100644 --- a/src/tag/__snapshots__/tag.spec.js.snap +++ b/src/tag/__snapshots__/tag.spec.js.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Tag Class Path property should return valid path 1`] = ` +Object { + "data": Array [ + 145, + 3, + 116, + 97, + 103, + 0, + ], + "type": "Buffer", +} +`; + exports[`Tag Class Read Message Generator Method Generates Appropriate Buffer 1`] = ` Object { "data": Array [ @@ -174,6 +188,44 @@ Object { } `; +exports[`Tag Class Read Message Generator Method Generates Appropriate Buffer 10`] = ` +Object { + "data": Array [ + 76, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 1, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Read Message Generator Method Generates Appropriate Buffer 11`] = ` +Object { + "data": Array [ + 76, + 4, + 145, + 3, + 116, + 97, + 103, + 0, + 40, + 0, + 1, + 0, + ], + "type": "Buffer", +} +`; + exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 1`] = ` Object { "data": Array [ @@ -395,3 +447,221 @@ Object { "type": "Buffer", } `; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 10`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 1, + 0, + 1, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 11`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 2, + 0, + 1, + 0, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 12`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 4, + 0, + 1, + 0, + 0, + 0, + 255, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 13`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 1, + 0, + 0, + 254, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 14`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 2, + 0, + 0, + 0, + 254, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 15`] = ` +Object { + "data": Array [ + 78, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 254, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 16`] = ` +Object { + "data": Array [ + 78, + 4, + 145, + 3, + 116, + 97, + 103, + 0, + 40, + 0, + 4, + 0, + 1, + 0, + 0, + 0, + 255, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 17`] = ` +Object { + "data": Array [ + 78, + 4, + 145, + 3, + 116, + 97, + 103, + 0, + 40, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 254, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Tag Class Write Message Generator Method Generates Appropriate Buffer 18`] = ` +Object { + "data": Array [ + 77, + 3, + 145, + 3, + 116, + 97, + 103, + 0, + 160, + 2, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; diff --git a/src/tag/index.js b/src/tag/index.js index df60e83..405eefe 100644 --- a/src/tag/index.js +++ b/src/tag/index.js @@ -13,8 +13,8 @@ class Tag extends EventEmitter { super(); if (!Tag.isValidTagname(tagname)) throw new Error("Tagname Must be of Type "); - if (!isValidTypeCode(datatype) && datatype !== null) - throw new Error("Datatype must be a Valid Type Code "); + if (!isValidTypeCode(datatype) && datatype !== null && typeof datatype !== "string") + throw new Error("Datatype must be a Valid Type Code or "); if (typeof keepAlive !== "number") throw new Error( `Tag expected keepAlive of type instead got type <${typeof keepAlive}>` @@ -43,7 +43,7 @@ class Tag extends EventEmitter { // Tag can not be both a bit index and BIT_STRING if (isBitString && isBitIndex) - throw "Tag cannot be defined as a BIT_STRING and have a bit index"; + throw new Error("Tag cannot be defined as a BIT_STRING and have a bit index"); if (isBitString) { // BIT_STRING need to be converted to array with bit index @@ -84,13 +84,14 @@ class Tag extends EventEmitter { this.state = { tag: { + bitIndex, + program, name: tagname, type: datatype, - bitIndex: bitIndex, value: null, + controller: null, controllerValue: null, path: pathBuf, - program: program, stage_write: false }, read_size: 0x01, @@ -160,29 +161,30 @@ class Tag extends EventEmitter { * @returns {string} datatype */ get type() { - return getTypeCodeString(this.state.tag.type); + const { type } = this.state.tag; + return getTypeCodeString(type) || type; } /** - * Gets Tag Bit Index - * - Returns null if no bit index has been assigned + * Sets Tag Datatype if Valid * * @memberof Tag - * @returns {number} bitIndex + * @property {number} Valid Datatype Code */ - get bitIndex() { - return this.state.tag.bitIndex; + set type(type) { + if (!isValidTypeCode(type) && typeof type !== "string") throw new Error("Datatype must be a Valid Type Code or "); + this.state.tag.type = type; } /** - * Sets Tag Datatype if Valid + * Gets Tag Bit Index + * - Returns null if no bit index has been assigned * * @memberof Tag - * @property {number} Valid Datatype Code + * @returns {number} bitIndex */ - set type(type) { - if (!isValidTypeCode(type)) throw new Error("Datatype must be a Valid Type Code "); - this.state.tag.type = type; + get bitIndex() { + return this.state.tag.bitIndex; } /** @@ -202,7 +204,7 @@ class Tag extends EventEmitter { * @property {number} read size */ set read_size(size) { - if (typeof type !== "number") + if (typeof size !== "number") throw new Error("Read Size must be a Valid Type Code "); this.state.read_size = size; } @@ -229,6 +231,27 @@ class Tag extends EventEmitter { this.state.tag.value = newValue; } + /** + * Gets Tag parent Controller + * - Returns null if no value has been read + * + * @memberof Tag + * @returns {object} controller + */ + get controller() { + return this.state.tag.controller; + } + + /** + * Sets Tag parent Controller + * + * @memberof Tag + * @property {object} new controller + */ + set controller(controller) { + this.state.tag.controller = controller; + } + /** * Sets Controller Tag Value and Emits Changed Event * @@ -236,7 +259,7 @@ class Tag extends EventEmitter { * @property {number|string|boolean|object} new value */ set controller_value(newValue) { - if (newValue !== this.state.tag.controllerValue) { + if (JSON.stringify(newValue) !== JSON.stringify(this.state.tag.controllerValue)) { const lastValue = this.state.tag.controllerValue; this.state.tag.controllerValue = newValue; @@ -356,81 +379,48 @@ class Tag extends EventEmitter { * @memberof Tag */ parseReadMessageResponse(data) { - // Set Type of Tag Read - const type = data.readUInt16LE(0); - this.state.tag.type = type; - - if (this.state.tag.bitIndex !== null) this.parseReadMessageResponseValueForBitIndex(data); - else this.parseReadMessageResponseValueForAtomic(data); - } - - /** - * Parses Good Read Request Messages Using A Mask For A Specified Bit Index - * - * @param {buffer} Data Returned from Successful Read Tag Request - * @memberof Tag - */ - parseReadMessageResponseValueForBitIndex(data) { const { tag } = this.state; - const { SINT, INT, DINT, BIT_STRING } = Types; + const { SINT, INT, DINT, BIT_STRING, STRUCT } = Types; - // Read Tag Value - /* eslint-disable indent */ - switch (this.state.tag.type) { - case SINT: - this.controller_value = - (data.readInt8(2) & (1 << tag.bitIndex)) == 0 ? false : true; - break; - case INT: - this.controller_value = - (data.readInt16LE(2) & (1 << tag.bitIndex)) == 0 ? false : true; - break; - case DINT: - case BIT_STRING: - this.controller_value = - (data.readInt32LE(2) & (1 << tag.bitIndex)) == 0 ? false : true; - break; - default: - throw new Error( - "Data Type other than SINT, INT, DINT, or BIT_STRING returned when a Bit Index was requested" - ); + const type = data.readUInt16LE(0); + if (!tag.type) tag.type = type; + else { + if (tag.type !== type && ( typeof tag.type !== "string" || type !== Types.STRUCT)) + throw new Error(`Type Read Mismatch - tag: ${tag.type} vs read: ${type}`); } - /* eslint-enable indent */ - } - /** - * Parses Good Read Request Messages For Atomic Data Types - * - * @param {buffer} Data Returned from Successful Read Tag Request - * @memberof Tag - */ - parseReadMessageResponseValueForAtomic(data) { - const { SINT, INT, DINT, REAL, BOOL } = Types; - - // Read Tag Value - /* eslint-disable indent */ - switch (this.state.tag.type) { - case SINT: - this.controller_value = data.readInt8(2); - break; - case INT: - this.controller_value = data.readInt16LE(2); - break; - case DINT: - this.controller_value = data.readInt32LE(2); - break; - case REAL: - this.controller_value = data.readFloatLE(2); - break; - case BOOL: - this.controller_value = data.readUInt8(2) !== 0; - break; - default: - throw new Error( - `Unrecognized Type Passed Read from Controller: ${this.state.tag.type}` - ); + // bit index local deserialization + if (tag.bitIndex !== null) + /* eslint-disable indent */ + switch (tag.type) { + case SINT: + this.controller_value = + (data.readInt8(2) & (1 << tag.bitIndex)) === 0 ? false : true; + break; + case INT: + this.controller_value = + (data.readInt16LE(2) & (1 << tag.bitIndex)) === 0 ? false : true; + break; + case DINT: + case BIT_STRING: + this.controller_value = + (data.readInt32LE(2) & (1 << tag.bitIndex)) === 0 ? false : true; + break; + default: + throw new Error( + "Data Type other than SINT, INT, DINT, or BIT_STRING returned when a Bit Index was requested" + ); + } + /* eslint-enable indent */ + // not a bit index - template deserialization + else{ + const template = this._getTemplate(); + if (type === STRUCT) { + template.structure_handle = data.readUInt16LE(2); + this.controller_value = template.deserialize(data.slice(4)); + } + else this.controller_value = template.deserialize(data.slice(2)); } - /* eslint-enable indent */ } /** @@ -442,133 +432,99 @@ class Tag extends EventEmitter { * @memberof Tag */ generateWriteMessageRequest(value = null, size = 0x01) { - if (value !== null) this.state.tag.value = value; - const { tag } = this.state; + const { SINT, INT, DINT, BIT_STRING } = Types; + + if (value !== null) tag.value = value; - if (tag.type === null) + if (tag.type === null ) throw new Error( `Tag ${ tag.name } has not been initialized. Try reading the tag from the controller first or manually providing a valid CIP datatype.` ); - if (tag.bitIndex !== null) return this.generateWriteMessageRequestForBitIndex(tag.value); - else return this.generateWriteMessageRequestForAtomic(tag.value, size); - } - - /** - * Generates Write Tag Message For A Bit Index - * - * @param {number|boolean|object|string} value - * @param {number} size - * @returns {buffer} - Write Tag Message Service - * @memberof Tag - */ - generateWriteMessageRequestForBitIndex(value) { - const { tag } = this.state; - const { SINT, INT, DINT, BIT_STRING } = Types; - - // Build Message Router to Embed in UCMM let buf = null; - /* eslint-disable indent */ - switch (tag.type) { - case SINT: - buf = Buffer.alloc(4); - buf.writeInt16LE(1); //mask length - buf.writeUInt8(value ? 1 << tag.bitIndex : 0, 2); // or mask - buf.writeUInt8(value ? 255 : 255 & ~(1 << tag.bitIndex), 3); // and mask - break; - case INT: - buf = Buffer.alloc(6); - buf.writeInt16LE(2); //mask length - buf.writeUInt16LE(value ? 1 << tag.bitIndex : 0, 2); // or mask - buf.writeUInt16LE(value ? 65535 : 65535 & ~(1 << tag.bitIndex), 4); // and mask - break; - case DINT: - case BIT_STRING: - buf = Buffer.alloc(10); - buf.writeInt16LE(4); //mask length - buf.writeInt32LE(value ? 1 << tag.bitIndex : 0, 2); // or mask - buf.writeInt32LE(value ? -1 : -1 & ~(1 << tag.bitIndex), 6); // and mask - break; - default: - throw new Error( - "Bit Indexes can only be used on SINT, INT, DINT, or BIT_STRING data types." - ); + // bit index = local serialization + if (tag.bitIndex !== null){ + /* eslint-disable indent */ + switch (tag.type) { + case SINT: + buf = Buffer.alloc(4); + buf.writeInt16LE(1); //mask length + buf.writeUInt8(tag.value ? 1 << tag.bitIndex : 0, 2); // or mask + buf.writeUInt8(tag.value ? 255 : 255 & ~(1 << tag.bitIndex), 3); // and mask + break; + case INT: + buf = Buffer.alloc(6); + buf.writeInt16LE(2); //mask length + buf.writeUInt16LE(tag.value ? 1 << tag.bitIndex : 0, 2); // or mask + buf.writeUInt16LE(tag.value ? 65535 : 65535 & ~(1 << tag.bitIndex), 4); // and mask + break; + case DINT: + case BIT_STRING: + buf = Buffer.alloc(10); + buf.writeInt16LE(4); //mask length + buf.writeInt32LE(tag.value ? 1 << tag.bitIndex : 0, 2); // or mask + buf.writeInt32LE(tag.value ? -1 : -1 & ~(1 << tag.bitIndex), 6); // and mask + break; + default: + throw new Error( + "Bit Indexes can only be used on SINT, INT, DINT, or BIT_STRING data types." + ); + } + + // Build Current Message + return MessageRouter.build(READ_MODIFY_WRITE_TAG, tag.path, buf); } - // Build Current Message - return MessageRouter.build(READ_MODIFY_WRITE_TAG, tag.path, buf); + const template = this._getTemplate(); + + // default - template serialization + if (typeof tag.type === "string"){ + buf = Buffer.alloc(6); + buf.writeUInt16LE(Types.STRUCT, 0); + buf.writeUInt16LE(template.structure_handle, 2); + buf.writeUInt16LE(size, 4); + } else { + buf = Buffer.alloc(4); + buf.writeUInt16LE(tag.type, 0); + buf.writeUInt16LE(size, 2); + } + return MessageRouter.build(WRITE_TAG, tag.path, Buffer.concat([buf,template.serialize(tag.value)])); } /** - * Generates Write Tag Message For Atomic Types + * Unstages Value Edit * - * @param {number|boolean|object|string} value - * @param {number} size - * @returns {buffer} - Write Tag Message Service * @memberof Tag */ - generateWriteMessageRequestForAtomic(value, size) { + unstageWriteRequest() { const { tag } = this.state; - const { SINT, INT, DINT, REAL, BOOL } = Types; - // Build Message Router to Embed in UCMM - let buf = Buffer.alloc(4); - let valBuf = null; - buf.writeUInt16LE(tag.type, 0); - buf.writeUInt16LE(size, 2); - - /* eslint-disable indent */ - switch (tag.type) { - case SINT: - valBuf = Buffer.alloc(1); - valBuf.writeInt8(tag.value); - - buf = Buffer.concat([buf, valBuf]); - break; - case INT: - valBuf = Buffer.alloc(2); - valBuf.writeInt16LE(tag.value); - - buf = Buffer.concat([buf, valBuf]); - break; - case DINT: - valBuf = Buffer.alloc(4); - valBuf.writeInt32LE(tag.value); - - buf = Buffer.concat([buf, valBuf]); - break; - case REAL: - valBuf = Buffer.alloc(4); - valBuf.writeFloatLE(tag.value); - - buf = Buffer.concat([buf, valBuf]); - break; - case BOOL: - valBuf = Buffer.alloc(1); - if (!tag.value) valBuf.writeInt8(0x00); - else valBuf.writeInt8(0x01); - - buf = Buffer.concat([buf, valBuf]); - break; - default: - throw new Error(`Unrecognized Type to Write to Controller: ${tag.type}`); - } - - // Build Current Message - return MessageRouter.build(WRITE_TAG, tag.path, buf); + tag.stage_write = false; + tag.value = tag.controllerValue; } + // endregion + // region Private Methods /** - * Unstages Value Edit + * Gets Tag Template * * @memberof Tag */ - unstageWriteRequest() { - this.state.tag.stage_write = false; - this.state.tag.value = this.state.controllerValue; + _getTemplate() { + const { tag } = this.state; + + if (!tag.controller) throw new Error("Template read error - tag controller property not set"); + if (!tag.controller.templates) throw new Error("Template read error - tag controller templates property not set"); + if (!tag.type) throw new Error("Template read error - tag type property not set"); + + const template = tag.controller.templates[tag.type]; + + if (!template) throw new Error(`Template read error - cannot find template for type: ${tag.type}`); + + return template; } // endregion diff --git a/src/tag/tag.spec.js b/src/tag/tag.spec.js index c1fe0e3..f248daf 100644 --- a/src/tag/tag.spec.js +++ b/src/tag/tag.spec.js @@ -1,4 +1,5 @@ const Tag = require("./index"); +const Controller = require("../controller"); const { Types } = require("../enip/cip/data-types"); describe("Tag Class", () => { @@ -14,6 +15,7 @@ describe("Tag Class", () => { expect(fn("someTag", "prog", Types.EPATH)).not.toThrow(); expect(fn("someTag", "prog", 0xc1)).not.toThrow(); expect(fn("tag[0].0", null, Types.BIT_STRING)).toThrow(); + expect(fn("tag.32")).toThrow(); }); }); @@ -88,6 +90,8 @@ describe("Tag Class", () => { const tag7 = new Tag("tag[0]", null, Types.DINT); // test single dim array const tag8 = new Tag("tag[0,0]", null, Types.DINT); // test 2 dim array const tag9 = new Tag("tag[0,0,0]", null, Types.DINT); // test 3 dim array + const tag10 = new Tag("tag.0", null, Types.DINT); // test bit index + const tag11 = new Tag("tag[0]", null, Types.BIT_STRING); // test 3 dim array expect(tag1.generateReadMessageRequest()).toMatchSnapshot(); expect(tag2.generateReadMessageRequest()).toMatchSnapshot(); @@ -98,11 +102,111 @@ describe("Tag Class", () => { expect(tag7.generateReadMessageRequest()).toMatchSnapshot(); expect(tag8.generateReadMessageRequest()).toMatchSnapshot(); expect(tag9.generateReadMessageRequest()).toMatchSnapshot(); + expect(tag10.generateReadMessageRequest()).toMatchSnapshot(); + expect(tag11.generateReadMessageRequest()).toMatchSnapshot(); + }); + }); + + describe("Parse Read Message Response Method", () => { + it("should set type if not already set", () => { + const tag = new Tag("tag"); + + tag.controller = new Controller(); + + tag.parseReadMessageResponse(Buffer.from("c40000000000","hex")); + + expect(tag.type).toMatch("DINT"); + }); + + it("should throw if type doesn't match", () => { + const tag1 = new Tag("tag",null,Types.DINT); + const tag2 = new Tag("tag",null,"udt"); + + tag1.controller = new Controller(); + tag2.controller = new Controller(); + + expect(() => tag1.parseReadMessageResponse(Buffer.from("c10000000000","hex"))).toThrow(); + expect(() => tag2.parseReadMessageResponse(Buffer.from("c100000000000000","hex"))).toThrow(); + }); + + it("should not overwrite data type if type does match", () => { + const plc = new Controller(); + plc.addTemplate({name: "udt", definition: { member1: Types.DINT}}); + + const tag1 = new Tag("tag",null,Types.DINT); + const tag2 = new Tag("tag",null,"udt"); + + const tag1Type = tag1.type; + const tag2Type = tag2.type; + + tag1.controller = plc; + tag2.controller = plc; + + tag1.parseReadMessageResponse(Buffer.from("c40000000000","hex")); + tag2.parseReadMessageResponse(Buffer.from("a002000000000000","hex")); + + expect(tag1.type).toMatch(tag1Type); + expect(tag2.type).toMatch(tag2Type); + }); + + it("should set udt template structure handle", () => { + const plc = new Controller(); + plc.addTemplate({name: "udt", definition: { member1: Types.DINT}}); + + const tag1 = new Tag("tag",null,"udt"); + + tag1.controller = plc; + + tag1.parseReadMessageResponse(Buffer.from("a002aef300000000","hex")); + + expect(tag1._getTemplate().structure_handle).toEqual(Buffer.from("aef3","hex").readUInt16LE()); + }); + + it("should deserialize data", () => { + const plc = new Controller(); + + const tag1 = new Tag("tag",null, Types.DINT); + const tag2 = new Tag("tag.0",null, Types.SINT); + const tag3 = new Tag("tag.0",null, Types.INT); + const tag4 = new Tag("tag.0",null, Types.DINT); + + tag1.controller = plc; + tag2.controller = plc; + tag3.controller = plc; + tag4.controller = plc; + + tag1.parseReadMessageResponse(Buffer.from("c40001000000","hex")); + tag2.parseReadMessageResponse(Buffer.from("c20001000000","hex")); + tag3.parseReadMessageResponse(Buffer.from("c30001000000","hex")); + tag4.parseReadMessageResponse(Buffer.from("c40001000000","hex")); + + expect(tag1.value).toEqual(1); + expect(tag2.value).toEqual(true); + expect(tag3.value).toEqual(true); + expect(tag4.value).toEqual(true); + + tag2.parseReadMessageResponse(Buffer.from("c200feffffff","hex")); + tag3.parseReadMessageResponse(Buffer.from("c300feffffff","hex")); + tag4.parseReadMessageResponse(Buffer.from("c400feffffff","hex")); + + expect(tag2.value).toEqual(false); + expect(tag3.value).toEqual(false); + expect(tag4.value).toEqual(false); + }); + + it("should throw on bit index with wrong data type", () => { + const tag = new Tag("tag.0",null, Types.REAL); + tag.controller = new Controller(); + expect(() => tag.parseReadMessageResponse(Buffer.from("ca0000000000","hex"))).toThrow(); }); }); describe("Write Message Generator Method", () => { it("Generates Appropriate Buffer", () => { + const plc = new Controller(); + + plc.addTemplate({name: "udt",definition:{member1: Types.DINT}}); + const tag1 = new Tag("tag", null, Types.DINT); const tag2 = new Tag("tag", null, Types.BOOL); const tag3 = new Tag("tag", null, Types.REAL); @@ -112,6 +216,29 @@ describe("Tag Class", () => { const tag7 = new Tag("tag[0]", null, Types.DINT); // test single dim array const tag8 = new Tag("tag[0,0]", null, Types.DINT); // test 2 dim array const tag9 = new Tag("tag[0,0,0]", null, Types.DINT); // test 3 dim array + const tag10 = new Tag("tag.0", null, Types.SINT); // test sint bit index + const tag11 = new Tag("tag.0", null, Types.INT); // test int bit index + const tag12 = new Tag("tag.0", null, Types.DINT); // test dint bit index + const tag13 = new Tag("tag[0]", null, Types.BIT_STRING); // test bit string + const tag14 = new Tag("tag", null, "udt"); // test udt + + // required for template lookup + tag1.controller = plc; + tag2.controller = plc; + tag3.controller = plc; + tag4.controller = plc; + tag5.controller = plc; + tag6.controller = plc; + tag7.controller = plc; + tag8.controller = plc; + tag9.controller = plc; + tag10.controller = plc; + tag11.controller = plc; + tag12.controller = plc; + tag13.controller = plc; + tag14.controller = plc; + + tag14.parseReadMessageResponse(Buffer.from("a002000000000000","hex")); expect(tag1.generateWriteMessageRequest(100)).toMatchSnapshot(); expect(tag2.generateWriteMessageRequest(true)).toMatchSnapshot(); @@ -122,6 +249,143 @@ describe("Tag Class", () => { expect(tag7.generateWriteMessageRequest(99)).toMatchSnapshot(); expect(tag8.generateWriteMessageRequest(99)).toMatchSnapshot(); expect(tag9.generateWriteMessageRequest(99)).toMatchSnapshot(); + expect(tag10.generateWriteMessageRequest(true)).toMatchSnapshot(); + expect(tag11.generateWriteMessageRequest(true)).toMatchSnapshot(); + expect(tag12.generateWriteMessageRequest(true)).toMatchSnapshot(); + expect(tag10.generateWriteMessageRequest(false)).toMatchSnapshot(); + expect(tag11.generateWriteMessageRequest(false)).toMatchSnapshot(); + expect(tag12.generateWriteMessageRequest(false)).toMatchSnapshot(); + expect(tag13.generateWriteMessageRequest(true)).toMatchSnapshot(); + expect(tag13.generateWriteMessageRequest(false)).toMatchSnapshot(); + expect(tag14.generateWriteMessageRequest()).toMatchSnapshot(); + }); + + it("should throw on bit index with wrong data type", () => { + const tag = new Tag("tag.0",null, Types.REAL); + tag.controller = new Controller(); + expect(() => tag.generateWriteMessageRequest(true)).toThrow(); + }); + + it("should throw on type not initialized", () => { + const tag = new Tag("tag"); + tag.controller = new Controller(); + expect(() => tag.generateWriteMessageRequest(0)).toThrow(); + }); + }); + + describe("Name Property", () => { + it("should get and set name propery", () => { + const tag = new Tag("tag"); + + expect(tag.name).toMatch("tag"); + + tag.name = "newname"; + expect(tag.name).toMatch("newname"); + + const progTag = new Tag("tag","prog"); + expect(progTag.name).toMatch("Program:prog.tag"); + }); + + it("should throw on invalid names", () => { + const tag = new Tag("tag"); + expect(() => tag.name = 1).toThrow(); + expect(() => tag.name = true).toThrow(); + expect(() => tag.name = null).toThrow(); + expect(() => tag.name = {}).toThrow(); + }); + }); + + describe("Type Property", () => { + it("should type propery", () => { + const tag = new Tag("tag", null, Types.DINT); + + expect(tag.type).toMatch("DINT"); + + tag.type = Types.REAL; + expect(tag.type).toMatch("REAL"); + + tag.type = "udt"; + expect(tag.type).toMatch("udt"); + }); + + it("should throw on invalid types", () => { + const tag = new Tag("tag"); + expect(() => tag.type = 5).toThrow(); + expect(() => tag.name = null).toThrow(); + expect(() => tag.name = {}).toThrow(); + }); + }); + + describe("Read Size Property", () => { + it("should get and set read size propery", () => { + const tag = new Tag("tag"); + + expect(tag.read_size).toEqual(1); + + tag.read_size = 2; + expect(tag.read_size).toEqual(2); + + tag.read_size = 3; + expect(tag.read_size).toEqual(3); + }); + + it("should throw on invalid sizes", () => { + const tag = new Tag("tag"); + expect(() => tag.read_size = "asfd").toThrow(); + expect(() => tag.read_size = true).toThrow(); + expect(() => tag.read_size = null).toThrow(); + expect(() => tag.read_size = {}).toThrow(); + }); + }); + + describe("Controller Value Property", () => { + it("should get and set controller value", () => { + const tag = new Tag("tag",null, Types.DINT); + + tag.controller_value = true; + expect(tag.controller_value).toBeTruthy(); + + tag.controller_value = 1; + expect(tag.controller_value).toEqual(1); + + tag.controller_value = 5.0; + expect(tag.controller_value).toEqual(5.0); + }); + }); + + describe("Timestamp property", () => { + it("should return valid timestamp", () => { + const tag = new Tag("tag",null, Types.DINT); + + const preDate = new Date(); + tag.controller_value = 0; + const postDate = new Date(); + + const timestamp = new Date(tag.timestamp); + + expect(timestamp >= preDate).toBeTruthy(); + expect(timestamp <= postDate).toBeTruthy(); + }); + }); + + describe("Timestamp Raw property", () => { + it("should return valid timestamp", () => { + const tag = new Tag("tag",null, Types.DINT); + + const preDate = new Date(); + tag.controller_value = 0; + const postDate = new Date(); + + expect(tag.timestamp_raw >= preDate).toBeTruthy(); + expect(tag.timestamp_raw <= postDate).toBeTruthy(); + }); + }); + + describe("Path property", () => { + it("should return valid path", () => { + const tag = new Tag("tag"); + + expect(tag.path).toMatchSnapshot(); }); }); @@ -155,4 +419,62 @@ describe("Tag Class", () => { expect(testTag.bitIndex).toEqual(5); }); }); -}); + + describe("get template method", () => { + it("should throw on missing data", () => { + const testTag = new Tag("tag"); + + expect(() => testTag._getTemplate()).toThrow(); + + testTag.controller = {}; + expect(() => testTag._getTemplate()).toThrow(); + + testTag.controller = new Controller(); + expect(() => testTag._getTemplate()).toThrow(); + + testTag.type = "asdf"; + expect(() => testTag._getTemplate()).toThrow(); + + }); + + it("should find valid types", () => { + const plc = new Controller(); + + const tag1 = new Tag("tag", null, Types.BOOL); + const tag2 = new Tag("tag", null, Types.SINT); + const tag3 = new Tag("tag", null, Types.INT); + const tag4 = new Tag("tag", null, Types.DINT); + const tag5 = new Tag("tag", null, Types.LINT); + const tag6 = new Tag("tag", null, Types.REAL); + const tag7 = new Tag("tag", null, "STRING"); + + tag1.controller = plc; + tag2.controller = plc; + tag3.controller = plc; + tag4.controller = plc; + tag5.controller = plc; + tag6.controller = plc; + tag7.controller = plc; + + expect(() => tag1._getTemplate()).not.toThrow(); + expect(() => tag2._getTemplate()).not.toThrow(); + expect(() => tag3._getTemplate()).not.toThrow(); + expect(() => tag4._getTemplate()).not.toThrow(); + expect(() => tag5._getTemplate()).not.toThrow(); + expect(() => tag6._getTemplate()).not.toThrow(); + expect(() => tag7._getTemplate()).not.toThrow(); + }); + }); + + describe("Stage Write", () => { + it("should set stage write on value write", () => { + const tag = new Tag("tag"); + + expect(tag.write_ready).toBeFalsy(); + + tag.value = 1; + + expect(tag.write_ready).toBeTruthy(); + }); + }); +}); \ No newline at end of file diff --git a/src/template/__snapshots__/template.spec.js.snap b/src/template/__snapshots__/template.spec.js.snap new file mode 100644 index 0000000..56c78a7 --- /dev/null +++ b/src/template/__snapshots__/template.spec.js.snap @@ -0,0 +1,259 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Template Class Large UDT Testing should properly map, serialize, and deserialize large UDTs 1`] = ` +Object { + "member1": Array [ + Object { + "member1": 0, + "member2": 0, + "member3": 0, + "member4": Array [ + 0, + 0, + ], + }, + Object { + "member1": 0, + "member2": 0, + "member3": 0, + "member4": Array [ + 0, + 0, + ], + }, + ], + "member2": false, + "member3": false, + "member4": false, + "member5": Object { + "member1": Object { + "DATA": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "LEN": 0, + "getString": [Function], + "setString": [Function], + }, + "member2": Array [ + 0, + 0, + ], + }, +} +`; + +exports[`Template Class Large UDT Testing should properly map, serialize, and deserialize large UDTs 2`] = ` +Object { + "data": Array [ + 1, + 0, + 0, + 0, + 2, + 3, + 0, + 0, + 4, + 0, + 5, + 0, + 6, + 0, + 0, + 0, + 7, + 8, + 0, + 0, + 9, + 0, + 10, + 0, + 5, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 110, + 101, + 119, + 32, + 115, + 116, + 114, + 105, + 110, + 103, + 0, + 0, + 121, + 233, + 246, + 66, + 254, + 100, + 228, + 67, + ], + "type": "Buffer", +} +`; + +exports[`Template Class Large UDT Testing should properly map, serialize, and deserialize large UDTs 3`] = ` +Object { + "member1": Array [ + Object { + "member1": 1, + "member2": 2, + "member3": 3, + "member4": Array [ + 4, + 5, + ], + }, + Object { + "member1": 6, + "member2": 7, + "member3": 8, + "member4": Array [ + 9, + 10, + ], + }, + ], + "member2": true, + "member3": false, + "member4": true, + "member5": Object { + "member1": Object { + "DATA": Array [ + 110, + 101, + 119, + 32, + 115, + 116, + 114, + 105, + 110, + 103, + ], + "LEN": 10, + "getString": [Function], + "setString": [Function], + }, + "member2": Array [ + 123.45600128173828, + 456.78900146484375, + ], + }, +} +`; + +exports[`Template Class Strings should build a string definition if given a string length 1`] = ` +Object { + "data": Array [ + 4, + 0, + 0, + 0, + 116, + 116, + 116, + 116, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; diff --git a/src/template/atomics/__snapshots__/atomics.spec.js.snap b/src/template/atomics/__snapshots__/atomics.spec.js.snap new file mode 100644 index 0000000..923d7c7 --- /dev/null +++ b/src/template/atomics/__snapshots__/atomics.spec.js.snap @@ -0,0 +1,557 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 1`] = ` +Object { + "high": 0, + "low": 0, +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 2`] = ` +Object { + "high": 0, + "low": 0, +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 3`] = ` +Object { + "high": 0, + "low": 1, +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 4`] = ` +Object { + "high": -1, + "low": -1, +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 5`] = ` +Object { + "high": 0, + "low": 123, +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should deserialize all atomic types correctly 6`] = ` +Object { + "DATA": Array [ + 110, + 101, + 119, + 32, + 116, + 101, + 120, + 116, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "LEN": 8, + "getString": [Function], + "setString": [Function], +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 1`] = ` +Object { + "data": Array [ + 1, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 2`] = ` +Object { + "data": Array [ + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 3`] = ` +Object { + "data": Array [ + 128, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 4`] = ` +Object { + "data": Array [ + 127, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 5`] = ` +Object { + "data": Array [ + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 6`] = ` +Object { + "data": Array [ + 1, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 7`] = ` +Object { + "data": Array [ + 255, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 8`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 123, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 9`] = ` +Object { + "data": Array [ + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 10`] = ` +Object { + "data": Array [ + 1, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 11`] = ` +Object { + "data": Array [ + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 12`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 123, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 13`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 14`] = ` +Object { + "data": Array [ + 1, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 15`] = ` +Object { + "data": Array [ + 255, + 255, + 255, + 255, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 16`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 123, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 17`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 18`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 19`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 20`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 21`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 22`] = ` +Object { + "data": Array [ + 0, + 0, + 128, + 63, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 23`] = ` +Object { + "data": Array [ + 0, + 0, + 128, + 191, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 24`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 246, + 66, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; + +exports[`Atomic Templates Check Serialization and Deserialization should serialize all atomic types correctly 25`] = ` +Object { + "data": Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; diff --git a/src/template/atomics/atomics.spec.js b/src/template/atomics/atomics.spec.js new file mode 100644 index 0000000..e124b01 --- /dev/null +++ b/src/template/atomics/atomics.spec.js @@ -0,0 +1,118 @@ +const TemplateMap = require("./index"); +const Template = require("../../template"); +const { Types } = require("../../enip/cip/data-types"); + +describe("Atomic Templates", () => { + + describe("New Instance", () => { + it("Doesn't Throw When Creating a New Map",()=>{ + expect(() => TemplateMap()).not.toThrow(); + }); + }); + + describe("Check For Atomic Types", ()=>{ + it("has all atomic types",()=>{ + const map = TemplateMap(); + + expect(Object.keys(map)).toHaveLength(7); + + expect(map[Types.BOOL]).toBeInstanceOf(Template); + expect(map[Types.SINT]).toBeInstanceOf(Template); + expect(map[Types.INT]).toBeInstanceOf(Template); + expect(map[Types.DINT]).toBeInstanceOf(Template); + expect(map[Types.LINT]).toBeInstanceOf(Template); + expect(map[Types.REAL]).toBeInstanceOf(Template); + expect(map["STRING"]).toBeInstanceOf(Template); + }); + }); + + describe("Check Serialization and Deserialization",()=>{ + it ("should serialize all atomic types correctly",()=>{ + const map = TemplateMap(); + + expect(map[Types.BOOL].serialize(true)).toMatchSnapshot(); + expect(map[Types.BOOL].serialize(false)).toMatchSnapshot(); + expect(map[Types.BOOL].serialize(true,Buffer.alloc(1),7)).toMatchSnapshot(); + expect(map[Types.BOOL].serialize(false,Buffer.from("FF","hex"),7)).toMatchSnapshot(); + + expect(map[Types.SINT].serialize(0)).toMatchSnapshot(); + expect(map[Types.SINT].serialize(1)).toMatchSnapshot(); + expect(map[Types.SINT].serialize(-1)).toMatchSnapshot(); + expect(map[Types.SINT].serialize(123,Buffer.alloc(16),32)).toMatchSnapshot(); + + expect(map[Types.INT].serialize(0)).toMatchSnapshot(); + expect(map[Types.INT].serialize(1)).toMatchSnapshot(); + expect(map[Types.INT].serialize(-1)).toMatchSnapshot(); + expect(map[Types.INT].serialize(123,Buffer.alloc(16),32)).toMatchSnapshot(); + + expect(map[Types.DINT].serialize(0)).toMatchSnapshot(); + expect(map[Types.DINT].serialize(1)).toMatchSnapshot(); + expect(map[Types.DINT].serialize(-1)).toMatchSnapshot(); + expect(map[Types.DINT].serialize(123,Buffer.alloc(16),32)).toMatchSnapshot(); + + expect(map[Types.LINT].serialize(0)).toMatchSnapshot(); + expect(map[Types.LINT].serialize(1)).toMatchSnapshot(); + expect(map[Types.LINT].serialize(-1)).toMatchSnapshot(); + expect(map[Types.LINT].serialize(123,Buffer.alloc(16),32)).toMatchSnapshot(); + + expect(map[Types.REAL].serialize(0.0)).toMatchSnapshot(); + expect(map[Types.REAL].serialize(1.0)).toMatchSnapshot(); + expect(map[Types.REAL].serialize(-1.0)).toMatchSnapshot(); + expect(map[Types.REAL].serialize(123.0,Buffer.alloc(16),32)).toMatchSnapshot(); + + expect(map["STRING"].serialize("test string")).toMatchSnapshot(); + + }); + it ("should deserialize all atomic types correctly",()=>{ + const map = TemplateMap(); + + expect(map[Types.BOOL].deserialize()).toBeFalsy(); + expect(map[Types.BOOL].deserialize(Buffer.from("00","hex"))).toBeFalsy(); + expect(map[Types.BOOL].deserialize(Buffer.from("FF","hex"))).toBeTruthy(); + expect(map[Types.BOOL].deserialize(Buffer.from("80","hex"),7)).toBeTruthy(); + expect(map[Types.BOOL].deserialize(Buffer.from("80","hex"),6)).toBeFalsy(); + + expect(map[Types.SINT].deserialize()).toEqual(0); + expect(map[Types.SINT].deserialize(Buffer.from("00","hex"))).toEqual(0); + expect(map[Types.SINT].deserialize(Buffer.from("01","hex"))).toEqual(1); + expect(map[Types.SINT].deserialize(Buffer.from("FF","hex"))).toEqual(-1); + expect(map[Types.SINT].deserialize(Buffer.from("000000007b0000000000000000000000","hex"),32)).toEqual(123); + + expect(map[Types.INT].deserialize()).toEqual(0); + expect(map[Types.INT].deserialize(Buffer.from("0000","hex"))).toEqual(0); + expect(map[Types.INT].deserialize(Buffer.from("0100","hex"))).toEqual(1); + expect(map[Types.INT].deserialize(Buffer.from("FFFF","hex"))).toEqual(-1); + expect(map[Types.INT].deserialize(Buffer.from("000000007b0000000000000000000000","hex"),32)).toEqual(123); + + expect(map[Types.DINT].deserialize()).toEqual(0); + expect(map[Types.DINT].deserialize(Buffer.from("00000000","hex"))).toEqual(0); + expect(map[Types.DINT].deserialize(Buffer.from("01000000","hex"))).toEqual(1); + expect(map[Types.DINT].deserialize(Buffer.from("FFFFFFFF","hex"))).toEqual(-1); + expect(map[Types.DINT].deserialize(Buffer.from("000000007b0000000000000000000000","hex"),32)).toEqual(123); + + expect(map[Types.LINT].deserialize()).toMatchSnapshot(); + expect(map[Types.LINT].deserialize(Buffer.from("0000000000000000","hex"))).toMatchSnapshot(); + expect(map[Types.LINT].deserialize(Buffer.from("0100000000000000","hex"))).toMatchSnapshot(); + expect(map[Types.LINT].deserialize(Buffer.from("FFFFFFFFFFFFFFFF","hex"))).toMatchSnapshot(); + expect(map[Types.LINT].deserialize(Buffer.from("000000007b0000000000000000000000","hex"),32)).toMatchSnapshot(); + + expect(map[Types.REAL].deserialize()).toEqual(0); + expect(map[Types.REAL].deserialize(Buffer.from("00000000","hex"))).toEqual(0); + expect(map[Types.REAL].deserialize(Buffer.from("0000803F","hex"))).toEqual(1); + expect(map[Types.REAL].deserialize(Buffer.from("000080bF","hex"))).toEqual(-1.0); + expect(map[Types.REAL].deserialize(Buffer.from("000000000000F6420000000000000000","hex"),32)).toEqual(123); + + let testString = Buffer.alloc(88); + testString.writeInt32LE(8); + testString.writeInt8(110,4); + testString.writeInt8(101,5); + testString.writeInt8(119,6); + testString.writeInt8(32,7); + testString.writeInt8(116,8); + testString.writeInt8(101,9); + testString.writeInt8(120,10); + testString.writeInt8(116,11); + expect(map["STRING"].deserialize(testString)).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/src/template/atomics/index.js b/src/template/atomics/index.js new file mode 100644 index 0000000..bc93485 --- /dev/null +++ b/src/template/atomics/index.js @@ -0,0 +1,78 @@ +const { Types: { BOOL, SINT, INT, DINT, LINT, REAL } } = require("../../enip/cip/data-types"); +const Template = require("../../template"); + +module.exports = () => { + const templates = { + [BOOL]: new Template({ + size: 1, + alignment: 8, + consecutive_alignment: 1, + serialize(value,data=Buffer.alloc(1),offset=0){ + const bit_offset = offset % 8; + const byte_offset = ( offset - bit_offset ) / 8; + let byte_value = data.readUInt8(byte_offset); + data.writeUInt8(value ? byte_value | 1 << bit_offset : byte_value & (255 & ~(1 << bit_offset)), byte_offset); + return data; + }, + deserialize(data=Buffer.alloc(1),offset=0){ + const bit_offset = offset % 8; + const byte_offset = ( offset - bit_offset ) / 8; + return (data.readInt8(byte_offset) & (1 << bit_offset)) === 0 ? false : true; + } + }), + [SINT]: new Template({ + size: 8, + alignment: 8, + serialize(value,data=Buffer.alloc(1),offset=0){ + data.writeInt8(value,offset/8); + return data; + }, + deserialize: (data=Buffer.alloc(1),offset=0) => data.readInt8(offset/8) + }), + [INT]: new Template({ + size: 16, + alignment: 16, + serialize(value,data=Buffer.alloc(2),offset=0){ + data.writeInt16LE(value,offset/8); + return data; + }, + deserialize: (data=Buffer.alloc(2),offset=0) => data.readInt16LE(offset/8) + }), + [DINT]: new Template({ + size: 32, + serialize(value,data=Buffer.alloc(4),offset=0){ + data.writeInt32LE(value,offset/8); + return data; + }, + deserialize: (data=Buffer.alloc(4),offset=0) => data.readInt32LE(offset/8) + }), + [LINT]: new Template({ + size: 64, + alignment: 64, + size_multiple: 64, + serialize(value,data=Buffer.alloc(8),offset=0){ + data.writeInt32LE(value.low,offset/8); + data.writeInt32LE(value.high,offset/8 + 4); + return data; + }, + deserialize: (data=Buffer.alloc(8),offset=0) => { + return { + low: data.readInt32LE(offset/8), + high: data.readInt32LE(offset/8 + 4) + }; + } + }), + [REAL]: new Template({ + size: 32, + serialize(value,data=Buffer.alloc(4),offset=0){ + data.writeFloatLE(value,offset/8); + return data; + }, + deserialize: (data=Buffer.alloc(4),offset=0) => data.readFloatLE(offset/8) + }) + }; + + new Template({name: "STRING",string_length: 82}).addToTemplates(templates); + + return templates; +}; \ No newline at end of file diff --git a/src/template/index.js b/src/template/index.js new file mode 100644 index 0000000..f061212 --- /dev/null +++ b/src/template/index.js @@ -0,0 +1,352 @@ +const { Types } = require("../enip/cip/data-types"); + +class Template{ + constructor({ + name, + definition, + size, + alignment=32, + consecutive_alignment, + size_multiple=32, + string_length, + //buffer_definition, + //l5x_definition, + serialize, + deserialize }){ + + // default consecutive alignment to alignment if none specified + if (!consecutive_alignment) + consecutive_alignment = alignment; + + // build definition + if (string_length) + definition = this._buildDefinitionFromStringLength(string_length); + + //if (buffer_definition) + // definition = this._buildDefinitionFromBuffer(buffer_definition); + // + //if (l5x_definition) + // definition = this._buildDefinitionFromL5x(l5x_definition); + + // overwrite serializate/deserialize functions + if (serialize) + this.serialize = serialize; + + if (deserialize) + this.deserialize = deserialize; + + // save local data + this.state = { + template:{ + name, + size, + alignment, + consecutive_alignment, + size_multiple, + string_length, + structure_handle: null, + members:{} + }, + definition, + }; + } + + // region Property Definitions + /** + * Gets Template Name + * + * @readonly + * @memberof Template + * @returns {string} name + */ + get name(){ + return this.state.template.name; + } + + /** + * Gets Template Size in Bits (e.g. DINT returns 32) + * + * @readonly + * @memberof Template + * @returns {number} size + */ + get size(){ + return this.state.template.size; + } + + /** + * Gets Template Alignment + * + * @readonly + * @memberof Template + * @returns {number} alignment in bits (e.g. DINT returns 32) + */ + get alignment(){ + return this.state.template.alignment; + } + + /** + * Gets Template Consecutive Alignment + * + * Consecutive Alignment specificies how to pack consecutive instances + * of this template inside of another template + * + * @readonly + * @memberof Template + * @returns {number} consecutive_alignment in Bits (e.g. DINT returns 32) + */ + get consecutive_alignment(){ + return this.state.template.consecutive_alignment; + } + + /** + * Gets Template Size Multiple in Bits + * + * UDTs are typically sized to a multiple of 32 bits + * but version 28 and later sizes any UDT with a LINT + * as a member or nested member to be sized to a mutliple + * of 64 bits. This property allows LINT to be set to 64 + * and propagate up through parent templates + * + * @readonly + * @memberof Template + * @returns {number} size_mutiple in bits + */ + get size_multiple(){ + return this.state.template.size_multiple; + } + + /** + * Gets Template String Length + * + * This value is used to determine if template is a string or not. + * + * Non-string templates will have a value of 0. + * String tempalte will have a value equal to their string length + * + * This value is set to a value when a string signature is detected. + * + * This value can also be passed into the Template constructor + * instead of a definition. + * + * @memberof Template + * @returns {number} string_length in bytes + */ + get string_length(){ + return this.state.template.string_length; + } + + /** + * Gets Template Structure Handle + * + * The structure handle is the 16bit CRC of the template + * This is returned on tag reads to identify the structure type + * and must be included in the tag write + * + * @memberof Template + * @returns {number} structure_handle byte code + */ + get structure_handle(){ + return this.state.template.structure_handle; + } + + /** + * Sets Template Structure Handle + * + * @memberof Template + * @property {number} structure_handle byte code + */ + set structure_handle(structure_handle){ + this.state.template.structure_handle = structure_handle; + } + // endregion + + // region Public Method Definitions + /** + * Generates Template Map and Adds Template To Passed Object Map + * + * @memberof Template + * @returns {templates} Object Map of Templates + */ + addToTemplates(templates){ + let gen = this._generate(); + let req = gen.next(); + while (!req.done) + req = gen.next(templates[req.value]); + templates[this.name] = this; + } + + /** + * Serializes Tag Data + * + * @memberof Template + * @returns {Buffer} + */ + serialize(value, data = Buffer.alloc(this.size/8), offset = 0){ + const { template: { members }} = this.state; + + return Object.keys(value).reduce((template_data,member)=> + // is member array? + Array.isArray(value[member]) ? + // array - reduce elements + value[member].reduce((element_data,element,index)=> + // array - serailize element + members[member][index].template.serialize(element, element_data, offset + members[member][index].offset), + template_data): + // not array - serialize template (if template exists [e.g. string conversion functions will not pass]) + members[member] ? members[member].template.serialize(value[member], template_data, offset + members[member].offset) : template_data, + data); + } + + /** + * Deserializes Tag Data + * + * @memberof Template + * @returns {Object} + */ + deserialize(data, offset=0){ + const { template: { string_length, members }} = this.state; + + const deserializedValue = Object.keys(members).reduce((value,member)=>{ + // is memeber array? + if (Array.isArray(members[member])){ + // array - reduce elements + value[member] = members[member].reduce((working_value,element)=>{ + // array - deserialize element + working_value.push(element.template.deserialize(data,offset + element.offset)); + return working_value; + },[]); + } else { + // not array - deserialize template + value[member] = members[member].template.deserialize(data,offset + members[member].offset); + } + return value; + },{}); + + // add string conversions if member has string signature + if (string_length){ + deserializedValue.getString = () => String.fromCharCode(...deserializedValue.DATA).substring(0,deserializedValue.LEN); + deserializedValue.setString = (value) => { + deserializedValue.LEN = Math.min(value.length,string_length); + deserializedValue.DATA = value.split("").map(char=>char.charCodeAt(0)); + while (deserializedValue.DATA.length < string_length) + deserializedValue.DATA.push(0); + }; + } + + return deserializedValue; + } + // endregion + + // region Private Methods + /** + * Generates Template Members + * + * Generator for mapping template members. + * Will yield for each member type expecting the + * corresponding template to be returned. + * + * @memberof Template + * @returns {null} + */ + *_generate(){ + //TODO - calculate structure handle - needed to write UDT without reading + const { template, definition } = this.state; + const { members } = template; + let offset = 0; + let last_type; + let last_mem; + + // loop through definition keys + for(let mem in definition){ + // get type as either an object key or value of member (i.e { member: { type: type }} or { member: type }) + let type = definition[mem].type || definition[mem]; + + // get length as object key or default to 0 (needed for arrays) + let length = definition[mem].type ? definition[mem].length | 0 : 0; + + // request member template by type + let member_template = yield type; + + // align offset + let alignment = last_type === type ? member_template.consecutive_alignment : member_template.alignment; + if (length > 0 && alignment < 32) + alignment = 32; + offset = Math.ceil(offset/alignment)*alignment; + + // set final member key as member or array of members + if (length){ + members[mem] = []; + for(let index = 0; index < length; index++){ + members[mem].push({ + offset, + template: member_template, + }); + offset += member_template.size; + } + } else { + members[mem] = { + offset, + template: member_template, + }; + offset += member_template.size; + } + + // get boundary declaration - ONLY FOR V28? and higher! (LINT) + //template.size_multiple = Math.max(member_template.size_multiple, template.size_multiple); + + + // check if type is string and set string_length to DATA array length + template.string_length = type === Types.SINT && last_type === Types.DINT && mem === "DATA" && last_mem === "LEN" && length > 0 && Object.keys(members).length === 2 ? length : 0; + + // save last type and name + last_type = type; + last_mem = mem; + } + + // save final size on size multiple + template.size = Math.ceil(offset/template.size_multiple)*template.size_multiple; + } + + /** + * Builds a String Template Defintion from a String Length + * + * @memberof Template + * @returns {Object} template definition + */ + _buildDefinitionFromStringLength(string_length){ + return { + LEN: Types.DINT, + DATA: { type: Types.SINT, length: string_length} + }; + } + + /** + * Builds a Template Defintion from a Buffer read from the Controller + * + * Not Implemented Yet + * + * @memberof Template + * @returns {Object} template definition + */ + // _buildDefinitionFromBuffer(buffer){ + // // TODO - convert buffer to object definition - for templates read from controller + // throw new Error(`Template Buffer Definition Not Implemented: ${buffer}`); + // } + + /** + * Builds a Template Defintion from an L5x file + * + * Not Implemented Yet + * + * @memberof Template + * @returns {Object} template definition + */ + // _buildDefinitionFromL5x(l5x){ + // // TODO - convert l5x to object definition - for l5x exports + // throw new Error(`Template L5X Definition Not Implemented: ${l5x}`); + // } + // endregion +} + +module.exports = Template; \ No newline at end of file diff --git a/src/template/template.spec.js b/src/template/template.spec.js new file mode 100644 index 0000000..b2f9a77 --- /dev/null +++ b/src/template/template.spec.js @@ -0,0 +1,283 @@ +const Template = require("./index"); +const TemplateMap = require("./atomics"); +const { Types } = require("../enip/cip/data-types"); + +describe("Template Class", () => { + + describe("New Instance", () => { + it("shouldn't throw when creating a new instance",() => { + expect(() => new Template({})).not.toThrow(); + }); + }); + + describe("Strings", () => { + it("should have a string length on string signatures", () => { + const map = TemplateMap(); + + const stringTemplate = new Template({ + name: "STRING", + definition: { + LEN: Types.DINT, + DATA: { type: Types.SINT, length: 82} + } + }); + + stringTemplate.addToTemplates(map); + + expect(stringTemplate.string_length).toEqual(82); + }); + + it("should not have a string length on non-string signatures", () => { + const map = TemplateMap(); + + // only LEN + const nonStringTemplate1 = new Template({ + name: "template1", + definition: { + LEN: Types.DINT + } + }); + + // only DATA + const nonStringTemplate2 = new Template({ + name: "template2", + definition: { + DATA: { type: Types.SINT, length: 82} + } + }); + + // DATA not array + const nonStringTemplate3 = new Template({ + name: "template3", + definition: { + LEN: Types.DINT, + DATA: Types.SINT + } + }); + + // LEN wrong type + const nonStringTemplate4 = new Template({ + name: "template4", + definition: { + LEN: Types.INT, + DATA: { type: Types.SINT, length: 82} + } + }); + + // DATA wrong type + const nonStringTemplate5 = new Template({ + name: "template5", + definition: { + LEN: Types.DINT, + DATA: { type: Types.INT, length: 82} + } + }); + + // extra member before + const nonStringTemplate6 = new Template({ + name: "template6", + definition: { + Extra: Types.DINT, + LEN: Types.DINT, + DATA: { type: Types.SINT, length: 82} + } + }); + + // extra member after + const nonStringTemplate7 = new Template({ + name: "template7", + definition: { + LEN: Types.DINT, + DATA: { type: Types.SINT, length: 82}, + Extra: Types.DINT, + } + }); + + + // LEN mispelled + const nonStringTemplate8 = new Template({ + name: "template8", + definition: { + LENN: Types.DINT, + DATA: { type: Types.SINT, length: 82}, + } + }); + + + // DATA mispelled + const nonStringTemplate9 = new Template({ + name: "template9", + definition: { + LEN: Types.DINT, + DAT: { type: Types.SINT, length: 82}, + } + }); + + nonStringTemplate1.addToTemplates(map); + nonStringTemplate2.addToTemplates(map); + nonStringTemplate3.addToTemplates(map); + nonStringTemplate4.addToTemplates(map); + nonStringTemplate5.addToTemplates(map); + nonStringTemplate6.addToTemplates(map); + nonStringTemplate7.addToTemplates(map); + nonStringTemplate8.addToTemplates(map); + nonStringTemplate9.addToTemplates(map); + + + expect(nonStringTemplate1.string_length).toEqual(0); + expect(nonStringTemplate2.string_length).toEqual(0); + expect(nonStringTemplate3.string_length).toEqual(0); + expect(nonStringTemplate4.string_length).toEqual(0); + expect(nonStringTemplate5.string_length).toEqual(0); + expect(nonStringTemplate6.string_length).toEqual(0); + expect(nonStringTemplate7.string_length).toEqual(0); + expect(nonStringTemplate8.string_length).toEqual(0); + expect(nonStringTemplate9.string_length).toEqual(0); + }); + + it("should build a string definition if given a string length", () => { + const map = TemplateMap(); + + const stringTemplate = new Template({ + name: "STRING", + definition: { + LEN: Types.DINT, + DATA: { type: Types.SINT, length: 82}, + } + }); + + stringTemplate.addToTemplates(map); + + expect(stringTemplate.size).toEqual(704); + + const tagValue = stringTemplate.deserialize(Buffer.alloc(stringTemplate.size/8)); + + expect(tagValue.LEN).toEqual(0); + expect(tagValue.DATA).toHaveLength(82); + + tagValue.LEN = 4; + tagValue.DATA[0] = 116; + tagValue.DATA[1] = 116; + tagValue.DATA[2] = 116; + tagValue.DATA[3] = 116; + + const data = stringTemplate.serialize(tagValue); + + expect(data).toMatchSnapshot(); + }); + + it("should convert between strings and LEN and DATA", () => { + const map = TemplateMap(); + + const stringTemplate = new Template({ + name: "STRING", + definition: { + LEN: Types.DINT, + DATA: { type: Types.SINT, length: 82}, + } + }); + + stringTemplate.addToTemplates(map); + const tagValue = stringTemplate.deserialize(Buffer.alloc(stringTemplate.size/8)); + + expect(tagValue.getString()).toEqual(""); + + const testStrings = [ + "test", + "This test should reach eighty-two characters to test the max length for the array."]; + + + for (let testString of testStrings){ + tagValue.setString(testString); + expect(tagValue.getString()).toEqual(testString); + expect(tagValue.LEN).toEqual(testString.length); + } + + }); + }); + + describe("Structure Handle Property", () => { + it ("should get and set the structure handle", () => { + const template = new Template({}); + + template.structure_handle = 123; + expect(template.structure_handle).toEqual(123); + + template.structure_handle = 456; + expect(template.structure_handle).toEqual(456); + }); + }); + + describe("Large UDT Testing", () => { + it("should properly map, serialize, and deserialize large UDTs", () => { + const map = TemplateMap(); + + new Template({ + name: "string10", + string_length: 10, + }).addToTemplates(map); + + new Template({ + name: "udt1", + definition: { + member1: Types.DINT, + member2: Types.SINT, + member3: Types.SINT, + member4: { type: Types.INT, length: 2 } + }, + }).addToTemplates(map); + + new Template({ + name: "udt2", + definition: { + member1: "string10", + member2: { type: Types.REAL, length: 2 }, + }, + }).addToTemplates(map); + + new Template({ + name: "udt3", + definition: { + member1: { type: "udt1", length: 2 }, + member2: Types.BOOL, + member3: Types.BOOL, + member4: Types.BOOL, + member5: "udt2" + }, + }).addToTemplates(map); + + let data; + let value; + + data = Buffer.alloc(map["udt3"].size/8); + + value = map["udt3"].deserialize(data); + expect(value).toMatchSnapshot(); + + value.member1[0].member1 = 1; + value.member1[0].member2 = 2; + value.member1[0].member3 = 3; + value.member1[0].member4[0] = 4; + value.member1[0].member4[1] = 5; + value.member1[1].member1 = 6; + value.member1[1].member2 = 7; + value.member1[1].member3 = 8; + value.member1[1].member4[0] = 9; + value.member1[1].member4[1] = 10; + value.member2 = true; + value.member3 = false; + value.member4 = true; + value.member5.member1.setString("new string"); + value.member5.member2[0] = "123.456"; + value.member5.member2[1] = "456.789"; + + data = map["udt3"].serialize(value); + expect(data).toMatchSnapshot(); + + value = map["udt3"].deserialize(data); + expect(value).toMatchSnapshot(); + + }); + }); + +}); \ No newline at end of file diff --git a/src/utilities/utilities.spec.js b/src/utilities/utilities.spec.js index c48c925..eda1875 100644 --- a/src/utilities/utilities.spec.js +++ b/src/utilities/utilities.spec.js @@ -23,6 +23,22 @@ describe("Utilites", () => { await expect(fn(50, "hello")).resolves.toBe("hello"); await expect(fn(50, { a: 5, b: 6 })).resolves.toMatchObject({ a: 5, b: 6 }); }); + + it("Has Default Message", async () => { + const fn = (ms, arg) => { + return promiseTimeout( + new Promise(resolve => { + setTimeout(() => { + if (arg) resolve(arg); + resolve(); + }, ms); + }), + 100 + ); + }; + + await expect(fn(200)).rejects.toEqual(new Error("ASYNC Function Call Timed Out!!!")); + }); }); describe("Delay Utility", () => {