qdomyos-zwift icon indicating copy to clipboard operation
qdomyos-zwift copied to clipboard

[REQ] make Treadmill with simulated buttons smart

Open december-soul opened this issue 2 years ago • 105 comments

I have a simple treadmill that is not smart. However, the buttons to increase the speed and incline are very easily accessible so that you could simulate them with a raspi. I think you could do it this way: After switching on the treadmill, the speed and incline are set to 0. Each press of a button increases the speed by 0,1 km/h. Therefore you could control the treadmill with a Raspi. It only has to remember how many times it has pressed the "button".

Regardless, I would then take the power from my Stryd and the HR from my chest strap. But I can also link that directly to Zwift.

Where do I start best and are there already approaches? Feel free to close this issue or rename it.

december-soul avatar Dec 22 '21 18:12 december-soul

Hi! The best approach is to use stryd As a power sensor device and the option power sensor as a treadmill. So you will see the metrics from the stryd And we can easily add a way to trim the speed at the inclination with the plus and minus buttons. What do you think?

Roberto Viola Software engineer and open source enthusiast http://robertoviola.cloud

cagnulein avatar Dec 22 '21 19:12 cagnulein

My idea was more in the direction that the treadmill is controlled not that I can measure the values.

So only that my treadmill is controlled by Zwift. If my training plan says "10 min @ 7:00, 30 min @ 5:30, 10 min @ 7:30", then the treadmill should adjust to the speed. The real values are then measured with the Stryd. Additionally, the incline can be adjusted to the track incline of Zwift.

So the Raspi should "push" the buttons itself. Since I can get to the connector of the buttons, I could give a signal to the cable via the GPIOs of the Raspi. With this the Raspi can control the treadmill.

Do I see that correctly that this would have to be built into the treadmill bridge? So as a new TreadmillClient?

If I look at the treadmill bridge from ProH4Ck it is written in C#. Is this used by ProH4Ck or where do I have to look. Don't quite get through the code yet.

december-soul avatar Dec 23 '21 12:12 december-soul

or is it easier to take a Raspi that simulates a FTMS client on one side and then only controls the buttons? That would probably be more flixibler

december-soul avatar Dec 23 '21 12:12 december-soul

hah got it now! It's a really nice job! Yes i think it will be very easy to do! We should create a new treadmill.cpp file replacing the bluetooth commands to change incline and speed with the gpio of the raspberry! Do you want to try to do it? Or you need a hand for the code? @december-soul

My idea so is to have:

  • gpiotreadmil.cpp with the ability to manipulate 4 pins
  • stryd connected as a power sensor (and also used for speed and cadence, already did in this way)

What do you think?

cagnulein avatar Dec 23 '21 13:12 cagnulein

I need to understand the structure of the whole project. I am still new here.

C++ would not be the problem, maybe I need some help to find the right point.

And I don't know yet when I will find time to implement the whole thing. Just busy with other projects

december-soul avatar Dec 23 '21 14:12 december-soul

ok, no problem, i can give you a hand! @december-soul just put all together, the raspi, the current master branch of QZ, the stryd to QZ and the connection with gpio. When you're able to do so, i will write you the code on a separate branch and then we will merge it to master. I have the idea clear for this.

Just remind a thing: Zwift doesn't control ANY treadmill. With a workaround it can control the inclination, but not the speed. So probably your "master", i mean the one who schedule the speed and incline, it could be a train program inside QZ.

cagnulein avatar Dec 23 '21 14:12 cagnulein

Hi everyone, I have a rebook GT50 without bluetooth and by Roberto's advice I installed a raspberry pi zero w2 connecting it to the various buttons of the treadmill. The Keys have been connected to the GPIO of the raspberry. Now Roberto will have to do the miracle by defining the gpio that I assigned to the keys in the program QZ! for Raspebby. Greetings Manuel tm1

hb9odk avatar Jan 05 '22 12:01 hb9odk

For the power supply I had to look up the voltage that powers the console. I installed a step-down converter to get 5V to drive the raspberry 2 .

hb9odk avatar Jan 05 '22 13:01 hb9odk

Here are the connections I used to connect the GPIOs to the treadmill. conessioni

hb9odk avatar Jan 05 '22 13:01 hb9odk

cool that is exactly what I have in mind right now. Just today my SD card for the Raspi arrived. Let's see if I can get everything set up today after the workout.

