@ncd-io/node-red-enterprise-sensors
Version:
You can install this library through the Palette Manager in Node-Red's UI.
1,258 lines (1,185 loc) • 290 kB
JavaScript
const wireless = require("./index.js");
const comms = require('ncd-red-comm');
const sp = require('serialport');
const Queue = require("promise-queue");
const events = require("events");
const fs = require('fs');
const path = require('path');
const home_dir = require('os').homedir;
const { isDeepStrictEqual } = require('util');
// We're using v8 to handle deep cloning of objects with buffer data.
const v8 = require('v8');
module.exports = function(RED) {
var gateway_pool = {};
function NcdGatewayConfig(config){
RED.nodes.createNode(this,config);
this.port = config.comm_type == 'serial' ? config.port : config.tcp_port;
this.baudRate = parseInt(config.baudRate);
this.listeners = [];
this.sensor_pool = [];
// TODO sensor_list is a temporary property, should be combined with sensor_pool
this.sensor_list = {};
this._emitter = new events.EventEmitter();
this.on = (e,c) => this._emitter.on(e, c);
const config_file_location = home_dir()+'/.node-red/node_modules/@ncd-io/node-red-enterprise-sensors/data/sensor_configs.json';
const configuration_map_location = home_dir()+'/.node-red/node_modules/@ncd-io/node-red-enterprise-sensors/data/configuration_map.json';
const sensor_type_map_location = home_dir()+'/.node-red/node_modules/@ncd-io/node-red-enterprise-sensors/data/sensor_type_map.json';
// comms_timer object var added to clear the time to prevent issues with rapid redeploy
this.comms_timer;
// this.sensor_configs = {};
try {
this.sensor_configs = JSON.parse(fs.readFileSync(config_file_location,));
} catch(err){
this.sensor_configs = {};
};
try {
this.configuration_map = JSON.parse(fs.readFileSync(configuration_map_location,));
} catch(err){
this.configuration_map = {};
console.log('Error reading configuration list');
};
try {
this.sensor_type_map = JSON.parse(fs.readFileSync(sensor_type_map_location,));
} catch(err){
this.sensor_type_map = {};
console.log('Error reading sensor type map file');
};
if(config.comm_type == 'serial'){
this.key = config.port;
}
else{
this.key = config.ip_address+":"+config.tcp_port;
}
this.store_sensor_configs = function(sensor_configs){
fs.writeFile(config_file_location, sensor_configs, function(err){
if(err){
return err;
}else{
return true;
}
});
};
var node = this;
node.is_config = 3;
node.open_comms = function(cb){
if(typeof gateway_pool[this.key] == 'undefined'){
if(config.comm_type == 'serial'){
var comm = new comms.NcdSerial(this.port, this.baudRate);
comm._emitter.on('error', (err) => {
console.log('gateway config serial error', err);
});
}else{
if(!config.ip_address){
return;
}
if(!config.tcp_inactive_timeout){
config.tcp_inactive_timeout = 1200;
}
if(config.tcp_inactive_timeout_active){
var comm = new comms.NcdTCP(config.ip_address, this.port, false, parseInt(config.tcp_inactive_timeout));
}else{
var comm = new comms.NcdTCP(config.ip_address, this.port, false, false);
}
comm._emitter.on('error', (err) => {
console.log('tcp init error', err);
});
}
var modem = new wireless.Modem(comm);
gateway_pool[this.key] = new wireless.Gateway(modem);
gateway_pool[this.key].pan_id = false;
this.gateway = gateway_pool[this.key];
this.gateway.digi.report_rssi = config.rssi;
if(config.comm_type == 'serial'){
if(config.port !== ''){
this.comms_timer = setTimeout(()=>{node.gateway.digi.serial.setupSerial()}, 5000);
}else{
node.warn('No Port Selected for Serial Communications.')
}
}else{
if(config.tcp_port === '' || config.ip_address === ''){
node.warn('TCP Socket not configured for Network Communications. Please enter a Port and IP Address.');
}else{
this.comms_timer = setTimeout(()=>{node.gateway.digi.serial.setupClient()}, 5000);
}
}
node.gateway.digi.serial.on('ready', () => {
node.gateway.digi.send.at_command('SL').then((res) => {
node.gateway.modem_mac = '00:13:A2:00:'+toMac(res.data);
}).catch((err) => {
console.log(err);
node.gateway.digi.serial.reconnect();
}).then(node.check_mode((mode) => {
var pan_id = parseInt(config.pan_id, 16);
// if(!mode && node.gateway.pan_id != pan_id){
if(node.gateway.pan_id != pan_id){
node.gateway.digi.send.at_command("ID", [pan_id >> 8, pan_id & 255]).then((res) => {
node.gateway.pan_id = pan_id;
}).catch((err) => {
console.log(err);
node.gateway.digi.serial.reconnect();
});
}
// Listen for FLY messages to run FOTA updates on older FLY messages
node.gateway.on('sensor_mode', (d) => {
if(d.mode == "FLY" && !Object.hasOwn(d, 'sync')){
if(Object.hasOwn(node.sensor_list, d.mac) && Object.hasOwn(node.sensor_list[d.mac], 'update_request') && d.mode == "FLY"){
node.request_manifest(d.mac);
}
};
});
// Event listener to make sure this only triggers once no matter how many gateway nodes there are
node.gateway.on('sync', (d) => {
if(d.type == 'sync_check_in' || d.type == 'sync_end' || d.type == 'manual_sync_check_in' || d.type == 'sync_acknowledgment'){
// TODO replace with deepclone once we update node version on Gateway.
const cloned_payload = v8.deserialize(v8.serialize(d.payload));
// const payload = structuredClone(d.payload);
const {
machine_values: { sensor_type, ...actual_values},
human_readable,
address: addr,
type,
...remaining
} = cloned_payload;
const payload = { ...remaining, ...actual_values, sensor_type };
if(Object.hasOwn(node.sensor_list, addr) && Object.hasOwn(node.sensor_list[addr], 'update_request')){
node.request_manifest(addr);
}else if(Object.hasOwn(this.gateway.sensor_libs, sensor_type)){
// API CONFIGURATION NODE
// let values = d.reported_config.machine_values;
const config_map = this.gateway.sensor_libs[sensor_type].get_config_map(payload.firmware_version);
let store_flag = false;
let fly_payload = null;
if(config.enable_fly_compatibility){
fly_payload = {
...human_readable,
'machine_values': {...actual_values}
};
for (const [new_key, conf] of Object.entries(config_map)) {
if (conf.old_fly_id) {
const old_key = conf.old_fly_id;
// Make sure a small slipup in the config map doesn't cause issues with the payload
if(old_key !== new_key){
if (Object.hasOwn(fly_payload, new_key)) {
fly_payload[old_key] = fly_payload[new_key];
delete fly_payload[new_key];
}
// Alias the machine value (inside machine_values)
if (Object.hasOwn(fly_payload.machine_values, new_key)) {
fly_payload.machine_values[old_key] = fly_payload.machine_values[new_key];
delete fly_payload.machine_values[new_key];
}
}
}
};
}
if(!Object.hasOwn(node.sensor_configs, addr)){
node.sensor_configs[addr] = {};
store_flag = true;
}
// delete so we don't have to consider it for storage
// TODO re-add, but remove from storage consideration for mapping
// if(Object.hasOwn(d.payload, 'tx_lifetime_counter')){
// delete d.payload.tx_lifetime_counter;
// }
// Store hardware_id outside of reported_configs
// if(!Object.hasOwn(node.sensor_configs[addr], 'hardware_id') && Object.hasOwn(d.payload, 'hardware_id')){
// node.sensor_configs[addr].hardware_id = d.payload.hardware_id;
// delete d.payload.hardware_id;
// store_flag = true;
// }
// Store core_version outside of reported_configs
// if(Object.hasOwn(d.payload, 'core_version')){
// node.sensor_configs[addr].core_version = d.payload.core_version;
// store_flag = true;
// }
// Loop through and translate and validate values.
// We want to allow passing in integers instead of byte arrays etc.
// This the FLY so we don't need to validate.
// values.network_id = config.pan_id;
// if(Object.hasOwn(d, 'node_id')){
// values.node_id = d.nodeId;
// }
for(let key in payload){
if(Object.hasOwn(config_map, key)){
if(!Object.hasOwn(config_map[key], 'write_index')){
// TODO
if(!Object.hasOwn(node.sensor_configs[addr], key) || Object.hasOwn(node.sensor_configs[addr], key) && node.sensor_configs[addr][key] != payload[key]){
// console.log('Config '+key+' is read only, moving from payload');
node.sensor_configs[addr][key] = payload[key];
if(key !== 'tx_lifetime_counter'){
store_flag = true;
}
}
delete payload[key];
}
}else{
// console.log('Not in config map, deleting key: '+key);
delete payload[key];
}
}
// If object has no reported configs, instantiate them
if(!Object.hasOwn(node.sensor_configs[addr], 'reported_configs')){
// node.sensor_configs[d.mac].reported_configs = {};
node.sensor_configs[addr].reported_configs = payload;
store_flag = true;
}
// If object has no desired configs, add them
if(!Object.hasOwn(node.sensor_configs[addr], 'desired_configs')){
node.sensor_configs[addr].desired_configs = payload;
store_flag = true;
}
// We're setting sensor_type above, so we can remove this.
// if(!Object.hasOwn(node.sensor_configs[addr], 'type')){
// node.sensor_configs[addr].type = d.type;
// store_flag = true;
// }else if(node.sensor_configs[d.mac].type != d.type){
// // This code is only called when the type doesn't match what the sensor reports, only foreseeable if user swaps sensor modules OR preloads configs for wrong sensor type
// delete node.sensor_configs[d.mac].desired_configs;
// node.sensor_configs[d.mac].type = d.type;
// node._emitter.emit('config_node_error', {topic: 'sensor_type_error', payload: {'error': "Sensor type used for desired configs does not match sensor type reported by sensor. Deleting desired configs"}, addr: d.mac, time: Date.now()});
// store_flag = true;
// }
if(!isDeepStrictEqual(node.sensor_configs[addr].reported_configs, payload)){
// Values are different, update the stored configs and write to file
node.sensor_configs[addr].reported_configs = payload;
// If reported configs change, but the auto config does not control configs, update automatically
// Will duplicate setting desired configs on first run, but only once or if user clear desired_configs
if(!Object.hasOwn(node.sensor_configs[addr], 'api_config_override')){
node.sensor_configs[addr].desired_configs = payload;
}
store_flag = true;
node._emitter.emit('config_node_msg', {topic: 'sensor_configs_update', payload: node.sensor_configs[addr], address: addr, time: Date.now()});
// node.send({topic: 'sensor_configs_update', payload: node.sensor_configs[d.mac], time: Date.now()});
}
if(type == 'sync_check_in'){
// TODO add backwards compatibility
if(config.enable_fly_compatibility){
this.gateway._emitter.emit('sensor_mode', {mac: addr, type: sensor_type, nodeId: payload.node_id, mode: 'FLY', lastHeard: Date.now(), reported_config: fly_payload, sync: true});
this.gateway._emitter.emit('sensor_mode-'+addr, {mac: addr, type: sensor_type, nodeId: payload.node_id, mode: 'FLY', lastHeard: Date.now(), reported_config: fly_payload, sync: true});
}
if(Object.hasOwn(node.sensor_configs[addr], 'desired_configs') && Object.hasOwn(node.sensor_configs[addr], 'api_config_override') && !isDeepStrictEqual(node.sensor_configs[addr].reported_configs, node.sensor_configs[addr].desired_configs)){
store_flag = true;
var tout = setTimeout(() => {
node.configure(d.address, d.payload.sensor_type);
// this.sync_init(addr, sensor_type);
}, 100);
}
}else if(type == 'manual_sync_check_in'){
if(Object.hasOwn(node.sensor_configs[addr], 'desired_configs') && Object.hasOwn(node.sensor_configs[addr], 'api_config_override') && !isDeepStrictEqual(node.sensor_configs[addr].reported_configs, node.sensor_configs[addr].desired_configs)){
node.configure(addr, sensor_type);
}
}else if(type == 'sync_end'){
if(config.enable_fly_compatibility){
this.gateway._emitter.emit('sensor_mode', {mac: addr, type: sensor_type, nodeId: payload.node_id, mode: 'OTF', lastHeard: Date.now(), reported_config: fly_payload, sync: true});
this.gateway._emitter.emit('sensor_mode-'+addr, {mac: addr, type: sensor_type, nodeId: payload.node_id, mode: 'OTF', lastHeard: Date.now(), reported_config: fly_payload, sync: true});
}
}
if(store_flag){
node.store_sensor_configs(JSON.stringify(node.sensor_configs));
}
}
}else if(d.type == 'sync_init'){
if(config.enable_fly_compatibility){
this.gateway._emitter.emit('sensor_mode', {mac: d.address, type: d.payload.sensor_type, nodeId: d.payload.node_id, mode: 'OTN', lastHeard: Date.now(), sync: true});
this.gateway._emitter.emit('sensor_mode-'+d.address, {mac: d.address, type: d.payload.sensor_type, nodeId: d.payload.node_id, mode: 'OTN', lastHeard: Date.now(), sync: true});
}
// if(node.sensor_configs[d.address] && node.sensor_configs[d.address].api_config_override){
// node.configure(d.address, d.payload.sensor_type);
// }
}
});
node.gateway.on('manifest_received', (manifest_data) => {
// read manifest length is 37. Could use the same event for both
if(Object.hasOwn(node.sensor_list, manifest_data.addr) && Object.hasOwn(node.sensor_list[manifest_data.addr], 'update_request')){
// TODO check manifest data and start update process
}
manifest_data.data = node._parse_manifest_read(manifest_data.data);
node._emitter.emit('send_manifest', manifest_data);
let firmware_data = node._compare_manifest(manifest_data);
if(!firmware_data){
delete node.sensor_list[manifest_data.addr].update_request;
return;
}
// TODO Right now assume everything is good
// node.gateway.firmware_set_to_ota_mode(manifest_data.addr);
setTimeout(() => {
var tout = setTimeout(() => {
console.log('Start OTA Timed Out');
}, 10000);
var promises = {};
promises.firmware_set_to_ota_mode = node.gateway.firmware_set_to_ota_mode(manifest_data.addr);
promises.finish = new Promise((fulfill, reject) => {
node.gateway.queue.add(() => {
return new Promise((f, r) => {
clearTimeout(tout);
// node.status(modes.FLY);
fulfill();
f();
});
});
});
for(var i in promises){
(function(name){
promises[name].then((res) => {
if(name != 'finish'){
// IF we receive an FON message with success
if(Object.hasOwn(res, 'data') && res.data[0] == 70 && res.data[1] == 79 && res.data[2] == 78 && res.result == 255){
manifest_data.enter_ota_fota_version = res.original.data[5];
manifest_data.enter_ota_success = true;
console.log(res);
}else{
manifest_data.enter_ota_success = false;
}
} else{
if(manifest_data.enter_ota_success){
// enter ota mode
node.gateway.digi.send.at_command("ID", [0x7a, 0xaa]).then().catch().then(() => {
console.log(manifest_data);
if(manifest_data.enter_ota_fota_version > 16){
console.log('V17 PROCESS');
console.log(manifest_data);
node.start_firmware_update_v17(manifest_data, firmware_data);
}else if(manifest_data.enter_ota_fota_version > 12){
console.log('V13 PROCESS');
console.log(manifest_data);
node.start_firmware_update_v13(manifest_data, firmware_data);
}else{
console.log('OLD PROCESSS');
console.log(manifest_data);
node.start_firmware_update(manifest_data, firmware_data);
}
});
}
}
}).catch((err) => {
console.log(err);
// msg[name] = err;
});
})(i);
};
});
});
}));
});
node.gateway.digi.serial.on('closed_comms', () => {
node.is_config = 3;
node._emitter.emit('mode_change', node.is_config);
});
}
};
this.sync_init = function(addr, type){
// console.log('!!!! Sync Init for sensor '+addr);
return new Promise((top_fulfill, top_reject) => {
var msg = {};
setTimeout(() => {
var tout = setTimeout(() => {
// switch to emitter for this
// node.status(modes.PGM_ERR);
// node.send({topic: 'OTN Request Results', payload: msg, time: Date.now()});
console.log('Sync Request Timed Out');
}, 10000);
var promises = {};
// This command is used for OTF on types 53, 80,81,82,83,84, 101, 102, 110, 111, 518, 519
let original_otf_devices = [53, 80, 81, 82, 83, 84, 87, 101, 102, 103, 110, 111, 112, 114, 117, 180, 181, 518, 519, 520, 538, 543];
if(original_otf_devices.includes(type)){
console.log('!!!! Entering Sync mode with original command');
// This command is used for OTF on types 53, 80, 81, 82, 83, 84, 101, 102, 110, 111, 518, 519
promises.config_enter_otn_mode = node.gateway.config_enter_otn_mode(addr);
}else{
console.log('!!!! Entering Sync mode with other command');
// This command is used for OTF on types not 53, 80, 81, 82, 83, 84, 101, 102, 110, 111, 518, 519
promises.config_enter_otn_mode = node.gateway.config_enter_otn_mode_common(addr);
}
promises.finish = new Promise((fulfill, reject) => {
node.gateway.queue.add(() => {
return new Promise((f, r) => {
clearTimeout(tout);
// node.status(modes.FLY);
fulfill();
f();
});
});
});
for(var i in promises){
(function(name){
promises[name].then((f) => {
if(name != 'finish') msg[name] = true;
else{
// #OTF
node.send({topic: 'OTN Request Results', payload: msg, time: Date.now()});
top_fulfill(msg);
}
}).catch((err) => {
msg[name] = err;
});
})(i);
}
});
});
};
this.get_required_configs = function(desired_configs, reported_configs){
const mismatched = {};
for (const key in desired_configs) {
if (reported_configs.hasOwnProperty(key)) {
if (!isDeepStrictEqual(desired_configs[key], reported_configs[key])) {
// if (desired_configs[key] !== reported_configs[key]) {
mismatched[key] = desired_configs[key];
};
};
};
return mismatched;
};
this.sync_check_reboot = function(addr){
if(node.sensor_configs[addr].reported_configs.network_id != node.sensor_configs[addr].desired_configs.network_id){
return true;
}else{
return false;
}
};
this.configure = function(addr, type){
return new Promise((top_fulfill, top_reject) => {
var success = {};
setTimeout(() => {
var tout = setTimeout(() => {
// TODO handle timeout and send emitters
node._emitter.emit('config_node_msg', {topic: 'Config Results', payload: success, addr: sensor.mac, time: Date.now()});
// node.status(modes.PGM_ERR);
// node.send({topic: 'Config Results', payload: success, time: Date.now(), addr: sensor.mac});
}, 60000);
// node.status(modes.PGM_NOW);
var mac = addr;
var promises = {};
var reboot = this.sync_check_reboot(addr);
promises.sync_command = node.gateway.send_sync_configs(addr, node.sensor_configs[addr]);
// These sensors listed in original_otf_devices use a different OTF code.
let original_otf_devices = [53, 80, 81, 82, 83, 84, 87, 101, 102, 103, 110, 111, 112, 114, 117, 180, 181, 518, 519, 520, 538];
// If we changed the network ID reboot the sensor to take effect.
// TODO if we add the encryption key command to node-red we need to reboot for it as well.
if(reboot){
promises.reboot_sensor = node.gateway.config_reboot_sensor(addr);
} else {
if(original_otf_devices.includes(type)){
promises.exit_otn_mode = node.gateway.config_exit_otn_mode(addr);
}else{
promises.config_exit_otn_mode_common = node.gateway.config_exit_otn_mode_common(addr);
}
}
promises.finish = new Promise((fulfill, reject) => {
node.gateway.queue.add(() => {
return new Promise((f, r) => {
clearTimeout(tout);
// node.status(modes.READY);
fulfill();
f();
});
});
});
for(var i in promises){
(function(name){
promises[name].then((f) => {
if(name != 'finish'){
if(Object.hasOwn(f, 'result')){
switch(f.result){
case 255:
success[name] = true;
break;
default:
success[name] = {
res: "Bad Response",
result: f.result,
sent: f.sent
};
}
}else{
success[name] = {
res: "no result",
result: null,
sent: f.sent
}
}
} else{
// #OTF
// Check if sensor responded with true for each config sent, if so delete from temp_required_configs
// we could auto reboot sensor, but this could lead to boot cycle for bad configs/cmds
if(Object.keys(node.sensor_configs[mac].temp_required_configs).length === 0){
delete node.sensor_configs[mac].temp_required_configs;
}
// TODO turn into event emitter.
node._emitter.emit('config_node_msg', {topic: 'Config Results', payload: success, addr: mac, time: Date.now()});
// node.send({topic: 'Config Results', payload: success, time: Date.now(), addr: mac});
// top_fulfill(success);
}
}).catch((err) => {
success[name] = err;
});
})(i);
}
}, 1000);
});
};
this._send_otn_request = function(sensor){
return new Promise((top_fulfill, top_reject) => {
var msg = {};
setTimeout(() => {
var tout = setTimeout(() => {
node.status(modes.PGM_ERR);
node.send({topic: 'OTN Request Results', payload: msg, time: Date.now()});
}, 10000);
var promises = {};
// This command is used for OTF on types 53, 80,81,82,83,84, 101, 102, 110, 111, 518, 519
const original_otf_devices = [53, 80, 81, 82, 83, 84, 87, 101, 102, 103, 110, 111, 112, 114, 117, 180, 181, 518, 519, 520, 538];
if(original_otf_devices.includes(sensor.type)){
// This command is used for OTF on types 53, 80, 81, 82, 83, 84, 101, 102, 110, 111, 518, 519
promises.config_enter_otn_mode = node.gateway.config_enter_otn_mode(sensor.mac);
}else{
// This command is used for OTF on types not 53, 80, 81, 82, 83, 84, 101, 102, 110, 111, 518, 519
promises.config_enter_otn_mode = node.gateway.config_enter_otn_mode_common(sensor.mac);
}
promises.finish = new Promise((fulfill, reject) => {
node.gateway.queue.add(() => {
return new Promise((f, r) => {
clearTimeout(tout);
// TODO emitter to set status on config nodes.
// node.status(modes.FLY);
fulfill();
f();
});
});
});
for(var i in promises){
(function(name){
promises[name].then((f) => {
if(name != 'finish') msg[name] = true;
else{
// #OTF
node.warn('OTN Request Finished');
// node.send({topic: 'OTN Request Results', payload: msg, time: Date.now()});
top_fulfill(msg);
}
}).catch((err) => {
if(Object.hasOwn(node.sensor_configs[sensor.mac], 'temp_required_configs')){
delete node.sensor_configs[sensor.mac].temp_required_configs;
}
node.warn('Error entering OTN mode');
msg[name] = err;
});
})(i);
}
});
});
};
node.check_mode = function(cb){
node.gateway.digi.send.at_command("ID").then((res) => {
var pan_id = (res.data[0] << 8) | res.data[1];
if(pan_id == 0x7BCD && parseInt(config.pan_id, 16) != 0x7BCD){
node.is_config = 1;
}else{
node.gateway.pan_id = pan_id;
node.is_config = 0;
}
if(cb) cb(node.is_config);
return node.is_config;
}).catch((err) => {
console.log(err);
node.is_config = 2;
node.gateway.digi.serial.reconnect();
if(cb) cb(node.is_config);
return node.is_config;
}).then((mode) => {
node._emitter.emit('mode_change', mode);
});
};
node.start_firmware_update = function(manifest_data, firmware_data){
return new Promise((top_fulfill, top_reject) => {
var success = {};
setTimeout(() => {
let chunk_size = 128;
let image_start = firmware_data.firmware.slice(1, 5).reduce(msbLsb)+6;
var promises = {};
promises.manifest = node.gateway.firmware_send_manifest(manifest_data.addr, firmware_data.firmware.slice(5, image_start-1));
firmware_data.firmware = firmware_data.firmware.slice(image_start+4);
var index = 0;
if(Object.hasOwn(node.sensor_list[manifest_data.addr], 'last_chunk_success')){
index = node.sensor_list[manifest_data.addr].last_chunk_success;
}
var temp_count = 0;
while(index*chunk_size < firmware_data.manifest.image_size){
let offset = index*chunk_size;
// console.log(index);
// let packet = [254, 59, 0, 0, 0];
let offset_bytes = int2Bytes(offset, 4);
let firmware_chunk = firmware_data.firmware.slice(index*chunk_size, index*chunk_size+chunk_size);
temp_count += 1;
// packet = packet.concat(offset_bytes, firmware_chunk);
promises[index] = node.gateway.firmware_send_chunk(manifest_data.addr, offset_bytes, firmware_chunk);
index++;
}
promises.reboot = node.gateway.config_reboot_sensor(manifest_data.addr);
for(var i in promises){
(function(name){
promises[name].then((f) => {
if(name == 'manifest'){
// delete node.sensor_list[manifest_data.addr].promises[name];
node.sensor_list[manifest_data.addr].test_check = {name: true};
node.sensor_list[manifest_data.addr].update_in_progress = true;
}else {
success[name] = true;
node.sensor_list[manifest_data.addr].test_check[name] = true;
node.sensor_list[manifest_data.addr].last_chunk_success = name;
// delete node.sensor_list[manifest_data.addr].promises[name];
}
}).catch((err) => {
if(name != 'reboot'){
node.gateway.clear_queue();
success[name] = err;
}else{
delete node.sensor_list[manifest_data.addr].last_chunk_success;
delete node.sensor_list[manifest_data.addr].update_request;
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
// #OTF
// node.send({topic: 'Config Results', payload: success, time: Date.now(), addr: manifest_data.addr});
top_fulfill(success);
}
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
node.resume_normal_operation();
});
})(i);
}
}, 1000);
});
};
node.start_firmware_update_v13 = function(manifest_data, firmware_data){
console.log('V13');
return new Promise((top_fulfill, top_reject) => {
var success = {successes:{}, failures:{}};
let chunk_size = 128;
let image_start = firmware_data.firmware.slice(1, 5).reduce(msbLsb)+6;
var promises = {
manifest: node.gateway.firmware_send_manifest(manifest_data.addr, firmware_data.firmware.slice(5, image_start-1))
};
// promises.manifest = node.gateway.firmware_send_manifest(manifest_data.addr, firmware_data.firmware.slice(5, image_start-1));
firmware_data.firmware = firmware_data.firmware.slice(image_start+4);
var index = 0;
if(Object.hasOwn(node.sensor_list[manifest_data.addr], 'last_chunk_success')){
index = node.sensor_list[manifest_data.addr].last_chunk_success;
}
while(index*chunk_size < firmware_data.manifest.image_size){
let offset = index*chunk_size;
let offset_bytes = int2Bytes(offset, 4);
let firmware_chunk = firmware_data.firmware.slice(index*chunk_size, index*chunk_size+chunk_size);
promises[index] = node.gateway.firmware_send_chunk_v13(manifest_data.addr, offset_bytes, firmware_chunk);
if(((index + 1) % 50) == 0 || (index+1)*chunk_size >= firmware_data.manifest.image_size){
promises[index+'_check'] = node.gateway.firmware_read_last_chunk_segment(manifest_data.addr);
};
index++;
}
console.log('Update Started');
console.log(Object.keys(promises).length);
console.log(Date.now());
promises.reboot = node.gateway.config_reboot_sensor(manifest_data.addr);
var firmware_continue = true;
for(var i in promises){
(function(name){
let retryCount = 0;
const maxRetries = 3; // Set the maximum number of retries
function attemptPromise() {
console.log(name);
promises[name].then((status_frame) => {
if(name == 'manifest'){
console.log('MANIFEST SUCCESFULLY SENT');
node.sensor_list[manifest_data.addr].test_check = {name: true};
node.sensor_list[manifest_data.addr].update_in_progress = true;
}
else if(name.includes('_check')){
console.log(name);
console.log(parseInt(name.split('_')[0]) * chunk_size);
let last_chunk = status_frame.data.reduce(msbLsb);
console.log(last_chunk);
if(last_chunk != (parseInt(name.split('_')[0]) * chunk_size)){
console.log('ERROR DETECTED IN OTA UPDATE');
success.failures[name] = {chunk: last_chunk, last_transmit: (parseInt(name.split('_')[0]) * chunk_size), last_report: last_chunk};
// node.gateway.clear_queue_except_last();
node.gateway.clear_queue();
node.resume_normal_operation();
} else {
success.successes[name] = {chunk: last_chunk};
}
}
else {
success[name] = true;
node.sensor_list[manifest_data.addr].test_check[name] = true;
node.sensor_list[manifest_data.addr].last_chunk_success = name;
}
}).catch((err) => {
console.log(name);
console.log(err);
if(name != 'reboot'){
node.gateway.clear_queue();
success[name] = err;
} else {
delete node.sensor_list[manifest_data.addr].last_chunk_success;
delete node.sensor_list[manifest_data.addr].update_request;
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
top_fulfill(success);
}
console.log('Update Finished')
console.log(Date.now());
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
node.resume_normal_operation();
});
}
attemptPromise(); // Start the initial attempt
})(i);
}
});
};
node.start_firmware_update_v17 = function(manifest_data, firmware_data){
console.log('V17 Update Start');
var start_offset = 0;
setTimeout(() => {
if(Object.hasOwn(node.sensor_list[manifest_data.addr], 'last_chunk_success')){
node.gateway.firmware_read_last_chunk_segment(manifest_data.addr).then((status_frame) => {
console.log('Last Chunk Segment Read');
console.log(status_frame);
start_offset = status_frame.data.slice(4,8).reduce(msbLsb);
node.queue_firmware_update_v17(manifest_data, firmware_data, start_offset);
}).catch((err) => {
// TODO FOTA EMIT ERROR FOTA
console.log('Error reading last chunk segment');
node.resume_normal_operation();
});
}else{
node.queue_firmware_update_v17(manifest_data, firmware_data, start_offset);
};
}, 1000);
};
node.queue_firmware_update_v17 = function(manifest_data, firmware_data, start_offset){
console.log('V17 Queue Update Start');
console.log('Start Offset: '+start_offset);
return new Promise((top_fulfill, top_reject) => {
var success = {successes:{}, failures:{}};
let chunk_size = 128;
let image_start = firmware_data.firmware.slice(1, 5).reduce(msbLsb)+6;
var promises = {};
if(start_offset == 0){
promises.manifest = node.gateway.firmware_send_manifest(manifest_data.addr, firmware_data.firmware.slice(5, image_start-1));
};
// promises.manifest = node.gateway.firmware_send_manifest(manifest_data.addr, firmware_data.firmware.slice(5, image_start-1));
firmware_data.firmware = firmware_data.firmware.slice(image_start+4);
// if(Object.hasOwn(node.sensor_list[manifest_data.addr], 'last_chunk_success')){
// index = node.sensor_list[manifest_data.addr].last_chunk_success;
// }
// reverse calculate index based on start_offset.
var index = parseInt(start_offset / chunk_size);
console.log('Index: '+index);
while(index*chunk_size < firmware_data.manifest.image_size){
let offset = index*chunk_size;
let offset_bytes = int2Bytes(offset, 4);
let firmware_chunk = firmware_data.firmware.slice(index*chunk_size, index*chunk_size+chunk_size);
promises[index] = node.gateway.firmware_send_chunk_v13(manifest_data.addr, offset_bytes, firmware_chunk);
if(((index + 1) % 50) == 0 || (index+1)*chunk_size >= firmware_data.manifest.image_size){
promises[index+'_check'] = node.gateway.firmware_read_last_chunk_segment(manifest_data.addr);
};
index++;
}
console.log('Update Started');
console.log(Object.keys(promises).length);
console.log(Date.now());
promises.reboot = node.gateway.config_reboot_sensor(manifest_data.addr);
var firmware_continue = true;
for(var i in promises){
(function(name){
let retryCount = 0;
const maxRetries = 3; // Set the maximum number of retries
function attemptPromise() {
console.log(name);
promises[name].then((status_frame) => {
if(name == 'manifest'){
console.log('MANIFEST SUCCESFULLY SENT');
node.sensor_list[manifest_data.addr].test_check = {name: true};
node.sensor_list[manifest_data.addr].update_in_progress = true;
}
else if(name.includes('_check')){
console.log(name);
console.log(parseInt(name.split('_')[0]) * chunk_size);
let last_chunk = status_frame.data.slice(0,4).reduce(msbLsb);
console.log(last_chunk);
if(last_chunk != (parseInt(name.split('_')[0]) * chunk_size)){
console.log('ERROR DETECTED IN OTA UPDATE');
success.failures[name] = {chunk: last_chunk, last_transmit: (parseInt(name.split('_')[0]) * chunk_size), last_report: last_chunk};
// node.gateway.clear_queue_except_last();
node.gateway.clear_queue();
node.resume_normal_operation();
} else {
success.successes[name] = {chunk: last_chunk};
}
}
else {
success[name] = true;
node.sensor_list[manifest_data.addr].test_check[name] = true;
node.sensor_list[manifest_data.addr].last_chunk_success = name;
}
}).catch((err) => {
console.log(name);
console.log(err);
if(name != 'reboot'){
node.gateway.clear_queue();
success[name] = err;
} else {
delete node.sensor_list[manifest_data.addr].last_chunk_success;
delete node.sensor_list[manifest_data.addr].update_request;
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
top_fulfill(success);
}
console.log('Update Finished')
console.log(Date.now());
node._emitter.emit('send_firmware_stats', {state: success, addr: manifest_data.addr});
node.resume_normal_operation();
});
}
attemptPromise(); // Start the initial attempt
})(i);
}
});
}
node.resume_normal_operation = function(){
let pan_id = parseInt(config.pan_id, 16);
node.gateway.digi.send.at_command("ID", [pan_id >> 8, pan_id & 255]).then().catch().then(() => {
console.log('Set Pan ID to: '+pan_id);
});
}
node.request_manifest = function(sensor_addr){
// Request Manifest
node.gateway.firmware_request_manifest(sensor_addr);
};
node.close_comms = function(){
// node.gateway._emitter.removeAllListeners('sensor_data');
if(typeof gateway_pool[this.key] != 'undefined'){
if(config.comm_type == 'serial'){
node.gateway.digi.serial.close();
clearTimeout(this.comms_timer);
// node.gateway.digi.serial.close(() => {
delete gateway_pool[this.key];
// });
}else{
node.gateway.digi.serial.close();
clearTimeout(this.comms_timer);
// node.gateway.digi.serial.close(() => {
delete gateway_pool[this.key];
// });
}
}
}
node._compare_manifest = function(sensor_manifest){
let firmware_dir = home_dir()+'/.node-red/node_modules/@ncd-io/node-red-enterprise-sensors/firmware_files';
let filename = '/' + sensor_manifest.data.device_type + '-' + sensor_manifest.data.hardware_id[0] + '_' + sensor_manifest.data.hardware_id[1] + '_' + sensor_manifest.data.hardware_id[2] + '.ncd';
try {
let firmware_file = fs.readFileSync(firmware_dir+filename,)
let stored_manifest = node._parse_manifest(firmware_file);
if(stored_manifest.firmware_version === sensor_manifest.data.firmware_version){
console.log('firmware versions SAME');
return false;
}
if(stored_manifest.max_image_size < sensor_manifest.data.image_size){
console.log('firmware image too large');
return false;
}
return {manifest: stored_manifest, firmware: firmware_file};
} catch(err){
console.log(err);
return err;
}
}
node._parse_manifest = function(bin_data){
return {
manifest_check: bin_data[0] == 0x01,
manifest_size: bin_data.slice(1, 5).reduce(msbLsb),
firmware_version: bin_data[5],
image_start_address: bin_data.slice(6, 10).reduce(msbLsb),
image_size: bin_data.slice(10, 14).reduce(msbLsb),
max_image_size: bin_data.slice(14, 18).reduce(msbLsb),
image_digest: bin_data.slice(18, 34),
device_type: bin_data.slice(34, 36).reduce(msbLsb),
hardware_id: bin_data.slice(36, 39),
reserve_bytes: bin_data.slice(39, 42)
}
};
node._parse_manifest_read = function(bin_data){
return {
// manifest_size: bin_data.slice(0,4).reduce(msbLsb),
firmware_version: bin_data[0],
image_start_address: bin_data.slice(1, 5).reduce(msbLsb),
image_size: bin_data.slice(5, 9).reduce(msbLsb),
max_image_size: bin_data.slice(9, 13).reduce(msbLsb),
image_digest: bin_data.slice(13, 29),
device_type: bin_data.slice(29, 31).reduce(msbLsb),
hardware_id: bin_data.slice(31, 34),
reserve_bytes: bin_data.slice(34, 37)
}
}
}
RED.nodes.registerType("ncd-gateway-config", NcdGatewayConfig);
function NcdGatewayNode(config){
RED.nodes.createNode(this,config);
this._gateway_node = RED.nodes.getNode(config.connection);
this._gateway_node.open_comms();
this.gateway = this._gateway_node.gateway;
var node = this;
node.on('close', function(){
this._gateway_node.close_comms();
this._gateway_node._emitter.removeAllListeners('send_manifest');
this._gateway_node._emitter.removeAllListeners('send_firmware_stats');
this._gateway_node._emitter.removeAllListeners('mode_change');
this.gateway._emitter.removeAllListeners('ncd_error');
this.gateway._emitter.removeAllListeners('sensor_data');
this.gateway._emitter.removeAllListeners('sensor_mode');
this.gateway._emitter.removeAllListeners('receive_packet-unknown_device');
this.gateway._emitter.removeAllListeners('route_info');
this.gateway._emitter.removeAllListeners('link_info');
this.gateway._emitter.removeAllListeners('converter_response');
this.gateway._emitter.removeAllListeners('manifest_received');
this.gateway._emitter.removeAllListeners('sync');
// console.log(this.gateway._emitter.eventNames());
});
node.is_config = false;
var statuses =[
{fill:"green",shape:"dot",text:"Ready"},
{fill:"yellow",shape:"ring",text:"Configuring"},
{fill:"red",shape:"dot",text:"Failed to Connect"},
{fill:"green",shape:"ring",text:"Connecting..."}
];
node.set_status = function(){
node.status(statuses[node._gateway_node.is_config]);
};
node.temp_send_1024 = function(frame){
console.log('node.temp_send_1024 TODO - Move to Emitter');
node.send({
topic: "remote_at_response",
payload: frame,
time: Date.now()
});
}
node.temp_send_local = function(frame){
console.log('node.temp_send_local TODO - Move to Emitter');
node.send({
topic: "local_at_response",
payload: frame,
time: Date.now()
});
}
node._gateway_node.on('send_manifest', (manifest_data) => {
node.send({
topic: 'sensor_manifest',
payload: {
addr: manifest_data.addr,
sensor_type: manifest_data.sensor_type,
manifest: manifest_data.data
},
time: Date.now()
});
});
// node.on('input', function(msg){
// switch(msg.topic){
// case "route_trace":
// var opts = {trace:1};
// node.gateway.route_discover(msg.payload.address,opts).then().catch(console.log);
// break;
// case "link_test":
// node.gateway.link_test(msg.payload.source_address,msg.payload.destination_address,msg.payload.options);
// break;
// case "fft_request":
// break;
// case "fidelity_test":
// break;
// default:
// const byteArrayToHexString = byteArray => Array.from(msg.payload.address, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
// node.gateway.control_send(msg.payload.address, msg.payload.data, msg.payload.options).then().catch(console.log);
// }
// console.log("input triggered, topic:"+msg.topic);
// if(msg.topic == "transmit"){
// const byteArrayToHexString = byteArray => Array.from(msg.payload.address, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
// node.gateway.control_send(msg.payload.address, msg.payload.data, msg.payload.options).then().catch(console.log);
// }
// if(msg.topic == "route_trace"){
// var opts = {trace:1};
// node.gateway.route_discover(msg.payload.address,opts).then().catch(console.log);
// }
// if(msg.topic == "link_test"){
// node.gateway.link_test(msg.payload.source_address,msg.payload.destination_address,msg.payload.options);
// }
// if(msg.topic == "fft_request"){
// }
// if(msg.topic == "fidelity_test"){
// }
// });
node._gateway_node.on('send_firmware_stats', (data) => {
node.send({
topic: 'update_stats',
payload: data.state,
addr: data.addr,
time: Date.now()
});
});
node.on('input', function(msg){
switch(msg.topic){
case "route_trace":
var opts = {trace:1};
node.gateway.route_discover(msg.payload.address,opts).then().catch(console.log);
break;
case "link_test":
node.gateway.link_test(msg.payload.source_address,msg.payload.destination_address,msg.payload.options);
break;
case "fidelity_test":
break;
case "converter_send_single":
// Example message:
// msg.topic = 'rs485_single';
// msg.payload.address = "00:13:a2:00:42:37:3e:e2";
// msg.payload.data = [0x01, 0x03, 0x00, 0x15, 0x00, 0x01, 0x95, 0xCE];
// msg.payload.meta = {
// 'command_id': 'query_water_levels',
// 'description': 'Query water levels in mm/cm',
// 'target_parser': 'parse_water_levels'
// }
if(!Object.hasOwn(msg.payload, 'timeout')){
msg.payload.timeout = 1500;
}
if(msg.payload.hasOwnProperty('meta')){
node.gateway.queue_bridge_query(msg.payload.address, msg.payload.command, msg.payload.meta, msg.payload.timeout);
}else{
node.gateway.queue_bridge_query(msg.payload.address, msg.payload.command, null, msg.payload.timeout);
}
break;
case "converter_send_multiple":
// Example message:
// msg.topic = 'converter_send_multiple';
// msg.payload.address = "00:13:a2:00:42:37:3e:e2";
// msg.payload.commands = [
// {
// 'command': [0x01, 0x03, 0x00, 0x15, 0x00, 0x01, 0x95, 0xCE],
// 'meta': {
// 'command_id': 'command_1',
// 'description': 'Example Command 1',
// 'target_parser': 'parse_water_levels'
// }
// },
// {
// 'command': [0x01, 0x03, 0x00, 0x15, 0x00, 0x01, 0x95, 0xCE],
// 'meta': {
// 'command_id': 'command_2',
// 'description': 'Example Command 2',
// 'target_parser': 'parse_temperature'
// }
// }
// ];
if(!Object.hasOwn(msg.payload, 'timeout')){
msg.payload.timeout = 1500;
}
node.gateway.prepare_bridge_query(msg.payload.address, msg.payload.commands, msg.payload.timeout);
break;
case "start_luber":
// msg = {
// 'topic': start_luber,
// 'payload': {
// 'address': '00:13:a2:00:42:37:87:0a', //REQUIRED
// duration: 3, //REQUIRED valid values 1-255
// channel: 2 //OPTIONAL default value of 1
// }
// }
if(!Object.hasOwn(msg.payload, 'duration')){
console.log('ERROR: No duration specified, please specify duration in msg.payload.duration');
break;
}
if(msg.payload.duration < 1 || msg.payload.duration > 255){
console.log('ERROR: Duration out of bounds. Duration');
break;
}
var cmd_promise;
if(Object.hasOwn(msg.payload, 'channel')){
node.gateway.control_start_luber(msg.payload.address, msg.payload.channel, msg.payload.duration).then((f) => {
node.send({
topic: 'command_results',
payload: {
res: 'Automatic Luber '+msg.payload.channel+' Activation Complete',
address: msg.payload.address,
channel: msg.payload.channel,
duration: msg.payload.duration
},
time: Date.now(),
addr: msg.payload.address
});
}).catch((err) => {
node.send({
topic: 'command_error',
payload: {
res: err,
address: msg.payload.address,
channel: msg.payload.channel,
duration: msg.payload.duration
},
time: Date.now(),
addr: msg.payload.address
});
// node.send({topic: 'Command Error', payload: err});
});
}else{
node.gateway.control_start_luber(msg.payload.address, 1, msg.payload.duration).then((f) => {
node.send({
topic: 'Command Results',
payload: {
res: 'Automatic Luber 1 Activation Complete',
address: msg.payload.address,
channel: 1,
duration: msg.payload.duration
},
time: Date.now(),
addr: msg.payload.address
});
}).catch((err) => {
node.send({
topic: 'Command Results',
payload: {
res: 'Automatic Luber 1 Activation Complete',
address: msg.payload.address,
channel: 1,
duration: msg.payload.duration
},
time: Date.now(),
addr: msg.payload.address
});
node.send({topic: 'Command Error', payload: err});
});
}
break;
case "add_firmware_file":
// Parse Manifest to grab information and store it for later use
// msg.payload = [0x01, 0x00, ...]
let new_msg = {
topic: 'add_firmware_file_response',
payload: node._gateway_node._parse_manifest(msg.payload)
}
let firmware_dir = home_dir()+'/.node-red/node_modules/@ncd-io/node-red-enterprise-sensors/firmware_files';
if (!fs.existsSync(firmware_dir)) {
fs.mkdirSync(firmware_dir);
};
let filename = '/' + new_msg.payload.device_type + '-' + new_msg.payload.hardware_id[0] + '_' + new_msg.payload.hardware_id[1] + '_' + new_msg.payload.hardware_id[2] + '.ncd';
fs.writeFile(firmware_dir+filename, msg.payload, function(err){
if(err){
console.log(err);
};
console.log('Success');
});
node.send(new_msg);
break;
// case "get_firmware_file":
// Commented out as I'd rather use a flow to request the file. More robust. Maybe more complicated, wait for feedback.
// // This input makes a request to the specified url and downloads a firmware file at that location
// // msg.payload = "https://github.com/ncd-io/WiFi_MQTT_Temperature_Firmware/raw/main/v1.0.3/firmware.bin"
case "check_firmware_file":
// Read file that should be at location and spit out the binary
// Example msg.payload
// msg.payload = {
// device_type: 80,
// hardware_id: [88, 88, 88]
// }
let fw_dir =