> ## 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.

# Cyclist Fall Detection and Alert - Arduino Nesso N1

> Train a model to detect a bike crash, and when confirmed send an SMS to emergency contact.

Created By: Haziqa Sajid

Public Project Links

* Bike Fall Detection: [https://studio.edgeimpulse.com/public/1029020/live](https://studio.edgeimpulse.com/public/1029020/live)

GitHub Repo: [https://github.com/haziqa5122/nesso-n1-fall-detection](https://github.com/haziqa5122/nesso-n1-fall-detection)

<Frame caption="The Bike That Calls for Help When You Crash">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/bike-fall-detection.jpg?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=18ad3386d3a98ba5623bb1b577970b5a" width="2394" height="1796" data-path=".assets/images/cyclist-fall-detection/bike-fall-detection.jpg" />
</Frame>

## Introduction

Did you know that a US bicyclist gets killed in traffic about every eight hours? That was the pace in 2025, when 1,148 riders died in crashes, up 4% from the year before, near a forty-year high. The gear hasn't caught up. Helmets, gloves, lights, and mirrors are all passive. Nothing on a typical bike knows when the rider is on the ground. Some high-end GPS units and smartwatches have crash detection, but that gear costs \$400 and up, and most riders don't own it.

That's the gap this build fills. An Arduino Nesso N1 mounts on the top tube, reads its onboard BMI270 IMU at 100 Hz, and runs an Edge Impulse model that sorts motion into two states: `normal riding` and a `fall`. When it flags a fall, the board pings a paired phone over Bluetooth Low Energy 5.3, and the phone fires off an SMS to a contact you've set up in advance.

By the end of this piece, you'll collect motion data, train a fall detection model in Edge Impulse, deploy it to the Arduino Nesso N1, and connect it to a BLE-powered SMS alert system.

## The problem with cyclist safety today

Bikes have gotten fast over the years. A road bike on a steep descent can hit 35 mph or more, no motor required. Gravel and mountain routes put you in places with no cell signal and nobody around. Even a routine evening commute puts you on roads shared with cars, where a fall could be very dangerous.

And most crashes don't involve a car at all. In an Australian study of cyclists hospitalized after on-road crashes, more than half went down on their own, with no other vehicle involved. Most of those were simply the rider losing control. Those crashes often happen when nobody else is around. If you go down on a quiet road and nobody sees it, help doesn't come until someone finds you. The usual answer is to press the SOS button on your watch, but a bad crash is exactly when you may not be able to.

There's room here for something better. Something that lives on the bike, runs the whole ride, recognizes a crash from the motion pattern, and gets the alert out without the rider having to do anything after the impact.

## Why edge AI is the right approach

The obvious way to add intelligence to a small device is to let the cloud think. You could stream raw Inertial Measurement Unit (IMU) data from a phone up to a server, run the model there, and send the result back. It works for plenty of applications. For a fall detector on a bike, it's the wrong choice. Here's why:

* **Latency**: A round-trip to a server adds anywhere from 200 to 800 ms of network time, and that's before the model does any actual work. A fall happens in a fraction of a second. You want the alert armed the moment the IMU detects the impact, not a second later after the data has traveled to a data center and back.
* **Connectivity**: Half the routes a recreational rider cares about are exactly where there's no cell signal. Forest trails, mountain switchbacks, gravel back roads. A detector that only works with a connection doesn't work where it's most needed.
* **Power**: The Nesso N1 runs on a small 250 mAh battery. Keeping a WiFi or LTE radio streaming nonstop is one of the most draining things you can ask a board like this to do. And on a battery that size, it won't last long. Run the model on the chip instead, and the radio can stay asleep for the whole ride, waking up only when there's a fall to report.
* **Privacy**: Your ride data is surprisingly revealing. It shows where you stop, how you take corners, and whether you're riding alone. Running inference on the board keeps all of that on the device. Nothing gets sent anywhere unless a real fall triggers an alert.

Edge Impulse handles the unglamorous parts of getting a small ML model onto a microcontroller. The Nesso N1 is reachable from the same Arduino IDE flow used for data collection, so the path from "data on the device" to "inference on the device" stays in one toolchain.

<Frame caption="(Left) The Nesso N1 attached to the bicycle frame for data collection. (Right) The display showing the Uploads and PAUSED status. Pressing the START button (not visible here) begins data collection.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/bike-arduino-nesso.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=23e58af65d759fff7284875e72c5975e" width="800" height="600" data-path=".assets/images/cyclist-fall-detection/bike-arduino-nesso.png" />
</Frame>

## Hardware overview

### The Arduino Nesso N1

The Nesso N1 is a joint Arduino and M5Stack board built around an ESP32-C6. It's small, runs on its own battery, and already has everything the fall detector needs on board. Here's what we actually use.

<Frame caption="Arduino Nesso N1 development board">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/arduino-nesso-n1.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=2908b87ff2657f2c26e7075d66457737" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/arduino-nesso-n1.png" />
</Frame>

* **Sensor**: The [BMI270](https://content.arduino.cc/assets/bmi270-ds000.pdf) is a 6-axis IMU, a 3-axis accelerometer and a 3-axis gyroscope. That's the only sensor fall detection needs, and it's already wired to the chip. We sample it at 100 Hz, fast enough to catch the sharp shape of an impact, still light on the battery.
* **Compute**: The board runs on an ESP32-C6, its main processor, clocked at 160 MHz, with 512 KB of RAM. Small, but a two-class model like this one leaves plenty of headroom. Inference runs on the chip with room to spare.
* **WiFi**: The board has 2.4 GHz WiFi 6, which we use only during data collection. You record over your phone's hotspot, so there's no USB cable tethering the bike to a laptop while you ride.
* **Buttons**: Two programmable buttons sit next to the screen, so you can label events one-handed as you ride. One button marks a fall, and everything else is just left as normal. The onboard buzzer beeps to confirm the press, so you don't have to look down to know it registered.
* **Battery**: A 250 mAh LiPo runs the whole thing untethered, which is enough for a couple of hours of recording. No external power module to bolt on.

The Nesso N1 is small enough to mount right on the top tube. We just used zip ties, which are cheap, hold tight, and are easy to reposition while you're dialing in the placement. Velcro straps or a 3D-printed cradle are the tidier options if you want something more permanent.

## Collecting motion data with Edge Impulse

Before any model can learn, it needs examples to learn from. For a fall detector, that means recording what the IMU sees during normal rides and during crashes. This is a two-class problem: `normal_riding` and `fall`.

Everything that isn't a crash goes in the `normal_riding` bucket. Pedaling, coasting, cruising, gentle turns, stop-and-go in traffic, and hard braking all count as normal. That last one matters. A hard brake produces a big deceleration spike that, for a split second, looks a lot like the start of a fall. Instead of giving braking its own class, we just record plenty of it as normal riding. The model learns that a spike followed by the bike staying upright is nothing to worry about, and only a spike followed by a tumble is a fall.

## Setup: IDE, libraries, account

Four things to install before the board can record anything:

1. [Arduino IDE](https://www.arduino.cc/en/software/)
2. The M5Stack board package. Into Arduino IDE go to `File` → `Preferences` → `Additional boards manager URLs` and paste the following:

```
https://static-cdn.m5stack.com/resource/arduino/package_m5stack_index.json
```

<Frame caption="Installing the M5Stack Board Package">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/arduino-ide-installing-tools.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=f340a2ff53498fb4e63ff368d2153243" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/arduino-ide-installing-tools.png" />
</Frame>

3. Then open Board Manager, search M5Stack, install it, and select `Tools` → `Board` → `M5Stack` → `ArduinoNessoN1`.
4. The M5Unified library, through the Library Manager. It pulls in M5GFX on its own. M5Unified is the library that lets your code talk to the board's hardware.
5. An [Edge Impulse account](https://edgeimp.com/community26) and project. Create a project (something like `bike-fall-detection`) and copy the API key from Dashboard → Keys. The board needs that key to push data in.

The board talks to Edge Impulse's ingestion API directly, so you don't need Node.js or the CLI for this workflow.

### Reading the BMI270 at 100 Hz

The read loop is short. M5Unified brings the chip up in one call and hands you the readings as floats:

```
    #include <M5Unified.h>
    void setup() {
        auto cfg = M5.config();
        M5.begin(cfg);
        M5.Imu.init();
    }

    void loop() {
        float ax, ay, az, gx, gy, gz;
        M5.Imu.getAccel(&ax, &ay, &az);
        M5.Imu.getGyro(&gx, &gy, &gz);
        delay(10); // 100 Hz
    }
```

`M5.begin()` powers up the IMU, display, buttons, and buzzer together. `M5.Imu.init()` starts the BMI270 over the internal I²C bus. Each pass gives you one reading of `[ax, ay, az, gx, gy, gz]`, accelerometer in m/s² and gyroscope in deg/s. The `delay(10)` is what holds the loop at 100 Hz.

That 100 Hz stays the same everywhere. It's the record rate, the `interval_ms` in the upload, and the rate Edge Impulse sees when it builds features later.

### Streaming to Edge Impulse

Instead of saving to flash (onboard storage) and offloading after the ride, the board streams straight to Edge Impulse over your phone's hotspot:

<Frame caption="The data collection path: the Nesso N1 samples the BMI270 at 100 Hz and streams two-second batches over WiFi through the rider's phone to the Edge Impulse ingestion API.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/data-collection.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=809f5050a5fa371fd8382b781ef42c40" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/data-collection.png" />
</Frame>

Readings go up in batches, not one at a time. The board collects 200 readings (100 Hz × 2 seconds) and sends them as a single POST, which becomes one labeled sample in Edge Impulse. Two seconds is also the window length we use for the model later, so recording and inference line up by design.

One detail is easy to miss. HTTPS needs a valid clock to verify the server's certificate, and the ESP32-C6 has no clock at boot. So NTP sync has to run once, right after WiFi connects and before any upload.

```
    configTime(0, 0, "pool.ntp.org", "time.nist.gov");
    while (time(nullptr) < 100000) {
        delay(500);
    }
```

The upload itself POSTs to the ingestion endpoint using Edge Impulse's [JSON format](https://docs.edgeimpulse.com/tools/specifications/data-acquisition/json-cbor), with the API key and the active label in the headers:

```
https.addHeader("x-api-key", apiKey);
https.addHeader("x-file-name", label + "_" + String(millis()));
https.addHeader("x-label", label);
https.POST(json);

```

The payload carries `interval_ms: 10` to match the sample rate, plus the 200 readings from the current batch. Each batch lands in your training data under whatever label was active when it was sent.

One nice detail worth knowing is that uploads run on a background task on the second core, using two buffers. While one buffer is being sent, the other keeps filling with fresh samples. Sampling and the buttons never stall waiting on the network.

### Labeling as you ride

You label events with the buttons on the board. Button A is the only labeling button, and it does one thing: mark a `fall`. Press it, and the label flips to fall, the screen turns red, and the buzzer beeps so you know it registered without looking down. After two seconds, the label drops back to `normal_riding` on its own. That two-second auto-revert is the same length as the upload batch, so every sample that goes up is cleanly one label instead of smeared across both.

This code below is the labeling logic. Press button A and it beeps, flags the current window as `fall`, and starts a timer. After 2 seconds, it flips back to `normal_riding` on its own, so one press tags only that fall window and everything else stays normal.

```
    if (M5.BtnA.wasPressed()) {
        beep(200);
        currentLabel = "fall";
        timedLabel = true;
        labelTimer = millis();
    }
    if (timedLabel && (millis() - labelTimer > 2000)) {
        currentLabel = "normal_riding"; // auto-revert
    }
```

The screen shows what's being recorded at a glance. Green means normal riding, and red means a fall. It also keeps a running count of each so you can see your dataset filling up.

There's also a touch button along the bottom of the screen that pauses and resumes recording. Tap it, and the board stops sampling and uploading, so you can stop at a café or reposition the mount without dumping junk into your dataset. Tap it again to pick back up.

<Frame caption="(Left) the board defaults to the normal_riding label at startup. (Right) pressing the button marks the window as a fall.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/labeling-ride-arduino-nesso-n1.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=f8c3018fd9d3377da462737575a27a79" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/labeling-ride-arduino-nesso-n1.png" />
</Frame>

That's the whole setup. The board on the top tube, phone in your pocket for internet, one button to flag falls, and clean two-second batches landing in Edge Impulse as you ride.

### Reviewing the data in Studio

Everything the board sent lands in the **Data acquisition** section in Edge Impulse Studio. This is where you confirm the collection actually worked and where you spend time before training.

<Frame caption="Data Acquisition Tab in the Edge Impulse Studio with a fall sample. The gyroscope ramps as the bike rotates over, then the accelerometer spikes at impact.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/1_with_original_fall.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=625a4024cfbd67e5cca6a46c5d894f41" width="1657" height="888" data-path=".assets/images/cyclist-fall-detection/1_with_original_fall.png" />
</Frame>

Open your project and go to Data acquisition. You will see every sample in a list, grouped and filterable by label, with the class balance shown at the top. Click any sample and the raw 6-axis trace opens on the right. This is the review step as shown in the figure below. Scroll the fall samples and check that each one really is a fall, that the event sits inside the window, and that nothing is mislabeled or clipped.

Here is what actually came off the bike:

* `normal_riding`: 555 samples recorded (about 18 minutes)
* `fall`: 77 samples recorded (about 2.5 minutes)

Click into a sample, and you can see exactly that shape. It is worth doing for a handful of samples per class. Two minutes of eyeballing the raw traces catches labeling mistakes that would otherwise quietly cost you accuracy later.

### Not enough falls, so we made more

Look at that table again. 555 riding samples against 77 falls. That gap is the central problem with this dataset, and it is the kind of gap you cannot close by riding more. You will not crash a bike a few hundred times to fill a class, and even if you did, it would be dangerous and slow. Falls are rare by nature. That is the whole point of detecting them, and it is also why they are hard to collect. So we generated more fall samples synthetically, from the real ones we already had.

The method is a transplant, not random noise. A real fall has a clear, repeatable signature. The gyroscope ramps up as the bike rotates over, then the accelerometer spikes at impact. We took the fall event from each real crash and pasted it onto a real normal-riding window.

We uploaded these the same way the board does, a POST to the ingestion API with the fall label, so they land right in the Data acquisition tab next to the real samples. We added 450 of them, which brings the fall class from 77 to 527 and roughly levels it with normal riding.

<Frame caption="The synthetic fall samples in the Data acquisition tab, tagged and sitting alongside the real recordings.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/1.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=97d47e2f2891b4b2af78b43466902241" width="1649" height="888" data-path=".assets/images/cyclist-fall-detection/1.png" />
</Frame>

### Why quality and balance beat quantity

The synthetic step was restoring balance using the signal we already had from real falls. That distinction is the whole lesson of this stage. Three things matter more than raw sample count:

* **Balance**: A model trained on 555 riding samples and 77 falls learns that guessing `normal_riding` is almost always safe. It can score 88% by never predicting a fall at all, which is useless for a fall detector. This is exactly what the synthetic falls buy, and it is why we grew the rare class instead of the easy one.

* **Clean labels**: One fall sample that is really a curb bump does more damage than ten good samples do. The model cannot tell a wrong label from a hard example. It simply learns from both. That's why we labeled live and reviewed the traces in Studio. It's also why the synthetic falls are built only from confirmed real falls, so we're amplifying clean signal, not noise.

* **Consistent capture**: The board has to be mounted the same way every time, rigid to the frame. If it flops around, the IMU records the mount slipping instead of the bike moving, and the same event looks different from one ride to the next. A rigid, repeatable mount is worth more than a looser setup with twice the samples.

The collection goal was never "record as much as possible." It was a set of falls that are genuinely falls, cleanly labeled, captured the same way each time, balanced against a representative spread of real riding. That foundation is what everything downstream is built on.

## Training the fall detection model in Edge Impulse

With the data in place, training is three blocks in sequence. A processing block turns raw motion into features. A learning block classifies those features. Then you run the training. Edge Impulse calls the whole chain an impulse.

<Frame caption="The impulse. Time series input, Spectral Analysis plus Flatten, into Classification.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/edge_impulse_processing_learning_blocks.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=0a40eb0ce0568188761da007212efe06" width="1739" height="904" data-path=".assets/images/cyclist-fall-detection/edge_impulse_processing_learning_blocks.png" />
</Frame>

### The Impulse

The input is the window from the last section: 2 seconds, 100 Hz, six axes, zero-padded if a sample runs short. From there, the flow is short. The processing blocks compute features. The learning block trains on them. You generate features once, then train. That is the loop.

Before training, split the data 80/20, stratified by label, so each class keeps its proportion. Every result below comes from that held-out test set, which the model never sees during training.

### Choosing the processing block for IMU data

[Spectral Analysis](https://docs.edgeimpulse.com/studio/projects/processing-blocks/blocks/spectral-analysis) is the default for motion data, and it earns it. It pulls the frequency and power content out of each axis, which is what separates steady riding from the tumble of a crash. Studio's autotune sets the parameters: a high-pass filter at 7.6 Hz, FFT length 256, log spectrum. The high-pass filter is the key one. It removes the constant pull of gravity, so the block sees the actual motion rather than a fixed baseline.

But a fall is not only a frequency pattern. It is also a hard, one-off impact. Frequency features smear that spike across bins and dull it. So we ran a [Flatten](https://docs.edgeimpulse.com/studio/projects/processing-blocks/blocks/flatten) block alongside it, which computes basic statistics per axis.

The max value captures the single hardest spike in the impact, and the RMS captures the overall strength of the shaking over the entire window. (RMS, root mean square, is basically a measure of average intensity, so a violent crash scores high and smooth cruising scores low.) The classifier sees both blocks together, Spectral Analysis and Flatten, so it reads both the
shape of the motion and the size of the impact at the same time.

<Frame caption="Feature explorer. Fall separates from the bulk of riding, with overlap where a hard bump briefly looks like an impact.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/3_Feature-explorer.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=e0f4c5315c3773f529a0cc71b334b58d" width="829" height="497" data-path=".assets/images/cyclist-fall-detection/3_Feature-explorer.png" />
</Frame>

### Training and reading the results

The classifier is deliberately small: Dense 32, Dropout 0.1, Dense 16. 150 cycles, learning rate 0.0007, class weights on to keep the model honest about the rarer class, int8 output for the board.

<Frame caption="Small network, class weighting on, int8 output.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/4-model-classifier.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=1a7ca84859ab67525ee1cf85a72ee3c5" width="813" height="863" data-path=".assets/images/cyclist-fall-detection/4-model-classifier.png" />
</Frame>

The number that matters is not overall accuracy. On a set that is mostly normal riding, a model can score high by rarely calling a fall and still be useless. Read the confusion matrix instead, and read the fall row in particular.

<Frame caption="Classify all on the held-out test set. Full matrix, no cherry-picking.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/5_confusion_matrix.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=f3a12b451eec8d532fa4919f32d0e955" width="799" height="325" data-path=".assets/images/cyclist-fall-detection/5_confusion_matrix.png" />
</Frame>

That precision is the thing to watch. A single window is not a reliable trigger on its own. We do not fix that in the model. We fix it on the board with debounce, covered later. Worth noting the int8 and float32 models score the same, so nothing is lost to quantization, which matters because int8 is what ships.

## Deploying the ML model to the Arduino Nesso N1

Training in the browser is the easy part. Now the model has to leave the cloud and run on the board itself, where it will do inference on live IMU data with no internet in the loop. Edge Impulse packages the whole thing, signal processing and classifier both, into an Arduino library you drop straight into a sketch.

### Deploy as an Arduino library

Go to the Deployment tab and pick `Arduino library`. Select `int8` quantization with the [EON Compiler](https://docs.edgeimpulse.com/studio/projects/deployment/eon-compiler), which cuts RAM and flash for the same model. Then build to get a `.zip`.

The classifier block needs about 2 KB of RAM and 37 KB of flash. On a board with 512 KB of SRAM and 16 MB of flash, that leaves almost all of it free for the BLE stack and your own logic.

<Frame caption="Deployment. Arduino library, int8, EON Compiler, ready to build.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/edge_impulse_deployment_arduino_library.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=4e06913a30de5d8a40d9c9f1549e3a89" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/edge_impulse_deployment_arduino_library.png" />
</Frame>

In the Arduino IDE, go to Sketch > Include Library > Add .ZIP Library, then point to the download. Edge Impulse names the library's header after your project, so the include line has to match. Ours was `bike-fall-detection`, which makes the include `bike-fall-detection_inferencing.h`. Swap in whatever you named yours.

<Frame caption="To add the library, go to Sketch → Include Library → Add .ZIP Library, then select the .zip you downloaded from Edge Impulse.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/add-library-arduino-ide.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=e88e2600938d43fe1105f4f1df8f1de6" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/add-library-arduino-ide.png" />
</Frame>

### Flash the Arduino and verify

The sketch reads the BMI270, buffers one 2-second window, and calls `run_classifier` on it. One unit detail decides whether it works at all. Match the inference input to how the data was collected. The accelerometer was recorded in raw G, so it goes into the buffer as raw G, with no unit conversion. The gyroscope was recorded in deg/s and passes through the same way.

```
    buffer[ix + 0] = ax; // raw G, matches how the data was collected
    buffer[ix + 1] = ay;
    buffer[ix + 2] = az;
```

Select the Nesso N1 board, compile, and upload. Open the Serial monitor. You should see one prediction per window with timing:

```
Predictions (DSP: 4 ms, classification: 1 ms):
    fall: 0.01234
    normal_riding: 0.98766
```

DSP (digital signal processing) plus classification finishes in a small fraction of the 2-second window, so the board keeps up in real-time with margin to spare. The inference is fully on the board. No phone, no network.

### Test across each motion class

Mount the board and ride. normal\_riding should hold through cruising, corners, and bumps. Then stage a fall to the side and watch the fall score jump. Test the conditions you will actually meet. This includes smooth road, rough road, hard braking, curbs, and real tip-overs. The bumps and curbs are the ones to scrutinize, since a sharp jolt is what most resembles an impact.

### Tune the confidence threshold

The sketch fires on `fall_score > 0.5`. You can raise that to cut false positives, at the cost of missing marginal falls. But the threshold alone is blunt here, because single-window precision is low. The real false-positive control is debounce, which is the next section.

## Triggering the SMS alert via a paired smartphone

Inference on the board only helps if it reaches a person. The Nesso N1 has no cell radio, so it hands off to a paired phone over BLE, and the phone sends the SMS.

<Frame caption="Triggering the SMS alert after a fall detection">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/app-workflow.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=d134e0df861bf267d1d3884996d0fba0" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/app-workflow.png" />
</Frame>

### The alert flow

The board confirms a fall, sets a BLE characteristic to `FALL_DETECTED`, and notifies. The paired phone, subscribed to that characteristic, receives the string, checks that it contains `FALL`, and texts the emergency contact. The companion app is a small [MIT App Inventor](https://appinventor.mit.edu/) project using the BluetoothLE extension. About six blocks do the whole job.

### BLE setup

The board is the GATT server. It advertises as `NessoFallAlert` with one service and one notify characteristic:

```
#define SERVICE_UUID    "a1e8f350-3d38-4f1c-9b8e-6f2b1c4a7d10"
#define ALERT_CHAR_UUID "a1e8f351-3d38-4f1c-9b8e-6f2b1c4a7d10"
```

We used NimBLE-Arduino, not the classic ESP32 BLE library. The C6's Bluetooth host is NimBLE-based, and the older library does not fully support it. NimBLE is also lighter on RAM and flash, which matters when you share memory with the inferencing runtime.

The app is the client. On a button tap, it scans for the service UUID, matches the device by name, connects, and calls `RegisterForStrings` on the characteristic. When the board notifies, `StringsReceived` fires.

<Frame caption="The complete App Inventor block logic for the companion app. It scans for the Nesso N1, connects, listens for the fall alert over BLE, and sends the SMS the moment a message containing FALL arrives.">
  <img src="https://mintcdn.com/edgeimpulse/dhdSkQm5FGE6VO__/.assets/images/cyclist-fall-detection/app-inventor-logic.png?fit=max&auto=format&n=dhdSkQm5FGE6VO__&q=85&s=06830340b8e8f91d4d5ada1795ef0e0e" width="1280" height="960" data-path=".assets/images/cyclist-fall-detection/app-inventor-logic.png" />
</Frame>

### SMS, kept lightweight

The phone sends the message with App Inventor's Texting component, a single SendMessage block. No server, no cloud, no internet in the path. The phone already in the rider's pocket does the delivery, which keeps the safety-critical link off the network. The app matches FALL as a substring, and the board sends `FALL_DETECTED`, so the trigger holds even if you extend the message text later.

### Debounce, so alerts fire only when they should

This is where the low single-window precision gets handled. Three guards, all on the board:

* **Two consecutive fall windows**: One window at around 40% precision is close to a coin flip. Two in a row are far more reliable, and a real fall easily spans two 2-second windows.
* **A 30-second cooldown** after an alert, so one crash does not trigger a burst of texts.
* **A 0.5 confidence threshold** before a window counts at all.

```
if (fall_score > 0.5f)  consecutive_fall++;
else                    consecutive_fall = 0;

if (consecutive_fall >= REQUIRED_CONSECUTIVE_FALLS && cooldownOver) {
    alertChar->setValue("FALL_DETECTED");
    alertChar->notify();
}
```

The cost of debounce is a little latency, one extra window, about two seconds. In return, false alarms drop sharply. For a text to a contact, that's the right trade. A slightly late alert is fine. A dozen false ones shut down the whole system, which helps no one.

## Results and honest limitations

The system works, and it is worth being clear about what "works" means here.

On the held-out test set of real falls, the model catches roughly two out of three. Recall lands around 69% , precision around 40%. So a single flagged window is closer to a hint than a verdict, which is exactly why the board waits for two falls in a row before it texts anyone. That debounce is doing real work. It turns a noisy per-window signal into a trigger you can trust enough to wire to a phone.

This is where the limitations show. Smooth roads and clear tip-overs are easy. The model separates them cleanly. Rough gravel, curbs, and hard braking are harder because a sharp jolt looks a lot like the start of an impact. Most of those get filtered out by the two-window rule, but a bad enough pothole at speed can still trigger a single window. It just rarely triggers two.

Mounting matters more than anything else on the bike. The model learned from a board zip-tied rigid to the top tube. Move it to the handlebars, or let it work loose mid-ride, and the IMU starts recording the mount instead of the crash. Keep it tight and keep it in the same spot, and the numbers hold. Let it flop, and they don't.

Then there are the honest misses. A slow, sideways dismount where you step off and gently lay the bike down can read as normal, since there is no impact spike or significant rotation. Aggressive cornering on a fast descent is the normal maneuver most likely to trip the model. Neither is common, but neither is fully solved.

Power is the quiet constraint. The 250 mAh battery is more than enough for inference, which uses very little power. The radio is what draws the most energy. BLE stays efficient because it only wakes to send an alert, while WiFi is the expensive one and is only needed for data collection, not riding.

In normal use, the board runs inference on-chip and the radio is nearly silent, so a ride lasts much longer than a data collection session. Still, the internal battery is good for a couple of hours, not an all-day ride. A small power bank in a frame bag easily closes that gap for longer rides.

Where it has room to grow: more real falls in more conditions would lift precision, which is the number holding everything back. Right now the debounce is compensating for a model that flags too eagerly. A better-fed model would let you lean less on the delay and shave the alert latency.

### Would anomaly detection have worked better?

It's a fair question, and possibly yes. Our biggest hurdle was imbalance. Falls are rare, so we had to synthesize most of the fall class. Anomaly detection avoids that entirely. You train only on normal riding, which we have plenty of, and flag anything that deviates far enough as a possible fall. No crash data to collect or fabricate. The trade-off is specificity. An anomaly detector says "this isn't normal riding," but it can't tell a fall from a curb slam or the bike being tossed in a car, whereas the classifier learns the fall signature directly. Given our modest precision, it's genuinely worth testing both. Retraining this as an anomaly detector, or a hybrid where anomaly detection gates the classifier, is high on the list of things we'd try next.

## Conclusion

We started with a real gap. Bikes have gotten faster, riders often ride alone, and most cycling gear does nothing after a crash. The usual answer is a button you press after the fall, at the moment you're least able to press it.

This build takes a different approach. The bike monitors its own motion, recognizes a fall, and calls for help automatically. No button, no cloud, and no cell signal are required on the critical path. A model small enough to run on a coin-sized board handles inference in a few milliseconds, while a phone already in your pocket sends the text.

That's the real shift. A helmet protects you during the crash. This system detects the crash and gets someone moving toward you afterward. They solve different problems, and until now only one had a practical answer on a typical bike.

The same approach extends beyond cycling. A fall has a recognizable motion signature whether you're on a motorcycle, on skis, or walking across a room. Retrain the model on the right data, and the same pipeline can detect a downhill crash or an elderly parent's fall at home. Only the training data changes.

There is still room to improve. GPS could include a location with the alert. More real fall data would improve precision and reduce the debounce delay. A direct link to emergency services, instead of a single contact, would make the system even more useful. None of that changes the core idea. A low-cost board, a compact model, and a rider who is no longer entirely on their own after a crash.

If you want to try it yourself, Edge Impulse is free to start, and the whole pipeline here runs in the browser.
