diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 15d48c6a4..4f8ef7f57 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -649,6 +649,153 @@ jobs: # asset_name: fdroid-android-trial.zip # asset_content_type: application/zip + android-nordictrack-build: + # The type of runner that the job will run on + runs-on: ubuntu-20.04 + + env: + NORDICTRACK: "1" + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # - name: Cache Qt Linux Desktop + # id: cache-qt-linux-desktop + # uses: actions/cache@v1 + # with: + # path: '${{ github.workspace }}/output/linux-desktop/' + # key: ${{ runner.os }}-QtCache-Linux-Desktop + + # - name: Cache Qt Linux Android + # id: cache-qt-android + # uses: actions/cache@v1 + # with: + # path: '${{ github.workspace }}/output/android/' + # key: ${{ runner.os }}-QtCache-Android + + - name: Xvfb install and run + run: | + sudo apt-get install -y xvfb + Xvfb -ac ${{ env.DISPLAY }} -screen 0 1280x780x24 & + + - name: Checkout repository + uses: actions/checkout@v2 + with: + # This token is provided by Actions, you do not need to create your own token + token: ${{ secrets.GITHUB_TOKEN }} + submodules: recursive # or 'true' if you want to check out only immediate submodules + + - name: Install packages required to run QZ inside workflow + run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev + + # - name: Test Peloton API + # if: github.event_name == 'push' || github.event_name == 'schedule' + # run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-peloton -peloton-username ${{ secrets.peloton_username }} -peloton-password ${{ secrets.peloton_password }} + # timeout-minutes: 2 + + # - name: Test Home Fitness Buddy API + # run: cd /home/runner/work/qdomyos-zwift/qdomyos-zwift/src/; ./qdomyos-zwift -test-hfb + # timeout-minutes: 2 + + # - uses: actions/checkout@v2 + # with: + # repository: nttld/setup-ndk + # path: setup-ndk + # The packages.json in nttld/setup-ndk has already been updated, + # https://github.com/nttld/setup-ndk/commit/831db5b02a0f0cab80614619efe461a3dcc140e6 + # but `dist/*` has not been rebuilt yet. Build it. + # https://github.com/nttld/setup-ndk/tree/main/dist + # - name: Locally rebuilt setup-ndk + # run: | + # npm -prefix ./setup-ndk install + # npm -prefix ./setup-ndk run all + # Install using locally rebuilt setup-ndk + # - name: Setup Android NDK r21d + # uses: ./setup-ndk + #- uses: nttld/setup-ndk@v1 + # with: + # ndk-version: r21d + + # waiting github.com/jurplel/install-qt-action/issues/63 + - name: Install Qt Android + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.0' + host: 'linux' + target: 'android' + arch: 'android' + modules: 'qtcharts qtnetworkauth' + dir: '${{ github.workspace }}/output/android/' + cache: 'true' + cache-key-prefix: 'install-qt-action-android' + + - name: Install Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '11' + + - name: patching qt for bluetooth + run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/ + + - name: download 3rd party files for qthttpserver + run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/ + + - name: Set Android NDK 21 && build + run: | + # Install NDK 21 after GitHub update + # https://github.com/actions/virtual-environments/issues/5595 + ANDROID_ROOT="/usr/local/lib/android" + ANDROID_SDK_ROOT="${ANDROID_ROOT}/sdk" + SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" + echo "y" | $SDKMANAGER "ndk;21.4.7075529" + export ANDROID_NDK="${ANDROID_SDK_ROOT}/ndk-bundle" + export ANDROID_NDK_ROOT="${ANDROID_NDK}" + cd src + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js + echo "#define LICENSE" >> secret.h + cd .. + + ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK + rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393 + + # QTHTTPSERVER must use the same NDK + cd src/qthttpserver + qmake + make -j8 + make install + cd ../.. + + qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.4.7075529' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install + sed -i '1s|{|{\n "android-extra-libs": "${{ github.workspace }}/android_openssl/no-asm/latest/arm/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/arm64/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86/libssl_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libcrypto_1_1.so,${{ github.workspace }}/android_openssl/no-asm/latest/x86_64/libssl_1_1.so",|' src/android-qdomyos-zwift-deployment-settings.json + cat src/android-qdomyos-zwift-deployment-settings.json + + - name: Build APK (not usable for production due to unpatched QT library) + run: cd src; androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-31 --gradle --aab + + - name: Archive apk binary + uses: actions/upload-artifact@v2 + with: + name: nordictrack-android-trial + path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/ + + # - name: Exit if not on master branch + # if: github.ref == 'refs/heads/master' + # run: exit 1 + + # - name: upload windows artifact + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ github.token }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: ${{ github.workspace }}/output/android/build/outputs/apk/debug/android-debug.apk + # asset_name: fdroid-android-trial.zip + # asset_content_type: application/zip + ios-build: # The type of runner that the job will run on runs-on: macos-12 @@ -1056,7 +1203,7 @@ jobs: permissions: write-all runs-on: ubuntu-20.04 if: github.event_name == 'schedule' - needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build] # Specify the job dependencies + needs: [linux-x86-build, window-msvc2019-build, ios-build, window-build, android-build, android-nordictrack-build] # Specify the job dependencies steps: - name: Download artifacts uses: actions/download-artifact@v3 @@ -1085,3 +1232,4 @@ jobs: windows-binary-no-python/* windows-binary/* fdroid-android-trial/* + nordictrack-android-trial/* diff --git a/src/android/build.gradle b/src/android/build.gradle index d8ddbe197..4ae032794 100644 --- a/src/android/build.gradle +++ b/src/android/build.gradle @@ -24,7 +24,10 @@ apply plugin: 'com.android.application' apply plugin: 'com.google.protobuf' def amazon = System.getenv('AMAZON') +def nordictrack = System.getenv('NORDICTRACK') println(amazon) +println(nordictrack) + dependencies { implementation "androidx.core:core:1.12.0" @@ -32,7 +35,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0" implementation 'com.google.protobuf:protobuf-javalite:3.25.1' - if(amazon == "1") { + if(amazon == "1" || nordictrack == "1") { // amazon app store implementation 'com.google.mlkit:text-recognition:16.0.0-beta6' } else { diff --git a/src/devices/bluetooth.cpp b/src/devices/bluetooth.cpp index 512faad44..ec4738df9 100644 --- a/src/devices/bluetooth.cpp +++ b/src/devices/bluetooth.cpp @@ -34,8 +34,7 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc this->useDiscovery = startDiscovery; - QString nordictrack_2950_ip = - settings.value(QZSettings::nordictrack_2950_ip, QZSettings::default_nordictrack_2950_ip).toString(); + const bool nordictrack = true; // to replace if (settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() && !pelotonBike) { pelotonBike = new pelotonbike(noWriteResistance, noHeartService); @@ -47,6 +46,18 @@ bluetooth::bluetooth(bool logs, const QString &deviceName, bool noWriteResistanc } // this signal is not associated to anything in this moment, since the homeform is not loaded yet this->signalBluetoothDeviceConnected(pelotonBike); + } else if(nordictrack) { + nordictrackifitadbTreadmill = new nordictrackifitadbtreadmill(noWriteResistance, noHeartService); + emit deviceConnected(QBluetoothDeviceInfo()); + connect(nordictrackifitadbTreadmill, &bluetoothdevice::connectedAndDiscovered, this, + &bluetooth::connectedAndDiscovered); + connect(nordictrackifitadbTreadmill, &nordictrackifitadbtreadmill::debug, this, &bluetooth::debug); + if (this->discoveryAgent && !this->discoveryAgent->isActive()) { + emit searchingStop(); + } + // this signal is not associated to anything in this moment, since the homeform is not loaded yet + this->signalBluetoothDeviceConnected(nordictrackifitadbTreadmill); + return; } #ifdef TEST @@ -2558,6 +2569,7 @@ void bluetooth::connectedAndDiscovered() { } } #ifdef Q_OS_ANDROID + const bool nordictrack = true; // to replace if (settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool() || settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) { QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", @@ -2571,7 +2583,7 @@ void bluetooth::connectedAndDiscovered() { device()->deviceType() == bluetoothdevice::ELLIPTICAL); } - if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool()) { + if (settings.value(QZSettings::android_notification, QZSettings::default_android_notification).toBool() || nordictrack) { QAndroidJniObject javaNotification = QAndroidJniObject::fromString("QZ is running!"); QAndroidJniObject::callStaticMethod( "org/cagnulen/qdomyoszwift/NotificationClient", "notify", "(Landroid/content/Context;Ljava/lang/String;)V", @@ -2579,10 +2591,10 @@ void bluetooth::connectedAndDiscovered() { } #endif -#ifdef Q_OS_ANDROID +#ifdef Q_OS_ANDROID if (settings.value(QZSettings::peloton_workout_ocr, QZSettings::default_peloton_workout_ocr).toBool() || settings.value(QZSettings::peloton_bike_ocr, QZSettings::default_peloton_bike_ocr).toBool() || - settings.value(QZSettings::zwift_ocr, QZSettings::default_zwift_ocr).toBool()) { + settings.value(QZSettings::zwift_ocr, QZSettings::default_zwift_ocr).toBool() || nordictrack) { AndroidActivityResultReceiver *a = new AndroidActivityResultReceiver(); QAndroidJniObject MediaProjectionManager = QtAndroid::androidActivity().callObjectMethod( "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", diff --git a/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.cpp b/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.cpp index d258453c7..fb3eb3b95 100644 --- a/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.cpp +++ b/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.cpp @@ -107,6 +107,96 @@ void nordictrackifitadbtreadmillLogcatAdbThread::runAdbTailCommand(QString comma #endif } +nordictrackifitadbtreadmill::DisplayValue nordictrackifitadbtreadmill::extractValue(const QString& ocrText, int imageWidth, bool isLeftSide) { + QStringList lines = ocrText.split("ยงยง"); + QRegularExpression rectRegex("Rect\\((\\d+), (\\d+) - (\\d+), (\\d+)\\)"); + QRegularExpression numericRegex("^-?\\d+(\\.\\d+)?$"); + + DisplayValue result; + int minX = isLeftSide ? 0 : imageWidth - 200; + int maxX = isLeftSide ? 200 : imageWidth; + QStringList targetLabels = isLeftSide ? QStringList{"INCLINE"} : QStringList{"SPEED", "RESISTANCE", "MPH", "KPH"}; + + QRect labelRect; + int closestDistance = INT_MAX; + + // First pass: find the label + for (const QString& line : lines) { + QStringList parts = line.split("$$"); + if (parts.size() == 2) { + QString value = parts[0]; + QRegularExpressionMatch match = rectRegex.match(parts[1]); + + if (match.hasMatch()) { + int x1 = match.captured(1).toInt(); + int x2 = match.captured(3).toInt(); + + if (x1 >= minX && x2 <= maxX) { + for (const QString& targetLabel : targetLabels) { + if (value.contains(targetLabel, Qt::CaseInsensitive)) { + labelRect = QRect(x1, match.captured(2).toInt(), + x2 - x1, match.captured(4).toInt() - match.captured(2).toInt()); + result.label = value; + break; + } + } + if (!result.label.isEmpty()) break; + } + } + } + } + + // Second pass: find the closest numeric value to the label + if (!labelRect.isNull()) { + for (const QString& line : lines) { + QStringList parts = line.split("$$"); + if (parts.size() == 2) { + QString value = parts[0]; + QRegularExpressionMatch match = rectRegex.match(parts[1]); + + if (match.hasMatch() && numericRegex.match(value).hasMatch()) { + int x1 = match.captured(1).toInt(); + int y1 = match.captured(2).toInt(); + int x2 = match.captured(3).toInt(); + int y2 = match.captured(4).toInt(); + + QRect valueRect(x1, y1, x2 - x1, y2 - y1); + + if (x1 >= minX && x2 <= maxX) { + int distance = qAbs(valueRect.center().y() - labelRect.center().y()); + if (distance < closestDistance) { + closestDistance = distance; + result.value = value; + result.rect = valueRect; + } + } + } + } + } + } + + return result; +} + +void nordictrackifitadbtreadmill::processOCROutput(const QString& ocrText, int imageWidth) { + DisplayValue leftValue = extractValue(ocrText, imageWidth, true); + DisplayValue rightValue = extractValue(ocrText, imageWidth, false); + + if (!leftValue.value.isEmpty()) { + qDebug() << "Left value (" << leftValue.label << "):" << leftValue.value; + Inclination = leftValue.label.toDouble(); + } else { + qDebug() << "Left value not found"; + } + + if (!rightValue.value.isEmpty()) { + qDebug() << "Right value (" << rightValue.label << "):" << rightValue.value; + Speed = rightValue.label.toDouble(); + } else { + qDebug() << "Right value not found"; + } +} + double nordictrackifitadbtreadmill::getDouble(QString v) { QChar d = QLocale().decimalPoint(); if (d == ',') { @@ -507,6 +597,29 @@ void nordictrackifitadbtreadmill::update() { // writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape"); requestStop = -1; } + +#ifdef Q_OS_ANDROID + { + QAndroidJniObject text = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastText"); + QString t = text.toString(); + QAndroidJniObject textExtended = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastTextExtended"); + QString tt = textExtended.toString(); + // 2272 1027 + jint w = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/ScreenCaptureService", + "getImageWidth", "()I"); + jint h = QAndroidJniObject::callStaticMethod("org/cagnulen/qdomyoszwift/ScreenCaptureService", + "getImageHeight", "()I"); + QString tExtended = textExtended.toString(); + QAndroidJniObject packageNameJava = QAndroidJniObject::callStaticObjectMethod( + "org/cagnulen/qdomyoszwift/MediaProjection", "getPackageName"); + QString packageName = packageNameJava.toString(); + + qDebug() << QStringLiteral("OCR") << packageName << tt; + processOCROutput(tt, w); + } +#endif } void nordictrackifitadbtreadmill::changeInclinationRequested(double grade, double percentage) { diff --git a/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h b/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h index 22e3ec195..85aa34f01 100644 --- a/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h +++ b/src/devices/nordictrackifitadbtreadmill/nordictrackifitadbtreadmill.h @@ -18,7 +18,8 @@ #include #include #include - +#include +#include #include "treadmill.h" #ifdef Q_OS_IOS @@ -64,6 +65,12 @@ class nordictrackifitadbtreadmill : public treadmill { double minStepSpeed() override { return 0.1; } private: + struct DisplayValue { + QString value; + QString label; + QRect rect; + }; + void forceIncline(double incline); void forceSpeed(double speed); double getDouble(QString v); @@ -91,6 +98,9 @@ class nordictrackifitadbtreadmill : public treadmill { nordictrackifitadbtreadmillLogcatAdbThread *logcatAdbThread = nullptr; #endif + DisplayValue extractValue(const QString& ocrText, int imageWidth, bool isLeftSide); + void processOCROutput(const QString& ocrText, int imageWidth); + int x14i_inclination_lookuptable(double reqInclination); #ifdef Q_OS_IOS