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/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 45215a56..addb254c 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -17,6 +17,7 @@ import { logger, preventAppCloseUntilComplete, selectedDevice, + selectedDeviceInfo, SidePanel, Toggle, truncateMiddle, @@ -27,6 +28,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 +44,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 +406,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..55478e54 --- /dev/null +++ b/src/features/ImeiProgramming/index.tsx @@ -0,0 +1,415 @@ +/* + * 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, + 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 getStatus = async (device?: Device, deviceInfo?: DeviceInfo) => { + if (!device || !deviceInfo) return 'Not connected'; + 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_AVAILABLE'; + return 'IMEI_SET'; + } + return 'IMEI_AVAILABLE'; + } catch (e) { + return 'FIRMWARE'; + } +}; + +const updateDeviceInfo = async (device: Device) => { + const deviceInfo = await NrfutilDeviceLib.deviceInfo(device); + setSelectedDeviceInfo(deviceInfo); + return deviceInfo; +}; + +const recover = async (device: Device) => { + try { + await NrfutilDeviceLib.batch() + .recover('Application') + .reset('Application') + .run(device); + return getStatus(device, await updateDeviceInfo(device)); + } catch (e) { + // TODO + } + return 'TODO'; +}; + +const programFirmware = async (device: Device) => { + await NrfutilDeviceLib.program( + device, + getAppFile(path.join('resources', 'firmware', 'pcm.zip')), + () => {}, + 'Modem', + { reset: 'RESET_SYSTEM' } + ); + const newDeviceInfo = await updateDeviceInfo(device); + return getStatus(device, newDeviceInfo); +}; + +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 newStatus = await programFirmware( + device + ); + if (newStatus === 'FIRMWARE') { + setError( + 'Unable to communicate with device.' + ); + setStatus('UNSUPPORTED'); + } else { + setStatus(newStatus); + } + setHasProgrammedFirmware(true); + }).catch(() => + setError( + 'Failed to program firmware. Please try again.' + ) + ); + }} + > + Program firmware + + )} + {status === 'IMEI_AVAILABLE' && ( + { + waitForAction(async () => { + if (!device?.serialNumber) return; + let imei = ''; + if (useCloud && !cloudIMEI) { + try { + imei = await fetchIMEI( + 'nRF9161', + isDevelopment + ? 'DEVELOPMENT' + : 'PRODUCTION', + apiKey + ); + setCloudIMEI(imei); + } catch (e) { + setError( + 'Failed to fetch IMEI. Make sure you are connected to the internet and that your API key is valid before trying again.' + ); + return; + } + } + + await writeIMEI( + device.serialNumber, + useCloud ? cloudIMEI : manualIMEI + ) + .then(() => { + setStatus('FINISHED'); + }) + .catch(() => { + setError( + 'Failed to write IMEI.' + ); + if (!hasProgrammedFirmware) { + setStatus('FIRMWARE'); + } + }); + }); + }} + > + Write IMEI + + )} + setStatus('NONE')} + > + Close + + + } + > + {error && ( +
+
+ +
{error}
+
+
+ )} + {status === 'INIT' && ( +
Checking that IMEI can be written to device.
+ )} + {status === 'PROTECTED' && ( +
+ Device needs to be recovered in order to check whether + programming IMEI is supported. +
+ )} + {status === 'UNSUPPORTED' && ( +
+ Programming IMEI is not supported for this device. +
+ )} + {status === 'FIRMWARE' && ( + <> +
+ Unable to communicate with device. Do you want to + program ptm firmware? +
+ {cloudIMEI && ( + <> + Copy and store the IMEI as it has been consumed + from the cloud. +
+ {cloudIMEI} + + + )} + + )} + {status === 'IMEI_AVAILABLE' && ( + <> + {useCloud && ( + <> + Cloud API key +
+ { + persistApiKey('nrfcloud', key); + setAPIKey(key); + }} + value={apiKey} + /> + + {cloudIMEI && ( + <> + Copy and store the IMEI as it has been + consumed from the cloud. +
+ {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..59aa773c --- /dev/null +++ b/src/features/nrfutil91.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import { getModule } from '@nordicsemiconductor/pc-nrfconnect-shared/nrfutil'; + +interface IMEINumber { + devices: { + imei_numbers: string[]; + serial_number: string; + }[]; +} + +const modemFirmware = '/home/papi/temp.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 };