Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sc 122867/make particle usb flash be aware of device #681

14 changes: 12 additions & 2 deletions src/cmd/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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();
Expand Down
74 changes: 59 additions & 15 deletions src/cmd/usb-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
56 changes: 27 additions & 29 deletions src/lib/flash-helper.js
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
};
74 changes: 68 additions & 6 deletions src/lib/flash-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -458,35 +468,39 @@ 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 {
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('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;
Expand Down Expand Up @@ -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 });
});

});
});
40 changes: 40 additions & 0 deletions src/lib/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down