# How the OMI Device Communicates with the Backend via BLE: NimBLE GATT Server Architecture

> Learn how the OMI device uses NimBLE's GATT server to stream audio photos and sensor data over BLE to a Flutter mobile app which forwards it to the cloud backend.

- Repository: [omi/omi](https://github.com/basedhardware/omi)
- Tags: internals
- Published: 2026-02-26

---

**The OMI wearable uses a NimBLE-based GATT server to stream audio, photos, and sensor data over Bluetooth Low Energy, while the Flutter mobile app acts as a BLE client that discovers services, subscribes to notifications, and forwards data to the cloud backend.**

The **basedhardware/omi** open-source project implements a low-power communication pipeline between the OMI Glass wearable and backend services. The architecture relies entirely on **Bluetooth Low Energy (BLE)** for device-to-mobile transmission, with the Flutter application serving as the bridge to cloud infrastructure while the ESP32S3 firmware manages the peripheral BLE stack.

## Device-Side BLE Architecture: NimBLE GATT Server

The firmware running on the OMI Glass implements a **NimBLE-based GATT server** that exposes multiple services for bi-directional data flow. All BLE configuration resides in the ESP32 firmware source tree under `omiGlass/firmware/`.

### Initializing the BLE Stack and Advertising

The device initializes the NimBLE stack in [`omiGlass/firmware/src/app.cpp`](https://github.com/basedhardware/omi/blob/main/omiGlass/firmware/src/app.cpp) using parameters defined in [`config.h`](https://github.com/basedhardware/omi/blob/main/config.h). The firmware creates a `BLEServer` instance, sets the device name via `BLE_DEVICE_NAME`, and configures continuous advertising with `BLE_ADV_TIMEOUT_MS = 0` to remain discoverable when disconnected. The advertising intervals are power-optimized to balance discovery speed with battery consumption.

### GATT Services and Characteristics

The firmware instantiates four primary services using `server->createService(uuid)`:

- **Audio Service** – Contains `audioDataCharacteristic` (Notify) for streaming encoded audio frames and `audioCodecCharacteristic` (Read) for codec configuration.
- **Photo Service** – Contains `photoDataCharacteristic` (Notify) for JPEG chunks and `photoControlCharacteristic` (Write) for capture commands.
- **Battery Service** – Contains `batteryLevelCharacteristic` (Notify) for power monitoring.
- **Device Info Service** – Standard manufacturer, model, and firmware version strings (Read).

Each characteristic is created with specific properties (`PROPERTY_NOTIFY`, `PROPERTY_WRITE`, `PROPERTY_READ`) and includes Client Characteristic Configuration (CCC) descriptors (`BLE2902`) to enable subscription management.

### Streaming Data via Notifications

When sensor data is ready, the device pushes payloads to subscribed clients using `setValue()` followed by `notify()`. In [`app.cpp`](https://github.com/basedhardware/omi/blob/main/app.cpp), the firmware calls `audioDataCharacteristic->notify()` to stream Opus-encoded audio packets, `photoDataCharacteristic->notify()` to transmit fragmented JPEG data, and `batteryLevelCharacteristic->notify()` for power updates.

### Handling Control Commands and OTA

For downstream communication, the device registers callback classes using `setCallbacks()`. When the mobile app writes to control characteristics like `photoControlCharacteristic`, the `onWrite()` handler triggers device functions such as `startPhotoCapture()`. The OTA update mechanism in [`ota.cpp`](https://github.com/basedhardware/omi/blob/main/ota.cpp) handles firmware binary writes through a dedicated OTA service characteristic, with status responses sent via `ota_notify_status()` callbacks.

## Mobile App BLE Client Implementation

The Flutter application in `app/lib/services/devices/` implements the BLE central role, managing discovery, connection, and protocol parsing.

### Device Discovery and Connection

The `BluetoothDiscoverer` class scans for peripherals advertising OMI service UUIDs and filters by device name. Once identified, `BLETransport` establishes the connection, discovers the GATT table using `discoverAllServicesAndCharacteristics()`, and caches characteristic handles for low-latency subsequent operations.

### Subscribing to Data Streams

After connection, the app enables notifications on upstream characteristics using `setNotifyValue(true)`. The `OmiConnection` class consumes these notification streams, parsing BLE packets and handling fragmentation for large payloads like photos. Audio frames route to the Opus decoder, while photo chunks accumulate in `_photoAssembler` until the complete JPEG is reconstructed.

### Sending Commands and OTA Updates

For downstream commands, the app writes byte arrays to control characteristics using `writeCharacteristic()`. The `BLETransport` class wraps these writes with retry logic and timeouts. OTA updates stream the firmware binary to the device's OTA data characteristic, with progress notifications flowing back from [`ota.cpp`](https://github.com/basedhardware/omi/blob/main/ota.cpp) to the Flutter stream handlers.

## End-to-End Communication Flow

1. **Power-on** – The device initializes the NimBLE server and begins advertising OMI services continuously.
2. **Discovery** – The Flutter app scans for and connects to the device using `BLETransport`.
3. **Subscription** – The client enables notifications on `audioDataCharacteristic`, `photoDataCharacteristic`, and `batteryLevelCharacteristic`.
4. **Audio Streaming** – The device captures audio, calls `notify()` on the audio characteristic, and the app decodes the Opus stream for playback or backend upload.
5. **Photo Capture** – The app writes `0x01` to `photoControlCharacteristic`; the device captures a JPEG, fragments it, and notifies chunks via the photo data characteristic for reassembly.
6. **Battery Monitoring** – Periodic `batteryLevelCharacteristic->notify()` calls update the mobile UI.
7. **OTA Updates** – The app writes firmware chunks to the OTA characteristic; the device writes status responses and reboots upon completion.

## Code Implementation Examples

### Creating an Audio Service and Notifying Data

```cpp
// omiGlass/firmware/src/app.cpp
BLEService *service = server->createService(AUDIO_SERVICE_UUID);

audioDataCharacteristic = service->createCharacteristic(
    AUDIO_DATA_CHAR_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
);
audioDataCharacteristic->addDescriptor(new BLE2902());

// When audio packet is ready:
audioDataCharacteristic->setValue(audio_packet_buffer, packet_len);
audioDataCharacteristic->notify();

```

### Receiving Control Commands on the Device

```cpp
// omiGlass/firmware/src/app.cpp
photoControlCharacteristic = service->createCharacteristic(
    PHOTO_CONTROL_CHAR_UUID,
    BLECharacteristic::PROPERTY_WRITE
);
photoControlCharacteristic->setCallbacks(new PhotoControlCallback());

class PhotoControlCallback : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *c) override {
        startPhotoCapture();
    }
};

```

### Connecting and Subscribing in Flutter

```dart
// app/lib/services/devices/transports/ble_transport.dart
await _peripheral.discoverAllServicesAndCharacteristics();
final audioChar = await _peripheral.getCharacteristic(
    serviceUuid: audioServiceUuid,
    characteristicUuid: audioDataCharUuid,
);
await audioChar.setNotifyValue(true);
audioChar.value.listen(_handleAudioPacket);

```

### Writing Control Commands from the App

```dart
// app/lib/services/devices/omi_connection.dart
final photoCtrlChar = await _peripheral.getCharacteristic(
    serviceUuid: photoServiceUuid,
    characteristicUuid: photoControlCharUuid,
);
await photoCtrlChar.write([0x01]); // Trigger capture

```

### Reassembling Fragmented Photo Data

```dart
// app/lib/services/devices/omi_connection.dart
await photoDataChar.setNotifyValue(true);
photoDataChar.value.listen((fragment) {
  _photoAssembler.addFragment(fragment);
  if (_photoAssembler.isComplete) {
    final Uint8List jpeg = _photoAssembler.build();
    displayImage(jpeg);
  }
});

```

## Summary

- The OMI device implements a **NimBLE GATT server** in [`app.cpp`](https://github.com/basedhardware/omi/blob/main/app.cpp) that exposes notification characteristics for audio, photos, and battery data.
- **Four primary services** handle audio streaming, photo transfer, battery monitoring, and device information using standard BLE properties.
- The Flutter app acts as the BLE client, using `BLETransport` for connection management and `OmiConnection` for packet parsing and fragmentation handling.
- **Bidirectional communication** occurs through notify operations (device-to-app) and write operations (app-to-device) on specific characteristics.
- OTA firmware updates use a dedicated service in [`ota.cpp`](https://github.com/basedhardware/omi/blob/main/ota.cpp) with binary streaming and status notification callbacks.

## Frequently Asked Questions

### How does the OMI device handle large photo transfers over BLE?

The device fragments JPEG images into multiple packets and transmits them sequentially through the `photoDataCharacteristic` using `notify()` calls. The Flutter app's `_photoAssembler` collects these fragments in `omi_connection.dart` and reconstructs the complete image once all chunks arrive, handling the MTU limitations inherent in BLE 4.x/5.x.

### What BLE library does the OMI firmware use?

The firmware uses the **NimBLE-Arduino** library, which provides the `BLEServer`, `BLEService`, and `BLECharacteristic` classes. This implementation resides in the ESP32S3 firmware under `omiGlass/firmware/` and offers lower memory footprint compared to the legacy Bluedroid stack.

### Can the OMI device communicate with the backend without the mobile app?

No. The device requires the Flutter mobile application as a BLE-to-IP bridge. The device communicates exclusively via BLE to the phone; the app then forwards audio, photos, and telemetry to cloud backend services. The `DeviceProvider` class coordinates BLE and Wi-Fi sync modes to prevent connection conflicts.

### What is the power consumption strategy for BLE advertising?

The firmware configures `BLE_ADV_TIMEOUT_MS = 0` in [`config.h`](https://github.com/basedhardware/omi/blob/main/config.h) for continuous advertising while disconnected, but uses power-optimized intervals to minimize battery drain. Once connected, the device stops advertising and maintains the connection with negotiated connection intervals suitable for high-throughput audio streaming.