mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
59 Commits
1260
...
Mobi-Rower
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba867a1b5 | ||
|
|
72f57053a7 | ||
|
|
13ea5313b1 | ||
|
|
7f694733b2 | ||
|
|
b1755c004a | ||
|
|
360ab66431 | ||
|
|
04b659a91f | ||
|
|
9487fa3cb4 | ||
|
|
1ac2149424 | ||
|
|
28558697b2 | ||
|
|
6918fb9eba | ||
|
|
365abbb7cb | ||
|
|
d3f52682cc | ||
|
|
da4f360f63 | ||
|
|
b1c6cf70f5 | ||
|
|
50e18b1db4 | ||
|
|
47ea3c2176 | ||
|
|
c83c272ed4 | ||
|
|
de3ea61ecf | ||
|
|
2d53ebf190 | ||
|
|
93feea6c16 | ||
|
|
baaf689b4c | ||
|
|
4dea13d78e | ||
|
|
914b02f8e0 | ||
|
|
0e01889ed3 | ||
|
|
19ef4bb230 | ||
|
|
51c8d060de | ||
|
|
d1afe0ebb2 | ||
|
|
e5532ca04e | ||
|
|
bf37d681de | ||
|
|
03bf9d8fd1 | ||
|
|
161362f11f | ||
|
|
2bb0e212db | ||
|
|
67344ea130 | ||
|
|
727fb99572 | ||
|
|
89ec6eef1f | ||
|
|
a7ca3f329a | ||
|
|
695c05c284 | ||
|
|
b38712851d | ||
|
|
73241765d1 | ||
|
|
ad79beb44b | ||
|
|
39288f1343 | ||
|
|
ff093a126e | ||
|
|
489ef0a665 | ||
|
|
73771f42c2 | ||
|
|
dbdd58c398 | ||
|
|
5c0aa59bed | ||
|
|
7ec3b601b4 | ||
|
|
9d2bf0821c | ||
|
|
5d85cdd8e5 | ||
|
|
56bbc2439c | ||
|
|
31e1bb8a9f | ||
|
|
88a8b138ca | ||
|
|
10cfac1e40 | ||
|
|
79952ad73c | ||
|
|
3a3755d18c | ||
|
|
9a45b28f8c | ||
|
|
9b70d6c144 | ||
|
|
137036efd7 |
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -428,7 +428,16 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: test_results_xml
|
||||
path: tst/test-results/**/*.xml
|
||||
path: tst/test-results/**/*.xml
|
||||
|
||||
- name: Upload test FIT files and database
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test_fit_files_and_db
|
||||
path: |
|
||||
tst/test-artifacts/*.fit
|
||||
tst/test-artifacts/*.sqlite
|
||||
|
||||
# - name: Test Peloton API
|
||||
# if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
@@ -4573,7 +4573,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -4774,7 +4774,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
@@ -5011,7 +5011,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -5107,7 +5107,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5199,7 +5199,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -5315,7 +5315,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
|
||||
ENABLE_BITCODE = YES;
|
||||
@@ -5425,7 +5425,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -5516,7 +5516,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1260;
|
||||
CURRENT_PROJECT_VERSION = 1274;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 6335M7T29D;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
||||
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
BIN
src/ConnectIQ/iOS/ConnectIQ.xcframework/ios-arm64_x86_64-simulator/ConnectIQ.framework/ConnectIQ
Normal file → Executable file
Binary file not shown.
@@ -6,9 +6,10 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQConstants.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQApp.h>
|
||||
|
||||
#import "IQConstants.h"
|
||||
#import "IQDevice.h"
|
||||
#import "IQApp.h"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - PUBLIC TYPES
|
||||
@@ -49,9 +50,22 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's connection status has
|
||||
/// changed.
|
||||
///
|
||||
/// When the device status is updated to ``IQDeviceStatus.IQDeviceStatus_Connected``
|
||||
/// it does not mean the device services and characteristics have been discovered yet. To wait
|
||||
/// till the services and characteristics to be discovered the client app has to wait on the delegate call
|
||||
/// ``deviceCharacteristicsDiscovered:(IQDevice *)``. After that the client
|
||||
/// app can start communicating with the device. The method ``deviceCharacteristicsDiscovered:``
|
||||
/// was added to keep backwards compatibility for ``IQDeviceStatus``.
|
||||
///
|
||||
/// @param device The IQDevice whose status changed.
|
||||
/// @param status The new status of the device.
|
||||
- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
|
||||
|
||||
/// @brief Called by the ConnectIQ SDK when an IQDevice's charactersitics are discovered.
|
||||
/// When this method is called the device is ready for communication with the client app.
|
||||
///
|
||||
/// @param device The IQDevice whose characteristics are discovered.
|
||||
- (void)deviceCharacteristicsDiscovered:(IQDevice *)device;
|
||||
@end
|
||||
|
||||
/// @brief Conforming to the IQAppMessageDelegate protocol indicates that an
|
||||
@@ -88,8 +102,11 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
#pragma mark - INITIALIZATION
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
|
||||
/// its operation.
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme. See also
|
||||
/// - (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
@@ -99,6 +116,60 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with a URL Scheme.
|
||||
///
|
||||
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this scheme.
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUrlScheme:(NSString *)urlScheme
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links. See also
|
||||
/// - (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
/// uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
/// stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
/// for comparison.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
|
||||
|
||||
/// @brief Initializes the ConnectIQ SDK for use with Universal links.
|
||||
///
|
||||
/// @param urlHost The URL host for this companion app. When Garmin Connect
|
||||
/// Mobile is launched, it will return to the companion app by
|
||||
/// launching a URL with this host. The host URL shall be added
|
||||
/// to associated domains list and shall have an entry in apple-app-site-association
|
||||
/// JSON file hosted on the same domain to be able to launch the companion app
|
||||
/// @param delegate The delegate that the SDK will use for notifying the
|
||||
/// companion app about events that require user input. If this
|
||||
/// is nil, the SDK's default UI will be used.
|
||||
/// @param restorationIdentifier The string which will be used as the value for
|
||||
/// CBCentralManagerOptionRestoreIdentifierKey for the internal CBCentralManager.
|
||||
/// The benefit of adding this identifier is that it allows the app to relaunch in the background
|
||||
/// when BLE activity is detected on associated devices after being suspended by iOS. The SDK
|
||||
/// does not currently handle the resulting call to willRestoreState because most CIQ companion apps
|
||||
/// will reconnect to devices they are interested in during app launch.
|
||||
- (void)initializeWithUniversalLinks:(NSString *)urlHost
|
||||
uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate
|
||||
stateRestorationIdentifier:(NSString *) restorationIdentifier;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
#pragma mark - EXTERNAL LAUNCHING
|
||||
// --------------------------------------------------------------------------------
|
||||
@@ -224,6 +295,21 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
|
||||
/// message operation is complete.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
|
||||
|
||||
/// @brief Begins sending a message to an app while allowing the message to be marked as transient. This method returns immediately.
|
||||
///
|
||||
/// @param message The message to send to the app. This message must be one of
|
||||
/// the following types: NSString, NSNumber, NSNull, NSArray,
|
||||
/// or NSDictionary. Arrays and dictionaries may be nested.
|
||||
/// @param app The app to send the message to.
|
||||
/// @param progress A progress block that will be triggered periodically
|
||||
/// throughout the transfer. This is guaranteed to be triggered
|
||||
/// at least once.
|
||||
/// @param completion A completion block that will be triggered when the send
|
||||
/// message operation is complete.
|
||||
/// @param isTransient Flag to mark the message as transient.
|
||||
- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress
|
||||
completion:(IQSendMessageCompletion)completion isTransient:(BOOL)isTransient;
|
||||
|
||||
/// @brief Sends an open app request message request to the device. This method returns immediately.
|
||||
///
|
||||
/// @param app The app to open.
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ConnectIQ/IQDevice.h>
|
||||
#import <ConnectIQ/IQAppStatus.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
/// @brief Represents an instance of a ConnectIQ app that is installed on a
|
||||
/// Garmin device.
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// Garmin Connect Mobile.
|
||||
@property (nonatomic, readonly) NSString *friendlyName;
|
||||
|
||||
/// @brief The part number of the device per the Garmin catalog of devices.
|
||||
@property (nonatomic, readonly) NSString *partNumber;
|
||||
|
||||
/// @brief Creates a new device instance.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
@@ -51,6 +54,17 @@ typedef NS_ENUM(NSInteger, IQDeviceStatus){
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
|
||||
|
||||
/// @brief Creates a new device instance with part number included.
|
||||
///
|
||||
/// @param uuid The UUID of the device to create.
|
||||
/// @param modelName The model name of the device to create.
|
||||
/// @param friendlyName The friendly name of the device to create.
|
||||
/// @param partNumber The part number of the device to create.
|
||||
///
|
||||
/// @return A new IQDevice instance with the appropriate values set.
|
||||
+ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName
|
||||
partNumber:(NSString *)partNumber;
|
||||
|
||||
/// @brief Creates a new device instance by copying another device's values.
|
||||
///
|
||||
/// @param device The device to copy values from.
|
||||
|
||||
Binary file not shown.
@@ -6,11 +6,11 @@
|
||||
<dict>
|
||||
<key>Headers/ConnectIQ.h</key>
|
||||
<data>
|
||||
yih4e2KjbC/GqavxdCZ3xQ4mHmA=
|
||||
oktDCwqbdQQg6rdcptAN5TGhUZs=
|
||||
</data>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<data>
|
||||
NDlj8k5C84UPFmD+qEMz2WcZloY=
|
||||
CMQ9wDp2PKaw9dRd8NBYpX9xkzE=
|
||||
</data>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
<data>
|
||||
@@ -22,11 +22,11 @@
|
||||
</data>
|
||||
<key>Headers/IQDevice.h</key>
|
||||
<data>
|
||||
bl545C/cu0mw2KlRmzojKmHPom0=
|
||||
a4hkgIut7ETtkOJXPkn/nGElEYg=
|
||||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
YUOCJU/YBLc4CRWV1z8JHDjCx8M=
|
||||
LeO8CbXcC4FrKgyl2zDm7R7nOj0=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
@@ -300,14 +300,14 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
kAenemss8n98vVLi54JqBUtGwaL1/i+HSejFBZgawHA=
|
||||
E2QDme6rWC+CJc/kKtxIVSpPzbE4ArUwNagnLG6Nxis=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQApp.h</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
bSRRooQ0FKFr3BgrFolAnkU402889YFHrH+6EEca3cg=
|
||||
KhyZorkoK2Qipuzee5aE5ENCarHR+Ni21GdxCV3FQ0s=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Headers/IQAppStatus.h</key>
|
||||
@@ -328,7 +328,7 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
|
||||
Xx+4dhu0JD6w2pd9UMvLXukYVQfKzaLJhU0paDUQyls=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/module.modulemap</key>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.22" android:versionCode="1255" android:installLocation="auto">
|
||||
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.23" android:versionCode="1264" 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 -->
|
||||
|
||||
@@ -694,6 +694,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
const QString deviceName = b.name();
|
||||
const QString upperDeviceName = deviceName.toUpper();
|
||||
bool isRI009R = upperDeviceName.contains(QStringLiteral("RI009R"));
|
||||
bool isTrxAppGateUsbBikeTC = false;
|
||||
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
|
||||
isTrxAppGateUsbBikeTC = true;
|
||||
@@ -967,7 +968,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(nordictrackifitadbRower);
|
||||
} else if (((csc_as_bike && b.name().startsWith(cscName)) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("JOROTO-BK-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("BGYM")) && b.name().length() == 8)) &&
|
||||
!cscBike && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1049,8 +1051,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
emit searchingStop();
|
||||
}
|
||||
this->signalBluetoothDeviceConnected(domyosBike);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower)) &&
|
||||
} else if ((((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) && iconsole_rower)) &&
|
||||
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -1564,8 +1566,11 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
|
||||
(b.name().toUpper().startsWith(QStringLiteral("X-T"))) || // FTMS (X-T421)
|
||||
(b.name().toUpper().startsWith(QStringLiteral("TC-"))) || // FTMS (Focus Fitness Jet 7 iPlus)
|
||||
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) || // FTMS
|
||||
b.name().toUpper().startsWith(QStringLiteral("BODYCRAFT_")) || // Bodycraft T850 Treadmill
|
||||
(b.name().toUpper().startsWith(QStringLiteral("WT")) && b.name().length() == 5 && b.name().midRef(2).toInt() > 0) // WT treadmill (e.g. WT703)
|
||||
) &&
|
||||
!horizonTreadmill && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
@@ -1819,6 +1824,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
|
||||
(b.name().toUpper().startsWith("RIVO COG")) ||
|
||||
(b.name().toUpper().startsWith("RAVE")) ||
|
||||
(b.name().toUpper().startsWith("BESP-")) || // FITFIU BESP 250 indoor bike
|
||||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
|
||||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
|
||||
(b.name().toUpper().startsWith("FS-YK-")) ||
|
||||
@@ -1971,6 +1977,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-CRYDN-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("MRK-R06-")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) && !iconsole_rower) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) || // Chaoke 107a magnetic rowing machine (Discussion #4029)
|
||||
@@ -2087,9 +2094,18 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
|
||||
lifespanTreadmill->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(lifespanTreadmill);
|
||||
} else if ((b.name().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
} else if (b.name().startsWith(QStringLiteral("AT-R")) && !mobiRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
mobiRower = new mobirower(noWriteResistance, noHeartService);
|
||||
emit deviceConnected(b);
|
||||
connect(mobiRower, &bluetoothdevice::connectedAndDiscovered, this,
|
||||
&bluetooth::connectedAndDiscovered);
|
||||
mobiRower->deviceDiscovered(b);
|
||||
this->signalBluetoothDeviceConnected(mobiRower);
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
|
||||
b.name().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
|
||||
!echelonRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
@@ -2545,9 +2561,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
|
||||
isRI009R ||
|
||||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
|
||||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
|
||||
@@ -2555,7 +2572,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
|
||||
upperDeviceName.startsWith(QStringLiteral("DKN MOTION"))) &&
|
||||
(toorx_bike))) &&
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
|
||||
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && (filter || isRI009R) && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
trxappgateusbBike =
|
||||
@@ -2659,7 +2676,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
|
||||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
|
||||
b.name().startsWith(QStringLiteral("MRK-"))) &&
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && filter) {
|
||||
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && !trxappgateusbRower && filter) {
|
||||
this->setLastBluetoothDevice(b);
|
||||
this->stopDiscovery();
|
||||
fitPlusBike =
|
||||
@@ -3597,6 +3614,11 @@ void bluetooth::restart() {
|
||||
delete echelonRower;
|
||||
echelonRower = nullptr;
|
||||
}
|
||||
if (mobiRower) {
|
||||
|
||||
delete mobiRower;
|
||||
mobiRower = nullptr;
|
||||
}
|
||||
if (echelonStride) {
|
||||
|
||||
delete echelonStride;
|
||||
@@ -4032,6 +4054,8 @@ bluetoothdevice *bluetooth::device() {
|
||||
return echelonConnectSport;
|
||||
} else if (echelonRower) {
|
||||
return echelonRower;
|
||||
} else if (mobiRower) {
|
||||
return mobiRower;
|
||||
} else if (echelonStride) {
|
||||
return echelonStride;
|
||||
} else if (echelonStairclimber) {
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
|
||||
#include "zwift_play/zwiftPlayDevice.h"
|
||||
#include "zwift_play/zwiftclickremote.h"
|
||||
#include "devices/mobirower/mobirower.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
@@ -269,6 +270,7 @@ class bluetooth : public QObject, public SignalHandler {
|
||||
echelonrower *echelonRower = nullptr;
|
||||
ftmsrower *ftmsRower = nullptr;
|
||||
smartrowrower *smartrowRower = nullptr;
|
||||
mobirower *mobiRower = nullptr;
|
||||
echelonstride *echelonStride = nullptr;
|
||||
echelonstairclimber *echelonStairclimber = nullptr;
|
||||
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
|
||||
|
||||
@@ -301,7 +301,7 @@ void domyosrower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
|
||||
void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
|
||||
qDebug() << QStringLiteral(" << ") + QString::number(newValue.length()) + QStringLiteral(" ") + newValue.toHex(' ');
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
@@ -643,7 +643,7 @@ void domyosrower::characteristicChanged(const QLowEnergyCharacteristic &characte
|
||||
|
||||
double domyosrower::GetSpeedFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(6) << 8) | packet.at(7);
|
||||
uint16_t convertedData = (packet.at(6) << 8) | ((uint8_t)packet.at(7));
|
||||
if (convertedData > 65000 || convertedData == 0 || currentCadence().value() == 0)
|
||||
return 0;
|
||||
return (60.0 / (double)(convertedData)) * 30.0;
|
||||
@@ -657,7 +657,7 @@ double domyosrower::GetKcalFromPacket(const QByteArray &packet) {
|
||||
|
||||
double domyosrower::GetDistanceFromPacket(const QByteArray &packet) {
|
||||
|
||||
uint16_t convertedData = (packet.at(12) << 8) | packet.at(13);
|
||||
uint16_t convertedData = (packet.at(12) << 8) | ((uint8_t)packet.at(13));
|
||||
double data = ((double)convertedData) / 10.0f;
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -769,6 +769,10 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
m_watt = ftms_watt; // Only update watt if no external power sensor
|
||||
}
|
||||
|
||||
if(!wattReceived && m_watt.value() > 0) {
|
||||
wattReceived = true;
|
||||
}
|
||||
}
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
|
||||
@@ -785,7 +789,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
index += 2;
|
||||
emit debug(QStringLiteral("Current Average Watt: ") + QString::number(avgPower));
|
||||
// Use average power if instant power is zero or not available
|
||||
if ((!Flags.instantPower || m_watt.value() == 0) && avgPower > 0) {
|
||||
if ((!Flags.instantPower || m_watt.value() == 0) && avgPower > 0 && !wattReceived) {
|
||||
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
|
||||
.toString()
|
||||
.startsWith(QStringLiteral("Disabled"))) {
|
||||
@@ -1195,7 +1199,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (watts())
|
||||
if (watts() && !ftmsFrameReceived)
|
||||
KCal += ((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
@@ -1473,6 +1477,29 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
if (gattWriteCharControlPointId.isValid()) {
|
||||
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
|
||||
|
||||
// D500V2 workaround: track request control (0x00) and start simulation (0x07) commands
|
||||
// If we receive simulation params (0x11) without start simulation, inject it first
|
||||
if (D500V2 && b.length() > 0) {
|
||||
uint8_t commandCode = (uint8_t)b.at(0);
|
||||
|
||||
if (commandCode == FTMS_REQUEST_CONTROL) {
|
||||
// Command 0x00: Request Control - expect start simulation next
|
||||
awaiting_start_simulation_after_request_control = true;
|
||||
qDebug() << "D500V2 workaround: received REQUEST_CONTROL (0x00), now awaiting START_RESUME (0x07)";
|
||||
} else if (commandCode == FTMS_START_RESUME) {
|
||||
// Command 0x07: Start Resume - no longer awaiting
|
||||
awaiting_start_simulation_after_request_control = false;
|
||||
qDebug() << "D500V2 workaround: received START_RESUME (0x07), ready for simulation params";
|
||||
} else if (commandCode == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && D500V2 && awaiting_start_simulation_after_request_control) {
|
||||
// Command 0x11: Set Simulation Params - but we're still awaiting start simulation
|
||||
// For D500V2, inject the start simulation command (0x07) first
|
||||
qDebug() << "D500V2 workaround: received SET_INDOOR_BIKE_SIMULATION_PARAMS (0x11) without START_RESUME, injecting 0x07 first";
|
||||
uint8_t startSimulation[] = {FTMS_START_RESUME};
|
||||
writeCharacteristic(startSimulation, sizeof(startSimulation), "injectWrite [D500V2 workaround: start simulation 0x07]", false, true);
|
||||
awaiting_start_simulation_after_request_control = false;
|
||||
}
|
||||
}
|
||||
|
||||
// handling gears
|
||||
if (b.at(0) == FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS && (zwiftPlayService == nullptr || !gears_zwift_ratio)) {
|
||||
double min_inclination = settings.value(QZSettings::min_inclination, QZSettings::default_min_inclination).toDouble();
|
||||
@@ -1547,11 +1574,12 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
|
||||
// handling watt gain and offset for erg mode
|
||||
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
|
||||
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
|
||||
double bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toDouble();
|
||||
|
||||
if (watt_gain != 1.0 || watt_offset != 0) {
|
||||
if (watt_gain != 1.0 || watt_offset != 0 || bike_power_offset != 0) {
|
||||
uint16_t powerRequested = (((uint8_t)b.at(1)) + (b.at(2) << 8));
|
||||
qDebug() << "applying watt_gain/watt_offset from" << powerRequested;
|
||||
powerRequested = ((powerRequested / watt_gain) - watt_offset);
|
||||
powerRequested = ((powerRequested / watt_gain) - watt_offset + bike_power_offset);
|
||||
qDebug() << "to" << powerRequested;
|
||||
|
||||
b[1] = powerRequested & 0xFF;
|
||||
@@ -1679,6 +1707,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
ergModeSupported = false;
|
||||
max_resistance = 32;
|
||||
DOMYOS = true;
|
||||
} else if (bluetoothDevice.name().toUpper().startsWith("D500V2")) {
|
||||
qDebug() << QStringLiteral("D500V2 found - enabling workaround for start simulation command");
|
||||
D500V2 = true;
|
||||
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
|
||||
qDebug() << QStringLiteral("_3G_Cardio_RB found");
|
||||
_3G_Cardio_RB = true;
|
||||
|
||||
@@ -45,7 +45,7 @@ enum FtmsControlPointCommand {
|
||||
FTMS_START_RESUME,
|
||||
FTMS_STOP_PAUSE,
|
||||
FTMS_SET_TARGETED_EXP_ENERGY,
|
||||
FTMS_SET_TARGETED_STEPS,
|
||||
FTMS_SET_TARGETED_STEPS,
|
||||
FTMS_SET_TARGETED_STRIDES,
|
||||
FTMS_SET_TARGETED_DISTANCE,
|
||||
FTMS_SET_TARGETED_TIME,
|
||||
@@ -135,9 +135,13 @@ class ftmsbike : public bike {
|
||||
bool resistance_received = false;
|
||||
inclinationResistanceTable _inclinationResistanceTable;
|
||||
|
||||
// D500V2 workaround: track if we're awaiting start simulation command after request control
|
||||
bool awaiting_start_simulation_after_request_control = false;
|
||||
|
||||
bool DU30_bike = false;
|
||||
bool ICSE = false;
|
||||
bool DOMYOS = false;
|
||||
bool D500V2 = false;
|
||||
bool _3G_Cardio_RB = false;
|
||||
bool SCH_190U = false;
|
||||
bool SCH_290R = false;
|
||||
@@ -177,6 +181,8 @@ class ftmsbike : public bike {
|
||||
|
||||
uint8_t battery_level = 0;
|
||||
|
||||
bool wattReceived = false;
|
||||
|
||||
uint16_t oldLastCrankEventTime = 0;
|
||||
uint16_t oldCrankRevs = 0;
|
||||
QDateTime lastGoodCadence = QDateTime::currentDateTime();
|
||||
|
||||
@@ -78,7 +78,7 @@ void ftmsrower::update() {
|
||||
}
|
||||
|
||||
if (initRequest) {
|
||||
if(I_ROWER || ROWER || MRK_R06) {
|
||||
if(I_ROWER || SF_RW || ROWER || MRK_R06) {
|
||||
uint8_t write[] = {FTMS_REQUEST_CONTROL};
|
||||
writeCharacteristic(write, sizeof(write), "start", false, true);
|
||||
|
||||
@@ -396,8 +396,8 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
if (ICONSOLE_PLUS || FITSHOW) {
|
||||
// For ICONSOLE+, always calculate distance from speed instead of using characteristic data
|
||||
if (ICONSOLE_PLUS || FITSHOW || MRK_R11S) {
|
||||
// For ICONSOLE+/FITSHOW/MRK_R11S, always calculate distance from speed instead of using characteristic data
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
} else {
|
||||
@@ -451,7 +451,7 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
|
||||
if((DFIT_L_R && Cadence.value() > 0) || !DFIT_L_R)
|
||||
m_watt = watt;
|
||||
}
|
||||
} else {
|
||||
} else if(!PM5) {
|
||||
qDebug() << "rower doesn't send wattage, let's calculate it...";
|
||||
if(Speed.value() > 0)
|
||||
m_watt = rower::calculateWattsFromPace(instantPace);
|
||||
@@ -592,10 +592,10 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
connect(s, &QLowEnergyService::descriptorWritten, this, &ftmsrower::descriptorWritten);
|
||||
connect(s, &QLowEnergyService::descriptorRead, this, &ftmsrower::descriptorRead);
|
||||
|
||||
if (I_ROWER || ROWER || MRK_R06) {
|
||||
if (I_ROWER || SF_RW || ROWER || MRK_R06 || DOMYOS) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s->serviceUuid() != ftmsService) {
|
||||
qDebug() << QStringLiteral("I-ROWER/ROWER/MRK-R06 wants to be subscribed only to FTMS service in order to send metrics")
|
||||
qDebug() << QStringLiteral("I-ROWER/SF-RW/ROWER/MRK-R06/DOMYOS wants to be subscribed only to FTMS service in order to send metrics")
|
||||
<< s->serviceUuid();
|
||||
continue;
|
||||
}
|
||||
@@ -774,20 +774,28 @@ void ftmsrower::serviceScanDone(void) {
|
||||
QBluetoothUuid concept2InfoService(QStringLiteral("ce060010-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2ControlService(QStringLiteral("ce060020-43e5-11e4-916c-0800200c9a66"));
|
||||
QBluetoothUuid concept2RowingService(QStringLiteral("ce060030-43e5-11e4-916c-0800200c9a66"));
|
||||
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
if (s == concept2InfoService || s == concept2ControlService || s == concept2RowingService) {
|
||||
hasConcept2Services = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasConcept2Services) {
|
||||
emit debug(QStringLiteral("PM5 without FTMS service detected, using Concept2 protocol"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
// For DOMYOS, discover only FTMS service (0x1826)
|
||||
if (DOMYOS) {
|
||||
QBluetoothUuid ftmsService((quint16)0x1826);
|
||||
if (s != ftmsService) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&ftmsrower::stateChanged);
|
||||
@@ -831,12 +839,18 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("I-ROWER"))) {
|
||||
I_ROWER = true;
|
||||
qDebug() << "I_ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("SF-RW"))) {
|
||||
SF_RW = true;
|
||||
qDebug() << "SF-RW found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("IROWER "))) {
|
||||
ROWER = true;
|
||||
qDebug() << "ROWER found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("MRK-R06-"))) {
|
||||
MRK_R06 = true;
|
||||
qDebug() << "MRK_R06 found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("MRK-R11S-"))) {
|
||||
MRK_R11S = true;
|
||||
qDebug() << "MRK_R11S found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("PM5"))) {
|
||||
PM5 = true;
|
||||
qDebug() << "PM5 found!";
|
||||
@@ -849,6 +863,9 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("FS-"))) {
|
||||
FITSHOW = true;
|
||||
qDebug() << "FITSHOW found!";
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW-"))) {
|
||||
DOMYOS = true;
|
||||
qDebug() << "DOMYOS found!";
|
||||
}
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
|
||||
@@ -73,12 +73,15 @@ class ftmsrower : public rower {
|
||||
bool NORDLYS = false;
|
||||
bool ICONSOLE_PLUS = false;
|
||||
bool FITSHOW = false;
|
||||
bool DOMYOS = false;
|
||||
|
||||
bool WATER_ROWER = false;
|
||||
bool DFIT_L_R = false;
|
||||
bool I_ROWER = false;
|
||||
bool SF_RW = false;
|
||||
bool ROWER = false;
|
||||
bool MRK_R06 = false;
|
||||
bool MRK_R11S = false;
|
||||
QDateTime lastStroke = QDateTime::currentDateTime();
|
||||
double lastStrokesCount = 0;
|
||||
|
||||
|
||||
@@ -921,6 +921,8 @@ void horizontreadmill::update() {
|
||||
settings.value(QZSettings::horizon_treadmill_7_8, QZSettings::default_horizon_treadmill_7_8).toBool();
|
||||
bool horizon_paragon_x =
|
||||
settings.value(QZSettings::horizon_paragon_x, QZSettings::default_horizon_paragon_x).toBool();
|
||||
bool treadmill_direct_distance =
|
||||
settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
update_metrics(!powerReceivedFromPowerSensor, watts(settings.value(QZSettings::weight, QZSettings::default_weight).toFloat()));
|
||||
|
||||
if (firstDistanceCalculated) {
|
||||
@@ -932,9 +934,11 @@ void horizontreadmill::update() {
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
now)))); //(( (0.048* Output in watts +1.19) * body weight in
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
// kg * 3.5) / 200 ) / 60
|
||||
if (!treadmill_direct_distance) {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = now;
|
||||
}
|
||||
@@ -1256,11 +1260,11 @@ void horizontreadmill::forceSpeed(double requestSpeed) {
|
||||
uint8_t writeS[] = {FTMS_SET_TARGET_SPEED, 0x00, 0x00};
|
||||
if(BOWFLEX_T9) {
|
||||
requestSpeed *= miles_conversion; // this treadmill wants the speed in miles, at least seems so!!
|
||||
}
|
||||
if(TM4800 || TM6500) {
|
||||
}
|
||||
if(TM4800 || TM6500 || T3G_ELITE || WT_TREADMILL) {
|
||||
bool miles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
if(miles) {
|
||||
requestSpeed *= miles_conversion; // this treadmill wants the speed in miles when miles_unit is enabled
|
||||
requestSpeed *= miles_conversion; // these treadmills want the speed in miles when miles_unit is enabled
|
||||
}
|
||||
}
|
||||
uint16_t speed_int = round(requestSpeed * 100);
|
||||
@@ -1533,6 +1537,8 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
settings.value(QZSettings::heart_ignore_builtin, QZSettings::default_heart_ignore_builtin).toBool();
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
bool treadmill_direct_distance =
|
||||
settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
double weight = settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
|
||||
@@ -1589,7 +1595,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1615,7 +1621,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1639,7 +1645,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
|
||||
emit debug(QStringLiteral("Current KCal: ") + QString::number(KCal.value()));
|
||||
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
@@ -1733,19 +1739,19 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totDistance) {
|
||||
|
||||
/*
|
||||
* the distance sent from the most trainers is a total distance, so it's useless for QZ
|
||||
*
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;*/
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
}
|
||||
index += 3;
|
||||
}
|
||||
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
if (!treadmill_direct_distance) {
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
|
||||
@@ -1865,9 +1871,15 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
double speed = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint16_t)((uint8_t)newValue.at(index)))) /
|
||||
100.0;
|
||||
if(BOWFLEX_T9) {
|
||||
bool fitshow_treadmill_miles = settings.value(QZSettings::fitshow_treadmill_miles, QZSettings::default_fitshow_treadmill_miles).toBool();
|
||||
if(BOWFLEX_T9 && fitshow_treadmill_miles) {
|
||||
// this treadmill sends the speed in miles!
|
||||
speed *= miles_conversion;
|
||||
} else if(T3G_ELITE) {
|
||||
if(miles) {
|
||||
// this treadmill sends the speed in miles when miles_unit is enabled!
|
||||
speed /= miles_conversion;
|
||||
}
|
||||
} else if(horizon_treadmill_7_8 && miles) {
|
||||
// this treadmill sends the speed in miles!
|
||||
speed /= miles_conversion;
|
||||
@@ -1888,14 +1900,16 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totalDistance) {
|
||||
// ignoring the distance, because it's a total life odometer
|
||||
// Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
// (uint32_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint32_t)((uint8_t)newValue.at(index)))) / 1000.0;
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
}
|
||||
index += 3;
|
||||
}
|
||||
// else
|
||||
{
|
||||
if (firstDistanceCalculated && !isPaused())
|
||||
if (firstDistanceCalculated && !isPaused() && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
distanceEval = true;
|
||||
@@ -1904,7 +1918,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
emit debug(QStringLiteral("Current Distance: ") + QString::number(Distance.value()));
|
||||
|
||||
if (Flags.inclination) {
|
||||
if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill)
|
||||
if(domyos_treadmill_ts100) {
|
||||
// Domyos TS100 has a fixed 15° inclination
|
||||
Inclination = 15;
|
||||
} else if(!tunturi_t60_treadmill && !ICONCEPT_FTMS_treadmill && !T01)
|
||||
parseInclination(treadmillInclinationOverride((double)(
|
||||
(int16_t)(
|
||||
((int16_t)(int8_t)newValue.at(index + 1) << 8) |
|
||||
@@ -1912,7 +1929,7 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
)
|
||||
) /
|
||||
10.0));
|
||||
else if(ICONCEPT_FTMS_treadmill) {
|
||||
else if(ICONCEPT_FTMS_treadmill || T01) {
|
||||
uint8_t val1 = (uint8_t)newValue.at(index);
|
||||
uint8_t val2 = (uint8_t)newValue.at(index + 1);
|
||||
if(val1 == 0x3C && val2 == 0x00) {
|
||||
@@ -1951,6 +1968,10 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
index += 4; // the ramo value is useless
|
||||
emit debug(QStringLiteral("Current Inclination: ") + QString::number(Inclination.value()));
|
||||
} else if(domyos_treadmill_ts100) {
|
||||
// Domyos TS100 has a fixed 15° inclination (no inclination flag in 2ACD)
|
||||
Inclination = 15;
|
||||
emit debug(QStringLiteral("Current Inclination (TS100 fixed): ") + QString::number(Inclination.value()));
|
||||
}
|
||||
|
||||
if (Flags.elevation) {
|
||||
@@ -2094,13 +2115,20 @@ void horizontreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
|
||||
}
|
||||
|
||||
if (Flags.totDistance && newValue.length() > index + 2) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
if (treadmill_direct_distance) {
|
||||
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
|
||||
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
|
||||
(uint32_t)((uint8_t)newValue.at(index)))) /
|
||||
1000.0;
|
||||
} else {
|
||||
if (firstDistanceCalculated)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
}
|
||||
index += 3;
|
||||
distanceEval = true;
|
||||
} else {
|
||||
if (firstDistanceCalculated)
|
||||
if (firstDistanceCalculated && !treadmill_direct_distance)
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
|
||||
distanceEval = true;
|
||||
@@ -2635,6 +2663,10 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
} else if ((device.name().toUpper().startsWith("DOMYOS"))) {
|
||||
qDebug() << QStringLiteral("DOMYOS found");
|
||||
DOMYOS = true;
|
||||
domyos_treadmill_ts100 = settings.value(QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100).toBool();
|
||||
if(domyos_treadmill_ts100) {
|
||||
qDebug() << QStringLiteral("Domyos TS100 mode ON - Fixed 15° inclination");
|
||||
}
|
||||
} else if ((device.name().toUpper().startsWith(QStringLiteral("BFX_T9_")))) {
|
||||
qDebug() << QStringLiteral("BOWFLEX T9 found");
|
||||
BOWFLEX_T9 = true;
|
||||
@@ -2666,6 +2698,9 @@ void horizontreadmill::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << QStringLiteral("TM6500 treadmill found");
|
||||
TM6500 = true;
|
||||
minInclination = -3.0;
|
||||
} else if (device.name().toUpper().startsWith(QStringLiteral("WT")) && device.name().length() == 5) {
|
||||
qDebug() << QStringLiteral("WT treadmill found");
|
||||
WT_TREADMILL = true;
|
||||
}
|
||||
|
||||
if (device.name().toUpper().startsWith(QStringLiteral("TRX3500"))) {
|
||||
|
||||
@@ -106,6 +106,7 @@ class horizontreadmill : public treadmill {
|
||||
bool ICONCEPT_FTMS_treadmill = false;
|
||||
bool iconcept_ftms_treadmill_inclination_table = false;
|
||||
bool DOMYOS = false;
|
||||
bool domyos_treadmill_ts100 = false;
|
||||
bool SW_TREADMILL = false;
|
||||
bool BOWFLEX_T9 = false;
|
||||
bool YPOO_MINI_PRO = false;
|
||||
@@ -118,6 +119,7 @@ class horizontreadmill : public treadmill {
|
||||
bool T01 = false;
|
||||
bool TM4800 = false;
|
||||
bool TM6500 = false;
|
||||
bool WT_TREADMILL = false;
|
||||
|
||||
void testProfileCRC();
|
||||
void updateProfileCRC();
|
||||
|
||||
@@ -412,6 +412,38 @@ void kingsmithr2treadmill::characteristicChanged(const QLowEnergyCharacteristic
|
||||
}
|
||||
if (lastRunState != runState) {
|
||||
lastRunState = runState;
|
||||
|
||||
// Only handle hardware buttons if setting is enabled
|
||||
QSettings settingsForHW;
|
||||
if (settingsForHW.value(QZSettings::kingsmith_r2_enable_hw_buttons,
|
||||
QZSettings::default_kingsmith_r2_enable_hw_buttons).toBool()) {
|
||||
|
||||
// Connection packet check: runState=0 + controlMode=1
|
||||
bool isConnectionPacket = (runState == STOP) && (controlMode == MANUAL) && !initDone;
|
||||
|
||||
if (runState == START) {
|
||||
emit debug(QStringLiteral("start button pressed on treadmill!"));
|
||||
emit buttonHWStart();
|
||||
} else if (runState == STOP && !isConnectionPacket) {
|
||||
emit debug(QStringLiteral("pause button pressed on treadmill!"));
|
||||
emit buttonHWPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for real stop: paused (from bluetoothdevice) + metrics reset + has distance
|
||||
// Only if setting is enabled
|
||||
QSettings settingsForStopCheck;
|
||||
if (settingsForStopCheck.value(QZSettings::kingsmith_r2_enable_hw_buttons,
|
||||
QZSettings::default_kingsmith_r2_enable_hw_buttons).toBool() &&
|
||||
paused) {
|
||||
|
||||
if (props.value("RunningTotalTime", -1) == 0 &&
|
||||
props.value("RunningSteps", -1) == 0 &&
|
||||
Distance.value() > 0) {
|
||||
emit debug(QStringLiteral("stop button pressed on treadmill!"));
|
||||
emit buttonHWStop();
|
||||
}
|
||||
}
|
||||
firstCharacteristicChanged = false;
|
||||
}
|
||||
|
||||
353
src/devices/mobirower/mobirower.cpp
Normal file
353
src/devices/mobirower/mobirower.cpp
Normal file
@@ -0,0 +1,353 @@
|
||||
#include "mobirower.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "keepawakehelper.h"
|
||||
#endif
|
||||
#include "virtualdevices/virtualbike.h"
|
||||
#include "virtualdevices/virtualrower.h"
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMetaEnum>
|
||||
#include <QSettings>
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
|
||||
#endif
|
||||
|
||||
mobirower::mobirower(bool noWriteResistance, bool noHeartService) {
|
||||
#ifdef Q_OS_IOS
|
||||
QZ_EnableDiscoveryCharsAndDescripttors = true;
|
||||
#endif
|
||||
m_watt.setType(metric::METRIC_WATT, deviceType());
|
||||
Speed.setType(metric::METRIC_SPEED);
|
||||
refresh = new QTimer(this);
|
||||
this->noWriteResistance = noWriteResistance;
|
||||
this->noHeartService = noHeartService;
|
||||
initDone = false;
|
||||
connect(refresh, &QTimer::timeout, this, &mobirower::update);
|
||||
refresh->start(200ms);
|
||||
}
|
||||
|
||||
void mobirower::update() {
|
||||
if (m_control == nullptr)
|
||||
return;
|
||||
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
|
||||
gattCommunicationChannelService && gattNotifyCharacteristic.isValid() && initDone) {
|
||||
update_metrics(true, watts());
|
||||
|
||||
if (requestStart != -1) {
|
||||
qDebug() << QStringLiteral("starting...");
|
||||
requestStart = -1;
|
||||
emit bikeStarted();
|
||||
}
|
||||
if (requestStop != -1) {
|
||||
qDebug() << QStringLiteral("stopping...");
|
||||
requestStop = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::serviceDiscovered(const QBluetoothUuid &gatt) {
|
||||
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
|
||||
}
|
||||
|
||||
void mobirower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
|
||||
Q_UNUSED(characteristic);
|
||||
QSettings settings;
|
||||
QString heartRateBeltName =
|
||||
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
|
||||
|
||||
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
|
||||
|
||||
// Validate packet: 13 bytes, starts with 0xab 0x04
|
||||
if (newValue.length() < 13 ||
|
||||
(uint8_t)newValue.at(0) != 0xab ||
|
||||
(uint8_t)newValue.at(1) != 0x04) {
|
||||
qDebug() << QStringLiteral("Invalid packet format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse power from bytes 9-10 (big-endian uint16)
|
||||
uint16_t power = ((uint8_t)newValue.at(9) << 8) | (uint8_t)newValue.at(10);
|
||||
|
||||
// Parse stroke count from bytes 11-12 (big-endian uint16)
|
||||
uint16_t strokeCount = ((uint8_t)newValue.at(11) << 8) | (uint8_t)newValue.at(12);
|
||||
|
||||
// Calculate cadence from stroke delta
|
||||
double timeDelta = lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime());
|
||||
if (timeDelta > 0 && strokeCount >= lastStrokeCount) {
|
||||
uint16_t strokeDelta = strokeCount - lastStrokeCount;
|
||||
// Convert to strokes per minute (SPM)
|
||||
double cadence = (strokeDelta / (timeDelta / 60000.0));
|
||||
if (cadence < 200) { // sanity check
|
||||
Cadence = cadence;
|
||||
}
|
||||
}
|
||||
lastStrokeCount = strokeCount;
|
||||
|
||||
m_watt = power;
|
||||
StrokesCount = strokeCount;
|
||||
|
||||
// Calculate speed from strokes (standard rower formula)
|
||||
// Using a simplified formula: speed in km/h derived from cadence
|
||||
if (Cadence.value() > 0) {
|
||||
// Typical rower: ~10m per stroke at normal pace
|
||||
// Speed = (cadence * meters_per_stroke * 60) / 1000 for km/h
|
||||
double metersPerStroke = 8.0; // approximate
|
||||
Speed = (Cadence.value() * metersPerStroke * 60.0) / 1000.0;
|
||||
} else {
|
||||
Speed = 0;
|
||||
}
|
||||
|
||||
StrokesLength =
|
||||
((Speed.value() / 60.0) * 1000.0) /
|
||||
Cadence.value(); // this is just to fill the tile
|
||||
|
||||
if (watts())
|
||||
KCal +=
|
||||
((((0.048 * ((double)watts()) + 1.19) *
|
||||
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
|
||||
200.0) /
|
||||
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
|
||||
QDateTime::currentDateTime()))));
|
||||
Distance += ((Speed.value() / 3600000.0) *
|
||||
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
|
||||
|
||||
if (Cadence.value() > 0) {
|
||||
CrankRevs++;
|
||||
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
|
||||
}
|
||||
|
||||
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
|
||||
Heart = (uint8_t)KeepAwakeHelper::heart();
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
|
||||
update_hr_from_external();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
if (ios_peloton_workaround && cadence && !virtual_device_rower && h && firstStateChanged) {
|
||||
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
|
||||
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
qDebug() << QStringLiteral("Current Power: ") + QString::number(m_watt.value());
|
||||
qDebug() << QStringLiteral("Current Stroke Count: ") + QString::number(StrokesCount.value());
|
||||
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
|
||||
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
|
||||
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
|
||||
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::stateChanged(QLowEnergyService::ServiceState state) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
|
||||
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
|
||||
|
||||
if (state == QLowEnergyService::ServiceDiscovered) {
|
||||
// Find the notify characteristic (0xffe4)
|
||||
QBluetoothUuid notifyCharUuid((quint16)0xffe4);
|
||||
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(notifyCharUuid);
|
||||
|
||||
if (!gattNotifyCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattNotifyCharacteristic not valid, trying to find by properties");
|
||||
auto characteristics_list = gattCommunicationChannelService->characteristics();
|
||||
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
|
||||
qDebug() << QStringLiteral("c -> ") << c.uuid() << c.properties();
|
||||
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
|
||||
gattNotifyCharacteristic = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!gattNotifyCharacteristic.isValid()) {
|
||||
qDebug() << QStringLiteral("gattNotifyCharacteristic still not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
// establish hook into notifications
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
|
||||
&mobirower::characteristicChanged);
|
||||
connect(gattCommunicationChannelService,
|
||||
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
|
||||
this, &mobirower::errorService);
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
|
||||
&mobirower::descriptorWritten);
|
||||
|
||||
// ******************************************* virtual bike/rower init *************************************
|
||||
if (!firstStateChanged && !this->hasVirtualDevice()
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
&& !h
|
||||
#endif
|
||||
#endif
|
||||
) {
|
||||
QSettings settings;
|
||||
bool virtual_device_enabled =
|
||||
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
|
||||
bool virtual_device_rower =
|
||||
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
bool cadence =
|
||||
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
|
||||
bool ios_peloton_workaround =
|
||||
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
|
||||
if (ios_peloton_workaround && cadence && !virtual_device_rower) {
|
||||
qDebug() << "ios_peloton_workaround activated!";
|
||||
h = new lockscreen();
|
||||
h->virtualbike_ios();
|
||||
} else
|
||||
#endif
|
||||
#endif
|
||||
if (virtual_device_enabled) {
|
||||
if (!virtual_device_rower) {
|
||||
qDebug() << QStringLiteral("creating virtual bike interface...");
|
||||
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("creating virtual rower interface...");
|
||||
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
|
||||
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
firstStateChanged = 1;
|
||||
// ********************************************************************************************************
|
||||
|
||||
QByteArray descriptor;
|
||||
descriptor.append((char)0x01);
|
||||
descriptor.append((char)0x00);
|
||||
gattCommunicationChannelService->writeDescriptor(
|
||||
gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
void mobirower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
|
||||
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
|
||||
|
||||
initDone = true;
|
||||
emit connectedAndDiscovered();
|
||||
}
|
||||
|
||||
void mobirower::serviceScanDone(void) {
|
||||
qDebug() << QStringLiteral("serviceScanDone");
|
||||
|
||||
// Service UUID 0xffe0
|
||||
QBluetoothUuid serviceUuid((quint16)0xffe0);
|
||||
|
||||
gattCommunicationChannelService = m_control->createServiceObject(serviceUuid);
|
||||
if (!gattCommunicationChannelService) {
|
||||
qDebug() << "service 0xffe0 not found, trying to find any service";
|
||||
auto services = m_control->services();
|
||||
for (const QBluetoothUuid &s : qAsConst(services)) {
|
||||
qDebug() << QStringLiteral("service ") << s.toString();
|
||||
}
|
||||
if (!services.isEmpty()) {
|
||||
gattCommunicationChannelService = m_control->createServiceObject(services.first());
|
||||
}
|
||||
}
|
||||
|
||||
if (!gattCommunicationChannelService) {
|
||||
qDebug() << "no service found";
|
||||
return;
|
||||
}
|
||||
|
||||
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &mobirower::stateChanged);
|
||||
gattCommunicationChannelService->discoverDetails();
|
||||
}
|
||||
|
||||
void mobirower::errorService(QLowEnergyService::ServiceError err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
|
||||
qDebug() << QStringLiteral("mobirower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
|
||||
m_control->errorString();
|
||||
}
|
||||
|
||||
void mobirower::error(QLowEnergyController::Error err) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
|
||||
qDebug() << "mobirower::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString();
|
||||
}
|
||||
|
||||
void mobirower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||
qDebug() << "Found new device: " + device.name() + " (" + device.address().toString() + ')';
|
||||
bluetoothDevice = device;
|
||||
|
||||
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
|
||||
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &mobirower::serviceDiscovered);
|
||||
connect(m_control, &QLowEnergyController::discoveryFinished, this, &mobirower::serviceScanDone);
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, &mobirower::error);
|
||||
connect(m_control, &QLowEnergyController::stateChanged, this, &mobirower::controllerStateChanged);
|
||||
|
||||
connect(m_control,
|
||||
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
|
||||
this, [this](QLowEnergyController::Error error) {
|
||||
Q_UNUSED(error);
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Cannot connect to remote device.");
|
||||
emit disconnected();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::connected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("Controller connected. Search services...");
|
||||
m_control->discoverServices();
|
||||
});
|
||||
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
|
||||
Q_UNUSED(this);
|
||||
qDebug() << QStringLiteral("LowEnergy controller disconnected");
|
||||
emit disconnected();
|
||||
});
|
||||
|
||||
// Connect
|
||||
m_control->connectToDevice();
|
||||
return;
|
||||
}
|
||||
|
||||
bool mobirower::connected() {
|
||||
if (!m_control) {
|
||||
return false;
|
||||
}
|
||||
return m_control->state() == QLowEnergyController::DiscoveredState;
|
||||
}
|
||||
|
||||
uint16_t mobirower::watts() {
|
||||
return m_watt.value();
|
||||
}
|
||||
|
||||
void mobirower::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
}
|
||||
82
src/devices/mobirower/mobirower.h
Normal file
82
src/devices/mobirower/mobirower.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#ifndef MOBIROWER_H
|
||||
#define MOBIROWER_H
|
||||
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QtBluetooth/qlowenergyadvertisingdata.h>
|
||||
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristic.h>
|
||||
#include <QtBluetooth/qlowenergycharacteristicdata.h>
|
||||
#include <QtBluetooth/qlowenergycontroller.h>
|
||||
#include <QtBluetooth/qlowenergydescriptordata.h>
|
||||
#include <QtBluetooth/qlowenergyservice.h>
|
||||
#include <QtBluetooth/qlowenergyservicedata.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#else
|
||||
#include <QtGui/qguiapplication.h>
|
||||
#endif
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qmutex.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "rower.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/lockscreen.h"
|
||||
#endif
|
||||
|
||||
class mobirower : public rower {
|
||||
Q_OBJECT
|
||||
public:
|
||||
mobirower(bool noWriteResistance, bool noHeartService);
|
||||
bool connected() override;
|
||||
|
||||
private:
|
||||
void startDiscover();
|
||||
uint16_t watts() override;
|
||||
|
||||
QTimer *refresh;
|
||||
|
||||
QLowEnergyService *gattCommunicationChannelService = nullptr;
|
||||
QLowEnergyCharacteristic gattNotifyCharacteristic;
|
||||
|
||||
uint8_t firstStateChanged = 0;
|
||||
uint16_t lastStrokeCount = 0;
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
|
||||
bool initDone = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
Q_SIGNALS:
|
||||
void disconnected();
|
||||
|
||||
public slots:
|
||||
void deviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||
|
||||
private slots:
|
||||
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
|
||||
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
|
||||
void stateChanged(QLowEnergyService::ServiceState state);
|
||||
void controllerStateChanged(QLowEnergyController::ControllerState state);
|
||||
|
||||
void serviceDiscovered(const QBluetoothUuid &gatt);
|
||||
void serviceScanDone(void);
|
||||
void update();
|
||||
void error(QLowEnergyController::Error err);
|
||||
void errorService(QLowEnergyService::ServiceError);
|
||||
};
|
||||
|
||||
#endif // MOBIROWER_H
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <chrono>
|
||||
#include <math.h>
|
||||
#include <qmath.h>
|
||||
#include "homeform.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -608,6 +609,89 @@ void nordictrackelliptical::forceResistance(resistance_t requestResistance) {
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackelliptical::se7i_send_next_frame() {
|
||||
if (!nordictrack_se7i) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit debug(QStringLiteral("se7i_send_next_frame: state = ") + QString::number(se7i_init_state));
|
||||
|
||||
switch (se7i_init_state) {
|
||||
case 0: {
|
||||
// Frame 1: se7i_initData11, se7i_initData12, se7i_initData13 (ends with 0xff)
|
||||
uint8_t se7i_initData11[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x06, 0x28, 0x90, 0x04, 0x00, 0x0d, 0x68, 0xc9, 0x28, 0x95, 0xf0, 0x69, 0xc0, 0x3d};
|
||||
uint8_t se7i_initData12[] = {0x01, 0x12, 0xa8, 0x19, 0x88, 0xf5, 0x60, 0xf9, 0x70, 0xcd, 0x48, 0xc9, 0x48, 0xf5, 0x70, 0xe9, 0x60, 0x1d, 0x88, 0x39};
|
||||
uint8_t se7i_initData13[] = {0xff, 0x08, 0xa8, 0x55, 0xc0, 0x80, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_initData11, sizeof(se7i_initData11), QStringLiteral("se7i_frame1_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_initData12, sizeof(se7i_initData12), QStringLiteral("se7i_frame1_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_initData13, sizeof(se7i_initData13), QStringLiteral("se7i_frame1_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 1;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 1 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// Frame 2: se7i_initData14, se7i_initData15, se7i_initData16 (ends with 0xff)
|
||||
uint8_t se7i_initData14[] = {0xfe, 0x02, 0x19, 0x03};
|
||||
uint8_t se7i_initData15[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x06, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData16[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_initData14, sizeof(se7i_initData14), QStringLiteral("se7i_frame2_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_initData15, sizeof(se7i_initData15), QStringLiteral("se7i_frame2_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_initData16, sizeof(se7i_initData16), QStringLiteral("se7i_frame2_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 2;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 2 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// Frame 3: se7i_init_020, se7i_init_021, se7i_init_022 (ends with 0xff)
|
||||
uint8_t se7i_init_020[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t se7i_init_021[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x06, 0x13, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_init_022[] = {0xff, 0x05, 0x00, 0x80, 0x01, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_init_020, sizeof(se7i_init_020), QStringLiteral("se7i_frame3_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_init_021, sizeof(se7i_init_021), QStringLiteral("se7i_frame3_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_init_022, sizeof(se7i_init_022), QStringLiteral("se7i_frame3_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 3;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 3 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// Frame 4: se7i_init_023, se7i_init_024, se7i_init_025 (ends with 0xff)
|
||||
uint8_t se7i_init_023[] = {0xfe, 0x02, 0x17, 0x03};
|
||||
uint8_t se7i_init_024[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x13, 0x06, 0x13, 0x02, 0x00, 0x0d, 0x00, 0x10, 0x00, 0xd8, 0x1c, 0x4c, 0x00, 0x00, 0xe0};
|
||||
uint8_t se7i_init_025[] = {0xff, 0x05, 0x00, 0x00, 0x00, 0x10, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeCharacteristic(se7i_init_023, sizeof(se7i_init_023), QStringLiteral("se7i_frame4_pkt1"), false, false);
|
||||
writeCharacteristic(se7i_init_024, sizeof(se7i_init_024), QStringLiteral("se7i_frame4_pkt2"), false, false);
|
||||
writeCharacteristic(se7i_init_025, sizeof(se7i_init_025), QStringLiteral("se7i_frame4_pkt3_FF"), false, false);
|
||||
|
||||
se7i_waiting_for_response = true;
|
||||
se7i_init_state = 4;
|
||||
emit debug(QStringLiteral("se7i: Sent frame 4 (3 packets), waiting for response with 0xFF marker"));
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// Initialization complete!
|
||||
emit debug(QStringLiteral("se7i: Initialization completed successfully!"));
|
||||
if(homeform::singleton())
|
||||
homeform::singleton()->setToastRequested("SE7i init completed!");
|
||||
initDone = true;
|
||||
se7i_waiting_for_response = false;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
emit debug(QStringLiteral("se7i_send_next_frame: invalid state ") + QString::number(se7i_init_state));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void nordictrackelliptical::update() {
|
||||
if (m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
emit disconnected();
|
||||
@@ -1016,6 +1100,21 @@ void nordictrackelliptical::characteristicChanged(const QLowEnergyCharacteristic
|
||||
|
||||
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
|
||||
|
||||
// SE7i frame-based protocol: check for 0xFF marker indicating end of response frame
|
||||
if (nordictrack_se7i && se7i_waiting_for_response && newValue.length() > 0) {
|
||||
if ((uint8_t)newValue.at(0) == 0xFF) {
|
||||
emit debug(QStringLiteral("SE7i: Received 0xFF marker - end of response frame detected"));
|
||||
se7i_waiting_for_response = false;
|
||||
// Schedule next frame send in the next event loop iteration to avoid reentrancy issues
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
se7i_send_next_frame();
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
emit debug(QStringLiteral("SE7i: Received packet (waiting for 0xFF): ") + QString::number((uint8_t)newValue.at(0), 16));
|
||||
}
|
||||
}
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
// SE7i Speed and Cadence parsing (Type 0x01 packets with byte[4]=0x46)
|
||||
@@ -1298,66 +1397,19 @@ void nordictrackelliptical::btinit() {
|
||||
writeCharacteristic(initData9, sizeof(initData9), QStringLiteral("init"), false, false);
|
||||
QThread::msleep(400);
|
||||
if (nordictrack_se7i) {
|
||||
// NordicTrack Elliptical SE7i initialization (19 packets: pkt944 to pkt1020)
|
||||
max_resistance = 22;
|
||||
max_inclination = 20;
|
||||
|
||||
uint8_t se7i_initData1[] = {0xfe, 0x02, 0x08, 0x02};
|
||||
uint8_t se7i_initData2[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x81, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData3[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x06, 0x04, 0x80, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData4[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x06, 0x04, 0x88, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t se7i_initData5[] = {0xfe, 0x02, 0x0b, 0x02}; // pkt972
|
||||
uint8_t se7i_initData6[] = {0xff, 0x0b, 0x02, 0x04, 0x02, 0x07, 0x02, 0x07, 0x82, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt975
|
||||
uint8_t se7i_initData7[] = {0xfe, 0x02, 0x0a, 0x02}; // pkt982
|
||||
uint8_t se7i_initData8[] = {0xff, 0x0a, 0x02, 0x04, 0x02, 0x06, 0x02, 0x06, 0x84, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt985
|
||||
uint8_t se7i_initData9[] = {0xff, 0x08, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x95, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt994
|
||||
uint8_t se7i_initData10[] = {0xfe, 0x02, 0x2c, 0x04}; // pkt1000
|
||||
uint8_t se7i_initData11[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x28, 0x06, 0x28, 0x90, 0x04, 0x00, 0x0d, 0x68, 0xc9, 0x28, 0x95, 0xf0, 0x69, 0xc0, 0x3d}; // pkt1003
|
||||
uint8_t se7i_initData12[] = {0x01, 0x12, 0xa8, 0x19, 0x88, 0xf5, 0x60, 0xf9, 0x70, 0xcd, 0x48, 0xc9, 0x48, 0xf5, 0x70, 0xe9, 0x60, 0x1d, 0x88, 0x39}; // pkt1006
|
||||
uint8_t se7i_initData13[] = {0xff, 0x08, 0xa8, 0x55, 0xc0, 0x80, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1009
|
||||
uint8_t se7i_initData14[] = {0xfe, 0x02, 0x19, 0x03}; // pkt1014
|
||||
uint8_t se7i_initData15[] = {0x00, 0x12, 0x02, 0x04, 0x02, 0x15, 0x06, 0x15, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1017
|
||||
uint8_t se7i_initData16[] = {0xff, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // pkt1020
|
||||
// Initialize frame-based communication state machine
|
||||
se7i_init_state = 0;
|
||||
se7i_waiting_for_response = false;
|
||||
|
||||
int sleepms = 400;
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData2, sizeof(se7i_initData2), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData3, sizeof(se7i_initData3), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData4, sizeof(se7i_initData4), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData5, sizeof(se7i_initData5), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData6, sizeof(se7i_initData6), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData7, sizeof(se7i_initData7), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData8, sizeof(se7i_initData8), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData1, sizeof(se7i_initData1), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData9, sizeof(se7i_initData9), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData10, sizeof(se7i_initData10), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData11, sizeof(se7i_initData11), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData12, sizeof(se7i_initData12), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData13, sizeof(se7i_initData13), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData14, sizeof(se7i_initData14), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData15, sizeof(se7i_initData15), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
writeCharacteristic(se7i_initData16, sizeof(se7i_initData16), QStringLiteral("init"), false, true);
|
||||
QThread::msleep(sleepms);
|
||||
// Start frame-based initialization sequence
|
||||
emit debug(QStringLiteral("SE7i: Starting frame-based initialization (no sleep mode)"));
|
||||
se7i_send_next_frame();
|
||||
|
||||
// Do NOT set initDone here - it will be set when all frames complete
|
||||
return;
|
||||
} else if (nordictrack_elliptical_c7_5) {
|
||||
max_resistance = 22;
|
||||
max_inclination = 20;
|
||||
|
||||
@@ -86,6 +86,11 @@ class nordictrackelliptical : public elliptical {
|
||||
bool nordictrack_elliptical_c7_5 = false;
|
||||
bool nordictrack_se7i = false;
|
||||
|
||||
// SE7i frame-based initialization state management
|
||||
int se7i_init_state = 0;
|
||||
bool se7i_waiting_for_response = false;
|
||||
void se7i_send_next_frame();
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
lockscreen *h = 0;
|
||||
#endif
|
||||
|
||||
@@ -654,7 +654,16 @@ void strydrunpowersensor::serviceScanDone(void) {
|
||||
emit debug(QStringLiteral("serviceScanDone"));
|
||||
|
||||
auto services_list = m_control->services();
|
||||
bool isZwiftPod = bluetoothDevice.name().contains(QStringLiteral("Zwift RunPod"), Qt::CaseInsensitive);
|
||||
|
||||
for (const QBluetoothUuid &s : qAsConst(services_list)) {
|
||||
// For Zwift RunPod, skip both fff0 and ffc0 services that cause discovery issues
|
||||
if (isZwiftPod && (s.toString() == QStringLiteral("{0000fff0-0000-1000-8000-00805f9b34fb}") ||
|
||||
s.toString() == QStringLiteral("{f000ffc0-0451-4000-b000-000000000000}"))) {
|
||||
qDebug() << QStringLiteral("Skipping problematic services for Zwift RunPod:") << s.toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
gattCommunicationChannelService.append(m_control->createServiceObject(s));
|
||||
connect(gattCommunicationChannelService.constLast(), &QLowEnergyService::stateChanged, this,
|
||||
&strydrunpowersensor::stateChanged);
|
||||
|
||||
@@ -66,6 +66,9 @@ class treadmill : public bluetoothdevice {
|
||||
|
||||
signals:
|
||||
void tapeStarted();
|
||||
void buttonHWStart(); // Physical start button pressed on hardware
|
||||
void buttonHWPause(); // Physical pause button pressed on hardware
|
||||
void buttonHWStop(); // Physical stop button pressed on hardware
|
||||
|
||||
protected:
|
||||
volatile double requestSpeed = -1;
|
||||
|
||||
@@ -84,6 +84,29 @@ void trxappgateusbelliptical::update() {
|
||||
QSettings settings;
|
||||
update_metrics(true, watts());
|
||||
|
||||
// Restore resistance after reconnection and init
|
||||
if (needsResistanceRestore && lastResistanceBeforeDisconnection > 0) {
|
||||
qDebug() << QStringLiteral("Restoring resistance after reconnection:") << lastResistanceBeforeDisconnection;
|
||||
forceResistance(lastResistanceBeforeDisconnection);
|
||||
needsResistanceRestore = false;
|
||||
lastResistanceBeforeDisconnection = -1;
|
||||
}
|
||||
|
||||
// Calculate time since last valid packet
|
||||
qint64 msSinceLastValidPacket = lastValidPacketTime.msecsTo(QDateTime::currentDateTime());
|
||||
|
||||
// If we haven't received a valid packet for more than 5 seconds, reinitialize
|
||||
if (msSinceLastValidPacket > 5000) {
|
||||
qDebug() << QStringLiteral("NO VALID PACKETS for") << (msSinceLastValidPacket / 1000.0)
|
||||
<< QStringLiteral("seconds. Reinitializing connection...");
|
||||
|
||||
// Reset timer
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
|
||||
m_control->disconnectFromDevice();
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
if (requestResistance != -1) {
|
||||
if (requestResistance < 1)
|
||||
@@ -191,10 +214,24 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
|
||||
lastPacket = newValue;
|
||||
|
||||
if(newValue.length() != 21) {
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
|
||||
// Check for invalid packet length first
|
||||
bool isValidPacket = (newValue.length() == 21);
|
||||
|
||||
if (!isValidPacket) {
|
||||
// Invalid packet length - log and return
|
||||
qDebug() << QStringLiteral("Invalid packet length:") << newValue.length();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log controller errors but don't block processing of valid packets
|
||||
bool hasError = (m_control->error() != QLowEnergyController::NoError);
|
||||
if (hasError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
// Continue processing - the packet is still valid
|
||||
}
|
||||
|
||||
Resistance = newValue.at(18) - 1;
|
||||
Speed = GetSpeedFromPacket(newValue);
|
||||
Cadence = (GetCadenceFromPacket(newValue) * cadence_gain) + cadence_offset;
|
||||
@@ -227,10 +264,6 @@ void trxappgateusbelliptical::characteristicChanged(const QLowEnergyCharacterist
|
||||
emit debug(QStringLiteral("Current Calculate Distance: ") + QString::number(Distance.value()));
|
||||
// debug("Current Distance: " + QString::number(distance));
|
||||
emit debug(QStringLiteral("Current Watt: ") + QString::number(watts()));
|
||||
|
||||
if (m_control->error() != QLowEnergyController::NoError) {
|
||||
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void trxappgateusbelliptical::btinit() {
|
||||
@@ -497,9 +530,26 @@ bool trxappgateusbelliptical::connected() {
|
||||
void trxappgateusbelliptical::controllerStateChanged(QLowEnergyController::ControllerState state) {
|
||||
qDebug() << QStringLiteral("controllerStateChanged") << state;
|
||||
if (state == QLowEnergyController::UnconnectedState && m_control) {
|
||||
qDebug() << QStringLiteral("trying to connect back again...");
|
||||
qDebug() << QStringLiteral("trying to connect back again in 3 seconds...");
|
||||
|
||||
// Save current resistance before disconnection
|
||||
if (Resistance.value() > 0) {
|
||||
lastResistanceBeforeDisconnection = Resistance.value();
|
||||
needsResistanceRestore = true;
|
||||
qDebug() << QStringLiteral("Saved resistance before disconnection:") << lastResistanceBeforeDisconnection;
|
||||
}
|
||||
|
||||
initDone = false;
|
||||
m_control->connectToDevice();
|
||||
|
||||
// Schedule reconnection after 3 seconds
|
||||
QTimer::singleShot(3000, this, [this]() {
|
||||
if (m_control && m_control->state() == QLowEnergyController::UnconnectedState) {
|
||||
qDebug() << QStringLiteral("Reconnection timer fired, attempting to reconnect...");
|
||||
// Reset the last valid packet timer
|
||||
lastValidPacketTime = QDateTime::currentDateTime();
|
||||
m_control->connectToDevice();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ class trxappgateusbelliptical : public elliptical {
|
||||
|
||||
uint8_t sec1Update = 0;
|
||||
QByteArray lastPacket;
|
||||
QDateTime lastValidPacketTime = QDateTime::currentDateTime();
|
||||
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
|
||||
uint8_t firstStateChanged = 0;
|
||||
int8_t bikeResistanceOffset = 4;
|
||||
@@ -69,6 +70,9 @@ class trxappgateusbelliptical : public elliptical {
|
||||
bool initDone = false;
|
||||
bool initRequest = false;
|
||||
|
||||
resistance_t lastResistanceBeforeDisconnection = -1;
|
||||
bool needsResistanceRestore = false;
|
||||
|
||||
bool noWriteResistance = false;
|
||||
bool noHeartService = false;
|
||||
|
||||
|
||||
@@ -128,8 +128,10 @@ void FitDatabaseProcessor::processDirectory(const QString& dirPath) {
|
||||
|
||||
void FitDatabaseProcessor::processFile(const QString& filePath) {
|
||||
if (!db.isOpen()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
if (!initializeDatabase()) {
|
||||
emit error("Failed to initialize database for single file processing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processFitFile(filePath)) {
|
||||
|
||||
218
src/homeform.cpp
218
src/homeform.cpp
@@ -1471,6 +1471,12 @@ void homeform::trainProgramSignals() {
|
||||
((elliptical *)bluetoothManager->device()), &elliptical::changeRequestedPelotonResistance);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::tapeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStart, this,
|
||||
&homeform::StartFromDevice);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWPause, this,
|
||||
&homeform::PauseFromDevice);
|
||||
disconnect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStop, this,
|
||||
&homeform::StopFromDevice);
|
||||
disconnect(((bike *)bluetoothManager->device()), &bike::bikeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
disconnect(trainProgram, &trainprogram::changeGeoPosition, bluetoothManager->device(),
|
||||
@@ -1497,6 +1503,12 @@ void homeform::trainProgramSignals() {
|
||||
&treadmill::changeSpeedAndInclination);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::tapeStarted, trainProgram,
|
||||
&trainprogram::onTapeStarted);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStart, this,
|
||||
&homeform::StartFromDevice);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWPause, this,
|
||||
&homeform::PauseFromDevice);
|
||||
connect(((treadmill *)bluetoothManager->device()), &treadmill::buttonHWStop, this,
|
||||
&homeform::StopFromDevice);
|
||||
connect(trainProgram, &trainprogram::changePower, ((treadmill *)bluetoothManager->device()), &treadmill::changePower);
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
connect(trainProgram, &trainprogram::changeCadence, ((bike *)bluetoothManager->device()),
|
||||
@@ -5118,6 +5130,21 @@ void homeform::Start_inner(bool send_event_to_device) {
|
||||
}
|
||||
}
|
||||
|
||||
void homeform::StartFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical start button pressed on device");
|
||||
Start_inner(false); // false = don't send command back to device (it already started)
|
||||
}
|
||||
|
||||
void homeform::PauseFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical pause button pressed on device");
|
||||
Start_inner(false); // false = don't send command back to device
|
||||
}
|
||||
|
||||
void homeform::StopFromDevice() {
|
||||
qDebug() << QStringLiteral("Physical stop button pressed on device - stopping app");
|
||||
Stop();
|
||||
}
|
||||
|
||||
void homeform::StartRequested() {
|
||||
Start();
|
||||
m_stopRequested = false;
|
||||
@@ -5158,6 +5185,17 @@ void homeform::Stop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothManager->device()) {
|
||||
if (bluetoothManager->device()->deviceType() == TREADMILL) {
|
||||
QTime zero(0, 0, 0, 0);
|
||||
if (bluetoothManager->device()->currentSpeed().value() == 0.0 &&
|
||||
zero.secsTo(bluetoothManager->device()->elapsedTime()) == 0) {
|
||||
qDebug() << QStringLiteral("Stop pressed - nothing to do. Elapsed time is 0 and current speed is 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
// due to #857
|
||||
if (!settings.value(QZSettings::peloton_companion_workout_ocr, QZSettings::default_companion_peloton_workout_ocr)
|
||||
@@ -5169,16 +5207,6 @@ void homeform::Stop() {
|
||||
m_speech.say("Stop pressed");
|
||||
|
||||
if (bluetoothManager->device()) {
|
||||
|
||||
if (bluetoothManager->device()->deviceType() == TREADMILL) {
|
||||
QTime zero(0, 0, 0, 0);
|
||||
if (bluetoothManager->device()->currentSpeed().value() == 0.0 &&
|
||||
zero.secsTo(bluetoothManager->device()->elapsedTime()) == 0) {
|
||||
qDebug() << QStringLiteral("Stop pressed - nothing to do. Elapsed time is 0 and current speed is 0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothManager->device()->stop(false);
|
||||
}
|
||||
|
||||
@@ -7053,34 +7081,74 @@ void homeform::update() {
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
double step = 1;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableBySoftware();
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableByHardware();
|
||||
bool inclinationAvailable = ((bike*)bluetoothManager->device())->inclinationAvailableBySoftware();
|
||||
|
||||
if(ergMode) {
|
||||
step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
}
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
if(ergMode)
|
||||
// Use power control for bikes with erg mode support
|
||||
double step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone) && ((maxResistance >= currentResistance + step && !ergMode) || ergMode)) {
|
||||
if(ergMode)
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
} else if(inclinationAvailable) {
|
||||
// Use inclination control for bikes without erg mode but with inclination support (e.g., ftmsbike)
|
||||
double step = 0.5;
|
||||
double currentInclination = ((bike *)bluetoothManager->device())->currentInclination().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination - step, currentInclination - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone)) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to resistance control for bikes without erg mode or inclination
|
||||
double step = 1;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableBySoftware();
|
||||
if(ergMode) {
|
||||
step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
}
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
if (zone < ((uint8_t)currentHRZone)) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if (zone > ((uint8_t)currentHRZone) && ((maxResistance >= currentResistance + step && !ergMode) || ergMode)) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
} else if(trainprogram_pid_pushy) {
|
||||
pid_heart_zone_small_inc_counter++;
|
||||
if (pid_heart_zone_small_inc_counter > (5 * fabs(((float)zone) - currentHRZone))) {
|
||||
if(ergMode)
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
else
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance + step);
|
||||
pid_heart_zone_small_inc_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == ROWING) {
|
||||
@@ -7201,24 +7269,63 @@ void homeform::update() {
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == BIKE) {
|
||||
|
||||
const int step = 1;
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - currentResistance:") << currentResistance
|
||||
<< QStringLiteral("maxResistance:") << maxResistance;
|
||||
bool ergMode = ((bike*)bluetoothManager->device())->ergModeSupportedAvailableByHardware();
|
||||
bool inclinationAvailable = ((bike*)bluetoothManager->device())->inclinationAvailableBySoftware();
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << (currentResistance - step);
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s() &&
|
||||
currentResistance < maxResistance) {
|
||||
resistance_t newResistance = std::min(static_cast<resistance_t>(currentResistance + step), static_cast<resistance_t>(maxResistance));
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << newResistance;
|
||||
((bike *)bluetoothManager->device())->changeResistance(newResistance);
|
||||
if (ergMode) {
|
||||
// Use power control for bikes with erg mode support
|
||||
const int step = settings.value(QZSettings::pid_heart_zone_erg_mode_watt_step, QZSettings::default_pid_heart_zone_erg_mode_watt_step).toInt();
|
||||
double current_target_watt = ((bike *)bluetoothManager->device())->lastRequestedPower().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - ergMode enabled, currentPower:") << current_target_watt;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING power from")
|
||||
<< current_target_watt << QStringLiteral("to") << (current_target_watt - step);
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING power from")
|
||||
<< current_target_watt << QStringLiteral("to") << (current_target_watt + step);
|
||||
((bike *)bluetoothManager->device())->changePower(current_target_watt + step);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
} else if (inclinationAvailable) {
|
||||
// Use inclination control for bikes without erg mode but with inclination support (e.g., ftmsbike)
|
||||
const double step = 0.5;
|
||||
double currentInclination = ((bike *)bluetoothManager->device())->currentInclination().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - Using inclination control, currentInclination:") << currentInclination;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING inclination from")
|
||||
<< currentInclination << QStringLiteral("to") << (currentInclination - step);
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination - step, currentInclination - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING inclination from")
|
||||
<< currentInclination << QStringLiteral("to") << (currentInclination + step);
|
||||
((bike *)bluetoothManager->device())->changeInclination(currentInclination + step, currentInclination + step);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
const int step = 1;
|
||||
resistance_t currentResistance =
|
||||
((bike *)bluetoothManager->device())->currentResistance().value();
|
||||
qDebug() << QStringLiteral("BIKE PID HR - currentResistance:") << currentResistance
|
||||
<< QStringLiteral("maxResistance:") << maxResistance;
|
||||
|
||||
if (hrmax < bluetoothManager->device()->currentHeart().average20s()) {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR > HRmax, DECREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << (currentResistance - step);
|
||||
((bike *)bluetoothManager->device())->changeResistance(currentResistance - step);
|
||||
} else if (hrmin > bluetoothManager->device()->currentHeart().average20s() &&
|
||||
currentResistance < maxResistance) {
|
||||
resistance_t newResistance = std::min(static_cast<resistance_t>(currentResistance + step), static_cast<resistance_t>(maxResistance));
|
||||
qDebug() << QStringLiteral("BIKE PID HR - HR < HRmin, INCREASING resistance from")
|
||||
<< currentResistance << QStringLiteral("to") << newResistance;
|
||||
((bike *)bluetoothManager->device())->changeResistance(newResistance);
|
||||
} else {
|
||||
qDebug() << QStringLiteral("BIKE PID HR - No action taken (in zone or at limits)");
|
||||
}
|
||||
}
|
||||
} else if (bluetoothManager->device()->deviceType() == ROWING) {
|
||||
|
||||
@@ -7536,10 +7643,17 @@ void homeform::update() {
|
||||
}
|
||||
}
|
||||
|
||||
if(bluetoothManager->device()->currentSpeed().value() > 0 && !isinf(bluetoothManager->device()->currentSpeed().value()))
|
||||
bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0));
|
||||
|
||||
qDebug() << "Current Distance 1s:" << bluetoothManager->device()->currentDistance1s().value() << bluetoothManager->device()->currentSpeed().value() << watts;
|
||||
bool treadmill_direct_distance = settings.value(QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance).toBool();
|
||||
double distance1s = 0;
|
||||
if (treadmill_direct_distance) {
|
||||
distance1s = bluetoothManager->device()->odometer();
|
||||
} else {
|
||||
if(bluetoothManager->device()->currentSpeed().value() > 0 && !isinf(bluetoothManager->device()->currentSpeed().value()))
|
||||
bluetoothManager->device()->addCurrentDistance1s((bluetoothManager->device()->currentSpeed().value() / 3600.0));
|
||||
distance1s = bluetoothManager->device()->currentDistance1s().value();
|
||||
}
|
||||
|
||||
qDebug() << "Current Distance 1s:" << distance1s << bluetoothManager->device()->currentSpeed().value() << watts;
|
||||
|
||||
// Calculate current elapsed time in seconds
|
||||
uint32_t currentElapsedSeconds = bluetoothManager->device()->elapsedTime().second() +
|
||||
@@ -7561,7 +7675,7 @@ void homeform::update() {
|
||||
uint32_t lastRecordedTime = Session.last().elapsedTime;
|
||||
for (int i = 1; i <= missedSeconds; i++) {
|
||||
SessionLine gapFill(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, distance1s,
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
@@ -7596,7 +7710,7 @@ void homeform::update() {
|
||||
}
|
||||
|
||||
SessionLine s(
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, bluetoothManager->device()->currentDistance1s().value(),
|
||||
bluetoothManager->device()->currentSpeed().value(), inclination, distance1s,
|
||||
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
|
||||
pace, cadence, bluetoothManager->device()->calories().value(),
|
||||
bluetoothManager->device()->elevationGain().value(),
|
||||
|
||||
@@ -203,6 +203,7 @@ class homeform : public QObject {
|
||||
Q_PROPERTY(QString previewWorkoutDescription READ previewWorkoutDescription NOTIFY previewWorkoutDescriptionChanged)
|
||||
Q_PROPERTY(QString previewWorkoutTags READ previewWorkoutTags NOTIFY previewWorkoutTagsChanged)
|
||||
Q_PROPERTY(bool miles_unit READ miles_unit)
|
||||
Q_PROPERTY(bool iPadMultiWindowMode READ iPadMultiWindowMode)
|
||||
|
||||
Q_PROPERTY(bool currentCoordinateValid READ currentCoordinateValid)
|
||||
Q_PROPERTY(bool trainProgramLoadedWithVideo READ trainProgramLoadedWithVideo)
|
||||
@@ -705,6 +706,18 @@ class homeform : public QObject {
|
||||
return settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
|
||||
}
|
||||
|
||||
bool iPadMultiWindowMode() {
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
return lockscreen::isInMultiWindowMode();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool currentCoordinateValid() {
|
||||
if (bluetoothManager && bluetoothManager->device()) {
|
||||
return bluetoothManager->device()->currentCordinate().isValid();
|
||||
@@ -1053,6 +1066,9 @@ class homeform : public QObject {
|
||||
void strava_upload_file_prepare();
|
||||
void garmin_upload_file_prepare();
|
||||
void handleRestoreDefaultWheelDiameter();
|
||||
void StartFromDevice(); // Called when physical start button pressed on hardware
|
||||
void PauseFromDevice(); // Called when physical pause button pressed on hardware
|
||||
void StopFromDevice(); // Called when physical stop button pressed on hardware
|
||||
|
||||
#if defined(Q_OS_WIN) || (defined(Q_OS_MAC) && !defined(Q_OS_IOS)) || (defined(Q_OS_ANDROID) && defined(LICENSE))
|
||||
void licenseReply(QNetworkReply *reply);
|
||||
|
||||
@@ -111,6 +111,8 @@ class lockscreen {
|
||||
static void set_action_profile(const char* profile);
|
||||
static const char* get_action_profile();
|
||||
|
||||
// multi-window detection for iPadOS
|
||||
static bool isInMultiWindowMode();
|
||||
};
|
||||
|
||||
#endif // LOCKSCREEN_H
|
||||
|
||||
@@ -616,13 +616,43 @@ void lockscreen::zwiftClickRemote(const char* Name, const char* UUID, void* devi
|
||||
|
||||
void lockscreen::zwiftClickRemote_WriteCharacteristic(unsigned char* qdata, unsigned char length, void* deviceClass) {
|
||||
if (ios_zwiftClickRemotes == nil) return;
|
||||
|
||||
|
||||
// Get the specific remote for this device
|
||||
NSValue *key = [NSValue valueWithPointer:deviceClass];
|
||||
ios_zwiftclickremote *remote = [ios_zwiftClickRemotes objectForKey:key];
|
||||
|
||||
|
||||
if(remote) {
|
||||
[remote writeCharacteristic:qdata length:length];
|
||||
}
|
||||
}
|
||||
|
||||
bool lockscreen::isInMultiWindowMode() {
|
||||
// Check if we're on iPad and in multi-window mode (Stage Manager, Split View, Slide Over)
|
||||
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
// Get the foreground active scene
|
||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
||||
[scene isKindOfClass:[UIWindowScene class]]) {
|
||||
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
||||
|
||||
// Get the window bounds and screen bounds
|
||||
CGRect windowBounds = windowScene.coordinateSpace.bounds;
|
||||
CGRect screenBounds = windowScene.screen.bounds;
|
||||
|
||||
// If window is smaller than screen in either dimension, we're in multi-window mode
|
||||
// Add a small tolerance for floating point comparison
|
||||
if (windowBounds.size.width < screenBounds.size.width - 1 ||
|
||||
windowBounds.size.height < screenBounds.size.height - 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
22
src/main.qml
22
src/main.qml
@@ -31,20 +31,30 @@ ApplicationWindow {
|
||||
|
||||
// Helper functions for cleaner padding calculations
|
||||
function getTopPadding() {
|
||||
// Add padding for iPadOS multi-window mode (Stage Manager, Split View, Slide Over)
|
||||
// to avoid overlap with window control buttons (red/yellow/green)
|
||||
// Check both the native detection and window size comparison for reactivity
|
||||
if (Qt.platform.os === "ios") {
|
||||
var isMultiWindow = (typeof rootItem !== "undefined" && rootItem && rootItem.iPadMultiWindowMode) ||
|
||||
(window.width < Screen.width - 10); // Window smaller than screen = multi-window
|
||||
if (isMultiWindow) {
|
||||
return 15; // Space for window control buttons
|
||||
}
|
||||
}
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.height : AndroidStatusBar.leftInset;
|
||||
}
|
||||
|
||||
|
||||
function getBottomPadding() {
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
return (Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
|
||||
AndroidStatusBar.navigationBarHeight : AndroidStatusBar.rightInset;
|
||||
}
|
||||
|
||||
|
||||
function getLeftPadding() {
|
||||
if (Qt.platform.os !== "android" || AndroidStatusBar.apiLevel < 31) return 0;
|
||||
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
|
||||
return (Screen.orientation === Qt.LandscapeOrientation || Screen.orientation === Qt.InvertedLandscapeOrientation) ?
|
||||
AndroidStatusBar.leftInset : 0;
|
||||
}
|
||||
|
||||
@@ -925,7 +935,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
text: "version 2.20.22"
|
||||
text: "version 2.20.23"
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ SOURCES += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.cpp \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
|
||||
$$PWD/devices/mobirower/mobirower.cpp \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.cpp \
|
||||
$$PWD/devices/stairclimber.cpp \
|
||||
@@ -378,6 +379,7 @@ HEADERS += \
|
||||
$$PWD/devices/pitpatbike/pitpatbike.h \
|
||||
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
|
||||
$$PWD/devices/sportsplusrower/sportsplusrower.h \
|
||||
$$PWD/devices/mobirower/mobirower.h \
|
||||
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
|
||||
$$PWD/devices/sramAXSController/sramAXSController.h \
|
||||
$$PWD/devices/stairclimber.h \
|
||||
@@ -1004,4 +1006,4 @@ INCLUDEPATH += purchasing/inapp
|
||||
|
||||
WINRT_MANIFEST = AppxManifest.xml
|
||||
|
||||
VERSION = 2.20.22
|
||||
VERSION = 2.20.23
|
||||
|
||||
109
src/qfit.cpp
109
src/qfit.cpp
@@ -162,6 +162,11 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
double watt_sum = 0;
|
||||
int watt_count = 0;
|
||||
|
||||
// Variables for jump rope cadence
|
||||
double cadence_sum = 0;
|
||||
int cadence_count = 0;
|
||||
uint8_t max_cadence = 0;
|
||||
|
||||
for (int i = firstRealIndex; i < session.length(); i++) {
|
||||
if (session.at(i).coordinate.isValid()) {
|
||||
gps_data = true;
|
||||
@@ -204,6 +209,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
watt_sum += session.at(i).watt;
|
||||
watt_count++;
|
||||
}
|
||||
|
||||
// Collect cadence data for jump rope
|
||||
if (type == JUMPROPE && session.at(i).cadence > 0) {
|
||||
cadence_sum += session.at(i).cadence;
|
||||
cadence_count++;
|
||||
if (session.at(i).cadence > max_cadence) {
|
||||
max_cadence = session.at(i).cadence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (speed_count > 0) {
|
||||
@@ -298,7 +312,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
activityTitle.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
activityTitle.SetFieldName(0, L"Activity Title");
|
||||
activityTitle.SetUnits(0, L"Title");
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
activityTitle.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg targetCadenceMesg;
|
||||
targetCadenceMesg.SetDeveloperDataIndex(0);
|
||||
@@ -330,7 +344,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
ftpSessionMesg.SetFitBaseTypeId(FIT_BASE_TYPE_FLOAT64);
|
||||
ftpSessionMesg.SetFieldName(0, L"FTP");
|
||||
ftpSessionMesg.SetUnits(0, L"FTP");
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
ftpSessionMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
// Peloton and workout source fields
|
||||
fit::FieldDescriptionMesg workoutSourceMesg;
|
||||
@@ -339,7 +353,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workoutSourceMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
workoutSourceMesg.SetFieldName(0, L"Workout Source");
|
||||
workoutSourceMesg.SetUnits(0, L"source");
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
workoutSourceMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonWorkoutIdMesg;
|
||||
pelotonWorkoutIdMesg.SetDeveloperDataIndex(0);
|
||||
@@ -347,7 +361,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonWorkoutIdMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonWorkoutIdMesg.SetFieldName(0, L"Peloton Workout ID");
|
||||
pelotonWorkoutIdMesg.SetUnits(0, L"id");
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonWorkoutIdMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg pelotonUrlMesg;
|
||||
pelotonUrlMesg.SetDeveloperDataIndex(0);
|
||||
@@ -355,7 +369,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
pelotonUrlMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
pelotonUrlMesg.SetFieldName(0, L"Peloton URL");
|
||||
pelotonUrlMesg.SetUnits(0, L"url");
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
pelotonUrlMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::FieldDescriptionMesg trainingProgramFileMesg;
|
||||
trainingProgramFileMesg.SetDeveloperDataIndex(0);
|
||||
@@ -363,7 +377,7 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileMesg.SetFitBaseTypeId(FIT_BASE_TYPE_STRING);
|
||||
trainingProgramFileMesg.SetFieldName(0, L"Training Program File");
|
||||
trainingProgramFileMesg.SetUnits(0, L"filename");
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_SESSION);
|
||||
trainingProgramFileMesg.SetNativeMesgNum(FIT_MESG_NUM_WORKOUT); // Workout message for developer metadata
|
||||
|
||||
fit::SessionMesg sessionMesg;
|
||||
sessionMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -385,15 +399,18 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
// Set training load in FIT file
|
||||
// Always set training_load_peak (Garmin uses this for acute training load)
|
||||
// COMMENTED OUT: Garmin Connect doesn't properly reflect these values
|
||||
// Moving to developer data message instead
|
||||
if (training_load > 0) {
|
||||
sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Setting training_load_peak in FIT file:" << training_load;
|
||||
//sessionMesg.SetTrainingLoadPeak(training_load);
|
||||
qDebug() << "Training load will be stored in developer data:" << training_load;
|
||||
}
|
||||
|
||||
|
||||
// For cycling with power, also set training_stress_score (TSS)
|
||||
// COMMENTED OUT: Moving to developer data message
|
||||
if (has_tss) {
|
||||
sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "Setting training_stress_score (TSS) in FIT file:" << tss;
|
||||
//sessionMesg.SetTrainingStressScore(tss);
|
||||
qDebug() << "TSS will be stored in developer data:" << tss;
|
||||
}
|
||||
|
||||
// First, set sport and subsport based on device type
|
||||
@@ -445,6 +462,15 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
sessionMesg.SetSubSport(FIT_SUB_SPORT_GENERIC);
|
||||
if (session.last().stepCount)
|
||||
sessionMesg.SetJumpCount(session.last().stepCount);
|
||||
// Total cycles
|
||||
if (session.last().stepCount)
|
||||
sessionMesg.SetTotalCycles(session.last().stepCount);
|
||||
// Avg cadence (jump rate)
|
||||
if (cadence_count > 0)
|
||||
sessionMesg.SetAvgCadence((uint8_t)(cadence_sum / cadence_count));
|
||||
// Max cadence (max jump rate)
|
||||
if (max_cadence > 0)
|
||||
sessionMesg.SetMaxCadence(max_cadence);
|
||||
} else {
|
||||
|
||||
sessionMesg.SetSport(FIT_SPORT_CYCLING);
|
||||
@@ -540,18 +566,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
trainingProgramFileField.SetSTRINGValue(trainingProgramFile.toStdWString());
|
||||
}
|
||||
|
||||
sessionMesg.AddDeveloperField(activityTitleField);
|
||||
sessionMesg.AddDeveloperField(ftpSessionField);
|
||||
sessionMesg.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
sessionMesg.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
// Developer fields are now added to custom message instead of session
|
||||
// This improves Garmin Connect compatibility
|
||||
|
||||
fit::ActivityMesg activityMesg;
|
||||
activityMesg.SetTimestamp(session.at(firstRealIndex).time.toSecsSinceEpoch() - 631065600L);
|
||||
@@ -608,6 +624,8 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
|
||||
encode.Write(timestampCorrelationMesg);
|
||||
|
||||
// Write workout message with developer metadata fields when workout name exists
|
||||
// This keeps workout-related metadata separate from session/activity for better compatibility
|
||||
if (workoutName.length() > 0) {
|
||||
fit::TrainingFileMesg trainingFile;
|
||||
trainingFile.SetTimestamp(sessionMesg.GetTimestamp());
|
||||
@@ -622,6 +640,21 @@ void qfit::save(const QString &filename, QList<SessionLine> session, BLUETOOTH_T
|
||||
workout.SetWktName(workoutName.toStdWString());
|
||||
#endif
|
||||
workout.SetNumValidSteps(1);
|
||||
|
||||
// Add developer fields to workout message
|
||||
workout.AddDeveloperField(activityTitleField);
|
||||
workout.AddDeveloperField(ftpSessionField);
|
||||
workout.AddDeveloperField(workoutSourceField);
|
||||
if (!pelotonWorkoutId.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonWorkoutIdField);
|
||||
}
|
||||
if (!pelotonUrl.isEmpty()) {
|
||||
workout.AddDeveloperField(pelotonUrlField);
|
||||
}
|
||||
if (!trainingProgramFile.isEmpty()) {
|
||||
workout.AddDeveloperField(trainingProgramFileField);
|
||||
}
|
||||
|
||||
encode.Write(workout);
|
||||
|
||||
fit::WorkoutStepMesg workoutStep;
|
||||
@@ -870,6 +903,36 @@ class Listener : public fit::FileIdMesgListener,
|
||||
// std::wcout << L" New Mesg: " << mesg.GetName().c_str() << L". It has " << mesg.GetNumFields() << L"
|
||||
// field(s) and " << mesg.GetNumDevFields() << " developer field(s).\n";
|
||||
|
||||
// Check if this is a Workout message with developer fields (new format)
|
||||
if (mesg.GetNum() == FIT_MESG_NUM_WORKOUT) {
|
||||
printf("Found Workout message with developer fields\n");
|
||||
// Read developer fields from workout message (new format)
|
||||
for (auto devField : mesg.GetDeveloperFields()) {
|
||||
std::string fieldName = devField.GetName();
|
||||
if (fieldName == "Activity Title" && workoutName != nullptr) {
|
||||
std::wstring wWorkoutName = devField.GetSTRINGValue(0);
|
||||
*workoutName = QString::fromStdWString(wWorkoutName);
|
||||
printf(" Found Activity Title in workout: %s\n", workoutName->toStdString().c_str());
|
||||
} else if (fieldName == "Workout Source" && workoutSource != nullptr) {
|
||||
std::wstring wWorkoutSource = devField.GetSTRINGValue(0);
|
||||
*workoutSource = QString::fromStdWString(wWorkoutSource);
|
||||
printf(" Found Workout Source in workout: %s\n", workoutSource->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton Workout ID" && pelotonWorkoutId != nullptr) {
|
||||
std::wstring wPelotonWorkoutId = devField.GetSTRINGValue(0);
|
||||
*pelotonWorkoutId = QString::fromStdWString(wPelotonWorkoutId);
|
||||
printf(" Found Peloton Workout ID in workout: %s\n", pelotonWorkoutId->toStdString().c_str());
|
||||
} else if (fieldName == "Peloton URL" && pelotonUrl != nullptr) {
|
||||
std::wstring wPelotonUrl = devField.GetSTRINGValue(0);
|
||||
*pelotonUrl = QString::fromStdWString(wPelotonUrl);
|
||||
printf(" Found Peloton URL in workout: %s\n", pelotonUrl->toStdString().c_str());
|
||||
} else if (fieldName == "Training Program File" && trainingProgramFile != nullptr) {
|
||||
std::wstring wTrainingProgramFile = devField.GetSTRINGValue(0);
|
||||
*trainingProgramFile = QString::fromStdWString(wTrainingProgramFile);
|
||||
printf(" Found Training Program File in workout: %s\n", trainingProgramFile->toStdString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (FIT_UINT16 i = 0; i < (FIT_UINT16)mesg.GetNumFields(); i++) {
|
||||
fit::Field *field = mesg.GetFieldByIndex(i);
|
||||
// std::wcout << L" Field" << i << " (" << field->GetName().c_str() << ") has " << field->GetNumValues()
|
||||
|
||||
@@ -677,6 +677,7 @@ const QString QZSettings::treadmill_inclination_override_150 = QStringLiteral("t
|
||||
const QString QZSettings::sole_elliptical_e55 = QStringLiteral("sole_elliptical_e55");
|
||||
const QString QZSettings::horizon_treadmill_force_ftms = QStringLiteral("horizon_treadmill_force_ftms");
|
||||
const QString QZSettings::horizon_treadmill_7_0_at_24 = QStringLiteral("horizon_treadmill_7_0_at_24");
|
||||
const QString QZSettings::treadmill_direct_distance = QStringLiteral("treadmill_direct_distance");
|
||||
const QString QZSettings::treadmill_pid_heart_min = QStringLiteral("treadmill_pid_heart_min");
|
||||
const QString QZSettings::treadmill_pid_heart_max = QStringLiteral("treadmill_pid_heart_max");
|
||||
const QString QZSettings::nordictrack_elliptical_c7_5 = QStringLiteral("nordictrack_elliptical_c7_5");
|
||||
@@ -768,6 +769,7 @@ const QString QZSettings::domyos_treadmill_button_16kmh = QStringLiteral("domyos
|
||||
const QString QZSettings::domyos_treadmill_button_22kmh = QStringLiteral("domyos_treadmill_button_22kmh");
|
||||
const QString QZSettings::proform_treadmill_sport_8_5 = QStringLiteral("proform_treadmill_sport_8_5");
|
||||
const QString QZSettings::domyos_treadmill_t900a = QStringLiteral("domyos_treadmill_t900a");
|
||||
const QString QZSettings::domyos_treadmill_ts100 = QStringLiteral("domyos_treadmill_ts100");
|
||||
const QString QZSettings::domyos_treadmill_sync_start = QStringLiteral("domyos_treadmill_sync_start");
|
||||
const QString QZSettings::enerfit_SPX_9500 = QStringLiteral("enerfit_SPX_9500");
|
||||
const QString QZSettings::proform_treadmill_505_cst = QStringLiteral("proform_treadmill_505_cst");
|
||||
@@ -1045,9 +1047,10 @@ const QString QZSettings::taurua_ic90 = QStringLiteral("taurua_ic90");
|
||||
const QString QZSettings::proform_csx210 = QStringLiteral("proform_csx210");
|
||||
const QString QZSettings::skandika_wiri_x2000_protocol = QStringLiteral("skandika_wiri_x2000_protocol");
|
||||
const QString QZSettings::trainprogram_auto_lap_on_segment = QStringLiteral("trainprogram_auto_lap_on_segment");
|
||||
const QString QZSettings::kingsmith_r2_enable_hw_buttons = QStringLiteral("kingsmith_r2_enable_hw_buttons");
|
||||
|
||||
|
||||
const uint32_t allSettingsCount = 853;
|
||||
const uint32_t allSettingsCount = 856;
|
||||
|
||||
QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
|
||||
@@ -1603,6 +1606,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::sole_elliptical_e55, QZSettings::default_sole_elliptical_e55},
|
||||
{QZSettings::horizon_treadmill_force_ftms, QZSettings::default_horizon_treadmill_force_ftms},
|
||||
{QZSettings::horizon_treadmill_7_0_at_24, QZSettings::default_horizon_treadmill_7_0_at_24},
|
||||
{QZSettings::treadmill_direct_distance, QZSettings::default_treadmill_direct_distance},
|
||||
{QZSettings::treadmill_pid_heart_min, QZSettings::default_treadmill_pid_heart_min},
|
||||
{QZSettings::treadmill_pid_heart_max, QZSettings::default_treadmill_pid_heart_max},
|
||||
{QZSettings::nordictrack_elliptical_c7_5, QZSettings::default_nordictrack_elliptical_c7_5},
|
||||
@@ -1685,6 +1689,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::domyos_treadmill_button_22kmh, QZSettings::default_domyos_treadmill_button_22kmh},
|
||||
{QZSettings::proform_treadmill_sport_8_5, QZSettings::default_proform_treadmill_sport_8_5},
|
||||
{QZSettings::domyos_treadmill_t900a, QZSettings::default_domyos_treadmill_t900a},
|
||||
{QZSettings::domyos_treadmill_ts100, QZSettings::default_domyos_treadmill_ts100},
|
||||
{QZSettings::domyos_treadmill_sync_start, QZSettings::default_domyos_treadmill_sync_start},
|
||||
{QZSettings::enerfit_SPX_9500, QZSettings::default_enerfit_SPX_9500},
|
||||
{QZSettings::proform_treadmill_505_cst, QZSettings::default_proform_treadmill_505_cst},
|
||||
@@ -1916,6 +1921,7 @@ QVariant allSettings[allSettingsCount][2] = {
|
||||
{QZSettings::proform_csx210, QZSettings::default_proform_csx210},
|
||||
{QZSettings::skandika_wiri_x2000_protocol, QZSettings::default_skandika_wiri_x2000_protocol},
|
||||
{QZSettings::trainprogram_auto_lap_on_segment, QZSettings::default_trainprogram_auto_lap_on_segment},
|
||||
{QZSettings::kingsmith_r2_enable_hw_buttons, QZSettings::default_kingsmith_r2_enable_hw_buttons},
|
||||
{QZSettings::toorxtreadmill_discovery_completed, QZSettings::default_toorxtreadmill_discovery_completed},
|
||||
{QZSettings::proform_treadmill_sport_3_0, QZSettings::default_proform_treadmill_sport_3_0},
|
||||
{QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token},
|
||||
|
||||
@@ -1871,6 +1871,9 @@ class QZSettings {
|
||||
static const QString horizon_treadmill_7_0_at_24;
|
||||
static constexpr bool default_horizon_treadmill_7_0_at_24 = false;
|
||||
|
||||
static const QString treadmill_direct_distance;
|
||||
static constexpr bool default_treadmill_direct_distance = false;
|
||||
|
||||
static const QString treadmill_pid_heart_min;
|
||||
static constexpr int default_treadmill_pid_heart_min = 0;
|
||||
|
||||
@@ -2119,6 +2122,9 @@ class QZSettings {
|
||||
static const QString domyos_treadmill_t900a;
|
||||
static constexpr bool default_domyos_treadmill_t900a = false;
|
||||
|
||||
static const QString domyos_treadmill_ts100;
|
||||
static constexpr bool default_domyos_treadmill_ts100 = false;
|
||||
|
||||
static const QString domyos_treadmill_sync_start;
|
||||
static constexpr bool default_domyos_treadmill_sync_start = false;
|
||||
|
||||
@@ -2861,6 +2867,12 @@ class QZSettings {
|
||||
static const QString trainprogram_auto_lap_on_segment;
|
||||
static constexpr bool default_trainprogram_auto_lap_on_segment = false;
|
||||
|
||||
/**
|
||||
* @brief Enable hardware button handling (Start/Pause/Stop) for KingSmith R2 Treadmill
|
||||
*/
|
||||
static const QString kingsmith_r2_enable_hw_buttons;
|
||||
static constexpr bool default_kingsmith_r2_enable_hw_buttons = 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.
|
||||
|
||||
101
src/settings.qml
101
src/settings.qml
@@ -1270,6 +1270,9 @@ import Qt.labs.platform 1.1
|
||||
property int tile_power_avg_order: 77
|
||||
property bool life_fitness_ic5: false
|
||||
property bool technogym_bike: false
|
||||
property bool kingsmith_r2_enable_hw_buttons: false
|
||||
property bool treadmill_direct_distance: false
|
||||
property bool domyos_treadmill_ts100: false
|
||||
}
|
||||
|
||||
|
||||
@@ -8112,6 +8115,34 @@ import Qt.labs.platform 1.1
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: treadmillDirectDistanceDelegate
|
||||
text: qsTr("Direct Distance from Treadmill")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.treadmill_direct_distance
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: settings.treadmill_direct_distance = checked
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Turn this on to read the distance directly from the treadmill instead of calculating it from speed. Some treadmills report distance more accurately than the speed-based calculation. Default is off.")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
id: treadmillDifficultyGainOffsetDelegate
|
||||
text: qsTr("Difficulty offset based")
|
||||
@@ -8931,7 +8962,34 @@ import Qt.labs.platform 1.1
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: { settings.kingsmith_encrypt_g1_walking_pad = checked; settings.kingsmith_encrypt_v5 = false; settings.kingsmith_encrypt_v3 = false; settings.kingsmith_encrypt_v2 = false; settings.kingsmith_encrypt_v4 = false; window.settings_restart_to_apply = true; }
|
||||
}
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("Hardware Buttons")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.kingsmith_r2_enable_hw_buttons
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: { settings.kingsmith_r2_enable_hw_buttons = checked; window.settings_restart_to_apply = true; }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Enable handling of physical Start/Pause/Stop buttons on the treadmill hardware")
|
||||
font.bold: true
|
||||
font.italic: true
|
||||
font.pixelSize: Qt.application.font.pixelSize - 2
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
color: Material.color(Material.Lime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9010,6 +9068,20 @@ import Qt.labs.platform 1.1
|
||||
onClicked: settings.domyos_treadmill_t900a = checked
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("TS100 (Fixed 15° Inclination)")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.domyos_treadmill_ts100
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: settings.domyos_treadmill_ts100 = checked
|
||||
}
|
||||
|
||||
IndicatorOnlySwitch {
|
||||
text: qsTr("Sync Start (Old Behavior)")
|
||||
spacing: 0
|
||||
@@ -9663,8 +9735,33 @@ import Qt.labs.platform 1.1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccordionElement {
|
||||
id: bowflexTreadmillAccordion
|
||||
title: qsTr("Bowflex Treadmill Options")
|
||||
indicatRectColor: Material.color(Material.Grey)
|
||||
textColor: Material.color(Material.Yellow)
|
||||
color: Material.backgroundColor
|
||||
accordionContent: ColumnLayout {
|
||||
spacing: 0
|
||||
IndicatorOnlySwitch {
|
||||
id: bowflexT9MilesDelegate
|
||||
text: qsTr("T9 mi/h speed")
|
||||
spacing: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
clip: false
|
||||
checked: settings.fitshow_treadmill_miles
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
onClicked: settings.fitshow_treadmill_miles = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccordionElement {
|
||||
id: toorxTreadmillAccordion
|
||||
|
||||
@@ -1895,7 +1895,7 @@ QTime trainprogram::currentRowRemainingTime() {
|
||||
uint32_t currentLine = calculateTimeForRow(calculatedLine);
|
||||
calculatedElapsedTime += currentLine;
|
||||
|
||||
if (calculatedElapsedTime > static_cast<uint32_t>(ticks)) {
|
||||
if (calculatedElapsedTime >= static_cast<uint32_t>(ticks)) {
|
||||
if (rows.at(calculatedLine).rampDuration != QTime(0, 0, 0)) {
|
||||
calculatedElapsedTime += ((rows.at(calculatedLine).rampDuration.second() +
|
||||
(rows.at(calculatedLine).rampDuration.minute() * 60) +
|
||||
|
||||
@@ -337,6 +337,29 @@ void virtualrower::rowerProvider() {
|
||||
|
||||
uint16_t normalizeSpeed = (uint16_t)qRound(Rower->currentSpeed().value() * 100);
|
||||
|
||||
// Get stroke count based on device type
|
||||
uint32_t strokeCount = 0;
|
||||
if (Rower->deviceType() == ROWING) {
|
||||
strokeCount = ((rower *)Rower)->currentStrokesCount().value();
|
||||
} else {
|
||||
// For bikes/other devices, estimate strokes from cadence
|
||||
strokeCount = (uint32_t)(Rower->currentCadence().value() * 2 * Rower->movingTime().hour() * 3600 +
|
||||
Rower->movingTime().minute() * 60 + Rower->movingTime().second());
|
||||
}
|
||||
|
||||
// Get pace based on device type
|
||||
uint16_t paceSecs = 0;
|
||||
if (Rower->deviceType() == ROWING) {
|
||||
paceSecs = QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace());
|
||||
} else {
|
||||
// For bikes, pace = odometer / moving_time in seconds
|
||||
if (Rower->movingTime().hour() > 0 || Rower->movingTime().minute() > 0 || Rower->movingTime().second() > 0) {
|
||||
double totalSecs = Rower->movingTime().hour() * 3600 + Rower->movingTime().minute() * 60 + Rower->movingTime().second();
|
||||
if (totalSecs > 0)
|
||||
paceSecs = (uint16_t)(Rower->odometer() * 1000.0 / totalSecs);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#ifndef IO_UNDER_QT
|
||||
if (h) {
|
||||
@@ -344,8 +367,8 @@ void virtualrower::rowerProvider() {
|
||||
if (h->virtualrower_updateFTMS(
|
||||
normalizeSpeed, (char)Rower->currentResistance().value(), (uint16_t)Rower->currentCadence().value() * 2,
|
||||
(uint16_t)normalizeWattage, Rower->currentCrankRevolutions(), Rower->lastCrankEventTime(),
|
||||
((rower *)Rower)->currentStrokesCount().value(), Rower->odometer() * 1000, Rower->calories().value(),
|
||||
QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace()), static_cast<uint8_t>(Rower->deviceType()))) {
|
||||
strokeCount, Rower->odometer() * 1000, Rower->calories().value(),
|
||||
paceSecs, static_cast<uint8_t>(Rower->deviceType()))) {
|
||||
h->virtualrower_setHeartRate(Rower->currentHeart().value());
|
||||
|
||||
uint8_t ftms_message[255];
|
||||
@@ -406,15 +429,15 @@ void virtualrower::rowerProvider() {
|
||||
|
||||
value.append((char)((uint8_t)(Rower->currentCadence().value() * 2) & 0xFF)); // Stroke Rate
|
||||
|
||||
value.append((char)((uint16_t)(((rower *)Rower)->currentStrokesCount().value()) & 0xFF)); // Stroke Count
|
||||
value.append((char)(((uint16_t)(((rower *)Rower)->currentStrokesCount().value()) >> 8) & 0xFF)); // Stroke Count
|
||||
value.append((char)((uint16_t)(strokeCount & 0xFF))); // Stroke Count
|
||||
value.append((char)(((uint16_t)(strokeCount >> 8) & 0xFF))); // Stroke Count
|
||||
|
||||
value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0)) & 0xFF)); // Distance
|
||||
value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0) >> 8) & 0xFF)); // Distance
|
||||
value.append((char)(((uint16_t)(((rower *)Rower)->odometer() * 1000.0) >> 16) & 0xFF)); // Distance
|
||||
value.append((char)(((uint16_t)(Rower->odometer() * 1000.0)) & 0xFF)); // Distance
|
||||
value.append((char)(((uint16_t)(Rower->odometer() * 1000.0) >> 8) & 0xFF)); // Distance
|
||||
value.append((char)(((uint16_t)(Rower->odometer() * 1000.0) >> 16) & 0xFF)); // Distance
|
||||
|
||||
value.append((char)(((uint16_t)QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace())) & 0xFF)); // pace
|
||||
value.append((char)(((uint16_t)QTime(0, 0, 0).secsTo(((rower *)Rower)->currentPace())) >> 8) & 0xFF); // pace
|
||||
value.append((char)((uint16_t)(paceSecs & 0xFF))); // pace
|
||||
value.append((char)(((uint16_t)(paceSecs >> 8) & 0xFF))); // pace
|
||||
|
||||
value.append((char)(((uint16_t)Rower->wattsMetricforUI()) & 0xFF)); // watts
|
||||
value.append((char)(((uint16_t)Rower->wattsMetricforUI()) >> 8) & 0xFF); // watts
|
||||
|
||||
210
tst/ToolTests/qfittestsuite.cpp
Normal file
210
tst/ToolTests/qfittestsuite.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "qfittestsuite.h"
|
||||
#include "../../src/qfit.h"
|
||||
#include "../../src/fitdatabaseprocessor.h"
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
|
||||
QFitTestSuite::QFitTestSuite() : tempDir(nullptr) {
|
||||
}
|
||||
|
||||
QFitTestSuite::~QFitTestSuite() {
|
||||
if (tempDir) {
|
||||
delete tempDir;
|
||||
tempDir = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void QFitTestSuite::SetUp() {
|
||||
tempDir = new QTemporaryDir();
|
||||
ASSERT_TRUE(tempDir->isValid()) << "Failed to create temporary directory";
|
||||
}
|
||||
|
||||
void QFitTestSuite::TearDown() {
|
||||
if (tempDir) {
|
||||
delete tempDir;
|
||||
tempDir = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QList<SessionLine> QFitTestSuite::createTestSession() {
|
||||
QList<SessionLine> session;
|
||||
QDateTime startTime = QDateTime::currentDateTime();
|
||||
|
||||
// Create a simple 10-minute workout session
|
||||
for (int i = 0; i < 600; i += 5) { // 5 second intervals for 10 minutes
|
||||
SessionLine line;
|
||||
line.time = startTime.addSecs(i);
|
||||
line.elapsedTime = i;
|
||||
line.distance = i * 0.05; // 3 km/h = 0.05 km per 5 seconds
|
||||
line.speed = 3.0; // 3 km/h
|
||||
line.cadence = 60;
|
||||
line.heart = 120 + (i % 30); // Varying HR between 120-150
|
||||
line.calories = i / 10;
|
||||
line.watt = 100;
|
||||
|
||||
session.append(line);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
QString QFitTestSuite::createNewFormatFitFile() {
|
||||
QString filename = tempDir->filePath("test_new_format.fit");
|
||||
QList<SessionLine> session = createTestSession();
|
||||
|
||||
// Create a FIT file with developer fields
|
||||
qfit::save(filename, session, BIKE, QFIT_PROCESS_NONE, FIT_SPORT_CYCLING,
|
||||
"Test Workout Title",
|
||||
"Test Device",
|
||||
"PELOTON",
|
||||
"test_workout_id_123",
|
||||
"https://peloton.com/workout/123",
|
||||
"/path/to/training.zwo");
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
bool QFitTestSuite::verifyDeveloperFields(const QString& workoutName, const QString& workoutSource,
|
||||
const QString& pelotonWorkoutId, const QString& pelotonUrl,
|
||||
const QString& trainingProgramFile) {
|
||||
bool allCorrect = true;
|
||||
|
||||
if (workoutName != "Test Workout Title") {
|
||||
qDebug() << "Workout name mismatch. Expected: 'Test Workout Title', Got:" << workoutName;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (workoutSource != "PELOTON") {
|
||||
qDebug() << "Workout source mismatch. Expected: 'PELOTON', Got:" << workoutSource;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (pelotonWorkoutId != "test_workout_id_123") {
|
||||
qDebug() << "Peloton workout ID mismatch. Expected: 'test_workout_id_123', Got:" << pelotonWorkoutId;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (pelotonUrl != "https://peloton.com/workout/123") {
|
||||
qDebug() << "Peloton URL mismatch. Expected: 'https://peloton.com/workout/123', Got:" << pelotonUrl;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
if (trainingProgramFile != "/path/to/training.zwo") {
|
||||
qDebug() << "Training program file mismatch. Expected: '/path/to/training.zwo', Got:" << trainingProgramFile;
|
||||
allCorrect = false;
|
||||
}
|
||||
|
||||
return allCorrect;
|
||||
}
|
||||
|
||||
void QFitTestSuite::test_newFormatDeveloperFields() {
|
||||
// Create a FIT file with new format
|
||||
QString filename = createNewFormatFitFile();
|
||||
ASSERT_TRUE(QFile::exists(filename)) << "Failed to create FIT file";
|
||||
|
||||
// Copy to test-artifacts directory for download
|
||||
QDir artifactsDir("test-artifacts");
|
||||
if (!artifactsDir.exists()) {
|
||||
artifactsDir.mkpath(".");
|
||||
}
|
||||
QString artifactPath = "test-artifacts/test_new_format.fit";
|
||||
QFile::remove(artifactPath);
|
||||
QFile::copy(filename, artifactPath);
|
||||
qDebug() << "FIT file saved to:" << artifactPath;
|
||||
|
||||
// Read the file back
|
||||
QList<SessionLine> session;
|
||||
FIT_SPORT sport = FIT_SPORT_INVALID;
|
||||
QString workoutName;
|
||||
QString workoutSource;
|
||||
QString pelotonWorkoutId;
|
||||
QString pelotonUrl;
|
||||
QString trainingProgramFile;
|
||||
|
||||
qfit::open(filename, &session, &sport, &workoutName, &workoutSource,
|
||||
&pelotonWorkoutId, &pelotonUrl, &trainingProgramFile);
|
||||
|
||||
// Verify basic data was read
|
||||
EXPECT_FALSE(session.isEmpty()) << "Session should not be empty";
|
||||
EXPECT_EQ(sport, FIT_SPORT_CYCLING) << "Sport should be cycling";
|
||||
|
||||
// Verify all developer fields were read correctly from WorkoutMesg
|
||||
EXPECT_TRUE(verifyDeveloperFields(workoutName, workoutSource, pelotonWorkoutId,
|
||||
pelotonUrl, trainingProgramFile))
|
||||
<< "Developer fields should be read correctly from WorkoutMesg";
|
||||
|
||||
qDebug() << "✓ New format developer fields test passed";
|
||||
}
|
||||
|
||||
void QFitTestSuite::test_databaseReadability() {
|
||||
// Create a FIT file with new format
|
||||
QString filename = createNewFormatFitFile();
|
||||
ASSERT_TRUE(QFile::exists(filename)) << "Failed to create FIT file";
|
||||
|
||||
// Copy to test-artifacts directory for download
|
||||
QDir artifactsDir("test-artifacts");
|
||||
if (!artifactsDir.exists()) {
|
||||
artifactsDir.mkpath(".");
|
||||
}
|
||||
QString artifactPath = "test-artifacts/test_database_readability.fit";
|
||||
QFile::remove(artifactPath);
|
||||
QFile::copy(filename, artifactPath);
|
||||
qDebug() << "FIT file saved to:" << artifactPath;
|
||||
|
||||
// Create a temporary database path
|
||||
QString dbPath = tempDir->filePath("test_db.sqlite");
|
||||
|
||||
// Create a FIT database processor with the database path
|
||||
FitDatabaseProcessor processor(dbPath);
|
||||
|
||||
// Setup event loop to wait for async processing
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
timeout.setInterval(5000); // 5 second timeout
|
||||
|
||||
// Process the FIT file
|
||||
bool processed = false;
|
||||
QObject::connect(&processor, &FitDatabaseProcessor::fileProcessed,
|
||||
[&processed, &loop](const QString&) {
|
||||
processed = true;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
bool error = false;
|
||||
QString errorMsg;
|
||||
QObject::connect(&processor, &FitDatabaseProcessor::error,
|
||||
[&error, &errorMsg, &loop](const QString& msg) {
|
||||
qDebug() << "Database processor error:" << msg;
|
||||
error = true;
|
||||
errorMsg = msg;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
processor.processFile(filename);
|
||||
timeout.start();
|
||||
|
||||
// Wait for processing to complete or timeout
|
||||
loop.exec();
|
||||
timeout.stop();
|
||||
|
||||
EXPECT_TRUE(processed) << "FIT file should be processed successfully by database";
|
||||
EXPECT_FALSE(error) << "No errors should occur during database processing. Error: "
|
||||
<< errorMsg.toStdString();
|
||||
|
||||
// Copy database file to test-artifacts directory for download
|
||||
if (QFile::exists(dbPath)) {
|
||||
QString dbArtifactPath = "test-artifacts/test_database.sqlite";
|
||||
QFile::remove(dbArtifactPath);
|
||||
QFile::copy(dbPath, dbArtifactPath);
|
||||
qDebug() << "Database file saved to:" << dbArtifactPath;
|
||||
}
|
||||
|
||||
qDebug() << "✓ Database readability test passed";
|
||||
}
|
||||
66
tst/ToolTests/qfittestsuite.h
Normal file
66
tst/ToolTests/qfittestsuite.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef QFITTESTSUITE_H
|
||||
#define QFITTESTSUITE_H
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QList>
|
||||
#include "../../src/sessionline.h"
|
||||
#include "../../src/devices/bluetoothdevice.h"
|
||||
|
||||
/**
|
||||
* @brief Test suite for qfit FIT file reading/writing
|
||||
*
|
||||
* Tests developer fields moved from Session to WorkoutMesg for better
|
||||
* Garmin Connect compatibility
|
||||
*/
|
||||
class QFitTestSuite: public testing::Test {
|
||||
|
||||
public:
|
||||
QFitTestSuite();
|
||||
~QFitTestSuite() override;
|
||||
|
||||
/**
|
||||
* @brief Test that FIT files with new format (developer fields in WorkoutMesg) can be read correctly
|
||||
*/
|
||||
void test_newFormatDeveloperFields();
|
||||
|
||||
/**
|
||||
* @brief Test that FIT files can be processed by the database
|
||||
*/
|
||||
void test_databaseReadability();
|
||||
|
||||
protected:
|
||||
void SetUp() override;
|
||||
void TearDown() override;
|
||||
|
||||
private:
|
||||
QTemporaryDir* tempDir;
|
||||
|
||||
/**
|
||||
* @brief Create a sample session for testing
|
||||
*/
|
||||
QList<SessionLine> createTestSession();
|
||||
|
||||
/**
|
||||
* @brief Create a FIT file with developer fields in new format (WorkoutMesg)
|
||||
*/
|
||||
QString createNewFormatFitFile();
|
||||
|
||||
/**
|
||||
* @brief Verify developer fields were read correctly
|
||||
*/
|
||||
bool verifyDeveloperFields(const QString& workoutName, const QString& workoutSource,
|
||||
const QString& pelotonWorkoutId, const QString& pelotonUrl,
|
||||
const QString& trainingProgramFile);
|
||||
};
|
||||
|
||||
TEST_F(QFitTestSuite, TestNewFormatDeveloperFields) {
|
||||
this->test_newFormatDeveloperFields();
|
||||
}
|
||||
|
||||
TEST_F(QFitTestSuite, TestDatabaseReadability) {
|
||||
this->test_databaseReadability();
|
||||
}
|
||||
|
||||
#endif // QFITTESTSUITE_H
|
||||
@@ -21,6 +21,7 @@ SOURCES += \
|
||||
Devices/devicetestdataindex.cpp \
|
||||
Erg/ergtabletestsuite.cpp \
|
||||
GarminConnect/garminconnecttestsuite.cpp \
|
||||
ToolTests/qfittestsuite.cpp \
|
||||
ToolTests/testsettingstestsuite.cpp \
|
||||
ToolTests/testtrainingloadtestsuite.cpp \
|
||||
Tools/testsettings.cpp \
|
||||
@@ -55,6 +56,7 @@ HEADERS += \
|
||||
Devices/devicetestdataindex.h \
|
||||
Erg/ergtabletestsuite.h \
|
||||
GarminConnect/garminconnecttestsuite.h \
|
||||
ToolTests/qfittestsuite.h \
|
||||
ToolTests/testsettingstestsuite.h \
|
||||
ToolTests/testtrainingloadtestsuite.h \
|
||||
Tools/devicetypeid.h \
|
||||
|
||||
Reference in New Issue
Block a user