Skip to content

Commit

Permalink
Add esim provision command
Browse files Browse the repository at this point in the history
  • Loading branch information
keeramis committed Nov 19, 2024
1 parent ec1bb51 commit c8ee5a3
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 2 deletions.
43 changes: 43 additions & 0 deletions src/cli/esim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const unindent = require('../lib/unindent');

module.exports = ({ commandProcessor, root }) => {
const esim = commandProcessor.createCategory(root, 'esim', 'Download eSIM profiles (INTERNAL ONLY)');

commandProcessor.createCommand(esim, 'provision', 'Provisions eSIM profiles on a device', {
options: Object.assign({
'lpa': {
description: 'Provide the LPA tool path'
},
'input': {
description: 'Provide the input json file path'
},
'output': {
description: 'Provide the output json file path'
},
'bulk': {
description: 'Provision multiple devices'
}
}),
handler: (args) => {
const eSimCommands = require('../cmd/esim');
if (args.bulk) {
return new eSimCommands().bulkProvision(args);
} else {
return new eSimCommands().provision(args);
}
},
examples: {
'$0 $command': 'TBD'
},
epilogue: unindent(`
The JSON file should look like this:
{
"TBD": "TBD"
}
TBD TBD
`)
});
return esim;
};

2 changes: 2 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const bundle = require('./bundle');
const cloud = require('./cloud');
const config = require('./config');
const doctor = require('./doctor');
const esim = require('./esim');
const protection = require('./device-protection');
const flash = require('./flash');
const func = require('./function');
Expand Down Expand Up @@ -50,6 +51,7 @@ module.exports = function registerAllCommands(context) {
cloud(context);
config(context);
doctor(context);
esim(context);
protection(context);
flash(context);
func(context);
Expand Down
191 changes: 191 additions & 0 deletions src/cmd/esim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
const spinnerMixin = require('../lib/spinner-mixin');
const usbUtils = require('../cmd/usb-util');
const fs = require('fs-extra');
const utilities = require('../lib/utilities');
const os = require('os');
const { platformForId } = require('../lib/platform');
const CLICommandBase = require('./base');
const execa = require('execa');
const SerialCommand = require('./serial');
const FlashCommand = require('./flash');
const path = require('path');

// TODO: Get these from exports
const PATH_TO_PASS_THROUGH_BINARIES = '/Users/keerthyamisagadda/code/kigen-resources/binaries';


module.exports = class eSimCommands extends CLICommandBase {
constructor() { // TODO: Bring ui class
super();
spinnerMixin(this);
this.serial = new SerialCommand();
this.lpa = null;
this.inputJson = null;
this.outputJson = null;
}

async provision(args) {
this._validateArgs(args);

const port = await this._getSerialPortForSingleDevice();

// get the device usb handle from the serial port
const device = await this.serial.whatSerialPortDidYouMean(port);
const platform = platformForId(device.specs.productId).name;

console.log(`${os.EOL}Provisioning device ${device.deviceId} with platform ${platform}`);

await this._flashATPassThroughFirmware(device, platform, port);

// This assumes that the input JSON does not have a mapping between EID and profiles
// So obtain the EID from the device and put it in outupt JSON
const eid = await this._getEid(port);
console.log(`${os.EOL}EID: ${eid}`);

await this._checkForExistingProfiles(port);

// Parse the JSON to get EID and profiles
const input = fs.readFileSync(inputJson);
const profilesJson = JSON.parse(input);

// Work only with one device
const first = profilesJson.EIDs[0];
const profiles = first.profiles;
if (profiles.length === 0) {
throw new Error('No profiles to provision in the input json');
}

console.log(`${os.EOL}Provisioning the following profiles: `);
let cnt = 0;
for (const profile of profiles) {
cnt++;
const smdp = profile.smdp;
const matchingId = profile.matching_id;
const rspUrl = `1\$${smdp}\$${matchingId}`;
console.log(`\t${cnt}. ${profile.provider} (${rspUrl})`);
}

// Download each profile
for (const profile of profiles) {
let iccid = null;
const smdp = profile.smdp;
const matchingId = profile.matching_id;
const rspUrl = `1\$${smdp}\$${matchingId}`;
console.log(`${os.EOL}Downloading ${profile.provider} profile from ${rspUrl}`);

const start = Date.now();

const res = await execa(this.lpa, ['download', rspUrl, `--serial=${port}`]);

const end = Date.now();
const timeTaken = (end - start) / 1000;

const output = res.stdout;
if (output.includes('Profile successfully downloaded')) {
console.log(`${os.EOL}Profile successfully downloaded in ${timeTaken} sec`);
const iccidLine = output.split('\n').find((line) => line.includes('Profile with ICCID'));
// FIXME: Get the string after the "ICCID" string
iccid = iccidLine.split(' ')[4];
} else {
console.log(`${os.EOL}Profile download failed`);
}

const outputData = {
EID: eid,
provider: profile.provider,
iccid: iccid,
time: timeTaken,
output: output
};

await fs.writeJson(outputJson, outputLogs);
}
}

_validateArgs(args) {
if (!args) {
throw new Error('Missing args');
}
if (!args.input) {
throw new Error('Missing input json file');
}
if (!args.output) {
throw new Error('Missing input output json file');
}
if (!args.lpa) {
throw new Error('Missing input LPA tool path');
}
this.inputJson = args.input;
this.outputJson = args.output;
this.lpa = args.lpa;
}

async _getSerialPortForSingleDevice() {
const deviceSerialPorts = await usbUtils.getUsbSystemPathsForMac();
if (deviceSerialPorts.length > 1) {
throw new Error('Multiple devices found. Please unplug all but one device or use --bulk option');
}
return deviceSerialPorts[0];
}

async _flashATPassThroughFirmware(device, platform, port) {
// Obtain the pre-compiled firmware binary from local folder
// TODO: Do this with binary inspect for more reliability
const fwBinaries = fs.readdirSync(PATH_TO_PASS_THROUGH_BINARIES);
const validBin = fwBinaries.filter((file) => file.endsWith(`${platform}.bin`));
const fwPath = path.join(PATH_TO_PASS_THROUGH_BINARIES, validBin[0]);

// Flash the AT passthrough firmware
console.log(`${os.EOL}Flashing AT passthrough firmware ${fwPath} to the device`);
const flashCmdInstance = new FlashCommand();
await flashCmdInstance.flashLocal({ files: [fwPath], applicationOnly: true, verbose: true });
console.log(`${os.EOL}AT passthrough firmware flashed`);

const deviceResponded = await usbUtils.waitForDeviceToRespond(device.deviceId);
if (!deviceResponded) {
throw new Error('Device did not respond after flashing AT passthrough firmware');
}
console.log(`${os.EOL}Device responded after flashing AT passthrough firmware`);
await deviceResponded.close();

// FIXME: Use the firmware that does not have initial logs and then remove this block
console.log(`${os.EOL}Wait for the initial logs to clear up`);
const monitor = await this.serial.monitorPort({ port, follow: false });
await utilities.delay(30000);
await monitor.stop();
await utilities.delay(5000);
console.log(`${os.EOL}Initial logs likely cleared`);
}

async _getEid(port) {
console.log(`${os.EOL}Getting EID from the device`);
const resEid = await execa(this.lpa, ['getEid', `--serial=${port}`]);
const eidOutput = resEid.stdout;
let eid = null;

// get the line in eidOutput that starts with "EID: " and then get the next word
eidOutput.split('\n').forEach((line) => {
if (line.startsWith('EID: ')) {
eid = line.split(' ')[1];
}
});
if (!eid) {
throw new Error('EID not found in the output');
}
return eid;
}

async _checkForExistingProfiles(port) {
console.log(`${os.EOL}Checking for existing profiles`);
const resProfiles = await execa(this.lpa, ['listProfiles', `--serial=${port}`]);
const profilesOutput = resProfiles.stdout;
const profilesList = profilesOutput
.split('\n')
.filter((line) => line.match(/^\d+:\[\w+,\s(?:enabled|disabled),\s?\]$/));
// console.log('Profiles list: ', profilesList);
if (profilesList.length > 0) {
throw new Error('Profile(s) already exist. Bad device bucket.');
}
console.log(`${os.EOL}No existing profiles found`);
}
};
8 changes: 7 additions & 1 deletion src/cmd/serial.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,13 @@ module.exports = class SerialCommand extends CLICommandBase {
this.ui.stdout.write('Polling for available serial device...');
}

return this.whatSerialPortDidYouMean(port, true).then(handlePortFn);
return this.whatSerialPortDidYouMean(port, true).then(handlePortFn).then(() => ({
stop: async () => {
if (serialPort && serialPort.isOpen) {
await serialPort.close();
}
}
}));
}

