> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edgeimpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# BLE GATT sensor bridge for Edge Impulse

> Stream IMU data and inference results over BLE using Zephyr — supports Nesso N1 (ESP32-C6), Arduino Nano 33 BLE Sense, and Nordic Thingy:53

<Info>
  This project runs in two modes from the same codebase. In **local sensor mode** a board with an on-board IMU (Nesso N1, Arduino Nano 33 BLE Sense) reads sensor data, optionally runs Edge Impulse inference, and advertises results via BLE GATT to an Android phone. In **relay mode** a Nordic Thingy:53 connects to an `EI-Golioth` peripheral and forwards its inference notifications to Android.\
  Reference code: [https://github.com/edgeimpulse/ei-zephyr-ble-gatt-client](https://github.com/edgeimpulse/ei-zephyr-ble-gatt-client)
</Info>

## Overview

```
  LOCAL SENSOR MODE                       RELAY MODE
  ─────────────────                       ──────────
  ┌────────────────┐  BLE GATT            ┌────────────────┐  BLE GATT (client)  ┌───────────────┐
  │  Nesso N1      │ ──────────────────►  │  Thingy:53     │ ◄────────────────── │  EI-Golioth   │
  │  (ESP32-C6)    │  sensor + inference  │                │   inference notify  │  (nRF52840)   │
  │                │                      │                │                     └───────────────┘
  │  BMI270 IMU    │                      │  Relay + serve │
  └────────────────┘                      └───────┬────────┘
                                                  │ BLE GATT (server)
  ┌────────────────┐  BLE GATT                    │
  │  Nano 33 BLE   │ ──────────────────►          ▼
  │  (nRF52840)    │  sensor + inference   ┌──────────────┐
  │                │                       │  Android app │
  │  LSM9DS1 IMU   │                       └──────────────┘
  └────────────────┘
```

Both modes expose the same GATT service so the same Android app connects to either without changes.

## Modes at a Glance

|                            | Local Sensor mode                                | Relay mode                             |
| -------------------------- | ------------------------------------------------ | -------------------------------------- |
| **Board**                  | Nesso N1 (ESP32-C6) or Nano 33 BLE Sense         | Nordic Thingy:53                       |
| **BLE role**               | Peripheral (server)                              | Central (client) + Peripheral (server) |
| **Data source**            | On-board BMI270 / LSM9DS1                        | EI-Golioth peripheral over BLE         |
| **Edge Impulse inference** | Optional (drop in a `model/` dir)                | On the peripheral, not here            |
| **Kconfig**                | `CONFIG_EI_SENSOR_LOCAL=y` (auto via board conf) | default (`CONFIG_EI_SENSOR_LOCAL=n`)   |

## Hardware

### Sensor-side boards (local sensor mode)

| Board                          | SoC               | IMU                                               | Notes                                                                            |
| ------------------------------ | ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Arduino Nesso N1**           | ESP32-C6 (RISC-V) | Bosch BMI270 (6-axis)                             | Requires `west blobs fetch hal_espressif` before building; needs Zephyr ≥ v4.1.0 |
| **Arduino Nano 33 BLE Sense**  | nRF52840          | ST LSM9DS1 (6-axis) + HTS221 + LPS22HB + APDS9960 | All sensors on-board, no extra config needed                                     |
| Any board with a supported IMU | —                 | LSM9DS1, BMI270, or FXOS8700                      | Add a `boards/<board>.overlay` and a matching `.conf`                            |

### Monitor board (relay mode)

| Board                | SoC     | Notes                                             |
| -------------------- | ------- | ------------------------------------------------- |
| **Nordic Thingy:53** | nRF5340 | BLE 5.3, 1350 mAh Li-Po; no on-board sensors used |

## Prerequisites

* **Zephyr SDK 1.0+** and **West 1.5.0+**
* For Nesso N1 only: the `riscv64-zephyr-elf` toolchain (install with `./setup.sh -t riscv64-zephyr-elf` from the SDK directory) and the Espressif HAL blobs (see [Step 1b](#1b-nesso-n1-fetch-espressif-blobs))
* For relay mode: a running `EI-Golioth` peripheral (see [example-edge-impulse](https://github.com/golioth/example-edge-impulse))

***

## 1. Initialize the Repository

```bash theme={"system"}
west init -m https://github.com/edgeimpulse/ei-zephyr-ble-gatt-client.git
cd ei-zephyr-ble-gatt-client
west update
```

`west update` fetches Zephyr RTOS `main` (required for the Nesso N1 board), the Edge Impulse Zephyr SDK module, and all dependencies.

### 1b. Nesso N1: Fetch Espressif Blobs

ESP32 targets require closed-source binary blobs for the BLE and Wi-Fi stack. Fetch them once after `west update`:

```bash theme={"system"}
west blobs fetch hal_espressif
```

<Note>
  The Arduino Nesso N1 board (`arduino_nesso_n1/esp32c6/hpcore`) is provided by Zephyr `main`. Make sure `west.yml` pins zephyr to `revision: main` and that you have built the **`riscv64-zephyr-elf`** toolchain in your Zephyr SDK install. Export `ZEPHYR_SDK_INSTALL_DIR` so CMake picks up the right SDK:

  ```bash theme={"system"}
  export ZEPHYR_SDK_INSTALL_DIR=$HOME/zephyr-sdk-1.0.1
  ```
</Note>

***

## 2. (Optional) Add an Edge Impulse Model

If you want the sensor board to run local inference and notify the result label over BLE:

1. In Edge Impulse Studio go to **Deployment → Zephyr library**, click **Build**, and download the `.zip`
2. Extract the archive into a `model/` directory **next to** the project root:

```
ei-zephyr-ble-gatt-client/
model/
  CMakeLists.txt
  edge-impulse-sdk/
  model-parameters/
  tflite-model/
```

`CMakeLists.txt` auto-detects the `model/` directory and links the SDK at build time. Without it the firmware streams raw sensor data only.

***

## 3. Build

### Nesso N1 (local sensor mode)

```bash theme={"system"}
west build --pristine -b arduino_nesso_n1/esp32c6/hpcore
```

<Note>
  The board qualifier `/esp32c6/hpcore` selects the RISC-V HP application core. `prj.conf` enables `CONFIG_EI_SENSOR_LOCAL=y`, `CONFIG_BMI270=y`, `CONFIG_I2C=y`, and `CONFIG_SENSOR=y` so no manual Kconfig changes are needed.
</Note>

### Arduino Nano 33 BLE Sense (local sensor mode)

```bash theme={"system"}
west build --pristine -b arduino_nano_33_ble
```

### Nordic Thingy:53 (relay mode)

```bash theme={"system"}
west build --pristine -b thingy53/nrf5340/cpuapp
```

Or set your board once in `.west/config`:

```ini theme={"system"}
[build]
board = arduino_nesso_n1/esp32c6/hpcore
```

Then just run `west build --pristine`.

***

## 4. Flash

```bash theme={"system"}
west flash
```

For Nordic boards you can specify a runner:

```bash theme={"system"}
west flash --runner jlink
west flash --runner nrfjprog
```

For Nesso N1 / ESP32-C6, `west flash` uses the built-in ESP-IDF flasher via the ESP32-C6's native USB-Serial-JTAG port — no external programmer or BOOT/RESET dance required. Just plug in via USB-C and run `west flash`.

***

## 5. Monitor Serial Output

```bash theme={"system"}
# Nordic boards
ls /dev/tty.usbmodem*
minicom -D /dev/tty.usbmodem14401 -b 115200

# Nesso N1 (ESP32-C6) — use the Espressif monitor tool
west espressif monitor
# or
ls /dev/tty.usbserial*
minicom -D /dev/tty.usbserial-0001 -b 115200
```

### Local sensor mode (Nesso N1 / Nano 33 BLE Sense)

Boot log on a Nesso N1 (ESP32-C6) with `west espressif monitor`:

```
*** Booting Zephyr OS build v4.4.0-3391-gc2961410b98f ***
========================================
  Edge Impulse BLE GATT Client
  Mode: Local Sensor Collection
  Board: arduino_nesso_n1/esp32c6/hpcore
========================================
[00:00:00.137,000] <inf> bt_hci_core: HW Platform: Espressif Systems (0x0004)
[00:00:00.137,000] <inf> bt_hci_core: HW Variant: ESP32-C6 (0x0005)
[00:00:00.137,000] <inf> bt_hci_core: Identity: 58:8C:81:50:37:92 (public)
[00:00:00.137,000] <inf> gatt_client: Bluetooth initialized
[00:00:00.139,000] <inf> gatt_server: EI GATT server advertising as "EI-Monitor"
[00:00:00.139,000] <inf> ei_sensor: BMI270 initialised
Starting local sensor collection (sampling every 10 ms)...
[00:00:00.139,000] <inf> ei_sensor: Local sensor loop started (10 ms interval)
```

Without a model the firmware streams raw sensor samples:

```
Sensor data: ax=-0.12 ay=9.81 az=0.04 gx=0.01 gy=-0.02 gz=0.00
Sensor data: ax=-0.11 ay=9.80 az=0.05 gx=0.00 gy=-0.01 gz=0.01
```

With a model (`model/` present at build time):

```
=== Inference Result ===
Label: idle
Confidence: 91.2%
DSP Time: 52 ms
Classification Time: 8 ms
========================
```

### Relay mode (Thingy:53)

```
========================================
  Edge Impulse BLE GATT Client
  Mode: BLE Relay (EI-Golioth Monitor)
========================================

Scanning for EI-Golioth devices...

Device found: EI-Golioth (RSSI: -45 dBm)
Connecting...

>>> Connected to EI-Golioth device!

=== Inference Result ===
Label: wave
Confidence: 97.0%
DSP Time: 47 ms
Classification Time: 6 ms
Total Time: 53 ms
========================
```

***

## How It Works

### Local sensor mode

1. BLE stack + GATT server start advertising as `EI-Monitor`
2. `ei_sensor_init()` binds to the on-board IMU via the Zephyr Sensor API (device resolved at compile time from devicetree)
3. `ei_sensor_run_loop()` samples the IMU every `CONFIG_EI_SENSOR_SAMPLE_INTERVAL_MS` milliseconds (default 10 ms = 100 Hz)
4. Each sample (`[accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z]` in m/s² and rad/s) is notified to connected Android centrals via `gatt_server_notify_sensor_data()`
5. If a model is present and the feature buffer is full, `run_classifier()` fires and the label/confidence is notified via `gatt_server_notify_inference()`

### Relay mode

1. BLE stack starts in both Central and Peripheral roles (`CONFIG_BT_MAX_CONN=2`)
2. GATT client scans for `EI-Golioth` by name, connects, discovers the EI service, and subscribes to the inference characteristic
3. GATT server simultaneously advertises as `EI-Monitor` so Android can connect
4. Inference notifications from the peripheral are forwarded to Android via `gatt_server_notify_inference()`

### Sensor driver (`ei_sensor.cpp`)

The IMU is selected at compile time via `DT_HAS_COMPAT_STATUS_OKAY()`:

| Devicetree compatible | Board             | Channels                |
| --------------------- | ----------------- | ----------------------- |
| `st,lsm9ds1`          | Nano 33 BLE Sense | `ACCEL_XYZ`, `GYRO_XYZ` |
| `bosch,bmi270`        | Nesso N1          | `ACCEL_XYZ`, `GYRO_XYZ` |
| `nxp,fxos8700`        | NXP boards        | `ACCEL_XYZ`, `GYRO_XYZ` |

### GATT service layout

| Characteristic   | UUID suffix | Properties   | Payload                                          |
| ---------------- | ----------- | ------------ | ------------------------------------------------ |
| Inference result | `...def1`   | READ, NOTIFY | `inference_result_t` (label, confidence, timing) |
| Sensor data      | `...def2`   | READ, NOTIFY | `float[]` — raw IMU axes                         |
| Device state     | `...def3`   | READ, WRITE  | Status flags                                     |

***

## Project Structure

```
ei-zephyr-ble-gatt-client/
├── CMakeLists.txt                          # Auto-detects model/ for optional inference
├── Kconfig                                 # EI_SENSOR_LOCAL, EI_SENSOR_SAMPLE_INTERVAL_MS
├── prj.conf                                # BLE central + peripheral, C++17, logging
├── west.yml                                # Zephyr v4.0.0 + EI SDK Zephyr module
├── boards/
│   ├── arduino_nesso_n1.conf              # Sets EI_SENSOR_LOCAL=y, CONFIG_BMI270=y
│   ├── arduino_nesso_n1.overlay           # Placeholder (BMI270 in upstream DTS)
│   ├── arduino_nano_33_ble.conf           # Sets EI_SENSOR_LOCAL=y, all sensor drivers
│   ├── arduino_nano_33_ble.overlay        # Enables all on-board sensors
│   └── thingy53_nrf5340_cpuapp.overlay    # Routes UART0 for serial console
└── src/
    ├── main.cpp                            # Mode switch, startup, callbacks
    ├── ble/
    │   ├── gatt_client.cpp / .h           # BLE Central: scan, connect, subscribe
    │   └── gatt_server.cpp / .h           # BLE Peripheral: advertise, notify Android
    └── sensors/
        ├── ei_sensor.cpp                   # IMU read loop, optional EI inference
        └── ei_sensor.h                     # Public API + EI_SENSOR_IMU_AXES constant
```

***

## Customising the Example

### Adjust the sensor sampling rate

In `prj.conf` or a board-specific `.conf`:

```ini theme={"system"}
CONFIG_EI_SENSOR_SAMPLE_INTERVAL_MS=20   # 50 Hz instead of 100 Hz
```

### Port to a different board

1. Add `boards/<your_board>.overlay` and `boards/<your_board>.conf`
2. In `.conf`, set `CONFIG_EI_SENSOR_LOCAL=y` and enable the IMU driver (e.g. `CONFIG_ICM42688_P=y`)
3. In `.overlay`, enable the IMU node if it is not already on in the upstream board DTS
4. Add a `DT_HAS_COMPAT_STATUS_OKAY(your_compat)` branch in `src/sensors/ei_sensor.cpp`

### Change the target peripheral name (relay mode)

In `src/ble/gatt_client.cpp`:

```cpp theme={"system"}
static const char *target_device_name = "MY-DEVICE";
```

### Increase memory for larger EI models

```ini theme={"system"}
CONFIG_HEAP_MEM_POOL_SIZE=65536
CONFIG_MAIN_STACK_SIZE=32768
```

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nesso N1 build error: board not found">
    The Nesso N1 was added after Zephyr v4.0.0. Update `west.yml` to `revision: v4.1.0` (or `main`) and run `west update` again.
  </Accordion>

  <Accordion title="Nesso N1 flash error or blank serial output">
    ESP32-C6 requires binary blobs. Run `west blobs fetch hal_espressif` once after `west update`. The Nesso N1 uses native USB-Serial-JTAG so no BOOT/RESET sequence is needed — flashing is fully automatic over USB-C. If the serial console is blank, use `west espressif monitor` instead of minicom to open the port.
  </Accordion>

  <Accordion title="IMU not detected at runtime (local sensor mode)">
    * Check the serial log for the `device not ready` error — it prints the compatible string it searched for
    * Verify the IMU node has `status = "okay"` in the devicetree
    * Enable I²C and sensor debug logs:
      ```ini theme={"system"}
      CONFIG_I2C_LOG_LEVEL_DBG=y
      CONFIG_SENSOR_LOG_LEVEL_DBG=y
      ```
  </Accordion>

  <Accordion title="Android cannot connect (local sensor mode)">
    * Confirm the board is advertising; a BLE scan on the phone should show `EI-Monitor`
    * Check that `gatt_server_init()` completed without error in the serial log
    * Only one Android central can connect at a time (`CONFIG_BT_MAX_CONN=2` reserves one slot for the EI-Golioth peripheral in relay mode)
  </Accordion>

  <Accordion title="Relay mode: no EI-Golioth device found during scan">
    * Confirm the peripheral is powered on and advertising as `EI-Golioth`
    * Enable BLE scan debug output:
      ```ini theme={"system"}
      CONFIG_BT_LOG_LEVEL_DBG=y
      ```
    * The scan filter matches by exact advertised name — verify the peripheral's `CONFIG_BT_DEVICE_NAME` matches exactly
  </Accordion>

  <Accordion title="No serial output on Thingy:53">
    Set your terminal to **115200 baud, 8N1**. On macOS use `ls /dev/tty.usbmodem*` to find the port. The `boards/thingy53_nrf5340_cpuapp.overlay` already routes `zephyr,console` to `uart0`.
  </Accordion>
</AccordionGroup>

***

## Next Steps

* [Zephyr series overview](./zephyr-module-series)
* [IMU inference on Zephyr](./zephyr-module-imu) — build a standalone inference node
* [ei-zephyr-golioth-integration](https://golioth.io/integrations/edge-impulse) — the relay-mode peripheral with Golioth cloud sync

## Resources

* [GitHub: ei-zephyr-ble-gatt-client](https://github.com/edgeimpulse/ei-zephyr-ble-gatt-client)
* [Arduino Nesso N1 in Zephyr](https://docs.zephyrproject.org/latest/boards/arduino/nesso_n1/doc/index.html)
* [Arduino Nano 33 BLE board guide](https://docs.zephyrproject.org/latest/boards/arduino/nano_33_ble/doc/index.html)
* [Nordic Thingy:53 board guide](https://docs.zephyrproject.org/latest/boards/nordic/thingy53/doc/index.html)
* [Zephyr Sensor API](https://docs.zephyrproject.org/latest/hardware/peripherals/sensor/index.html)
* [Zephyr Bluetooth GATT](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gatt.html)
