Skip to content

Commit a6dd6c5

Browse files
authored
Merge pull request #483 from PlayerData/bootloader-info
feat: Add bootloader info
2 parents 0b9c041 + 9b74b27 commit a6dd6c5

File tree

8 files changed

+224
-5
lines changed

8 files changed

+224
-5
lines changed

example/src/app/_layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ const RootLayout = () => {
1515
return (
1616
<SelectedDeviceProvider value={{ selectedDevice, setSelectedDevice }}>
1717
<Tabs>
18+
<Tabs.Screen name="(home)" options={{ title: 'Home' }} />
1819
<Tabs.Screen
19-
name="(home)"
20-
options={{ title: 'Home' }}
20+
name="bootloader_info"
21+
options={{ title: 'Bootloader Info' }}
2122
/>
2223
<Tabs.Screen name="update" options={{ title: 'Update' }} />
2324
</Tabs>

example/src/app/bootloader_info.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
BootloaderInfo,
3+
bootloaderInfo as rnmcumgrBootloaderInfo,
4+
} from '@playerdata/react-native-mcu-manager';
5+
6+
import React, { useCallback, useState } from 'react';
7+
import {
8+
Button,
9+
SafeAreaView,
10+
ScrollView,
11+
StyleSheet,
12+
Text,
13+
View,
14+
} from 'react-native';
15+
16+
import { useSelectedDevice } from '../context/selectedDevice';
17+
18+
const styles = StyleSheet.create({
19+
root: {
20+
padding: 16,
21+
},
22+
23+
block: {
24+
marginBottom: 16,
25+
},
26+
});
27+
28+
const BootloaderInfoView = () => {
29+
const { selectedDevice } = useSelectedDevice();
30+
31+
const [fetchInProgress, setFetchInProgress] = useState(false);
32+
const [error, setError] = useState<string | null>(null);
33+
const [bootloaderInfo, setBootloaderInfo] = useState<BootloaderInfo | null>(
34+
null
35+
);
36+
37+
const fetchBootloaderInfo = useCallback(async () => {
38+
setError(null);
39+
setFetchInProgress(true);
40+
41+
try {
42+
setBootloaderInfo(
43+
await rnmcumgrBootloaderInfo(selectedDevice?.deviceId || '')
44+
);
45+
} catch (error: unknown) {
46+
const errorMessage =
47+
error instanceof Error ? error.message : 'Unknown error';
48+
49+
setError(errorMessage);
50+
} finally {
51+
setFetchInProgress(false);
52+
}
53+
}, [selectedDevice]);
54+
55+
return (
56+
<SafeAreaView>
57+
<ScrollView contentContainerStyle={styles.root}>
58+
<View style={styles.block}>
59+
<Button
60+
disabled={fetchInProgress}
61+
onPress={() => fetchBootloaderInfo()}
62+
title="Fetch Bootloader Info"
63+
/>
64+
</View>
65+
66+
{error && (
67+
<View style={styles.block}>
68+
<Text style={{ color: 'red' }}>{error}</Text>
69+
</View>
70+
)}
71+
72+
{bootloaderInfo && (
73+
<View style={styles.block}>
74+
<Text>{JSON.stringify(bootloaderInfo)}</Text>
75+
</View>
76+
)}
77+
</ScrollView>
78+
</SafeAreaView>
79+
);
80+
};
81+
82+
export default BootloaderInfoView;

react-native-mcu-manager/android/src/main/java/uk/co/playerdata/reactnativemcumanager/ReactNativeMcuManagerModule.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package uk.co.playerdata.reactnativemcumanager
22

3-
import android.bluetooth.BluetoothAdapter
43
import android.bluetooth.BluetoothDevice
54
import android.bluetooth.BluetoothManager
65
import android.content.Context
@@ -14,7 +13,9 @@ import expo.modules.kotlin.modules.ModuleDefinition
1413
import expo.modules.kotlin.records.Field
1514
import expo.modules.kotlin.records.Record
1615
import io.runtime.mcumgr.McuMgrCallback
16+
import io.runtime.mcumgr.McuMgrErrorCode
1717
import io.runtime.mcumgr.ble.McuMgrBleTransport
18+
import io.runtime.mcumgr.exception.McuMgrErrorException
1819
import io.runtime.mcumgr.exception.McuMgrException
1920
import io.runtime.mcumgr.managers.DefaultManager
2021
import io.runtime.mcumgr.managers.ImageManager
@@ -29,7 +30,15 @@ class UpdateOptions : Record {
2930
@Field val upgradeMode: Int? = null
3031
}
3132

33+
class BootloaderInfo : Record {
34+
@Field var bootloader: String? = null
35+
@Field var mode: Int? = null
36+
@Field var noDowngrade: Boolean = false
37+
}
38+
3239
class ReactNativeMcuManagerModule() : Module() {
40+
private val MCUBOOT = "MCUboot"
41+
3342
private val upgrades: MutableMap<String, DeviceUpgrade> = mutableMapOf()
3443
private val context
3544
get() = requireNotNull(appContext.reactContext) { "React Application Context is null" }
@@ -44,6 +53,41 @@ class ReactNativeMcuManagerModule() : Module() {
4453
override fun definition() = ModuleDefinition {
4554
Name(MODULE_NAME)
4655

56+
AsyncFunction("bootloaderInfo") { macAddress: String?, promise: Promise ->
57+
val device: BluetoothDevice = getBluetoothDevice(macAddress)
58+
59+
val transport = McuMgrBleTransport(context, device)
60+
transport.connect(device).timeout(60000).await()
61+
62+
val manager = DefaultManager(transport)
63+
val info = BootloaderInfo()
64+
65+
try {
66+
val nameResult = manager.bootloaderInfo(DefaultManager.BOOTLOADER_INFO_QUERY_BOOTLOADER)
67+
info.bootloader = nameResult.bootloader
68+
} catch(ex: McuMgrErrorException) {
69+
transport.release()
70+
71+
// For consistency with iOS, if the error code is 8 (MGMT_ERR_ENOTSUP), return null
72+
if (ex.code == McuMgrErrorCode.NOT_SUPPORTED) {
73+
promise.resolve(info)
74+
return@AsyncFunction
75+
}
76+
77+
throw ex;
78+
}
79+
80+
if (info.bootloader == MCUBOOT) {
81+
val mcuMgrResult = manager.bootloaderInfo(DefaultManager.BOOTLOADER_INFO_MCUBOOT_QUERY_MODE)
82+
83+
info.mode = mcuMgrResult.mode
84+
info.noDowngrade = mcuMgrResult.noDowngrade
85+
}
86+
87+
transport.release()
88+
promise.resolve(info)
89+
}
90+
4791
AsyncFunction("eraseImage") { macAddress: String?, promise: Promise ->
4892
try {
4993
val device: BluetoothDevice = getBluetoothDevice(macAddress)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import ExpoModulesCore
2+
3+
struct BootloaderInfo: Record {
4+
@Field var bootloader: String?
5+
@Field var mode: Int?
6+
@Field var noDowngrade: Bool?
7+
}

react-native-mcu-manager/ios/ReactNativeMcuManagerModule.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,61 @@ public class ReactNativeMcuManagerModule: Module {
1212
public func definition() -> ModuleDefinition {
1313
Name(MODULE_NAME)
1414

15+
AsyncFunction("bootloaderInfo") { (bleId: String, promise: Promise) in
16+
guard let bleUuid = UUID(uuidString: bleId) else {
17+
promise.reject(Exception(name: "UUIDParseError", description: "Failed to parse UUID"))
18+
return
19+
}
20+
21+
let bleTransport = McuMgrBleTransport(bleUuid)
22+
let manager = DefaultManager(transport: bleTransport)
23+
24+
manager.bootloaderInfo(query: DefaultManager.BootloaderInfoQuery.name) { (nameResponse: BootloaderInfoResponse?, err: Error?) in
25+
if err != nil {
26+
bleTransport.close()
27+
promise.reject(Exception(name: "BootloaderInfoError", description: err!.localizedDescription))
28+
return
29+
}
30+
31+
guard let nameResponse = nameResponse else {
32+
bleTransport.close()
33+
promise.reject(Exception(name: "BootloaderInfoError", description: "Bootloader name response null, but no error occurred?"))
34+
return
35+
}
36+
37+
let info = BootloaderInfo()
38+
info.bootloader = nameResponse.bootloader?.description
39+
40+
if nameResponse.bootloader != BootloaderInfoResponse.Bootloader.mcuboot {
41+
info.mode = nameResponse.mode?.rawValue
42+
info.noDowngrade = nameResponse.noDowngrade
43+
44+
bleTransport.close()
45+
promise.resolve(info)
46+
return
47+
}
48+
49+
manager.bootloaderInfo(query: DefaultManager.BootloaderInfoQuery.mode) { (mcubootResponse: BootloaderInfoResponse?, err: Error?) in
50+
bleTransport.close()
51+
52+
if err != nil {
53+
promise.reject(Exception(name: "BootloaderInfoError", description: err!.localizedDescription))
54+
return
55+
}
56+
57+
guard let mcubootResponse = mcubootResponse else {
58+
promise.reject(Exception(name: "BootloaderInfoError", description: "MCUboot response null, but no error occurred?"))
59+
return
60+
}
61+
62+
info.mode = mcubootResponse.mode?.rawValue
63+
info.noDowngrade = mcubootResponse.noDowngrade
64+
65+
promise.resolve(info)
66+
}
67+
}
68+
}
69+
1570
AsyncFunction("eraseImage") { (bleId: String, promise: Promise) in
1671
guard let bleUuid = UUID(uuidString: bleId) else {
1772
promise.reject(Exception(name: "UUIDParseError", description: "Failed to parse UUID"))

react-native-mcu-manager/src/Upgrade.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export enum UpgradeFileType {
1313
}
1414

1515
export enum UpgradeMode {
16+
/**
17+
* When this flag is set, the manager will immediately send the reset command after
18+
* the upload is complete. The device will reboot and will run the new image on its next
19+
* boot.
20+
*/
21+
NONE = 0,
22+
1623
/**
1724
* This mode is the default and recommended mode for performing upgrades due to it's ability to
1825
* recover from a bad firmware upgrade. The process for this mode is upload, test, reset, confirm.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import McuManagerModule from './ReactNativeMcuManagerModule';
2+
3+
export enum MCUBootMode {
4+
MCUBOOT_MODE_SINGLE_SLOT = 0,
5+
MCUBOOT_MODE_SWAP_USING_SCRATCH = 1,
6+
MCUBOOT_MODE_UPGRADE_ONLY = 2,
7+
MCUBOOT_MODE_SWAP_USING_MOVE = 3,
8+
MCUBOOT_MODE_DIRECT_XIP = 4,
9+
MCUBOOT_MODE_DIRECT_XIP_WITH_REVERT = 5,
10+
MCUBOOT_MODE_RAM_LOAD = 6,
11+
MCUBOOT_MODE_FIRMWARE_LOADER = 7,
12+
}
13+
14+
export interface BootloaderInfo {
15+
bootloader: string | null;
16+
mode: MCUBootMode | null;
17+
noDowngrade: boolean;
18+
}
19+
20+
export const bootloaderInfo = McuManagerModule?.bootloaderInfo as (
21+
bleId: string
22+
) => Promise<BootloaderInfo>;

react-native-mcu-manager/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import McuManagerModule from './ReactNativeMcuManagerModule';
22
import type { FirmwareUpgradeState, UpgradeOptions } from './Upgrade';
33
import Upgrade, { UpgradeMode, UpgradeFileType } from './Upgrade';
4+
import { BootloaderInfo, bootloaderInfo, MCUBootMode } from './bootloaderInfo';
45

56
export const eraseImage = McuManagerModule?.eraseImage as (
67
bleId: string
@@ -10,5 +11,5 @@ export const resetDevice = McuManagerModule?.resetDevice as (
1011
bleId: string
1112
) => Promise<void>;
1213

13-
export { Upgrade, UpgradeMode, UpgradeFileType };
14-
export type { FirmwareUpgradeState, UpgradeOptions };
14+
export { bootloaderInfo, Upgrade, UpgradeMode, UpgradeFileType, MCUBootMode };
15+
export type { BootloaderInfo, FirmwareUpgradeState, UpgradeOptions };

0 commit comments

Comments
 (0)