Skip to content

Commit

Permalink
Merge pull request #787 from particle-iot/feature/esim-enable-command
Browse files Browse the repository at this point in the history
Add more esim commands for Tachyon - enable, delete, and list
  • Loading branch information
monkbroc authored Jan 22, 2025
2 parents e2a1b14 + 7798a9a commit be7cbd4
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 29 deletions.
37 changes: 37 additions & 0 deletions src/cli/esim.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ module.exports = ({ commandProcessor, root }) => {
TBD TBD
`)
});

commandProcessor.createCommand(esim, 'enable', '(Only for Tachyon) Enables a downloaded eSIM profile', {
params: '<iccid>',
handler: (args) => {
const ESimCommands = require('../cmd/esim');
return new ESimCommands().enableCommand(args.params.iccid);
},
examples: {
'$0 $command': 'TBD'
}
});

commandProcessor.createCommand(esim, 'delete', '(Only for Tachyon) Deletes an eSIM profile', {
options: Object.assign({
'lpa': {
description: 'Provide the LPA tool path'
},
}),
params: '<iccid>',
handler: (args) => {
const ESimCommands = require('../cmd/esim');
return new ESimCommands().deleteCommand(args, args.params.iccid);
},
examples: {
'$0 $command': 'TBD'
}
});

commandProcessor.createCommand(esim, 'list', '(Only for Tachyon) Lists all the profiles on the eSIM', {
handler: (args) => {
const ESimCommands = require('../cmd/esim');
return new ESimCommands().listCommand(args);
},
examples: {
'$0 $command': 'TBD'
}
});
return esim;
};

134 changes: 110 additions & 24 deletions src/cmd/esim.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ module.exports = class ESimCommands extends CLICommandBase {
this.isTachyon = true;
}

this._validateArgs(args);
this._validateArgs(args, { lpa: true, input: true, output: true, binary: !this.isTachyon });
await this._generateAvailableProvisioningData();

await this.doProvision(device);
}

async bulkProvisionCommand(args) {
console.log(chalk.red(`Do not use bulk mode for Tachyon${os.EOL}`));
this._validateArgs(args);
this._validateArgs(args, { lpa: true, input: true, output: true, binary: true });

await this._generateAvailableProvisioningData();

Expand All @@ -78,6 +78,33 @@ module.exports = class ESimCommands extends CLICommandBase {
console.log('Ready to bulk provision. Connect devices to start. Press Ctrl-C to exit.');
}

async _checkForTachyonDevice() {
console.log(chalk.bold(`Ensure only one device is connected${os.EOL}`));
this.verbose = true;
const device = await this.serial.whatSerialPortDidYouMean();
if (device.type !== 'Tachyon') {
throw new Error('This command is only for Tachyon devices');
}
this.isTachyon = true;
return device;
}

async enableCommand(iccid) {
await this._checkForTachyonDevice();
await this.doEnable(iccid);
}

async deleteCommand(args, iccid) {
this._validateArgs(args, { lpa: true });
const device = await this._checkForTachyonDevice();
await this.doDelete(device, iccid);
}

async listCommand() {
await this._checkForTachyonDevice();
await this.doList();
}

// Populate the availableProvisioningData set with the indices of the input JSON data
// If a profile is already provisioned (output JSON file exists with an entry), remove it from the set
async _generateAvailableProvisioningData() {
Expand Down Expand Up @@ -246,33 +273,88 @@ module.exports = class ESimCommands extends CLICommandBase {
}
}

_validateArgs(args) {
if (!args) {
throw new Error('Missing args');
async doEnable(iccid) {
try {
const { stdout } = await execa('adb', ['shell', 'qlril-app', 'enable', iccid]);
if (stdout.includes(`ICCID currently active: ${iccid}`)) {
console.log(`ICCID ${iccid} enabled successfully`);
}
} catch (error) {
console.error(`Failed to enable profiles: ${error.message}`);
}
}

const requiredArgs = {
input: 'Missing input JSON file',
lpa: 'Missing LPA tool path',
...(this.isTachyon ? {} : { binary: 'Missing folder path to binaries' })
};
async doDelete(device, iccid) {
try {
const port = device.port;

for (const [key, errorMessage] of Object.entries(requiredArgs)) {
if (!args[key]) {
throw new Error(errorMessage);
await this._initializeQlril();

const iccidsOnDevice = await this._getIccidOnDevice(port);
if (!iccidsOnDevice.includes(iccid)) {
console.log(`ICCID ${iccid} not found on the device or is a test ICCID`);
return;
}
try {
await execa(this.lpa, ['disable', iccid, `--serial=${port}`]);
} catch (error) {
// Ignore the error if the profile is already disabled
}
await execa(this.lpa, ['delete', iccid, `--serial=${port}`]);

console.log('Profile deleted successfully');
} catch (error) {
console.error(`Failed to delete profile: ${error.message}`);
} finally {
this._exitQlril();
}
}

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

const iccids = stdout
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '')
.split(',')
.map(iccid => iccid.trim())
.filter(Boolean);

if (iccids.length > 0) {
console.log(`Profiles found:${os.EOL}`);
iccids.forEach(iccid => console.log(`\t- ${iccid}`));
}
} catch (error) {
console.error(`Failed to list profiles: ${error.message}`);
}
}

this.inputJson = args.input;
this.inputJsonData = JSON.parse(fs.readFileSync(this.inputJson));

this.outputFolder = args.output || 'esim_loading_logs';
_validateArgs(args, required) {
this.lpa = args?.lpa;
this.inputJson = args?.input;
if (this.inputJson) {
try {
this.inputJsonData = JSON.parse(fs.readFileSync(this.inputJson));
} catch (error) {
throw new Error(`Invalid JSON in input file: ${error.message}`);
}
}

this.outputFolder = args?.output || 'esim_loading_logs';
if (!fs.existsSync(this.outputFolder)) {
fs.mkdirSync(this.outputFolder);
}

this.lpa = args.lpa;
this.binaries = args.binary;
this.binaries = args?.binary;

for (const key in required) {
if (required[key] && !args[key]) {
throw new Error(`Missing required argument: ${key}`);
}
}
}


Expand All @@ -288,15 +370,13 @@ module.exports = class ESimCommands extends CLICommandBase {
}
};

const profilesOnDeviceAfterDownload = await this._listProfiles(port);
const iccidsOnDeviceAfterDownload = profilesOnDeviceAfterDownload.map((line) => line.split('[')[1].split(',')[0].trim());

const iccidsOnDevice = await this._getIccidOnDevice(port);
// remove test ICCIDs from iccidsOnDeviceAfterDownload
const iccidsOnDeviceAfterDownloadFiltered = iccidsOnDeviceAfterDownload.filter((iccid) => !TEST_ICCID.includes(iccid));
const iccidsOnDeviceNotTest = iccidsOnDevice.filter((iccid) => !TEST_ICCID.includes(iccid));

const equal = _.isEqual(_.sortBy(expectedIccids), _.sortBy(iccidsOnDeviceAfterDownloadFiltered));
const equal = _.isEqual(_.sortBy(expectedIccids), _.sortBy(iccidsOnDeviceNotTest));

res.details.iccidsOnDevice = iccidsOnDeviceAfterDownload;
res.details.iccidsOnDevice = iccidsOnDevice;
res.details.rawLogs.push(equal ? ['Profiles on device match the expected profiles'] :
['Profiles on device do not match the expected profiles']);
res.status = equal ? 'success' : 'failed';
Expand Down Expand Up @@ -581,6 +661,12 @@ module.exports = class ESimCommands extends CLICommandBase {
return profilesList;
}

async _getIccidOnDevice(port) {
const profiles = await this._listProfiles(port);
const iccids = profiles.map((line) => line.split('[')[1].split(',')[0].trim());
return iccids;
}

// Get the next available profile from availableProvisioningData
// Once a profile is fetched, remove it from the set so other devices don't get the same profile
_getProfiles() {
Expand Down
9 changes: 5 additions & 4 deletions src/lib/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ class QdlFlasher {
}

processFlashingLogs(line) {
if (!this.preparingDownload) {
this.preparingDownload = true;
this.ui.stdout.write('Preparing for download...');
}

if (line.includes('status=getProgramInfo')) {
this.handleProgramInfo(line);
} else if (line.includes('status=Start flashing module')) {
Expand All @@ -110,10 +115,6 @@ class QdlFlasher {
}

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);
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/help.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Help & Unknown Command / Argument Handling', () => {
'cloud', 'compile', 'config', 'device add', 'device remove',
'device rename', 'device doctor', 'device',
'device-protection status', 'device-protection disable', 'device-protection enable', 'device-protection',
'doctor', 'esim provision', 'esim', 'flash',
'doctor', 'esim provision', 'esim enable', 'esim delete', 'esim list', 'esim', 'flash',
'function list', 'function call', 'function', 'get', 'identify',
'keys new', 'keys load', 'keys save', 'keys send', 'keys doctor',
'keys server', 'keys address', 'keys', 'library add',
Expand Down

0 comments on commit be7cbd4

Please sign in to comment.