[SOLVED-WORKAROUND] Using javascript to set power data

This is probably a dumb question that stems from a dumb idea, but here goes.

I have an old bike on a dumb trainer with no power meters anywhere. However, I have the power curve for the dumb trainer matching speed vs power, so with a speed sensor, I can roughly estimate power. I have a bit of code to give me a chart of estimated power based on speed, but I was wondering if there was a way to modify it to set the actual power data, rather than a custom stream. The idea being if I can set the actual power data for the activity (rather than a custom stream), I can use the power analysis parts of intervals.icu with my MacGyver-ed setup.

I can see there is the "“Edit data” option that would let me manually type in power at each second, or “Fix data” which would let me do a y=mx+c type change to existing power data, but I guess what I’m looking for is a “Fix data” that lets me run custom code, or a way to use my existing code to set the actual power stream, rather than a custom one.

For reference, this is how I get my power estimate for the custom stream chart:

for(let m of icu.fit.record) {
   speed = m.enhanced_speed?.value * 3.6;
   data.setAt(m.timestamp.value, speed*(0.072258*speed+4.528797));
}

I realise that this is a woefully inaccurate way to calculate power, but I figure that a crude estimate of power has to be better than nothing (TBD, jury is still out).

(Sorry if I’m completely mucking up terminology here, I’m new to intervals.icu and still getting my head around everything)

Cheers,

Chris

Not sure if you’re aware of this but there are a number of indoor training apps that will do the math for you. Meaning, if you don’t have a Power source but do have a speed/cadence sensor, you set up the app to work with speed and virtual power. Those apps usually have quite a number of speed/power curves for different brands and models, so just pick out your own trainer.
An example is TrainerDay, but it sure is not the only one. I’m not sure if @app4g has this as a feature in his BreakAway indoor app. I have used that way of recording for quite some time on a laptop PC before I got my first SmartTrainer and later Power pedals. Can’t remember the name of the different apps I used but one was MaximumTrainer, which is no longer supported. You will probably find other phone aps now that use ‘Virtual Power’.
The advantage is that those apps create an output file just like a normal one with power device.

Yeah… BreakAway does have this feature. In fact, I was doing this way before I got my DirectDrive Smart Trainer.

In The App, I call it “SpeedPower”



But a word of warning… the numbers are NOT going to be accurate, when I got an actual Trainer w/ Power, the numbers (for my FTP) differ by up to 50w.

Having said that, if you’re up to mucking w/ the custom javascript, then by all means. (I’ll be honest, javascript is not my forte, but I had to learn it to look for clues on intervals.icu’s API as well as some other sites like Garmin)

I’ll chalk it up to differences in Drive Drive vs Wheels On (Slippage) and of course the differences in the (some of the manufacturer provided) Speed-To-Power Curve

Sure thing, but if this is your only power source, it is absolutely fine to follow-up on performance. Just never compare those numbers to those from real power meters or SmartTrainers. And if they are high, never mention them to someone with a powermeter because he might challenge you to reproduce those numbers on his equipment :wink:

hahaha… i had my first “real” try at an organised mass trainer event (it was a promo for a newly opened Trainer Gym) and I knew better than to open my mouth. (crash and burn baby… crash and burn…)

Thanks @MedTechCD and @app4g. It looks like BreakAway is iOS only, so unfortunately not an option for me. I’ll keep having a look around to see if I can get something that works for my setup.

One interesting option that came up was ConnectIQ AppBuilder, which lets you make a custom data field on a Garmin device with whatever maths you want behind it. If I can use that to take my equation and save that as power, I’d be quids in.

I was hoping there was a way to do this just in intervals.icu, but having played around with it a little more, it seems like the lack of power data in the original fit file is a bit of a sticking point, so more research is needed if I want to go down that route.

Oh, absolutely. But it should at least be consistent, so I can still use it as a progression measure of arbitrary value. The fact that the power curve I found was a small and blurry graph, and I did my curve fit by measuring pixels in paint screams of the whole setup being a total bodge. But a consistent bodge :slight_smile:

Ah… Yes… ConnectIQ, this will work as well. This was one of the route I went previously. But the equation needs to be simpler (no quadratic stuffs)

You can. Although it will be saved as “developer field” and won’t be useful in some sites. (intervals.icu will still be able to do it, but you need to map the field)

How so? You “technically” only need the speed data. But not knowing much aout javascript, I’m not sure how this can be done, although it should still be doable as most of the calculated fields I’ve seen others shared is doing things like power = speed * X

your mileage may vary… lolz. I know cos diff days my tyre would have diff tyre pressure and then i didn’t tighten up the wheels to the trainer. things like that.

There’s always Golden Cheetah… bit of a learning curve tho.

Using “Edit data” to try and manually type in some power numbers throws an error “Stream type [watts] not found”. So I have some blanks to fill in somewhere before adding the estimated data that I haven’t quite figured out how to do yet.

I’m working on the assumption that the biggest variable in the system is the engine :wink: This is all a bit of a “try-before-you-buy” solution to dabble in “power” based training. I’m used to training by RPE, but thought this would be a fun experiment over the winter to see if power meters might be worth the investment for me.

