Calculating Speed and Distance


Getting the treadmill's belt length 

The first thing we need to know is the belt length of the treadmill. You'll need a tape measure or measuring stick. Even better if it can measure in metric (unlike my All-American tape measure seen here).
You'll also need a few pieces of tape; probably three small pieces.

Start by places one of the pieces on the belt towards the rear of the treadmill and the second piece up towards the front. Measure the distance between the edges of tape and write this down. Remember to always measure from the same edge of the tape so if you measured from the edge closest to the back of the treadmill, always use that edge. Manually rotate the belt until the second piece of tape is near the rear of the treadmill, add another piece towards the front, and take another measurement. Rotate the treadmill one more time and you're likely back to the first piece of tape. Measure once more and add all of your measurements together. This is the length of the treadmill belt.

It's easiest in the code to have this number in millimeters. If you measured in metric, you're probably all set. If you measured in imperial units, your measurement is likely in inches. It's time to convert this:
Hey Siri, how many millimeters is X inches? 
Alexa, how many millimeters is X inches?
Ok Google, how many millimeters is X inches?
Or just multiply by 25.4: millimeters = inches * 25.4

We've got our belt length in millimeters, time to add that to the code:
#define BELT_LENGTH 3162 /* millimeters */

Do you really need to know the belt length?

Earlier I said that the first thing we needed to do was to measure the length of the belt. Measuring the belt is going to get the most accurate representation of the belt speed, but you could probably get away without doing it by utilizing the foot pod calibration offered by the watch itself. Typically (all the time?) this calibration is a constant scaling of the foot pod's reported distance and pace. 

After one run with the wrong belt length defined to the wrong value you could calibrate the "foot pod" within the watch using the distance reported by the treadmill and the watch will essentially adjust the preset belt length by scaling it up or down. 

Here's a few examples: 
If you ran 3 units of measurement according to the treadmill and the reported distance was 2.7 on the watch, you'd calibrate the "foot pod" by entering 3. The watch can calculate the scale factor by dividing the reported (3) by the recorded (2.7) coming up with 1.11. This means that future distances and paces will be treated as if they were 111% of the reported value coming from the "foot pod".

The same holds true if the recorded distance was more than the reported length. In this example, the treadmill reports 3 and the watch recorded 3.3. The scale can still be calculated the same way; by dividing reported (3) by recorded (3.3) to come up with 0.909. This time future distances and paces will be treated as 90.9% of the actual reported value.

What's the drawback to this approach?
Without knowing the actual length of the belt you're going to be relying on the treadmill to report back accurate distance. This is most likely not going to be the case (as others have also observed). In my testing, I've found that my actual belt speed is faster than what the treadmill reports and that it seems to get a little faster as time goes on. But if all you care about is getting the watch a little closer to what the display on the treadmill shows, this approach should work. 

The Calculations

After a bit of black-box testing using two Garmin watches, I've narrowed down the ANT+ SDM Profile fields affecting recorded distance and pace to Instantaneous Speed and Distance.

Strides

This field actually doesn't seem to have an any ill-effects against the recorded cadence, so I just increment once for each white mark seen. Even though the profile specification states that this field is required, I didn't seen any issues when leaving it set to 0. 
m_ant_sdm.SDM_PROFILE_strides += 1;

Distance

This one is really easy, for each white mark seen add one belt length:
static uint32_t distance = 0;
distance += BELT_LENGTH;
distance %= MAX_DISTANCE_ROLLOVER;
m_ant_sdm.SDM_PROFILE_distance = value_rescale(distance, 1000, ANT_SDM_DISTANCE_UNIT_REVERSAL);

I originally overlooked the negative implications of scaling the belt length into sixteenths of a meter for each interrupt. By using the belt length value directly in this scaling, the code was potentially adding an additional 1/16 of a meter for each belt length. In reality, this was making my belt length of 3.162m into a belt of 3.1875m. I later fixed this issue by storing the overall distance in millimeters, taking into account that the profile says that distance rolls over at 256 meters, or 256000 millimeters (MAX_DISTANCE_ROLLOVER). This appears to have made the watch recorded distance and pace closer to that of the treadmill, but more testing is needed.

Recall that ANT+ SDM Profile expects the reported distance in meters with fractional portions in 1/16 of a meter. Internally, this code stores the distance in sixteenths of a meter (in m_ant_sdm.SDM_PROFILE_distance) so the value_rescale function takes our current distance in millimeters, divides it by 1000 to get meters, and then multiplies that by 16 to get the distance in sixteenths of a meter before adding it to the overall distance.

Pace

Remembering back to high school physics or if you're not there yet (first, kudos to you for following along), here's a spoiler:
velocity = distance / time
So calculating pace is relatively simple each time the white mark is seen on the belt. By keeping track of the time between the last white mark and the current white mark we know how much time elapsed for one belt length. Since we measured the belt length earlier and defined it as a constant then calculating pace is simply:
instantaneous pace = belt length in millimeters / time delta in milliseconds
In this case, the ANT+ SDM Profile expects the reported instantaneous speed in meters per second with fractional portions in 1/256. Internally, the m_ant_sdm.SDM_PROFILE_speed is in two-hundred-fifty-sixths so we'll use value_rescale again to convert.

m_ant_sdm.SDM_PROFILE_speed = 
  value_rescale(
    BELT_LENGTH, 
    timeInMillis, 
    ANT_SDM_SPEED_UNIT_REVERSAL);

At this point we need to know the time delta between the last seen mark and current mark. We'll track this in milliseconds which is where storing the belt length in millimeters saves us a bit of work. Millimeters per millisecond is the same as meters per second so we'll get that part for free. Multiplying that result by 256 gets us the value the example code profile structure expects.

Back to the time value now. For this we need to know the last time we saw the white mark. Using the static keyword tells the code to store the value in the global scope and allows it to retain its value when this function goes out of scope.
static uint32_t lastTickCount = 0;

The next time the white mark is seen we'll get the tick count again. The app_timer_cnt_get function gets us this value:
currentTickCount = app_timer_cnt_get();

But we've got tick counts and we really need milliseconds. So we'll calculate the delta between the current and last seen tick counts and convert it to milliseconds:
timeInMillis = 
  app_timer_cnt_diff_compute(currentTickCount, lastTickCount)
  * ((APP_TIMER_CONFIG_RTC_FREQUENCY + 1) * 1000)
  / APP_TIMER_CLOCK_FREQ;

In this case it's important to use the app_timer_cnt_diff_compute function because it will take into the fact that the tick count could have overflowed.

After all of the calculations are done, set the lastTickCount to currentTickCount so the next mark's calculations are correct.

A note on debouncing

On the surface it would seem that for each trigger of the optical sensor there would be one high-to-low interrupt triggered. This isn't necessarily true, but it's something we can deal with in software by marking sure that some minimum about of time has gone by between white mark "detections" to clean up these spurious pulses.

Next up, we'll talk about a small problem to calculating the instantaneous pace this way.

Comments