cagnulein / qdomyos-zwift

Zwift bridge for smart treadmills and bike/cyclette
https://www.qzfitness.com/
GNU General Public License v3.0
353 stars 107 forks source link

[BUG] Peloton Power Zone / Zwift ERG Auto Resistance Broken for EX3 w/ Power Meter Pedals #2431

Closed mp3guy closed 1 week ago

mp3guy commented 2 weeks ago

Describe the bug Setup: Echelon EX3 Bike, Garmin Rally XC200 Power Meter Pedals, QZ v2.16.60 on Android 10 on Samsung Galaxy S9, Laptop for Zwift/Peloton.

When using auto resistance in Peloton Power Meter classes, or ERG Mode with Zwift Workouts, the resistance control of the bike to match the target power is chaotic, unreliable, unresponsive.

I have tried removing and repairing the power meter pedals multiple times, and I have also tried with and without the 5 second average option; the problem persists for both. This problem does not occur if not using the power meter pedals.

To Reproduce Steps to reproduce the behavior:

  1. Setup QZ with power meter pedals.
  2. Start a Peloton Power Zone class, or a Zwift Workout with ERG Mode on.
  3. Observe chaotic resistance control behaviour.

Expected behavior I expect it to function as well as it did without power meter pedals; I was able to do both of these activities fine with just the EX3 bike on its own before I got the power meters.

Screenshots N/A

Desktop (please complete the following information):

Smartphone (please complete the following information):

Append a debug log peloton_5s_avg.log zwift_workout_erg_no_avg.log

Additional context https://www.facebook.com/groups/149984563348738/posts/833968418283679/

cagnulein commented 2 weeks ago

@mp3guy I guess i fixed https://github.com/cagnulein/qdomyos-zwift/actions/runs/9835459688

Google is very slow in these days, reviewing the app on the store, even the beta ones. For this reason, in order to check right now if the patch that I did is right, you can use the android build in the link (check at the bottom of the page in about 1 hour). In order to test it you need first to remove the current QZ version on your phone, install the one that you downloaded. This version is a 15 minutes trial (you can restart it as many times as you want) and I can unlock it if you want, send me an email to roberto.viola83@gmail.com mentioning this ticket and that you are on android.

After installing it, follow these in order to reset the erg table learned previously: 1) remove the power sensor setting and press ok and restart qz 2) add again the power sensor and press ok and restart qz 3) do some spinups staying on each resistance for at least 30 seconds in order to give the possibility to qz to learn the wattage on each resistance (it can also be done on realtime, but it's better to do first since you already had an issue)

Let me know if you have any questions

mp3guy commented 2 weeks ago

I tried this out, there was no change in the experience.

Is the erg table trying to dynamically and constantly populate while riding? I don't think that's going to work without compensating for the time lag between the power meters and the bike resistance control, the variability is huge. And I noticed even on the EX3 the resistance is non linear when changing levels, it usually dips a little bit momentarily making the power measurement even less accurate during that small window.

cagnulein commented 2 weeks ago

yes it's dinamically populating it. please add the debug log so i can check that the patch is working correctly, unfortunately i can't test it myself so i have to check the debug log to understand if it's working ok

thanks i'm going to bed now, i will check it tomorrow

mp3guy commented 2 weeks ago

Attached a new debug log, taken after disabling the power sensor, restarting, adding the power sensor and then restarting. debug-Tue_Jul_9_07_46_49_2024.log

cagnulein commented 2 weeks ago

ok in this log i can see only a value in the erg table, that's the issue, so qz was still learning your bike. Did you do, as I suggested, some spins for at least 10s or more on all the resistance levels? so qz could learn the different wattage to the all resistance levels and to the different cadence values

Let me know

mp3guy commented 2 weeks ago

I don't think that was the issue, as I said this was captured after a fresh remove/re-add of the power meters, so a table rebuild is expected.

With 32 resistance levels, it would take 5 minutes of constant spin ups for all to complete, that seems a bit excessive? Yesterday when I first tested your patch I did spend some time varying the resistance and cadence for about a minute before engaging ERG. Either way there will still be collisions in the ERG table; the latency is not accounted for.

Is there a way to log all triplets for a period of time? I suspect if we do that, and plot the 3D resistance/cadence/power matrix, we'll see a range of different power values for the same resistance/cadence values due to the total system latency. That's the problem.

cagnulein commented 2 weeks ago

I don't think that was the issue, as I said this was captured after a fresh remove/re-add of the power meters, so a table rebuild is expected.

This is for sure the issue since in the log the table is not there. That's why I said so.

With 32 resistance levels, it would take 5 minutes of constant spin ups for all to complete, that seems a bit excessive?

Yes, it's what you have to do the first time. QZ is learning, it's already amazing that a software can do this, so please follow my advice and we will get it. Remember that we are basically hacking the bike in this way, so there is no shortcuts on this. Also I can't test this algorithm with your bike, so only you can help yourself on this :)

Yesterday when I first tested your patch I did spend some time varying the resistance and cadence for about a minute before engaging ERG.

it's important that you stay on each resistance more than 10 seconds, i added this filter in order to staibilize the power read from the sensor, otherwise from other bikes, we had a lot of spikes.

Is there a way to log all triplets for a period of time? I suspect if we do that, and plot the 3D resistance/cadence/power matrix, we'll see a range of different power values for the same resistance/cadence values due to the total system latency. That's the problem.

yes it's what qz does when the table is filled. Please follow my advice and we will check, that's the only way

cagnulein commented 2 weeks ago

also you can see the table yourself in the log, check for

ergTable::loadSettings(

mp3guy commented 2 weeks ago

It seems to load nonsensical values to start?

Tue Jul 9 07:46:51 2024 1720507611956 Debug: ./ergtable.h void ergTable::loadSettings() inputs.append(ergDataPoint( 0 ,  43 ,  18 ));

How can a cadence of 0 provide 43W?

So looking at the source, you reject additional samples when the resistance has changed within the last 1000ms, but you take the first cadence/resistance pair in a greedy/first-come-first-served manner. As you know power meter pedals have much higher variance so just waiting 1 second isn't enough to account for the sub-second cadence/force dynamics.

See these samples from the log I sent last:

Tue Jul 9 07:47:55 2024 1720507675567 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) newPointAdded C 102 W 168 R 18
Tue Jul 9 07:47:56 2024 1720507676587 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) newPointAdded C 118 W 205 R 18
Tue Jul 9 07:47:57 2024 1720507677567 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) newPointAdded C 119 W 171 R 18

