diff --git a/src/cmd/flash.js b/src/cmd/flash.js index 3792fd970..ea8453d71 100644 --- a/src/cmd/flash.js +++ b/src/cmd/flash.js @@ -16,7 +16,14 @@ const { knownAppNames, knownAppsForPlatform } = require('../lib/known-apps'); const { sourcePatterns, binaryPatterns, binaryExtensions } = require('../lib/file-types'); const deviceOsUtils = require('../lib/device-os-version-util'); const semver = require('semver'); -const { createFlashSteps, filterModulesToFlash, parseModulesToFlash, flashFiles, validateDFUSupport } = require('../lib/flash-helper'); +const { + createFlashSteps, + filterModulesToFlash, + parseModulesToFlash, + flashFiles, + validateDFUSupport, + getFileFlashInfo +} = require('../lib/flash-helper'); const createApiCache = require('../lib/api-cache'); module.exports = class FlashCommand extends CLICommandBase { @@ -63,7 +70,8 @@ module.exports = class FlashCommand extends CLICommandBase { } const { api, auth } = this._particleApi(); - device = await usbUtils.getOneUsbDevice({ api, auth, ui: this.ui }); + const { flashMode, platformId } = await getFileFlashInfo(binary); + device = await usbUtils.getOneUsbDevice({ api, auth, ui: this.ui, flashMode, platformId }); const platformName = platformForId(device.platformId).name; validateDFUSupport({ device, ui: this.ui }); @@ -92,6 +100,8 @@ module.exports = class FlashCommand extends CLICommandBase { this.ui.write(`Flashing ${platformName} device ${device.id}`); const resetAfterFlash = !factory && modulesToFlash[0].prefixInfo.moduleFunction === ModuleInfo.FunctionType.USER_PART; await flashFiles({ device, flashSteps, resetAfterFlash, ui: this.ui }); + } catch (error) { + throw error; } finally { if (device && device.isOpen) { await device.close(); diff --git a/src/cmd/usb-util.js b/src/cmd/usb-util.js index 9dc063043..ccf59440d 100644 --- a/src/cmd/usb-util.js +++ b/src/cmd/usb-util.js @@ -178,39 +178,64 @@ async function getUsbDevices({ dfuMode = false } = {}){ } } -async function getOneUsbDevice({ idOrName, api, auth, ui }) { +async function getOneUsbDevice({ idOrName, api, auth, ui, flashMode, platformId }) { + let usbDevice; + const normalModes = ['NORMAL', 'LISTENING', '']; + const dfuModes = ['DFU']; if (idOrName) { - return openUsbDeviceByIdOrName(idOrName, api, auth, { dfuMode: true }); + const device = await openUsbDeviceByIdOrName(idOrName, api, auth, { dfuMode: true }); + await checkFlashMode({ flashMode, device }); } const usbDevices = await getUsbDevices({ dfuMode: true }); + let devices = await Promise.all(usbDevices.map(async (d) => { + const { id, mode } = await _getDeviceInfo(d); + const name = await _getDeviceName({ id, api, auth, ui }); + return { + id, + name: `${name} [${id}] (${(platformForId(d._info.id)).displayName}${mode ? ', ' + mode : '' })`, + platformId: d._info.id, + mode, + value: d + }; + })); - let usbDevice; - if (usbDevices.length > 1) { + devices = devices.sort((d1, d2) => d1.id.localeCompare(d2.id)); + + if (flashMode === 'DFU') { + devices = devices.filter(d => dfuModes.includes(d.mode)); + } + if (flashMode === 'NORMAL') { + devices = devices.filter(d => normalModes.includes(d.mode)); + } + if (platformId) { + devices = devices.filter(d => d.platformId === platformId); + } + + if (devices.length > 1) { const question = { type: 'list', name: 'device', message: 'Which device would you like to select?', choices() { - return Promise.all(usbDevices.map(async (d) => { - const { id, mode } = await _getDeviceInfo(d); - const name = await _getDeviceName({ id, api, auth, ui }); - return { - name: `${name} [${id}] (${platformForId(d._info.id).displayName}${mode ? ', ' + mode : '' })`, - value: d - }; - })); + return devices; } }; const nonInteractiveError = 'Multiple devices found. Connect only one device when running in non-interactive mode.'; const ans = await ui.prompt([question], { nonInteractiveError }); usbDevice = ans.device; - } else if (usbDevices.length === 1) { - usbDevice = usbDevices[0]; + } else if (!devices.length) { + if (flashMode === 'DFU') { + ui.logDFUModeRequired(); + } else { + ui.logNormalModeRequired(); + } + throw new Error('No devices found'); } else { - throw new NotFoundError('No device found'); + usbDevice = devices[0].value; } + try { await usbDevice.open(); return usbDevice; @@ -219,6 +244,25 @@ async function getOneUsbDevice({ idOrName, api, auth, ui }) { } } +async function checkFlashMode({ flashMode, device, ui }){ + switch (flashMode) { + case 'DFU': + if (!device.isInDfuMode) { + ui.logDFUModeRequired(); + throw new Error('Put the device in DFU mode and try again'); + } + break; + case 'NORMAL': + if (device.isInDfuMode) { + ui.logNormalModeRequired(); + throw new Error('Put the device in Normal mode and try again'); + } + break; + default: + break; + } +} + async function reopenInDfuMode(device) { const { id } = device; const start = Date.now(); diff --git a/src/lib/flash-helper.js b/src/lib/flash-helper.js index 663a5fd9c..23e300489 100644 --- a/src/lib/flash-helper.js +++ b/src/lib/flash-helper.js @@ -1,10 +1,11 @@ const _ = require('lodash'); const usbUtils = require('../cmd/usb-util'); const { delay } = require('./utilities'); -const { PLATFORMS } =require('./platform'); +const { PLATFORMS, platformForId } =require('./platform'); const { moduleTypeToString, sortBinariesByDependency } = require('./dependency-walker'); const { HalModuleParser: ModuleParser, ModuleInfo } = require('binary-version-reader'); const path = require('path'); +const fs = require('fs-extra'); const os = require('os'); const semver = require('semver'); @@ -198,6 +199,25 @@ async function parseModulesToFlash({ files }) { })); } +async function getFileFlashInfo(file) { + // verify if exist the file in other case could be a knownApp + // we will check the file in flashSteps + if (!await fs.pathExists(file)) { + return { flashMode: 'DFU' }; + } + const normalModules = ['assets', 'bootloader']; + const parser = new ModuleParser(); + const binary = await parser.parseFile(file); + const moduleType = moduleTypeToString(binary.prefixInfo.moduleFunction); + const moduleDefinition = PLATFORMS.find(p => p.id === binary.prefixInfo.platformID).firmwareModules + .find(firmwareModule => firmwareModule.type === moduleType); + + return { + flashMode: normalModules.includes(moduleType) || moduleDefinition.storage === 'externalMcu' ? 'NORMAL' : 'DFU', + platformId: binary.prefixInfo.platformID + }; +} + async function createFlashSteps({ modules, isInDfuMode, factory, platformId }) { const platform = PLATFORMS.find(p => p.id === platformId); const sortedModules = await sortBinariesByDependency(modules); @@ -259,42 +279,20 @@ async function createFlashSteps({ modules, isInDfuMode, factory, platformId }) { } function validateDFUSupport({ device, ui }) { - if (!device.isInDfuMode && (!semver.valid(device.firmwareVersion) || semver.lt(device.firmwareVersion, '2.0.0'))) { - const { chalk } = ui; - ui.write(`${chalk.red('!!!')} The device needs to be in DFU mode for this command.\n`); - ui.write(`${chalk.cyan('>')} This version of Device OS doesn't support automatically switching to DFU mode.`); - ui.write(`${chalk.cyan('>')} To put your device in DFU manually, please:\n`); - ui.write([ - chalk.bold.white('1)'), - 'Press and hold both the', - chalk.bold.cyan('RESET'), - 'and', - chalk.bold.cyan('MODE/SETUP'), - 'buttons simultaneously.\n' - ].join(' ')); - ui.write([ - chalk.bold.white('2)'), - 'Release only the', - chalk.bold.cyan('RESET'), - 'button while continuing to hold the', - chalk.bold.cyan('MODE/SETUP'), - 'button.\n' - ].join(' ')); - ui.write([ - chalk.bold.white('3)'), - 'Release the', - chalk.bold.cyan('MODE/SETUP'), - 'button once the device begins to blink yellow.\n' - ].join(' ')); + const platform = platformForId(device.platformId); + if (!device.isInDfuMode && (!semver.valid(device.firmwareVersion) || semver.lt(device.firmwareVersion, '2.0.0')) && platform.generation === 2) { + ui.logDFUModeRequired({ showVersionWarning: true }); throw new Error('Put the device in DFU mode and try again'); } } + module.exports = { flashFiles, filterModulesToFlash, parseModulesToFlash, createFlashSteps, prepareDeviceForFlash, - validateDFUSupport + validateDFUSupport, + getFileFlashInfo, }; diff --git a/src/lib/flash-helper.test.js b/src/lib/flash-helper.test.js index 739482724..f1c155bad 100644 --- a/src/lib/flash-helper.test.js +++ b/src/lib/flash-helper.test.js @@ -2,7 +2,17 @@ const { expect, sinon } = require('../../test/setup'); const { HalModuleParser, firmwareTestHelper, ModuleInfo, createAssetModule } = require('binary-version-reader'); const chalk = require('chalk'); const usbUtils = require('../cmd/usb-util'); -const { createFlashSteps, filterModulesToFlash, prepareDeviceForFlash, validateDFUSupport } = require('./flash-helper'); +const { + createFlashSteps, + filterModulesToFlash, + prepareDeviceForFlash, + validateDFUSupport, + getFileFlashInfo +} = require('./flash-helper'); +const { PATH_TMP_DIR } = require('../../test/lib/env'); +const path = require('path'); +const fs = require('fs-extra'); +const { ensureDir } = require('fs-extra/lib/mkdirs'); describe('flash-helper', () => { const createModules = async () => { @@ -458,14 +468,16 @@ describe('flash-helper', () => { beforeEach(() => { ui = { write: sinon.stub(), - chalk + chalk, + logDFUModeRequired: sinon.stub(), + logNormalModeRequired: sinon.stub() }; }); it('throws an error if the device os version does not support DFU', async () => { let error; const device = { isInDfuMode: false, - platformId: 32, + platformId: 6, firmwareVersion: '1.0.0', }; try { @@ -473,20 +485,22 @@ describe('flash-helper', () => { } catch (e) { error = e; } - expect(error).to.have.property('message', 'Put the device in DFU mode and try again'); + expect(ui.logDFUModeRequired).to.be.called; + expect(error.message).to.equal('Put the device in DFU mode and try again'); }); it('throws an error if the current device os is not defined and the device is not in DFU', async () => { let error; const device = { isInDfuMode: false, - platformId: 32, + platformId: 6, }; try { await validateDFUSupport({ device, ui }); } catch (e) { error = e; } - expect(error).to.have.property('message', 'Put the device in DFU mode and try again'); + expect(ui.logDFUModeRequired).to.be.called; + expect(error.message).to.equal('Put the device in DFU mode and try again'); }); it('passes if the device is in DFU mode', async () => { let error; @@ -516,4 +530,52 @@ describe('flash-helper', () => { expect(error).to.be.undefined; }); }); + + describe('getFileFlashInfo', () => { + const createBinary = async (moduleFunction, platformId) => { + const tempPath = 'flash-mode/binaries'; + const fileName = 'my-binary.bin'; + const binary = firmwareTestHelper.createFirmwareBinary({ + platformId: platformId, + moduleFunction: moduleFunction, + }); + // save binary + const filePath = path.join(PATH_TMP_DIR, tempPath); + const file = path.join(filePath, fileName); + await ensureDir(filePath); + await fs.writeFile(file, binary); + return file; + }; + + afterEach(async () => { + await fs.remove(path.join(PATH_TMP_DIR, 'flash-mode/binaries')); + }); + + it('returns dfu for known apps', async() => { + const fileName = 'tinker'; + const mode = await getFileFlashInfo(fileName); + expect(mode).to.deep.equal({ flashMode: 'DFU' }); + }); + it('returns dfu for system parts', async () => { + const p2PlatformId = 32; + const file = await createBinary(ModuleInfo.FunctionType.SYSTEM_PART, p2PlatformId); + const mode = await getFileFlashInfo(file); + expect(mode).to.deep.equal({ flashMode: 'DFU', platformId: 32 }); + }); + + it('returns normal for bootloader', async() => { + const p2PlatformId = 32; + const file = await createBinary(ModuleInfo.FunctionType.BOOTLOADER, p2PlatformId); + const mode = await getFileFlashInfo(file); + expect(mode).to.deep.equal({ flashMode: 'NORMAL', platformId: 32 }); + }); + + it ('returns normal for ncp', async() => { + const trackerPlatformId = 26; + const file = await createBinary(ModuleInfo.FunctionType.NCP_FIRMWARE, trackerPlatformId); + const mode = await getFileFlashInfo(file); + expect(mode).to.deep.equal({ flashMode: 'NORMAL', platformId: 26 }); + }); + + }); }); diff --git a/src/lib/ui/index.js b/src/lib/ui/index.js index dedf72775..a7e01f79d 100644 --- a/src/lib/ui/index.js +++ b/src/lib/ui/index.js @@ -79,6 +79,46 @@ module.exports = class UI { settings.override(settings.profile, 'flashWarningShownOn', Date.now()); } + logDFUModeRequired({ showVersionWarning } = {}) { + this.write(`${this.chalk.red('!!!')} The device needs to be in DFU mode for this command.\n`); + if (showVersionWarning ) { + this.write(`${this.chalk.cyan('>')} This version of Device OS doesn't support automatically switching to DFU mode.`); + } + this.write(`${this.chalk.cyan('>')} To put your device in DFU manually, please:\n`); + this.write([ + this.chalk.bold.white('1)'), + 'Press and hold both the', + this.chalk.bold.cyan('RESET'), + 'and', + this.chalk.bold.cyan('MODE/SETUP'), + 'buttons simultaneously.\n' + ].join(' ')); + this.write([ + this.chalk.bold.white('2)'), + 'Release only the', + this.chalk.bold.cyan('RESET'), + 'button while continuing to hold the', + this.chalk.bold.cyan('MODE/SETUP'), + 'button.\n' + ].join(' ')); + this.write([ + this.chalk.bold.white('3)'), + 'Release the', + this.chalk.bold.cyan('MODE/SETUP'), + 'button once the device begins to blink yellow.\n' + ].join(' ')); + } + + logNormalModeRequired() { + this.write(`${this.chalk.red('!!!')} The device needs to be in Normal mode for this command.\n`); + this.write(`${this.chalk.cyan('>')} To put your device in Normal mode manually, please:\n`); + this.write([ + 'Press the', + this.chalk.bold.cyan('RESET'), + 'button and Release it' + ].join(' ')); + } + logDeviceDetail(devices, { varsOnly = false, fnsOnly = false } = {}){ const { EOL, chalk } = this; const deviceList = Array.isArray(devices) ? devices : [devices];