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 more esim commands for Tachyon - enable, delete, and list #787

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
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;
};

181 changes: 159 additions & 22 deletions src/cmd/esim.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ 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('Enable command is only for Tachyon devices');
}
this.isTachyon = true;
return device;
}

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

async deleteCommand(iccid) {
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 +272,140 @@ module.exports = class ESimCommands extends CLICommandBase {
}
}

_validateArgs(args) {
if (!args) {
throw new Error('Missing args');
async doEnable(iccid) {
const TACHYON_QLRIL_WAIT_TIMEOUT = 20000;
let output = '';

try {
this.adbProcess = execa('adb', ['shell', 'qlril-app', 'enable', iccid]);

await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for qlril app to start'));
}, TACHYON_QLRIL_WAIT_TIMEOUT);

this.adbProcess.stdout.on('data', (data) => {
output += data.toString();
if (output.includes(`ICCID currently active: ${iccid}`)) {
console.log(`ICCID ${iccid} enabled successfully`);
clearTimeout(timeout);
resolve();
}
});

this.adbProcess.catch((error) => {
clearTimeout(timeout);
reject(new Error(`ADB process error: ${error.message}`));
});

this.adbProcess.then(() => {
clearTimeout(timeout);
reject(new Error('ADB process ended early without valid output'));
});
});

console.log(os.EOL);
} catch (error) {
console.error(`Failed to enable profiles: ${error.message}`);
} finally {
this._exitQlril();
}
}

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;

await this._initializeQlril();

for (const [key, errorMessage] of Object.entries(requiredArgs)) {
if (!args[key]) {
throw new Error(errorMessage);
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;
}

await execa(this.lpa, ['disable', iccid, `--serial=${port}`]);
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() {
const TACHYON_QLRIL_WAIT_TIMEOUT = 10000;
let output = '';

try {
this.adbProcess = execa('adb', ['shell', 'qlril-app', 'listProfiles']);

await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for qlril app to start'));
}, TACHYON_QLRIL_WAIT_TIMEOUT);

this.inputJson = args.input;
this.inputJsonData = JSON.parse(fs.readFileSync(this.inputJson));
this.adbProcess.stdout.on('data', (data) => {
output += data.toString();

this.outputFolder = args.output || 'esim_loading_logs';
const iccids = output
.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}`));
clearTimeout(timeout);
resolve();
}
});

this.adbProcess.catch((error) => {
clearTimeout(timeout);
reject(new Error(`ADB process error: ${error.message}`));
});

this.adbProcess.then(() => {
clearTimeout(timeout);
reject(new Error('ADB process ended early without valid output'));
});
});

console.log(os.EOL);
} catch (error) {
console.error(`Failed to list profiles: ${error.message}`);
} finally {
this._exitQlril();
}
}


_validateArgs(args) {
if (!args?.lpa) {
throw new Error('Missing LPA tool path');
}

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;
}


Expand All @@ -288,15 +421,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 +712,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
Loading