Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Adeunis FTD decoder to the catalog #53

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions manufacturers/Adeunis/models/FTD-ARF8123AA/config_LoRaWAN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"uplinkDecoder": "uplink_decoder.js",
"documentation": "https://www.uco.es/atdfiware/images/doc/comunica/lora/User_Guide_FTD_LoRaWAN_EU863-870_V123.pdf",
"ngsildContext": "https://easy-global-market.github.io/ngsild-api-data-models/device/jsonld-contexts/device-compound.jsonld",
"devicePicture": "ARF8123AA.png",
"chirpstack": {
"region": "EU868",
"macVersion": 0,
"regParamsRevision": "A",
"adrAlgorithmId": "default",
"flushQueueOnActivate": true,
"uplinkInterval": 86400,
"supportsOTAA": true,
"supportsClassB": false,
"supportsClassC": false
},
"liveObjects": {
"profile": "Generic_classA_RX2SF12",
"activationType": "OTAA",
"connectivityPlan": "orange-cs/CP_Basic"
}
}
214 changes: 214 additions & 0 deletions manufacturers/Adeunis/models/FTD-ARF8123AA/uplink_decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
function Decode(fPort, bytes, variables) {
// Functions
function parseCoordinate(raw_value, coordinate) {
var raw_itude = raw_value;
var temp = "";
var itude_string = ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
itude_string += ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
coordinate.degrees += itude_string;
itude_string += "°";
temp = ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
temp += ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
itude_string += temp;
itude_string += ".";
coordinate.minutes += temp;
temp = ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
temp += ((raw_itude >> 28) & 0xF).toString();
raw_itude <<= 4;
itude_string += temp;
coordinate.minutes += ".";
coordinate.minutes += temp;
return itude_string;
}

function parseLatitude(raw_latitude, coordinate) {
var latitude = parseCoordinate(raw_latitude, coordinate);
latitude += ((raw_latitude & 0xF0) >> 4).toString();
coordinate.minutes += ((raw_latitude & 0xF0) >> 4).toString();
return latitude;
}

function parseLongitude(raw_longitude, coordinate) {
var longitude = (((raw_longitude >> 28) & 0xF)).toString();
coordinate.degrees = longitude;
longitude += parseCoordinate(raw_longitude << 4, coordinate);
return longitude;
}

function addField(field_no, payload) {
switch (field_no) {
case 0:
payload.temperature = bytes[bytes_pos_] & 0x7F;
if ((bytes[bytes_pos_] & 0x80) > 0) {
payload.temperature -= 128;
}
bytes_pos_++;
break;
case 1:
payload.trigger = "accelerometer";
break;
case 2:
payload.trigger = "pushbutton";
break;
case 3:
var coordinate = {};
coordinate.degrees = "";
coordinate.minutes = "";
var raw_value = (bytes[bytes_pos_++] << 24) | (bytes[bytes_pos_++] << 16) | (bytes[bytes_pos_++] << 8) | bytes[bytes_pos_++];
payload.lati_hemisphere = (raw_value & 1) == 1 ? "South" : "North";
payload.latitude_dmm = payload.lati_hemisphere.charAt(0) + " ";
payload.latitude_dmm += parseLatitude(raw_value, coordinate);
payload.latitude = (parseFloat(coordinate.degrees) + parseFloat(coordinate.minutes) / 60) * ((raw_value & 1) == 1 ? -1.0 : 1.0);
coordinate.degrees = "";
coordinate.minutes = "";
raw_value = (bytes[bytes_pos_++] << 24) | (bytes[bytes_pos_++] << 16) | (bytes[bytes_pos_++] << 8) | bytes[bytes_pos_++];
payload.long_hemisphere = (raw_value & 1) == 1 ? "West" : "East";
payload.longitude_dmm = payload.long_hemisphere.charAt(0) + " ";
payload.longitude_dmm += parseLongitude(raw_value, coordinate);
payload.longitude = (parseFloat(coordinate.degrees) + parseFloat(coordinate.minutes) / 60) * ((raw_value & 1) == 1 ? -1.0 : 1.0);
raw_value = bytes[bytes_pos_++];
switch ((raw_value & 0xF0) >> 4) {
case 1:
payload.gps_quality = "Good";
break;
case 2:
payload.gps_quality = "Average";
break;
case 3:
payload.gps_quality = "Poor";
break;
default:
payload.gps_quality = (raw_value >> 4) & 0xF;
break;
}
payload.hdop = (raw_value >> 4) & 0xF;
payload.sats = raw_value & 0xF;
break;
case 4:
payload.ul_counter = bytes[bytes_pos_++];
break;
case 5:
payload.dl_counter = bytes[bytes_pos_++];
break;
case 6:
payload.battery_level = (bytes[bytes_pos_] << 8) | bytes[bytes_pos_ + 1];
bytes_pos_ += 2;
break;
case 7:
payload.rssi_dl = bytes[bytes_pos_++];
payload.rssi_dl *= -1;
payload.snr_dl = bytes[bytes_pos_] & 0x7F;
if ((bytes[bytes_pos_] & 0x80) > 0) {
payload.snr_dl -= 128;
}
bytes_pos_++;
break;
default:
break;
}
}

// Declaration & initialization
var status_ = bytes[0];
var bytes_len_ = bytes.length;
var bytes_pos_ = 1;
var i = 0;
var payload = {};
var temp_hex_str = "";
payload.payload = "";
for (var j = 0; j < bytes_len_; j++) {
temp_hex_str = bytes[j].toString(16).toUpperCase();
if (temp_hex_str.length == 1) {
temp_hex_str = "0" + temp_hex_str;
}
payload.payload += temp_hex_str;
}

// Get payload values
do {
if ((status_ & 0x80) > 0) {
addField(i, payload);
}
i++;
} while (((status_ <<= 1) & 0xFF) > 0);

return payload;
}

