Device Type Decoders
Overview
To help manage devices of the same type e.g Semtech LoRaMote SX1272 RuBAN allows you to create Device Types. A Device Type allows you to define
decoders, used to read metrics from a sensors. Therefore Device Types can be shared across all device of a certain type.
In this document we will take you through setting up a Device Type Decoder. In this example we will use the Semtech LoRaMote SX1272.
Configuration
In RuBAN under Admin there is a Payload Decoder section. This page lists all of the Device Types in the system. A Device Type consists of a decoder, and multiple encoders (available soon) and allows you to send and receive data.
The device type should be named after the manufacture device name to make it easy for other users to find the correct type e.g Semtech LoRaMote SX1272
Decoders
There are two types of decoder Dsl and Raw. We will first walk through the steps to create a Dsl decoder for the Semtech LoRaMote SX1272 as an example.
Dsl Decoder
When writing a Dsl decoder you use a json schema to describe the payload. The schema is split into two sections preprocessing and processing.
- Preprocessing, is used to prepare the payload e.g convert the string to hex.
- Processing, allows you define the bytes of the payload and how to interpret them.
The decoder we will build in this section will convert this payload 0026fd0a6001c0b54be236fb6ebe005b from a Semtech LoRaMote SX1272 to the following,
- pressure, 998.1
- temperature, 26.56
- batteryLevel, 71.25984251968504
- latitude, 53.35568825670341
- longitude, -6.422924995422363
Step 1. The datasheet
The first point of reference is the payload description in the manufactures datasheet for the mote you want to use.
In this example were using the Semtech LoRaMote SX1272 and here is the section on the payload.
The key information we need from this datasheet is,
- Ignore the first byte it just a header
- Bytes 1 and 2 are the pressure value stored in big-endian. Value should be divided by 10 to get hPa
- Bytes 3 and 4 are the temperature value stored in big-endian. Value should be divided by 100 to get temperature in decimals.
- Ignore bytes 5 and 6
- Byte 7 is the battery, it has a range 1 being the minimum and 254 maximum. Also a value of 0x00 means its powered externally and 0xFF means it cannot be read.
- Bytes 8,9 and 10 are the latitude, signed 24 bit word where - corresponds to 90° south (the South Pole) and - 1 corresponds to 90° north (the North Pole)
- Bytes 11,12 and 13 are the longitude, signed 24 bit word where - corresponds to 180° west and - 1 corresponds to 180° east. The Greenwich meridian corresponds to 0
Step 2. The Dsl json schema
We now need to convert the bullet points of information in to the Dsl json schema using json attributes and Dsl Functions.
Dsl json Schema Attributes
Dsl json attribute | Description |
---|---|
size | The number of bytes this second will operate on |
name | The RuBAN metric name that will be used to store the value |
type | This is the value type. This can be metric, latitude or longitude |
call | Call a dsl function |
execute | Use instead of call to execute javascript instead of a function. |
format | Execute this javascript to convert the raw value to the correct value |
validation | This value should only be used if this validation passes |
comment | Text to describe the the action of the current block |
isPressent | For variable size payload bytes may not be present. |
Dsl Functions
The call attribute is used to call a Dsl function. Dsl functions can be managed form the Dsl Functions section under RuBAN Admin page.
/* Read 2 bytes big-endian */ function readInt16BE(context) { context.value = context.currentBytes.readInt16BE(0); }
A function takes a parameter which is the context. This has the following properties,
- payload, the full preprocessed payload
- currentBytes, this determined by the offset of the byte definition in bytes and the size attribute.
Note: A function does not return a value it stores the resulting value in the context, see last line context.value = latitude.
Once a function has been created and saved it can be used in a dsl decoder. Functions can be used to share common code across all decoders.
The next diagram shows how we associate the payload body with our Dsl json schema using the attributes and Dsl functions we describe above.
Finally this is the Dsl decoder.
{ "preprocessing": { "call": "payloadToHex" }, "processing": { "bytes": [ { "size": 1, "comment": "Ignore first byte" }, { "size": 2, "name": "pressure", "type": "metric", "call": "readInt16BE", "format": "context.value = context.value/10", "comment": "Read 1 byte for pressure" }, { "size": 2, "name": "temperature", "type": "metric", "call": "readInt16BE", "format": "context.value = context.value/100", "comment": "Read 1 byte for temperature" }, { "size": 2, "comment": "Ignore bytes" }, { "size": 1, "name": "batteryLevel", "type": "metric", "call": "readUInt8", "format": "context.value = (context.value/254) * 100", "validation": "context.value != '0xFF' && context.value !== 0", "comment": "Read 1 byte for battery level" }, { "size": 3, "type": "latitude", "call": "decode24BitGpsLatitude", "comment": "Read a 3 byte gps latitude" }, { "size": 3, "type": "longitude", "call": "decode24BitGpsLongitude", "comment": "Read a 3 byte gps longitude" } ] } }
Raw Decoder
If a payload is very complex and cannot be expressed with the dsl schema you can use a raw decoder mode.
var payload = new Buffer(context.payload, "hex"); var metrics = []; metrics.push({name: "pressure", value: payload.readInt16BE(1)/10}); metrics.push({name: "temperature", value: payload.readInt16BE(3)/100}); var batteryStatus = payload.readUInt8(7); if(batteryStatus != 0xFF && batteryStatus !== 0) { metrics.push({name: "batteryLevel", value: (batteryStatus/254) * 100}); } var latitude = (payload.readUInt16BE(8) << 8) | payload.readUInt8(10); if( latitude & 0x800000 ) { latitude = -(latitude/8388608) * 90; } else { latitude = (latitude/8388607) * 90; } var longitude = (payload.readUInt16BE(11) << 8) | payload.readUInt8(13); if( longitude & 0x800000 ) { longitude = ((longitude/8388608) * 180) - 360; } else { longitude = (longitude/8388607) * 180; } context.value = { metrics: metrics, gps: { latitude: latitude, longitude: longitude } };
This is the raw decoder version of the Dsl decoder above. Note the context is a variable thats available and has the original raw payload.
The result of this code should be stored in the context.value (silimar to Dsl Functions). The structure of the results also should be an object with the following structure.
{ metrics: [ { namme: "pressure", value: 998.1 } ], gps: { latitude: 53.35568825670341, longitude: -6.422924995422363 } }
Testing a Decoder
You can test a decoder at any time by specifying a payload in the "Select Payload" tab and clicking on the Run button on the "Confirm Result" tab.
The result and/or errors will be displayed on the page.
Appendix.
- DSL Functions for sample decoder.
/* This is decode24BitGpsLatitude */ function decode24BitGpsLatitude(context) { var latitude = (context.currentBytes.readUInt16BE(0) << 8) | context.currentBytes.readUInt8(2); if( latitude & 0x800000 ) { latitude = -(latitude/8388608) * 90; } else { latitude = (latitude/8388607) * 90; } context.value = latitude; }
/* This decode24BitGpsLonditude */ function decode24BitGpsLongitude(context) { var longitude = (context.currentBytes.readUInt16BE(0) << 8) | context.currentBytes.readUInt8(2); if( longitude & 0x800000 ) { longitude = ((longitude/8388608) * 180) - 360; } else { longitude = (longitude/8388607) * 180; } context.value = longitude; }
/* Read unsigned byte */ function readUInt8(context) { context.value = context.currentBytes.readUInt8(0); }
/* Convert the payload to HEX */ function payloadToHex(context) { context.payload = new Buffer(context.payload, 'hex'); }
/* Read 2 bytes big endian */ function readInt16BE(context) { context.value = context.currentBytes.readInt16BE(0); }