How the OMI Device Communicates with the Backend via BLE: NimBLE GATT Server Architecture
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 using parameters defined in 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 andaudioCodecCharacteristic(Read) for codec configuration. - Photo Service – Contains
photoDataCharacteristic(Notify) for JPEG chunks andphotoControlCharacteristic(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, 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 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 to the Flutter stream handlers.
End-to-End Communication Flow
- Power-on – The device initializes the NimBLE server and begins advertising OMI services continuously.
- Discovery – The Flutter app scans for and connects to the device using
BLETransport. - Subscription – The client enables notifications on
audioDataCharacteristic,photoDataCharacteristic, andbatteryLevelCharacteristic. - Audio Streaming – The device captures audio, calls
notify()on the audio characteristic, and the app decodes the Opus stream for playback or backend upload. - Photo Capture – The app writes
0x01tophotoControlCharacteristic; the device captures a JPEG, fragments it, and notifies chunks via the photo data characteristic for reassembly. - Battery Monitoring – Periodic
batteryLevelCharacteristic->notify()calls update the mobile UI. - 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
// 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
// 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
// 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
// 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
// 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.cppthat 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
BLETransportfor connection management andOmiConnectionfor 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.cppwith 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →