Initial commit

This commit is contained in:
Marcelo
2025-11-20 15:27:34 -06:00
commit cc72c9fc5d
3221 changed files with 737477 additions and 0 deletions

21
node_modules/node-red-contrib-buffer-parser/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Steve-Mcl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

170
node_modules/node-red-contrib-buffer-parser/README.md generated vendored Normal file
View File

@@ -0,0 +1,170 @@
node-red-contrib-buffer-parser
==============================
<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIoAAABJCAYAAADi+75+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAgPSURBVHhe7ZzfbxRVFMf5k4y7+mLif7BP+uJDH1ATNaSJGl4oTyZESYhAlBVW2G5LkyKhdGvQWh9EIzUpkNg0kEABI6YtNqSS0kAp8uMw5+69d8/cvTN75+7s7pQ5J/nS+XHvmTtzPnvvzDD37ACLra+vy6XOjP3E23byw6B4WB79MCgelkc/DIqH5dFPJChbW1vw8OFD+OW3C1AZHoNypZpYRyonrNtNVaqjsTpWHbFuTyr206rqyVPw+x9z8OTJEz9QVlZWoPztMLz3ZR3e2H8BXv1irit65bNfobD/Erx28AqrD3rz0J/wYXlGALO8vCwJaLVIUKpj38HbB36wBjdNMSjZ0DtfnYeTpyclAa1mBeXWrVvw+dExa2DTFoOSHR2onoHV1VVJQdisoFy+fFkMObbApi0GJTvCIejq1auSgrBZQbl48SIMHJiyBjZtMSjZ0btf/wwLCwuSgrD1AJR5+GD8imV7QwxKdtRHUObh4N//Bx6fwsLcdct+BiVLcgbl+fPn4i8FpWAENlY/3oVL1/6BklhXkDRs47baHlZ7UGahWCpAcdesZV871aFYKEAhUHHXtPCDy4WBuqVsL9XJOaWv1+VfCopiQZkGBW9gp6enYWJiAmrDNagcr8KR4XE4XJuAj7851xJgqxY2ha+N20tQppCs3IFBW/lAXQVl104olMqN5T17A0j2tpbpi7IDyqeVGTg8Ognl2imonBiGWq0mGEAWkAllAhTcMDU1JWhaXFwMCbednZx0g0WCQi0OElTXQVG9B4KioOm7sgHKJwEkZyfrkXFHJhQsO7CLQXpshZVw36GgZ2k7DJmgrK/BkK0ckTsoZT2MNANuueADcmhBSFT5wltkeScU92HZ5rCEKu6R9ffhcfZCEf3Q7UqyZyoS/6KMLB+u02if2t7s0Yx2C5+kHm07hTs4RnEgOLa53UM43GBP0i7uyAYyInoU7GpsBanKwTBkC3RIth4l4t5EyRWUZoDluugpYkDB5cihx6gn4JD+xbLhk0oFVe2XQdVBxuOrY+I+fT9Ej0mWQ+0KZK7T8xEwpjd84nBjizUVsoG2A1/Xj4yMWAtRHT1eswY6pLkH8Fi4JfZsE+pjlrJSzqDoCx5IX0xPUGSvoeuggnrCD4WG7ldqF1h6zJAaPVgIlBJCFtEOtU7bSs8tBR09UbPGmgrZQEZEjzI6OmotRBUG5S+YfSAwCNvqHVLGTV73KPrieYIilvHXaQjrpQlK6DiBT91WCX9oG9ZX203J9vQBFGQDzXnowScgGuDS+fuw9kz4aNra3VAZF3mBooPjCYqtR1FKDRSzbXSdLJvHM3sUqpRBSTT0uN7M4mNy+GZ2HspLT4UTbV0EpRkMua4uGF483dXLG9R2oLQEkQwLKYOi2yLKWUAJ1Ynwp9ZTBMXrZjb543H4ZZq2rvYoGMDgQqE0GIFEYOX24IKKp5G2oKDCTz26TmqgqH3Evw50DKjKh6pn9DZpgYLCdyjOj8fi38BwA9KDXY164YZPOvhYHAcJvicZUsNQV0BhdVP4LsX5hZsy9do27hX+0LUtUQaNvkwrzdyBsZn5UFkXMSj9V6JX+NRi/1OwtgSXNtu/cXUVg5Id9fkzg3gxKNkRg8JyUmJQ8CaGP4XMnxJ/CskfV+dTiT+u5uka+ZPXdA2eAJYfdTwBjKeURounlEqLq5DE2E+8bSc/DIqH5dEPg+JhefQjvnAzde/evZZtPmI/8dpOfrhH8bA8+mFQPCyPfhgUD8ujHwbFw/LoJxIUfuEWLX7hJg0rqFf47/Mr/Jda+Ar/I87hxnIV53BjOcsrhxsOObbApi0GJTviHG4sJ/X8m9nS98tQv7IGPxHVZ296Zlxi9Uo9B2Vo0TKL8NF9OGgpy6BkR86guEwAc9N1GFsx5iV7g2JOv0yi5rTRbOVwy5Z6n8NNyzI3uR+gxM49ZqH6k8NNyJib/Ej2LP0CRfUeCAqd3M7qYw43ywT2QTUMdQyKLZuBBSI14z+UEaAHOdxC+4lf1TYBalBPDH+yDfIYQibEdJ8eKuX5DshjdDCE9jeH2/n7sCEQMecmz0PpGClH5ApKM8ByXVykGFBwOXLoMerRVBcyjUZkD6ZAUMcQ6xQ+taz2yWPKehoqXKdwBO3Wx6TtDrVVnrsJlaf6l8Mt0ODCJiwvuU9gdwaF/np0ADxBkb2GroNSgXLKj0L3W9qgJHodCgo5plgP6pk9lvJn9lLiPGKO5aEe5HCjagw3j9fuemU38LpH0YH2BEUGqUVYzwmUCMhke5S/UDI/Wz2jHQ04jAQ/WhHn24G6msOtNH4DBmsq0OF7kuVrNzQArvICRV90T1BsPYpSJz2KCQNdN/eZ0r2P9NfS06DSBaWrOdzqa0GtRw/g9Bnbjasdhji536OoiyzXFQwIhh6z5a+xHSgtF7xRT6w7gULqinVZni6b7TZBoW1T+9W6uU+fY3qgdD2HmwAFjWSE7CSpjnuPggHEC0/BCCQCK7cHgehVDjf1VIRq/voVHKjAxx7iq+X4gYiPcC9l7osCvDN1NYebBkXaxr/+kKDag5Ix2QK+jdW1HG4mKI1hKFwmiRiU/qsrOdxaQEFb/w92W8q6iEHJjtL93+PadRg6dzOk3ePJs0EqbTtQXmL1/DODJGJQsqPEoOBNDH8KmT9xDjeWkziHG6utOs7hxhPAXm51PAGMp5RGi6eUSourkMTYT7xtJz8Miofl0Q+D4mF59MM53DyURz/co3hY/vwAvABP3UATcHo5dQAAAABJRU5ErkJggg=='/>
## About
A pair of <a href="http://nodered.org" target="_new">Node-RED</a> nodes to convert values to and from buffer/array. Supports Big/Little Endian, BCD, byte swapping and much more.
## A picture is worth a thousand words
### Convert array of integers into individual topic/payload messages - ideal for sending to MQTT
![example1](/images/example1.png)
### Convert a buffer into key/value items - ideal for sending to dashboard or database
![example3](/images/example3.png)
![example3b](/images/example3b.png)
### Fan out
![example3](/images/fanned.png)
### Scaling final values
![example3](/images/scaling.png)
## buffer-maker - Summary of functionality **(New in V3.0)**
* Set-up a specification and convert multiple values into a buffer from...
* int, int8, byte, uint, uint8,
* int16, int16le, int16be, uint16, uint16le, uint16be,
* int32, int32le, int32be, uint32, uint32le, uint32be,
* bigint64, bigint64be, bigint64le, biguint64, biguint64be, biguint64le,
* float, floatle, floatbe, double, doublele, doublebe,
* 8bit, 16bit, 16bitle, 16bitbe, bool,
* bcd, bcdle, bcdbe,
* string, hex, ascii, utf8, utf16le, ucs2, latin1, binary, buffer
* Specification is either configured by the built in UI or can be set by a msg/flow/global
* Input data for each item to include in the final buffer can come from just about anywhere, making it very flexible...
* a constant (e.g. a number, a string, a boolean, a JSON array)
* a `msg` property (e.g from `msg.payload.myInteger`)
* a `flow` property (e.g from `flow.myInteger`)
* a `global` property (e.g from `global.myInteger`)
* The final built buffer can be byte swapped one or more times. 16, 32 or 64 bit swaps are possible. The byte swaps are performed the data conversions like LE or BE functions (sometimes it is necessary to do multiple swaps)
* The final buffer can be output to any `msg` property (defaults to `msg.payload`)
* Built in help
## buffer-parser - Summary of functionality
* Set-up a specification and convert multiple parts of an array or buffer to...
* int, int8, byte, uint, uint8,
* int16, int16le, int16be, uint16, uint16le, uint16be,
* int32, int32le, int32be, uint32, uint32le, uint32be,
* bigint64, bigint64be, bigint64le, biguint64, biguint64be, biguint64le,
* float, floatle, floatbe, double, doublele, doublebe,
* 8bit, 16bit, 16bitle, 16bitbe, bool,
* bcd, bcdle, bcdbe,
* string, hex, ascii, utf8, utf16le, ucs2, latin1, binary, buffer
* Specification is either configured by the built in UI or can be set by a msg/flow/global property - permitting fully dynamic setup (e.g. via a dashboard)
* The specification format permits random access (e.g. no need for any skips when accessing only first and last elements)
* You can specify the same offset many times to convert the same piece of data several times
* The data can be byte swapped one or more times. 16, 32 or 64 bit swaps are possible. The byte swaps are done prior to any data conversions like LE or BE functions (sometimes it is necessary to do multiple swaps)
* The output can be sent in any `msg` property. e.g. you can send results out in `msg.my.nested.property`. This has the advantage of leaving the original payload in tact.
* Input data can come from any msg property (not limited to `msg.payload`)
* Input data can be a 16bit array (common plc data format) simplifying working with PLC type data arrays
* Input data can be a hex string e.g. `1FE2D7FFBE`
* Output results can be multiple messages as `topic` and `payload`
* ideal for taking PLC data and sending it directly to MQTT
* Output results can be multiple messages fanned out so that each item in the specification is sent out of its own output **(New in V3.1)**
* Output results can be a single msg style output
* ideal for converting multiple different data elements into one object to pass on to perhaps a template node for preparing a SQL or HTML statement using {{mustache}} formatting
* additionally, output results can be 1 of 4 styles...
* "value" : the parsed values are sent in an array
* "keyvalue" : the parsed values are sent in an object as key/value pairs. Use a fat arrow `=>` in the name to create object.properties e.g. `motor1=>power` will end up in `msg.payload.motor1.power`.'
* "object" : the parsed values are sent as named objects with the value set `.value` and other contextual properties included (like the item specification). Use a fat arrow `=>` in the name to create object.properties e.g. `motor1=>power` will end up in `msg.payload.motor1.power`.'
* "array" : the parsed values are sent as objects in an array, with each object containing a `.value` property and other contextual properties included (like the item specification)
* "buffer" : this mode simply returns a buffer (no item processing)
* Final values can be masked (e.g. a MASK of `0x7FFF` could be used to remove the MSB or `0b1000000000000001` to keep only MSB and LSB)
* Binary and Octal masks only available in **V3.1** onwards
* Final values can be have a Scale value or a simple Scale Equation **(New in V3.1)** applied...
* e.g. Entering a Scale value of `0.01` would turn `9710` into `97.1`
* e.g. Entering a Scale value of `10` would turn `4.2` into `42`
* e.g. Entering a Scale Equation of `>> 4` would bit shift the value `0x0070` to `0x0007`
* e.g. Entering a Scale Equation of `+ 42` would add an offset of 42 to the final value **(New in V3.1)**
* Supported Scaling Equations are...
* `<<` e.g. `<<2` would left shift the parsed value 2 places
* `>>` e.g. `>>2` would right shift the parsed value 2 places
* `>>>` e.g. `>>>2` would zero-fill right shift the parsed value 2 places (returns a 32bit unsigned value)
* `+` e.g. `+10` would add 10 to the parsed value
* `-` e.g. `-10` would deduct 10 from the parsed value
* `/` e.g. `/10` would divide the parsed value by 10
* `*` e.g. `*10` would multiply the parsed value by 10
* `**` e.g. `**2` would raise the parsed value to the power of 2
* `^` e.g. `^0xf0` would XOR the parsed value with 0xf0
* `==` e.g. `==10` would result in `true` if the parsed value was equal to 10
* `!=` e.g. `!=10` would result in `false` if the parsed value was equal to 10
* `!!` e.g. `!!` would result in `true` if the parsed value was `1` (same as `!!1 == true`)
* `>` e.g. `>10` would result in `true` if the parsed value was greater than 10
* `<` e.g. `<10` would result in `true` if the parsed value was less than 10
* NOTE: the scale/equation is applied AFTER the mask
* Final values can be have a scale applied (e.g. a scale of `0.01` would turn `9710` into `97.1` or a scale of 10 would turn `50` into `500`)
* NOTE: the scale is applied AFTER the mask
* Built in help
![help](/images/help.png)
## Examples
### Example 1 - array of data to MQTT (multiple topics / payloads)
Screen shot - the flow
![example1a](/images/example1a.png)
Screen shot - the output
![example1b](/images/example1b.png)
Flow...
```
[{"id":"1194a28a.49d0ad","type":"buffer-parser","z":"c70ba4a4.e7fb58","name":"","data":"payload","dataType":"msg","specification":"{\"options\":{\"byteSwap\":[\"swap16\"],\"resultType\":\"value\",\"singleResult\":false,\"msgProperty\":\"payload\"},\"items\":[{\"name\":\"plc1/production/alphabet\",\"type\":\"string\",\"offset\":0,\"length\":26},{\"name\":\"plc1/production/status/count\",\"type\":\"int\",\"offset\":25},{\"name\":\"plc1/production/status/sequence\",\"type\":\"bcd\",\"offset\":4},{\"name\":\"plc1/machine/status/runner/temperature\",\"type\":\"int16le\",\"offset\":26},{\"name\":\"plc1/machine/status/runner/speed\",\"type\":\"int16be\",\"offset\":26},{\"name\":\"plc1/machine/status/running\",\"type\":\"bool\",\"offset\":0,\"offsetbit\":0},{\"name\":\"plc1/machine/status/warning\",\"type\":\"bool\",\"offset\":0,\"offsetbit\":1},{\"name\":\"plc1/machine/status/fault\",\"type\":\"bool\",\"offset\":0,\"offsetbit\":2}]}","specificationType":"json","x":1110,"y":480,"wires":[["858b1ecf.77b58"]]},{"id":"858b1ecf.77b58","type":"debug","z":"c70ba4a4.e7fb58","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1350,"y":480,"wires":[]},{"id":"c22cd2e8.52649","type":"inject","z":"c70ba4a4.e7fb58","name":"Fake PLC data 16bit Array","topic":"","payload":"[25185,25699,26213,26727,27241,27755,28013,28783,29297,29811,30325,30839,31353,256,512,768,1024,1280,1536,1792,2048,2304,2560,2816,3072,3597]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":890,"y":480,"wires":[["1194a28a.49d0ad"]]},{"id":"970db39d.106a6","type":"comment","z":"c70ba4a4.e7fb58","name":"take a array of 16bit values, byte reverse, split out several values and transmit individual messages with topic + payload","info":"","x":1160,"y":440,"wires":[]}]
```
### Example 2 - array of data to an named objects
Screen shot - the flow
![example2a](/images/example2a.png)
Screen shot - the output
![example2b](/images/example2b.png)
Flow...
```
[{"id":"1523dd03.6332f3","type":"buffer-parser","z":"c70ba4a4.e7fb58","name":"","data":"payload","dataType":"msg","specification":"{\"options\":{\"byteSwap\":[\"swap16\"],\"resultType\":\"object\",\"singleResult\":true,\"msgProperty\":\"data\"},\"items\":[{\"name\":\"alphabet\",\"type\":\"string\",\"offset\":0,\"length\":26},{\"name\":\"single byte pos 4\",\"type\":\"int\",\"offset\":4},{\"name\":\"bcd equiv\",\"type\":\"bcd\",\"offset\":4,\"length\":5},{\"name\":\"Array[6] of int16le\",\"type\":\"int16le\",\"offset\":26,\"length\":6},{\"name\":\"Array[6] of int16be\",\"type\":\"int16be\",\"offset\":26,\"length\":6},{\"name\":\"32 bools\",\"type\":\"bool\",\"offset\":0,\"length\":32},{\"name\":\"Array[4] of 16bits\",\"type\":\"16bit\",\"offset\":0,\"length\":4}]}","specificationType":"json","x":1110,"y":560,"wires":[["a3051c67.b82ad"]]},{"id":"a3051c67.b82ad","type":"debug","z":"c70ba4a4.e7fb58","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"data","targetType":"msg","x":1340,"y":560,"wires":[]},{"id":"9b72f1f5.1aacc","type":"inject","z":"c70ba4a4.e7fb58","name":"Fake PLC data 16bit Array","topic":"","payload":"[25185,25699,26213,26727,27241,27755,28013,28783,29297,29811,30325,30839,31353,256,512,768,1024,1280,1536,1792,2048,2304,2560,2816,3072,3597]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":890,"y":560,"wires":[["1523dd03.6332f3"]]},{"id":"a9a2dd4c.118f9","type":"comment","z":"c70ba4a4.e7fb58","name":"take a array of 16bit values, byte reverse, split out several values and transmit one message with named objects in msg.data","info":"","x":1180,"y":520,"wires":[]}]
```
## Install
### Pallet Manager...
The simplest method is to install via the pallet manager in node red. Simply search for **node-red-contrib-buffer-parser** then click install
### Terminal...
Run the following command in the root directory of your Node-RED install (usually `~/.node-red` or `%userprofile%\.node-red`)
npm install node-red-contrib-buffer-parser
Or, install direct from github
npm install steve-mcl/node-red-contrib-buffer-parser
Or clone to a local folder and install using NPM
git clone https://github.com/Steve-Mcl/node-red-contrib-buffer-parser.git
npm install c:/source/node-red-contrib-buffer-parser
## Dependencies
none :smile:

