diff --git a/package-lock.json b/package-lock.json index bf19a6eb..08298260 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "4.3.1", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@nordicsemiconductor/pc-nrfconnect-shared": "^170.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": "170.0.0", - "resolved": "https://registry.npmjs.org/@nordicsemiconductor/pc-nrfconnect-shared/-/pc-nrfconnect-shared-170.0.0.tgz", - "integrity": "sha512-mZMPYpJRkqjwb74ueA2cHsR8HZVRboXeIvqfL8aScBUKRyC1aMEeV8JPCwxRVvM4FArWKUz9WOgWxuJtGNUUTA==", + "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 b74fd3eb..654c6700 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": "^170.0.0" + "@nordicsemiconductor/pc-nrfconnect-shared": "^171.0.0" }, "eslintConfig": { "extends": "./node_modules/@nordicsemiconductor/pc-nrfconnect-shared/config/eslintrc" diff --git a/resources/firmware/mfw_nrf91x1_2.0.1.zip b/resources/firmware/mfw_nrf91x1_2.0.1.zip new file mode 100644 index 00000000..9b48ad78 Binary files /dev/null and b/resources/firmware/mfw_nrf91x1_2.0.1.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 13917532..d7361e3d 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..1a98e207 --- /dev/null +++ b/src/features/ImeiProgramming/index.tsx @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import React, { useCallback, 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 ptiFirmware = getAppFile(path.join('resources', 'firmware', 'pti.zip')); +const modemFirmware = getAppFile( + path.join('resources', 'firmware', 'mfw_nrf91x1_2.0.1.zip') +); + +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 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' + | 'IMEI_NOTICE'; + +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); + }); + }; + + const onClose = useCallback(() => { + if (cloudIMEI !== '' && status !== 'IMEI_NOTICE') { + setStatus('IMEI_NOTICE'); + } else { + setStatus('NONE'); + } + }, [status, cloudIMEI]); + + useEffect(() => { + // if device disconnects + if (!device && status !== 'NONE') { + onClose(); + } + }, [device, status, onClose]); + + const program = (mfw: string) => { + if (!device) return; + waitForAction(async () => { + 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.') + ); + }; + + return ( + <> + + {status === 'PROTECTED' && ( + { + if (!device) return; + + waitForAction(async () => + setStatus(await recover(device)) + ).catch(() => + setError( + 'Failed to recover. Please try again.' + ) + ); + }} + > + Recover + + )} + {status === 'FIRMWARE' && ( + <> + program(modemFirmware)} + > + Program standard firmware + + program(ptiFirmware)} + > + Program PTI 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(() => { + setCloudIMEI(''); + setStatus('FINISHED'); + }) + .catch(() => { + setError( + 'Failed to write IMEI.' + ); + if (!hasProgrammedFirmware) { + setStatus('FIRMWARE'); + } else { + setStatus('UNSUPPORTED'); + } + }); + }); + }} + > + Write IMEI + + )} + + Close + + + } + > + {error && ( +
+
+ +
{error}
+
+
+ )} + {status === 'INIT' && ( +
Checking that IMEI can be written to device.
+ )} + {status === 'PROTECTED' && ( +
+ Recover the device to check if programming IMEI is + supported. +
+ Note: recovering the device removes the application + firmware. +
+ )} + {status === 'UNSUPPORTED' && ( +
+ Programming IMEI is not supported for this device. +
+ )} + {status === 'FIRMWARE' && ( +
+ Unable to communicate with the device. Do you want to + program PTI firmware? +
+ )} + {status === 'IMEI_NOT_SET' && ( + <> + {useCloud && ( + <> + Cloud API key +
+ { + persistApiKey('nrfcloud', key); + setAPIKey(key); + }} + value={apiKey} + /> + + + )} + {!useCloud && ( + <> + IMEI number +
+ + setManualIMEI(event.target.value) + } + /> +
+ + + )} + + )} + {status === 'IMEI_SET' && ( +
The device already has an IMEI programmed.
+ )} + {status === 'FINISHED' &&
Finished.
} + {status === 'IMEI_NOTICE' && ( + <> + IMEI from the cloud is unique. Copy and store it to + avoid losing it. +
+ {cloudIMEI} + + + )} +
+ + + ); +}; 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 };