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/tachyon flash config blob #791

Merged
merged 6 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all 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 modified assets/qdl/darwin/arm64/qdl
Binary file not shown.
Binary file modified assets/qdl/darwin/x64/liblzma.5.dylib
Binary file not shown.
Binary file modified assets/qdl/darwin/x64/qdl
Binary file not shown.
Binary file modified assets/qdl/linux/arm64/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.
8 changes: 4 additions & 4 deletions src/cmd/esim.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ module.exports = class ESimCommands extends CLICommandBase {
return;
}

// Start qlril-app through ADB for Tachyon
// Start particle-ril through ADB for Tachyon
const qlrilStep = await this._initializeQlril();
provisionOutputLogs.push(qlrilStep);
if (qlrilStep?.status === 'failed') {
Expand Down Expand Up @@ -275,7 +275,7 @@ module.exports = class ESimCommands extends CLICommandBase {

async doEnable(iccid) {
try {
const { stdout } = await execa('adb', ['shell', 'qlril-app', 'enable', iccid]);
const { stdout } = await execa('adb', ['shell', 'particle-ril', 'enable', iccid]);
if (stdout.includes(`ICCID currently active: ${iccid}`)) {
console.log(`ICCID ${iccid} enabled successfully`);
}
Expand Down Expand Up @@ -312,7 +312,7 @@ module.exports = class ESimCommands extends CLICommandBase {

async doList() {
try {
const { stdout } = await execa('adb', ['shell', 'qlril-app', 'listProfiles']);
const { stdout } = await execa('adb', ['shell', 'particle-ril', 'listProfiles']);

const iccids = stdout
.trim()
Expand Down Expand Up @@ -521,7 +521,7 @@ module.exports = class ESimCommands extends CLICommandBase {
}

logAndPush('Initalizing qlril app on Tachyon through adb');
this.adbProcess = execa('adb', ['shell', 'qlril-app', '--port', '/dev/ttyGS2']);
this.adbProcess = execa('adb', ['shell', 'particle-ril', '--port', '/dev/ttyGS2']);

try {
await new Promise((resolve, reject) => {
Expand Down
74 changes: 63 additions & 11 deletions src/cmd/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ module.exports = class FlashCommand extends CLICommandBase {
}

//returns true if successful or false if failed
async flashTachyon({ files, output }) {
async flashTachyon({ files, skipReset, output, verbose=true }) {
let zipFile;
let includeDir = '';
let updateFolder = '';
Expand Down Expand Up @@ -101,28 +101,65 @@ module.exports = class FlashCommand extends CLICommandBase {
filesToProgram = files;
}

if (output && !fs.existsSync(output)) {
fs.mkdirSync(output);
}
const outputLog = path.join(output ? output : process.cwd(), `tachyon_flash_${Date.now()}.log`);
const outputLog = await this._getOutputLogPath(output);

try {
this.ui.write(`Starting download. This may take several minutes. See logs at: ${outputLog}${os.EOL}`);
if (verbose) {
this.ui.write(`${os.EOL}Starting download. See logs at: ${outputLog}${os.EOL}`);
}
const qdl = new QdlFlasher({
files: filesToProgram,
includeDir,
updateFolder,
zip: zipFile,
ui: this.ui,
outputLogFile: outputLog
outputLogFile: outputLog,
skipReset,
currTask: 'OS'
});
await qdl.run();
fs.appendFileSync(outputLog, 'Download complete.');
return true;
fs.appendFileSync(outputLog, `OS Download complete.${os.EOL}`);
} catch (error) {
this.ui.write('Download failed');
fs.appendFileSync(outputLog, 'Download failed with error: ' + error.message);
return false;
throw new Error('Download failed with error: ' + error.message);
}
}

async flashTachyonXml({ files, output }) {
try {
const zipFile = files.find(f => f.endsWith('.zip'));
const xmlFile = files.find(f => f.endsWith('.xml'));

const firehoseFile = await this._getFirehoseFileFromZip(zipFile);
const qdl = new QdlFlasher({
files: [firehoseFile, xmlFile],
ui: this.ui,
outputLogFile: output,
currTask: 'Configuration file'
});

await qdl.run();
fs.appendFileSync(output, `Config file download complete.${os.EOL}`);
} 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 defaultLogFile = path.join(process.cwd(), `tachyon_flash_${Date.now()}.log`);
await fs.ensureFile(defaultLogFile);
return defaultLogFile;
}

async _extractFlashFilesFromDir(dirPath) {
Expand Down Expand Up @@ -176,6 +213,21 @@ module.exports = class FlashCommand extends CLICommandBase {
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,
Expand Down
60 changes: 48 additions & 12 deletions src/cmd/setup-tachyon.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const CloudCommand = require('./cloud');
const { sha512crypt } = require('sha512crypt-node');
const DownloadManager = require('../lib/download-manager');
const { platformForId } = require('../lib/platform');
const path = require('path');

module.exports = class SetupTachyonCommands extends CLICommandBase {
constructor({ ui } = {}) {
Expand Down Expand Up @@ -93,27 +94,41 @@ module.exports = class SetupTachyonCommands extends CLICommandBase {
);

let configBlobPath = loadConfig;
let configBlob = null;
if (configBlobPath) {
try {
const data = fs.readFileSync(configBlobPath, 'utf8');
configBlob = JSON.parse(data);
const res = await this._createConfigBlob({
registrationCode,
systemPassword: configBlob.system_password,
wifi: configBlob.wifi,
sshKey: configBlob.ssh_key
});
configBlobPath = res.path;
} catch (error) {
throw new Error(`The configuration file is not a valid JSON file: ${error.message}`);
}
this.ui.write(
`${os.EOL}${os.EOL}Skipping Step 6 - Using configuration file: ` + loadConfig + `${os.EOL}`
);
} else {
configBlobPath = await this._runStepWithTiming(
const res = await this._runStepWithTiming(
'Creating the configuration file to write to the Tachyon device...',
6,
() => this._createConfigBlob({ registrationCode, ...config })
);
configBlobPath = res.path;
configBlob = res.configBlob;
}

const xmlPath = await this._createXmlFile(configBlobPath);

if (saveConfig) {
this.ui.write(`${os.EOL}${os.EOL}Configuration file written here: ${saveConfig}${os.EOL}`);
fs.copyFileSync(configBlobPath, saveConfig);
fs.writeFileSync(saveConfig, JSON.stringify(configBlob, null, 2));
this.ui.write(`${os.EOL}Configuration file written here: ${saveConfig}${os.EOL}`);
}

//what files to flash?
const filesToFlash = skipFlashingOs ? [xmlPath] : [packagePath, xmlPath];

const flashSuccessful = await this._runStepWithTiming(
`Okay—last step! We're now flashing the device with the configuration, including the password, Wi-Fi settings, and operating system.${os.EOL}` +
`Heads up: this is a large image and will take around 10 minutes to complete. Don't worry—we'll show a progress bar as we go!${os.EOL}${os.EOL}` +
Expand All @@ -125,7 +140,10 @@ module.exports = class SetupTachyonCommands extends CLICommandBase {
` - When the light starts flashing yellow, release the button.${os.EOL}` +
' Your device is now in flashing mode!',
7,
() => this._flash(filesToFlash)
() => this._flash({
files: [packagePath, xmlPath],
skipFlashingOs
})
);

if (flashSuccessful) {
Expand Down Expand Up @@ -455,6 +473,9 @@ Welcome to the Particle Tachyon setup! This interactive command:
return 'You need to provide a path to your SSH public key';
}
return true;
},
filter: (value) => {
return value.startsWith('~') ? value.replace('~', os.homedir()) : value;
}
},
];
Expand Down Expand Up @@ -485,7 +506,7 @@ Welcome to the Particle Tachyon setup! This interactive command:

// Write config JSON to a temporary file (generate a filename with the temp npm module)
// prefixed by the JSON string length as a 32 bit integer
let jsonString = JSON.stringify(config);
let jsonString = JSON.stringify(config, null, 2);
const buffer = Buffer.alloc(4 + Buffer.byteLength(jsonString));
buffer.writeUInt32BE(Buffer.byteLength(jsonString), 0);
buffer.write(jsonString, 4);
Expand All @@ -494,7 +515,7 @@ Welcome to the Particle Tachyon setup! This interactive command:
fs.writeSync(tempFile.fd, buffer);
fs.closeSync(tempFile.fd);

return tempFile.path;
return { path: tempFile.path, configBlob: config };
}

_generateShadowCompatibleHash(password) {
Expand Down Expand Up @@ -524,13 +545,16 @@ Welcome to the Particle Tachyon setup! This interactive command:
].join(os.EOL);

// Create a temporary file for the XML content
const tempFile = temp.openSync();
const tempFile = temp.openSync({ prefix: 'config', suffix: '.xml' });
fs.writeSync(tempFile.fd, xmlContent, 0, xmlContent.length, 0);
fs.closeSync(tempFile.fd);
return tempFile.path;
}

async _flash(files) {
async _flash({ files, skipFlashingOs, output }) {

const packagePath = files[0];

const question = {
Copy link
Member

Choose a reason for hiding this comment

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

Will this question be asked if --load_config is passed? Remember the goal of load config is to have a setup without a single question. Andrew will use this in the Tachyon CI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will still ask the question. Let me fix it.

Copy link
Contributor Author

@keeramis keeramis Feb 4, 2025

Choose a reason for hiding this comment

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

I removed this. There is another question about product. Is this something we want to put in the config file as well?

type: 'confirm',
name: 'flash',
Expand All @@ -540,7 +564,19 @@ Welcome to the Particle Tachyon setup! This interactive command:
await this.ui.prompt(question);

const flashCommand = new FlashCommand();
return await flashCommand.flashTachyon({ files });

if (output && !fs.existsSync(output)) {
fs.mkdirSync(output);
}
const outputLog = path.join(process.cwd(), `tachyon_flash_${Date.now()}.log`);
fs.ensureFileSync(outputLog);

this.ui.write(`${os.EOL}Starting download. See logs at: ${outputLog}${os.EOL}`);
if (!skipFlashingOs) {
await flashCommand.flashTachyon({ files: [packagePath], skipReset: true, output: outputLog, verbose: false });
}
await flashCommand.flashTachyonXml({ files, output: outputLog });
return true;
}

_particleApi() {
Expand Down
15 changes: 11 additions & 4 deletions src/lib/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const mkdirTemp = util.promisify(temp.mkdir);
const TACHYON_STORAGE_TYPE = 'ufs';

class QdlFlasher {
constructor({ files, includeDir, updateFolder, zip, ui, outputLogFile }) {
constructor({ files, includeDir, updateFolder, zip, ui, outputLogFile, skipReset=false, currTask=null }) {
this.files = files;
this.includeDir = includeDir;
this.updateFolder = updateFolder;
Expand All @@ -24,17 +24,20 @@ class QdlFlasher {
this.currentModuleSectors = 0;
this.progressBarInitialized = false;
this.preparingDownload = false;
this.skipReset = skipReset;
this.currTask = currTask;
}

async run() {
let qdlProcess;
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`);

const qdlProcess = execa(qdlPath, qdlArguments, {
qdlProcess = execa(qdlPath, qdlArguments, {
cwd: this.updateFolder || process.cwd(),
stdio: 'pipe'
});
Expand All @@ -55,6 +58,9 @@ class QdlFlasher {
if (this.progressBarInitialized) {
this.progressBar.stop();
}
if (qdlProcess && qdlProcess.kill) {
qdlProcess.kill();
}
}
}

Expand All @@ -78,7 +84,8 @@ class QdlFlasher {
'--storage', TACHYON_STORAGE_TYPE,
...(zip ? ['--zip', zip] : []),
...(includeDir ? ['--include', includeDir] : []),
...files
...files,
...(this.skipReset ? ['--skip-reset'] : [])
];
}

Expand Down Expand Up @@ -154,7 +161,7 @@ class QdlFlasher {
}

if (this.totalSectorsFlashed === this.totalSectorsInAllFiles) {
this.progressBar.update({ description: 'Flashing complete' });
this.progressBar.update({ description: `Flashing complete ${this.currTask ? this.currTask : ''}` });
}
}
}
Expand Down