View File

@@ -0,0 +1,595 @@
<!--
MIT License
Copyright (c) 2020 Steve-Mcl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
-->
<script type="text/javascript">
(function () {
//console.log("Initialising buffer-maker"); //uncomment me to help locate SRC in devtools!
var compatibleDataTypes = {
'int': 'int8',
'uint': 'uint8',
'int16': 'int16be',
'uint16': 'uint16be',
'int32': 'int32be',
'uint32': 'uint32be',
'bigint64': 'bigint64be',
'biguint64': 'biguint64be',
'float': 'floatbe',
'double': 'doublebe',
'16bit': '16bitbe',
'boolean': 'bool'
}
function coerceDataType(t) {
return compatibleDataTypes[t] || t;
}
function setupTypedInput(varName, allowableTypes, defType, defVal, isOptional, width) {
var node = this;
console.log("setupTypedInput", varName, allowableTypes, defType, defVal, isOptional, width);
//TODO: move to common library
function isObject(val) {
if (val === null) { return false; }
return (typeof val === 'object');
};
function isNumeric(n) {
return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);
};
function isTypeInArray(arr, findType) {
try {
var typeToFind = findType;
if (isObject(findType))
typeToFind = findType.value;
for (var index = 0; index < arr.length; index++) {
var element = arr[index];
var thisType = isObject(element) ? element.value : element;
if (thisType === typeToFind) return true;
}
} catch (error) { }
return false;
}
var varSel = "#node-input-" + varName;
var typeSel = varSel + "Type";
var currentValue = node[varName];
var currentType = node[varName + "Type"];
var defShouldHaveNoValue = false;
//if optional but 'none' is not yet an option - add it as first option in allowableTypes
if (isOptional === true) {
if (!isTypeInArray(allowableTypes, "none")) {
allowableTypes.unshift("none");
}
}
//if defType is not in the list of parameter type, change defType to 1st item
if (!isTypeInArray(allowableTypes, defType)) {
var _firstType = allowableTypes[0];
if (isObject(_firstType)) {
_firstType = _firstType.value;
defShouldHaveNoValue = _firstType.hasValue == false;
}
defType = _firstType;
}
//if currently set type is not in the list of parameter types, set currentType to defType
if (!isTypeInArray(allowableTypes, currentType)) {
currentType = defType;
}
//catch all
if (!currentType) {
currentType = defType || "";
}
var $ti = $(varSel);
var alreadyBuilt = $ti.data("built") == true;
if (alreadyBuilt) {
//already built? just update types :)
$ti.typedInput("types", allowableTypes);
} else {
var opts = {
default: defType,
typeField: $(typeSel),
types: allowableTypes
};
if (isOptional) {
opts.default = defType || "none";
opts.validate = function (v) {
//todo - handle this when spec changes
return !!v;
};
}
$ti.typedInput(opts);
$ti.data("built", true);
}
if (currentValue && currentType == "bool") {
currentValue = (currentValue === true || currentValue === "true") ? "true" : "false";
}
$ti.typedInput("type", currentType);
$ti.typedInput("value", currentValue);
if (width) $ti.typedInput("width", width);
return $ti;
}
function getEditItem(index) {
try {
var items = $("#node-input-items-container").editableList('items');
var r = parseEditItem(items[index]);
return parseEditItem(items[index]);
} catch (error) { }
return null;
}
function parseEditItem($row) {
try {
var rule = $($row);
var r = {};
r.name = rule.find(".node-input-item-property-name").val() || "item" + (i + 1);
r.type = rule.find(".node-input-item-property-type").val() || "int16be";
r.length = parseInt(rule.find(".node-input-item-property-length").val() || 1);
var d = $(rule.find(".node-input-item-property-data"));
r.dataType = d.typedInput("type");
r.data = d.typedInput("value");
return r;
} catch (error) { }
return null;
}
RED.nodes.registerType('buffer-maker', {
category: 'parser',
color: '#0090d4',
defaults: {
name: { value: "" },
specification: { value: "" },
specificationType: { value: "ui" },
items: { value: [{ name: "item1", type: "int16be", data: "payload", dataType: "msg" }], required: true },
swap1: { value: "" },
swap2: { value: "" },
swap3: { value: "" },
swap1Type: { value: "swap" },
swap2Type: { value: "swap" },
swap3Type: { value: "swap" },
msgProperty: { value: "payload" },
msgPropertyType: { value: "str" },
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-compress",
label: function () {
return this.name || "buffer maker";
},
oneditprepare: function () {
// console.log("buffer-maker->oneditprepare()")
var node = this;
var sti = setupTypedInput.bind(this);
var specOpt = {
value: "ui",
label: "UI Specification",
hasValue: false
}
var swapOpt = {
value: "swap",
label: "Swap",
title: "Swap",
showLabel: true,
// icon:"fa fa-exchange",
options: [
{ label: "none", value: '', title: '' },
{ label: "16", value: 'swap16', title: 'Interprets data as an array of 16-bit integers and swaps the byte order in-place' },
{ label: "32", value: 'swap32', title: 'Interprets data as an array of 32-bit integers and swaps the byte order in-place' },
{ label: "64", value: 'swap64', title: 'Interprets data as an array of 64-bit integers and swaps the byte order in-place' },
],
default: "none"
}
sti('data', ['msg', 'json', 'bin'], 'msg');//data
var specificationField = sti('specification', [specOpt, 'msg', 'flow', 'global'], 'ui');//specification
var sw1 = sti('swap1', [swapOpt, 'json', 'msg', 'flow', 'global', 'env'], 'swap');//specification
var sw2 = sti('swap2', [swapOpt], 'swap');//specification
var sw3 = sti('swap3', [swapOpt], 'swap');//specification
var msgPropertyField = $("#node-input-msgProperty").typedInput({ types: [{ label: "msg.", value: "str" }] });
specificationField.on("change", function () {
var v = $(this).val();
var t = specificationField.typedInput("type");
if (t == "ui") {
$(".ui-row").show();
if (!v) specificationField.typedInput("value", "spec");
console.log("calling RED.tray.resize();")
RED.tray.resize();
} else {
$(".ui-row").hide();
console.log("calling RED.tray.resize();")
RED.tray.resize();
}
});
sw1.on("change", function () {
var v = $(this).val();
var t = $(this).typedInput("type");
if (t != "swap" || !v || v == "none") {
sw1.typedInput("width", "70%");
sw2.typedInput('hide');
} else {
sw1.typedInput("width", "23%");
sw2.typedInput('show');
}
sw2.change();
});
sw2.on("change", function () {
var v1 = sw1.val();
var v2 = sw2.val();
var t = sw1.typedInput("type");
if (t != "swap" || !v1 || !v2 || v1 == "none" || v2 == "none") {
sw3.typedInput('hide');
} else {
sw3.typedInput('show');
}
});
$('#node-input-items-container').css('min-height', '150px').css('min-width', '500px').editableList({
addItem: function (container, i, opt) {
var rule = opt;
if (!rule.hasOwnProperty('type')) {
//its a newly added item!
rule = { type: "int16be", name: "item" + (i + 1), length: 1, dataType: "msg", data: "payload" };//default
var prev = i > 0 ? getEditItem(i - 1) : null;
if (prev && prev.type != null) {
var byteOffsetMultiplier = 0;
var bitOffset = 0;
rule.type = prev.type;
rule.length = prev.length;
rule.data = prev.data;
rule.dataType = prev.dataType;
}
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
let fragment = document.createDocumentFragment();
var row1 = $('<div/>', { style: "display:flex;", class: "buffer-maker-row" }).appendTo(fragment);
var _types = {
value: "type",
label: "Type",
showLabel: true,
options: [
{ value: "byte", label: "byte" },
{ value: "int8", label: "int8" },
{ value: "uint8", label: "uint8" },
{ value: "int16le", label: "int16 (le)" },
{ value: "int16be", label: "int16 (be)" },
{ value: "uint16le", label: "uint16 (le)" },
{ value: "uint16be", label: "uint16 (be)" },
{ value: "int32le", label: "int32 (le)" },
{ value: "int32be", label: "int32 (be)" },
{ value: "uint32le", label: "uint32 (le)" },
{ value: "uint32be", label: "uint32 (be)" },
{ value: "bigint64le", label: "bigint64 (le)" },
{ value: "bigint64be", label: "bigint64 (be)" },
{ value: "biguint64le", label: "biguint64 (le)" },
{ value: "biguint64be", label: "biguint64 (be)" },
{ value: "floatle", label: "float (le)" },
{ value: "floatbe", label: "float (be)" },
{ value: "doublele", label: "double (le)" },
{ value: "doublebe", label: "double (be)" },
{ value: "8bit", label: "8bit" },
{ value: "16bitle", label: "16bit (le)" },
{ value: "16bitbe", label: "16bit (be)" },
{ value: "bool", label: "bool" },
{ value: "bcdle", label: "bcd (le)" },
{ value: "bcdbe", label: "bcd (be)" },
{ value: "string", label: "string" },
{ value: "hex", label: "hex" },
{ value: "ascii", label: "ascii" },
{ value: "utf8", label: "utf8" },
{ value: "utf16le", label: "utf16 (le)" },
{ value: "ucs2", label: "ucs2" },
{ value: "latin1", label: "latin1" },
{ value: "binary", label: "binary" },
{ value: "buffer", label: "buffer" },
]
}
let row1_1 = $('<div/>', { style: "display:flex;", class: "buffer-maker-row-item" }).appendTo(row1);
let nameField = $('<input/>', { class: "node-input-item-property-name", type: "text", width: "125px" })
.appendTo(row1_1)
.typedInput({ types: [{ label: "Name", value: "str" }] })
let row1_2 = $('<div/>', { style: "display:flex;", class: "buffer-maker-row-item" }).appendTo(row1);
let typeField = $('<input/>', { class: "node-input-item-property-type", type: "text", width: "165px" })
.appendTo(row1_2)
.typedInput({ types: [_types] })
let row1_3 = $('<div/>', { style: "display:flex;", class: "buffer-maker-row-item" }).appendTo(row1);
let lengthField = $('<input/>', { class: "node-input-item-property-length", type: "number", min: "0", width: "110px" })
.appendTo(row1_3)
.typedInput({ types: [{ label: "Length", value: "num" }] })
let row1_4 = $('<div/>', { style: "display:flex;", class: "buffer-maker-row-item" }).appendTo(row1);
let dataField = $('<input/>', { class: "node-input-item-property-data", type: "text", min: "0", width: "155px" })
.appendTo(row1_4)
.typedInput({ types: ['msg','flow','global','str','num','bool','bin','jsonata','env']});
rule.type = coerceDataType(rule.type);
typeField.typedInput('type', "type");
typeField.typedInput('value', rule.type || "int16be");
nameField.typedInput('type', "str");
nameField.typedInput('value', rule.name || ("item" + (i + 1)));
dataField.typedInput('type', rule.dataType || "msg");
dataField.typedInput('value', rule.data || "payload");
lengthField.typedInput('type', "num");
lengthField.typedInput('value', rule.length || 1);
container[0].appendChild(fragment);
},
removable: true,
sortable: true
});
for (var i = 0; i < this.items.length; i++) {
var item = this.items[i];
$("#node-input-items-container").editableList('addItem', item);
}
},
oneditsave: function () {
var items = $("#node-input-items-container").editableList('items');
var node = this;
node.items = [];
items.each(function (i) {
var rule = $(this);
var r = parseEditItem(rule);
node.items.push(r);
});
},
oneditresize: function (size) {
console.log("inside oneditresize");
var rows = $("#dialog-form>div:not(.node-input-items-container-row)");
var height = size.height;
for (var i = 0; i < rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-items-container-row");
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-items-container").editableList('height', height);
}
});
})()
</script>
<script type="text/html" data-template-name="buffer-maker">
<div class="form-row buffer-maker-form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row buffer-maker-form-row">
<label for="node-input-specification"><i class="fa fa-code"></i><span data-i18n="buffer-maker.label.specification"> Specification</span></label>
<input type="hidden" id="node-input-specificationType">
<input style="width: 70%" type="text" id="node-input-specification" placeholder="">
</div>
<div class="form-row buffer-maker-form-row ui-row node-input-items-container-row" id="ui-row5">
<ol id="node-input-items-container"></ol>
</div>
<div class="form-row buffer-maker-form-row ui-row" id="ui-row1">
<label for="node-input-swap1"><i class="fa fa-exchange"></i><span data-i18n="buffer-maker.label.swap"> Byte swap</span></label>
<span>
<input type="hidden" id="node-input-swap1Type">
<input style="width: 23%" type="text" id="node-input-swap1" placeholder="">
<input type="hidden" id="node-input-swap2Type">
<input style="width: 23%" type="text" id="node-input-swap2" placeholder="">
<input type="hidden" id="node-input-swap3Type">
<input style="width: 23%" type="text" id="node-input-swap3" placeholder="">
</span>
</div>
<div class="form-row buffer-maker-form-row ui-row" id="ui-row2">
<label for="node-input-msgProperty"><i class="fa fa-sign-out"></i><span data-i18n="buffer-maker.label.msgProperty"> Output property</span></label>
<input type="hidden" id="node-input-msgPropertyType">
<input style="width: 70%" type="text" id="node-input-msgProperty" placeholder="payload">
</div>
</script>
<style>
ol#node-input-items-container .red-ui-typedInput-container {
flex: 1;
}
.buffer-maker-form-row>label {
width: 120px !important;
}
.buffer-maker-row-item {
margin: 2px 4px 2px 2px;
}
.buffer-maker-row {
flex-wrap: wrap;
}
span.buffer-maker-prop-name {
padding: 0px 3px 2px 3px;
margin: 1px;
color: #AD1625;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 2px;
font-family: monospace;
}
span.buffer-maker-prop-type {
color: #666;
font-style: italic;
font-size: smaller;
padding-left: 5px;
}
span.buffer-maker-prop-desc {
/* color: #333; */
padding-left: 5px;
}
.buffer-maker-node-help div.buffer-maker-node-help-indent {
margin: 0px 0px 0px 6px;
}
</style>
<script type="text/html" data-help-name="buffer-maker">
<p>A node that converts input data items into a buffer based on the specification provided</p>
<h3>Foreword</h3>
<dl class="message-properties">
A number of examples have been included to help you do some common tasks. To use the examples, press the hamburger menu <a id="red-ui-header-button-sidemenu" class="button" href="#"><i class="fa fa-bars"></i></a> select <b>import</b> then <b>examples</b>
</dl>
<h3>Output</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">object | []</span></dt>
<dd>the results.</dd>
<h4>NOTES:<br>
Payload can be set to an alternative <code>msg</code> property specified by "Output property"<br>
Additional useful properties are available in the <code>msg</code> object. Use a debug node (set to show complete output) to inspect them
</h4>
</dl>
<h3>UI specification</h3>
<h4><b>Property...</b></h4>
<div class="buffer-maker-node-help-indent">
The data to be processed in accordance with the specification.
<div>
<h4><b>Specification...</b></h4>
<div class="buffer-maker-node-help-indent">
If Specification is set to "UI" then enter the specification in the fields provided below, otherwise, the specification must be an object provided in the format described below in <a href="#buffer-maker-help-dyn-spec">Dynamic specification</a>
<div>
<h4><b>Byte swap...</b></h4>
<div class="buffer-maker-node-help-indent">
Swap permits 16 bit, 32 bit or 64 bit swap options. Swap is applied to the whole data before extracting the items specified. If the 1st swap option is set to msg, flow, global, it must be an array containing any number of "swap16" "swap32" "swap64" strings. If the 1st swap option is set to env, then the environment variable must be set to a comma separated list of swaps e.g. swap32,swap16
<div>
<h4><b>Output property...</b></h4>
<div class="buffer-maker-node-help-indent">
This allows you to return the result in a property other than payload. For example, you can have results returned to <code>msg.output</code> or <code>msg.result.data</code>.
<div>
<h4><b>items...</b></h4>
<div class="buffer-maker-node-help-indent">
<ul>
<li><span class="buffer-maker-prop-name">name</span> <span class="buffer-maker-prop-desc"> A name to identify the resulting data.</span></li>
<li><span class="buffer-maker-prop-name">type</span> <span class="buffer-maker-prop-desc"> The type that input data should be written to the buffer as (see Allowable types below)</span></li>
<li><span class="buffer-maker-prop-name">length</span> <span class="buffer-maker-prop-desc"> The quantity of items to be written to the buffer. e.g. 12 floats or 34 int32s. NOTE: setting <code>length</code> to -1 will attempt to read all data items from the input data array. NOTE: If input data does not have enough bytes, the operation will fail.</span></li>
</ul>
<div>
<h3 id="buffer-maker-help-dyn-spec">Dynamic specification</h3>
<div class="buffer-maker-node-help-indent">The <code>specification</code> can be passed in via msg, flow or global instead of being configured by the UI. The dynamic specification must be an object with the following properties...
<ul>
<li><span class="buffer-maker-prop-name">options</span> <span class="buffer-maker-prop-type">(Object)</span> <span class="buffer-maker-prop-desc"> processing options</span></li>
<li><span class="buffer-maker-prop-name">items</span> <span class="buffer-maker-prop-type">(Array)</span> <span class="buffer-maker-prop-desc"> array of items (see below) </span>
</ul>
<div>The <span class="buffer-maker-prop-name">options</span> object can have the following properties...
<ul>
<li><span class="buffer-maker-prop-name">byteSwap</span> <span class="buffer-maker-prop-type">(Boolean|Array|optional)</span>
<span class="buffer-maker-prop-desc"> swap all bytes before processing items. If <code>true</code>, then <code>swap16</code> will be performed. If <code>byteSwap</code> is an Array (e.g. <code>["swap64", "swap32", "swap64", "swap16"]</code>) multiple swaps can be performed in the specified order </span>
</li>
<li><span class="buffer-maker-prop-name">msgProperty</span> <span class="buffer-maker-prop-type">(String|optional)</span> <span class="buffer-maker-prop-desc"> How to return data. By default, data will be sent in <code>msg.payload</code> - this can be changed as required. e.g. set <span class="buffer-maker-prop-name">msgProperty</span> to <b>newPayload.data</b> to have results sent to <code>msg.newPayload.data</code> </span></li>
</ul>
</div>
<div>The <span class="buffer-maker-prop-name">items</span> array must contain 1 or more objects in the following format (not used when <code>resultType</code>="buffer")...
<ul>
<li><span class="buffer-maker-prop-name">name</span> <span class="buffer-maker-prop-type">(String)</span> <span class="buffer-maker-prop-desc"> A name to identify the resulting data.</span></li>
<li><span class="buffer-maker-prop-name">type</span> <span class="buffer-maker-prop-type">(String)</span> <span class="buffer-maker-prop-desc"> The type that input data should be written to the buffer as (see Allowable types below)</span></li>
<li><span class="buffer-maker-prop-name">length</span> <span class="buffer-maker-prop-type">(Number|optional)</span> <span class="buffer-maker-prop-desc"> The quantity of items to be written to the buffer. e.g. 12 floats or 34 int32s. NOTE: setting <code>length</code> to -1 will attempt to read all data items from the input data array. NOTE: If input data does not have enough bytes, the operation will fail.</span></li>
</ul>
</div>
</div>
<div class="buffer-maker-node-help-indent">Allowable <span class="buffer-maker-prop-name">type</span> options...
<ul>
<li>int, int8, byte, uint, uint8</li>
<li>int16, int16le, int16be, uint16, uint16le, uint16be</li>
<li>int32, int32le, int32be, uint32, uint32le, uint32be</li>
<li>bigint64, bigint64le, bigint64be, biguint64, biguint64le, biguint64be</li>
<li>float, floatle, floatbe, double, doublele, doublebe</li>
<li>8bit, 16bit 16bitle 16bitbe, bool</li>
<li>bcd, bcdle, bcdbe</li>
<li>string, hex, ascii, utf8, utf16le, ucs2, latin1, binary, buffer</li>
<li>NOTES...
<ul>
<li>If <b>le</b> (little endian) or <b>be</b> (big endian) is not specified, <b>be</b> will be assumed</li>
<li>bcd will convert the number to a 4BCD equivalent (bcd is not a standard buffer function)</li>
<li>bool data must be provided in an array e.g. <code>[true,true,false,false]</code> </li>
<li>8bit data must be provided in an array of 8bit arrays e.g. <code>[ [1,0,0,1,1,0,0,1], [1,1,0,0,1,1,0,0] ]</code> </li>
<li>16bit data must be provided in an array of 16bit arrays e.g. <code>[ [1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1], [1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0] ]</code> </li>
</ul>
</li>
</ul>
</div>
<div class="buffer-maker-node-help-indent">Example specification...
<pre><code style="font-size: smaller">
{
"options": {
"byteSwap": ["swap32", "swap16"],
"msgProperty": "payload"
},
"items": [
{
"name": "myInts",
"type": "int",
"length": 6,
"data": "payload.myInts"
"dataType": "msg"
},
{
"name": "myInt16ArrayInFlow",
"type": "int16be",
"length": 6,
"data": "myInt16Array"
"dataType": "flow"
},
{
"name": "booleans",
"type": "bool",
"length": 8,
"data": "[true,true,false,false,true,true,false,false]"
"dataType": "json"
},
{
"name": "16bits",
"type": "16bit",
"length": 1,
"data": "[1,1,0,0,1,0,1,0,1,1,0,0,1,0,1,0]"
"dataType": "json"
},
{
"name": "myString",
"type": "string",
"length": -1,
"data": "title",
"dataType": "global"
}
]
}
</pre></code>
</div>
</script>

View File

@@ -0,0 +1,710 @@
/*
MIT License
Copyright (c) 2020 Steve-Mcl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
module.exports = function (RED) {
const { number2bcd, bitsToByte, bitsToWord, setObjectProperty, isNumber, TYPEOPTS, SWAPOPTS } = require('./common-functions.js');
function bufferMakerNode(config) {
RED.nodes.createNode(this, config);
var node = this;
node.specification = config.specification || "";//specification
node.specificationType = config.specificationType || "ui";
node.items = config.items || [];
node.swap1 = config.swap1 || '';
node.swap2 = config.swap2 || '';
node.swap3 = config.swap3 || '';
node.swap1Type = config.swap1Type || 'swap';
node.swap2Type = config.swap2Type || 'swap';
node.swap3Type = config.swap3Type || 'swap';
node.msgProperty = config.msgProperty || 'payload';
node.msgPropertyType = config.msgPropertyType || 'str';
/**
* Generate a spec item from users input
* @param {object} item - a spec item with properties name, type, offset and length
* @param {Number} itemNumber - which item is this
* @returns An object with expected properties that has been (kinda) validated
*/
function parseSpecificationItem(item, itemNumber) {
if (!item)
throw new Error("Spec item is invalid");
let isObject = (item != null && typeof item === 'object' && (Array.isArray(item) === false));
if (!isObject)
throw new Error("Spec item is invalid");
let formattedSpecItem = Object.assign({}, item, {
"name": item.name || "item" + itemNumber,
"type": item.type,
"data": item.data,
"dataType": item.dataType,
"length": item.length || 1,
"id": itemNumber - 1
});
//ensure name is something
if (!formattedSpecItem.name) {
formattedSpecItem.name = `item[${formattedSpecItem.id}]`
}
//ensure type is provided
if (!formattedSpecItem.type)
throw new Error("type is not specified for item '" + (formattedSpecItem.name || "unnamed") + "'");
//ensure data is provided
if (!formattedSpecItem.data)
throw new Error("data is not specified for item '" + (formattedSpecItem.name || "unnamed") + "'");
//ensure dataType is provided
if (!formattedSpecItem.dataType)
throw new Error("dataType is not specified for item '" + (formattedSpecItem.name || "unnamed") + "'");
//validate type
if (!TYPEOPTS.includes(formattedSpecItem.type.toLowerCase())) {
throw new Error("'" + formattedSpecItem.type + "' is not a valid type (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//ensure length is valid
if (formattedSpecItem.length == null || formattedSpecItem.length == undefined) {
formattedSpecItem.length = 1;
} else if (isNumber(formattedSpecItem.length)) {
formattedSpecItem.length = parseInt(formattedSpecItem.length);
if (formattedSpecItem.length == 0 /* || formattedSpecItem.length < -1 */) {
throw new Error("length is not a valid number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
} else {
throw new Error("length is not a valid number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
return formattedSpecItem;
}
/**
* Check the provided specification is valid & set any defaults. Throws an error if the specification is invalid.
* @param {object | string} specification
* @returns correctly formated and validate specification object
*/
function parseSpecification(specification) {
if (typeof specification == "string") {
specification = JSON.parse();
}
let _spec = {
options: {
byteSwap: false
},
items: []
};
_spec.options.byteSwap = specification.options.byteSwap || false;
_spec.options.msgProperty = specification.options.msgProperty || "payload";
//validate byteSwap
if (Array.isArray(_spec.options.byteSwap)) {
let allFound = _spec.options.byteSwap.every(ai => SWAPOPTS.includes(ai));
if (!allFound) {
throw new Error("byteSwap property contains unsupported option");
}
}
//validate items
if (specification.items == null || Array.isArray(specification.items) == false || specification.items.length < 1) {
throw new Error("items property is not an array of objects")
}
let itemNum = 0;
_spec.items = specification.items.map(function (item) {
itemNum++;
return parseSpecificationItem(item, itemNum);
});
return _spec;
}
/**
* maker function reads the provided `specification` (json or JS object) and converts the items into the a buffer/array
*
* @param {Object} validatedSpec - The specification object with `{options:{byteSwap: boolean}}` and `{items[ {name: string, offset: number, length: number, type: string} ]}`
* @param {Object} msg - the incoming msg object
* @returns {Object} Returns an object containing `buffer` and `specification`
*/
function maker(validatedSpec, msg) {
let result = {
/** @type Buffer */buffer: null,
specification: validatedSpec
}
let bufferExpectedLength = 0;
const itemCount = validatedSpec.items.length;
/** @type Buffer */ var buf = Buffer.alloc(0);
//#region Helper function ......................................................
/**
* helper function to return 1 or more correctly formatted values from the buffer
* @param {Object} item item to convert
* @param {String} bufferFunction The buffer function to use
* @param {Number} dataSize
* @param {Function} [dataConversion] the conversion function to execute
*/
function itemReader(item, bufferFunction, dataSize, dataConversion) {
const data = Array.isArray(item.value) ? item.value : [item.value];
const dataCount = item.length === -1 ? data.length : item.length;
const b = dataToBuffer(data, dataCount, bufferFunction, dataSize, dataConversion);
const expectedLength = dataCount * dataSize;
if (!b) throw new Error(`Data item ${item.name} converted data is empty`);
if (b.length != expectedLength) throw new Error(`Data item ${item.name} converted byte length error. Expected ${expectedLength}, got ${b.length != expectedLength}`);
return {
buffer: b,
dataCount: dataCount,
dataSize: dataSize
};
}
//helper function to return 1 or more correctly formatted values from the buffer
function dataToBuffer(data, dataCount, bufferFunction, dataSize, dataConversion) {
const siz = dataSize * dataCount;
let buf = Buffer.alloc(siz);
if (buf[bufferFunction] == null) {
throw new Error(`Unknown Buffer method '${bufferFunction}'`);
}
let fn = buf[bufferFunction].bind(buf);
if (!Array.isArray(data)) data = [data];
for (let index = 0; index < dataCount; index++) {
let bufPos = (index * dataSize);
let dataItem = data[index];
if (dataConversion) dataItem = dataConversion(dataItem);
fn(dataItem, bufPos);//call specified function on the buffer
}
return buf;
}
/** Convert string or integer to bigint */
function toBigint(e) {
return BigInt(e);//a data convertor to handle implicit int ot big int conversions (otherwise buffer throws error)
}
function appendBuffer(dst, buf) {
return Buffer.concat([dst, buf]);
}
//#endregion
const options = {
lengthMultiplier: {
"hex": 0.5,
"utf16le": 2
},
lengthMod: {
"hex": 2
}
}
for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
let item = validatedSpec.items[itemIndex];
let itemDesc = item.name || ("item " + (itemIndex + 1));
let type = item.type;
let length = item.length || item.bytes || 1;
RED.util.evaluateNodeProperty(item.data, item.dataType, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate data of '" + itemDesc + "'", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate data" });
return;//halt flow!
} else {
item.value = value;
}
});
switch (type.toLowerCase()) {
case 'int':
case 'int8':
{
const dataSize = 1;
const irResult = itemReader(item, "writeInt8", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'uint':
case 'uint8':
case 'byte':
{
const dataSize = 1;
const irResult = itemReader(item, "writeUInt8", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'int16le':
{
const dataSize = 2;
const irResult = itemReader(item, "writeInt16LE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'int16':
case 'int16be':
{
const dataSize = 2;
const irResult = itemReader(item, "writeInt16BE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'uint16le':
{
const dataSize = 2;
const irResult = itemReader(item, "writeUInt16LE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'uint16':
case 'uint16be':
{
const dataSize = 2;
const irResult = itemReader(item, "writeUInt16BE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'int32le':
{
const dataSize = 4;
const irResult = itemReader(item, "writeInt32LE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'int32':
case 'int32be':
{
const dataSize = 4;
const irResult = itemReader(item, "writeInt32BE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'uint32le':
{
const dataSize = 4;
const irResult = itemReader(item, "writeUInt32LE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'uint32':
case 'uint32be':
{
const dataSize = 4;
const irResult = itemReader(item, "writeUInt32BE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'bigint64le':
{
const dataSize = 8;
const irResult = itemReader(item, "writeBigInt64LE", dataSize, toBigint);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'bigint64':
case 'bigint64be':
{
const dataSize = 8;
const irResult = itemReader(item, "writeBigInt64BE", dataSize, toBigint);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'biguint64le':
{
const dataSize = 8;
const irResult = itemReader(item, "writeBigUInt64LE", dataSize, toBigint);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'biguint64':
case 'biguint64be':
{
const dataSize = 8;
const irResult = itemReader(item, "writeBigUInt64BE", dataSize, toBigint);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break;
case 'floatle': //Reads a 32-bit float from buf at the specified offset
{
const dataSize = 4;
const irResult = itemReader(item, "writeFloatLE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break
case 'float': //Reads a 32-bit float from buf at the specified offset
case 'floatbe': //Reads a 32-bit float from buf at the specified offset
{
const dataSize = 4;
const irResult = itemReader(item, "writeFloatBE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break
case 'doublele': //Reads a 64-bit double from buf at the specified offset
{
const dataSize = 8;
const irResult = itemReader(item, "writeDoubleLE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break
case 'double': //Reads a 64-bit double from buf at the specified offset
case 'doublebe': //Reads a 64-bit double from buf at the specified offset
{
const dataSize = 8;
const irResult = itemReader(item, "writeDoubleBE", dataSize);
bufferExpectedLength += (irResult.dataCount * irResult.dataSize);
buf = appendBuffer(buf, irResult.buffer);
}
break
case 'string':// supported: 'ascii', 'utf8', 'utf16le', 'ucs2', 'latin1', and 'binary'.
type = "ascii";
case 'ascii':
case 'hex':
case 'utf8':
case 'utf-8':
case "utf16le":
case "ucs2":
case "latin1":
case "binary":
{
const dataSize = 1;
const _end = length === -1 ? undefined : length;
let itemValue = item.value;
const _length = _end || itemValue.length;
const lengthMod = options.lengthMod[type.toLowerCase()];
if (lengthMod) {
let m = _length % lengthMod;
if (m) throw new Error(`Length of '${itemDesc}' should be divisible by ${lengthMod}`);
}
const lengthMultiplier = options.lengthMultiplier[type.toLowerCase()];
if (lengthMultiplier != null) {
bufferExpectedLength += (_length * lengthMultiplier);
} else {
bufferExpectedLength += _length;
}
if (itemValue.length < _length) {
if(type=="ascii"||type=="utf8"||type=="utf-8"||type=="latin1") {
itemValue += "\0".repeat(_length-itemValue.length);//pad nulls to string
} else if(type=="utf16le"||type=="ucs2") {
itemValue += "\0\0".repeat(_length-itemValue.length);//pad nulls to string
} else {
throw new Error(`data for '${itemDesc}' is shorter than required length`);
}
}
const v = itemValue.slice(0, _end);
const b = Buffer.from(v, type);
buf = appendBuffer(buf, b);
}
break;
case "bool":
case "boolean":
{
//expect bools to be an array e.g. [true,false,true...]
let _byteCount;
if (length === -1) {
_byteCount = Math.floor(item.value.length / 8) + ((item.value.length % 8) > 0 ? 1 : 0)
} else {
_byteCount = Math.floor(length / 8) + ((length % 8) > 0 ? 1 : 0)
}
bufferExpectedLength += (_byteCount);
const b = Buffer.alloc(_byteCount);
for (let index = 0; index < _byteCount; index++) {
const offs = index * 8;
const bits = item.value.slice(offs, 8);
const bval = bitsToByte(bits);
b.writeUInt8(bval, index);
}
buf = appendBuffer(buf, b);
}
break;
case "8bit":
{
//expect bits to be an array of 8bit arrays e.g. [ [1,0,1,0,...], [1,0,1,0,...], ... ]
let _byteCount;
if (length === -1) {
_byteCount = item.value.length;
} else {
_byteCount = length;
}
bufferExpectedLength += (_byteCount);
const b = Buffer.alloc(_byteCount);
for (let index = 0; index < _byteCount; index++) {
const bits = item.value[index];
const bval = bitsToByte(bits);
b.writeUInt8(bval, index);
}
buf = appendBuffer(buf, b);
}
break;
case "16bit":
case "16bitle":
case "16bitbe":
{
//expect bits to be an array of 16bit arrays e.g. [ [1,0,1,0,...], [1,0,1,0,...], ... ]
let _byteCount;
let _len;
if (length === -1) {
_byteCount = item.value.length * 2;
_len = item.value.length;
} else {
_byteCount = length * 2;
_len = length;
}
bufferExpectedLength += _byteCount;
const b = Buffer.alloc(_byteCount);
let fn = type == "16bitle" ? b.writeUInt16LE.bind(b) : b.writeUInt16BE.bind(b);
for (let index = 0; index < _len; index++) {
const bits = item.value[index];
const bval = bitsToWord(bits);
fn(bval, index * 2);
}
buf = appendBuffer(buf, b);
}
break;
case "bcd":
case "bcdle":
case "bcdbe":
{
let _byteCount;
let _len;
let data = item.value;
if (!Array.isArray(data)) data = [data];
if (length === -1) {
_byteCount = data.length * 2;
_len = data.length;
} else {
_byteCount = length * 2;
_len = length;
}
data = data.slice(0, _len);
bufferExpectedLength += _byteCount;
dataBCD = data.map(e => number2bcd(e));
const b = Buffer.alloc(_byteCount);
let fn = type === "bcdle" ? b.writeUInt16LE.bind(b) : b.writeUInt16BE.bind(b);
for (let index = 0; index < _len; index++) {
fn(dataBCD[index], index * 2);
}
buf = appendBuffer(buf, b);
}
break;
case "buffer":
{
const _end = length === -1 ? undefined : length;
if (!(item.value instanceof Buffer)) throw new Error(`Expected value of '${itemDesc}' to be a Buffer`)
const b = item.value.slice(0, _end);
const _length = _end || b.length;
bufferExpectedLength += _length;
buf = appendBuffer(buf, b);
}
break;
default: {
const errmsg = `type '${item.type}' specified in '${itemDesc}' is not a recognised spec type`;
console.warn(errmsg);
throw new Error(errmsg);
break;
}
}
}
//byte swap the data if requested
//byteSwap can be boolean (i.e. swap16)
//or
//an array of directives e.g. ["swap64", "swap", "swap32"] - they will be executed in order
if (validatedSpec.options.byteSwap) {
if (Array.isArray(validatedSpec.options.byteSwap)) {
let swaps = validatedSpec.options.byteSwap;
for (let index = 0; index < swaps.length; index++) {
let sw = swaps[index];
if (sw && typeof sw == "string" && sw.length > 0) {
sw = sw.toLowerCase();
try {
switch (sw) {
case "swap":
case "swap16":
buf.swap16();
break;
case "swap32":
buf.swap32();
break;
case "swap64":
buf.swap64();
break;
default:
break;
}
} catch (error) {
throw new Error("Cannot " + sw + ": " + error.message);
}
}
}
} else {
try {
buf.swap16();
} catch (error) {
throw new Error("Cannot swap16: " + error.message);
}
}
}
if (buf.length !== bufferExpectedLength) throw new Error(`Final buffer length is not correct. Expected ${bufferExpectedLength}, got ${buf.length}`)
result.buffer = buf;
return result;
}
node.on('input', function (msg) {
node.status({});//clear status
let specification;
RED.util.evaluateNodeProperty(node.specification, node.specificationType, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate specification", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate specification" });
return;//halt flow!
} else {
specification = value;
}
});
if (node.specificationType == "ui") {
specification = {};
var swap1;
RED.util.evaluateNodeProperty(node.swap1, node.swap1Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap1", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap1" });
return;//halt flow!
} else {
if (node.swap1Type == "env") {
swap1 = value.split(",");
swap1 = swap1.map(e => e.trim());
} else {
swap1 = value;
}
}
});
var swap2;
var swap3;
if (node.swap1Type == "swap") {
RED.util.evaluateNodeProperty(node.swap2, node.swap2Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap2", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap2" });
return;//halt flow!
} else {
swap2 = value;
}
});
RED.util.evaluateNodeProperty(node.swap3, node.swap3Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap3", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap3" });
return;//halt flow!
} else {
swap3 = value;
}
});
}
var msgProperty = node.msgProperty;
var swap = [];
if (Array.isArray(swap1)) {
swap = swap1;
} else {
if (swap1) {
swap.push(swap1);
if (swap2) {
swap.push(swap2);
if (swap3) {
swap.push(swap3);
}
}
}
}
specification = {
"options": {
"byteSwap": swap,
"msgProperty": msgProperty,
},
"items": node.items
}
}
let validatedSpec;
try {
validatedSpec = parseSpecification(specification)
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: error.message });
return;//halt flow
}
msg.originalPayload = msg.payload;//store original Payload incase user still wants it
try {
let results = maker(validatedSpec, msg);
if (validatedSpec.options.singleResult !== false) {
msg.specification = results.specification;
setObjectProperty(msg, validatedSpec.options.msgProperty, results.buffer, ".")
node.send(msg);
}
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: "Error parsing data" });
return;//halt flow
}
});
}
RED.nodes.registerType("buffer-maker", bufferMakerNode);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,818 @@
/*
MIT License
Copyright (c) 2020 Steve-Mcl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
module.exports = function (RED) {
const SUPPORTS_BIGINT = parseFloat(process.versions.node) >= 10.4;
const RESULTYPEOPTS = ["object", "keyvalue", "value", "array", "buffer"];
const { setObjectProperty, bcd2number, byteToBits, wordToBits, isNumber, TYPEOPTS, SWAPOPTS } = require('./common-functions.js');
const scalingOps = {
">": (v, o) => v > o,
"<": (v, o) => v < o,
"==": (v, o) => v == o,
"!=": (v, o) => v != o,
"%": (v, o) => v % o,
"<<": (v, o) => v << o,
">>": (v, o) => v >> o,
">>>": (v, o) => v >>> o,
"**": (v, o) => v ** o,
"^": (v, o) => v ^ o,
"/": (v, o) => v / o,
"*": (v, o) => v * o,
"+": (v, o) => v + o,
"-": (v, o) => v - o,
"!!": (v) => !!v,
};
const scalerRegex = /\s*?([\/\-\+\*<>^!%=]*?)\s*?(\w+)/g;
function bufferParserNode(config) {
RED.nodes.createNode(this, config);
var node = this;
node.data = config.data || "";//data
node.dataType = config.dataType || "msg";
node.specification = config.specification || "";//specification
node.specificationType = config.specificationType || "ui";
node.items = config.items || [];
node.swap1 = config.swap1 || '';
node.swap2 = config.swap2 || '';
node.swap3 = config.swap3 || '';
node.swap1Type = config.swap1Type || 'swap';
node.swap2Type = config.swap2Type || 'swap';
node.swap3Type = config.swap3Type || 'swap';
node.msgProperty = config.msgProperty || 'payload';
node.msgPropertyType = config.msgPropertyType || 'str';
node.resultType = config.resultType || 'value';
node.resultTypeType = config.resultTypeType || 'str';
node.multipleResult = config.multipleResult == true;
node.fanOutMultipleResult = node.multipleResult == true && config.fanOutMultipleResult == true;
node.setTopic = config.setTopic != false;
/**
* Generate a spec item from users input
* @param {object} item - a spec item with properties name, type, offset and length
* @param {Number} itemNumber - which item is this
* @returns An object with expected properties that has been (kinda) validated
*/
function parseSpecificationItem(item, itemNumber) {
if (!item)
throw new Error("Spec item is invalid");
let isObject = (item != null && typeof item === 'object' && (Array.isArray(item) === false));
if (!isObject)
throw new Error("Spec item is invalid");
let formattedSpecItem = Object.assign({}, item, {
"name": item.name || "item" + itemNumber,
"type": item.type,
"offset": item.offset,
"offsetbit": item.offsetbit,
"scale": item.scale,
"length": item.length || 1,
"id": itemNumber - 1
});
//ensure name is something
if (!formattedSpecItem.name) {
formattedSpecItem.name = `item[${formattedSpecItem.id}]`
}
//ensure type is provided
if (!formattedSpecItem.type)
throw new Error("type is not specified for item '" + (formattedSpecItem.name || "unnamed") + "'");
//validate type
if (!TYPEOPTS.includes(formattedSpecItem.type.toLowerCase())) {
throw new Error("'" + formattedSpecItem.type + "' is not a valid type (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//ensure length is valid
if (formattedSpecItem.length == null || formattedSpecItem.length == undefined) {
formattedSpecItem.length = 1;
} else if (isNumber(formattedSpecItem.length)) {
formattedSpecItem.length = parseInt(formattedSpecItem.length);
if (formattedSpecItem.length == 0 /* || formattedSpecItem.length < -1 */) {
throw new Error("length is not a valid number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
} else {
throw new Error("length is not a valid number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//ensure offest is something (check other permissable property names for "offset" e.g. index & start)
if (formattedSpecItem.offset == null || formattedSpecItem.offset == undefined) {
formattedSpecItem.offset == item.index;
if (formattedSpecItem.offset == null)
formattedSpecItem.offset == item.start || 0;
}
formattedSpecItem.offset = formattedSpecItem.offset || 0;
if (isNumber(formattedSpecItem.offset)) {
formattedSpecItem.offset = parseInt(formattedSpecItem.offset)
if (formattedSpecItem.offset < 0) {
throw new Error("offsetbit must be zero or greater (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
} else {
throw new Error("offset is not a number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//ensure offsetbit is something
if (formattedSpecItem.offsetbit == null || formattedSpecItem.offsetbit == undefined) {
formattedSpecItem.offsetbit = 0;
}
if (isNumber(formattedSpecItem.offsetbit)) {
formattedSpecItem.offsetbit = parseInt(formattedSpecItem.offsetbit);
if (formattedSpecItem.offsetbit < 0) {
throw new Error("offsetbit must be zero or greater (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
} else {
throw new Error("offsetbit is not a number (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//compile scaler
let scale = null;
if (formattedSpecItem.scale) {
if (typeof formattedSpecItem.scale == "number") {
scale = formattedSpecItem.scale.toString();
} else {
scale = formattedSpecItem.scale && formattedSpecItem.scale.trim();
}
}
if (scale) {
try {
if (scale != "1" && scale != "0") {
if (isNumber(scale)) {
formattedSpecItem.scaler = { operator: '*', operand: Number(scale) };
} else {
if (scale == "!" || scale == "!!") scale += "0"
let matches = [];
if(!scale.matchAll) {
//throw new Error("Scaling equations not supported by the running version of node-js. It is recommended you upgrade nodejs to V12 or greater. Alternatively, do your own scaling on the output.")
//TEMP: emulate matchAll
while ((match = scalerRegex.exec(scale)) !== null) {
matches.push([...match])
break;
}
} else {
matches = scale.matchAll(scalerRegex);
}
for (const match of matches) {
formattedSpecItem.scaler = {
operator: match["1"].trim(),
operand: Number(match["2"])
}
break;
}
if (!formattedSpecItem.scaler || !formattedSpecItem.scaler.operator || !scalingOps[formattedSpecItem.scaler.operator] || !isNumber(formattedSpecItem.scaler.operand)) {
throw new Error("scaling equation '" + formattedSpecItem.scale + "' is not valid (item " + itemNumber + " '" + (formattedSpecItem.name || "unnamed") + "')");
}
}
}
} catch (e) {
throw e
}
}
return formattedSpecItem;
}
/**
* Check the provided specification is valid & set any defaults. Throws an error if the specification is invalid.
* @param {object | string} specification
* @returns correctly formated and validate specification object
*/
function parseSpecification(specification) {
if (typeof specification == "string") {
specification = JSON.parse();
}
let _spec = {
options: {
byteSwap: false,
resultType: "value",
singleResult: true
},
items: []
};
_spec.options.resultType = specification.options.resultType || "value";
_spec.options.byteSwap = specification.options.byteSwap || false;
_spec.options.msgProperty = specification.options.msgProperty || "payload";
if (specification.options.multipleResult === true) _spec.options.singleResult = false;
if (specification.options.multipleResult === false) _spec.options.singleResult = true;
if (specification.options.singleResult === false) _spec.options.singleResult = false;
if (specification.options.singleResult === true) _spec.options.singleResult = true;
_spec.options.setTopic = specification.options.setTopic === false ? false : true;
//validate resultType
if (!RESULTYPEOPTS.includes(_spec.options.resultType)) {
throw new Error("resultType property is invalid");
}
//validate byteSwap
if (Array.isArray(_spec.options.byteSwap)) {
let allFound = _spec.options.byteSwap.every(ai => SWAPOPTS.includes(ai));
if (!allFound) {
throw new Error("byteSwap property contains unsupported option");
}
}
//dont parse .items if user just wants a buffer
if (_spec.options.resultType !== "buffer") {
//validate items
if (specification.items == null || Array.isArray(specification.items) == false || specification.items.length < 1) {
throw new Error("items property is not an array of objects")
}
let itemNum = 0;
_spec.items = specification.items.map(function (item) {
itemNum++;
return parseSpecificationItem(item, itemNum);
});
}
return _spec;
}
/**
* parser function reads the provided `specification` (json or JS object) and converts the items in the `data` to the type specified in each element of `specification.items`
*
* @param {Buffer|integer[]} data - The data to parse. Must be either an array of `integer` or a `Buffer`
* @param {object} specification - an object with `{options:{byteSwap: boolean}}` and `{items[ {name: string, offset: number, length: number, type: string} ]}`
* @returns result object containing . `objectResults:{}`, `arrayResults[]`, `values[]`
*/
function parser(data, validatedSpec, msg) {
let result = {
objectResults: {},
keyvalues: {},
arrayResults: [],
values: [],
specification: validatedSpec
}
/** @type Buffer */ var buf;
let isArray = Array.isArray(data);
let isBuffer = Buffer.isBuffer(data);
if (typeof data == "string") {
data = new Buffer.from(data, "hex");
isBuffer = true;
}
if (!isArray && !isBuffer) {
throw new Error(`data is not an array or a buffer`);
}
//get buffer
if (isBuffer) {
buf = data;
}
//convert int16 array to buffer for easy access to data
if (isArray) {
buf = new Buffer.alloc(data.length * 2);
let pos = 0;
var arrayLength = data.length;
for (var i = 0; i < arrayLength; i++) {
let lb = (data[i] & 0x00ff);
let hb = ((data[i] & 0xff00) >> 8);
buf.writeUInt8(hb, pos++);
buf.writeUInt8(lb, pos++);
}
}
//byte swap the data if requested
//byteSwap can be boolean (i.e. swap16)
//or
//an array of directives e.g. ["swap64", "swap", "swap32"] - they will be executed in order
if (validatedSpec.options.byteSwap) {
if (Array.isArray(validatedSpec.options.byteSwap)) {
let swaps = validatedSpec.options.byteSwap;
for (let index = 0; index < swaps.length; index++) {
let sw = swaps[index];
if (sw && typeof sw == "string" && sw.length > 0) {
sw = sw.toLowerCase();
try {
switch (sw) {
case "swap":
case "swap16":
buf.swap16();
break;
case "swap32":
buf.swap32();
break;
case "swap64":
buf.swap64();
break;
default:
break;
}
} catch (error) {
throw new Error("Cannot " + sw + ": " + error.message);
}
}
}
} else {
try {
buf.swap16();
} catch (error) {
throw new Error("Cannot swap16: " + error.message);
}
}
}
//helper function to return 1 or more correctly formatted values from the buffer
function itemReader(item, buffer, bufferFunction, dataSize) {
item.value = dataGetter(buffer, item.offset, item.length, bufferFunction, dataSize, item.mask, item.scaler);
// result.objectResults[item.name] = item;
setObjectProperty(result.objectResults, item.name, item, "=>");
// result.keyvalues[item.name] = item.value;
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
function sanitizeMask(mask, numberFn, throwError) {
let _mask = mask
try {
if (_mask) {
if (typeof _mask == "string" && _mask.trim() == "") {
return 0;
}
_mask = numberFn(_mask)
if (isNaN(Number(_mask))) {
if (throwError) throw new Error("mask " + mask + " is invalid")
}
}
} catch (error) {
if (throwError) throw e
}
return _mask;
}
//helper function to return 1 or more correctly formatted values from the buffer
function dataGetter(buffer, startByte, dataCount, bufferFunction, dataSize, mask, scaler) {
const numberConvertor = bufferFunction.indexOf("readBig") == 0 ? BigInt : Number
const _mask = sanitizeMask(mask, numberConvertor, true);
let index = 0;
let value;
if (dataCount === -1) {
dataCount = Math.floor((buffer.length - startByte) / dataSize);
}
if (dataCount > 1) {
value = [];
}
if (buffer[bufferFunction] == null) {
throw new Error(`Unknown Buffer method '${bufferFunction}'`);
}
const fn = buffer[bufferFunction].bind(buffer);
for (index = 0; index < dataCount; index++) {
const bufPos = startByte + (index * dataSize);
let val = fn(bufPos);//call specified function on the buffer
if (_mask != 0) {
val = (val & _mask);
}
if (scaler && scaler.operator && scalingOps[scaler.operator]) {
val = scalingOps[scaler.operator](val, scaler.operand);
}
if (dataCount > 1) {
value.push(val);
} else {
value = val
}
}
return value;
}
result.buffer = buf;
if (validatedSpec.options.resultType === "buffer") {
return result;
}
var itemCount = validatedSpec.items.length;
var fanOut = [];
for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
let item = validatedSpec.items[itemIndex];
let type = item.type;
let offset = item.startByte || item.offset || 0;
let length = item.length || item.bytes || 1;
switch (type.toLowerCase()) {
case 'int':
case 'int8':
itemReader(item, buf, "readInt8", 1);
break;
case 'uint':
case 'uint8':
case 'byte':
itemReader(item, buf, "readUInt8", 1);
break;
case 'int16le':
itemReader(item, buf, "readInt16LE", 2);
break;
case 'int16':
case 'int16be':
itemReader(item, buf, "readInt16BE", 2);
break;
case 'uint16le':
itemReader(item, buf, "readUInt16LE", 2);
break;
case 'uint16':
case 'uint16be':
itemReader(item, buf, "readUInt16BE", 2);
break;
case 'int32le':
itemReader(item, buf, "readInt32LE", 4);
break;
case 'int32':
case 'int32be':
itemReader(item, buf, "readInt32BE", 4);
break;
case 'uint32le':
itemReader(item, buf, "readUInt32LE", 4);
break;
case 'uint32':
case 'uint32be':
itemReader(item, buf, "readUInt32BE", 4);
break;
case 'bigint64le':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0 or greater")
}
itemReader(item, buf, "readBigInt64LE", 8);
break;
case 'bigint64':
case 'bigint64be':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0 or greater")
}
itemReader(item, buf, "readBigInt64BE", 8);
break;
case 'biguint64le':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0 or greater")
}
itemReader(item, buf, "readBigUInt64LE", 8);
break;
case 'biguint64':
case 'biguint64be':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0 or greater")
}
itemReader(item, buf, "readBigUInt64BE", 8);
break;
case 'floatle': //Reads a 32-bit float from buf at the specified offset
itemReader(item, buf, "readFloatLE", 4);
break
case 'float': //Reads a 32-bit float from buf at the specified offset
case 'floatbe': //Reads a 32-bit float from buf at the specified offset
itemReader(item, buf, "readFloatBE", 4);
break
case 'doublele': //Reads a 64-bit double from buf at the specified offset
itemReader(item, buf, "readDoubleLE", 8);
break
case 'double': //Reads a 64-bit double from buf at the specified offset
case 'doublebe': //Reads a 64-bit double from buf at the specified offset
itemReader(item, buf, "readDoubleBE", 8);
break
case 'string':// supported: 'ascii', 'utf8', 'utf16le', 'ucs2', 'latin1', and 'binary'.
type = "ascii"
case 'ascii':
case 'hex':
case 'utf8':
case "utf16le":
case "ucs2":
case "latin1":
case "binary":
{
let _end = length === -1 ? undefined : offset + length;
item.value = buf.toString(type, offset, _end);
if(type=="ascii"||type=="utf8"||type=="utf-8"||type=="latin1") {
const nullIdx = item.value.indexOf('\0');
if(nullIdx > -1) {
item.value = item.value.substr(0, nullIdx);
}
}
setObjectProperty(result.objectResults, item.name, item, "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "bool":
case "boolean":
{
let _byteCount;
if (length === -1) {
_byteCount = -1
} else {
_byteCount = Math.floor(((item.offsetbit + length) / 8)) + (((item.offsetbit + length) % 8) > 0 ? 1 : 0)
}
let data = dataGetter(buf, item.offset, _byteCount, "readUInt8", 1, item.mask)
let bitData = []
if (Array.isArray(data) == false) {
data = [data]
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = byteToBits(thisByte);
bitData.push(...bits.bits.map(e => e ? true : false));
}
if (length === 1) {
item.value = bitData[item.offsetbit];
} else if (length === -1) {
item.value = bitData.slice(item.offsetbit); // -1 - return all to the end.
} else {
item.value = bitData.slice(item.offsetbit, item.offsetbit + length);
}
setObjectProperty(result.objectResults, item.name, item, "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "8bit":
{
let data = dataGetter(buf, item.offset, length, "readUInt8", 1, item.mask)
let bitData = [];
if (Array.isArray(data) === false) {
data = [data]
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = byteToBits(thisByte);
bitData.push(bits);
}
item.value = bitData;
setObjectProperty(result.objectResults, item.name, item, "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "16bit":
case "16bitle":
case "16bitbe":
{
let fn = type == "16bitle" ? "readUInt16LE" : "readUInt16BE";
let data = dataGetter(buf, item.offset, length, fn, 2, item.mask)
let bitData = [];
if (Array.isArray(data) == false) {
data = [data];
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = wordToBits(thisByte);
bitData.push(bits);
}
item.value = bitData;
setObjectProperty(result.objectResults, item.name, item, "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "bcd":
case "bcdle":
case "bcdbe":
{
let fn = type == "bcdle" ? "readUInt16LE" : "readUInt16BE";
let data = dataGetter(buf, item.offset, length, fn, 2, item.mask)
if (item.length > 1) {
dataBCD = data.map(e => bcd2number(e));
} else {
dataBCD = bcd2number(data)
}
item.value = dataBCD;
setObjectProperty(result.objectResults, item.name, item, "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "buffer":
{
let _end = length === -1 ? undefined : offset + length;
item.value = buf.slice(offset, _end);
setObjectProperty(result.objectResults, item.name, item, "=>", "=>");
setObjectProperty(result.keyvalues, item.name, item.value, "=>", "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
default: {
let errmsg = `type '${item.type}' is not a recognised parse specification`;
console.warn(errmsg);
throw new Error(errmsg);
break;
}
}
if (validatedSpec.options.singleResult === false) {
let m = { topic: msg.topic, specification: item };
if (validatedSpec.options.setTopic) m.topic = item.name;
switch (validatedSpec.options.resultType) {
case "value":
case "keyvalue":
setObjectProperty(m, validatedSpec.options.msgProperty, item.value, ".")
break;
case "object":
setObjectProperty(m, validatedSpec.options.msgProperty, item, ".")
break;
}
if (node.fanOutMultipleResult) {
fanOut[itemIndex] = m;
} else {
node.send(m);
}
}
}
if (node.fanOutMultipleResult) {
return fanOut;
}
return result;
}
node.on('input', function (msg) {
node.status({});//clear status
var data;
RED.util.evaluateNodeProperty(node.data, node.dataType, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate data", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate data" });
return;//halt flow!
} else {
data = value;
}
});
var specification;
RED.util.evaluateNodeProperty(node.specification, node.specificationType, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate specification", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate specification" });
return;//halt flow!
} else {
specification = value;
}
});
if (node.specificationType == "ui") {
specification = {};
var swap1;
RED.util.evaluateNodeProperty(node.swap1, node.swap1Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap1", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap1" });
return;//halt flow!
} else {
if (node.swap1Type == "env") {
swap1 = value.split(",");
swap1 = swap1.map(e => e.trim());
} else {
swap1 = value;
}
}
});
var swap2;
var swap3;
if (node.swap1Type == "swap") {
RED.util.evaluateNodeProperty(node.swap2, node.swap2Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap2", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap2" });
return;//halt flow!
} else {
swap2 = value;
}
});
RED.util.evaluateNodeProperty(node.swap3, node.swap3Type, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap3", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate swap3" });
return;//halt flow!
} else {
swap3 = value;
}
});
}
var resultType;
RED.util.evaluateNodeProperty(node.resultType, node.resultTypeType, node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate resultType", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to evaluate resultType" });
return;//halt flow!
} else {
resultType = value;
}
});
var msgProperty = node.msgProperty;
var swap = [];
if (Array.isArray(swap1)) {
swap = swap1;
} else {
if (swap1) {
swap.push(swap1);
if (swap2) {
swap.push(swap2);
if (swap3) {
swap.push(swap3);
}
}
}
}
specification = {
"options": {
"byteSwap": swap,
"resultType": resultType,
"msgProperty": msgProperty,
"multipleResult": node.multipleResult,
"setTopic": node.setTopic
},
"items": node.items
}
}
let validatedSpec;
try {
validatedSpec = parseSpecification(specification)
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: error.message });
return;//halt flow
}
msg.originalPayload = msg.payload;//store original Payload in case user still wants it
try {
let results = parser(data, validatedSpec, msg);
if (validatedSpec.options.singleResult !== false) {
msg.specification = results.specification;
msg.values = results.values;
msg.objectResults = results.objectResults;
msg.keyvalues = results.keyvalues;
msg.arrayResults = results.arrayResults;
msg.buffer = results.buffer;
switch (validatedSpec.options.resultType) {
case "buffer":
setObjectProperty(msg, validatedSpec.options.msgProperty, msg.buffer, ".");
break;
case "value":
setObjectProperty(msg, validatedSpec.options.msgProperty, msg.values, ".");
break;
case "object":
setObjectProperty(msg, validatedSpec.options.msgProperty, msg.objectResults, ".");
break;
case "keyvalue":
case "keyvalues":
setObjectProperty(msg, validatedSpec.options.msgProperty, msg.keyvalues, ".");
break;
case "array":
setObjectProperty(msg, validatedSpec.options.msgProperty, msg.arrayResults, ".");
break;
}
node.send(msg);
} else if (node.fanOutMultipleResult) {
node.send(results);
}
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: "Error parsing data" });
return;//halt flow
}
});
}
RED.nodes.registerType("buffer-parser", bufferParserNode);
}

View File

@@ -0,0 +1,190 @@
/**
* number2bcd -> takes a bcd number and returns the corresponding decimal value
* @param {Number} number BCD number to convert
* @param {*} digits no of digits (default 4)
*/
const bcd2number = function (number, digits = 4) {
let loByte = (number & 0x00ff);
let hiByte = (number >> 8) & 0x00ff;
let n = 0;
n += (loByte & 0x0F) * 1;
if (digits < 2) return n;
n += ((loByte >> 4) & 0x0F) * 10;
if (digits < 3) return n;
n += (hiByte & 0x0F) * 100;
if (digits < 4) return n;
n += ((hiByte >> 4) & 0x0F) * 1000;
return n;
}
/**
* number2bcd -> takes a number and returns the corresponding BCD value.
* @param {Number} number number to convert to bcd
* @param {Number} [digits] no of digits (default 4)
* @returns {Buffer} nodejs buffer
*/
const number2bcd = function (number, digits) {
var s = digits || 4; //default value: 4
var n = 0;
n = (number % 10);
number = (number / 10) | 0;
if (s < 2) return n;
n += (number % 10) << 4;
number = (number / 10) | 0;
if (s < 3) return n;
n += (number % 10) << 8;
number = (number / 10) | 0;
if (s < 4) return n;
n += (number % 10) << 12;
number = (number / 10) | 0;
return n;
}
function byteToBits(val) {
var bits = [];
for (let index = 0; index < 8; index++) {
const bit = getBit(val, index);
bits.push(bit);
}
return {
bits: bits,
bit0: bits[0],
bit1: bits[1],
bit2: bits[2],
bit3: bits[3],
bit4: bits[4],
bit5: bits[5],
bit6: bits[6],
bit7: bits[7],
}
}
function wordToBits(val) {
var bits = [];
for (let index = 0; index < 16; index++) {
const bit = getBit(val, index);
bits.push(bit);
}
return {
bits: bits,
bit0: bits[0],
bit1: bits[1],
bit2: bits[2],
bit3: bits[3],
bit4: bits[4],
bit5: bits[5],
bit6: bits[6],
bit7: bits[7],
bit8: bits[8],
bit9: bits[9],
bit10: bits[10],
bit11: bits[11],
bit12: bits[12],
bit13: bits[13],
bit14: bits[14],
bit15: bits[15],
}
}
//Get Bit
function getBit(number, bitPosition) {
return (number & (1 << bitPosition)) === 0 ? 0 : 1;
}
//Set Bit
function setBit(number, bitPosition) {
return number | (1 << bitPosition);
}
//Clear Bit
function clearBit(number, bitPosition) {
const mask = ~(1 << bitPosition);
return number & mask;
}
//Update Bit
function updateBit(number, bitPosition, bitValue) {
const bitValueNormalized = bitValue ? 1 : 0;
const clearMask = ~(1 << bitPosition);
return (number & clearMask) | (bitValueNormalized << bitPosition);
}
function bitsToByte(bits) {
var byte = 0;
for (let index = 0; index < 8; index++) {
let bit = bits[index];
if (bit) byte = setBit(byte, index);
}
return byte;
}
function bitsToWord(val) {
var wd = 0;
for (let index = 0; index < 16; index++) {
let bit = val[index];
if (bit) wd = setBit(wd, index);
}
return wd;
}
const SWAPOPTS = ["swap16", "swap32", "swap64"];
const TYPEOPTS = [
"int", "int8", "byte",
"uint", "uint8",
"int16", "int16le", "int16be", "uint16", "uint16le", "uint16be",
"int32", "int32le", "int32be", "uint32", "uint32le", "uint32be",
"bigint64", "bigint64le", "bigint64be", "biguint64", "biguint64le", "biguint64be",
"float", "floatle", "floatbe", "double", "doublele", "doublebe",
"8bit", "16bit", "16bitle", "16bitbe", "bool",
"bcd", "bcdle", "bcdbe",
"string", "hex", "ascii", "utf8", "utf-8", "utf16le", "ucs2", "latin1", "binary", "buffer"
];
/**
* helper function to set a nested property by path
* @param {*} obj - the object in which to set a properties value
* @param {string} path - the path to the property e.g. payload.value
* @param {*} val - the value to set in obj.path
*/
function setObjectProperty(obj, path, val, sep) {
sep = sep == null ? "." : sep;
const keys = path.split(sep);
const lastKey = keys.pop();
const lastObj = keys.reduce((obj, key) =>
obj[key] = obj[key] || {},
obj);
lastObj[lastKey] = val;
}
/**
* helper function to get a property by path
* @param {*} obj - the object in which to set a properties value
* @param {string} path - the path to the property e.g. payload.value
* @param {*} [sep] - the path property separator (defaults to `.`)
*/
function getObjectProperty(obj, path, sep) {
sep = sep == null ? "." : sep;
for (var i=0, path=path.split(sep), len=path.length; i<len; i++){
obj = obj[path[i]];
};
return obj;
}
function isNumber(n) {
if (n === "" || n === true || n === false) return false;
return !isNaN(parseFloat(n)) && isFinite(n);
}
exports.bcd2number = bcd2number;
exports.number2bcd = number2bcd;
exports.byteToBits = byteToBits;
exports.wordToBits = wordToBits;
exports.bitsToByte = bitsToByte;
exports.bitsToWord = bitsToWord;
exports.getBit = getBit;
exports.setBit = setBit;
exports.clearBit = clearBit;
exports.updateBit = updateBit;
exports.isNumber = isNumber;
exports.setObjectProperty = setObjectProperty;
exports.getObjectProperty = getObjectProperty;
exports.SWAPOPTS = SWAPOPTS;
exports.TYPEOPTS = TYPEOPTS;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,53 @@
{
"name": "node-red-contrib-buffer-parser",
"version": "3.2.2",
"description": "Node-red nodes to convert values to and from buffer/array. Supports Big/Little Endian, BCD, byte swapping and much more",
"keywords": [
"node-red",
"parser",
"converter",
"buffer",
"plc",
"BCD",
"endian",
"swap",
"byte swap"
],
"license": "MIT",
"maintainers": [
{
"name": "Steve-Mcl",
"email": "44235289+Steve-Mcl@users.noreply.github.com"
}
],
"author": {
"name": "Steve-Mcl"
},
"node-red": {
"version": ">=0.20.0",
"nodes": {
"buffer-parser": "buffer-parser.js",
"buffer-maker": "buffer-maker.js"
}
},
"engines": {
"node" : ">=10.4.0"
},
"bugs": {
"url": "https://github.com/Steve-Mcl/node-red-contrib-buffer-parser/issues"
},
"homepage": "https://github.com/Steve-Mcl/node-red-contrib-buffer-parser",
"repository": {
"type": "git",
"url": "https://github.com/Steve-Mcl/node-red-contrib-buffer-parser.git"
},
"scripts": {
"publish": "npm publish",
"test": "mocha \"test/**/*_spec.js\""
},
"devDependencies": {
"mocha": "^8.2.1",
"node-red-node-test-helper": "^0.2.5",
"should": "^13.2.3"
}
}

View File

@@ -0,0 +1,103 @@
var should = require("should");
var helper = require("node-red-node-test-helper");
var bufferMaker = require("../buffer-maker.js");
const getTestFlow = (nodeName, resultPayloadPropName) => {
resultPayloadPropName = resultPayloadPropName ? resultPayloadPropName : "payload";
return [
{ id: 'helperNode', type: 'helper' },
{
"id": "testNode", "type": "buffer-maker", "name": nodeName, "msgProperty": resultPayloadPropName, "specification": "spec", "specificationType": "ui",
"items": [
{ "name": "item1", "type": "byte", "length": 1, "dataType": "num", "data": "1" },
{ "name": "item2", "type": "int8", "length": 1, "dataType": "num", "data": "-2" },
{ "name": "item3", "type": "uint8", "length": 1, "dataType": "num", "data": "3" },
{ "name": "item4", "type": "int16le", "length": 1, "dataType": "num", "data": "-4" },
{ "name": "item5", "type": "int16be", "length": 1, "dataType": "num", "data": "-5" },
{ "name": "item6", "type": "uint16le", "length": 1, "dataType": "num", "data": "6" },
{ "name": "item7", "type": "uint16le", "length": 1, "dataType": "num", "data": "7" },
{ "name": "item8", "type": "int32le", "length": 1, "dataType": "num", "data": "-8" },
{ "name": "item9", "type": "int32be", "length": 1, "dataType": "num", "data": "-9" },
{ "name": "item10", "type": "uint32le", "length": 1, "dataType": "num", "data": "10" },
{ "name": "item11", "type": "uint32be", "length": 1, "dataType": "num", "data": "11" },
{ "name": "item12", "type": "bigint64le", "length": 1, "dataType": "num", "data": "-120000000000" },
{ "name": "item13", "type": "bigint64be", "length": 1, "dataType": "num", "data": "-130000000000" },
{ "name": "item14", "type": "biguint64le", "length": 1, "dataType": "num", "data": "14000000000" },
{ "name": "item15", "type": "biguint64be", "length": 1, "dataType": "num", "data": "15000000000" },
{ "name": "item16", "type": "floatle", "length": 1, "dataType": "num", "data": "16.161616" },
{ "name": "item17", "type": "floatbe", "length": 1, "dataType": "num", "data": "17.171717" },
{ "name": "item18", "type": "doublele", "length": 1, "dataType": "num", "data": "18.1818e-18" },
{ "name": "item19", "type": "doublebe", "length": 1, "dataType": "num", "data": "19.1919e-19" },
{ "name": "item20", "type": "8bit", "length": 1, "dataType": "jsonata", "data": "[[1,0,1,0,0,1,1,0]]" },
{ "name": "item21", "type": "16bitle", "length": 1, "dataType": "jsonata", "data": "[[1,0,1,0,0,1,1,0,1,0,1,0,0,1,1,0]]" },
{ "name": "item22", "type": "16bitbe", "length": 1, "dataType": "jsonata", "data": "[[1,0,1,0,0,1,1,0,1,0,1,0,0,1,1,0]]" },
{ "name": "item23", "type": "bcdle", "length": 1, "dataType": "num", "data": "2323" },
{ "name": "item24", "type": "bcdbe", "length": 1, "dataType": "num", "data": "2424" },
{ "name": "item25", "type": "string", "length": 2, "dataType": "str", "data": "25" },
{ "name": "item26", "type": "hex", "length": 4, "dataType": "str", "data": "2626" },
{ "name": "item27", "type": "buffer", "length": 24, "dataType": "bin", "data": "[50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55]" },
{ "name": "item28", "type": "buffer", "length": -1, "dataType": "bin", "data": "[50,55,50,55]" }
],
"swap1": "", "swap2": "", "swap3": "", "swap1Type": "swap", "swap2Type": "swap", "swap3Type": "swap", "msgPropertyType": "str",
"wires": [["helperNode"]]
}
];
};
helper.init(require.resolve('node-red'));
describe('buffer-maker Node', function(){
"use strict";
beforeEach(done => { helper.startServer(done); });
afterEach(done => { helper.unload().then(() => helper.stopServer(done)); });
it('should be loaded', done => {
// const flow = [{ id: 'testNode', type: 'buffer-maker', name: 'DEMO buffer-maker' }];
const flow = [{"id":"testNode","type":"buffer-maker","name":"DEMO buffer-maker","specification":"spec","specificationType":"ui","items":[{"name":"item1","type":"byte","length":1,"dataType":"num","data":"1"},{"name":"item2","type":"int8","length":1,"dataType":"num","data":"-2"},{"name":"item3","type":"uint8","length":1,"dataType":"num","data":"3"},{"name":"item4","type":"int16le","length":1,"dataType":"num","data":"-4"},{"name":"item5","type":"uint16le","length":1,"dataType":"num","data":"5"},{"name":"item6","type":"uint16le","length":1,"dataType":"num","data":"6"}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str"}]
helper.load(bufferMaker, flow, () => {
try {
const n = helper.getNode('testNode');
n.should.have.property('name', 'DEMO buffer-maker');
done();
} catch (error) {
done(error);
}
});
});
it('should generate output messages when data is received', done => {
//this flow is a buffer maker configured with every possible conversion & some fixed values. Expect output to be a buffer of [1,254,3,252,255,5,0,6,0]
const resultProp = "my.custom.payload";
const testNodeName = "buffer-maker-node-name";
const flow = getTestFlow(testNodeName,resultProp);
helper.load(bufferMaker, flow, function() {
try {
const helperNode = helper.getNode("helperNode");
const testNode = helper.getNode("testNode");
should(helperNode).not.be.null();
should(testNode).not.be.null();
testNode.should.have.property('name', testNodeName);
helperNode.on("input", function (msg) {
try {
msg.should.have.propertyByPath(...resultProp.split("."));
/** @type {should.Assertion} */ var rp = msg.should.have.propertyByPath(...resultProp.split(".")).obj;//get the nested property
rp.should.be.an.Object()
rp.should.instanceOf(Buffer);
var expectedPayload = Buffer.from([1,254,3,252,255,255,251,6,0,7,0,248,255,255,255,255,255,255,247,10,0,0,0,0,0,0,11,0,80,113,15,228,255,255,255,255,255,255,225,187,101,108,0,0,12,119,66,3,0,0,0,0,0,0,3,126,17,214,0,253,74,129,65,65,137,95,173,159,29,121,247,81,246,116,60,60,65,179,143,43,255,224,32,101,101,101,101,101,35,35,36,36,50,53,38,38,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55,50,55]).toString("hex");
rp.toString("hex").should.equal(expectedPayload);
done();
} catch (error) {
done(error);
}
});
testNode.receive({ i1: 1111, i2: 2222 }); //fire input of testNode
} catch (error) {
done(error);
}
});
});
});

View File

@@ -0,0 +1,144 @@
var should = require("should");
var helper = require("node-red-node-test-helper");
var bufferParser = require("../buffer-parser.js");
const getTestFlow = (nodeName, resultPayloadPropName) => {
resultPayloadPropName = resultPayloadPropName ? resultPayloadPropName : "payload";
return [
{ id: 'helperNode1', type: 'helper' },
{ id: 'helperNode2', type: 'helper' },
{ id: 'helperNode3', type: 'helper' },
{ id: 'helperNode4', type: 'helper' },
{ id: 'helperNode5', type: 'helper' },
{"id":"testNode","type":"buffer-parser","name":nodeName,"msgProperty":resultPayloadPropName,"data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"int16be","name":"item1","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"int32be","name":"item2","offset":2,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"bigint64be","name":"item3","offset":6,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"hex","name":"item4","offset":14,"length":10,"offsetbit":0,"scale":"1","mask":""},{"type":"string","name":"item5","offset":24,"length":10,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgPropertyType":"str","resultType":"value","resultTypeType":"output","multipleResult":true,"fanOutMultipleResult":true,"setTopic":true,"outputs":5,"wires":[["helperNode1"],["helperNode2"],["helperNode3"],["helperNode4"],["helperNode5"]]}
];
};
helper.init(require.resolve('node-red'));
describe('buffer-parser Node', function(){
"use strict";
beforeEach(done => { helper.startServer(done); });
afterEach(done => { helper.unload().then(() => helper.stopServer(done)); });
it('should be loaded', done => {
// const flow = [{ id: 'testNode', type: 'buffer-parser', name: 'test--buffer-parser' }];
const flow = [{"id":"testNode","type":"buffer-parser","name":"test--buffer-parser","specification":"spec","specificationType":"ui","items":[{"name":"item1","type":"byte","length":1,"dataType":"num","data":"1"},{"name":"item2","type":"int8","length":1,"dataType":"num","data":"-2"},{"name":"item3","type":"uint8","length":1,"dataType":"num","data":"3"},{"name":"item4","type":"int16le","length":1,"dataType":"num","data":"-4"},{"name":"item5","type":"uint16le","length":1,"dataType":"num","data":"5"},{"name":"item6","type":"uint16le","length":1,"dataType":"num","data":"6"}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str"}]
helper.load(bufferParser, flow, () => {
try {
const testNode = helper.getNode('testNode');
testNode.should.have.property('name', 'test--buffer-parser');
done();
} catch (error) {
done(error);
}
});
});
it('should make BigInt values with and without mask', done => {
const flow = [{ id: 'helperNode1', type: 'helper' }, {"id":"testNode","type":"buffer-parser","name":"test--buffer-parser","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"bigint64be","name":"MASK_00000001FFFFFFFF","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":"0x00000001FFFFFFFF"},{"type":"bigint64be","name":"MASK_000001FFFFFFFFFF","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":"0x000001FFFFFFFFFF"},{"type":"bigint64be","name":"MASK_0001FFFFFFFFFFFF","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":"0x0001FFFFFFFFFFFF"},{"type":"bigint64be","name":"MASK_000FFFFFFFFFFFFF","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":"0x000FFFFFFFFFFFFF"},{"type":"bigint64be","name":"NO_MASK","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"wires":[["helperNode1"]]}]
helper.load(bufferParser, flow, () => {
try {
const testNode = helper.getNode('testNode');
const helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function (msg) {
try {
msg.should.have.property("payload");
msg.payload.should.have.property("MASK_00000001FFFFFFFF");
msg.payload.should.have.property("MASK_000001FFFFFFFFFF");
msg.payload.should.have.property("MASK_0001FFFFFFFFFFFF");
msg.payload.should.have.property("MASK_000FFFFFFFFFFFFF");
msg.payload.should.have.property("NO_MASK");
msg.payload.MASK_00000001FFFFFFFF.should.eql(8589934591n);
msg.payload.MASK_000001FFFFFFFFFF.should.eql(2199023255551n);
msg.payload.MASK_0001FFFFFFFFFFFF.should.eql(562949953421311n);
msg.payload.MASK_000FFFFFFFFFFFFF.should.eql(4503599627370495n);
msg.payload.NO_MASK.should.eql(4503599627370495n);
done();
} catch(err) {
done(err);
}
});
testNode.should.have.property('name', 'test--buffer-parser');
testNode.receive({ payload: Buffer.from([0,15,255,255,255,255,255,255]) }); //fire input of testNode with a buffer of 0x000FFFFFFFFFFFFF (4503599627370495)
} catch (error) {
done(error);
}
});
});
it('should generate 5 values (fan out test)', done => {
const resultProp = "my.custom.payload";
const testNodeName = "buffer-parser-node-name";
const flow = getTestFlow(testNodeName,resultProp);
this.timeout(2000); //timeout with an error if done() isn't called within one second
helper.load(bufferParser, flow, function() {
try {
var helperNode1 = helper.getNode("helperNode1");
var helperNode2 = helper.getNode("helperNode2");
var helperNode3 = helper.getNode("helperNode3");
var helperNode4 = helper.getNode("helperNode4");
var helperNode5 = helper.getNode("helperNode5");
var testNode = helper.getNode("testNode");
should(helperNode1).not.be.null();
should(helperNode2).not.be.null();
should(helperNode3).not.be.null();
should(helperNode4).not.be.null();
should(helperNode5).not.be.null();
should(testNode).not.be.null();
testNode.should.have.property('name', testNodeName);
var results = {};
setTimeout(function () {
var doTest = function(msg, expectedType, expectedValue) {
var path = resultProp.split(".");
msg.should.have.propertyByPath(...path);
/** @type {should.Assertion} */ var rp = msg.should.have.propertyByPath(...path).obj;//get the nested property
should(rp).be.of.type(expectedType);
should(rp).eql(expectedValue);
}
try {
results.should.have.properties(["resultMsg1","resultMsg2","resultMsg3","resultMsg4","resultMsg5"])
doTest(results.resultMsg1, "number", 24930);
doTest(results.resultMsg2, "number", 1667523942);
doTest(results.resultMsg3, "bigint", 7451321489274203502n);
doTest(results.resultMsg4, "string", "6f707172737475767778");
doTest(results.resultMsg5, "string", "yzABCDEFGH");
done();
return;
} catch (error) {
done(error);
}
}, 1000);
helperNode1.on("input", function (msg) { results.resultMsg1 = msg; });
helperNode2.on("input", function (msg) { results.resultMsg2 = msg; });
helperNode3.on("input", function (msg) { results.resultMsg3 = msg; });
helperNode4.on("input", function (msg) { results.resultMsg4 = msg; });
helperNode5.on("input", function (msg) { results.resultMsg5 = msg; });
testNode.receive({ payload: Buffer.from([97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90]) }); //fire input of testNode
} catch (error) {
done(error);
}
});
});
//TODO: Test the following...
/*
* all functions
* all output types
* byte swaps
* scaling operators
* dynamic spec
*/
});

View File

@@ -0,0 +1,108 @@
var should = require("should");
const commonFunctions = require('.././common-functions.js');
describe('common-functions.js', function(){
var stop = false;
it('should be loaded', done => {
function shouldHaveFunction(obj, fName) {
obj.should.have.property(fName).which.is.a.Function();
}
try {
commonFunctions.should.be.type("object");
commonFunctions.should.have.property("SWAPOPTS").which.is.instanceOf(Array);
commonFunctions.should.have.property("TYPEOPTS").which.is.instanceOf(Array);
shouldHaveFunction(commonFunctions, "bcd2number");//additional tests TO DO
shouldHaveFunction(commonFunctions, "number2bcd");//additional tests TO DO
shouldHaveFunction(commonFunctions, "byteToBits");
shouldHaveFunction(commonFunctions, "wordToBits");
shouldHaveFunction(commonFunctions, "bitsToByte");
shouldHaveFunction(commonFunctions, "bitsToWord");
shouldHaveFunction(commonFunctions, "getBit");//additional tests TO DO
shouldHaveFunction(commonFunctions, "setBit");//additional tests TO DO
shouldHaveFunction(commonFunctions, "clearBit");//additional tests TO DO
shouldHaveFunction(commonFunctions, "updateBit");//additional tests TO DO
shouldHaveFunction(commonFunctions, "isNumber");
shouldHaveFunction(commonFunctions, "setObjectProperty");
shouldHaveFunction(commonFunctions, "getObjectProperty");
var parent = {};
var grandchildNamePath = "child.child.name";
var grandchildName = "dum dum";
describe('#setObjectProperty()', () => {
it('should set object property by path', done => {
try {
commonFunctions.setObjectProperty(parent, "child.child.name", grandchildName);
parent.should.have.propertyByPath(...grandchildNamePath.split(".")).which.eqls(grandchildName);
done();
} catch (error) {
done(error);
}
})
})
describe('#getObjectProperty()', () => {
it('should get object property by path', done => {
try {
var name = commonFunctions.getObjectProperty(parent, grandchildNamePath) || "";
name.should.eql(grandchildName);
done();
} catch (error) {
done(error);
}
})
})
describe('#isNumber()', () => {
it('should test numbers', done => {
try {
commonFunctions.isNumber("123").should.eql(true);
commonFunctions.isNumber("0x123").should.eql(true);
commonFunctions.isNumber("0b1001").should.eql(true);
commonFunctions.isNumber("0o1234567").should.eql(true);
commonFunctions.isNumber("0o12345678").should.eql(false);
commonFunctions.isNumber("efg").should.eql(false);
commonFunctions.isNumber(null).should.eql(false);
done();
} catch (error) {
done(error);
}
})
})
describe('#byteToBits() #bitsToByte()', () => {
it('should convert byte to bits and back to byte', done => {
try {
var x96 = commonFunctions.byteToBits(0x96);
x96.bits.should.eql([0, 1, 1, 0, 1, 0, 0, 1]); //bit 0 ~ 7
commonFunctions.bitsToByte(x96.bits).should.eql(0x96);
done();
} catch (error) {
done(error);
}
})
})
describe('#wordToBits() #bitsToWord()', () => {
it('should convert word to bits and back to word', done => {
try {
var xf708 = commonFunctions.wordToBits(0xf708); //
xf708.bits.should.eql([0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1]); //bit 0 ~ 15
commonFunctions.bitsToWord(xf708.bits).should.eql(0xf708);
done();
} catch (error) {
done(error);
}
})
})
done(); //success :)
} catch (error) {
stop = true;
done(error);
}
});
});