diff --git a/package-lock.json b/package-lock.json index 2498f131..b1a92b94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "packages": { "": { "name": "pc-nrfconnect-programmer", - "version": "4.2.1", + "version": "4.3.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@nordicsemiconductor/pc-nrfconnect-shared": "^167.0.0" + "@nordicsemiconductor/pc-nrfconnect-shared": "^171.0.0" }, "engines": { "nrfconnect": ">=4.4.1" @@ -2868,9 +2868,9 @@ } }, "node_modules/@nordicsemiconductor/pc-nrfconnect-shared": { - "version": "167.0.0", - "resolved": "https://registry.npmjs.org/@nordicsemiconductor/pc-nrfconnect-shared/-/pc-nrfconnect-shared-167.0.0.tgz", - "integrity": "sha512-UHu7sokZu/HRhz1CemSas6Lkv1YQWxIhahkt8eVzJ7DfAFaUrfjzqg5bXDi0NzXYi5geGeJrL1gYgVetxrBIiA==", + "version": "171.0.0", + "resolved": "https://registry.npmjs.org/@nordicsemiconductor/pc-nrfconnect-shared/-/pc-nrfconnect-shared-171.0.0.tgz", + "integrity": "sha512-WNoFIRMOSJU+fj9e3GKjDLKFY4Bk1WVyq+fbQpgfsnT1pdptZAWKfKvUynmgf+Pt9exOZOP4IIKtJloUCtiG5w==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/package.json b/package.json index c7712f77..ad2d39cd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ }, "nrfConnectForDesktop": { "nrfutil": { + "91": [ + "0.4.1" + ], "device": [ "2.1.1" ] @@ -46,7 +49,7 @@ "prepare": "husky install" }, "devDependencies": { - "@nordicsemiconductor/pc-nrfconnect-shared": "^167.0.0" + "@nordicsemiconductor/pc-nrfconnect-shared": "^171.0.0" }, "eslintConfig": { "extends": "./node_modules/@nordicsemiconductor/pc-nrfconnect-shared/config/eslintrc" diff --git a/resources/firmware/pcm.zip b/resources/firmware/pcm.zip new file mode 100644 index 00000000..fcddc313 Binary files /dev/null and b/resources/firmware/pcm.zip differ diff --git a/resources/firmware/pti.zip b/resources/firmware/pti.zip new file mode 100644 index 00000000..fcddc313 Binary files /dev/null and b/resources/firmware/pti.zip differ diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 45215a56..4408873f 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -27,6 +27,7 @@ import * as jlinkTargetActions from '../actions/jlinkTargetActions'; import * as settingsActions from '../actions/settingsActions'; import * as targetActions from '../actions/targetActions'; import * as usbsdfuTargetActions from '../actions/usbsdfuTargetActions'; +import ImeiProgramming from '../features/ImeiProgramming'; import { getDeviceDefinition, getDeviceIsBusy, @@ -42,7 +43,7 @@ import { getIsWritable } from '../reducers/targetReducer'; import { convertDeviceDefinitionToCoreArray } from '../util/devices'; const useRegisterDragEvents = () => { - const dispatch = useDispatch(); + const dispatch = useDispatch(); useEffect(() => { const onDragover = (event: DragEvent) => { if (!event.dataTransfer) return; @@ -404,6 +405,7 @@ Are you sure you want to continue?`, Read + { + const response = await fetch( + 'https://api.imei.nrfcloud.com/v1/imei-management/allocations', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ scope, product, count: 1 }), + } + ); + + if (response.status !== 200) { + throw new Error( + `Error fetching IMEI: ${response.status}.${ + response.statusText ? `statusText: ${response.statusText}` : '' + }. Make sure the API key is valid.` + ); + } + + const imei = ((await response.json()) as ImeiAllocationResource).imeis[0]; + return imei; +}; diff --git a/src/features/ImeiProgramming/index.tsx b/src/features/ImeiProgramming/index.tsx new file mode 100644 index 00000000..dd86c9a1 --- /dev/null +++ b/src/features/ImeiProgramming/index.tsx @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + Button, + Device, + DialogButton, + GenericDialog, + getAppFile, + getPersistedApiKey, + isDevelopment, + logger, + persistApiKey, + selectedDevice, + selectedDeviceInfo, + setSelectedDeviceInfo, +} from '@nordicsemiconductor/pc-nrfconnect-shared'; +import { + DeviceInfo, + NrfutilDeviceLib, +} from '@nordicsemiconductor/pc-nrfconnect-shared/nrfutil/device'; +import { clipboard } from 'electron'; +import path from 'path'; + +import { readIMEI, writeIMEI } from '../nrfutil91'; +import fetchIMEI from './fetchIMEI'; + +const PasswordComponent = ({ + onChanging, + value, +}: { + onChanging: (value: string) => void; + value: string; +}) => { + const [show, setShow] = useState(false); + return ( +
+ { + onChanging(event.target.value); + }} + /> + +
+ ); +}; + +const isSupportedDevice = (deviceInfo?: DeviceInfo) => + deviceInfo?.jlink?.protectionStatus === 'NRFDL_PROTECTION_STATUS_NONE' && + deviceInfo?.jlink?.deviceVersion?.toLocaleUpperCase().match(/NRF91\d1/); +const isMaybeSupportedDevice = (deviceInfo?: DeviceInfo) => + deviceInfo?.jlink?.protectionStatus === 'NRFDL_PROTECTION_STATUS_NONE' + ? deviceInfo?.jlink?.deviceVersion + ?.toLocaleUpperCase() + .match(/NRF91\d1/) + : deviceInfo?.jlink?.deviceFamily !== 'NRF91_FAMILY'; + +const getStatus = async (device?: Device, deviceInfo?: DeviceInfo) => { + if (!device || !deviceInfo) return 'NONE'; + if (deviceInfo.jlink?.protectionStatus !== 'NRFDL_PROTECTION_STATUS_NONE') + return 'PROTECTED'; + + if (!device.serialNumber || !isSupportedDevice(deviceInfo)) + return 'UNSUPPORTED'; + + const defaultImeiNumber = 'FFFFFFFFFFFFFFF'; + try { + const res = await readIMEI(device.serialNumber); + if (res !== defaultImeiNumber) { + return 'IMEI_SET'; + } + return 'IMEI_NOT_SET'; + } catch (e) { + return 'FIRMWARE'; + } +}; + +const updateDeviceInfo = async (device: Device) => { + const deviceInfo = await NrfutilDeviceLib.deviceInfo(device); + setSelectedDeviceInfo(deviceInfo); + return deviceInfo; +}; + +const recover = async (device: Device) => { + await NrfutilDeviceLib.batch() + .recover('Application') + .reset('Application') + .run(device); + return getStatus(device, await updateDeviceInfo(device)); +}; + +const CopyImeiNotice = ({ imei }: { imei: string }) => ( + <> + Copy and store the IMEI as it has been consumed from the cloud. +
+ {imei} + + +); + +const formatDeviceVersionToProduct = (deviceVersion: string) => { + switch (deviceVersion.toLocaleUpperCase().slice(0, 7)) { + case 'NRF9161': + return 'nRF9161'; + case 'NRF9151': + return 'nRF9151'; + case 'NRF9131': + return 'nRF9131'; + default: + throw new Error(`Unknown device version: ${deviceVersion}`); + } +}; + +type Status = + | 'NONE' + | 'INIT' + | 'IMEI_NOT_SET' + | 'IMEI_SET' + | 'FIRMWARE' + | 'PROTECTED' + | 'UNSUPPORTED' + | 'FINISHED'; + +export default () => { + const device = useSelector(selectedDevice); + const deviceInfo = useSelector(selectedDeviceInfo); + const [status, setStatus] = useState('NONE'); + const [showSpinner, setShowSpinner] = useState(false); + const [useCloud, setUseCloud] = useState(false); + const [apiKey, setAPIKey] = useState(''); + const [manualIMEI, setManualIMEI] = useState(''); + const [cloudIMEI, setCloudIMEI] = useState(''); + const [hasProgrammedFirmware, setHasProgrammedFirmware] = useState(false); + const [error, setError] = useState(); + + const waitForAction = async (action: () => Promise) => { + setShowSpinner(true); + setError(undefined); + await action().finally(() => { + setShowSpinner(false); + }); + }; + + useEffect(() => { + // if device disconnects + if (!device && status !== 'NONE') { + setStatus('NONE'); + } + }, [device, status]); + + return ( + <> + setStatus('NONE')} + title="Program IMEI" + showSpinner={showSpinner} + footer={ + <> + {status === 'PROTECTED' && ( + { + if (!device) return; + + waitForAction(async () => + setStatus(await recover(device)) + ).catch(() => + setError( + 'Failed to recover. Please try again.' + ) + ); + }} + > + Recover + + )} + {status === 'FIRMWARE' && ( + { + if (!device) return; + + waitForAction(async () => { + const mfw = ''; + await NrfutilDeviceLib.program( + device, + mfw, + () => {}, + 'Modem', + { + reset: 'RESET_SYSTEM', + } + ); + + const newStatus = await getStatus( + device, + await updateDeviceInfo(device) + ); + if (newStatus === 'FIRMWARE') { + setError( + 'Unable to communicate with the device.' + ); + setStatus('UNSUPPORTED'); + } else { + setStatus(newStatus); + } + setHasProgrammedFirmware(true); + }).catch(() => + setError( + 'Failed to program firmware. Please try again.' + ) + ); + }} + > + Program firmware + + )} + {status === 'IMEI_NOT_SET' && ( + { + waitForAction(async () => { + if ( + !device?.serialNumber || + !deviceInfo?.jlink?.deviceVersion + ) + return; + let imei = ''; + if (useCloud && !cloudIMEI) { + try { + imei = await fetchIMEI( + formatDeviceVersionToProduct( + deviceInfo?.jlink + ?.deviceVersion + ), + isDevelopment + ? 'DEVELOPMENT' + : 'PRODUCTION', + apiKey + ); + logger.info( + `Fetched IMEI: ${imei}` + ); + setCloudIMEI(imei); + } catch (e) { + setError( + 'Failed to fetch IMEI. Before trying again, check your internet connection and your API key.' + ); + return; + } + } + + await writeIMEI( + device.serialNumber, + useCloud ? cloudIMEI : manualIMEI + ) + .then(() => { + setStatus('FINISHED'); + }) + .catch(() => { + setError( + 'Failed to write IMEI.' + ); + if (!hasProgrammedFirmware) { + setStatus('FIRMWARE'); + } else { + setStatus('UNSUPPORTED'); + } + }); + }); + }} + > + Write IMEI + + )} + setStatus('NONE')} + > + Close + + + } + > + {error && ( +
+
+ +
{error}
+
+
+ )} + {status === 'INIT' && ( +
Checking that IMEI can be written to device.
+ )} + {status === 'PROTECTED' && ( +
+ Recover the device in order to check whether programming + IMEI is supported. +
+ )} + {status === 'UNSUPPORTED' && ( +
+ Programming IMEI is not supported for this device. + {cloudIMEI && } +
+ )} + {status === 'FIRMWARE' && ( + <> +
+ Unable to communicate with the device. Do you want + to program ptm firmware? +
+ {cloudIMEI && } + + )} + {status === 'IMEI_NOT_SET' && ( + <> + {useCloud && ( + <> + Cloud API key +
+ { + persistApiKey('nrfcloud', key); + setAPIKey(key); + }} + value={apiKey} + /> + + {cloudIMEI && ( + + )} + + )} + {!useCloud && ( + <> + IMEI Number +
+ + setManualIMEI(event.target.value) + } + /> +
+ + + )} + + )} + {status === 'IMEI_SET' && ( +
The device already has an IMEI programmed.
+ )} + {status === 'FINISHED' &&
Finished.
} +
+ + + ); +}; diff --git a/src/features/nrfutil91.ts b/src/features/nrfutil91.ts new file mode 100644 index 00000000..64b03b56 --- /dev/null +++ b/src/features/nrfutil91.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import { getAppFile } from '@nordicsemiconductor/pc-nrfconnect-shared'; +import { getModule } from '@nordicsemiconductor/pc-nrfconnect-shared/nrfutil'; +import path from 'path'; + +interface IMEINumber { + devices: { + imei_numbers: string[]; + serial_number: string; + }[]; +} + +const modemFirmware = getAppFile(path.join('resources', 'firmware', 'pti.zip')); + +const writeIMEI = async (serialNumber: string, imei: string) => { + const box = await getModule('91'); + const args: string[] = [ + '--serial-number', + serialNumber, + '--modem-firmware', + modemFirmware, + '--imei', + imei, + '--slot', + '1', + '--force', + ]; + + return box.singleInfoOperationOptionalData( + 'imei-write', + undefined, + args + ); +}; + +const readIMEI = async (serialNumber: string) => { + const box = await getModule('91'); + const args: string[] = [ + '--serial-number', + serialNumber, + '--modem-firmware', + modemFirmware, + ]; + + return box + .singleInfoOperationOptionalData( + 'imei-read', + undefined, + args + ) + .then((data: IMEINumber) => { + const imeiNumbers = data.devices.find( + d => d.serial_number === serialNumber + )?.imei_numbers; + return imeiNumbers && imeiNumbers.length >= 1 + ? imeiNumbers[1] + : undefined; + }); +}; + +export { writeIMEI, readIMEI };