Community project

Bicycle Motion Data Logger

ESP32
Photo of Bicycle Motion Data Logger
Generated with AI

Sangjoon Lee

Published September 15, 2026

This project builds a compact motion data logger that captures acceleration, gyroscope, and wheel speed data from a bicycle in real time. The ESP32 reads a Bosch BMI270 inertial measurement unit for 3-axis motion, detects wheel rotations via a Hall-effect sensor and magnet, and displays live metrics on an SSD1306 OLED screen while logging all measurements to CSV format.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the logger on a bicycle frame. Firmware is included with configurable sample rates and wheel circumference, making it easy to adapt the logger to different bike sizes and data collection needs.

Wiring diagram

Wiring diagram for Bicycle Motion Data Logger

Gather all the parts

QtyComponent
1

Bosch BMI270 IMU

BMI270 I2C breakout

Bosch BMI270 low-power 6-axis IMU with accelerometer and gyroscope. This entry models the maker-breakout I2C path, not the raw IC power-domain pinout.

1

SSD1306 OLED

128 × 64 I2C, address 0x3C

0.96 inch 128x64 OLED display with I2C interface

1

3.3 V A3144 Hall-effect wheel sensor module

A3144 3.3 V digital module

A small 3.3 V magnetic switch module that produces one digital pulse each time the wheel magnet passes it.

1

Neodymium Disc Magnet 10 x 3 mm

10 × 3 mm

Small cylindrical neodymium magnet used as a magnetic target for Hall-effect homing or position sensing. It has no electrical pins and should be mounted mechanically on the moving split-flap wheel or index point, close enough for the chosen Hall sensor to detect repeatably without striking the sensor.

Assemble it in 5 steps

1. Keep the board unpowered while wiring

Place the ESP32-S3 board, BMI270 breakout, OLED, and hall sensor module where you can reach their printed pin labels. Leave the USB cable unplugged until every wire has been checked.

  • Use short jumper wires for the IMU and OLED so vibration is less likely to loosen them.
  • Do not connect wires while USB power is plugged in — a misplaced wire can damage a sensor or the board.

2. Wire the motion sensor and screen

Connect BMI270 VCC to ESP32-S3 3V3 (power), BMI270 GND to GND (ground), BMI270 SDA to GPIO8 (data), and BMI270 SCL to GPIO9 (clock). Connect OLED VCC to ESP32-S3 3V3 (power), OLED GND to GND (ground), OLED SDA to GPIO8 (data), and OLED SCL to GPIO9 (clock). Also connect the BMI270 SDO pin to GND if the breakout exposes it; this selects the address used by the logger.

  • Both small boards share GPIO8 and GPIO9; that is normal because they take turns using the same two data wires.
  • Make sure VCC and GND are not swapped — swapped power can damage the BMI270 or OLED.

3. Wire the wheel rotation sensor

Connect the 3.3 V hall sensor module VCC to ESP32-S3 3V3 (power), GND to GND (ground), and OUT to GPIO4 (wheel-pulse signal). Keep this sensor powered only from 3.3 V, not 5 V, so its output remains safe for the ESP32-S3.

  • Run the sensor cable along the frame and secure it with cable ties, leaving enough slack at the fork for steering.
  • Keep the cable away from the brake disc, spokes, chain, and tyre — moving parts can cut the wire or pull the sensor loose.

4. Mount and align the magnet

Secure the magnet to one wheel spoke and fix the hall sensor to the fork or frame so the magnet passes the marked sensing face once per wheel rotation. Start with a 3 to 8 mm gap and turn the wheel by hand while checking that the rotation number changes on the OLED.

  • Use thread-locking adhesive or a strong spoke mount; one magnet pass is counted as one rotation.
  • A loose magnet can fly off into the wheel. Keep it firmly attached and clear of the brake rotor and spokes.

5. Secure the logger for data collection

Mount the ESP32-S3, OLED, and BMI270 in a small weather-protected enclosure on the bicycle frame. Keep the IMU firmly fixed in one orientation because its X, Y, and Z readings follow how the board is mounted. Plug the ESP32-S3 into USB after the wiring is complete.

  • Before collecting research data, measure one full tyre rollout on the ground and replace WHEEL_CIRCUMFERENCE_M in the top of src/main.cpp with that distance in metres.
  • Do not operate the bicycle with loose electronics or wires that can interfere with steering, brakes, pedals, or wheels.

Review all connections

1. Connections between "bmi270_1" and "ESP32"

Functionbmi270_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

2. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

3. Connections between "hall_1" and "ESP32"

Functionhall_1ESP32
powerVCC3V3
groundGNDGND
digitalOUTGPIO 4

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include "ImuDriver.h"
#include "HallSpeed.h"
#include "StatusDisplay.h"
#include "CsvLogger.h"

// Easy-to-change hardware and collection settings.
constexpr int I2C_SDA_PIN = 8;
constexpr int I2C_SCL_PIN = 9;
constexpr int HALL_PIN = 4;
constexpr uint8_t BMI270_ADDRESS = 0x68;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint32_t SAMPLE_RATE_HZ = 100;
constexpr uint32_t SAMPLE_INTERVAL_US = 1000000UL / SAMPLE_RATE_HZ;
constexpr float WHEEL_CIRCUMFERENCE_M = 2.105f;

ImuDriver imu;
HallSpeed wheel;
StatusDisplay display;
CsvLogger logger;
uint32_t nextSampleUs = 0;
uint32_t lastDisplayMs = 0;
ImuSample latestSample{};
float latestSpeedKph = 0.0f;

[[noreturn]] void haltWithMessage(const char *message) {
  Serial.println(message);
  while (true) delay(1000);
}

void setup() {
  logger.begin(115200);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setClock(400000);
  if (!imu.begin(Wire, BMI270_ADDRESS)) haltWithMessage("ERROR: BMI270 not found");
  if (!display.begin(Wire, OLED_ADDRESS)) haltWithMessage("ERROR: SSD1306 not found");
  wheel.begin(HALL_PIN, WHEEL_CIRCUMFERENCE_M);
  nextSampleUs = micros();
  Serial.println("# logger_ready");
}

void loop() {
  const uint32_t nowUs = micros();
  if ((int32_t)(nowUs - nextSampleUs) < 0) return;
  nextSampleUs += SAMPLE_INTERVAL_US;
  if ((int32_t)(nowUs - nextSampleUs) >= 0) nextSampleUs = nowUs + SAMPLE_INTERVAL_US;

  ImuSample sample;
  if (!imu.read(sample)) return;
  latestSample = sample;
  latestSpeedKph = wheel.speedKph();
  logger.log(millis(), sample, latestSpeedKph);

  if (millis() - lastDisplayMs >= 200) {
    lastDisplayMs = millis();
    display.showStatus(latestSample, latestSpeedKph, wheel.rotations());
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik