Hirak Desai

Founding Engineer @ Oximy (YC W26) · Los Angeles · USC MSCS ’25

Every Treadmill Runs the Same Program — So I Built One That Doesn’t

How a gym frustration became a capstone project: building an AI-powered IoT device that attaches to any treadmill and personalizes your run in real time.

The Person Next to You

It’s 7 a.m. and the gym is half-full. I’ve been coming here for over five years now—started at a bare-bones basement spot with rusted dumbbells and a single treadmill that had a speed knob and nothing else, eventually upgraded to a mid-tier chain, and finally landed at a place with rows of gleaming machines and a touchscreen on every console. Progress.

I step onto a treadmill, tap “Hill Climb,” plug in my weight, and start running. The belt accelerates. The incline rises. I glance at the person next to me. He’s maybe twenty years older, different build, different stride, probably training for something completely different than I am. He taps the same button. “Hill Climb.” His belt accelerates at the exact same rate. His incline follows the exact same curve.

The treadmill doesn’t know either of us. It doesn’t know that I want to peak hard in the middle and cool down aggressively, or that he might be building endurance for a half marathon. It just runs a preset curve and hopes for the best. And that bugged me more than it probably should have.


The Treadmill Hierarchy

After five years of upgrading gyms, I started seeing treadmills in tiers. Not officially—no one publishes this list—but once you notice it, you can’t unsee it.

Tier 1 is the basement treadmill. A speed knob. Maybe a kill switch lanyard if you’re lucky. You set it and you run. No data, no programs, no feedback. Pure analog. Honestly, there’s a charm to it.

Tier 2 adds incline control. Now you’ve got two dials: speed and grade. You can simulate hills manually, but you’re still the intelligence. The treadmill is a dumb motor that goes where you point it.

Tier 3 is where things get interesting—and deceptive. These are the machines that ask for your weight, age, and gender. They offer programs: Cardio, Fat Burn, Interval, Hill Climb, Heart Rate, Quick Start. You tap “Hill Climb,” it draws a nice mountain on the display, and the belt follows that profile. It feels personalized.

It isn’t.

Here’s what’s actually happening: every program is the same preset speed-and-incline curve. “Hill Climb” is one curve. “Fat Burn” is another. When you enter your weight and age, the machine multiplies that base curve by a scaling factor. A 180-pound 25-year-old gets a slightly steeper version of the same hill that a 140-pound 45-year-old gets. The shape is identical. The timing is identical. The machine never learns anything about how you actually run. It’s a lookup table pretending to be intelligence.

Tier 4 is the real deal: Apple Watch integration, Peloton-grade connected treadmills, Nike Run Club syncing, Fitbit coaching. These actually adapt. They read your heart rate in real time, adjust difficulty based on your history, and build profiles over weeks. They’re also prohibitively expensive. A connected treadmill runs $3,000+, plus a monthly subscription. Most gyms don’t have them. Most people can’t justify them.

So the gap between Tier 3 and Tier 4 is massive. On one side, you’ve got preset curves with a marketing layer. On the other, genuine personalization that costs a small fortune. That gap is where the idea started.


Not Everyone Runs the Same

Think about how different people actually use a treadmill. Some runners want a slow warmup, a hard peak in the middle, and a gradual cooldown. Others want to escalate the entire time—start easy and finish gasping. Some people want to end their run at max heart rate because that’s what their training plan demands. And then there are the goals: someone training for a marathon has completely different needs than someone focused on weight loss. The marathon runner wants sustained moderate intensity over long durations. The weight-loss runner might want high-intensity intervals that maximize caloric burn in 30 minutes.

Every one of these people steps onto the same Tier 3 treadmill and gets the same “Hill Climb.”

What if you could build an affordable device—something small enough to clip onto any treadmill, any brand, any tier—that tracks what the treadmill is doing, learns how you run, and guides you in real time toward your goals? Not a $3,000 connected treadmill. Not a subscription. Just a device and a phone.

That question became a capstone project. I built it with Yashvi Agrawal and Diya Hirani. We called it SmartStride.


