Compare commits

..

19 Commits

Author SHA1 Message Date
Roberto Viola
53a41b35e2 build fix 2022-12-16 11:57:51 +01:00
Roberto Viola
1f178ff3fa using lastrunstate in order to set speed and inclination 2022-12-16 11:44:09 +01:00
Sunguk Lee
fd3cc25732 Initial send device state signal of kingsmith r2 to homeform 2022-12-16 11:12:02 +01:00
Sunguk Lee
64e9052f25 Initial implement start/stop control of KingSmith R2 when press start/pause/stop buttons
This patch is not perfect.
If a user doesn't do workout, treadmill will stop automatically,
but an UI of the application couldn't update anything.

Also in the timing issue, sometimes changeing `ControlMode` or `runState` command
just return `Error` packet.
I think, sending message too early (before finishing handshake) could
make the problem.

Fully implement needs passing the signal to homeform or MainApp.
And also it needs more complex state machine for handling buttons.
(eg; press the start button & press the pause button before
finish start treadmill action, pause action never call)
2022-12-16 11:11:40 +01:00
Roberto Viola
e8a59937f6 SmartSpin2K as smart trainer #1080 2022-12-16 08:44:31 +01:00
Roberto Viola
20c69a374c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2022-12-14 14:45:47 +01:00
Roberto Viola
3eb3dbecc8 power calibration for Domyos Bike[REQ] #1078 2022-12-14 14:45:39 +01:00
Roberto Viola
f8a6e428ee version 2.12.13 on android 2022-12-14 14:38:03 +01:00
Roberto Viola
196bfe351d Android floating window (#1077)
* first implementation of the webview. didn't compile yet

* it's building

* window appears but is not showing the webview and also doesn't move. permission window needs to be fixed

* floating window works!

* adding html page for floating

* adding floating.htm to the floating window

* creating tiles...

* table

* dinamicize metrics on the floating window

* adding transparency

* finalizing

* alpha channel fixed

* fixing 1 minute timeout
2022-12-14 14:25:26 +01:00
Roberto Viola
5169cf78a4 ifit kcal fixed 2022-12-14 08:23:49 +01:00
Roberto Viola
11425ea861 Bowflex T6 treadmill #1064 2022-12-13 18:46:16 +01:00
Roberto Viola
d4a5604af6 Add option to disable pausing/stopping of machinery when pause/stop is pressed in QZ #1075 2022-12-13 18:44:43 +01:00
Roberto Viola
b53ff4a516 notification icon on android restored to false as default 2022-12-12 09:37:18 +01:00
Roberto Viola
1adf0b2e08 Gears tile for elliptical #1071 2022-12-12 08:46:49 +01:00
Roberto Viola
883c4c56d6 Bowflex T6 treadmill #1064 2022-12-12 07:59:43 +01:00
Roberto Viola
13a2c95671 Add option to disable pausing/stopping of machinery when pause/stop is pressed in QZ #1075 2022-12-11 20:04:27 +01:00
Roberto Viola
48df8b3057 Add option to disable pausing/stopping of machinery when pause/stop is pressed in QZ (Issue #1075) 2022-12-11 19:53:57 +01:00
Roberto Viola
e771b4a62d Add option to disable pausing/stopping of machinery when pause/stop is pressed in QZ (Issue #1075) 2022-12-11 19:44:19 +01:00
Roberto Viola
c7bed0a259 Add option to disable pausing/stopping of machinery when pause/stop is pressed in QZ #1075 2022-12-11 18:50:11 +01:00
31 changed files with 871 additions and 166 deletions

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.12.10" android:versionCode="440" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.12.13" android:versionCode="443" android:installLocation="auto">
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS -->
@@ -10,7 +10,7 @@
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
<activity android:exported="true" android:resizeableActivity="true" android:supportsPictureInPicture="true" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="qdomyos-zwift" android:launchMode="singleTask">
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="qdomyos-zwift" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -67,13 +67,14 @@
* none - useful for apps that don't use any of the above Qt modules
-->
<meta-data android:name="android.app.extract_android_style" android:value="default"/>
<!-- extract android style -->
<!-- extract android style -->
</activity>
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"></service>
<service android:name=".ChannelService"></service>
<service android:name=".FloatingWindowGFG" android:enabled="true" android:exported="true"/>
<activity android:name="org.cagnulen.qdomyoszwift.MyActivity" />
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
@@ -87,7 +88,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
</manifest>

View File

@@ -17,9 +17,14 @@ repositories {
apply plugin: 'com.android.application'
dependencies {
def appcompat_version = "1.1.0"
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "com.android.billingclient:billing:4.0.0"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
androidTestImplementation "com.android.support:support-annotations:28.0.0"
}
@@ -41,6 +46,11 @@ android {
buildToolsVersion '29.0.2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'

View File

@@ -0,0 +1,2 @@
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CC000000">
<WebView android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonMaximize"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,65 @@
package org.cagnulen.qdomyoszwift;
import android.app.ActivityManager;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class FloatingHandler {
static Context _context;
static public int _port = 0;
public static void show(Context context, int port) {
_context = context;
_port = port;
// First it confirms whether the
// 'Display over other apps' permission in given
if (checkOverlayDisplayPermission()) {
// FloatingWindowGFG service is started
context.startService(new Intent(context, FloatingWindowGFG.class));
// The MainActivity closes here
//finish();
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
// This method will start the intent. It takes two parameter, one is the Intent and the other is
// an requestCode Integer. Here it is -1.
Activity a = (Activity)_context;
a.startActivityForResult(intent, -1);
}
}
private static boolean checkOverlayDisplayPermission() {
// Android Version is lesser than Marshmallow
// or the API is lesser than 23
// doesn't need 'Display over other apps' permission enabling.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// If 'Display over other apps' is not enabled it
// will return false or else true
if (!Settings.canDrawOverlays(_context)) {
return false;
} else {
return true;
}
} else {
return true;
}
}
}

View File

@@ -0,0 +1,188 @@
// https://www.geeksforgeeks.org/how-to-make-a-floating-window-application-in-android/
package org.cagnulen.qdomyoszwift;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.WebViewClient;
import android.util.Log;
import androidx.annotation.Nullable;
public class FloatingWindowGFG extends Service {
// The reference variables for the
// ViewGroup, WindowManager.LayoutParams,
// WindowManager, Button, EditText classes are created
private ViewGroup floatView;
private int LAYOUT_TYPE;
private WindowManager.LayoutParams floatWindowLayoutParam;
private WindowManager windowManager;
private Button maximizeBtn;
// As FloatingWindowGFG inherits Service class,
// it actually overrides the onBind method
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
// The screen height and width are calculated, cause
// the height and width of the floating window is set depending on this
DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
// To obtain a WindowManager of a different Display,
// we need a Context for that display, so WINDOW_SERVICE is used
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// A LayoutInflater instance is created to retrieve the
// LayoutInflater for the floating_layout xml
LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);
// inflate a new view hierarchy from the floating_layout xml
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
WebView wv = (WebView)floatView.findViewById(R.id.webview);
wv.setWebViewClient(new WebViewClient(){
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
WebSettings settings = wv.getSettings();
settings.setJavaScriptEnabled(true);
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/floating.htm");
wv.clearView();
wv.measure(100, 100);
wv.setAlpha(0.6f);
settings.setBuiltInZoomControls(true);
settings.setUseWideViewPort(true);
settings.setDomStorageEnabled(true);
Log.d("QZ","loadurl");
// WindowManager.LayoutParams takes a lot of parameters to set the
// the parameters of the layout. One of them is Layout_type.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// If API Level is more than 26, we need TYPE_APPLICATION_OVERLAY
LAYOUT_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
// If API Level is lesser than 26, then we can
// use TYPE_SYSTEM_ERROR,
// TYPE_SYSTEM_OVERLAY, TYPE_PHONE, TYPE_PRIORITY_PHONE.
// But these are all
// deprecated in API 26 and later. Here TYPE_TOAST works best.
LAYOUT_TYPE = WindowManager.LayoutParams.TYPE_TOAST;
}
// Now the Parameter of the floating-window layout is set.
// 1) The Width of the window will be 55% of the phone width.
// 2) The Height of the window will be 58% of the phone height.
// 3) Layout_Type is already set.
// 4) Next Parameter is Window_Flag. Here FLAG_NOT_FOCUSABLE is used. But
// problem with this flag is key inputs can't be given to the EditText.
// This problem is solved later.
// 5) Next parameter is Layout_Format. System chooses a format that supports
// translucency by PixelFormat.TRANSLUCENT
floatWindowLayoutParam = new WindowManager.LayoutParams(
(int) (width * (0.30f)),
(int) (height * (0.22f)),
LAYOUT_TYPE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
// The Gravity of the Floating Window is set.
// The Window will appear in the center of the screen
floatWindowLayoutParam.gravity = Gravity.CENTER;
// X and Y value of the window is set
floatWindowLayoutParam.x = 0;
floatWindowLayoutParam.y = 0;
// The ViewGroup that inflates the floating_layout.xml is
// added to the WindowManager with all the parameters
windowManager.addView(floatView, floatWindowLayoutParam);
// Another feature of the floating window is, the window is movable.
// The window can be moved at any position on the screen.
floatView.setOnTouchListener(new View.OnTouchListener() {
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
double x;
double y;
double px;
double py;
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("QZ","onTouch");
switch (event.getAction()) {
// When the window will be touched,
// the x and y position of that position
// will be retrieved
case MotionEvent.ACTION_DOWN:
x = floatWindowLayoutUpdateParam.x;
y = floatWindowLayoutUpdateParam.y;
// returns the original raw X
// coordinate of this event
px = event.getRawX();
// returns the original raw Y
// coordinate of this event
py = event.getRawY();
break;
// When the window will be dragged around,
// it will update the x, y of the Window Layout Parameter
case MotionEvent.ACTION_MOVE:
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
// updated parameter is applied to the WindowManager
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
break;
}
return false;
}
});
}
// It is called when stopService()
// method is called in MainActivity
@Override
public void onDestroy() {
super.onDestroy();
stopSelf();
// Window is removed from the screen
windowManager.removeView(floatView);
}
}

View File

@@ -8,8 +8,9 @@ import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
public class ForegroundService extends Service {
public static final String CHANNEL_ID = "ForegroundServiceChannel";
@Override

View File

@@ -1,54 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import android.content.Intent;
import android.app.PictureInPictureParams;
import android.util.Rational;
import android.provider.Settings;
import android.view.Display;
import android.graphics.Point;
public class PiP {
public static void enterPiP(Activity a) {
Display d = a.getWindowManager()
.getDefaultDisplay();
Point p = new Point();
d.getSize(p);
int width = p.x;
int height = p.y;
Rational ratio
= new Rational(width, height);
PictureInPictureParams.Builder
pip_Builder
= new PictureInPictureParams
.Builder();
pip_Builder.setAspectRatio(ratio).build();
Log.v("QZ", "Pip");
a.enterPictureInPictureMode(pip_Builder.build());
}
}

View File

@@ -151,6 +151,10 @@ void bluetooth::finished() {
bool eliteSterzoSmartFound = eliteSterzoSmartName.startsWith(QStringLiteral("Disabled"));
bool heartRateBeltFound = heartRateBeltName.startsWith(QStringLiteral("Disabled"));
bool ftmsAccessoryFound = ftmsAccessoryName.startsWith(QStringLiteral("Disabled"));
bool ss2k_peloton = settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool();
if (ss2k_peloton)
ftmsAccessoryFound = true;
// since i can have multiple fanfit i can't wait more because i don't have the full list of the fanfit
// devices connected to QZ. edit: let's wait at the last one item
@@ -237,6 +241,9 @@ bool bluetooth::ftmsAccessoryAvaiable() {
QSettings settings;
QString ftmsAccessoryName =
settings.value(QZSettings::ftms_accessory_name, QZSettings::default_ftms_accessory_name).toString();
bool ss2k_peloton = settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool();
if (ss2k_peloton)
return false;
Q_FOREACH (QBluetoothDeviceInfo b, devices) {
if (!ftmsAccessoryName.compare(b.name())) {
@@ -1184,10 +1191,14 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
(b.name().toUpper().startsWith("FLXCY-")) || // Pro FlexBike
(b.name().toUpper().startsWith("WAHOO KICKR")) || (b.name().toUpper().startsWith("B94")) ||
(b.name().toUpper().startsWith("STAGES BIKE")) || (b.name().toUpper().startsWith("SUITO")) ||
(b.name().toUpper().startsWith("D2RIDE")) || (b.name().toUpper().startsWith("DIRETO XR")) ||
(b.name().toUpper().startsWith("SMB1")) || (b.name().toUpper().startsWith("INRIDE"))) &&
(b.name().toUpper().startsWith(ftmsAccessoryName.toUpper()) &&
settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton)
.toBool()) || // ss2k on a peloton bike
(b.name().toUpper().startsWith("WAHOO KICKR")) ||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
(b.name().toUpper().startsWith("DIRETO XR")) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("INRIDE"))) &&
!ftmsBike && !snodeBike && !fitPlusBike && !stagesBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1964,7 +1975,8 @@ void bluetooth::connectedAndDiscovered() {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().startsWith(ftmsAccessoryName))) && !ftmsAccessory &&
!ftmsAccessoryName.startsWith(QStringLiteral("Disabled"))) {
!ftmsAccessoryName.startsWith(QStringLiteral("Disabled")) &&
!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool()) {
settings.setValue(QZSettings::ftms_accessory_lastdevice_name, b.name());
#ifndef Q_OS_IOS

View File

@@ -254,14 +254,14 @@ void bowflext216treadmill::characteristicChanged(const QLowEnergyCharacteristic
}
double bowflext216treadmill::GetSpeedFromPacket(const QByteArray &packet) {
if(bowflex_t6 == false) {
if (bowflex_t6 == false) {
uint16_t convertedData = (packet.at(7) << 8) | packet.at(6);
double data = (double)convertedData / 100.0f;
return data * 1.60934;
} else {
uint16_t convertedData = (packet.at(13) << 8) | packet.at(12);
uint16_t convertedData = (uint16_t)((uint8_t)packet.at(12)) + ((uint16_t)((uint8_t)packet.at(13)) << 8);
double data = (double)convertedData / 100.0f;
return data;
return data / 1.60934;
}
}

View File

@@ -663,7 +663,30 @@ resistance_t domyosbike::resistanceFromPowerRequest(uint16_t power) {
}
uint16_t domyosbike::wattsFromResistance(double resistance) {
return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value()))));
QSettings settings;
if(!settings.value(QZSettings::domyos_bike_500_profile_v1, QZSettings::default_domyos_bike_500_profile_v1).toBool() || resistance < 8)
return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value()))));
else {
switch((int)resistance) {
case 8:
return (13.6 * Cadence.value()) / 9.5488;
case 9:
return (15.3 * Cadence.value()) / 9.5488;
case 10:
return (17.3 * Cadence.value()) / 9.5488;
case 11:
return (19.8 * Cadence.value()) / 9.5488;
case 12:
return (22.5 * Cadence.value()) / 9.5488;
case 13:
return (25.6 * Cadence.value()) / 9.5488;
case 14:
return (28.4 * Cadence.value()) / 9.5488;
case 15:
return (35.9 * Cadence.value()) / 9.5488;
}
return ((10.39 + 1.45 * (resistance - 1.0)) * (exp(0.028 * (currentCadence().value()))));
}
}
uint16_t domyosbike::watts() {

View File

@@ -10,7 +10,7 @@ void elliptical::update_metrics(bool watt_calc, const double watts) {
double deltaTime = (((double)_lastTimeUpdate.msecsTo(current)) / ((double)1000.0));
QSettings settings;
if (!_firstUpdate && !paused) {
if (currentSpeed().value() > 0.0 || settings.value(QZSettings::continuous_moving, true).toBool()) {
if (currentSpeed().value() > 0.0 || settings.value(QZSettings::continuous_moving, true).toBool()) {
elapsed += deltaTime;
}
if (currentSpeed().value() > 0.0) {
@@ -57,8 +57,17 @@ uint16_t elliptical::watts() {
return m_watt.value();
}
void elliptical::changeResistance(resistance_t resistance) {
requestResistance = resistance;
RequestedResistance = resistance;
lastRawRequestedResistanceValue = resistance;
requestResistance = resistance + gears();
RequestedResistance = resistance + gears();
}
int8_t elliptical::gears() { return m_gears; }
void elliptical::setGears(int8_t gears) {
qDebug() << "setGears" << gears;
m_gears = gears;
if (lastRawRequestedResistanceValue != -1) {
changeResistance(lastRawRequestedResistanceValue);
}
}
void elliptical::changeInclination(double grade, double inclination) {
Q_UNUSED(grade);

View File

@@ -28,6 +28,8 @@ class elliptical : public bluetoothdevice {
void setPaused(bool p);
void setLap();
uint16_t watts();
void setGears(int8_t d);
int8_t gears();
public Q_SLOTS:
virtual void changeSpeed(double speed);
@@ -51,6 +53,8 @@ class elliptical : public bluetoothdevice {
volatile double requestSpeed = -1;
double requestInclination = -100;
double CrankRevs = 0;
int8_t m_gears = 0;
resistance_t lastRawRequestedResistanceValue = -1;
};
#endif // ELLIPTICAL_H

View File

@@ -447,6 +447,7 @@ homeform::homeform(QQmlApplicationEngine *engine, bluetooth *bl) {
QObject::connect(stack, SIGNAL(volumeDown()), this, SLOT(volumeDown()));
QObject::connect(stack, SIGNAL(keyMediaPrevious()), this, SLOT(keyMediaPrevious()));
QObject::connect(stack, SIGNAL(keyMediaNext()), this, SLOT(keyMediaNext()));
QObject::connect(stack, SIGNAL(floatingOpen()), this, SLOT(floatingOpen()));
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QObject::connect(engine, &QQmlApplicationEngine::quit, &QGuiApplication::quit);
@@ -546,6 +547,14 @@ void homeform::volumeDown() {
Minus(QStringLiteral("gears"));
}
void homeform::floatingOpen() {
#ifdef Q_OS_ANDROID
QSettings settings;
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/FloatingHandler", "show",
"(Landroid/content/Context;I)V", QtAndroid::androidContext().object(), settings.value("template_inner_QZWS_port", 6666).toInt());
#endif
}
void homeform::peloton_abort_workout() {
qDebug() << QStringLiteral("peloton_abort_workout!");
pelotonAbortedName = pelotonAskedName;
@@ -1967,6 +1976,11 @@ void homeform::sortTiles() {
preset_resistance_5->setGridId(i);
dataList.append(preset_resistance_5);
}
if (settings.value(QZSettings::tile_gears_enabled, false).toBool() &&
settings.value(QZSettings::tile_gears_order, 25).toInt() == i) {
gears->setGridId(i);
dataList.append(gears);
}
}
}
@@ -2329,15 +2343,8 @@ void homeform::LargeButton(const QString &name) {
}
void homeform::Plus(const QString &name) {
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
"activity", "()Landroid/app/Activity;");
QAndroidJniObject::callStaticMethod<void>(
"org/cagnulen/qdomyoszwift/PiP",
"enterPiP", "(Landroid/app/Activity;)V", activity.object<jobject>());
QSettings settings;
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
qDebug() << QStringLiteral("Plus") << name;
if (name.contains(QStringLiteral("speed"))) {
@@ -2416,6 +2423,9 @@ void homeform::Plus(const QString &name) {
if (bluetoothManager->device()) {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
((bike *)bluetoothManager->device())->setGears(((bike *)bluetoothManager->device())->gears() + 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
((elliptical *)bluetoothManager->device())
->setGears(((elliptical *)bluetoothManager->device())->gears() + 1);
}
}
} else if (name.contains(QStringLiteral("target_resistance"))) {
@@ -2454,7 +2464,7 @@ void homeform::Plus(const QString &name) {
((elliptical *)bluetoothManager->device())
->changeResistance(((elliptical *)bluetoothManager->device())->currentResistance().value() + 1);
}
}
}
} else if (name.contains(QStringLiteral("target_power"))) {
if (bluetoothManager->device()) {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
@@ -2569,6 +2579,9 @@ void homeform::Minus(const QString &name) {
if (bluetoothManager->device()) {
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
((bike *)bluetoothManager->device())->setGears(((bike *)bluetoothManager->device())->gears() - 1);
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
((elliptical *)bluetoothManager->device())
->setGears(((elliptical *)bluetoothManager->device())->gears() - 1);
}
}
} else if (name.contains(QStringLiteral("target_resistance"))) {

View File

@@ -668,6 +668,7 @@ class homeform : public QObject {
void volumeUp();
void keyMediaPrevious();
void keyMediaNext();
void floatingOpen();
void deviceFound(const QString &name);
void deviceConnected(QBluetoothDeviceInfo b);
void ftmsAccessoryConnected(smartspin2k *d);

View File

@@ -866,26 +866,30 @@ void horizontreadmill::update() {
/*if (horizon_treadmill_7_8)*/ {
// stop
if (requestPause == -1) {
messageID++;
uint8_t write1[] = {0x55, 0xaa, 0x13, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x00};
write1[2] = messageID & 0xff;
write1[3] = messageID >> 8;
writeCharacteristic(gattCustomService, gattWriteCharCustomService, write1, sizeof(write1),
QStringLiteral("requestStop"), false, true);
Speed = 0; // forcing the speed to be sure, maybe I could remove this
if(!settings.value(QZSettings::horizon_treadmill_disable_pause, QZSettings::default_horizon_treadmill_disable_pause).toBool()) {
messageID++;
uint8_t write1[] = {0x55, 0xaa, 0x13, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x00};
write1[2] = messageID & 0xff;
write1[3] = messageID >> 8;
writeCharacteristic(gattCustomService, gattWriteCharCustomService, write1, sizeof(write1),
QStringLiteral("requestStop"), false, true);
}
// pause
} else {
requestPause = -1;
messageID++;
uint8_t write1[] = {0x55, 0xaa, 0x12, 0x00, 0x03, 0x03, 0x01, 0x00, 0xf0, 0xe1, 0x00};
write1[2] = messageID & 0xff;
write1[3] = messageID >> 8;
writeCharacteristic(gattCustomService, gattWriteCharCustomService, write1, sizeof(write1),
QStringLiteral("requestPause"), false, false);
Speed = 0; // forcing the speed to be sure, maybe I could remove this
horizonPaused = true;
if(!settings.value(QZSettings::horizon_treadmill_disable_pause, QZSettings::default_horizon_treadmill_disable_pause).toBool()) {
messageID++;
uint8_t write1[] = {0x55, 0xaa, 0x12, 0x00, 0x03, 0x03, 0x01, 0x00, 0xf0, 0xe1, 0x00};
write1[2] = messageID & 0xff;
write1[3] = messageID >> 8;
writeCharacteristic(gattCustomService, gattWriteCharCustomService, write1, sizeof(write1),
QStringLiteral("requestPause"), false, false);
horizonPaused = true;
}
}
}
} else {

View File

@@ -31,5 +31,6 @@
<file>icons/unlock.png</file>
<file>icons/maps-icon-16.png</file>
<file>icons/video.png</file>
<file>icons/mini-display.png</file>
</qresource>
</RCC>

BIN
src/icons/mini-display.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,120 @@
<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Include the CesiumJS JavaScript and CSS files -->
<script src="jquery-3.6.0.min.js"></script>
<script src="globals.js"></script>
<script src="main_ws_manager.js"></script>
<style>
td {
text-align: center;
vertical-align: middle;
}
canvas{
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>
<body style="background-color: #000000; opacity: 0.8">
<table class="metrics" style="color: #FFFFFF; font-family: sans-serif border: 0">
<tr class="speed"><td>🏃</td><td><small>km/h</small></td><td class="speed-value"><b>0.0</b></td><td><small>AVG</small></td><td class="speed-avg">0.0</td></tr>
<tr class="cadence"><td>🚴</td><td><small>rpm</small></td><td class="cadence-value"><b>0</b></td><td><small>AVG</small></td><td class="cadence-avg">0</td></tr>
<tr class="heart"><td>💓</td><td><small>bpm</small></td><td class="heart-value"><b>0</b></td><td><small>AVG</small></td><td class="heart-avg">0</td></tr>
<tr class="watt"><td></td><td><small>watt</small></td><td class="watt-value"><b>0</b></td><td><small>AVG</small></td><td class="watt-avg">0</td></tr>
<tr class="resistance"><td>🧲</td><td><small>level</small></td><td class="resistance-value"><b>1</b></td><td><small>AVG</small></td><td class="resistance-avg">1</td></tr>
<tr class="pelotonresistance"><td>🅿</td><td><small>level</small></td><td class="pelotonresistance-value"><b>1</b></td><td><small>AVG</small></td><td class="pelotonresistance-avg">1</td></tr>
<tr class="calories"><td>🔥</td><td><small>kcal</small></td><td class="calories-value" colspan="3"><b>0</b></td></tr>
<tr class="distance"><td>📏</td><td><small>km</small></td><td class="distance-value" colspan="3"><b>0.00</b></td></tr>
<tr class="elapsed"><td>⏲️</td><td><small>time</small></td><td class="elapsed-value" colspan="3"><b>0:00:00</b></td></tr>
</table>
<script type="text/javascript">
function a() {
onWorkout = false;
keys_arr = ['speed', 'speed_avg','cadence','cadence_avg', 'heart','heart_avg', 'calories', 'distance', 'watts','watts_avg', 'elapsed_h', 'elapsed_m', 'elapsed_s', 'resistance','resistance_avg', 'peloton_resistance','peloton_resistance_avg']
let ell = new MainWSQueueElement(null, function(msg) {
if (msg.msg === 'workout') {
var speed = 0;
var speed_avg = 0;
var cadence = 0;
var cadence_avg = 0;
var hr = 0;
var hr_avg = 0;
var calories = 0;
var odometer = 0;
var watt = 0;
var watt_avg = 0;
var elapsed_h = 0;
var elapsed_m = 0;
var elapsed_s = 0;
var resistance = 0;
var resistance_avg = 0;
var peloton_resistance = 0;
var peloton_resistance_avg = 0;
for (let key of keys_arr) {
if (msg.content[key] === undefined)
continue;
if (key === 'speed') {
speed = msg.content[key];
} else if (key === 'speed_avg') {
speed_avg = msg.content[key];
} else if (key === 'cadence') {
cadence = msg.content[key];
} else if (key === 'cadence_avg') {
cadence_avg = msg.content[key];
} else if (key === 'heart') {
hr = msg.content[key];
} else if (key === 'heart_avg') {
hr_avg = msg.content[key];
} else if (key === 'calories') {
calories = msg.content[key];
} else if (key === 'distance') {
odometer = msg.content[key];
} else if (key === 'watts') {
watt = msg.content[key];
} else if (key === 'watts_avg') {
watt_avg = msg.content[key];
} else if (key === 'elapsed_h') {
elapsed_h = msg.content[key];
} else if (key === 'elapsed_m') {
elapsed_m = msg.content[key];
} else if (key === 'elapsed_s') {
elapsed_s = msg.content[key];
} else if (key === 'resistance') {
resistance = msg.content[key];
} else if (key === 'resistance_avg') {
resistance_avg = msg.content[key];
} else if (key === 'peloton_resistance') {
peloton_resistance = msg.content[key];
} else if (key === 'peloton_resistance_avg') {
peloton_resistance_avg = msg.content[key];
}
}
$('.speed-value').html("<b>" + speed.toFixed(1) + "</b>");
$('.speed-avg').html(speed_avg.toFixed(1));
$('.cadence-value').html("<b>" + cadence.toFixed(0) + "</b>");
$('.cadence-avg').html(cadence_avg.toFixed(0));
$('.heart-value').html("<b>" + hr.toFixed(0) + "</b>");
$('.heart-avg').html(hr_avg.toFixed(0));
$('.watt-value').html("<b>" + watt.toFixed(0) + "</b>");
$('.watt-avg').html(watt_avg.toFixed(0));
$('.resistance-value').html("<b>" + resistance.toFixed(0) + "</b>");
$('.resistance-avg').html(resistance_avg.toFixed(0));
$('.peloton_resistance-value').html("<b>" + peloton_resistance.toFixed(0) + "</b>");
$('.peloton_resistance-avg').html(peloton_resistance_avg.toFixed(0));
$('.distance-value').html("<b>" + odometer.toFixed(2) + "</b>");
$('.elapsed-value').html("<b>" + elapsed_h.toString().padStart(2, "0") + ":" + elapsed_m.toString().padStart(2, "0") + ":"+ elapsed_s.toString().padStart(2, "0") + "</b>");
$('.calories-value').html("<b>" + calories.toFixed(0) + "</b>");
}
return null;
}, 15000, 3);
ell.enqueue().then(onWorkout).catch(function(err) {
console.error('Error is ' + err);
});
}
setTimeout(a,0);
</script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
const host_url = (!location.host || location.host.length == 0)?'192.168.25.24:7666':location.host;
function get_template_name() {
let splits = location.pathname.split('/');
if (splits.length>=2)
return splits[splits.length - 2];
else
return '';
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,132 @@
let main_ws = null;
let main_ws_queue = [];
class MainWSQueueElement {
constructor(msg_to_send, _inner_process, timeout, retry_num) {
this.msg_to_send = msg_to_send;
this.needs_to_send = msg_to_send != null;
this.timeout = timeout || 5000;
this.retry_num = retry_num || 1;
this.timer = null;
this.resolve = null;
this.reject = null;
this._inner_process = _inner_process;
}
inner_process_msg(msg) {
if (this._inner_process)
return this._inner_process(msg);
else
return {};
}
process_arrived_msg(msg) {
let out = this.inner_process_msg(msg);
if (out) {
if (this.timer !==null) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.resolve)
setTimeout(function() { this.resolve(out); }.bind(this), 0);
}
return out;
}
enqueue() {
main_ws_enqueue(this);
return new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
}
pop_msg_to_send() {
if (this.needs_to_send) {
this.needs_to_send = false;
if (this.retry_num < 0 || this.retry_num > 0)
this.timer = setTimeout(function() {
this.timer = null;
if (this.retry_num == 0) {
main_ws_dequeue(this);
if (this.reject) {
this.reject(new Error('Timeout error detected'));
}
}
else {
this.needs_to_send = true;
main_ws_enqueue();
}
}.bind(this), this.timeout);
if (this.retry_num > 0)
this.retry_num--;
return this.msg_to_send;
}
else
return null;
}
}
function main_ws_dequeue(el) {
let idx = main_ws_queue.indexOf(el);
if (idx >= 0) {
main_ws_queue.splice(idx, 1);
}
}
function main_ws_enqueue(el) {
if (el)
main_ws_queue.push(el);
if (main_ws)
main_ws_queue_process();
}
function main_ws_queue_process(msg) {
if (!main_ws)
return;
let jsonobj;
for (let i = 0; i < main_ws_queue.length; i++) {
let el = main_ws_queue[i];
if ((jsonobj = el.pop_msg_to_send())) {
let logString = JSON.stringify(jsonobj);
main_ws.send(logString);
console.log('WS >> ' + logString);
}
else if (msg) {
if (el.process_arrived_msg(msg)) {
main_ws_queue.splice(i, 1);
i--;
msg = null;
}
}
}
}
function main_ws_connect() {
let socket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + '/' + get_template_name() + '-ws');
socket.onopen = function (event) {
console.log('Upgrade HTTP connection OK');
main_ws = socket;
main_ws_queue_process();
};
socket.onclose = function(e) {
main_ws = null;
console.log('Socket is closed. Reconnect will be attempted in 30 second.', e.reason);
setTimeout(function() {
main_ws_connect();
}, 5000);
};
socket.onerror = function(err) {
main_ws = null;
console.error('Socket encountered error: ', err.message, 'Closing socket');
socket.close();
};
socket.onmessage = function (event) {
console.log(event.data);
let msg = JSON.parse(event.data);
main_ws_queue_process(msg);
};
}
main_ws_connect();

View File

@@ -25,6 +25,12 @@ kingsmithr2treadmill::kingsmithr2treadmill(uint32_t pollDeviceTime, bool noConso
if (forceInitInclination > 0) {
lastInclination = forceInitInclination;
}
if (lastControlMode != UNKNOWN_CONTROL_MODE) {
lastControlMode = UNKNOWN_CONTROL_MODE;
}
if (lastRunState != UNKNOWN_RUN_STATE) {
lastRunState = UNKNOWN_RUN_STATE;
}
refresh = new QTimer(this);
initDone = false;
@@ -120,8 +126,11 @@ void kingsmithr2treadmill::update() {
QSettings settings;
// ******************************************* virtual treadmill init *************************************
if (!firstInit && !virtualTreadMill && !virtualBike) {
bool virtual_device_enabled = settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_force_bike = settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike).toBool();
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_force_bike =
settings.value(QZSettings::virtual_device_force_bike, QZSettings::default_virtual_device_force_bike)
.toBool();
if (virtual_device_enabled) {
if (!virtual_device_force_bike) {
debug("creating virtual treadmill interface...");
@@ -151,74 +160,84 @@ void kingsmithr2treadmill::update() {
// writeCharacteristic(QStringLiteral(""), QStringLiteral("noOp"), false, true);
}
// byte 3 - 4 = elapsed time
// byte 17 = inclination
if (requestSpeed != -1) {
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
if (lastRunState == KINGSMITH_R2_RUN_STATE::START) {
if (requestSpeed != -1) {
if (requestSpeed != currentSpeed().value() && requestSpeed >= 0 && requestSpeed <= 22) {
emit debug(QStringLiteral("writing speed ") + QString::number(requestSpeed));
double inc = Inclination.value();
if (requestInclination != -100) {
double inc = Inclination.value();
if (requestInclination != -100) {
// only 0.5 steps ara available
requestInclination = qRound(requestInclination * 2.0) / 2.0;
inc = requestInclination;
requestInclination = -100;
// only 0.5 steps ara available
requestInclination = qRound(requestInclination * 2.0) / 2.0;
inc = requestInclination;
requestInclination = -100;
}
forceSpeedOrIncline(requestSpeed, inc);
}
forceSpeedOrIncline(requestSpeed, inc);
requestSpeed = -1;
}
requestSpeed = -1;
}
if (requestInclination != -100) {
if(requestInclination < 0)
requestInclination = 0;
// only 0.5 steps ara available
requestInclination = qRound(requestInclination * 2.0) / 2.0;
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
requestInclination <= 15) {
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
if (requestInclination != -100) {
if (requestInclination < 0)
requestInclination = 0;
// only 0.5 steps ara available
requestInclination = qRound(requestInclination * 2.0) / 2.0;
if (requestInclination != currentInclination().value() && requestInclination >= 0 &&
requestInclination <= 15) {
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
double speed = currentSpeed().value();
if (requestSpeed != -1) {
double speed = currentSpeed().value();
if (requestSpeed != -1) {
speed = requestSpeed;
requestSpeed = -1;
speed = requestSpeed;
requestSpeed = -1;
}
forceSpeedOrIncline(speed, requestInclination);
}
forceSpeedOrIncline(speed, requestInclination);
requestInclination = -100;
}
requestInclination = -100;
}
if (requestStart != -1) {
emit debug(QStringLiteral("starting..."));
if (lastSpeed == 0.0) {
lastSpeed = 0.5;
if (requestStop != -1) {
emit debug(QStringLiteral("stopping..."));
if (lastRunState != STOP) {
writeCharacteristic(QStringLiteral("props runState 0"), QStringLiteral("stopping"), false, true);
}
// don't go to standby mode automatically
requestStop = -1;
}
// btinit(true);
requestStart = -1;
emit tapeStarted();
}
if (requestStop != -1) {
emit debug(QStringLiteral("stopping..."));
requestStop = -1;
}
if (requestFanSpeed != -1) {
emit debug(QStringLiteral("changing fan speed..."));
if (requestFanSpeed != -1) {
emit debug(QStringLiteral("changing fan speed..."));
// sendChangeFanSpeed(requestFanSpeed);
requestFanSpeed = -1;
}
if (requestIncreaseFan != -1) {
emit debug(QStringLiteral("increasing fan speed..."));
// sendChangeFanSpeed(requestFanSpeed);
requestFanSpeed = -1;
}
if (requestIncreaseFan != -1) {
emit debug(QStringLiteral("increasing fan speed..."));
// sendChangeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
} else if (requestDecreaseFan != -1) {
emit debug(QStringLiteral("decreasing fan speed..."));
// sendChangeFanSpeed(FanSpeed + 1);
requestIncreaseFan = -1;
} else if (requestDecreaseFan != -1) {
emit debug(QStringLiteral("decreasing fan speed..."));
// sendChangeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}
// sendChangeFanSpeed(FanSpeed - 1);
requestDecreaseFan = -1;
}
} else if (lastRunState == KINGSMITH_R2_RUN_STATE::STOP ||
lastRunState == KINGSMITH_R2_RUN_STATE::UNKNOWN_RUN_STATE)
if (requestStart != -1) {
emit debug(QStringLiteral("starting..."));
if (lastControlMode != MANUAL) {
writeCharacteristic(QStringLiteral("props ControlMode 1"),
QStringLiteral("turn on treadmill to manual mode"), false, true);
}
if (lastRunState != START) {
writeCharacteristic(QStringLiteral("props runState 1"), QStringLiteral("starting"), false, true);
}
if (lastSpeed == 0.0) {
lastSpeed = 0.5;
}
requestStart = -1;
emit tapeStarted();
}
}
}
@@ -276,11 +295,16 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
QStringList _props = data.split(QStringLiteral(" "), QString::SkipEmptyParts);
for (int i = 1; i < _props.size(); i += 2) {
QString key = _props.at(i);
// Error key only can have error code
// props Error "ErrorCode" -5000
if (!key.compare(QStringLiteral("Error"))) {
break;
}
// skip string params
if (!key.compare(QStringLiteral("mcu_version")) || !key.compare(QStringLiteral("goal"))) {
continue;
}
if(i+1 >= _props.count()) {
if (i + 1 >= _props.count()) {
qDebug() << "error decoding" << i;
return;
}
@@ -291,6 +315,9 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
double speed = props.value("CurrentSpeed", 0);
Cadence = props.value("spm", 0);
KINGSMITH_R2_CONTROL_MODE controlMode =
(KINGSMITH_R2_CONTROL_MODE)(int)props.value("ControlMode", (double)UNKNOWN_CONTROL_MODE);
KINGSMITH_R2_RUN_STATE runState = (KINGSMITH_R2_RUN_STATE)(int)props.value("runState", (double)UNKNOWN_RUN_STATE);
// TODO:
// - RunningDistance (int; meter) : update each 10miters / 0.01 mile
@@ -299,6 +326,9 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
// - RunningTotalTime (int; sec)
// - spm (int) : steps per minute
// TODO: check 'ControlMode' and 'runState' of treadmill side
// then update current running status of application.
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
Heart = (uint8_t)KeepAwakeHelper::heart();
@@ -329,7 +359,8 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
if (!firstCharacteristicChanged) {
if (watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()))
KCal +=
((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) + 1.19) *
((((0.048 * ((double)watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat())) +
1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastTimeCharacteristicChanged.msecsTo(
@@ -364,6 +395,25 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
// lastInclination = incline;
}
if (lastControlMode != controlMode) {
lastControlMode = controlMode;
if (controlMode != UNKNOWN_CONTROL_MODE) {
emit debug(QStringLiteral("kingsmith r2 is ready"));
initDone = true;
}
}
if (lastRunState != runState) {
lastRunState = runState;
// TODO define bitflag enums to bluetoothdevice
switch (runState) {
case STOP:
// emit deviceStateChanged(1);
break;
case START:
// emit deviceStateChanged(2);
break;
}
}
firstCharacteristicChanged = false;
}
@@ -392,7 +442,7 @@ void kingsmithr2treadmill::btinit(bool startTape) {
writeCharacteristic(QStringLiteral("servers getProp 1 2 7 12 23 24 31"), QStringLiteral("init"), false, true);
// TODO need reset BurnCalories & RunningDistance
initDone = true;
// initDone = true;
}
void kingsmithr2treadmill::stateChanged(QLowEnergyService::ServiceState state) {

View File

@@ -80,6 +80,11 @@ class kingsmithr2treadmill : public treadmill {
QDateTime lastTimeCharacteristicChanged;
bool firstCharacteristicChanged = true;
enum KINGSMITH_R2_CONTROL_MODE { AUTOMODE = 0, MANUAL, STANDBY, UNKNOWN_CONTROL_MODE };
enum KINGSMITH_R2_RUN_STATE { STOP = 0, START, UNKNOWN_RUN_STATE };
KINGSMITH_R2_CONTROL_MODE lastControlMode = UNKNOWN_CONTROL_MODE;
KINGSMITH_R2_RUN_STATE lastRunState = UNKNOWN_RUN_STATE;
QTimer *refresh;
virtualtreadmill *virtualTreadMill = nullptr;
virtualbike *virtualBike = 0;

View File

@@ -34,6 +34,7 @@ ApplicationWindow {
signal volumeDown()
signal keyMediaPrevious()
signal keyMediaNext()
signal floatingOpen()
property bool lockTiles: false
@@ -334,6 +335,14 @@ ApplicationWindow {
}
}
ToolButton {
id: toolButtonFloating
icon.source: "icons/icons/mini-display.png"
onClicked: { console.log("floating!"); floatingOpen(); }
anchors.left: toolButton.right
visible: OS_VERSION === "Android" ? true : false
}
Popup {
id: popupAutoResistance
parent: Overlay.overlay
@@ -655,7 +664,7 @@ ApplicationWindow {
}
ItemDelegate {
text: "version 2.12.10"
text: "version 2.12.13"
width: parent.width
}
FileDialog {

View File

@@ -668,12 +668,14 @@ DISTFILES += \
android/gradlew \
android/gradlew.bat \
android/libs/android_antlib_4-14-0.jar \
android/res/layout/floating_layout.xml \
android/res/values/libs.xml \
android/src/Ant.java \
android/src/ChannelService.java \
android/src/FloatingHandler.java \
android/src/FloatingWindowGFG.java \
android/src/ForegroundService.java \
android/src/NotificationClient.java \
android/src/PiP.java \
android/src/ScanRecordResult.java \
android/src/NativeScanCallback.java \
android/src/HeartChannelController.java \
@@ -681,6 +683,7 @@ DISTFILES += \
android/src/PowerChannelController.java \
android/src/SpeedChannelController.java \
android/src/com/dsi/ant/channel/PredefinedNetwork.java \
android/gradle.properties \
android/src/org/qtproject/qt/android/purchasing/Security.java \
android/src/org/qtproject/qt/android/purchasing/InAppPurchase.java \
android/src/org/qtproject/qt/android/purchasing/Base64.java \
@@ -690,7 +693,7 @@ DISTFILES += \
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
ANDROID_ABIS = armeabi-v7a
ANDROID_ABIS = armeabi-v7a arm64-v8a x86 x86_64
ios {
ios_icon.files = $$files($$PWD/icons/ios/*.png)
@@ -727,4 +730,4 @@ INCLUDEPATH += purchasing/inapp
WINRT_MANIFEST = AppxManifest.xml
VERSION = 2.12.10
VERSION = 2.12.13

View File

@@ -86,5 +86,9 @@
<file>GPXList.qml</file>
<file>videoPlayback.qml</file>
<file>settings-tiles.qml</file>
<file>inner_templates/floating/floating.htm</file>
<file>inner_templates/floating/globals.js</file>
<file>inner_templates/floating/jquery-3.6.0.min.js</file>
<file>inner_templates/floating/main_ws_manager.js</file>
</qresource>
</RCC>

View File

@@ -557,8 +557,11 @@ const QString QZSettings::domyos_elliptical_inclination = QStringLiteral("domyos
const QString QZSettings::gpx_loop = QStringLiteral("gpx_loop");
const QString QZSettings::android_notification = QStringLiteral("android_notification");
const QString QZSettings::kingsmith_encrypt_v4 = QStringLiteral("kingsmith_encrypt_v4");
const QString QZSettings::horizon_treadmill_disable_pause = QStringLiteral("horizon_treadmill_disable_pause");
const QString QZSettings::domyos_bike_500_profile_v1 = QStringLiteral("domyos_bike_500_profile_v1");
const QString QZSettings::ss2k_peloton = QStringLiteral("ss2k_peloton");
const uint32_t allSettingsCount = 456;
const uint32_t allSettingsCount = 459;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
{QZSettings::bluetooth_no_reconnection, QZSettings::default_bluetooth_no_reconnection},
@@ -1018,6 +1021,9 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::gpx_loop, QZSettings::default_gpx_loop},
{QZSettings::android_notification, QZSettings::default_android_notification},
{QZSettings::kingsmith_encrypt_v4, QZSettings::default_kingsmith_encrypt_v4},
{QZSettings::horizon_treadmill_disable_pause, QZSettings::default_horizon_treadmill_disable_pause},
{QZSettings::domyos_bike_500_profile_v1, QZSettings::domyos_bike_500_profile_v1},
{QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton},
};
void QZSettings::qDebugAllSettings(bool showDefaults) {

View File

@@ -1589,11 +1589,20 @@ class QZSettings {
static constexpr bool default_gpx_loop = false;
static const QString android_notification;
static constexpr bool default_android_notification = true;
static constexpr bool default_android_notification = false;
static const QString kingsmith_encrypt_v4;
static constexpr bool default_kingsmith_encrypt_v4 = false;
static const QString horizon_treadmill_disable_pause;
static constexpr bool default_horizon_treadmill_disable_pause = false;
static const QString domyos_bike_500_profile_v1;
static constexpr bool default_domyos_bike_500_profile_v1 = false;
static const QString ss2k_peloton;
static constexpr bool default_ss2k_peloton = false;
/**
* @brief Write the QSettings values using the constants from this namespace.
* @param showDefaults Optionally indicates if the default should be shown with the key.

View File

@@ -590,10 +590,19 @@ import Qt.labs.settings 1.0
property bool gpx_loop: false
// from version 2.12.6
property bool android_notification: true
property bool android_notification: false
// from version 2.12.8
property bool kingsmith_encrypt_v4: false
// from versiomn 2.12.11
property bool horizon_treadmill_disable_pause: false
// from version 2.12.13
property bool domyos_bike_500_profile_v1: false
// from version 2.12.14
property bool ss2k_peloton: false
}
function paddingZeros(text, limit) {
@@ -2016,6 +2025,20 @@ import Qt.labs.settings 1.0
Layout.fillWidth: true
onClicked: settings.domyos_bike_display_calories = checked
}
SwitchDelegate {
id: domyosBike500ProfileV1Delegate
text: qsTr("Bike 500 wattage profile")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.domyos_bike_500_profile_v1
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.domyos_bike_500_profile_v1 = checked
}
}
AccordionElement {
id: proformBikeAccordion
@@ -3786,6 +3809,21 @@ import Qt.labs.settings 1.0
onClicked: settings.horizon_treadmill_7_8 = checked
}
SwitchDelegate {
id: horizonTreadmillDisablePauseDelegate
text: qsTr("Disable Pause")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.horizon_treadmill_disable_pause
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.horizon_treadmill_disable_pause = checked
}
RowLayout {
spacing: 10
Label {
@@ -4834,6 +4872,21 @@ import Qt.labs.settings 1.0
text: "Refresh Devices List"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: refresh_bluetooth_devices_clicked();
}
SwitchDelegate {
id: ss2kPelotonDelegate
text: qsTr("Peloton Bike")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.ss2k_peloton
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: settings.ss2k_peloton = checked
}
RowLayout {

View File

@@ -647,7 +647,8 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
<< QStringLiteral("resistance sent to ifit") << resistance;
double odometer = Bike->odometer() * 1000;
double calories = Bike->calories().value();
// ifit applies a constant multiplier to the kcal sent from bluetooth
double calories = Bike->calories().value() * 1.488;
if (resistance > 0x26)
resistance = 0x26;
qint64 t = (QDateTime::currentSecsSinceEpoch() - iFit_timer);
@@ -668,8 +669,10 @@ void virtualbike::characteristicChanged(const QLowEnergyCharacteristic &characte
reply3[15] = t & 0xff;
reply3[16] = t >> 8;
reply4[3] = ((uint16_t)calories); // KCal
reply4[4] = (((uint16_t)calories) >> 8); // KCal
reply4[10] = ((uint16_t)calories); // KCal estimated
reply3[19] = 0xEE - (reply3[15] * 3) - (reply4[10] * 2) - (reply2[18]) - (reply2[11]) - (reply2[12]) -
reply4[11] = (((uint16_t)calories) >> 8); // KCal
reply3[19] = 0xEE - (reply3[15] * 3) - (reply4[10] * 2) - (reply4[4] * 2) - (reply2[18]) - (reply2[11]) - (reply2[12]) -
(reply2[13]) - (reply3[13]) - (reply3[14]) - (reply2[14]) - (reply3[7]) - (reply3[12]) -
(reply3[16]) - (reply2[15]) - (reply2[16]);
reply4[19] = reply3[19];