TrueForm Runner treadmill UPDATE

Hi all,

Similar to Roby, I also am programming the older TrueForm Runner. My display didn't work, the arduino board was fried when i bought it used, so I had to replace it with a new one. In case anyone else is in the same spot, I'll add my code that reprograms the display for this treadmill.

The treadmill is manual, but comes with a sensor that connects to pin 2 on the adurino. There's 4 LCD displays on it that I needed to program around the sensor based on revolutions / second. All displayed info (pace, speed, distance) is in km.

Previous thread:

@robybrisson in case you are still working on this.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>

Adafruit_7segment disp1; // 0x70
Adafruit_7segment disp2; // 0x71
Adafruit_7segment disp3; // 0x72
Adafruit_7segment disp4; // 0x74

// Physical order: 3,2,4,1  =>  pace, speed, distance, time
Adafruit_7segment* PACE     = &disp3;
Adafruit_7segment* SPEED    = &disp2;
Adafruit_7segment* DISTANCE = &disp4;
Adafruit_7segment* TIMEHUD  = &disp1;

const byte  SENSOR_PIN = 2;            // interrupt-capable
const float CM_PER_REV = 15.34f;
const float M_PER_REV  = CM_PER_REV / 100.0f;

// --- ISR counters ---
volatile unsigned long revCountISR = 0;
volatile unsigned long lastPulseUs = 0;

// rolling window: 5 s, 10 Hz -> 50 buckets
const uint16_t SAMPLE_MS   = 100;
const uint8_t  WIN_SECONDS = 5;
const uint8_t  BUCKETS     = (WIN_SECONDS * 1000) / SAMPLE_MS;

unsigned long lastUpdateMs  = 0;
unsigned long startMs       = 0;

unsigned long prevTotal     = 0;
uint16_t buckets[BUCKETS];
uint8_t  idx = 0;
uint16_t sumWin = 0;

// --- ISR: software debounce using micros() ---
void onRise() {
  unsigned long t = micros();
  if ((t - lastPulseUs) >= 2000) {  // ignore pulses <2 ms apart
    revCountISR++;
    lastPulseUs = t;
  }
}

void beginDisp(Adafruit_7segment& d, uint8_t addr) {
  d.begin(addr);
  d.setBrightness(10);
  d.clear();
  d.writeDisplay();
}

void showMMSS(Adafruit_7segment* d, unsigned long secs) {
  unsigned int mm = secs / 60;
  unsigned int ss = secs % 60;
  if (mm > 99) mm = 99;
  d->clear();
  d->print(mm * 100 + ss);
  d->drawColon(true);
  d->writeDisplay();
}

void showInt(Adafruit_7segment* d, long v) {
  if (v > 9999) v = 9999;
  d->clear();
  d->print(v);
  d->writeDisplay();
}

void showFloat(Adafruit_7segment* d, float v, uint8_t decimals) {
  d->clear();
  d->print(v, decimals);   // Adafruit_7segment handles decimal point
  d->writeDisplay();
}

void setup() {
  Wire.begin();
  beginDisp(disp1, 0x70);
  beginDisp(disp2, 0x71);
  beginDisp(disp3, 0x72);
  beginDisp(disp4, 0x74);

  // mapping check
  PACE->print(1);     PACE->writeDisplay();
  SPEED->print(2);    SPEED->writeDisplay();
  DISTANCE->print(3); DISTANCE->writeDisplay();
  TIMEHUD->print(4);  TIMEHUD->writeDisplay();
  delay(300);
  PACE->clear();     PACE->writeDisplay();
  SPEED->clear();    SPEED->writeDisplay();
  DISTANCE->clear(); DISTANCE->writeDisplay();
  TIMEHUD->clear();  TIMEHUD->writeDisplay();

  pinMode(SENSOR_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onRise, RISING);

  memset(buckets, 0, sizeof(buckets));
  startMs      = millis();
  lastUpdateMs = startMs;
}

void loop() {
  unsigned long now = millis();

  // update at 10 Hz
  if (now - lastUpdateMs >= SAMPLE_MS) {
    lastUpdateMs += SAMPLE_MS;

    // atomic read
    noInterrupts();
    unsigned long total = revCountISR;
    interrupts();

    // bucket update
    unsigned long delta = total - prevTotal;
    prevTotal = total;

    sumWin -= buckets[idx];
    buckets[idx] = (delta > 65535UL) ? 65535 : (uint16_t)delta;
    sumWin += buckets[idx];
    idx = (idx + 1) % BUCKETS;

    // elapsed time
    unsigned long elapsedSec = (now - startMs) / 1000UL;

    // total distance (km)
    float metersTotal = total * M_PER_REV;
    float kmTotal     = metersTotal / 1000.0f;

    // window metrics
    float metersWin = sumWin * M_PER_REV;
    float speedKmh  = (metersWin * 3.6f) / (float)WIN_SECONDS;   // FIXED

    unsigned long paceSecPerKm = 0; // min/km as seconds
    if (speedKmh > 0.0001f) {
      float secPerKm = 3600.0f / speedKmh;                       // 3600 sec / (km/h)
      paceSecPerKm = (unsigned long)(secPerKm + 0.5f);
    }

    // render
    showMMSS(TIMEHUD, elapsedSec);

    // distance in km
    if (kmTotal < 10.0f)        showFloat(DISTANCE, kmTotal, 3);
    else if (kmTotal < 100.0f)  showFloat(DISTANCE, kmTotal, 1);
    else                        showInt(DISTANCE, (long)(kmTotal + 0.5f));

    // speed in km/h
    if (speedKmh < 0.05f) {
      showInt(SPEED, 0);
    } else if (speedKmh < 100.0f) {
      showFloat(SPEED, speedKmh, 1);           // e.g., 8.1
    } else {
      showInt(SPEED, (long)(speedKmh + 0.5f)); // 100+
    }

    // pace in min:sec per km
    if (paceSecPerKm == 0) {
      showInt(PACE, 0);
    } else {
      showMMSS(PACE, paceSecPerKm);
    }
  }
}

Haha, thanks! I ended up borking my displays by putting a power supply that was too strong on it (after I had got them working code wise, no less). In the end, I didn’t care and scrapped them (3d-printed a cover instead). I have had my treadmill working with Zwift for quite a while, and re-worked it to add additional functionality earlier this year using Claude. It is working very well and I just use my Zwift tablet for any of the metrics, so displays are unnecessary for me.

Good luck with your treadmill project!