Skip to content

Commit

Permalink
Merge pull request #785 from particle-iot/feature/show-progress-bar--…
Browse files Browse the repository at this point in the history
…flash-tachyon

Show progress bar when flashing Tachyon
  • Loading branch information
keeramis authored Jan 16, 2025
2 parents c3853ae + d0063e2 commit 07ac1e4
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 58 deletions.
Binary file modified assets/qdl/darwin/arm64/qdl
Binary file not shown.
Binary file modified assets/qdl/darwin/x64/qdl
Binary file not shown.
Binary file modified assets/qdl/linux/x64/qdl
Binary file not shown.
Binary file modified assets/qdl/win32/x64/qdl.exe
Binary file not shown.
6 changes: 5 additions & 1 deletion src/cli/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ module.exports = ({ commandProcessor, root }) => {
'tachyon' : {
boolean: true,
description: 'Flash Tachyon'
}
},
'output': {
describe: 'Folder to output the log file. Only available for Tachyon'
},
},
handler: (args) => {
const FlashCommand = require('../cmd/flash');
Expand All @@ -61,6 +64,7 @@ module.exports = ({ commandProcessor, root }) => {
'$0 $command --usb firmware.bin': 'Flash the binary over USB',
'$0 $command --tachyon': 'Flash Tachyon from the files in the current directory',
'$0 $command --tachyon /path/to/unpackaged-tool-and-files': 'Flash Tachyon from the files in the specified directory',
'$0 $command --tachyon /path/to/package.zip --output /path/to/log-folder': 'Flash Tachyon using the specified zip file and save the log to the given folder',
},
epilogue: unindent(`
When passing the --local flag, Device OS will be updated if the version on the device is outdated.
Expand Down
50 changes: 25 additions & 25 deletions src/cmd/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
const createApiCache = require('../lib/api-cache');
const { validateDFUSupport } = require('./device-util');
const unzip = require('unzipper');
const qdl = require('../lib/qdl');
const QdlFlasher = require('../lib/qdl');

const TACHYON_MANIFEST_FILE = 'manifest.json';

Expand All @@ -44,8 +44,8 @@ module.exports = class FlashCommand extends CLICommandBase {
target,
port,
yes,
verbose,
tachyon,
output,
'application-only': applicationOnly
}) {
if (!tachyon && !device && !binary && !local) {
Expand All @@ -64,14 +64,14 @@ module.exports = class FlashCommand extends CLICommandBase {
await this.flashLocal({ files: allFiles, applicationOnly, target });
} else if (tachyon) {
let allFiles = binary ? [binary, ...files] : files;
await this.flashTachyon({ verbose, files: allFiles });
await this.flashTachyon({ files: allFiles, output });
} else {
await this.flashCloud({ device, files, target });
}
}

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

let zipFile;
let includeDir = '';
Expand Down Expand Up @@ -101,27 +101,27 @@ module.exports = class FlashCommand extends CLICommandBase {
filesToProgram = files;
}

this.ui.write(`Starting download. The download may take several minutes...${os.EOL}`);

const res = await qdl.run({
files: filesToProgram,
includeDir,
updateFolder,
zip: zipFile,
verbose,
ui: this.ui
});
// put the output in a log file if not verbose
if (!verbose) {
const outputLog = path.join(process.cwd(), `qdl-output-${Date.now()}.log`);
if (res?.stdout) {
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}`);
this.ui.write(`Starting download. This may take several minutes...${os.EOL}`);
if (output && !fs.existsSync(output)) {
fs.mkdirSync(output);
}
const outputLog = path.join(output ? output : process.cwd(), `tachyon_flash_${Date.now()}.log`);
try {
this.ui.write(`Logs are being written to: ${outputLog}${os.EOL}`);
const qdl = new QdlFlasher({
files: filesToProgram,
includeDir,
updateFolder,
zip: zipFile,
ui: this.ui,
outputLogFile: outputLog
});
await qdl.run();
fs.appendFileSync(outputLog, 'Download complete.');
} catch (error) {
this.ui.write('Download failed');
fs.appendFileSync(outputLog, 'Download failed with error: ' + error.message);
}
// TODO: Handle errors
}

