Clean data, clean gears.
The viewer, the algorithm, and a new architecture.

A long Saturday that started with a dodgy JST connector and ended at 1am watching an ESP32-C6 read two simulated gear sensors simultaneously over Bluetooth, printing a rock-solid ratio of 2.000 to the Serial monitor. In between: a data viewer that became a serious tool, an algorithm that emerged from real ride data, and a hardware architecture decision that changes everything.

3
test rides logged and analysed
4
firmware versions shipped (v0.1→v0.4)
2.000
simulated gear ratio — steady as a rock
0
spoke magnets needed in Mark III

The data viewer

Everything that happened today was made possible by a tool that didn't exist yesterday. The GearSense ride data viewer started as a simple chart — three stacked plots of crank Hz, wheel Hz, gear ratio, and cumulative pulses, all synced and zoomable. It became something more.

The marquee selection tool lets you drag a bounding box over any region of any chart and toggle those points as "ignored" — removing sensor noise, EMI spikes, and transition artefacts from the visible data without destroying the underlying log. It filters by both time and value, so you can surgically remove the ratio-18 outliers at 775 seconds without touching the ratio-0.9 points that sit at the same timestamp. Ignored points don't break the line — the chart bridges across them so the underlying shape stays readable.

On top of that: a fit-line mode. Draw a marquee in fit mode and instead of ignoring the points, you get a linear regression line across them — mean value, slope, and point count. Multiple segments coexist in different colours. Everything saves to named views, which can be exported as JSON and reloaded later. Undo works across every operation.

"This is turning into a serious application." — me, about four hours in, slightly surprised

The important thing the viewer revealed: manual data cleaning is not a workaround. It is the work. Every threshold in the algorithm that follows — every parameter, every filter — traces back to a specific judgment call made visually in this tool. The viewer is where the algorithm lives before it gets written in C.

What three rides taught us

Ride 1 (13.6 min, mostly unassisted) was the source of truth. After manual cleanup in the viewer, two fit segments emerged cleanly: mean 0.864 (n=18) and mean 1.102 (n=17), both with slopes indistinguishable from zero. Genuinely flat gear plateaus. This is what GearSense is supposed to detect.

Ride 2 (mostly motor-assisted) produced 13 stable segments offline but zero on-device — a BLE delivery bug, the algorithm itself was fine. A diagnostic counter added in v0.4.0 will confirm this on the next ride.

Ride 3 produced nothing. The offline detector found zero stable segments. The gear_events counter stayed at 0 for the entire 16 minutes. For a moment this looked like a firmware bug — but it wasn't. The wheel sensor was reading 66.6 Hz continuously from the very first second, before the bike even moved. A physically-inspected spoke magnet had drifted oblique to the KY-003 sensor. A JST connector that "felt a bit dodgy" when plugged in was probably intermittent. The hardware had failed, not the firmware. The algorithm was correctly reporting: there is no useful data here.

The important lesson Ride 3's failure was correctly identified by the algorithm as a hardware problem, not a data problem. That's actually the system working. Garbage in, nothing out — which is better than garbage in, confident wrong answer out.

The algorithm — derived from real data

Running the Python reference implementation against all three rides produced something concrete. Three stages, each validated against real ride data rather than theoretical assumptions.

The important reframe
gear discrimination ≠ gear identification
GearSense doesn't need to know "this is 3rd gear." It needs to know "this is a different ratio than before." Lower ratio = lower speed for a given cadence, regardless of what number we call it. Five of seven gears correctly discriminated is already a usable product. The rest improves over time.

The live gear ladder — v0.4.0

The on-device firmware now builds a gear ladder in real time as you ride. As soon as a stable run reaches MIN_RUN samples, its mean ratio is folded into the ladder — either matching an existing entry (within LADDER_TOL=0.07) or starting a new one. The big green gear-number box on the display shows the 1-based rank of whichever gear you're currently in, live, the first time that gear is confirmed.

The numbering can shift mid-ride as new lower gears are discovered. This is expected behaviour — the ladder is a live, growing view of "distinct gears seen so far," not a fixed pre-calibrated table.

The experience we're building toward You set off on a ride. The display shows "--". A few seconds of steady pedalling and a number appears: "1". You shift up. A few seconds later: "2". Every new gear discovered adds to the ladder in real time. "Look — it found a gear. Hey, another one!" The algorithm building itself in public, one gear at a time.

The hardware problem — and its solution

Ride 3 crystallised something that had been building across several sessions: the KY-003 Hall-effect sensor with a spoke magnet is the weakest link in the system. Not conceptually wrong — mechanically fragile. The magnet drifts. The JST connector feels dodgy. Under motor load, EMI overwhelms the signal entirely.

