# react-native-dfu-nitro

[![npm version](https://badge.fury.io/js/react-native-dfu-nitro.svg)](https://badge.fury.io/js/react-native-dfu-nitro)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A high-performance React Native library for Nordic Semiconductor DFU (Device Firmware Update) built on [Nitro Modules](https://nitro.margelo.com/).

Originally developed for [Zyke Band](https://zykeband.com?utm_source=github&utm_medium=readme&utm_campaign=opensource) — a fitness and health tracker created by a small team.

## Features

- **High Performance**: Built on Nitro Modules with JSI for zero-overhead native communication
- **Promise-Based API**: Clean async/await interface with progress callbacks
- **Full DFU Support**: Legacy DFU, Secure DFU (nRF5 SDK), and buttonless DFU
- **Pause / Resume / Abort**: Full control over firmware updates in progress
- **Type-Safe**: Comprehensive TypeScript definitions for all options, states, and errors
- **Expo Ready**: Works with Expo (prebuild required)
- **New Architecture**: Full support for React Native's new architecture
- **Zero Bridge**: Direct JSI communication eliminates bridge bottlenecks

## Companion Library

This library handles **firmware updates only** and is intended to be used together with [react-native-ble-nitro](https://github.com/zykeco/react-native-ble-nitro) for BLE device scanning, connection, and communication. Use `react-native-ble-nitro` to discover and connect to your device, then use `react-native-dfu-nitro` to perform the firmware update.

## Quick Start

### Installation

```bash
npm install react-native-nitro-modules react-native-ble-nitro react-native-dfu-nitro
```

### iOS

```bash
npx pod-install
```

### Android

No additional setup required — the library auto-links.

### Expo

Prebuild and run:

```bash
npx expo prebuild
npx expo run:ios
# or
npx expo run:android
```

## Usage

### Basic DFU

```typescript
import { DfuNitro, DfuState } from 'react-native-dfu-nitro';

const dfu = DfuNitro.instance();

try {
  await dfu.startDfu({
    deviceId: 'AA:BB:CC:DD:EE:FF',
    filePath: 'file:///path/to/firmware.zip',
    onStateChange: (state) => {
      console.log('State:', DfuState[state]);
    },
    onProgress: ({ progress, currentSpeed, part, totalParts }) => {
      console.log(`Part ${part}/${totalParts}: ${progress}% (${currentSpeed} kB/s)`);
    },
  });
  console.log('DFU completed successfully!');
} catch (error) {
  console.error('DFU failed:', error);
}
```

### Using a Custom Manager Instance

```typescript
import { DfuNitroManager } from 'react-native-dfu-nitro';

const dfu = new DfuNitroManager();

await dfu.startDfu({
  deviceId: 'AA:BB:CC:DD:EE:FF',
  filePath: 'file:///path/to/firmware.zip',
});
```

### Pause, Resume, and Abort

```typescript
const dfu = DfuNitro.instance();

// Start DFU (non-blocking — store the promise)
const dfuPromise = dfu.startDfu({
  deviceId: 'AA:BB:CC:DD:EE:FF',
  filePath: 'file:///path/to/firmware.zip',
  onProgress: ({ progress }) => console.log(`${progress}%`),
});

// Pause
dfu.pause();

// Resume
dfu.resume();

// Abort — the promise will reject with a DfuException
dfu.abort();
```

### Error Handling

```typescript
import { DfuNitro, DfuException, DfuError } from 'react-native-dfu-nitro';

const dfu = DfuNitro.instance();

try {
  await dfu.startDfu({
    deviceId: 'AA:BB:CC:DD:EE:FF',
    filePath: 'file:///path/to/firmware.zip',
  });
} catch (error) {
  if (error instanceof DfuException) {
    switch (error.code) {
      case DfuError.DeviceDisconnected:
        console.error('Device disconnected during DFU');
        break;
      case DfuError.FileInvalid:
        console.error('Firmware file is invalid');
        break;
      case DfuError.CrcError:
        console.error('CRC mismatch — retry the update');
        break;
      default:
        console.error(`DFU error [${error.code}]: ${error.message}`);
    }
  }
}
```

### Advanced Configuration

```typescript
await dfu.startDfu({
  deviceId: 'AA:BB:CC:DD:EE:FF',
  filePath: 'file:///path/to/firmware.zip',

  // Force legacy DFU mode (for older bootloaders)
  forceDfu: true,

  // Packet receipt notification (0 = disabled, default varies by platform)
  packetReceiptNotificationParameter: 12,

  // Number of retries on transient failures
  numberOfRetries: 3,

  // Enable buttonless DFU for devices without a physical button
  enableUnsafeExperimentalButtonlessDfu: true,

  // Scan for new address after DFU in legacy mode
  forceScanningForNewAddressInLegacyDfu: true,

  // Disable resume (start from scratch if interrupted)
  disableResume: true,

  onStateChange: (state) => { /* ... */ },
  onProgress: (progress) => { /* ... */ },
});
```

## API Reference

### `DfuNitro`

Singleton wrapper around `DfuNitroManager`.

```typescript
import { DfuNitro } from 'react-native-dfu-nitro';

const dfu = DfuNitro.instance();
```

### `DfuNitroManager`

The main class for performing DFU operations.

| Method | Returns | Description |
|--------|---------|-------------|
| `startDfu(options)` | `Promise<void>` | Start a DFU process. Resolves on completion, rejects on error/abort. |
| `pause()` | `void` | Pause the current DFU process. |
| `resume()` | `void` | Resume a paused DFU process. |
| `abort()` | `void` | Abort the current DFU process. The `startDfu` promise will reject. |

### `DfuManagerOptions`

Extends `DfuOptions` with callback handlers.

| Property | Type | Description |
|----------|------|-------------|
| `onStateChange` | `(state: DfuState) => void` | Called when the DFU state changes. |
| `onProgress` | `(progress: DfuProgress) => void` | Called on upload progress updates. |

### `DfuOptions`

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `deviceId` | `string` | Yes | Bluetooth device address (MAC on Android, UUID on iOS). |
| `filePath` | `string` | Yes | URI to the firmware zip file. |
| `firmwareType` | `DfuFirmwareType` | No | Type of firmware being flashed. |
| `packetReceiptNotificationParameter` | `number` | No | PRN value. `0` to disable. |
| `forceDfu` | `boolean` | No | Force legacy DFU mode. |
| `enableUnsafeExperimentalButtonlessDfu` | `boolean` | No | Enable experimental buttonless DFU. |
| `forceScanningForNewAddressInLegacyDfu` | `boolean` | No | Scan for new address after legacy DFU. |
| `numberOfRetries` | `number` | No | Number of retries on failure. |
| `connectionTimeout` | `number` | No | Connection timeout in milliseconds (iOS only). |
| `dataObjectPreparationDelay` | `number` | No | Delay between data objects in ms (iOS only). |
| `alternativeAdvertisingNameEnabled` | `boolean` | No | Use alternative advertising name (iOS only). |
| `disableResume` | `boolean` | No | Disable resume — always restart from scratch. |

### `DfuProgress`

| Property | Type | Description |
|----------|------|-------------|
| `part` | `number` | Current part number (1-based). |
| `totalParts` | `number` | Total number of parts. |
| `progress` | `number` | Upload progress percentage (0–100). |
| `currentSpeed` | `number` | Current transfer speed (kB/s). |
| `avgSpeed` | `number` | Average transfer speed (kB/s). |

### `DfuState`

```typescript
enum DfuState {
  Connecting,
  Starting,
  EnablingDfuMode,
  Uploading,
  Validating,
  Disconnecting,
  Completed,
  Aborted,
}
```

### `DfuFirmwareType`

```typescript
enum DfuFirmwareType {
  Application,
  Bootloader,
  SoftDevice,
  SoftDeviceBootloader,
  SoftDeviceBootloaderApplication,
}
```

### `DfuException`

Thrown when a DFU process fails. Extends `Error`.

| Property | Type | Description |
|----------|------|-------------|
| `code` | `DfuError` | Machine-readable error code. |
| `message` | `string` | Human-readable error description. |

### `DfuError`

Unified error codes covering both iOS and Android platforms. See [src/specs/NativeDfuNitro.nitro.ts](src/specs/NativeDfuNitro.nitro.ts) for the full list.

**Categories:**

| Range | Category | Examples |
|-------|----------|----------|
| 2–6 | Remote Legacy DFU | `RemoteLegacyDfuCrcError`, `RemoteLegacyDfuOperationFailed` |
| 12–21 | Remote Secure DFU | `RemoteSecureDfuSignatureMismatch`, `RemoteSecureDfuExtendedError` |
| 22–33 | Remote Extended | `RemoteExtendedErrorFwVersionFailure`, `RemoteExtendedErrorInsufficientSpace` |
| 92–97 | Remote Buttonless | `RemoteButtonlessDfuBusy`, `RemoteButtonlessDfuNotBonded` |
| 101–107 | File / Firmware | `FileNotSpecified`, `FileInvalid`, `CrcError` |
| 201–204 | Connection | `FailedToConnect`, `DeviceDisconnected`, `BluetoothDisabled` |
| 301–309 | Service / Protocol | `ServiceDiscoveryFailed`, `DeviceNotSupported`, `BytesLost` |
| 500 | Internal | `InvalidInternalState` |

## Firmware File Preparation

This library expects a **DFU zip package** created with [nRF Util](https://www.nordicsemi.com/Products/Development-tools/nRF-Util) or [nrfutil](https://github.com/NordicSemiconductor/pc-nrfutil).

### Using nRF Util (recommended)

```bash
nrfutil pkg generate \
  --hw-version 52 \
  --sd-req 0x00 \
  --application firmware.hex \
  --application-version 1 \
  firmware.zip
```

### Using legacy nrfutil (Python)

```bash
pip install nrfutil
nrfutil pkg generate \
  --hw-version 52 \
  --sd-req 0x00 \
  --application firmware.hex \
  --application-version 1 \
  firmware.zip
```

The resulting `firmware.zip` can be bundled with your app or downloaded at runtime.

## Platform Notes

### iOS

- Uses [NordicDFU](https://github.com/NordicSemiconductor/IOS-DFU-Library) (CocoaPods, `~> 4.16`)
- `deviceId` is the Core Bluetooth peripheral UUID (not a MAC address)
- `connectionTimeout`, `dataObjectPreparationDelay`, and `alternativeAdvertisingNameEnabled` are iOS-only options
- Requires iOS 16.0+

### Android

- Uses [Android DFU Library](https://github.com/NordicSemiconductor/Android-DFU-Library) (`2.10.1` — pinned because `2.11.0` requires Kotlin 2.3, which is incompatible with React Native's Kotlin 2.1 compiler)
- `deviceId` is the Bluetooth MAC address
- DFU runs in a foreground service (`DfuService`) for reliability
- Requires `FOREGROUND_SERVICE` and `FOREGROUND_SERVICE_CONNECTED_DEVICE` permissions (added automatically)
- Requires `BLUETOOTH_CONNECT` permission at runtime (API 31+)

### Permissions

**iOS** — Add to your `Info.plist`:

```xml
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to update device firmware.</string>
```

**Android** — Add to your `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

## Troubleshooting

| Problem | Solution |
|---------|----------|
| `DfuError.FileNotSpecified` | Ensure `filePath` is a valid file URI (`file:///...`). |
| `DfuError.FileInvalid` | The zip is not a valid DFU package. Re-generate with nRF Util. |
| `DfuError.DeviceDisconnected` | Device went out of range or was turned off during DFU. Move closer and retry. |
| `DfuError.CrcError` | Data corruption during transfer. Retry the DFU. |
| `DfuError.BluetoothDisabled` | Bluetooth is turned off. Prompt the user to enable it. |
| Android: DFU stalls at 0% | Ensure `BLUETOOTH_CONNECT` permission is granted at runtime. |
| iOS: Device not found | Verify you're using the Core Bluetooth UUID, not a MAC address. |

## Architecture

Built on [Nitro Modules](https://nitro.margelo.com/) for direct JSI communication, type-safe native bindings, and high performance.

- **iOS**: Swift implementation using NordicDFU delegates
- **Android**: Kotlin implementation using Nordic DFU foreground service
- **TypeScript**: Promise-based manager layer wrapping native callbacks

```
react-native-dfu-nitro/
├── src/
│   ├── specs/          # Nitro HybridObject specs & types
│   ├── manager.ts      # DfuNitroManager (Promise-based API)
│   ├── singleton.ts    # DfuNitro (singleton wrapper)
│   └── index.ts        # Public exports
├── ios/                # Swift implementation
├── android/            # Kotlin implementation + DfuService
├── nitrogen/generated/ # Auto-generated Nitro bindings
└── nitro.json          # Nitro Modules configuration
```

## Contributing

Contributions are welcome! Fork the repo, make your changes, and submit a pull request.

```bash
git clone https://github.com/zykeco/react-native-dfu-nitro.git
cd react-native-dfu-nitro
npm install
npm run specs   # Regenerate Nitro bindings
npm run typecheck
```

## License

MIT License — see [LICENSE](./LICENSE) file.

## Acknowledgments

- [Zyke Band](https://zykeband.com) — The fitness tracker project that inspired this library
- [Marc Rousavy](https://github.com/mrousavy) — Creator of Nitro Modules
- [Nitro Modules](https://nitro.margelo.com/) — High-performance native module framework
- [Nordic Semiconductor](https://www.nordicsemi.com/) — DFU protocol and native libraries

## Support

- Bug Reports: [GitHub Issues](https://github.com/zykeco/react-native-dfu-nitro/issues)
- Questions: [GitHub Discussions](https://github.com/zykeco/react-native-dfu-nitro/discussions)