Tue Jul 9 07:47:55 2024 1720507675772 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 102 W 308 R 18
Tue Jul 9 07:47:55 2024 1720507675968 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 102 W 308 R 18
Tue Jul 9 07:47:56 2024 1720507676249 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 102 W 205 R 18
Tue Jul 9 07:47:56 2024 1720507676369 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 102 W 205 R 18
Tue Jul 9 07:47:56 2024 1720507676770 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 118 W 205 R 18
Tue Jul 9 07:47:57 2024 1720507677013 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 118 W 171 R 18
Tue Jul 9 07:47:57 2024 1720507677258 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 118 W 171 R 18
Tue Jul 9 07:47:57 2024 1720507677369 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 118 W 171 R 18
Tue Jul 9 07:47:57 2024 1720507677768 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 171 R 18
Tue Jul 9 07:47:57 2024 1720507677969 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 171 R 18
Tue Jul 9 07:47:58 2024 1720507678240 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 146 R 18
Tue Jul 9 07:47:58 2024 1720507678368 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 146 R 18
Tue Jul 9 07:47:58 2024 1720507678568 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 146 R 18
Tue Jul 9 07:47:58 2024 1720507678769 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 160 R 18
Tue Jul 9 07:47:58 2024 1720507678969 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 160 R 18
Tue Jul 9 07:47:59 2024 1720507679245 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 160 R 18
Tue Jul 9 07:47:59 2024 1720507679371 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 160 R 18
Tue Jul 9 07:47:59 2024 1720507679565 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) discarded C 119 W 153 R 18

Take C 102 with R 18: The setup is presented with potential watt values of 168, 308 and 205. So which is it? The code just takes the first one that arrives which is not going to be correct, it's just pure random chance.

I would imagine a better solution would be to compute the median/mode for every cadence/resistance pair collected in an online manner, either by storing a fixed sized window of samples, or by using an approximation like P-Square if worried about memory: https://www.cse.wustl.edu/~jain/papers/ftp/psqr.pdf. Napkin math: 32 resistance levels, 150 cadence levels and say we store 100 watt samples each, that's < 1MByte. And you could cache the last computed median for every pair, only recomputing when a new sample is introduced (or just stop after you get 100 and free the memory).

Also the table building doesn't enforce a monotonically increasing wattage output for increasing cadence/resistance. This means it's possible for it to be populated with unlikely values like this:

Tue Jul 9 07:47:56 2024 1720507676587 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) newPointAdded C 118 W 205 R 18
Tue Jul 9 07:47:57 2024 1720507677567 Debug: ./ergtable.h void ergTable::collectData(uint16_t, uint16_t, uint16_t, bool) newPointAdded C 119 W 171 R 18

How can the watts be 34 lower for the same resistance but with a higher (admittedly moderately) cadence?

If there is a way to spin up a fast, debug-able environment I might be able to help out with a more robust implementation. Or if I could dump all CWR triples for a sequence and analyse the data to work on an implementation in my spare time.

cagnulein commented 2 weeks ago

I suggest to do a PR on the code, so we can test directly your theory :) It worked on other bikes, so since I can't do a test on my own I suggest to write directly the patch so we can test it. The code is auto building for android, so you don't need an IDE or other. Just pure code :)

Thanks

cagnulein commented 2 weeks ago

in the log you can grep the "current cadence" "current watt" and "current resistance" or you can simpy use the xml line

TemplateInfoSender::update(QJSEngine *)

that has already all the metrics

mp3guy commented 1 week ago

I've replaced my EX3 with a new bike and no longer will be using the power meters with QZ so no longer need this fixed.

cagnulein commented 1 week ago

ok no problem, thanks for the update