Have you already tested if the activation of the GPIOs is recognized as a keystroke? It depends on the voltage. I have not yet tested it and am just thinking about I have to make a transistor circuit.

december-soul avatar Jan 05 '22 13:01 december-soul

Yes I tested the GPIOs without voltage but only as a pass-through. Everything works.

hb9odk avatar Jan 05 '22 13:01 hb9odk

Cool guys! I will create a new branch tomorrow for this!

cagnulein avatar Jan 05 '22 13:01 cagnulein

i started the branch here https://github.com/cagnulein/qdomyos-zwift/tree/treadmill-gpio i hope to finish this tomorrow or anyway for the end of the week

cagnulein avatar Jan 05 '22 22:01 cagnulein

@hb9odk @december-soul how does the electronical stuff went? Were you be able to command it through the gpios? i will try to ultimate the branch today/tomorrow

cagnulein avatar Jan 10 '22 11:01 cagnulein

I have the Raspi ready set up and the branch built (as far as it goes, compile error).

With the GPIOs I have now decided to use relays to decouple the systems. comes with the control but on the same out with. Then I do not have to worry about electronics from the treadmill.

We need a few parameters

  • Time how long the GPIO has to be on for the treadmill to recognize it as a button press.
  • Time how long the GPIO must be switched off before it can be switched on the next time.
  • Speed step size, how much km/h will the treadmill speed up/slow down when controlling it (default 0,1 km/h)
  • Slope step size, how much percent the slope will be bigger/smaller when controlling it (default 1%)
  • initial speed (default 1km/h)
  • max speed (default MAX_INT)
  • initial slope (default 0%)
  • max slope (default MAX_INT)

for the GPIO handling on the RasPi i use libpigpio, for the GPIOs we must disgust. I use my old Rasberry Pi B+ which have other GPIOs then the PI Zero that hb9odk use.

  • inc speed GPIO (default ? 17)

  • dec speed GPIO (default ? 18)

  • inc slope GPIO (default ? 22)

  • dec slope GPIO (default ? 23)

  • nice to have: speed correction factor. My treadmill has a drift of 3% in the speed.

december-soul avatar Jan 10 '22 11:01 december-soul

thanks @december-soul i will try to continue the work tomorrow adding also these parameters (as const in the header files for now)

cagnulein avatar Jan 10 '22 14:01 cagnulein

No need to hurry. I'm busy for the next few days and if I keep going, I'll have my little C tool to work with for now.

december-soul avatar Jan 10 '22 14:01 december-soul

@december-soul @hb9odk finished the very first implementation. In the file gpiotreadmill..h you can find the GPIO pins and some settings (for now as const). I used the @hb9odk pins.

You need to have the wiringPi library and headers on the raspberry.

Also you need to start the qdomyos with -gpiotreadmill argument

Append a debug log if something will not work.

Let me know

cagnulein avatar Jan 11 '22 13:01 cagnulein

Nice, i have updated my parameter suggestions above. ^^

wiringPi was nice but I read that the developer threw down and removed the libs. No idea if this is still supported now. I have, in my test program, used libpigpio. But I will see if I get along with your implementation.

december-soul avatar Jan 11 '22 15:01 december-soul

@december-soul wiringpi works well and it's mantained by another group of people, but if you want you can change the calls of the wiringpi to libpigpio. the job is very easy to do. I mean, now that you have the template, you can try to develop it more by yourself, what do you think?

cagnulein avatar Jan 11 '22 15:01 cagnulein

i will try. But it will take a few days. Maybe @hb9odk can do something with the code too.

when briefly skimming the code, I wondered if #ifdef Q_OS_ANDROID and #ifdef Q_OS_IOS can be out, since both have no GPIOs.

december-soul avatar Jan 11 '22 15:01 december-soul

@december-soul yes when will finish it, we will have to add some ifdefs

cagnulein avatar Jan 11 '22 15:01 cagnulein

I have found some time to tinker. qdomyos-zwift starts with sudo ./qdomyos-zwift -gpiotreadmill

I can already control speed and inclination when I press the button. (at least the relays next to me click)

at the moment i run it under Ubuntu on my laptop, there the development is a bit easier. BT is switched on. (I forgot that the raspi has no BT and my USB-BT dongle comes tomorrow).

But where I'm at the moment, Zwift doesn't find anything. qdomyos-zwift is still a bit new for me, so sorry for the stupid questions: How is the treadmill announced via BT? What name does it get? Do I have to implement something in the gpiotreadmill.cpp?

