UNPKG

nodebots-interchange

Version:

Tool to build custom backpack firmwares to make devices I2C compliant

457 lines (389 loc) 14.3 kB
const _ = require('lodash'); const Avrgirl = require('avrgirl-arduino'); const Serialport = require('serialport'); const child_process = require('child_process'); const colors = require('colors'); const fsextra = require('fs-extra'); const interchange_client = require('./interchange_client'); const Downloader = require('./downloader'); const creators = require('./firmwares.json').creators; const firmwares = require('./firmwares.json').firmwares; const version = require('../package.json').version; // I think this can be refactored to be purely in the function where it is needed let ic_client;// = new interchange_client.Client(); const Interchange = function() { this.firmwares = firmwares; ic_client = new interchange_client.Client(); }; Interchange.prototype.clean_temp_dir = function(tmpdir) { // takes a temporary directory and cleans up any files within it and // then calls the callback to remove itself. if (tmpdir != undefined && tmpdir !== null && tmpdir !== '') { fsextra.removeSync(tmpdir.name); } }; Interchange.prototype.list_devices = function() { // this method returns the list of available firmwares as a JSON object const fws = []; firmwares.forEach((firmware) => { fws.push({ name: firmware.name, firmata: (firmware.firmata ? true : false), description: firmware.description }); }); return (fws); }; Interchange.prototype.get_ports = function(cb) { return new Promise((resolve, reject) => { Serialport.list() .then((ports) => { resolve(ports); }) .catch((err) => { reject(err); }); }); }; Interchange.prototype.list_ports = function() { // this function lists out all the ports available to flash firmware // this function is now deprecated /* istanbul ignore next */ if (!process.env.TEST) { console.warn('list_ports method deprecated - use get_ports instead'); } return this.get_ports(); }; Interchange.prototype.check_firmware = (firmware, options = {}) => { // checks if the firmware makes sense and downloads the hex file // to a temporary location if (firmware == undefined || firmware == null) { throw new Error('Must define a firmware to flash'); } const board = options.board || 'nano'; // assumes nano if none provided const useFirmata = (firmware.indexOf('Firmata') > 0) || options.firmata || false; let firmataName = options.firmata || ''; // check for default where no firmata name is supplied or default is implied. if (firmataName === true) { // set it to empty string firmataName = ''; } // see if the firmware is in the directory fw = _.find(firmwares, function(f) { return f.name == firmware; }); if (fw == undefined) { if (firmware.indexOf('git+https') >= 0) { // command has been passed with a git repo so make a temp object for // fw with appropriate stuff in it. fw = { 'name': firmware, 'deviceID': 0x01, 'creatorID': 0x00, 'repo': firmware, 'firmata': useFirmata }; } else { throw new Error('No firmware found: ' + firmware); } } // we have a firmware - check if we need firmata // this is not currently an issue as all firmwares provide firmata support if (useFirmata) { if (! fw.firmata) { throw new Error(`Firmware ${fw.name} does not support custom firmata`); } } const opts = options || {}; opts['useFirmata'] = useFirmata; opts['firmataName'] = firmataName; return {fw, opts}; }; Interchange.prototype.download_firmware = (fw, opts) => { // Figure out how to download and locate the appropriate firmware // then return the path to the file and optionally the temporary directory // returns the promise to download which will fulfill later. const dl = new Downloader({fw}); return dl.download(fw, opts) .then(({hexpath, tmpdir}) => { return ({hexpath, tmpdir}); }) .catch((err) => { throw err }); }; Interchange.prototype.flash_firmware = function(firmware, opts) { // flashes the board with the options provided. const board = opts.board || 'nano'; // assumes nano if none provided let port = opts.port || ''; // will leave empty and sees what happens. return new Promise((resolve, reject) => { // wrap this in a promise to handle the callback flow a bit better. const avrgirl = new Avrgirl({ board, port, debug: () => {} }); avrgirl.flash(firmware, (err) => { if (err) { reject(err); } // send the port back to be able to configure it. if (port == '') { port = avrgirl.options.port; } resolve(port); }); }); }; Interchange.prototype.install_firmware = async function(firmware, options = {}) { // manages the firmware installation process if (!firmware) { throw new Error('Please supply a firmware to install'); } const settings = { board: options.board || process.env.INTERCHANGE_BOARD || 'nano', port: options.port || process.env.INTERCHANGE_PORT, firmata: options.firmata, i2c_address: options.address }; // check firmware // download firmware // flash firmware to device // optionally do interchange client configuration try { // check the firmware const {fw, opts} = await this.check_firmware(firmware, settings); const usingFirmata = opts.useFirmata || false; // Assumes not unless explicit /* istanbul ignore next */ if (!process.env.TEST) { console.log('Downloading firmware'); } const {hexpath, tmpdir} = await this.download_firmware(fw, opts); /* istanbul ignore next */ if (!process.env.TEST) { console.log('Flashing firmware to board'); } const port = await this.flash_firmware(hexpath, options) .then((serport) => { if (tmpdir) { this.clean_temp_dir(tmpdir); } return serport; }) .catch((err) => { throw err; }); // now we should configure it if required. if (!usingFirmata) { /* istanbul ignore next */ if (!process.env.TEST) { console.log('Configuring the firmware'.magenta); } // combine options and firmware details together and pass across return this.set_firmware_details(port, {...opts, ...fw}) .then(() => { return true }) .catch(err => { throw err }); } } catch (e) { throw e; } }; Interchange.prototype.get_firmware_info = function(port) { // attempts to connect to an interchange firmware and get the // installed details. return new Promise((resolve, reject) => { if (!port) { reject(new Error('No port specified')); } ic_client.port = port; ic_client.on('error', (err) => { reject(err); }); ic_client.on('ready', () => { ic_client.get_info((err, data) => { // json is returned if (err) { ic_client.close(); // emit this as we're inside the handler and need to pass it // back outwards appropriately due to context ic_client.emit('error', err); } else { // look up the details from the various resources let fw_details = _.find(firmwares, (f) => { return ((parseInt(f.creatorID, 16) == data.creatorID) && (parseInt(f.firmwareID, 16) == data.firmwareID)); }); if (typeof(fw_details) === 'undefined') { // Cannot find firmware match from library. Best guess follows if (data.creatorID == undefined || data.creatorID == 'undefined') data.creatorID = '0x00'; fw_details = { name: 'Unknown', firmwareID: data.firmwareID || 0, creatorID: data.creatorID, description: 'This is an unknown backpack firmware' }; } const creator = _.find(creators, {id: fw_details.creatorID}); fw_details.creator = creator; fw_details.firmware_version = data.fw_version; fw_details.interchange_version = data.ic_version; fw_details.compile_date = data.compile_date; fw_details.i2c_address = data.i2c_address; fw_details.use_custom_addr = data.use_custom_addr; // close the serial port. ic_client.close(); // send the data back. resolve(fw_details); } }); }) }) }; Interchange.prototype.set_firmware_details = (port, opts) => { // sets the firmware details on the hardware as needed // opts has values set as hex strings return new Promise((resolve, reject) => { if (!port) { reject(new Error('No port specified')); } else { // check that the shape of the opts is correct. if (typeof(opts.address) === 'undefined') { return reject(new Error('No default address supplied')); } if (typeof(opts.firmwareID) === 'undefined') { return reject(new Error('No firmware ID supplied')); } if (typeof(opts.creatorID) === 'undefined') { return reject(new Error('No creator ID supplied')); } // now the shape of the object is checked then we can proceed ic_client.port = port; ic_client.on('error', (err) => { ic_client.close(); reject(err); }); ic_client.on('ready', () => { // use defaults and then check if we need otherwise let address = parseInt(opts.address, 16); let use_custom = 0; if (opts.i2c_address != undefined && opts.i2c_address != 0) { // override with the custom one. address = parseInt(opts.i2c_address, 16); use_custom = 1; } const settings = { firmwareID: parseInt(opts.firmwareID, 16), creatorID: parseInt(opts.creatorID, 16), i2c_address: address, use_custom_address: use_custom }; ic_client.set_details(settings, () => { ic_client.close(); resolve(); }); }); } }); }; /** Interchange.prototype.download_from_github = function(firmware, options, cb) { // downloads the firmware from the GH repo const self = this; let manifest_uri = null; let base_uri = null; let branch = 'master'; let repo = ''; console.info('Retrieving manifest data from GitHub'.magenta); if (firmware.repo.indexOf('git+https') == 0) { if (firmware.repo.indexOf('#') > 0 ) { // we want a branch so get that detail. branch = '/' + firmware.repo.substring(firmware.repo.indexOf('#') + 1); repo = firmware.repo.substring(22, firmware.repo.indexOf('#')); } else { repo = firmware.repo.substring(22); branch = '/master'; } base_uri = 'https://raw.githubusercontent.com' + repo + branch; manifest_uri = base_uri + '/manifest.json?' + (new Date().getTime()); } if (manifest_uri == null) { throw new Error('Cannot find manifest of ' + firmware); } // download the manifest file and then hand it over to then // download the hex file ready to be written to the board const tmp_dir = tmp.dirSync(); const manifest_file_path = tmp_dir.name + '/manifest.json'; Download(manifest_uri).then(data => { let manifest = null; try { manifest = JSON.parse(data); } catch (e) { console.error('Manifest file incorrect'); self.clean_temp_dir(tmp_dir); throw new Error('Manifest file error'); } // now we need to download the hex file. let manifest_objects = (options.useFirmata ? manifest.firmata : manifest.backpack); if (manifest_objects == undefined) { console.error("An appropriate bin can't be found to flash in the manifest file.".red); console.error("This is likely because either the maintainer hasn't provided the right " + "path to the hex file or because you've passed in an innapropriate selector " + 'on the --firmata= switch'); console.log('Firmware types available from this manifest file:'); if (manifest.firmata) { console.log('FIRMATA:'.blue); if (manifest.firmata.multi) { Object.keys(manifest.firmata).forEach(function(key) { if (key != 'multi') { console.log('\t "' + key + '" use --firmata=' + key); } }); } else { console.log('\t default use --firmata'); } } if (manifest.backpack) { console.log('I2C BACKPACK'.blue); } throw new Error("Can't find binary to flash"); } // this deals with a firmata object supplied that isn't the default // one in order to grab the right hex file. if (options.useFirmata && options.firmataName != '') { manifest_objects = manifest.firmata[options.firmataName]; } else if (options.useFirmata && options.firmataName == '' && manifest.firmata.multi != undefined) { // we have multiple firmatas and none have been supplied. throw new Error('Multiple firmatas are available, please supply a name'); } if (manifest_objects.hexPath == undefined) { console.error(manifest_objects); self.clean_tmp_dir(tmp_dir); throw new Error('Hex path cannot be found'); } if (manifest_objects.hexPath.indexOf('/') != 0) { manifest_objects.hexPath = '/' + manifest_objects.hexPath; } const hex_uri = base_uri + manifest_objects.bins + options.board + manifest_objects.hexPath + '?' + (new Date().getTime()); console.info('Downloading hex file') Download(hex_uri).then(hex_data => { const hex_path = tmp_dir.name + '/bin.hex'; try { fs.writeFileSync(hex_path, hex_data); } catch (e) { console.error("Can't write hex file to file system".red); self.clean_tmp_dir(tmp_dir); throw new Error('HexWriteError'); } // about to call the writer. cb(hex_path, tmp_dir, options); }).catch(function() { console.error("Can't download the hex file (%s)".red, hex_uri); self.clean_tmp_dir(tmp_dir); throw new Error('HexDownloadError'); }); }).catch(function(err) { console.error('There was an error downloading or processing the the manifest file.'.red); console.log(err); }); }; **/ module.exports = Interchange;