Skip to main content
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

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

Modes at a Glance

Hardware

Sensor-side boards (local sensor mode)

Monitor board (relay mode)

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 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:
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:

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:
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)

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)

Nordic Thingy:53 (relay mode)

Or set your board once in .west/config:
Then just run west build --pristine.

4. Flash

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

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

Boot log on a Nesso N1 (ESP32-C6) with west espressif monitor:
Without a model the firmware streams raw sensor samples:
With a model (model/ present at build time):

Relay mode (Thingy:53)


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():

GATT service layout


Project Structure


Customising the Example

Adjust the sensor sampling rate

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

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:

Increase memory for larger EI models


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:
  • 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:
  • 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