async _extractFlashFilesFromDir(dirPath) {
Expand Down
175 changes: 143 additions & 32 deletions src/lib/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,157 @@ const mkdirTemp = util.promisify(temp.mkdir);

const TACHYON_STORAGE_TYPE = 'ufs';

async function getExecutable() {
const archType = utilities.getArchType();
const archName = utilities.getOs();
const qdlDir = path.join(__dirname, `../../assets/qdl/${archName}/${archType}`);
if (!await fs.pathExists(qdlDir)) {
throw new Error('Flashing Tachyon is not suppported on your OS');
class QdlFlasher {
constructor({ files, includeDir, updateFolder, zip, ui, outputLogFile }) {
this.files = files;
this.includeDir = includeDir;
this.updateFolder = updateFolder;
this.zip = zip;
this.ui = ui;
this.outputLogFile = outputLogFile;
this.progressBar = null;
this.totalSectorsInAllFiles = 0;
this.totalSectorsFlashed = 0;
this.currentModuleName = '';
this.currentModuleSectors = 0;
this.progressBarInitialized = false;
this.preparingDownload = false;
}

// Copy qdl to a temporary directory, so it can run outside the pkg snapshot
const tmpDir = await mkdirTemp('qdl');
await fs.copy(qdlDir, tmpDir);
async run() {
try {
const qdlPath = await this.getExecutable();
const qdlArguments = this.buildArgs({ files: this.files, includeDir: this.includeDir, zip: this.zip });
this.progressBar = this.ui.createProgressBar();
const command = `${qdlPath} ${qdlArguments.join(' ')}`;
fs.appendFileSync(this.outputLogFile, `Command: ${command}\n`);

return path.join(tmpDir, 'qdl' + (archName === 'win32' ? '.exe' : ''));
}
const qdlProcess = execa(qdlPath, qdlArguments, {
cwd: this.updateFolder || process.cwd(),
stdio: 'pipe'
});

const handleStream = (stream) => {
stream.on('data', chunk => {
chunk.toString().split('\n').map(line => line.trim()).filter(Boolean).forEach(line => {
this.processLogLine(line, qdlProcess);
});
});
};

handleStream(qdlProcess.stdout);
handleStream(qdlProcess.stderr);

await qdlProcess;
} finally {
if (this.progressBarInitialized) {
this.progressBar.stop();
}
}
}

async getExecutable() {
const archType = utilities.getArchType();
const archName = utilities.getOs();
const qdlDir = path.join(__dirname, `../../assets/qdl/${archName}/${archType}`);
if (!await fs.pathExists(qdlDir)) {
throw new Error('Flashing Tachyon is not suppported on your OS');
}

/**
*/
async function run({ files, includeDir, updateFolder, zip, verbose, ui }) {
const qdl = await getExecutable();
// Copy qdl to a temporary directory, so it can run outside the pkg snapshot
const tmpDir = await mkdirTemp('qdl');
await fs.copy(qdlDir, tmpDir);

const qdlArgs = [
'--storage',
TACHYON_STORAGE_TYPE,
...(zip ? ['--zip', zip] : []),
...(includeDir ? ['--include', includeDir] : []),
...files
];
return path.join(tmpDir, 'qdl' + (archName === 'win32' ? '.exe' : ''));
}

buildArgs({ files, includeDir, zip }) {
return [
'--storage', TACHYON_STORAGE_TYPE,
...(zip ? ['--zip', zip] : []),
...(includeDir ? ['--include', includeDir] : []),
...files
];
}

if (verbose) {
ui.write(`Command: ${qdl} ${qdlArgs.join(' ')}${os.EOL}`);
processLogLine(line, process) {
fs.appendFileSync(this.outputLogFile, `${line}\n`);

if (line.includes('Waiting for EDL device')) {
this.handleError(process, `Ensure your device is connected and in EDL mode${os.EOL}`);
} else if (line.includes('[ERROR]')) {
this.handleError(process, `${os.EOL}Error detected: ${line}${os.EOL}`);
} else {
this.processFlashingLogs(line);
}
}

const res = await execa(qdl, qdlArgs, {
cwd: updateFolder || process.cwd(),
stdio: verbose ? 'inherit' : 'pipe'
});
handleError(process, message) {
this.ui.stdout.write(message);
process.kill();
}

processFlashingLogs(line) {
if (line.includes('status=getProgramInfo')) {
this.handleProgramInfo(line);
} else if (line.includes('status=Start flashing module')) {
this.handleModuleStart(line);
} else if (line.includes('status=Flashing module')) {
this.handleModuleProgress(line);
}
}

handleProgramInfo(line) {
if (!this.preparingDownload) {
this.preparingDownload = true;
this.ui.stdout.write('Preparing to download files...');
}
const match = line.match(/sectors_total=(\d+)/);
if (match) {
this.totalSectorsInAllFiles += parseInt(match[1], 10);
}
}

handleModuleStart(line) {
const moduleNameMatch = line.match(/module=(.*?),/);
const sectorsTotalMatch = line.match(/sectors_total=(\d+)/);
if (moduleNameMatch && sectorsTotalMatch) {
this.currentModuleName = moduleNameMatch[1];
this.currentModuleSectors = parseInt(sectorsTotalMatch[1], 10);

if (!this.progressBarInitialized) {
this.progressBarInitialized = true;
this.progressBar.start(this.totalSectorsInAllFiles, this.totalSectorsFlashed, {
description: `Flashing ${this.currentModuleName}`
});
} else {
this.progressBar.update(this.totalSectorsFlashed, {
description: `Flashing ${this.currentModuleName}`
});
}
}
}

handleModuleProgress(line) {
const sectorsFlashedMatch = line.match(/sectors_done=(\d+)/);
if (sectorsFlashedMatch) {
const sectorsFlashed = parseInt(sectorsFlashedMatch[1], 10);
this.progressBar.update(this.totalSectorsFlashed + sectorsFlashed, {
description: `Flashing module: ${this.currentModuleName} (${sectorsFlashed}/${this.currentModuleSectors} sectors)`
});

if (sectorsFlashed === this.currentModuleSectors) {
this.totalSectorsFlashed += this.currentModuleSectors;
this.progressBar.update({ description: `Flashed ${this.currentModuleName}` });
}

if (this.totalSectorsFlashed === this.totalSectorsInAllFiles) {
this.progressBar.update({ description: 'Flashing complete' });
}
}
}

return res;
}

module.exports = {
run
};

module.exports = QdlFlasher;

0 comments on commit 07ac1e4

Please sign in to comment.