let parametersMapping = {
temperature: {label:"temperature", unitCode: "CEL", datasetId: 'temperature:Raw'},
trigger: {label:"trigger", unitCode: null, datasetId: 'trigger:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trigger: Needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, corrected.

latitude: {label:"latitude", unitCode: "DEG", datasetId: 'latitude:Raw'},
longitude: {label:"longitude", unitCode: "DEG", datasetId: 'longitude:Raw'},
gps_quality: {label:"gpsQuality", unitCode: null, datasetId: 'gps_quality:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gps_quality: needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, corrected.

hdop: {label:"hdop", unitCode: null, datasetId: 'hdop:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hdop: needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, corrected.

sats: {label:"satellites", unitCode: null, datasetId: 'sats:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sats: Needed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, corrected.

ul_counter: {label:"uplinkCounter", unitCode: null, datasetId: 'ul_counter:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ul_counter : does it provide any identifier that we could relate to a reception ion the gateway side ? If not, I don't really see an interest to colelct this parameter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not send any identifier.
ul_counter can be kept for the idea suggested above.

dl_counter: {label:"downlinkCounter", unitCode: null, datasetId: 'dl_counter:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downlinkCounter: needed ? We don't know about missed DL so not that relevant to know. Interest would be if from ul/dl counters we could infer areas where data transfers have been missed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values can be kept for the suggested idea.

battery_level: {label:"batteryLevel", unitCode: "VLT", datasetId: 'battery_level:Raw'},
rssi_dl: {label:"rssiDownlink", unitCode: "DBM", datasetId: 'rssi_dl:Raw'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use rssi and snr to avoid inventing a new attribute name. It is always DL here so no possble confusion

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use snr and rssi, but adding a datasetId 'Downlink:Raw' for these values, and another datasetId for the other snr/rssi from the gateway "gatewayId:7276ff002e05011c:Raw".
Updated.

snr_dl: {label:"snrDownlink", unitCode: "DB", datasetId: 'snr_dl:Raw'}
};

function ngsildInstance(value, time = null, unitCode = null, datasetSuffix = null) {
const ngsildInstance = {
type: "Property",
value: value,
};
if (time) {
ngsildInstance.observedAt = time;
}
if (unitCode) {
ngsildInstance.unitCode = unitCode;
}
if (datasetSuffix) {
ngsildInstance.datasetId = `urn:ngsi-ld:Dataset:${datasetSuffix}`;
}
return ngsildInstance;
}

function ngsildWrapper(decoded, time, entity_id) {
var ngsild_payload = [{
id: entity_id,
type: "Device"
}];

function addToPayload(key, value) {
if (ngsild_payload.every(d => d.hasOwnProperty(key))) {
ngsild_payload.push({id: entity_id, type: "Device", ...{[key]: value}});
} else {
for (let d of ngsild_payload) {
if (!d.hasOwnProperty(key)) {
d[key] = value;
break;
}
}
}
}

for (var key in decoded) {
if (parametersMapping[key]) {
addToPayload(parametersMapping[key].label, ngsildInstance(decoded[key], time, parametersMapping[key].unitCode, parametersMapping[key].datasetId));
}
}
return ngsild_payload;
}

function main() {
var fPort = process.argv[2];
var payload = Buffer.from(process.argv[3], 'hex');
var time = process.argv[4];
var entity_id = "urn:ngsi-ld:Device:" + process.argv[5];
var decoded = Decode(fPort, payload);
var ngsild_payload = ngsildWrapper(decoded, time, entity_id);
process.stdout.write(JSON.stringify(ngsild_payload));
}

if (require.main === module) {
main();
}