UNPKG

particle-cli

Version:

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

615 lines (540 loc) 19.7 kB
const fs = require('fs-extra'); const ParticleApi = require('./api'); const { ModuleInfo } = require('binary-version-reader'); const { errors: { usageError } } = require('../app/command-processor'); const usbUtils = require('./usb-util'); const CLICommandBase = require('./base'); const { platformForId } = require('../lib/platform'); const settings = require('../../settings'); const path = require('path'); const utilities = require('../lib/utilities'); const CloudCommand = require('./cloud'); const BundleCommand = require('./bundle'); const temp = require('temp').track(); const { knownAppNames, knownAppsForPlatform } = require('../lib/known-apps'); const { sourcePatterns, binaryPatterns, binaryExtensions } = require('../lib/file-types'); const deviceOsUtils = require('../lib/device-os-version-util'); const os = require('os'); const semver = require('semver'); const { createFlashSteps, filterModulesToFlash, parseModulesToFlash, maintainDeviceProtection, flashFiles, getFileFlashInfo } = require('../lib/flash-helper'); const createApiCache = require('../lib/api-cache'); const { validateDFUSupport } = require('./device-util'); const unzip = require('unzipper'); const QdlFlasher = require('../lib/qdl'); const { getEDLDevice, addLogHeaders, addLogFooter, addManifestInfoLog } = require('../lib/tachyon-utils'); const TACHYON_MANIFEST_FILE = 'manifest.json'; module.exports = class FlashCommand extends CLICommandBase { constructor(...args) { super(...args); } async flash(device, binary, files, { local, usb, serial, factory, target, port, yes, tachyon, output, 'skip-reset': skipReset, 'application-only': applicationOnly }) { if (!tachyon && !device && !binary && !local) { // if no device nor files are passed, show help throw usageError('You must specify a device or a file'); } this.ui.logFirstTimeFlashWarning(); if (usb) { await this.flashOverUsb({ binary, factory }); } else if (serial) { await this.flashSerialDeprecated({ binary, port, yes }); } else if (local) { let allFiles = binary ? [binary, ...files] : files; await this.flashLocal({ files: allFiles, applicationOnly, target }); } else if (tachyon) { let allFiles = binary ? [binary, ...files] : files; await this.flashTachyon({ files: allFiles, skipReset, output }); } else { await this.flashCloud({ device, files, target }); } } //returns true if successful or false if failed async flashTachyon({ device, files, skipReset, output, verbose=true }) { let zipFile; let includeDir = ''; let updateFolder = ''; if (files.length === 0) { // If no files are passed, use the current directory files = ['.']; } const [input, ...rest] = files; const stats = await fs.stat(input); let filesToProgram; let manifestInfo; if (stats.isDirectory()) { updateFolder = input; const dirInfo = await this._extractFlashFilesFromDir(input); includeDir = dirInfo.baseDir; filesToProgram = dirInfo.filesToProgram.map((file) => path.join(includeDir, file)); manifestInfo = dirInfo.manifest; } else if (utilities.getFilenameExt(input) === '.zip') { updateFolder = path.dirname(input); zipFile = path.basename(input); const zipInfo = await this._extractFlashFilesFromZip(input); includeDir = zipInfo.baseDir; filesToProgram = zipInfo.filesToProgram.map((file) => path.join(includeDir, file)); filesToProgram.push(...rest); manifestInfo = zipInfo.manifest; } else { filesToProgram = files; } const outputLog = await this._getOutputLogPath(output); try { if (verbose) { this.ui.write(`${os.EOL}Starting download. See logs at: ${outputLog}${os.EOL}`); } const startTime = new Date(); if (!device) { device = await getEDLDevice({ ui: this.ui }); } addLogHeaders({ outputLog, startTime, deviceId: device.id, commandName: 'Tachyon Flash' }); addManifestInfoLog({ outputLog, manifest: manifestInfo }); const qdl = new QdlFlasher({ files: filesToProgram, includeDir, updateFolder, zip: zipFile, ui: this.ui, outputLogFile: outputLog, skipReset, currTask: 'OS', serialNumber: device.serialNumber }); await qdl.run(); fs.appendFileSync(outputLog, `OS Download complete.${os.EOL}`); addLogFooter({ outputLog, startTime, endTime: new Date() }); } catch (error) { fs.appendFileSync(outputLog, error.message); throw error; } } async flashTachyonXml({ device, files, skipReset, output }) { try { const zipFile = files.find(f => f.endsWith('.zip')); const xmlFile = files.find(f => f.endsWith('.xml')); if (!device) { device = await getEDLDevice({ ui: this.ui }); } const firehoseFile = await this._getFirehoseFileFromZip(zipFile); // add log headers const startTime = new Date(); addLogHeaders({ outputLog: output, startTime, deviceId: device.id, commandName: 'Tachyon Flash XML' }); const qdl = new QdlFlasher({ files: [firehoseFile, xmlFile], ui: this.ui, outputLogFile: output, skipReset, currTask: 'Configuration file', serialNumber: device.serialNumber }); await qdl.run(); fs.appendFileSync(output, `Config file download complete.${os.EOL}`); // add log footer addLogFooter({ outputLog: output, startTime, endTime: new Date() }); } catch (error) { fs.appendFileSync(output, 'Download failed with error: ' + error.message); throw new Error('Download failed with error: ' + error.message); } } async _getOutputLogPath(output) { if (output) { const stats = await fs.stat(output); if (stats.isDirectory()) { const logFile = path.join(output, `tachyon_flash_${Date.now()}.log`); await fs.ensureFile(logFile); return logFile; } return output; } const particleDir = settings.ensureFolder(); const logsDir = path.join(particleDir, 'logs'); await fs.ensureDir(logsDir); const defaultLogFile = path.join(logsDir, `tachyon_flash_${Date.now()}.log`); await fs.ensureFile(defaultLogFile); return defaultLogFile; } async _extractFlashFilesFromDir(dirPath) { const manifestPath = path.join(dirPath, TACHYON_MANIFEST_FILE); if (!fs.existsSync(manifestPath)) { throw new Error(`Unable to find ${TACHYON_MANIFEST_FILE}${os.EOL}`); } const data = await this._loadManifestFromFile(manifestPath); const parsed = this._parseManfiestData(data); const baseDir = path.normalize(parsed.base); const filesToProgram = [ parsed.firehose, ...parsed.programXml, ...parsed.patchXml ]; return { baseDir, filesToProgram, manifest: data }; } async _extractFlashFilesFromZip(zipPath) { if (!fs.existsSync(zipPath)) { throw new Error(`Unable to find ${zipPath}${os.EOL}`); } const data = await this._loadManifestFromZip(zipPath); const parsed = this._parseManfiestData(data); const baseDir = path.normalize(parsed.base); const filesToProgram = [ parsed.firehose, ...parsed.programXml, ...parsed.patchXml ]; return { baseDir, filesToProgram, manifest: data }; } async _loadManifestFromFile(filePath) { const manifestFile = await fs.readFile(filePath, 'utf8'); return JSON.parse(manifestFile); } async _loadManifestFromZip(zipPath) { const dir = await unzip.Open.file(zipPath); const manifestFile = dir.files.find(file => file.path === TACHYON_MANIFEST_FILE); if (!manifestFile) { throw new Error(`Unable to find ${TACHYON_MANIFEST_FILE}${os.EOL}`); } const manifest = await manifestFile.buffer(); return JSON.parse(manifest.toString()); } async _getFirehoseFileFromZip(zipPath) { const dir = await unzip.Open.file(zipPath); const { filesToProgram } = await this._extractFlashFilesFromZip(zipPath); const firehoseFile = dir.files.find(file => file.path.endsWith(filesToProgram[0])); if (!firehoseFile) { throw new Error('Unable to find firehose file'); } const buffer = await firehoseFile.buffer(); const tempFile = temp.openSync({ prefix: 'firehose_', suffix: '.elf' }); fs.writeSync(tempFile.fd, buffer); fs.closeSync(tempFile.fd); return tempFile.path; } _parseManfiestData(data) { return { base: data?.targets[0]?.qcm6490?.edl?.base, firehose: data?.targets[0]?.qcm6490?.edl?.firehose, programXml: data?.targets[0]?.qcm6490?.edl?.program_xml, patchXml: data?.targets[0]?.qcm6490?.edl?.patch_xml }; } async flashOverUsb({ binary, factory }) { if (utilities.getFilenameExt(binary) === '.zip') { throw new Error("Use 'particle flash --local' to flash a zipped bundle."); } const { api, auth } = this._particleApi(); const { flashMode, platformId } = await getFileFlashInfo(binary); await usbUtils.executeWithUsbDevice({ args: { api, auth, ui: this.ui, flashMode, platformId }, func: (dev) => this._flashOverUsb(dev, binary, factory) }); } async _flashOverUsb(device, binary, factory) { const platformName = platformForId(device.platformId).name; validateDFUSupport({ device, ui: this.ui }); let files; const knownAppPath = knownAppsForPlatform(platformName)[binary]; if (knownAppPath) { files = [knownAppPath]; } else { files = [binary]; } const modulesToFlash = await parseModulesToFlash({ files }); await this._validateModulesForPlatform({ modules: modulesToFlash, platformId: device.platformId, platformName }); await maintainDeviceProtection({ modules: modulesToFlash, device }); const flashSteps = await createFlashSteps({ modules: modulesToFlash, isInDfuMode: device.isInDfuMode, platformId: device.platformId, factory }); 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 }); } flashCloud({ device, files, target }) { // We don't check for Device Protection here // because it will not matter for cloud flashing // These are rejected for Protected Devices even if the device is in Service Mode const CloudCommands = require('../cmd/cloud'); const args = { target, params: { device, files } }; return new CloudCommands().flashDevice(args); } flashSerialDeprecated({ binary, port, yes }) { const SerialCommands = require('../cmd/serial'); return new SerialCommands().flashDevice(binary, { port, yes }); } async flashLocal({ files, applicationOnly, target, verbose=true }) { const { files: parsedFiles, deviceIdOrName, knownApp } = await this._analyzeFiles(files); const { api, auth } = this._particleApi(); await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceIdOrName, api, auth, ui: this.ui }, func: (dev) => this._flashLocal(dev, parsedFiles, deviceIdOrName, knownApp, applicationOnly, target, verbose) }); } async _flashLocal(device, parsedFiles, deviceIdOrName, knownApp, applicationOnly, target, verbose=true) { const platformId = device.platformId; const platformName = platformForId(platformId).name; const currentDeviceOsVersion = device.firmwareVersion; if (verbose) { this.ui.write(`Flashing ${platformName} ${deviceIdOrName || device.id}`); } validateDFUSupport({ device, ui: this.ui }); let { skipDeviceOSFlash, files: filesToFlash } = await this._prepareFilesToFlash({ knownApp, parsedFiles, platformId, platformName, target }); filesToFlash = await this._processBundle({ filesToFlash }); const fileModules = await parseModulesToFlash({ files: filesToFlash }); await this._validateModulesForPlatform({ modules: fileModules, platformId, platformName }); const deviceOsBinaries = await this._getDeviceOsBinaries({ currentDeviceOsVersion, skipDeviceOSFlash, target, modules: fileModules, platformId, applicationOnly, verbose }); const deviceOsModules = await parseModulesToFlash({ files: deviceOsBinaries }); let modulesToFlash = [...fileModules, ...deviceOsModules]; modulesToFlash = filterModulesToFlash({ modules: modulesToFlash, platformId }); await maintainDeviceProtection({ modules: modulesToFlash, device }); const flashSteps = await createFlashSteps({ modules: modulesToFlash, isInDfuMode: device.isInDfuMode, platformId }); await flashFiles({ device, flashSteps, ui: this.ui, verbose }); } async _analyzeFiles(files) { const apps = knownAppNames(); // assume the user wants to compile/flash the current directory if no argument is passed if (files.length === 0) { return { files: ['.'], deviceIdOrName: null, knownApp: null }; } // check if the first argument is a known app const [knownApp] = files; if (apps.includes(knownApp)) { return { files: [], deviceIdOrName: null, knownApp }; } // check if the second argument is a known app if (files.length > 1) { const [deviceIdOrName, knownApp] = files; if (apps.includes(knownApp)) { return { files: [], deviceIdOrName, knownApp }; } } // check if the first argument exists in the filesystem, regardless if it's a file or directory try { await fs.stat(files[0]); return { files, deviceIdOrName: null, knownApp: null }; } catch (error) { // file doesn't exist, assume the first argument is a device const [deviceIdOrName, ...remainingFiles] = files; return { files: remainingFiles, deviceIdOrName, knownApp: null }; } } // Should be part fo CLICommandBase?? _particleApi() { const auth = settings.access_token; const api = new ParticleApi(settings.apiUrl, { accessToken: auth } ); const apiCache = createApiCache(api); return { api: apiCache, auth }; } async _prepareFilesToFlash({ knownApp, parsedFiles, platformId, platformName, target }) { if (knownApp) { const knownAppPath = knownAppsForPlatform(platformName)[knownApp]; if (knownAppPath) { return { skipDeviceOSFlash: true, files: [knownAppPath] }; } else { throw new Error(`Known app ${knownApp} is not available for ${platformName}`); } } const [filePath] = parsedFiles; let stats; try { stats = await fs.stat(filePath); } catch (error) { // ignore error } // if a directory, figure out if it's a source directory that should be compiled // or a binary directory that should be flashed directly if (stats && stats.isDirectory()) { const binaries = utilities.globList(filePath, binaryPatterns); const sources = utilities.globList(filePath, sourcePatterns); if (binaries.length > 0 && sources.length === 0) { // this is a binary directory so get all the binaries from all the parsedFiles const binaries = await this._findBinaries(parsedFiles); return { skipDeviceOSFlash: false, files: binaries }; } else if (sources.length > 0) { // this is a source directory so compile it const compileResult = await this._compileCode({ parsedFiles, platformId, target }); return { skipDeviceOSFlash: false, files: compileResult }; } else { throw new Error('No files found to flash'); } } else { // this is a file so figure out if it's a source file that should be compiled or a // binary that should be flashed directly if (binaryExtensions.includes(path.extname(filePath))) { const binaries = await this._findBinaries(parsedFiles); return { skipDeviceOSFlash: false, files: binaries }; } else { const compileResult = await this._compileCode({ parsedFiles, platformId, target }); return { skipDeviceOSFlash: false, files: compileResult }; } } } async _compileCode({ parsedFiles, platformId, target }) { const cloudCommand = new CloudCommand(); const saveTo = temp.path({ suffix: '.zip' }); // compileCodeImpl will pick between .bin and .zip as appropriate const { filename } = await cloudCommand.compileCodeImpl({ target, saveTo, platformId, files: parsedFiles }); return [filename]; } async _findBinaries(parsedFiles) { const binaries = new Set(); for (const filePath of parsedFiles) { try { const stats = await fs.stat(filePath); if (stats.isDirectory()) { const found = utilities.globList(filePath, binaryPatterns); for (const binary of found) { binaries.add(binary); } } else { binaries.add(filePath); } } catch (error) { throw new Error(`I couldn't find that: ${filePath}`); } } return Array.from(binaries); } async _processBundle({ filesToFlash }) { const bundle = new BundleCommand(); const processed = await Promise.all(filesToFlash.map(async (filename) => { if (path.extname(filename) === '.zip') { return bundle.extractModulesFromBundle({ bundleFilename: filename }); } else { return filename; } })); return processed.flat(); } async _validateModulesForPlatform({ modules, platformId, platformName }) { for (const moduleInfo of modules) { if (!moduleInfo.crc.ok) { throw new Error(`CRC check failed for module ${moduleInfo.filename}`); } if (moduleInfo.prefixInfo.platformID !== platformId && moduleInfo.prefixInfo.moduleFunction !== ModuleInfo.FunctionType.ASSET) { throw new Error(`Module ${moduleInfo.filename} is not compatible with platform ${platformName}`); } } } async _getDeviceOsBinaries({ skipDeviceOSFlash, target, modules, currentDeviceOsVersion, platformId, applicationOnly, verbose=true }) { const { api } = this._particleApi(); const { module: application, applicationDeviceOsVersion } = await this._pickApplicationBinary(modules, api); // if files to flash include Device OS binaries, don't override them with the ones from the cloud const includedDeviceOsModuleFunctions = [ModuleInfo.FunctionType.SYSTEM_PART, ModuleInfo.FunctionType.BOOTLOADER]; const systemPartBinaries = modules.filter(m => includedDeviceOsModuleFunctions.includes(m.prefixInfo.moduleFunction)); if (systemPartBinaries.length) { return []; } // no application so no need to download Device OS binaries if (!application) { return []; } // need to get the binary required version if (applicationOnly) { return []; } // force to flash device os binaries if target is specified if (target) { return deviceOsUtils.downloadDeviceOsVersionBinaries({ api, platformId, version: target, ui: this.ui, omitUserPart: true, verbose }); } // avoid downgrading Device OS for known application like Tinker compiled against older Device OS if (skipDeviceOSFlash) { return []; } // if Device OS needs to be upgraded, so download the binaries if (applicationDeviceOsVersion && currentDeviceOsVersion && semver.lt(currentDeviceOsVersion, applicationDeviceOsVersion)) { return deviceOsUtils.downloadDeviceOsVersionBinaries({ api: api, platformId, version: applicationDeviceOsVersion, ui: this.ui, verbose }); } else { // Device OS is up to date or we don't know the current Device OS version, so no need to download binaries return []; } } async _pickApplicationBinary(modules, api) { for (const module of modules) { // parse file and look for moduleFunction if (module.prefixInfo.moduleFunction === ModuleInfo.FunctionType.USER_PART) { const internalVersion = module.prefixInfo.depModuleVersion; let applicationDeviceOsVersionData = { version: null }; try { applicationDeviceOsVersionData = await api.getDeviceOsVersions(module.prefixInfo.platformID, internalVersion); } catch (error) { // ignore if Device OS version from the application cannot be identified } return { module, applicationDeviceOsVersion: applicationDeviceOsVersionData.version }; } } return { module: null, applicationDeviceOsVersion: null }; } };