Building the Hardware

The core challenge was measurement. A treadmill doesn’t expose an API. There’s no serial port on the side labeled “speed output.” If you want to know what a treadmill is doing, you have to observe it from the outside—like a physicist measuring a system without disturbing it.

The Brain

We chose the ESP32 as our microcontroller. Dual-core processor, built-in Bluetooth, plenty of GPIO pins, and it costs about $4. It handles sensor reading, signal processing, and wireless transmission all on one chip.

Measuring Speed

Speed was the harder problem. Our solution was an array of five light-dependent resistors (LDRs) mounted along the edge of the treadmill belt. As the belt rotates, a reflective marker passes the sensor array. The sensors detect the passage, and we measure the time between consecutive detections. Since we calibrated the belt circumference at 3.31 meters, the math is straightforward: distance divided by time, with a unit conversion to km/h.

The tricky part is noise. Gym lighting flickers. Belts vibrate. People bump the machine. We added a 500ms debounce filter—any detection within 500ms of the last one gets thrown out as a false positive. We also require more than one sensor in the array to trigger simultaneously, which eliminates most single-sensor noise.

Here’s the actual rotation detection and speed calculation running on the ESP32:

<span class="cm">// Sum sensor values to detect treadmill rotations</span>
<span class="type">int</span> sumSensorValues = <span class="num">0</span>;
<span class="kw">for</span> (<span class="type">int</span> i = <span class="num">0</span>; i < <span class="num">5</span>; i++) {
  sumSensorValues += <span class="fn">digitalRead</span>(sensorPins[i]);
}

<span class="cm">// Calculate treadmill speed when rotation detected</span>
<span class="kw">if</span> (countingStarted && sumSensorValues > <span class="num">1</span>
    && (<span class="fn">millis</span>() - rotationStartTime >= <span class="num">500</span>)) {
  <span class="type">unsigned long</span> rotationEndTime = <span class="fn">millis</span>();
  <span class="type">unsigned long</span> rotationTime = rotationEndTime - rotationStartTime;
  <span class="type">float</span> treadmillSpeed = (trackCircumference / (rotationTime / <span class="num">1000.0</span>) * <span class="num">3.6</span>) / <span class="num">2</span>;

  <span class="type">String</span> data = <span class="fn">String</span>(treadmillSpeed) + <span class="str">","</span> + <span class="fn">String</span>(angle) + <span class="str">","</span>;
  ESP_BT.<span class="fn">println</span>(data);  <span class="cm">// Send over Bluetooth</span>
}

Measuring Incline

For inclination, we used an MPU6050 gyroscope. It sits on the treadmill frame and measures the Z-axis rotation as the incline motor tilts the deck. We configured it for ±250°/s sensitivity with a 21Hz low-pass bandwidth filter to smooth out vibrations. The gyroscope integrates angular velocity over time to give us the current angle, which maps directly to the treadmill’s incline percentage.

The Package

An OLED display (128×64) shows real-time speed, incline, and connection status so you don’t need to pull out your phone to confirm it’s working. Everything fits inside a 3D-printed case designed to clip universally onto a treadmill’s side rail. No adhesive, no modification to the machine, no gym manager knocking on your door.

The data pipeline from device to phone is simple. The ESP32 formats speed and angle into a comma-separated string and transmits it over Bluetooth Low Energy at intervals of at least 500ms:

<span class="type">String</span> data = speedStr + <span class="str">","</span> + angleStr + <span class="str">","</span>;
ESP_BT.<span class="fn">println</span>(data);

That’s two floats and a delimiter, streamed continuously. The phone picks it up, parses it, and feeds it to the backend. No protocol overhead, no handshake complexity. Just numbers flowing.


Teaching It to Learn

With the hardware streaming live speed and incline data, the question became: what do you do with it? The obvious answer is “show a graph.” The interesting answer is “predict what should happen next.”

Why an LSTM

We needed a model that understands sequences. Running is inherently sequential—what you did five seconds ago matters more than what you did five minutes ago. If you just spiked your speed, the model should know that a brief recovery might be coming. If you’ve been steadily climbing for two minutes, the model should anticipate a plateau or descent.

