├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json ├── readers ├── bool.js ├── char.js ├── float.js ├── int.js └── uint.js └── test └── parser.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Test results 31 | test-results.xml 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '6' 6 | - '4' 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sigfox-parser 2 | 3 | [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | [![Build Status](https://travis-ci.org/waylayio/sigfox-parser.svg?branch=master)](https://travis-ci.org/waylayio/sigfox-parser) 5 | [![Coverage Status](https://coveralls.io/repos/github/waylayio/sigfox-parser/badge.svg?branch=master)](https://coveralls.io/github/waylayio/sigfox-parser?branch=master) 6 | 7 | Sigfox data format parser 8 | 9 | ## Usage 10 | 11 | ### Parse a message 12 | 13 | ```javascript 14 | require('sigfox-parser')(, ); 15 | ``` 16 | 17 | **messagebytes**: The bytes of the message encoded in a hexadecimal String. 18 | 19 | **formatstring**: A formatstring according to the Sigfox documentation. 20 | 21 | Example 22 | 23 | ```javascript 24 | var parsed = require('sigfox-parser')('C01234','b1::bool:7 b2::bool:6 i1:1:uint:16'); 25 | ``` 26 | 27 | The variable parsed now contains: 28 | 29 | ```javascript 30 | { 31 | b1: true, 32 | b2: true, 33 | i1: 0x1234 34 | } 35 | ``` 36 | 37 | ### Parse the syntax 38 | 39 | ```javascript 40 | var parser = require('sigfox-parser') 41 | 42 | var fields = parser.parseFields('lightAmbi::uint:16 temperature:2:int:8') 43 | 44 | console.log(fields) 45 | /** 46 | [ { name: 'lightAmbi', 47 | offset: '', 48 | type: 'uint', 49 | length: '16', 50 | endianness: 'big-endian' }, 51 | { name: 'temperature', 52 | offset: '2', 53 | type: 'int', 54 | length: '8', 55 | endianness: 'big-endian' } ] 56 | **/ 57 | ``` 58 | 59 | ## Formatstring 60 | A formatstring consists of multiple fields. A field is defined by its name, its position in the message bytes, its length and its type : 61 | 62 | * the field name is an identifier including letters, digits and the '-' and '_' characters. 63 | * the byte index is the offset in the message buffer where the field is to be read from, starting at zero. If omitted, the position used is the current byte for boolean fields and the next byte for all other types. For the first field, an omitted position means zero (start of the message buffer) 64 | * Next comes the type name and parameters, which varies depending on the type : 65 | * boolean : parameter is the bit position in the target byte 66 | * char : parameter is the number of bytes to gather in a string 67 | * float : parameters are the length in bits of the value, which can be either 32 or 64 bits, and optionally the endianness for multi-bytes floats. Default is big endian. Decoding is done according to the IEEE 754 standard. 68 | * uint (unsigned integer) : parameters are the number of bits to include in the value, and optionally the endianness for multi-bytes integers. Default is big endian. 69 | * int (signed integer) : parameters are the number of bits to include in the value, and optionally the endianness for multi-bytes integers. Default is big endian. 70 | 71 | _(cfr. Sigfox documentation)_ 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const curry = require('lodash.curry') 4 | 5 | /** 6 | * Parses a message accoring to the Sigfox Payload decoding grammer. 7 | * A field is defined by its name, its position in the message bytes, its length and its type : 8 | * the field name is an identifier including letters, digits and the '-' and '_' characters. 9 | * the byte index is the offset in the message buffer where the field is to be read from, starting at zero. If omitted, the position used is the current byte for boolean fields and the next byte for all other types. For the first field, an omitted position means zero (start of the message buffer) 10 | * Next comes the type name and parameters, which varies depending on the type : 11 | * boolean : parameter is the bit position in the target byte 12 | * char : parameter is the number of bytes to gather in a string 13 | * float : parameters are the length in bits of the value, which can be either 32 or 64 bits, and optionally the endianness for multi-bytes floats. Default is big endian. Decoding is done according to the IEEE 754 standard. 14 | * uint (unsigned integer) : parameters are the number of bits to include in the value, and optionally the endianness for multi-bytes integers. Default is big endian. 15 | * int (signed integer) : parameters are the number of bits to include in the value, and optionally the endianness for multi-bytes integers. Default is big endian. 16 | */ 17 | function parseMessage (data, format) { 18 | const buffer = Buffer.isBuffer(data) 19 | ? data 20 | : Buffer.from(data, 'hex') 21 | 22 | const types = { 23 | 'uint': curry(require('./readers/uint'))(buffer), 24 | 'int': curry(require('./readers/int'))(buffer), 25 | 'float': curry(require('./readers/float'))(buffer), 26 | 'bool': curry(require('./readers/bool'))(buffer), 27 | 'char': curry(require('./readers/char'))(buffer) 28 | } 29 | let current = 0 30 | let last = 0 31 | 32 | const fields = parseFields(format) 33 | return fields.reduce((obj, field) => { 34 | let l = current 35 | current += last 36 | if (field.type !== 'bool') { 37 | l = current 38 | } 39 | 40 | try { 41 | obj[field.name] = types[field.type](field.offset || l, field.length, field.endianness) 42 | } catch (e) { 43 | // most off time parser fields too long for datas buffer 44 | return obj 45 | } 46 | 47 | last = field.length / (field.type === 'char' ? 1 : 8) 48 | return obj 49 | }, {}) 50 | } 51 | 52 | function parseFields (format) { 53 | const fields = format.trim().replace(/\s+/g, ' ').split(' ') 54 | return fields.map(field => { 55 | const split = field.split(':') 56 | return { 57 | name: split[0], 58 | offset: parseInt(split[1]), 59 | type: split[2], 60 | length: parseInt(split[3]), 61 | endianness: typeof split[4] === 'undefined' ? 'big-endian' : split[4] 62 | } 63 | }) 64 | } 65 | 66 | module.exports = parseMessage 67 | module.exports.parseFields = parseFields 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigfox-parser", 3 | "version": "1.2.4", 4 | "description": "A parser for Sigfox's custom grammar format", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard .", 8 | "test": "npm run lint && tap test/*.test.js", 9 | "cover": "tap test/*.test.js --coverage" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/waylayio/sigfox-parser.git" 14 | }, 15 | "author": "Waylay ", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/waylayio/sigfox-parser/issues" 19 | }, 20 | "homepage": "https://github.com/waylayio/sigfox-parser#readme", 21 | "devDependencies": { 22 | "standard": "^8.0.0", 23 | "tap": "^6.3.0" 24 | }, 25 | "dependencies": { 26 | "lodash.curry": "^4.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readers/bool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * returns a boolean according to the n-th bit of the byte 5 | * <<: left bit shift 6 | * 7 | * eg.: 8 | * input 11100101 9 | * we want the boolean value for the 8th bit (beginning from the right) 10 | * inputmask: 1 << 7 11 | * 00000001 12 | * << 7 13 | * 10000000 14 | * 15 | * After this we apply the inputmask to the input 16 | * 11100101 17 | * & 10000000 18 | * ---------- 19 | * 10000000 -> true 20 | * !! convert to bool 21 | */ 22 | module.exports = function readBool (buffer, offset, position) { 23 | return !!(buffer.readInt8(offset) & (1 << position)) 24 | } 25 | -------------------------------------------------------------------------------- /readers/char.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function readChar (buffer, offset, length) { 4 | return buffer.slice(offset, offset + length).toString('utf-8') 5 | } 6 | -------------------------------------------------------------------------------- /readers/float.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * float : parameters are the length in bits of the value, 5 | * which can be either 32 or 64 bits, and optionally the endianness for 6 | * multi-bytes floats 7 | * 8 | * Default is big endian 9 | * Decoding is done according to the IEEE 754 standard 10 | */ 11 | module.exports = function readFloat (buffer, offset, length, endian) { 12 | // big-endian (default) 13 | if (endian === 'big-endian') { 14 | return length === 32 15 | ? buffer.readFloatBE(offset) 16 | : buffer.readDoubleBE(offset) 17 | } 18 | 19 | // little-endian 20 | return (length === 64) 21 | ? buffer.readDoubleLE(offset) 22 | : buffer.readFloatLE(offset) 23 | } 24 | -------------------------------------------------------------------------------- /readers/int.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * int (signed integer) : parameters are the number of bits to include 5 | * in the value, and optionally the endianness for multi-bytes integers 6 | * Default is big endian 7 | */ 8 | module.exports = function readInt (buffer, offset, length, endian) { 9 | return endian === 'big-endian' 10 | ? buffer.readIntBE(offset, length / 8) 11 | : buffer.readIntLE(offset, length / 8) 12 | } 13 | -------------------------------------------------------------------------------- /readers/uint.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * uint (unsigned integer) : parameters are the number of bits to 5 | * include in the value, and optionally the endianness for multi-bytes integers 6 | * Default is big endian 7 | */ 8 | module.exports = function readUInt (buffer, offset, length, endian) { 9 | return endian === 'big-endian' 10 | ? buffer.readUIntBE(offset, length / 8) 11 | : buffer.readUIntLE(offset, length / 8) 12 | } 13 | -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tap = require('tap') 4 | var parse = require('./../index') 5 | 6 | var cases = [ 7 | { 8 | data: '1234', 9 | format: 'int1::uint:8 int2::uint:8', 10 | expected: { int1: 0x12, int2: 0x34 } 11 | }, 12 | { 13 | data: 'C01234', 14 | format: 'b1::bool:7 b2::bool:6 i1:1:uint:16', 15 | expected: { b1: true, b2: true, i1: 0x1234 } 16 | }, 17 | { 18 | data: '801234', 19 | format: 'b1::bool:7 b2::bool:6 i1:1:uint:16:little-endian', 20 | expected: { b1: true, b2: false, i1: 0x3412 } 21 | }, 22 | { 23 | data: '80123456', 24 | format: 'b1::bool:7 b2::bool:6 i1:1:uint:16:little-endian i2::uint:8', 25 | expected: { b1: true, b2: false, i1: 0x3412, i2: 0x56 } 26 | }, 27 | { 28 | data: '41424344454601234567890A', 29 | format: 'str::char:6 i1::uint:16 i2::uint:32', 30 | expected: { str: 'ABCDEF', i1: 0x123, i2: 0x4567890A } 31 | }, 32 | { 33 | data: '171318', 34 | format: 'lightAmbi::uint:16 temperature:2:int:8', 35 | expected: { lightAmbi: 5907, temperature: 24 } 36 | }, 37 | // with additional whitespace 38 | { 39 | data: '171318', 40 | format: ' lightAmbi::uint:16 temperature:2:int:8 ', 41 | expected: { lightAmbi: 5907, temperature: 24 } 42 | }, 43 | { 44 | data: '1E660382BA583FF426D609', 45 | format: 'Battery::uint:8 pH::uint:16:little-endian Conductivity::float:32:little-endian DO::uint:16:little-endian Temp::uint:16:little-endian', 46 | expected: { Battery: 30, pH: 870, Conductivity: 0.8465958833694458, DO: 9972, Temp: 2518 } 47 | } 48 | ] 49 | 50 | cases.forEach(function (c) { 51 | tap.test('test format and expected values', function (t) { 52 | t.plan(1) 53 | 54 | var result = parse(c.data, c.format) 55 | t.deepEqual(result, c.expected) 56 | }) 57 | }) 58 | 59 | tap.test('test uinttypes', function (t) { 60 | t.plan(1) 61 | 62 | var expected = { 63 | int1: 100, 64 | int2: 200, 65 | int3: 42, 66 | int4: 0, 67 | int5: 8558, 68 | int6: 0x0123456789A, 69 | int7: 0x0123456789A 70 | } 71 | var buffer = Buffer.alloc(32) 72 | 73 | buffer.writeUInt8(100, 0) 74 | buffer.writeUInt16BE(200, 1) 75 | buffer.writeUInt16LE(42, 3) 76 | buffer.writeUInt32BE(0, 5) 77 | buffer.writeUInt32LE(8558, 9) 78 | buffer.writeUIntBE(0x0123456789A, 13, 6) 79 | buffer.writeUIntLE(0x0123456789A, 19, 6) 80 | 81 | var data = buffer.toString('hex') 82 | var result = parse(data, 'int1::uint:8 int2:1:uint:16 int3:3:uint:16:little-endian int4:5:uint:32 int5:9:uint:32:little-endian int6:13:uint:48 int7:19:uint:48:little-endian') 83 | t.deepEqual(result, expected) 84 | }) 85 | 86 | tap.test('test inttypes', function (t) { 87 | t.plan(1) 88 | 89 | var expected = { 90 | int1: -100, 91 | int2: -200, 92 | int3: -42, 93 | int4: 0, 94 | int5: -8558, 95 | int6: -0x0123456789B, 96 | int7: -0x0123456789A 97 | } 98 | var buffer = Buffer.alloc(32) 99 | 100 | buffer.writeInt8(-100, 0) 101 | buffer.writeInt16BE(-200, 1) 102 | buffer.writeInt16LE(-42, 3) 103 | buffer.writeInt32BE(-0, 5) 104 | buffer.writeInt32LE(-8558, 9) 105 | buffer.writeIntBE(-0x0123456789B, 13, 6) 106 | buffer.writeIntLE(-0x0123456789A, 19, 6) 107 | 108 | var data = buffer.toString('hex') 109 | var result = parse(data, 'int1::int:8 int2:1:int:16 int3:3:int:16:little-endian int4:5:int:32 int5:9:int:32:little-endian int6:13:int:48 int7:19:int:48:little-endian') 110 | t.deepEqual(result, expected) 111 | }) 112 | 113 | tap.test('test chars', function (t) { 114 | t.plan(1) 115 | 116 | var expected = { message: 'Hello world!' } 117 | var buffer = Buffer.alloc(32) 118 | 119 | buffer.write('Hello world!', 0) 120 | 121 | var data = buffer.toString('hex') 122 | var result = parse(data, 'message::char:12') 123 | t.deepEqual(result, expected) 124 | }) 125 | 126 | tap.test('test bools', function (t) { 127 | t.plan(1) 128 | 129 | var expected = { 130 | b1: true, 131 | b2: false, 132 | b3: true 133 | } 134 | var buffer = Buffer.alloc(1) 135 | 136 | buffer.writeUInt8(0b10110001, 0) 137 | 138 | var data = buffer.toString('hex') 139 | var result = parse(data, 'b1:0:bool:7 b2:0:bool:6 b3:0:bool:5') 140 | t.deepEqual(result, expected) 141 | }) 142 | 143 | tap.test('Give buffer object to parse', t => { 144 | t.plan(1) 145 | 146 | var toTest = cases[0] 147 | var data = Buffer.isBuffer(toTest.data) 148 | ? data 149 | : Buffer.from(toTest.data, 'hex') 150 | var format = toTest.format 151 | 152 | var parsed = parse(data, format) 153 | t.deepEqual(parsed, toTest.expected) 154 | }) 155 | --------------------------------------------------------------------------------