Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nordictrack OCR build #2501

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 149 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1085,3 +1232,4 @@ jobs:
windows-binary-no-python/*
windows-binary/*
fdroid-android-trial/*
nordictrack-android-trial/*
5 changes: 4 additions & 1 deletion src/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ 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"
implementation "androidx.core:core-ktx:1.12.0"
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 {
Expand Down
22 changes: 17 additions & 5 deletions src/devices/bluetooth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -2571,18 +2583,18 @@ 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<void>(
"org/cagnulen/qdomyoszwift/NotificationClient", "notify", "(Landroid/content/Context;Ljava/lang/String;)V",
QtAndroid::androidContext().object(), javaNotification.object<jstring>());
}
#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;",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 == ',') {
Expand Down Expand Up @@ -507,6 +597,29 @@ void nordictrackifitadbtreadmill::update() {
// writeCharacteristic(initDataF0C800B8, sizeof(initDataF0C800B8), "stop tape");
requestStop = -1;
}

#ifdef Q_OS_ANDROID
{
QAndroidJniObject text = QAndroidJniObject::callStaticObjectMethod<jstring>(
"org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastText");
QString t = text.toString();
QAndroidJniObject textExtended = QAndroidJniObject::callStaticObjectMethod<jstring>(
"org/cagnulen/qdomyoszwift/ScreenCaptureService", "getLastTextExtended");
QString tt = textExtended.toString();
// 2272 1027
jint w = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/ScreenCaptureService",
"getImageWidth", "()I");
jint h = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/ScreenCaptureService",
"getImageHeight", "()I");
QString tExtended = textExtended.toString();
QAndroidJniObject packageNameJava = QAndroidJniObject::callStaticObjectMethod<jstring>(
"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) {
Expand Down
Loading
Loading