I put this empty method in the gpiotreadmill.cpp. Do I have to fill it with life? Is this for the BT connection in Zwift direction or to the (not existing) treadmill direction? void gpiotreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {}

Do you have any other way to contact you than here via gitlab? Seems a bit too cumbersome for me for these general questions.

december-soul avatar Jan 12 '22 17:01 december-soul

In my branch I bring it up immediately the bridge to do Zwift In the name of the device is qdomyos As you can see in the file virtualtreadmill.cpp Show me a debug log, maybe there is something wrong with your Bluetooth dongle Please continue this here because it could be a reference for new users

Il giorno mer 12 gen 2022 alle 18:19 decembersoul @.***> ha scritto:

I have found some time to tinker. qdomyos-zwift starts with sudo ./qdomyos-zwift -gpiotreadmill

I can already control speed and inclination when I press the button. (at least the relays next to me click)

at the moment i run it under Ubuntu on my laptop, there the development is a bit easier. BT is switched on. (I forgot that the raspi has no BT and my USB-BT dongle comes tomorrow).

But where I'm at the moment, Zwift doesn't find anything. qdomyos-zwift is still a bit new for me, so sorry for the stupid questions: How is the treadmill announced via BT? What name does it get? Do I have to implement something in the gpiotreadmill.cpp?

I put this empty method in the gpiotreadmill.cpp. Do I have to fill it with life? Is this for the BT connection in Zwift direction or to the (not existing) treadmill direction? void gpiotreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {}

Do you have any other way to contact you than here via gitlab? Seems a bit too cumbersome for me for these general questions.

— Reply to this email directly, view it on GitHub https://github.com/cagnulein/qdomyos-zwift/issues/525#issuecomment-1011276836, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAALYWEDN6J3TLC7RJ6CNETUVWZZ5ANCNFSM5KTEOS4A . You are receiving this because you were assigned.Message ID: @.***>

-- Roberto Viola Software engineer and open source enthusiast http://robertoviola.cloud

cagnulein avatar Jan 12 '22 17:01 cagnulein

So, here is the logfile. My BT address of my computer is 8C:C6:81:2A:8D:DD

The one of my cell phone, which is currently running Zwift: D8:0B:9A:10:90:B9

I have also changed the speed and slope a few times.

Is it normal that it finds so many "found random device"?

debug-Do__Jan__13_16_16_23_2022.log

//EDIT i have removed the searchStopped from this line and now the virtualTreadMill will be initialized // ******************************************* virtual treadmill init ************************************* if (!firstInit && searchStopped && !virtualTreadMill && !virtualBike) {

now the logfile looks like this debug-Do__Jan__13_16_53_33_2022.log

//EDIT2 now i'm connected :-) connected.log

some more question, when I do a workout in Zwift and I should set the speed to 10 km/h for example, nothing arrives in the virtualtreadmill. How do I get this to work?

december-soul avatar Jan 13 '22 15:01 december-soul

thanks @december-soul i just commited it :) i found the same erro @december-soul zwift doesn't send the speed to ANY bluetooth devices. The incline instead, work, following my youtube video "qz auto inclination zwift"

cagnulein avatar Jan 13 '22 18:01 cagnulein

treadmill.log I have updated but with zwift I still do not see a match. I attach the log

hb9odk avatar Jan 13 '22 19:01 hb9odk

I don’t see the virtual bridge indeed. Maybe I did an error, I will check it tomorrow with a fresh mind. I’m too tired today Thanks

Il giorno gio 13 gen 2022 alle 20:12 hb9odk @.***> ha scritto:

treadmill.log https://github.com/cagnulein/qdomyos-zwift/files/7865351/treadmill.log I have updated but with zwift I still do not see a match. I attach the log

— Reply to this email directly, view it on GitHub https://github.com/cagnulein/qdomyos-zwift/issues/525#issuecomment-1012429920, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAALYWA5F2UZJYRTUTEBFKLUV4PZZANCNFSM5KTEOS4A . You are receiving this because you were assigned.Message ID: @.***>

-- Roberto Viola Software engineer and open source enthusiast http://robertoviola.cloud

cagnulein avatar Jan 13 '22 19:01 cagnulein

ok today we finally got the relays to go the way we wanted. We configured my treadmill specs as well. here is the code and photos

