Power and Speed Metrics#
This article covers how TrackMyIndoorWorkout applies per-device power correction factors, optionally cascades those factors into speed and distance, and derives cumulative distance by integrating speed over time for devices that don't report it natively.
Power Factor#
Each device has a _powerFactor (default 1.0) stored in FitnessEquipment . It is loaded from the PowerTune database collection, keyed by device MAC address, via DbUtils.getFactors which returns a Tuple3<double, double, double> of (powerFactor, calorieFactor, hrCalorieFactor). refreshFactors fetches these values from the DB and is called both at workout start and on every configuration read.
The factor is applied in Record.adjustByFactors:
poweris multiplied bypowerFactorwhenever(powerFactor - 1.0).abs() > eps.
Extend Tuning (Cascading to Speed & Distance)#
When the "Extend Power Tuning If Applicable" preference (extendTuningTag, default false) is enabled, adjustByFactors also scales speed, distance, and pace by the same factor :
speed *= powerFactordistance *= powerFactorpace /= powerFactor(inverse, since higher speed → lower pace)
Note: If the device already derives speed or calories from power internally, the cascade may compound the effect. If both a calorie factor and a power factor are configured, their effects combine. See the preference description for caveats .
adjustByFactors is called once per incoming record stub inside processRecord, before any supplemental calculations.
Speed Supplementation from Power#
When a device reports power but not speed (!hasSpeedReporting && isMoving), optionallyCalculateSpeed derives speed from power using the cycling model:
speed (km/h) = velocityForPowerCardano(power) × 3.6
velocityForPowerCardano solves the cubic power-speed relationship via Cardano's formula, with results cached in velocityForPowerDict for performance. The underlying physics model uses:
- Rolling resistance:
fRolling = g × (athleteWeight + bikeWeight) × rollingResistanceCoefficient - Aerodynamic drag:
fDrag = 0.5 × frontalArea × Cd × airDensity × v² - Drivetrain loss fraction
- A per-sport multiplier via
sportFactor(e.g.,1.9×for running/rowing,3.0×for kayaking,1.0×for cycling)
The constants (a, c, q, driveTrainFraction) are initialized by initPower2SpeedConstants, which reads user preferences (athlete weight, bike weight, drivetrain loss, air temperature, drag force tune) and clears the velocity cache whenever any of them change.
Symmetrically, powerForVelocity computes power from speed and is used to supplement power when a device only reports speed.
Distance Integration from Speed#
When a device does not report total distance ((stub.distance ?? 0.0) < eps), optionallyCalculateDistance integrates speed over the elapsed time delta:
dD = speed (km/h) × (1/3.6) × dT (seconds)
stub.distance += dD
dT is computed as (elapsedMillis - lastRecord.elapsedMillis) / 1000.0 . Because speed may already carry the powerFactor effect (from adjustByFactors when extendTuning is on), the comment in the code notes this explicitly .
Cumulative Distance Enforcement#
Once distance reporting is detected (hasTotalDistanceReporting), the system tracks a _startingDistance offset to normalize out the device's pre-workout accumulated total . Downstream, cumulativeMetricsEnforcements ensures that cumulative fields (distance, elapsed, calories, stroke count) never decrease across successive records.
Key Source Files#
| File | Purpose |
|---|---|
lib/utils/power_speed_mixin.dart | Cardano solver, powerForVelocity, physics constants, cache |
lib/devices/gadgets/fitness_equipment.dart | Factor application, speed/distance supplementation, processRecord |
lib/persistence/record.dart | adjustByFactors, cumulative enforcement |
lib/preferences/extend_tuning.dart | extendTuning preference definition and description |
lib/devices/device_descriptors/device_descriptor.dart | Unit conversion constants (ms2kmh, kmh2ms) |