Long Short-Term Memory networks are built for exactly this. Unlike a standard feedforward network that treats every input independently, an LSTM maintains a cell state—a kind of running memory that decides what to remember and what to forget. The key insight for our use case: the last few data points matter most. An LSTM’s forget gate naturally decays older information, giving recent readings more influence on the prediction. That recency bias is exactly what you want when someone is mid-run and the model needs to react to what’s happening now.

Engineering the Features

Raw sensor data alone isn’t enough. A speed reading of 8.2 km/h means nothing without context. Is it 8.2 km/h at 6 a.m. on a Monday (probably a routine run) or 8.2 km/h at 9 p.m. on a Friday (probably blowing off steam)? Is this user close to their target weight or far from it? Are they accelerating or decelerating?

We engineered 14 features from the raw data:

<span class="cm"># Feature Engineering</span>
data[<span class="str">'hour_of_day'</span>] = data[<span class="str">'timestamp'</span>].dt.hour
data[<span class="str">'day_of_week'</span>] = data[<span class="str">'timestamp'</span>].dt.dayofweek
data[<span class="str">'month'</span>] = data[<span class="str">'timestamp'</span>].dt.month
data[<span class="str">'time_of_day'</span>] = pd.<span class="fn">cut</span>(data[<span class="str">'hour_of_day'</span>],
    bins=[<span class="num">0</span>, <span class="num">6</span>, <span class="num">12</span>, <span class="num">18</span>, <span class="num">24</span>], labels=[<span class="num">0</span>, <span class="num">1</span>, <span class="num">2</span>, <span class="num">3</span>])

<span class="cm"># Running Metrics</span>
data[<span class="str">'average_speed'</span>] = data.<span class="fn">groupby</span>(<span class="str">'timestamp'</span>)[<span class="str">'speed'</span>].<span class="fn">transform</span>(<span class="str">'mean'</span>)
data[<span class="str">'max_speed'</span>] = data.<span class="fn">groupby</span>(<span class="str">'timestamp'</span>)[<span class="str">'speed'</span>].<span class="fn">transform</span>(<span class="str">'max'</span>)
data[<span class="str">'min_speed'</span>] = data.<span class="fn">groupby</span>(<span class="str">'timestamp'</span>)[<span class="str">'speed'</span>].<span class="fn">transform</span>(<span class="str">'min'</span>)
data[<span class="str">'speed_trend'</span>] = data[<span class="str">'speed'</span>].<span class="fn">rolling</span>(window=<span class="num">10</span>, min_periods=<span class="num">1</span>).<span class="fn">mean</span>()

<span class="cm"># Derived Features</span>
data[<span class="str">'progress_towards_target'</span>] = data[<span class="str">'current_weight'</span>] - data[<span class="str">'target_weight'</span>]
data[<span class="str">'speed_change'</span>] = data[<span class="str">'speed'</span>].<span class="fn">diff</span>()

These break down into three categories. Temporal features (hour, day, month, time-of-day bucket) capture when you run—because people run differently on Monday mornings versus Saturday afternoons. Performance features (average speed, max speed, min speed, speed trend, speed change) capture how you’re running right now. And goal features (progress toward target weight, weeks to achieve goal) capture why you’re running in the first place.

The Model

The architecture is a two-layer LSTM with 50 hidden units, feeding into a single dense output neuron that predicts the next recommended speed. We use 10-step input sequences—about five seconds of running history at our sampling rate—which gives the model enough context to detect patterns without drowning in stale data.

model = <span class="fn">Sequential</span>()
model.<span class="fn">add</span>(<span class="fn">LSTM</span>(<span class="num">50</span>, input_shape=(sequence_length, <span class="num">1</span>)))
model.<span class="fn">add</span>(<span class="fn">Dense</span>(<span class="num">1</span>))
model.<span class="fn">compile</span>(optimizer=<span class="str">'adam'</span>, loss=<span class="str">'mean_squared_error'</span>)