#include "gpiotreadmill.h" #include "ios/lockscreen.h" #include "keepawakehelper.h" #include "virtualtreadmill.h" #include <QBluetoothLocalDevice> #include <QDateTime> #include <QFile> #include <QMetaEnum> #include <QSettings> #include #include <wiringPi.h>

using namespace std::chrono_literals;

gpiotreadmill::gpiotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed, double forceInitInclination) { m_watt.setType(metric::METRIC_WATT); Speed.setType(metric::METRIC_SPEED); this->noConsole = noConsole; this->noHeartService = noHeartService;

if (wiringPiSetup() == -1) {
    qDebug() << QStringLiteral("wiringPiSetup ERROR!");
    exit(1);
}
pinMode(OUTPUT_INCLINE_DOWN, INPUT);
pinMode(OUTPUT_INCLINE_UP, INPUT);
pinMode(OUTPUT_SPEED_DOWN, INPUT);
pinMode(OUTPUT_SPEED_UP, INPUT);
pinMode(OUTPUT_START, INPUT);
pinMode(OUTPUT_STOP, INPUT);

if (forceInitSpeed > 0) { lastSpeed = forceInitSpeed; }

if (forceInitInclination > 0) {
    lastInclination = forceInitInclination;
}

refresh = new QTimer(this);
initDone = false;
connect(refresh, &QTimer::timeout, this, &gpiotreadmill::update);
refresh->start(pollDeviceTime);

}

void gpiotreadmill::changeInclinationRequested(double grade, double percentage) { if (percentage < 0) percentage = 0; changeInclination(grade, percentage); }

void gpiotreadmill::forceSpeed(double requestSpeed) { if (requestSpeed > Speed.value()) { pinMode(OUTPUT_SPEED_UP, OUTPUT); QThread::msleep(GPIO_KEEP_MS); pinMode(OUTPUT_SPEED_UP, INPUT); Speed = Speed.value() +0.1; } else { pinMode(OUTPUT_SPEED_DOWN, OUTPUT); QThread::msleep(GPIO_KEEP_MS); pinMode(OUTPUT_SPEED_DOWN, INPUT); Speed = Speed.value() -0.1; } }

void gpiotreadmill::forceIncline(double requestIncline) { if (requestIncline > Inclination.value()) { pinMode(OUTPUT_INCLINE_UP, OUTPUT); QThread::msleep(GPIO_KEEP_MS); pinMode(OUTPUT_INCLINE_UP, INPUT); Inclination = Inclination.value() +1; } else { pinMode(OUTPUT_INCLINE_DOWN, OUTPUT); QThread::msleep(GPIO_KEEP_MS); pinMode(OUTPUT_INCLINE_DOWN, INPUT); Inclination = Inclination.value() -1; }

}

void gpiotreadmill::update() {

QSettings settings;
// ******************************************* virtual treadmill init *************************************
if (!firstInit && !virtualTreadMill && !virtualBike) {
    bool virtual_device_enabled = settings.value("virtual_device_enabled", true).toBool();
    bool virtual_device_force_bike = settings.value("virtual_device_force_bike", false).toBool();
    if (virtual_device_enabled) {
        if (!virtual_device_force_bike) {
            debug("creating virtual treadmill interface...");
            virtualTreadMill = new virtualtreadmill(this, noHeartService);
            connect(virtualTreadMill, &virtualtreadmill::debug, this, &gpiotreadmill::debug);
            connect(virtualTreadMill, &virtualtreadmill::changeInclination, this,
                    &gpiotreadmill::changeInclinationRequested);
        } else {
            debug("creating virtual bike interface...");
            virtualBike = new virtualbike(this);
            connect(virtualBike, &virtualbike::changeInclination, this, &gpiotreadmill::changeInclinationRequested);
        }
        firstInit = 1;
    }
}
// ********************************************************************************************************

// debug("Domyos Treadmill RSSI " + QString::number(bluetoothDevice.rssi()));

double heart = 0;
QString heartRateBeltName =
    settings.value(QStringLiteral("heart_rate_belt_name"), QStringLiteral("Disabled")).toString();

#ifdef Q_OS_ANDROID if (settings.value("ant_heart", false).toBool()) Heart = (uint8_t)KeepAwakeHelper::heart(); else #endif { if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {

        if (heart == 0) {

#ifdef Q_OS_IOS #ifndef IO_UNDER_QT lockscreen h; long appleWatchHeartRate = h.heartRate(); h.setKcal(KCal.value()); h.setDistance(Distance.value()); Heart = appleWatchHeartRate; debug("Current Heart from Apple Watch: " + QString::number(appleWatchHeartRate)); #endif #endif } else

            Heart = heart;
    }
}