/**
Expand Down
26 changes: 25 additions & 1 deletion src/cmd/usb-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const {
} = require('particle-usb');
const deviceProtectionHelper = require('../lib/device-protection-helper');
const { validateDFUSupport } = require('./device-util');
const execa = require('execa');
const os = require('os');


// Timeout when reopening a USB device after an update via control requests. This timeout should be
// long enough to allow the bootloader apply the update
Expand Down Expand Up @@ -555,6 +558,26 @@ async function handleUsbError(err){
throw err;
}

async function getUsbSystemPathsForMac() {
const platform = os.platform();
if (platform !== 'darwin') {
throw new Error('getUsbSystemPathsForMac() is only supported on macOS');
}

const { stdout } = await execa('ls', ['/dev']);

let paths = [];
stdout.split('\n').forEach((path) => {
paths.push(path);
});

//filter out tty.usbmodem*
const modemPaths = paths.filter((path) => path.includes('tty.usbmodem'));
const updatedModemPaths = modemPaths.map((path) => '/dev/' + path);

return updatedModemPaths;
}

module.exports = {
openUsbDevice,
openUsbDeviceById,
Expand All @@ -570,5 +593,6 @@ module.exports = {
forEachUsbDevice,
openUsbDevices,
executeWithUsbDevice,
waitForDeviceToRespond
waitForDeviceToRespond,
getUsbSystemPathsForMac
};

0 comments on commit c8ee5a3

Please sign in to comment.