UNPKG

@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
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 =