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 0e8bb95
Show file tree
Hide file tree
Showing 5 changed files with 334 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
257 changes: 257 additions & 0 deletions src/cmd/esim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
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);

// Get the serial port and device details
const port = await this._getSerialPortForSingleDevice();
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}`);

// Flash firmware and retrieve EID
await this._flashATPassThroughFirmware(device, platform, port);
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(this.inputJson);
const inputJsonData = JSON.parse(input);

// Get the profile list that matches the EID that is given by the field eid
const eidBlock = inputJsonData.EIDs.find((block) => block.esim_id === eid);

if (!eidBlock || !eidBlock.profiles || eidBlock.profiles.length === 0) {
throw new Error('No profiles to provision in the input JSON');
}

const profiles = eidBlock?.profiles;

console.log(`${os.EOL}Provisioning the following profiles to EID ${eid}:`);
profiles.forEach((profile, index) => {
const rspUrl = `1\$${profile.smdp}\$${profile.matching_id}`;
console.log(`\t${index + 1}. ${profile.provider} (${rspUrl})`);
});

// Download each profile
for (const [index, profile] of profiles.entries()) {
const rspUrl = `1\$${profile.smdp}\$${profile.matching_id}`;
console.log(`${os.EOL}${index + 1}. Downloading ${profile.provider} profile from ${rspUrl}`);

const start = Date.now();
let timeTaken = 0;
let iccid = null;

try {
const res = await execa(this.lpa, ['download', rspUrl, `--serial=${port}`]);
timeTaken = ((Date.now() - start) / 1000).toFixed(2);

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

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

this._addToJson(this.outputJson, outputData);
} catch (err) {
const outputData = {
EID: eid,
provider: profile.provider,
iccid,
time: timeTaken,
output: err.message,
};
this._addToJson(this.outputJson, outputData);
throw new Error('Failed to download profile');
}

}

console.log(`${os.EOL}Provisioning complete`);
}

_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) {
const errorMessage = deviceSerialPorts.length > 1
? 'Multiple devices found. Please unplug all but one device or use the --bulk option.'
: 'No devices found. Please connect a device and try again.';
throw new Error(errorMessage);
}
return deviceSerialPorts[0];
}

async _flashATPassThroughFirmware(device, platform, port) {
// Locate the firmware binary
console.log(`${os.EOL}Locating firmware for platform: ${platform}`);
const fwBinaries = fs.readdirSync(PATH_TO_PASS_THROUGH_BINARIES);
const validBin = fwBinaries.find((file) => file.endsWith(`${platform}.bin`));

if (!validBin) {
throw new Error(`No firmware binary found for platform: ${platform}`);
}

const fwPath = path.join(PATH_TO_PASS_THROUGH_BINARIES, validBin);
console.log(`${os.EOL}Found firmware: ${fwPath}`);

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

// Wait for the device to respond
console.log(`${os.EOL}Waiting for device to respond...`);
const deviceResponded = await usbUtils.waitForDeviceToRespond(device.deviceId);

if (!deviceResponded) {
throw new Error('Device did not respond after flashing firmware');
}
console.log(`${os.EOL}Device responded successfully`);
await deviceResponded.close();

// Handle initial logs (temporary workaround)
console.log(`${os.EOL}Clearing initial logs (temporary workaround)...`);
console.log(`${os.EOL}--------------------------------------`);
const monitor = await this.serial.monitorPort({ port, follow: false });

// Wait for logs to clear
await utilities.delay(30000); // 30-second delay
await monitor.stop();
await utilities.delay(5000); // Additional delay to ensure logs are cleared
console.log(`${os.EOL}--------------------------------------`);
console.log(`${os.EOL}Initial logs cleared`);
}


async _getEid(port) {
console.log(`${os.EOL}Getting EID from the device...`);

try {
const resEid = await execa(this.lpa, ['getEid', `--serial=${port}`]);
const eidOutput = resEid.stdout;

// Find the line starting with "EID: " and extract the EID
const eid = eidOutput
.split('\n')
.find((line) => line.startsWith('EID: '))
?.split(' ')[1];

if (!eid) {
throw new Error('EID not found in the output');
}
return eid;
} catch (error) {
console.error(`${os.EOL}Failed to retrieve EID: ${error.message}`);
throw error;
}
}

async _checkForExistingProfiles(port) {
console.log(`${os.EOL}Checking for existing profiles...`);

try {
const resProfiles = await execa(this.lpa, ['listProfiles', `--serial=${port}`]);
const profilesOutput = resProfiles.stdout;

// Extract lines matching the profile format
const profilesList = profilesOutput
.split('\n')
.filter((line) => line.match(/^\d+:\[\w+,\s(?:enabled|disabled),\s?\]$/));

if (profilesList.length > 0) {
console.error(`${os.EOL}Profile(s) already exist:`, profilesList);
throw new Error('Profile(s) already exist. Device bucket is not clean.');
}

console.log(`${os.EOL}No existing profiles found`);
} catch (error) {
console.error(`${os.EOL}Failed to check for existing profiles: ${error.message}`);
throw error;
}
}

_addToJson(jsonFile, data) {
try {
// Read and parse existing JSON data
let existingJson = [];
if (fs.existsSync(jsonFile)) {
const existing = fs.readFileSync(jsonFile, 'utf-8');
existingJson = JSON.parse(existing);
if (!Array.isArray(existingJson)) {
throw new Error('Existing JSON data is not an array');
}
}

existingJson.push(data);

// Write updated JSON back to the file with indentation
fs.writeFileSync(jsonFile, JSON.stringify(existingJson, null, 4));
} catch (error) {
console.error(`Failed to append data to JSON file: ${error.message}`);
}
}

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

Please sign in to comment.