UNPKG

particle-cli

Version:

Simple Node commandline application for working with your Particle devices and using the Particle Cloud

1,265 lines (1,094 loc) 36.3 kB
'use strict'; const os = require('os'); const fs = require('fs'); const _ = require('lodash'); const path = require('path'); const chalk = require('chalk'); const VError = require('verror'); const inquirer = require('inquirer'); const prompt = require('inquirer').prompt; const wifiScan = require('node-wifiscanner2').scan; const { SerialPort } = require('serialport'); const log = require('../lib/log'); const specs = require('../lib/device-specs'); const CLICommandBase = require('./base'); const settings = require('../../settings'); const SerialBatchParser = require('../lib/serial-batch-parser'); const SerialTrigger = require('../lib/serial-trigger'); const spinnerMixin = require('../lib/spinner-mixin'); const { ensureError, delay } = require('../lib/utilities'); const FlashCommand = require('./flash'); const usbUtils = require('./usb-util'); const { platformForId } = require('../lib/platform'); const { FirmwareModuleDisplayNames } = require('particle-usb'); const semver = require('semver'); const { getProtectionStatus, disableDeviceProtection } = require('../lib/device-protection-helper'); const ParticleApi = require('../cmd/api'); const createApiCache = require('../lib/api-cache'); const IDENTIFY_COMMAND_TIMEOUT = 20000; // TODO: DRY this up somehow // The categories of output will be handled via the log class, and similar for protip. const cmd = path.basename(process.argv[1]); const arrow = chalk.green('>'); const timeoutError = 'Serial timed out'; const availability = (asset, availableAssets) => availableAssets.some( availableAsset => availableAsset.hash === asset.hash && availableAsset.name === asset.name ); const SERIAL_PORT_DEFAULTS = { baudRate: 9600, autoOpen: false }; module.exports = class SerialCommand extends CLICommandBase { constructor(){ super(); spinnerMixin(this); } findDevices(){ return SerialPort.list() .then(ports => { const devices = []; ports.forEach((port) => { // manufacturer value // Mac - Spark devices // Devices on old driver - Spark Core, Photon // Devices on new driver - Particle IO (https://github.com/spark/firmware/pull/447) // Windows only contains the pnpId field let device; const serialDeviceSpec = _.find(specs, (deviceSpec) => { if (!deviceSpec.serial){ return false; } const vid = deviceSpec.serial.vid; const pid = deviceSpec.serial.pid; const usbMatches = (port.vendorId === vid.toLowerCase() && port.productId === pid.toLowerCase()); const pnpMatches = !!(port.pnpId && (port.pnpId.indexOf('VID_' + vid.toUpperCase()) >= 0) && (port.pnpId.indexOf('PID_' + pid.toUpperCase()) >= 0)); return !!(usbMatches || pnpMatches); }); if (serialDeviceSpec){ device = { port: port.path, type: serialDeviceSpec.productName, deviceId: serialDeviceSpec.serial.deviceId && serialDeviceSpec.serial.deviceId(port.serialNumber || port.pnpId), specs: serialDeviceSpec }; } // Populate the Device ID based on the ports serial number: if (device && port.serialNumber && typeof device.deviceId == 'undefined') { device.deviceId = port.serialNumber; } const matchesManufacturer = port.manufacturer && (port.manufacturer.indexOf('Particle') >= 0 || port.manufacturer.indexOf('Spark') >= 0 || port.manufacturer.indexOf('Photon') >= 0); if (!device && matchesManufacturer){ device = { port: port.path, type: 'Core' }; } if (device){ devices.push(device); } }); //if I didn't find anything, grab any 'ttyACM's if (devices.length === 0){ ports.forEach((port) => { //if it doesn't have a manufacturer or pnpId set, but it's a ttyACM port, then lets grab it. if (port.path.indexOf('/dev/ttyACM') === 0){ devices.push({ port: port.path, type: '' }); } else if (port.path.indexOf('/dev/cuaU') === 0){ devices.push({ port: port.path, type: '' }); } }); } return devices; }) .catch(err => { throw new VError(ensureError(err), 'Error listing serial ports'); }); } listDevices(){ return this.findDevices() .then(devices => { if (devices.length === 0){ this.ui.stdout.write(`${chalk.bold.white('No devices available via serial')}`); return; } this.ui.stdout.write(`Found ${chalk.cyan(devices.length)} ${devices.length > 1 ? 'devices' : 'device'} connected via serial:${os.EOL}`); devices.forEach((device) => this.ui.stdout.write(`${device.port} - ${device.type} - ${device.deviceId}${os.EOL}`)); }); } monitorPort({ port, follow }){ let cleaningUp = false; let selectedDevice; let serialPort; const { api, auth } = this._particleApi(); const displayError = (err) => { if (err){ console.error('Serial err: ' + err); console.error('Serial problems, please reconnect the device.'); } }; // Called when port closes const handleClose = async () => { this.ui.stdout.write(`${os.EOL}`); if (follow && !cleaningUp){ this.ui.stdout.write(`${chalk.bold.white('Serial connection closed. Attempting to reconnect...')}${os.EOL}`); return reconnect(); } this.ui.stdout.write(`${chalk.bold.white('Serial connection closed.')}${os.EOL}`); }; // Handle interrupts and close the port gracefully const handleInterrupt = () => { if (!cleaningUp){ cleaningUp = true; if (serialPort && serialPort.isOpen){ serialPort.close(); } process.exit(0); } }; // Called only when the port opens successfully const handleOpen = () => { this.ui.stdout.write(`${chalk.bold.white('Serial monitor opened successfully:')}${os.EOL}`); }; const handleDeviceProtection = async () => { let usbDevice; try { usbDevice = await usbUtils.getOneUsbDevice({ idOrName: selectedDevice.deviceId, api, auth }); const s = await getProtectionStatus(usbDevice); if (s.protected) { await disableDeviceProtection(usbDevice); } } catch (_err) { // ignore error } finally { if (usbDevice && usbDevice.isOpen) { await usbDevice.close(); } } }; // If device is not found but we are still '--follow'ing to find a device, // handlePortFn schedules a delayed retry using setTimeout. const handlePortFn = async (device) => { if (!device) { if (follow) { await delay(settings.serial_follow_delay); if (cleaningUp){ return; } else { this.whatSerialPortDidYouMean(port, true) .catch(() => null) .then(handlePortFn); } return; } else { throw new VError('No serial port identified'); } } this.ui.stdout.write(`Opening serial monitor for com port: " ${device.port} "${os.EOL}`); selectedDevice = device; await handleDeviceProtection(); // Note that a Protected Device will be left in Service Mode after the operation openPort(); }; function openPort(){ serialPort = new SerialPort({ path: selectedDevice.port, ...SERIAL_PORT_DEFAULTS }); serialPort.on('close', handleClose); serialPort.on('readable', () => { process.stdout.write(serialPort.read().toString()); }); serialPort.on('error', displayError); serialPort.open((err) => { if (err && follow){ reconnect(selectedDevice); } else if (err){ displayError(err); } else { handleOpen(); } }); } async function reconnect(){ await delay(settings.serial_follow_delay); await handleDeviceProtection(); openPort(selectedDevice); } process.on('SIGINT', handleInterrupt); process.on('SIGQUIT', handleInterrupt); process.on('SIGBREAK', handleInterrupt); process.on('SIGTERM', handleInterrupt); process.on('exit', () => handleInterrupt()); if (follow){ this.ui.stdout.write('Polling for available serial device...'); } return this.whatSerialPortDidYouMean(port, true).then(handlePortFn); } /** * Creates and returns the Particle API and authentication token. * * @returns {Object} The Particle API instance and authentication token. */ _particleApi() { const auth = settings.access_token; const api = new ParticleApi(settings.apiUrl, { accessToken: auth }); const apiCache = createApiCache(api); return { api: apiCache, auth }; } /** * Device identify gives the following * - Device ID * - (Cellular devices) Cell radio IMEI * - Obtained via control req for dvos > 5.6.0 * - Obtained via serial otherwise * - (Cellular devices) Cell radio ICCID * - System firmware version * This command is committed to print the values that are obtained from the device * ignoring the ones that are not obtained * @param {Number|String} comPort */ async identifyDevice({ port }) { let deviceFromSerialPort; let deviceId = ''; // For Protected Devices, there's a brief delay after the operation // before Protection is re-enabled. A spinner provides visual feedback // to the user during this process. try { deviceFromSerialPort = await this.whatSerialPortDidYouMean(port, true); deviceId = deviceFromSerialPort.deviceId; await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceId }, func: (dev) => this._identifyDevice(dev, deviceFromSerialPort) }); } catch (err) { throw new VError(ensureError(err), 'Could not identify device'); } } async _identifyDevice(device, deviceFromSerialPort) { let fwVer = ''; let cellularImei = ''; let cellularIccid = ''; const deviceId = deviceFromSerialPort.deviceId; // Obtain system firmware version fwVer = device.firmwareVersion; // If the device is a cellular device, obtain imei and iccid const features = platformForId(device.platformId).features; if (features.includes('cellular')) { // since from 6.x onwards we can't use serial to get imei, we use control request if (semver.gte(fwVer, '6.0.0')) { try { const cellularInfo = await device.getCellularInfo({ timeout: 2000 }); if (!cellularInfo) { throw new VError('No data returned from control request for device info'); } cellularImei = cellularInfo.imei; cellularIccid = cellularInfo.iccid; } catch (err) { // ignore and move on to get other fields throw new VError(ensureError(err), 'Could not get device info'); } } else { try { const cellularInfo = await this.getDeviceInfoFromSerial(deviceFromSerialPort); if (!cellularInfo) { throw new VError('No data returned from serial port for device info'); } cellularImei = cellularInfo.imei; cellularIccid = cellularInfo.iccid; } catch (err) { // ignore and move on to get other fields throw new VError(ensureError(err), 'Could not get device info, ensure the device is in listening mode'); } } } this._printIdentifyInfo({ deviceId, fwVer, cellularImei, cellularIccid }); } _printIdentifyInfo({ deviceId, fwVer, cellularImei, cellularIccid }) { this.ui.stdout.write(`Your device id is ${chalk.bold.cyan(deviceId)}${os.EOL}`); if (cellularImei) { this.ui.stdout.write(`Your IMEI is ${chalk.bold.cyan(cellularImei)}${os.EOL}`); } if (cellularIccid) { this.ui.stdout.write(`Your ICCID is ${chalk.bold.cyan(cellularIccid)}${os.EOL}`); } if (fwVer) { this.ui.stdout.write(`Your system firmware version is ${chalk.bold.cyan(fwVer)}${os.EOL}`); } } /** * Obtains mac address for wifi and ethernet devices * @param {string} port */ async deviceMac({ port }) { try { const deviceFromSerialPort = await this.whatSerialPortDidYouMean(port, true); const deviceId = deviceFromSerialPort.deviceId; await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceId }, func: (dev) => this._deviceMac(dev) }); } catch (err) { throw new VError(ensureError(err), 'Could not identify device'); } } async _deviceMac(device) { let macAddress, currIfaceName; try { const networkIfaceListreply = await device.getNetworkInterfaceList({ timeout: 2000 }); // We expect either one Wifi interface or one Ethernet interface // Find it and return the hw address value from that interface for (const iface of networkIfaceListreply) { const index = iface.index; const type = iface.type; if (type === 'WIFI' || type === 'ETHERNET') { const networkIfaceReply = await device.getNetworkInterface({ index, timeout: 2000 }); macAddress = networkIfaceReply.hwAddress; currIfaceName = type; break; } } } catch (err) { throw new VError(ensureError(err), 'Could not get MAC address'); } this._printMacAddress({ macAddress, currIfaceName }); } _printMacAddress({ macAddress, currIfaceName }) { // Print output if (macAddress) { this.ui.stdout.write(`Your device MAC address is ${chalk.bold.cyan(macAddress)}${os.EOL}`); this.ui.stdout.write(`Interface is ${_.capitalize(currIfaceName)}${os.EOL}`); } else { this.ui.stdout.write(`Your device does not have a MAC address${os.EOL}`); } } /** * Inspects a Particle device and provides module info and asset info * @param {string} port */ async inspectDevice({ port }) { let deviceFromSerialPort, deviceId; try { deviceFromSerialPort = await this.whatSerialPortDidYouMean(port, true); deviceId = deviceFromSerialPort.deviceId; await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceId }, func: (dev) => this._inspectDevice(dev) }); } catch (err) { throw new VError(ensureError(err), 'Could not inspect device'); } } async _inspectDevice(device) { const platform = platformForId(device.platformId); const modules = await device.getFirmwareModuleInfo({ timeout: 5000 }); if (modules && modules.length > 0) { for (const m of modules) { if (m.assetDependencies && m.assetDependencies.length > 0) { const assetInfo = await device.getAssetInfo({ timeout: 5000 }); m.availableAssets = assetInfo.available; m.requiredAssets = assetInfo.required; } } } this._printInspectInfo({ deviceId: device.id, platform, modules }); } /** * Obtains module info from control requests * @param {object} device * @returns {boolean} returns error or true */ async _printInspectInfo({ deviceId, platform, modules }) { this.ui.stdout.write(`Device: ${chalk.bold.cyan(deviceId)}${os.EOL}`); this.ui.stdout.write(`Platform: ${platform.id} - ${chalk.bold.cyan(platform.displayName)}${os.EOL}${os.EOL}`); if (modules && modules.length > 0) { this.ui.stdout.write(chalk.underline(`Modules${os.EOL}`)); for (const m of modules) { const func = FirmwareModuleDisplayNames[m.type]; this.ui.stdout.write(` ${chalk.bold.cyan(_.capitalize(func))} module ${chalk.bold('#' + m.index)} - version ${chalk.bold(m.version)}${os.EOL}`); this.ui.stdout.write(` Size: ${m.size / 1000} kB${m.maxSize ? ` / MaxSize: ${m.maxSize / 1000} kB` : ''}${os.EOL}`); if (m.type === 'USER_PART' && m.hash) { this.ui.stdout.write(` UUID: ${m.hash}${os.EOL}`); } const errors = m.validityErrors; this.ui.stdout.write(` Integrity: ${errors.includes('INTEGRITY_CHECK_FAILED') ? chalk.red('FAIL') : chalk.green('PASS')}${os.EOL}`); this.ui.stdout.write(` Address Range: ${errors.includes('RANGE_CHECK_FAILED') ? chalk.red('FAIL') : chalk.green('PASS')}${os.EOL}`); this.ui.stdout.write(` Platform: ${errors.includes('PLATFORM_CHECK_FAILED') ? chalk.red('FAIL') : chalk.green('PASS')}${os.EOL}`); this.ui.stdout.write(` Dependencies: ${errors.includes('DEPENDENCY_CHECK_FAILED') ? chalk.red('FAIL') : chalk.green('PASS')}${os.EOL}`); if (m.dependencies.length > 0){ m.dependencies.forEach((dep) => { const df = FirmwareModuleDisplayNames[dep.type]; this.ui.stdout.write(` ${_.capitalize(df)} module #${dep.index} - version ${dep.version}${os.EOL}`); }); } if (m.assetDependencies && m.assetDependencies.length > 0) { const { availableAssets, requiredAssets } = m; this.ui.stdout.write(` Asset Dependencies:${os.EOL}`); this.ui.stdout.write(` Required:${os.EOL}`); requiredAssets.forEach((asset) => { this.ui.stdout.write(` ${asset.name} (${availability(asset, availableAssets) ? chalk.green('PASS') : chalk.red('FAIL')})${os.EOL}`); }); const notRequiredAssets = availableAssets.filter(asset => !requiredAssets.some(requiredAsset => requiredAsset.hash === asset.hash)); if (notRequiredAssets.length > 0) { this.ui.stdout.write(` Available but not required:${os.EOL}`); notRequiredAssets.forEach(asset => { this.ui.stdout.write(`\t${asset.name}${os.EOL}`); }); } } this.ui.stdout.write(`${os.EOL}`); } } } async flashDevice(binary, { port }) { this.ui.stdout.write( `NOTE: ${chalk.bold.white('particle flash serial')} has been replaced by ${chalk.bold.white('particle flash --local')}.${os.EOL}` + `Please use that command going forward.${os.EOL}${os.EOL}` ); const device = await this.whatSerialPortDidYouMean(port, true); const deviceId = device.deviceId; const flashCmdInstance = new FlashCommand(); await flashCmdInstance.flashLocal({ files: [deviceId, binary], applicationOnly: true }); } _scanNetworks(){ return new Promise((resolve, reject) => { this.newSpin('Scanning for nearby Wi-Fi networks...').start(); wifiScan((err, networkList) => { this.stopSpin(); if (err){ return reject(new VError('Unable to scan for Wi-Fi networks. Do you have permission to do that on this system?')); } resolve(networkList); }); }) .then(networkList => { // todo - if the prompt is auto answering, then only auto answer once, to prevent // never ending loops if (networkList.length === 0){ return prompt([{ type: 'confirm', name: 'rescan', message: 'Uh oh, no networks found. Try again?', default: true }]).then((answers) => { if (answers.rescan){ return this._scanNetworks(); } return []; }); } networkList = networkList.filter((ap) => { if (!ap){ return false; } // channel # > 14 === 5GHz if (ap.channel && parseInt(ap.channel, 10) > 14){ return false; } return true; }); networkList.sort((a, b) => { return a.ssid.toLowerCase().localeCompare(b.ssid.toLowerCase()); }); return networkList; }); } async configureWifi({ port, file }){ const deviceFromSerialPort = await this.whatSerialPortDidYouMean(port, true); const deviceId = deviceFromSerialPort.deviceId; if (!deviceFromSerialPort?.specs?.features?.includes('wifi')) { throw new VError('The device does not support Wi-Fi'); } await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceId }, func: this._configureWifi.bind(this, deviceFromSerialPort, file) }); } async _configureWifi(deviceFromSerialPort, file, device){ const fwVer = device.firmwareVersion; // XXX: Firmware version TBD if (semver.gte(fwVer, '6.2.0')) { this.ui.stdout.write(`${chalk.yellow('[Recommendation]')}${os.EOL}`); this.ui.stdout.write(`${chalk.yellow('Use the improved Wi-Fi configuration commands for this device-os version (>= 6.2.0)')}${os.EOL}`); this.ui.stdout.write(`${chalk.yellow('See \'particle wifi --help\' for more details on available commands')}${os.EOL}`); this.ui.stdout.write(`${os.EOL}`); } // configure Wi-Fi via serial interface let res; if (file){ res = await this._configWifiFromFile(deviceFromSerialPort, file); } else { res = await this.promptWifiScan(deviceFromSerialPort); } return res; } _configWifiFromFile(device, filename){ // TODO (mirande): use util.promisify once node@6 is no longer supported return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (error, data) => { if (error){ return reject(error); } return resolve(data); }); }) .then(content => { const opts = JSON.parse(content); return this.serialWifiConfig(device, opts); }); } promptWifiScan(device){ const question = { type: 'confirm', name: 'scan', message: chalk.bold.white('Should I scan for nearby Wi-Fi networks?'), default: true }; return prompt([question]) .then((ans) => { if (ans.scan){ return this._scanNetworks().then(networks => { return this._getWifiInformation(device, networks); }); } else { return this._getWifiInformation(device); } }); } _removePhotonNetworks(ssids){ return ssids.filter((ap) => { if (ap.indexOf('Photon-') === 0){ return false; } return true; }); } _getWifiInformation(device, networks){ const rescanLabel = '[rescan networks]'; networks = networks || []; const networkMap = _.keyBy(networks, 'ssid'); let ssids = _.map(networks, 'ssid'); ssids = this._removePhotonNetworks(ssids); const questions = [ { type: 'list', name: 'ap', message: chalk.bold.white('Select the Wi-Fi network with which you wish to connect your device:'), choices: () => { const ns = ssids.slice(); ns.unshift(new inquirer.Separator()); ns.unshift(rescanLabel); ns.unshift(new inquirer.Separator()); return ns; }, when: () => { return networks.length; } }, { type: 'confirm', name: 'detectSecurity', message: chalk.bold.white('Should I try to auto-detect the wireless security type?'), when: (answers) => { return !!answers.ap && !!networkMap[answers.ap] && !!networkMap[answers.ap].security; }, default: true } ]; return prompt(questions) .then(answers => { if (answers.ap === rescanLabel){ return this._scanNetworks().then(networks => { return this._getWifiInformation(device, networks); }); } const network = answers.ap; const ap = networkMap[network]; const security = answers.detectSecurity && ap && ap.security; if (security){ console.log(arrow, 'Detected', security, 'security'); } return this.serialWifiConfig(device, { network, security }); }); } /** * This is a wrapper function so _serialWifiConfig can return the * true promise state for testing. */ serialWifiConfig(...args) { return this._serialWifiConfig(...args) .then(() => { console.log('Done! Your device should now restart.'); }, (err) => { if (err && err.message) { log.error('Something went wrong:', err.message); } else { log.error('Something went wrong:', err); } }); } _serialWifiConfig(device, opts = {}){ if (!device){ return Promise.reject('No serial port available'); } log.verbose('Attempting to configure Wi-Fi on ' + device.port); let isEnterprise = false; const self = this; let cleanUpFn; const promise = new Promise((resolve, reject) => { const serialPort = self.serialPort || new SerialPort({ path: device.port, ...SERIAL_PORT_DEFAULTS }); const parser = new SerialBatchParser({ timeout: 250 }); cleanUpFn = () => { resetTimeout(); serialPort.removeListener('close', serialClosedEarly); return new Promise((resolve) => { serialPort.close(resolve); }); }; serialPort.pipe(parser); serialPort.on('error', (err) => reject(err)); serialPort.on('close', serialClosedEarly); const serialTrigger = new SerialTrigger(serialPort, parser); serialTrigger.addTrigger('SSID:', (cb) => { resetTimeout(); if (opts.network){ return cb(opts.network + '\n'); } const question = { type: 'input', name: 'ssid', message: 'SSID', validate: (input) => { if (!input || !input.trim()){ return 'Please enter a valid SSID'; } else { return true; } }, filter: (input) => { return input.trim(); } }; prompt([question]) .then((ans) => { cb(ans.ssid + '\n', startTimeout.bind(self, 5000)); }); }); serialTrigger.addTrigger('Security 0=unsecured, 1=WEP, 2=WPA, 3=WPA2:', parsesecurity.bind(null, false)); serialTrigger.addTrigger('Security 0=unsecured, 1=WEP, 2=WPA, 3=WPA2, 4=WPA Enterprise, 5=WPA2 Enterprise:', parsesecurity.bind(null, true)); serialTrigger.addTrigger('Security Cipher 1=AES, 2=TKIP, 3=AES+TKIP:', (cb) => { resetTimeout(); if (opts.security !== undefined){ let cipherType = 1; if (opts.security.indexOf('AES') >= 0 && opts.security.indexOf('TKIP') >= 0){ cipherType = 3; } else if (opts.security.indexOf('TKIP') >= 0){ cipherType = 2; } else if (opts.security.indexOf('AES') >= 0){ cipherType = 1; } return cb(cipherType + '\n', startTimeout.bind(self, 5000)); } const question = { type: 'list', name: 'cipher', message: 'Cipher Type', choices: [ { name: 'AES+TKIP', value: 3 }, { name: 'TKIP', value: 2 }, { name: 'AES', value: 1 } ] }; prompt([question]) .then((ans) => { cb(ans.cipher + '\n', startTimeout.bind(self, 5000)); }); }); serialTrigger.addTrigger('EAP Type 0=PEAP/MSCHAPv2, 1=EAP-TLS:', (cb) => { resetTimeout(); isEnterprise = true; if (opts.eap !== undefined){ let eapType = 0; if (opts.eap.toLowerCase().indexOf('peap') >= 0){ eapType = 0; } else if (opts.eap.toLowerCase().indexOf('tls')){ eapType = 1; } return cb(eapType + '\n', startTimeout.bind(self, 5000)); } const question = { type: 'list', name: 'eap', message: 'EAP Type', choices: [ { name: 'PEAP/MSCHAPv2', value: 0 }, { name: 'EAP-TLS', value: 1 } ] }; prompt([question]) .then((ans) => { cb(ans.eap + '\n', startTimeout.bind(self, 5000)); }); }); serialTrigger.addTrigger('Username:', (cb) => { resetTimeout(); if (opts.username){ cb(opts.username + '\n', startTimeout.bind(self, 15000)); } else { const question = { type: 'input', name: 'username', message: 'Username', validate: (val) => { return !!val; } }; prompt([question]) .then((ans) => { cb(ans.username + '\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Outer identity (optional):', (cb) => { resetTimeout(); if (opts.outer_identity){ cb(opts.outer_identity.trim + '\n', startTimeout.bind(self, 15000)); } else { const question = { type: 'input', name: 'outer_identity', message: 'Outer identity (optional)' }; prompt([question]) .then((ans) => { cb(ans.outer_identity + '\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Client certificate in PEM format:', (cb) => { resetTimeout(); if (opts.client_certificate){ cb(opts.client_certificate.trim() + '\n\n', startTimeout.bind(self, 15000)); } else { const question = { type: 'editor', name: 'client_certificate', message: 'Client certificate in PEM format', validate: (val) => { return !!val; } }; prompt([question]) .then((ans) => { cb(ans.client_certificate.trim() + '\n\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Private key in PEM format:', (cb) => { resetTimeout(); if (opts.private_key){ cb(opts.private_key.trim() + '\n\n', startTimeout.bind(self, 15000)); } else { const question = { type: 'editor', name: 'private_key', message: 'Private key in PEM format', validate: (val) => { return !!val; } }; prompt([question]) .then((ans) => { cb(ans.private_key.trim() + '\n\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Root CA in PEM format (optional):', (cb) => { resetTimeout(); if (opts.root_ca){ cb(opts.root_ca.trim() + '\n\n', startTimeout.bind(self, 15000)); } else { const questions = [ { type: 'confirm', name: 'provide_root_ca', message: 'Would you like to provide CA certificate?', default: true }, { type: 'editor', name: 'root_ca', message: 'CA certificate in PEM format', when: (answers) => { return answers.provide_root_ca; }, validate: (val) => { return !!val; }, default: '' } ]; prompt(questions) .then((ans) => { if (ans.root_ca === undefined){ ans.root_ca = ''; } cb(ans.root_ca.trim() + '\n\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Password:', (cb) => { resetTimeout(); // Skip password prompt as appropriate if (opts.password){ cb(opts.password + '\n', startTimeout.bind(self, 15000)); } else { const question = { type: 'input', name: 'password', message: !isEnterprise ? 'Wi-Fi Password' : 'Password', validate: (val) => { return !!val; } }; prompt([question]) .then((ans) => { cb(ans.password + '\n', startTimeout.bind(self, 15000)); }); } }); serialTrigger.addTrigger('Spark <3 you!', () => { resetTimeout(); resolve(); }); serialTrigger.addTrigger('Particle <3 you!', () => { resetTimeout(); resolve(); }); serialPort.open((err) => { if (err){ return reject(err); } serialTrigger.start(true); serialPort.write('w'); serialPort.drain(); // In case device is not in listening mode. startTimeout(5000, 'Serial timed out while initially listening to device, please ensure device is in listening mode with particle usb start-listening', 'InitialTimeoutError'); }); function serialClosedEarly(){ reject('Serial port closed early'); } function startTimeout(to, message = timeoutError, name = 'TimeoutError'){ self._serialTimeout = setTimeout(() => { reject(new VError({ name }, message)); }, to); } function resetTimeout(){ clearTimeout(self._serialTimeout); self._serialTimeout = null; } function parsesecurity(ent, cb){ resetTimeout(); if (opts.security){ let security = 3; if (opts.security.indexOf('WPA2') >= 0 && opts.security.indexOf('802.1x') >= 0){ security = 5; isEnterprise = true; } else if (opts.security.indexOf('WPA') >= 0 && opts.security.indexOf('802.1x') >= 0){ security = 4; isEnterprise = true; } else if (opts.security.indexOf('WPA2') >= 0){ security = 3; } else if (opts.security.indexOf('WPA') >= 0){ security = 2; } else if (opts.security.indexOf('WEP') >= 0){ security = 1; } else if (opts.security.indexOf('NONE') >= 0){ security = 0; } return cb(security + '\n', startTimeout.bind(self, 10000)); } const choices = [ { name: 'WPA2', value: 3 }, { name: 'WPA', value: 2 }, { name: 'WEP', value: 1 }, { name: 'Unsecured', value: 0 } ]; if (ent){ choices.push({ name: 'WPA Enterprise', value: 4 }); choices.push({ name: 'WPA2 Enterprise', value: 5 }); } const question = { type: 'list', name: 'security', message: 'Security Type', choices: choices }; prompt([question]) .then((ans) => { if (ans.security > 3){ isEnterprise = true; } cb(ans.security + '\n', startTimeout.bind(self, 10000)); }); } }); return promise.finally(cleanUpFn); } // TODO: If the comPort does not have an exact match with the device, // throw an error and return _parsePort(devices, comPort){ if (!comPort){ //they didn't give us anything. if (devices.length === 1){ //we have exactly one device, use that. return devices[0]; } //else - which one? } else { let portNum = parseInt(comPort); if (!isNaN(portNum)){ //they gave us a number if (portNum > 0){ portNum -= 1; } if (devices.length > portNum){ //we have it, use it. return devices[portNum]; } //else - which one? } else { const matchedDevices = devices.filter((d) => { return d.port === comPort; }); if (matchedDevices.length){ return matchedDevices[0]; } //they gave us a string //doesn't matter if we have it or not, give it a try. return { port: comPort, type: '' }; } } return null; } /** * Converts the specified comPort into a device object. If comPort is provided, * the device is obtained based on that port. If comPort is not specified, * the device is obtained from the selected port within the function. * * @param {string|null} comPort - Example port on Mac/Linux (/dev/tty.usbmodem123456) * @returns {object} - The resulting device object */ async whatSerialPortDidYouMean(comPort) { const devices = await this.findDevices(); const port = this._parsePort(devices, comPort); if (port) { if (!port.deviceId) { throw new Error('No serial port identified'); } return port; } if (!devices || devices.length === 0) { throw new Error('No serial port identified'); } const question = { name: 'port', type: 'list', message: 'Which device did you mean?', choices: devices.map((d) => { return { name: d.port + ' - ' + d.type + ' - ' + d.deviceId, value: d }; }) }; const answers = await prompt([question]); const portSelected = answers.port; if (!portSelected || (portSelected.type !== 'Tachyon' && !portSelected.deviceId)){ throw new Error('No serial port identified'); } return portSelected; } exit(){ this.ui.stdout.write(`${os.EOL}`); this.ui.stdout.write(arrow, chalk.bold.white('Ok, bye! Don\'t forget `' + chalk.bold.cyan(cmd + ' help') + '` if you\'re stuck!', chalk.bold.magenta('<3')) ); process.exit(0); } getDeviceInfoFromSerial(device){ return this._issueSerialCommand(device, 'i', IDENTIFY_COMMAND_TIMEOUT) .then((data) => { const matches = data.match(/Your (core|device) id is\s+(\w+)/); if (matches && matches.length === 3){ return matches[2]; } const electronMatches = data.match(/\s+([a-fA-F0-9]{24})\s+/); if (electronMatches && electronMatches.length === 2){ const info = { id: electronMatches[1] }; const imeiMatches = data.match(/IMEI: (\w+)/); if (imeiMatches){ info.imei = imeiMatches[1]; } const iccidMatches = data.match(/ICCID: (\w+)/); if (iccidMatches){ info.iccid = iccidMatches[1]; } return info; } }); } /** * Sends a command to the device and retrieves the response. * @param devicePort The device port to send the command to * @param command The command text * @param timeout How long in milliseconds to wait for a response * @returns {Promise} to send the command. * The serial port should not be open, and is closed after the command is sent. * @private */ _issueSerialCommand(device, command, timeout){ if (!device){ throw new VError('No serial port identified'); } const failDelay = timeout || 5000; let serialPort; return new Promise((resolve, reject) => { serialPort = new SerialPort({ path: device.port, ...SERIAL_PORT_DEFAULTS }); const parser = new SerialBatchParser({ timeout: 250 }); serialPort.pipe(parser); const failTimer = setTimeout(() => { reject(timeoutError); }, failDelay); parser.on('data', (data) => { clearTimeout(failTimer); resolve(data.toString()); }); serialPort.open((err) => { if (err){ console.error('Serial err: ' + err); console.error('Serial problems, please reconnect the device.'); reject('Serial problems, please reconnect the device.'); return; } serialPort.write(command, (werr) => { if (werr){ reject(err); } }); }); }) .finally(() => { if (serialPort){ serialPort.removeAllListeners('open'); if (serialPort.isOpen){ return new Promise((resolve) => { serialPort.close(resolve); }); } } }); } };