Choose a board
Both routes use the same Edge Impulse project steps. They differ in how data reaches Edge Impulse and how the model gets back onto the board.
| Nano 33 BLE Sense (original or Rev2) | ESP32 + MPU6050 | |
|---|---|---|
| Chip and RAM | nRF52840 at 64 MHz, 256 KB SRAM, 1 MB flash (Arduino) | 520 KB internal SRAM (ESP-IDF memory types) |
| Accelerometer | Built in, no wiring | MPU6050 breakout on I2C |
| Data collection | Edge Impulse firmware, sampled from Studio | Your own sketch plus the Edge Impulse data forwarder |
| Running the model | Ready-made example in the generated library | Sketch below, on ESP32 Arduino core 2.0.4 |
The Nano is the shorter path because Edge Impulse fully supports it (board guide). The two Nano revisions use different motion sensors, so you'll need to know which one you have: Rev2 boards have NANO 33 BLE SENSE REV2 printed on the underside.
A model learns the sensor it was trained on. If you build both versions, use a separate Edge Impulse project for each board.
Install the tools
- An Edge Impulse account. The Developer plan is aimed at individual developers and prototyping; the pricing page lists its current limits.
- Python 3 and Node.js 16 or later. Edge Impulse recommends Node.js 22. On Windows, accept the installer's offer to add the tools for native modules.
- Arduino CLI, which the Nano's firmware flash script uses, and Arduino IDE 2 for building the final sketch.
- On Linux, GNU Screen (
sudo apt install screenon Debian and Ubuntu).
Then install the Edge Impulse CLI, as its installation page shows:
npm install -g edge-impulse-cli
Check: in PowerShell, Get-Command edge-impulse-daemon, edge-impulse-data-forwarder lists both commands. On Linux or macOS, command -v edge-impulse-daemon edge-impulse-data-forwarder prints two paths. arduino-cli version should also work.
If PowerShell answers with "The execution of scripts is disabled on this system", it is blocking the script wrappers npm creates. Edge Impulse's troubleshooting fixes this by changing PowerShell's execution policy. Set-ExecutionPolicy -Scope CurrentUser RemoteSigned limits the change to your own account and still blocks unsigned scripts downloaded from the internet.
In Edge Impulse Studio, create a new project for this board before moving on.
Connect a Nano 33 BLE Sense
Skip to the ESP32 section if that's your board.
- Connect the board with a micro-USB data cable and press RESET twice quickly. The LED starts pulsing to show the bootloader is running.
- Download the Edge Impulse firmware for the Nano 33 BLE Sense and unzip it. The same firmware works on both revisions.
- Run the flash script for your system:
flash_windows.bat,flash_mac.commandorflash_linux.sh. When it finishes, press RESET once. - Run
edge-impulse-daemon, log in, and pick the project you created. Add--cleanlater if you want to switch the board to another project.
Check: the board appears on the project's Devices page with a green dot. Recent versions of Chrome and Edge can also connect to the board from Studio directly, without the daemon.
If the flash script fails, double-press RESET again so the LED pulses, then retry. Make sure the cable carries data. Continue at Collect training data.
Connect an ESP32 and MPU6050
Wire the MPU6050
The wiring uses the same I2C pins as the ESP32 BME280 weather station. You can leave the BME280 connected, since it uses a different address.
- VCC to 3V3, GND to GND
- SCL to GPIO22, SDA to GPIO21
- AD0 to GND, which sets the address to 0x68 (tie it to 3V3 instead for 0x69)
Check: the I2C scan sketch from the weather station guide prints Device found at 0x68.
Install the core version Edge Impulse tests
Edge Impulse tests its ESP32 Arduino examples with Arduino-ESP32 core 2.0.4. On the 3.x cores, the generated library can fail to compile with either all initializer clauses should be designated or none of them should be (Edge Impulse Arduino deployment docs). Install 2.0.4 for this project. In Boards Manager, pick version 2.0.4 of esp32 by Espressif Systems from the version list, or use the CLI:
arduino-cli core install esp32:esp32@2.0.4 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli lib install "Adafruit MPU6050@2.2.9"
Only one version of a core is installed at a time, so this replaces any newer ESP32 core. The cleanup section shows how to switch back. In the IDE, install Adafruit MPU6050 2.2.9 from Library Manager and accept its dependencies.
Stream accelerometer data
The data forwarder reads one line per sample over serial at 115200 baud, with the axes separated by commas. This sketch sends 50 samples a second in m/s². The accelerometer range and filter settings matter: the sketch that runs the model must use exactly the same ones.
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
constexpr int SDA_PIN = 21;
constexpr int SCL_PIN = 22;
constexpr uint8_t MPU_ADDR = 0x68; // 0x69 if AD0 is tied to 3V3
constexpr float SAMPLE_HZ = 50.0F; // pass the same value to --frequency
constexpr unsigned long INTERVAL_US = (unsigned long)(1000000.0F / SAMPLE_HZ);
Adafruit_MPU6050 mpu;
unsigned long lastSampleUs = 0;
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(SDA_PIN, SCL_PIN);
if (!mpu.begin(MPU_ADDR, &Wire)) {
Serial.println("MPU6050 not found");
while (true) {
delay(1000);
}
}
// Keep these two settings identical in the classification sketch.
mpu.setAccelerometerRange(MPU6050_RANGE_4_G);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); // below half of 50 Hz
lastSampleUs = micros();
}
void loop() {
if (micros() - lastSampleUs < INTERVAL_US) {
return;
}
lastSampleUs += INTERVAL_US;
sensors_event_t accel, gyro, temp;
mpu.getEvent(&accel, &gyro, &temp);
// One reading per line, comma-separated, in m/s^2
Serial.printf("%.3f,%.3f,%.3f\n",
accel.acceleration.x, accel.acceleration.y, accel.acceleration.z);
}
Select ESP32 Dev Module and upload. From the CLI, after arduino-cli sketch new MPU6050Forwarder and pasting the code in (use your own port):
arduino-cli compile --fqbn esp32:esp32:esp32 MPU6050Forwarder
arduino-cli upload -p COM5 --fqbn esp32:esp32:esp32 MPU6050Forwarder
Check: in the Serial Monitor at 115200 baud, a board lying flat prints lines like 0.120,-0.310,9.790, with gravity, about 9.8 m/s², on whichever axis points up. Tilt it and the value moves to another axis. Close the Serial Monitor afterwards, because the forwarder needs the port.
Start the data forwarder
edge-impulse-data-forwarder --frequency 50
Log in, choose your project, name the three axes accX, accY, accZ when asked, and give the device a name. Leave it running while you collect data. If you change the sample rate or axis count in the sketch later, run it once with --clean to reset the stored configuration.
Check: the device shows up on the project's Devices page.
Collect training data
Record three classes, one label each:
circle: draw circles in the air with the board.shake: shake it side to side.idle: hold it still or leave it on the desk. Without this class the model would have to call every moment either a circle or a shake.
In Studio, open Data acquisition and under Record new data:
- Select your device.
- Nano: choose Built-in accelerometer at 62.5Hz, the setting Edge Impulse's motion recognition tutorial uses. ESP32: the sensor axes and the 50 Hz rate come from the forwarder configuration.
- Enter the label and a sample length of
10000ms. - Click Start sampling and keep moving for the whole 10 seconds. Edge Impulse cuts each recording into short windows, so continuous movement matters.
Repeat until each class has around three minutes of data. Vary speed, grip and orientation, and if you can, have another person record a few samples. Keep some samples of every class in the test set, which Data acquisition lists separately, so you can test the model on recordings it never trained on.
Check: open a sample. You should see three lines, one per axis, that look different for each class.
Design the impulse
An impulse is Edge Impulse's name for the pipeline from raw data to a prediction. On Create impulse:
- Set the window size to
2000ms and the window increase to80ms. Each prediction looks at two seconds of motion, and windows overlap so training gets many examples from each recording. - Add the Spectral Analysis processing block, which pulls frequency and power features out of repetitive motion.
- Add the Classification learning block, then click Save impulse.
- Open Spectral features, click Save parameters, then Generate features.
Check: the feature explorer plots every window. Points from the same class should form clusters. If circle and shake points are mixed together, record more varied data before training.
Train and test the model
- Open the classifier page (NN Classifier), set the number of training cycles to
30, and click Start training. - Read the accuracy and the confusion matrix. The matrix shows which classes the model mixes up.
- Open Live classification, record a new 10-second sample while doing one gesture, and check the result.
Very high accuracy isn't automatically good. Edge Impulse warns that 100% can mean the network is overfitting, matching the training data closely but doing badly on new movement. If live results are poor, the motion recognition tutorial lists the usual causes:
- Too little data.
- New movement that doesn't look like the training data. Add those recordings to the training set.
- Too few training cycles.
- Overfitting. Lower the learning rate or add more data.
- A network that doesn't suit the data. Try changing the number of layers and neurons.
Pick int8 or float32
Microcontrollers have little RAM, so models are usually quantized: weights stored as 8-bit integers instead of 32-bit floats, which takes a quarter of the space and lets the chip use integer maths. How much accuracy that costs depends on the model, so measure it instead of assuming.
On the Deployment page, Edge Impulse lets you choose between Quantized (int8) and Unoptimized (float32) and shows estimated latency, flash and RAM for each. It also offers the EON Compiler, which aims to cut RAM and flash use without changing accuracy (deployment docs).
Check: click Run model testing and compare the int8 and float32 results on your test set. If int8 is close, use it. If it falls well short, use float32, as long as the estimated RAM fits your board.
Build the Arduino library
- On Deployment, search for and select Arduino library, then click Build. A
.zipfile downloads. - In Arduino IDE, choose Sketch ā Include Library ā Add .ZIP Library and select the file.
- Restart the IDE if the new examples don't show up under File ā Examples ā (your project name)_inferencing. Edge Impulse's docs also show this menu entry as "Your project name - Edge Impulse".
Run it on the Nano
- Install Arduino Mbed OS Nano Boards in Boards Manager (
arduino-cli core install arduino:mbed_nanodoes the same), then select Arduino Nano 33 BLE and the board's port. - Open File ā Examples ā (your project name)_inferencing, then the accelerometer example under
nano_ble33_sensefor the original board ornano_ble33_sense_rev2for Rev2. The two use different sensors, so the wrong one won't read motion data. - Upload, then open the Serial Monitor at 115200 baud.
Uploading this sketch replaces the Edge Impulse data-collection firmware. Flash it again with the script from earlier if you want to record more data.
Run it on the ESP32
The library's header is named after your project. To get that name right, start from the generated static_buffer example:
- Open File ā Examples ā (your project name)_inferencing ā static_buffer ā static_buffer and use File ā Save As to save a copy named
GestureClassifier. - In the copy, keep the line that includes
..._inferencing.h, delete everything else, and paste the code below underneath it. - Select ESP32 Dev Module on core 2.0.4 and upload. The first build compiles the whole Edge Impulse SDK, so it takes longer than a normal sketch.
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
constexpr int SDA_PIN = 21;
constexpr int SCL_PIN = 22;
constexpr uint8_t MPU_ADDR = 0x68;
// Sample at the rate the model was trained with.
constexpr unsigned long INTERVAL_US =
(unsigned long)(1000000.0F / EI_CLASSIFIER_FREQUENCY);
Adafruit_MPU6050 mpu;
static float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t featureIx = 0;
unsigned long lastSampleUs = 0;
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(SDA_PIN, SCL_PIN);
if (!mpu.begin(MPU_ADDR, &Wire)) {
Serial.println("MPU6050 not found");
while (true) {
delay(1000);
}
}
if (EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME != 3) {
Serial.println("This sketch expects a model trained on 3 accelerometer axes");
while (true) {
delay(1000);
}
}
// Same settings as MPU6050Forwarder.ino
mpu.setAccelerometerRange(MPU6050_RANGE_4_G);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
lastSampleUs = micros();
}
void loop() {
if (micros() - lastSampleUs < INTERVAL_US) {
return;
}
lastSampleUs += INTERVAL_US;
sensors_event_t accel, gyro, temp;
mpu.getEvent(&accel, &gyro, &temp);
features[featureIx++] = accel.acceleration.x;
features[featureIx++] = accel.acceleration.y;
features[featureIx++] = accel.acceleration.z;
if (featureIx < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
return; // keep filling the two-second window
}
featureIx = 0;
signal_t signal;
int err = numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
Serial.printf("signal_from_buffer failed (%d)\n", err);
return;
}
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);
if (res != EI_IMPULSE_OK) {
Serial.printf("run_classifier failed (%d)\n", (int)res);
return;
}
Serial.printf("DSP %d ms, classification %d ms\n",
result.timing.dsp, result.timing.classification);
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
Serial.printf(" %s: %.3f\n",
result.classification[ix].label, result.classification[ix].value);
}
lastSampleUs = micros(); // start the next window after classification finishes
}
The sketch fills a two-second buffer at the model's sample rate, classifies it, then starts a new window. The data forwarder's Arduino classification example in the Edge Impulse docs works the same way.
Test on the board
Open the Serial Monitor at 115200 baud. Each result shows how long feature extraction (DSP) and classification took, then a score between 0 and 1 for each label. The highest score is the model's answer.
Check: start a gesture before a new window begins and keep it going for the full two seconds. Changing movement halfway through a window mixes the two and gives unreliable scores. Leaving the board still should give a high idle score.
If the board's results don't match what Live classification showed in Studio, test the model on its own: Edge Impulse's static buffer procedure has you paste a sample's raw features from Live classification into the static_buffer example. If that output matches Studio, the model is fine and the difference is in how the sketch reads the sensor.
Troubleshooting
either all initializer clauses should be designated or none of them should be(ESP32). You're compiling on a 3.x core. Install core 2.0.4 as described above.Multiple libraries were found for .... Each Edge Impulse build is a new library version. Delete the older copies fromDocuments\Arduino\librarieson Windows,~/Documents/Arduino/libraries/on macOS, or~/Arduino/librarieson Linux (Arduino IDE 2 defaults; File ā Preferences ā Sketchbook location shows yours).- The forwarder or daemon can't open the port. Only one program can use the serial port at a time. Close the IDE's Serial Monitor and any other terminal on that port.
- The forwarder reports the wrong frequency or axis count. Run
edge-impulse-data-forwarder --clean, then start it again with--frequency 50. - Nano firmware flashing fails. Put the board back into bootloader mode with a quick double press on RESET and retry with a data cable. On Linux, make sure GNU Screen is installed.
MPU6050 not found. Run the I2C scan. No device means a wiring problem; 0x69 means AD0 is high, so changeMPU_ADDRin both sketches.- Good results in Studio, poor results on the ESP32. Check that the range, filter and sample rate in
GestureClassifier.inomatch the forwarder sketch. The model only knows data captured that way.
What to plan around
- RAM sets the ceiling. The model's weights go in flash, but feature extraction and inference need working memory too. The Deployment page's RAM estimate is the number to compare with your board.
- Data variety matters more than training time. A model trained on one person's way of moving may not work for someone else. Recording more people and grips helps more than adding training cycles.
- Test after quantizing. The int8 model is a different model. Run model testing on the version you deploy.
- Treat sensor settings as part of the model. Changing the accelerometer, its range, its filter or the sample rate means collecting new data and training again.
Going further
- Sound instead of motion. The Nano 33 BLE Sense also has a microphone. Edge Impulse's sound recognition tutorial follows the same collect, design, train and deploy flow with audio processing blocks. Use the microphone examples in the generated library to run it.
- Unknown movements. The motion recognition tutorial adds an anomaly detection block that flags movement unlike anything in the training data, instead of forcing it into one of the classes.
- Training outside Edge Impulse. To bring your own TensorFlow model, convert it with the LiteRT post-training integer quantization workflow and run it with TensorFlow Lite Micro, or Espressif's esp-tflite-micro component on ESP32. Their
hello_worldexamples show the pattern: register the operations the model uses withtflite::MicroMutableOpResolver, givetflite::MicroInterpretera tensor arena, and confirmAllocateTensors()returnskTfLiteOkbefore callingInvoke().
Cleanup
- Stop the data forwarder or daemon with Ctrl+C.
- Delete old
..._inferencingfolders from your Arduino libraries folder (paths in troubleshooting) once you've finished with them. - If you installed ESP32 core 2.0.4 for this project, switch back to the current stable core for other projects with
arduino-cli core install esp32:esp32@3.3.12 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json, or by picking 3.3.12 in Boards Manager. - Remove the Edge Impulse CLI with
npm uninstall -g edge-impulse-cliif you no longer need it. - Your recordings stay in the Edge Impulse project until you delete them or the project.
Related guides
- ESP32 BME280 Weather Station covers the ESP32 board setup, the I2C scan used above, and logging serial output to a file with arduino-cli.
- WLED Smart LED Controller runs WLED on an ESP32 and accepts commands over its JSON API, which makes it a visible output for gestures if you send recognised results to it from another device.
- Raspberry Pi Setup Guide sets up a small Linux machine that can collect results from several boards.