The solution: off-the-shelf BLE cycling speed and cadence sensors. The Magene S3+ uses a geomagnetic sensor — a 3-axis magnetometer — to detect rotation. As the sensor rotates with the crank arm or wheel hub, the Earth's own magnetic field traces a circle in the sensor's reference frame. Each full rotation is one full cycle. No external magnet. No alignment to maintain. No wire running near the motor controller.

Mounting the wheel speed sensor on the front fork eliminates the motor EMI problem entirely. The rear hub motor and the front fork are separated by the full length of the cargo bike — roughly 1.5 metres. At that distance, the Earth's field dominates comfortably over the motor's field.

And because these sensors use the standard Bluetooth SIG Cycling Speed and Cadence profile (UUID 0x1816), GearSense works with sensors from any manufacturer that implements the spec. Garmin. Wahoo. Magene. The sensor you already have on your bike. BYO sensors — zero additional hardware purchase for that customer segment.

Mark II sensor setup

KY-003 Hall sensor, spoke magnet, JST connector, wire routed near motor. Prone to magnet drift, connector intermittency, EMI saturation under load. Complex install.

Mark III sensor setup

Two Magene S3+ sensors. One clips to crank arm (cadence). One clips to front fork (speed, away from motor). No magnets. No wiring. Standard BLE. Trivial install on any bike.

The new architecture — one wire

Removing the sensor wires forces a rethink of everything else. With cadence and speed arriving over BLE, the only physical connections GearSense needs are to the bike is power and inject throttle signals. The bike's existing throttle connector provides exactly that: a 4.65V rail that can power the ESP32 via a small MT3608 boost converter stepping it up to a stable 5V. The same connector will carry the throttle injection signal when Phase 3 is ready.

Mark III: one connection. One M6 connector. Power in, throttle injection out, sensors over Bluetooth. The LiPo battery is gone. Nothing to charge. Nothing to forget to charge.

The C6 — a better chip for the job

The new board is the Waveshare ESP32-C6-Touch-LCD-1.47. The C6 has a dual-core RISC-V architecture — a high-performance core at 160 MHz and a separate low-power core — which makes it a natural fit for running BLE scanning and LVGL display code simultaneously. It also has a built-in TF card slot, which means the SD card storage tier (ride logs, persistent gear ladders, privacy-conscious local data) is included at no extra hardware cost.

The validation — tonight at 1am

Two LOLIN32 boards were flashed as CSC sensor simulators — one advertising as "GS_Cadence" at 75 RPM, one as "GS_Speed" at 150 RPM. The C6 was flashed as BLE central using NimBLE-Arduino. After some API version fixes and enabling USB CDC on boot, the Serial monitor showed:

# GearSense BLE Central (ESP32-C6)
# Seeking: 'GS_Cadence' and 'GS_Speed'
# Found: GS_Speed
# Found: GS_Cadence
# Subscribed to GS_Speed
# Subscribed to GS_Cadence
crank_hz=1.000  wheel_hz=2.000  ratio=2.000
crank_hz=1.000  wheel_hz=2.000  ratio=2.000
crank_hz=1.000  wheel_hz=2.000  ratio=2.000
# Status: cadence=OK(1.000Hz)  speed=OK(2.000Hz)  ratio=2.000
crank_hz=1.000  wheel_hz=2.000  ratio=2.000
crank_hz=1.000  wheel_hz=2.000  ratio=2.000

1.000 Hz crank (75 RPM ÷ 60). 2.000 Hz wheel (120 RPM ÷ 60). Ratio exactly 2.000. Rock solid. Not a single dropout across twenty minutes. The contrast with ride 3 — where the wheel sensor was saturated at 66.6 Hz before the bike even moved — is everything. The BLE CSC architecture solves, at a stroke, the three failure modes that have been plaguing the hardware: connector reliability, magnet alignment, and motor EMI interference.

What comes next

The Magene S3+ sensors and MT3608 boost converters arrive Monday. The Mark III build starts then.

  1. Bench test: ESP32 current draw at realistic load (display + BLE + WiFi active)
  2. Throttle rail voltage remeasurement — methodical, with notes
  3. MT3608 calibration: set output to ~5.0–5.2V from the 4.65V bike rail
  4. Mark III wiring: M6/M8 waterproof connectors throughout, throttle line to GPIO17 (disconnected for now)
  5. v0.5.0 firmware: BLE central reading two real S3+ sensors, gear detection feeding the live ladder
  6. First ride with clean data — does the viewer show a staircase?

Comments

Loading…

Leave a comment