Small differences in tire pressure, air pressure and clamp pressure, can easily influence up to 30-40W! It might come as a surprise but even environmental temperature can skew about 10 W.
My advice: if you can leave the bike on the trainer and inflate your rear tire to the same pressure before every workout, you wiĺl be able to spot performance changes over a couple of weeks.

1 Like

I had about 10% difference in estimated power from my “dumb” trainer. All my rides from back in the day have been marked as “ignore power” because it was too high, and irrelevant to my true ability.

I would very much like to be able to support this use-case. Unfortunately as things are it is not possible to generate the watts stream using a custom stream. These are evaluated late in the process after the activity analysis has been done.

As has been suggested on this thread you need to use an app or something similar to get a power stream into the fit file. Intervals.icu will let you select that to use for power (Actions → Settings under the ride timeline chart).

Thanks @david - I’ll see if I can find an android app that’ll do what I need to.

Thanks for your input everyone. I’ll report back if I find a nice working solution.

1 Like

OK, so I’ve managed to bodge something together that mostly does what I want. It screams “temporary workaround”, because that’s exactly what it is.

Here is my workflow:

  1. Download TCX from Garmin Connect
  2. Run script that inserts power data into TCX file
  3. Upload TCX to intervals.icu
  4. Set original powerless activity settings to ignore time, distance, load, HR, etc data to avoid double counting effort (I could move the original, but I’m keeping it there just in case)

The reason I use a TCX is because it is human readable, and FIT files are a bit of a pain to hack around in. The TCX is quite nice, because I don’t even need to parse it as XML. If you flick through it line by line, you find the speed information is stored in a TrackPoint with the handy tag of “ns3:Speed”:

          <Trackpoint>
            <Time>2023-10-20T07:30:17.000Z</Time>
            <DistanceMeters>7.039999961853027</DistanceMeters>
            <HeartRateBpm>
              <Value>98</Value>
            </HeartRateBpm>
            <Cadence>49</Cadence>
            <Extensions>
              <ns3:TPX>
                <ns3:Speed>2.4070000648498535</ns3:Speed>
              </ns3:TPX>
            </Extensions>
          </Trackpoint>

My script runs through the TCX line by line, and when it finds a line with “ns3:Speed”, it grabs the speed value, uses it to estimate power (using the power curve for my dumb trainer), and inserts the line back into the file with a “ns3:Watts” tag:

          <Trackpoint>
            <Time>2023-10-20T07:30:17.000Z</Time>
            <DistanceMeters>7.039999961853027</DistanceMeters>
            <HeartRateBpm>
              <Value>98</Value>
            </HeartRateBpm>
            <Cadence>49</Cadence>
            <Extensions>
              <ns3:TPX>
                <ns3:Speed>2.4070000648498535</ns3:Speed>
                <ns3:Watts>44.668474977210856</ns3:Watts>
              </ns3:TPX>
            </Extensions>
          </Trackpoint>

It rattles through the whole file, adding Watts where it finds Speed, and I can just throw this into intervals.icu and see power analysis.

It is a bit manual at the moment, because I haven’t looked at the Connect and intervals.icu APIs to try and fully automate this process. But given that this is likely a temporary workaround, I may not bother fully automating it.

Script below if anybody is interested:

#!/usr/bin/env python3
"""
Hack/bodge to insert power estimate into a TCX indoor cycling file

Makes loads of glorious assumptions
"""

import argparse
import logging
import re


def main(infile):
    """
    main : main function

    Really, it's the only function.
    """
    outfile = re.sub(r'\.tcx', '_pwr.tcx', infile)

    activity_file = []
    logging.info('Reading activity from %s', infile)
    with open(infile, 'r', encoding='utf-8') as infile_f:
        for line in infile_f:
            activity_file.append(line)

            re_speed = re.search(r'<ns3:Speed>([\d\.]+)</ns3:Speed>', line)
            if re_speed is None:
                continue
            logging.debug('Matched speed line, attaching power')
            speed = float(re_speed.group(1)) * 3.6 # m/s -> km/h
            power = speed*(0.072258*speed + 4.528797)
            power_line = re.sub(r'<ns3:Speed>([\d\.]+)</ns3:Speed>',
                    f'<ns3:Watts>{power}</ns3:Watts>',
                    line)
            logging.debug(line)
            logging.debug(power_line)
            activity_file.append(power_line)

    logging.info('Writing activity to %s', outfile)
    with open(outfile, 'w', encoding='utf-8') as outfile_f:
        for line in activity_file:
            outfile_f.write(line)


def _argparser():
    """
    Wrapper for argparse
    """
    parser = argparse.ArgumentParser(prog='set_power',
            description='Hack/bodge to insert power estimate into a TCX indoor cycling file')
    parser.add_argument('infile')
    parser.add_argument('-v', '--verbose',
            action='store_true', help='Increase verbosity')
    return parser.parse_args()


if __name__ == '__main__':
    args = _argparser()
    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO)
    main(args.infile)

2 Likes

Side note: I found this site a few years ago as i was looking for the power curve for the Minoura MagTurbo II.
Apparently the product is doing something similar to what you’re doing and appears as a ‘power meter’?

1 Like