From c8ee5a3df77171119eaa0d2a7beaab675132b54f Mon Sep 17 00:00:00 2001 From: keeramis Date: Tue, 19 Nov 2024 12:23:34 -0800 Subject: [PATCH] Add esim provision command --- src/cli/esim.js | 43 ++++++++++ src/cli/index.js | 2 + src/cmd/esim.js | 191 ++++++++++++++++++++++++++++++++++++++++++++ src/cmd/serial.js | 8 +- src/cmd/usb-util.js | 26 +++++- 5 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 src/cli/esim.js create mode 100644 src/cmd/esim.js diff --git a/src/cli/esim.js b/src/cli/esim.js new file mode 100644 index 000000000..793b5f446 --- /dev/null +++ b/src/cli/esim.js @@ -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; +}; + diff --git a/src/cli/index.js b/src/cli/index.js index 9d18999a9..f76d491f4 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -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'); @@ -50,6 +51,7 @@ module.exports = function registerAllCommands(context) { cloud(context); config(context); doctor(context); + esim(context); protection(context); flash(context); func(context); diff --git a/src/cmd/esim.js b/src/cmd/esim.js new file mode 100644 index 000000000..d9f1e240f --- /dev/null +++ b/src/cmd/esim.js @@ -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`); + } +}; diff --git a/src/cmd/serial.js b/src/cmd/serial.js index 4b8e7185e..104984598 100644 --- a/src/cmd/serial.js +++ b/src/cmd/serial.js @@ -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(); + } + } + })); } /** diff --git a/src/cmd/usb-util.js b/src/cmd/usb-util.js index a4f0888e9..b2a32085e 100644 --- a/src/cmd/usb-util.js +++ b/src/cmd/usb-util.js @@ -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 @@ -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, @@ -570,5 +593,6 @@ module.exports = { forEachUsbDevice, openUsbDevices, executeWithUsbDevice, - waitForDeviceToRespond + waitForDeviceToRespond, + getUsbSystemPathsForMac };