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

Add particle flash --tachyon #780

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/qdl/qdl
Binary file not shown.
12 changes: 11 additions & 1 deletion src/cli/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ module.exports = ({ commandProcessor, root }) => {
},
'port': {
describe: 'Use this serial port instead of auto-detecting. Useful if there are more than 1 connected device. Only available for serial'
},
'tachyon' : {
boolean: true,
description: 'Flash Tachyon'
},
'verbose' : {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to add verbose here. It's a global flag available to all commands already https://github.com/particle-iot/particle-cli/blob/master/src/app/cli.js#L42

boolean: false,
description: 'Enable logging'
}
},
handler: (args) => {
Expand All @@ -54,7 +62,9 @@ module.exports = ({ commandProcessor, root }) => {
'$0 $command --local application.bin': 'Flash the pre-compiled binary to the device connected over USB',
'$0 $command --local application.zip': 'Flash the pre-compiled binary and assets from the bundle to the device connected over USB',
'$0 $command --local tinker': 'Flash the default Tinker app to the device connected over USB',
'$0 $command --usb firmware.bin': 'Flash the binary over USB'
'$0 $command --usb firmware.bin': 'Flash the binary over USB',
'$0 $command --tachyon': 'Flash Tachyon from the files in the current directory. Use --verbose to see the progress',
'$0 $command --tachyon /path/to/unpackaged-tool-and-files': 'Flash Tachyon from the files in the specified directory. Use --verbose to see the progress',
},
epilogue: unindent(`
When passing the --local flag, Device OS will be updated if the version on the device is outdated.
Expand Down
133 changes: 124 additions & 9 deletions src/cmd/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ 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 { sourcePatterns, binaryPatterns, binaryExtensions, linuxExecPatterns } = require('../lib/file-types');
const deviceOsUtils = require('../lib/device-os-version-util');
const os = require('os');
const semver = require('semver');
const {
createFlashSteps,
Expand All @@ -25,6 +26,7 @@ const {
} = require('../lib/flash-helper');
const createApiCache = require('../lib/api-cache');
const { validateDFUSupport } = require('./device-util');
const execa = require('execa');

module.exports = class FlashCommand extends CLICommandBase {
constructor(...args) {
Expand All @@ -39,9 +41,11 @@ module.exports = class FlashCommand extends CLICommandBase {
target,
port,
yes,
verbose,
tachyon,
'application-only': applicationOnly
}) {
if (!device && !binary && !local) {
if (!tachyon && !device && !binary && !local) {
// if no device nor files are passed, show help
throw usageError('You must specify a device or a file');
}
Expand All @@ -55,11 +59,109 @@ module.exports = class FlashCommand extends CLICommandBase {
} 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({ verbose, files: allFiles });
} else {
await this.flashCloud({ device, files, target });
}
}

async flashTachyon({ verbose, files }) {
this.ui.write(`Ensure only one device is connected to a computer${os.EOL}`);

let unpackToolFolder;
if (files.length === 0) {
// If no files are passed, assume the current directory
unpackToolFolder = process.cwd();
files = await fs.readdir(unpackToolFolder);
} else if (files.length === 1) {
// If only one file is passed, check if it's a directory
const stats = await fs.stat(files[0]);
if (stats.isDirectory()) {
unpackToolFolder = files[0];
files = await fs.readdir(files[0]);
}
} else {
// If multiple files are passed, check the directory from the first file
unpackToolFolder = path.dirname(files[0]);
}

const parsedFiles = await this._analyzeFiles(files);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the implementation of _analyzeFiles, I'm not sure this call is useful

let linxuFiles = await this._findLinuxExecutableFiles(parsedFiles.files, { directory: unpackToolFolder });
linxuFiles = linxuFiles.map(f => path.basename(f));


const elfFiles = linxuFiles.filter(f => f.startsWith('prog') && f.endsWith('.elf'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the criteria to

Suggested change
const elfFiles = linxuFiles.filter(f => f.startsWith('prog') && f.endsWith('.elf'));
const elfFiles = linxuFiles.filter(f => f.includes('firehose') && f.endsWith('.elf'));

This matches https://www.notion.so/particle/PRD-Particle-CLI-Support-for-Flashing-Tachyon-157d40caa5dc805ab72fdabddf713381?pvs=4#158d40caa5dc80db8131ecd541a48d17

const rawProgramFiles = linxuFiles.filter(f => f.startsWith('rawprogram') && f.endsWith('.xml'));
const patchFiles = linxuFiles.filter(f => f.startsWith('patch') && f.endsWith('.xml'));

if (!elfFiles.length || !rawProgramFiles.length || !patchFiles.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think patch files are mandatory.

throw new Error('The directory should contain at least one .elf file, one rawprogram file, and one patch file');
}

const sortByNumber = (a, b) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary to sort the files, as long as the program files are before the patch files.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to process in the order given in the command. The original tools assume that the user correctly orders the raw program and patch files accordingly

Copy link
Contributor Author

@keeramis keeramis Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought these back (for time being) and kept the below changes as well (assuming they are important too)

const extractNumber = str => parseInt(str.match(/(\d+).xml/)[1]);
return extractNumber(a) - extractNumber(b);
};

rawProgramFiles.sort(sortByNumber);
patchFiles.sort(sortByNumber);

if (rawProgramFiles.length !== patchFiles.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary either

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think this is extra. What if a program doesn't have a patch file to go with it?

throw new Error('The number of rawprogram files should match the number of patch files');
}

let filesToProgram = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let filesToProgram = [];
const filesToProgram = [...rawProgramFiles, ...patchFiles];

// interleave the rawprogram files and patch files
for (let i = 0; i < rawProgramFiles.length; i++) {
filesToProgram.push(rawProgramFiles[i]);
filesToProgram.push(patchFiles[i]);
}

filesToProgram.unshift(elfFiles[0]);

this.ui.write(`Found the following files in the directory:${os.EOL}`);
this.ui.write('Loader file:');
this.ui.write(` - ${elfFiles[0]}${os.EOL}`);

this.ui.write('Program files:');
for (const file of rawProgramFiles) {
this.ui.write(` - ${file}`);
}
this.ui.write(os.EOL);

this.ui.write('Patch files:');
for (const file of patchFiles) {
this.ui.write(` - ${file}`);
}
this.ui.write(os.EOL);

const qdl = path.join(__dirname, '../../assets/qdl/qdl');
await fs.ensureFile(qdl);

this.ui.write(`Command: ${qdl} --storage ufs ${filesToProgram.join(' ')}${os.EOL}`);
this.ui.write(`Starting download. The download may take several minutes${os.EOL}`);

try {
const res = await execa(qdl, ['--storage', 'ufs', ...filesToProgram], {
cwd: unpackToolFolder,
stdio: verbose ? 'inherit' : 'pipe'
});
// put the output in a log file if not verbose
if (!verbose) {
const outputLog = path.join(process.cwd(), `qdl-output-${Date.now()}.log`);
await fs.writeFile(outputLog, res.stdout);
this.ui.write(`Download complete. Output log available at ${outputLog}${os.EOL}`);
} else {
this.ui.write(`Download complete${os.EOL}`);
}
} catch (err) {
throw new Error(`Download failed. Try powering up your board in EDL mode and try again.${os.EOL}Error: ${err.message}${os.EOL}`);
}
// TODO: Handle errors
}

async flashOverUsb({ binary, factory }) {
if (utilities.getFilenameExt(binary) === '.zip') {
throw new Error("Use 'particle flash --local' to flash a zipped bundle.");
Expand Down Expand Up @@ -293,24 +395,37 @@ module.exports = class FlashCommand extends CLICommandBase {
}

async _findBinaries(parsedFiles) {
const binaries = new Set();
for (const filePath of parsedFiles) {
return this._findFiles(parsedFiles, binaryPatterns);
}

async _findLinuxExecutableFiles(parsedFiles, { directory }) {

if (directory) {
const files = parsedFiles.map(f => path.join(directory, f));
return this._findFiles(files, linuxExecPatterns);
}
return this._findFiles(parsedFiles, linuxExecPatterns);
}

async _findFiles(files, patterns) {
const resFiles = new Set();
for (const filePath of files) {
try {
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
const found = utilities.globList(filePath, binaryPatterns);
for (const binary of found) {
binaries.add(binary);
const found = utilities.globList(filePath, patterns);
for (const file of found) {
resFiles.add(file);
}
} else {
binaries.add(filePath);
resFiles.add(filePath);
}
} catch (error) {
throw new Error(`I couldn't find that: ${filePath}`);
}

}
return Array.from(binaries);
return Array.from(resFiles);
}

async _processBundle({ filesToFlash }) {
Expand Down
9 changes: 8 additions & 1 deletion src/lib/file-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ const binaryExtensions = [
'.zip'
];

const linuxExecExtensions = [
'.elf',
'.xml'
];

const binaryPatterns = binaryExtensions.map(ext => `*${ext}`);
const linuxExecPatterns = linuxExecExtensions.map(ext => `*${ext}`);

module.exports = {
sourcePatterns,
binaryExtensions,
binaryPatterns
binaryPatterns,
linuxExecPatterns
};
Loading