if (!firstCharacteristicChanged) {
    if (watts(settings.value(QStringLiteral("weight"), 75.0).toFloat()))
        KCal +=
            ((((0.048 * ((double)watts(settings.value(QStringLiteral("weight"), 75.0).toFloat())) + 1.19) *
               settings.value(QStringLiteral("weight"), 75.0).toFloat() * 3.5) /
              200.0) /
             (60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(
                            QDateTime::currentDateTime())))); //(( (0.048* Output in watts +1.19) * body weight in
                                                              // kg * 3.5) / 200 ) / 60
    Distance += ((Speed.value() / (double)3600.0) /
                 ((double)1000.0 / (double)(lastTimeCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
    lastTimeCharacteristicChanged = QDateTime::currentDateTime();
}

emit debug(QStringLiteral("Current speed: ") + QString::number(Speed.value()));
emit debug(QStringLiteral("Current incline: ") + QString::number(Inclination.value()));
emit debug(QStringLiteral("Current heart: ") + QString::number(Heart.value()));
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
emit debug(QStringLiteral("Current KCal from the machine: ") + QString::number(KCal.value()));
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
emit debug(QStringLiteral("Current Distance Calculated: ") + QString::number(Distance.value()));

firstCharacteristicChanged = false;

update_metrics(true, watts(settings.value(QStringLiteral("weight"), 75.0).toFloat()));

// updating the treadmill console every second
if (sec1Update++ >= (1000 / refresh->interval())) {
}

// byte 3 - 4 = elapsed time
// byte 17    = inclination
{
    if (requestSpeed != -1) {
        if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 16) {
            emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));

            forceSpeed(requestSpeed);
        }
        requestSpeed = -1;
    }
    if (requestInclination != -1) {
        // only 1 steps ara avaiable
        requestInclination = qRound(requestInclination * 2.0) / 2.0;
        if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
            requestInclination <= 12) {
            emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));

            forceIncline(requestInclination);
        }
        requestInclination = -1;
    }
    if (requestStart != -1) {
        emit debug(QStringLiteral("starting..."));
        if (lastSpeed == 0.0) {

            lastSpeed = 0.5;
        }
        pinMode(OUTPUT_START, OUTPUT);
        QThread::msleep(GPIO_KEEP_MS);
        pinMode(OUTPUT_START, INPUT);
        requestStart = -1;
        emit tapeStarted();
        Speed = 1;        
    }
    if (requestStop != -1) {
        emit debug(QStringLiteral("stopping..."));
        pinMode(OUTPUT_STOP, OUTPUT);
        QThread::msleep(GPIO_KEEP_MS);
        pinMode(OUTPUT_STOP, INPUT);
        requestStop = -1;
    }
    if (requestFanSpeed != -1) {
        emit debug(QStringLiteral("changing fan speed..."));

        requestFanSpeed = -1;
    }
    if (requestIncreaseFan != -1) {
        emit debug(QStringLiteral("increasing fan speed..."));

        requestIncreaseFan = -1;
    } else if (requestDecreaseFan != -1) {
        emit debug(QStringLiteral("decreasing fan speed..."));

        requestDecreaseFan = -1;
    }
}

}

bool gpiotreadmill::connected() { return true; }

void *gpiotreadmill::VirtualTreadMill() { return virtualTreadMill; }

void *gpiotreadmill::VirtualDevice() { return VirtualTreadMill(); }

void gpiotreadmill::searchingStop() { searchStopped = true; }

PXL_20220114_182547461

PXL_20220114_182557918

hb9odk avatar Jan 14 '22 18:01 hb9odk

here is my current test program. it runs quite well. Now and then two "keystrokes" are recognized as one. I must look again whether it goes better if I make the GPIO_KEEP_MS even slower.

In my test tool the GPIO pins are assigned differently. There we must agree again.

(rename file to treadmill_gpio_test.cpp) treadmill_gpio_test.txt

now I will see why sudo ./qdomyos-zwift -gpiotreadmill to connect to zwift. It has already worked twice. It looks to me like qz only sends an announcement once and then zwift misses it unfortunately. But I can not imagine, because it would be a general problem and virtually all would have the problem.

december-soul avatar Jan 15 '22 19:01 december-soul