model.<span class="fn">fit</span>(X_train, y_train, epochs=<span class="num">10</span>, batch_size=<span class="num">32</span>,
          validation_data=(X_test, y_test))

We trained on 18,570 data points collected from 45 real participants over 8 weeks. The participants ranged from beginners who had never followed a structured treadmill program to intermediate and advanced runners with specific training goals. An 80/20 train-test split with min-max normalization across all features.

But here’s the thing that makes this more than a prediction engine. The model doesn’t just tell you what speed you should be running. When you deviate—you speed up because your favorite song comes on, or you slow down because your knee twinges—it doesn’t stubbornly insist you get back to the original plan. It re-plans from where you are, not where you were supposed to be. The last few data points it ingests are your actual current state, and the LSTM’s recency weighting means those fresh readings dominate the prediction. So the next recommendation accounts for your deviation. The run adapts live. It’s a conversation, not a lecture.


Bringing It Together

Building the hardware and building the model are two separate problems. The real engineering challenge is making them talk to each other in real time, reliably, while someone is running.

The full data path looks like this: the ESP32 reads its sensors and transmits speed and incline over Bluetooth Low Energy. A mobile app (built with MIT App Inventor for Android) picks up the Bluetooth stream, parses the values, and sends them to a FastAPI backend via HTTP. The backend stores every reading in PostgreSQL—building the user’s running history over time—and feeds the latest sequence into the LSTM model. The model returns a speed prediction, which travels back through the API to the app.

On the phone, the app shows a live graph: a line for your current speed and a line for where the model thinks you should be. The gap between those lines is your feedback signal. You manually adjust the treadmill to close the gap—or deliberately widen it if you’re feeling strong. When you deviate, the prediction line shifts on the next cycle. It’s a live feedback loop, not a rigid plan drawn before you started.

End-to-end data latency is under 100ms. Bluetooth pairing takes less than 5 seconds. Once connected, the system just runs. No manual syncing, no “please wait while we update your profile.”


Did It Work?

We ran the full system through a validation study with our 45 participants. The results:

94%
Speed Accuracy
91%
Incline Accuracy
18,570
Data Points
45
Participants

94% speed prediction accuracy means the model’s recommended speed was within a tight margin of what the runner actually needed at that moment—validated against both their stated goals and their physiological response. 91% inclination accuracy across a range of treadmill models, each with slightly different incline mechanics.

The study validated the approach across beginner, intermediate, and advanced fitness levels. Beginners benefited the most—the model effectively acted as a virtual coach, guiding them through progressions they wouldn’t have known to follow on their own. Advanced runners used it more as a calibration tool, comparing the model’s suggestions against their own instincts and adjusting their training plans accordingly.

We published the full methodology, architecture, and results as a peer-reviewed research paper. You can read it on Scribd, and the complete source code is on GitHub.

And here’s the part that still makes me smile: the device is still mounted on a treadmill today. It outlived the capstone. It outlived the semester, the graduation, and the “let’s never think about school projects again” phase. It just kept being useful, so it stayed.


What’s Next

SmartStride works, but it’s a first step. The obvious next move is heart rate integration—adding a Bluetooth HRM strap would give the model a direct physiological signal instead of inferring effort from speed patterns alone. Heart rate is the ground truth for intensity, and having it in the feature set would dramatically improve predictions for interval training and recovery periods.

Further out, there’s computer vision. A phone camera pointed at the runner could analyze form—stride length, cadence, posture breakdown—and provide real-time corrections. “You’re overstriding at this pace” is a coaching insight that no treadmill offers today.

On the model side, transformer architectures are the natural evolution. LSTMs handle short-term sequences well, but transformers with attention mechanisms could capture longer-horizon patterns—how your Tuesday runs relate to your Thursday runs, how your performance shifts over months, not just minutes.

But the core idea hasn’t changed: personalized fitness shouldn’t require a $3,000 treadmill and a monthly subscription. The technology exists to make every treadmill smart. Someone just needs to build it in a way that’s accessible to everyone, regardless of what machine they step onto. That’s still the goal.


← Back to blog