Skip to main content

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.

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

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 modeRelay mode
BoardNesso N1 (ESP32-C6) or Nano 33 BLE SenseNordic Thingy:53
BLE rolePeripheral (server)Central (client) + Peripheral (server)
Data sourceOn-board BMI270 / LSM9DS1EI-Golioth peripheral over BLE
Edge Impulse inferenceOptional (drop in a model/ dir)On the peripheral, not here
KconfigCONFIG_EI_SENSOR_LOCAL=y (auto via board conf)default (CONFIG_EI_SENSOR_LOCAL=n)

Hardware

Sensor-side boards (local sensor mode)

BoardSoCIMUNotes
Arduino Nesso N1ESP32-C6 (RISC-V)Bosch BMI270 (6-axis)Requires west blobs fetch hal_espressif before building; needs Zephyr ≥ v4.1.0
Arduino Nano 33 BLE SensenRF52840ST LSM9DS1 (6-axis) + HTS221 + LPS22HB + APDS9960All sensors on-board, no extra config needed
Any board with a supported IMULSM9DS1, BMI270, or FXOS8700Add a boards/<board>.overlay and a matching .conf

Monitor board (relay mode)

BoardSoCNotes
Nordic Thingy:53nRF5340BLE 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)
  • For relay mode: a running EI-Golioth peripheral (see example-edge-impulse)

1. Initialize the Repository

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:
west blobs fetch hal_espressif
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:
export ZEPHYR_SDK_INSTALL_DIR=$HOME/zephyr-sdk-1.0.1

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)

west build --pristine -b arduino_nesso_n1/esp32c6/hpcore
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.

Arduino Nano 33 BLE Sense (local sensor mode)

west build --pristine -b arduino_nano_33_ble

Nordic Thingy:53 (relay mode)

west build --pristine -b thingy53/nrf5340/cpuapp
Or set your board once in .west/config:
[build]
board = arduino_nesso_n1/esp32c6/hpcore
Then just run west build --pristine.

4. Flash

west flash
For Nordic boards you can specify a runner:
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

# 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 compatibleBoardChannels
st,lsm9ds1Nano 33 BLE SenseACCEL_XYZ, GYRO_XYZ
bosch,bmi270Nesso N1ACCEL_XYZ, GYRO_XYZ
nxp,fxos8700NXP boardsACCEL_XYZ, GYRO_XYZ

GATT service layout

CharacteristicUUID suffixPropertiesPayload
Inference result...def1READ, NOTIFYinference_result_t (label, confidence, timing)
Sensor data...def2READ, NOTIFYfloat[] — raw IMU axes
Device state...def3READ, WRITEStatus 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:
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:
static const char *target_device_name = "MY-DEVICE";

Increase memory for larger EI models

CONFIG_HEAP_MEM_POOL_SIZE=65536
CONFIG_MAIN_STACK_SIZE=32768

Troubleshooting

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.
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.
  • 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:
    CONFIG_I2C_LOG_LEVEL_DBG=y
    CONFIG_SENSOR_LOG_LEVEL_DBG=y
    
  • 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)
  • Confirm the peripheral is powered on and advertising as EI-Golioth
  • Enable BLE scan debug output:
    CONFIG_BT_LOG_LEVEL_DBG=y
    
  • The scan filter matches by exact advertised name — verify the peripheral’s CONFIG_BT_DEVICE_NAME matches exactly
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.

Next Steps

Resources