Compare commits

...

59 Commits

Author SHA1 Message Date
Roberto Viola
6ba867a1b5 Mobi Rower 2026-01-30 21:31:29 +01:00
Roberto Viola
72f57053a7 Update project.pbxproj 2026-01-30 09:37:13 +01:00
Roberto Viola
13ea5313b1 Add D500V2 workaround for FTMS start simulation command
Implements a workaround for D500V2 bikes that require a START_RESUME (0x07) command before accepting simulation parameters (0x11). The code now tracks the command sequence and injects the necessary command if missing, ensuring compatibility with D500V2 models. Also adds detection and flag for D500V2 during device discovery.
2026-01-30 09:23:31 +01:00
Roberto Viola
7f694733b2 Update project.pbxproj 2026-01-29 16:34:57 +01:00
Roberto Viola
b1755c004a Detect iPadOS multi-window mode and add padding for window control buttons (#4239)
* Detect iPadOS multi-window mode and add padding for window control buttons

When running on iPadOS in multi-window mode (Stage Manager, Split View,
Slide Over), the window control buttons (red/yellow/green) at the top-left
overlap with the hamburger menu button. This adds:

- Native iOS detection via UIWindowScene API to check if window is smaller
  than screen (indicating multi-window mode)
- QML-side window width check for reactive updates when user enters/exits
  multi-window mode
- 70px left padding on toolbar when in multi-window mode on iPadOS

Fixes #4238

https://claude.ai/code/session_01VPuuPcJnU1GEtGy1vosET9

* Update homeform.h

* Use top padding instead of left padding for iPadOS multi-window mode

Move the toolbar down instead of to the right when in multi-window mode,
which feels more natural and preserves the horizontal layout.

https://claude.ai/code/session_01VPuuPcJnU1GEtGy1vosET9

* Update main.qml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 16:28:58 +01:00
Roberto Viola
360ab66431 Update project.pbxproj 2026-01-29 15:17:25 +01:00
Roberto Viola
04b659a91f Add WT treadmill device support (e.g. WT703) (#4241)
Adds support for WT treadmill devices with names matching pattern "WT"
followed by 3 digits (max 5 characters). The forceSpeed is scaled to
miles when miles_unit setting is enabled, similar to TM4800/TM6500/T3G_ELITE.

https://claude.ai/code/session_01GJXMLrS4sA9LFxSkASSwuU

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 15:15:05 +01:00
Roberto Viola
9487fa3cb4 Add BGYM Bluetooth device support as CSC bike (#4240)
Add support for BGYM devices with 8-byte name length to be recognized
as CSC bikes, enabling compatibility with this fitness equipment brand.

https://claude.ai/code/session_01BDEuRWwTUYM12x5KM7Rd3s

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 14:39:19 +01:00
Roberto Viola
1ac2149424 Add Domyos TS100 treadmill support with fixed 15° inclination (#4234) 2026-01-29 13:36:11 +01:00
Roberto Viola
28558697b2 Fix indentation for Bowflex Treadmill options
Corrected the indentation of the Bowflex Treadmill AccordionElement and its contents in settings.qml to ensure proper UI structure and readability.
2026-01-29 12:09:59 +01:00
Roberto Viola
6918fb9eba Skip problematic services for Zwift RunPod
Added logic to detect Zwift RunPod devices and skip the fff0 and ffc0 services during service scan to avoid discovery issues. This prevents connection problems specific to Zwift RunPod devices.
2026-01-29 12:05:50 +01:00
Roberto Viola
365abbb7cb Add jump rope FIT session metrics: total_cycles, avg_cadence, max_cadence (#4232) 2026-01-28 21:27:12 +01:00
Roberto Viola
d3f52682cc Watt Gain also affects Zwift Power Offset in workout (ERG) mode #4205 (#4222) 2026-01-28 15:58:44 +01:00
Roberto Viola
da4f360f63 Fix data conversion and logging in domyosrower.cpp
Corrected type casting for packet data extraction in GetSpeedFromPacket and GetDistanceFromPacket to ensure proper uint16_t conversion. Updated debug logging in characteristicChanged to improve output formatting.
2026-01-28 08:38:05 +01:00
Roberto Viola
b1c6cf70f5 Fix case sensitivity in Echelon Rower device detection
Updated the Bluetooth device discovery logic to use case-insensitive matching for Echelon Rower device names by converting names to uppercase before comparison. This ensures devices with varying name cases are correctly detected.
2026-01-28 08:17:59 +01:00
Roberto Viola
50e18b1db4 Add Bodycraft T850 treadmill support to horizontreadmill (#4231) 2026-01-28 07:12:06 +01:00
Roberto Viola
47ea3c2176 QZ silently loses connection with Taurus FX9.9 elliptical (Issue #3933) (#3935) 2026-01-28 07:04:44 +01:00
Roberto Viola
c83c272ed4 Add treadmill_direct_distance setting to read distance directly from treadmill (#4215)
* Add treadmill_direct_distance setting to read distance directly from treadmill

This adds a new setting that allows users 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.

The setting is named generically (treadmill_direct_distance) so it can be
used with any treadmill that supports distance reporting via FTMS
characteristics (0x2AD2, 0x2ACD, 0x2ACE).

When enabled, speed-based distance increments are disabled in the update()
function and in all characteristicChanged() handlers.

* Use odometer directly for session distance when treadmill_direct_distance enabled

When the treadmill_direct_distance setting is enabled, the SessionLine
records now use the device's odometer value directly instead of the
speed-based currentDistance1s calculation.

This ensures consistency between the main Distance metric and the session
recording data when using direct distance from the treadmill.

* Update project.pbxproj

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 15:42:20 +01:00
Roberto Viola
de3ea61ecf Fix compilation errors in virtualrower.cpp (#4229)
- Change ROWER to ROWING enum value (ROWER doesn't exist in BLUETOOTH_TYPE enum)
- Fix missing closing parentheses in value.append() calls for strokeCount and paceSecs

https://claude.ai/code/session_01RXrvrLYiy2o58H9YhtJfE8

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 15:06:56 +01:00
Roberto Viola
2d53ebf190 avoiding crashes on echelonbike when the virtual rower is enabled 2026-01-27 13:47:57 +01:00
Roberto Viola
93feea6c16 HR program stuck in one gear (Issue #3897) (#4226) 2026-01-27 13:35:11 +01:00
Roberto Viola
baaf689b4c non-zero watt even after stopped pedalling #4223 2026-01-27 11:07:54 +01:00
Roberto Viola
4dea13d78e non-zero watt even after stopped pedalling #4223 2026-01-27 10:13:07 +01:00
Roberto Viola
914b02f8e0 Fix treadmill unit conversion for BFX_T9_ and T3G_ELITE devices (#4212)
* Fix treadmill unit conversion for BFX_T9_ and T3G_ELITE devices

This commit addresses Issue #4210 by implementing proper unit conversion
for BFX_T9_ (Bowflex T9) and T3G_ELITE treadmills when miles_unit is enabled.

Changes:
1. Fixed BOWFLEX_T9 speed conversion bug in characteristicChanged - was
   multiplying by miles_conversion (incorrect) instead of dividing
2. Made BOWFLEX_T9 unit conversion conditional on miles_unit setting,
   matching behavior of TM4800, TM6500, and T3G_ELITE
3. Added T3G_ELITE to characteristicChanged conversion logic (was missing)
4. Consolidated forceSpeed logic for consistent handling across devices

Both sending (forceSpeed) and receiving (characteristicChanged) speed data
now properly handle miles/km conversion when the treadmill is configured
to use miles.

* Remove BFX_T9_ from forceSpeed - device has no speed control

BFX_T9_ treadmill is receive-only and does not support speed control
commands, so it should not be included in the forceSpeed conversion
logic. Only characteristicChanged conversion is needed for reading
speed data from the device.

* Re-add BFX_T9_ to forceSpeed - device does support speed control

The BFX_T9_ treadmill does support speed control commands, so it needs
to be included in the forceSpeed conversion logic along with TM4800,
TM6500, and T3G_ELITE. When miles_unit is enabled, speed commands are
sent in miles.

* Remove BFX_T9_ from unit conversion logic

BFX_T9_ treadmill does not require miles/km unit conversion. Removed
from both characteristicChanged (receiving data) and forceSpeed
(sending commands). The device handles units natively without needing
conversion when miles_unit setting is enabled.

Only T3G_ELITE, TM4800, TM6500, and horizon_treadmill_7_8 need the
conversion logic.

* Convert speed to miles for Bowflex T9 treadmill

* Add fitshow_treadmill_miles setting for BFX_T9_ treadmill (read-only)

This commit adds support for optional miles/km unit conversion when
reading speed data from the BFX_T9_ (Bowflex T9) treadmill.

Changes:
1. Added "T9 mi/h speed" switch in Horizon Treadmill Options section
   - Uses existing fitshow_treadmill_miles setting
   - Located in settings.qml after Paragon X option

2. Updated horizontreadmill.cpp characteristicChanged() method:
   - Applies speed *= miles_conversion when reading data from device
   - Only active when fitshow_treadmill_miles setting is enabled

When enabled, this setting allows the BFX_T9_ treadmill to work correctly
when it sends speed data in miles per hour. The conversion is only applied
to incoming data (read operations), not outgoing commands.

* Move T9 mi/h speed setting to Bowflex Treadmill Options

Created a new "Bowflex Treadmill Options" section in Treadmill Options
and moved the "T9 mi/h speed" switch from Horizon Treadmill Options to
the new Bowflex section where it belongs.

This provides better organization by grouping Bowflex-specific settings
together in their own dedicated section.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 12:00:45 +01:00
Roberto Viola
0e01889ed3 No Peloton Prompt After Additional Class (Issue #4135)
https://github.com/cagnulein/qdomyos-zwift/issues/4135#issuecomment-3797911208
2026-01-26 11:32:13 +01:00
Roberto Viola
19ef4bb230 BH Fitness i.ZX 23 inclination fix 2026-01-25 16:48:03 +01:00
Roberto Viola
51c8d060de Revert "Ignore 2AD2 data when 2ACD treadmill data is present"
This reverts commit 161362f11f.
2026-01-25 16:31:54 +01:00
Roberto Viola
d1afe0ebb2 Update ConnectIQ iOS SDK to version 1.8.0 (#4211) 2026-01-24 19:14:30 +01:00
Roberto Viola
e5532ca04e Fix training load not being reflected in Garmin Connect (#4200) (#4202) 2026-01-24 05:24:50 +01:00
Roberto Viola
bf37d681de Handle speed conversion for T3G_ELITE treadmill (#4057) 2026-01-24 05:18:54 +01:00
Roberto Viola
03bf9d8fd1 Revert "Watt Gain also affects Zwift Power Offset in workout (ERG) mode (Issue #4205)"
This reverts commit 2bb0e212db.
2026-01-23 19:44:51 +01:00
Roberto Viola
161362f11f Ignore 2AD2 data when 2ACD treadmill data is present
Mail from: Uwe W. "Issues connecting my Nordtictrack treadmill" 23/01/2026

Added isTreadmillDataPresent flag to prevent processing 2AD2 characteristic if 2ACD data is available. This avoids duplicate or conflicting treadmill data handling.
2026-01-23 14:47:12 +01:00
Roberto Viola
2bb0e212db Watt Gain also affects Zwift Power Offset in workout (ERG) mode (Issue #4205) 2026-01-23 13:56:14 +01:00
Roberto Viola
67344ea130 FITFIU BESP 250 indoor bike 2026-01-23 13:28:23 +01:00
Roberto Viola
727fb99572 Add hardware button support for KingSmith R2 treadmill #813
Introduces a new setting to enable handling of physical Start, Pause, and Stop buttons on the KingSmith R2 treadmill. Implements signal and slot connections for the new Pause button, updates settings infrastructure, and adds a UI toggle in settings.qml to control this feature.
2026-01-23 10:52:07 +01:00
Roberto Viola
89ec6eef1f BH Fitness i.ZX 23 inclination fix
mail "App not functioning on my treadmill but its reading" from Thimo A. 23/01/2026
2026-01-23 09:47:38 +01:00
Roberto Viola
a7ca3f329a build fix 2026-01-22 15:35:29 +01:00
Roberto Viola
695c05c284 Handle physical treadmill start/stop button events for Walkinpad X21
Added signals and handlers for physical start/stop button presses on treadmill hardware. The app now responds to these hardware events by starting or stopping the treadmill session without sending redundant commands back to the device.
2026-01-22 15:26:25 +01:00
Roberto Viola
b38712851d Prevent duplicate calorie calculation on FTMS frame
Adds a check to ensure calories are only calculated when a new FTMS frame has not yet been received, preventing duplicate or erroneous updates.
2026-01-22 15:13:08 +01:00
Roberto Viola
73241765d1 Refactor treadmill stop condition in Stop() method
Mail from Rick F. subject: Wed Jan 21 18:07:27 2026 30 min Dance Music Walk 11-24-25 - Jon Hosking date 22/01/2026

Moved the treadmill-specific stop condition check earlier in the Stop() method to avoid redundant code and ensure the check is performed before platform-specific logic. This prevents unnecessary stop actions when the treadmill is already stopped and elapsed time is zero.
2026-01-22 15:08:19 +01:00
Roberto Viola
ad79beb44b Sunny Health Fitness Smart Multifunction Magnetic Rowing Machine SF-RW5941SMART #1289 2026-01-22 10:41:54 +01:00
Roberto Viola
39288f1343 QZ App doesn't show data from Domyos 900 (Issue #4188) 2026-01-22 09:44:42 +01:00
Roberto Viola
ff093a126e 2.20.23 2026-01-22 08:28:29 +01:00
Roberto Viola
489ef0a665 Add RI009R Bluetooth device support as trxappgateusbbike (#4201) 2026-01-22 06:47:27 +01:00
Roberto Viola
73771f42c2 No Peloton Prompt After Additional Class #4135 2026-01-21 20:33:52 +01:00
Roberto Viola
dbdd58c398 Update project.pbxproj 2026-01-21 15:42:02 +01:00
Roberto Viola
5c0aa59bed Add Merach NovaRow R50 settings option under Rower Options (#4198) 2026-01-21 15:40:28 +01:00
Roberto Viola
7ec3b601b4 Update project.pbxproj 2026-01-21 14:26:49 +01:00
Roberto Viola
9d2bf0821c Add Merach NovaRow R50 settings option under Rower Options (#4198) 2026-01-21 14:25:16 +01:00
Roberto Viola
5d85cdd8e5 Revert "Add Merach NovaRow R50 settings option under Rower Options (#4198)"
This reverts commit 9b70d6c144.
2026-01-21 14:22:13 +01:00
Roberto Viola
56bbc2439c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2026-01-21 14:15:33 +01:00
Roberto Viola
31e1bb8a9f Focus Fitness Jet 7 iPlus 2026-01-21 14:15:28 +01:00
Roberto Viola
88a8b138ca build fix 2026-01-21 12:11:29 +01:00
Roberto Viola
10cfac1e40 Update project.pbxproj 2026-01-21 12:08:14 +01:00
Roberto Viola
79952ad73c Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2026-01-21 12:07:05 +01:00
Roberto Viola
3a3755d18c Add Merach NovaRow R50 settings option under Rower Options (#4198) 2026-01-21 12:06:58 +01:00
Roberto Viola
9a45b28f8c Update project.pbxproj 2026-01-21 11:14:13 +01:00
Roberto Viola
9b70d6c144 Add Merach NovaRow R50 settings option under Rower Options (#4198)
Instead of auto-detecting the Merach NovaRow R50 based on the Bluetooth
name prefix "MRK-R11S-", add a manual settings option that allows users
to enable/disable the Merach NovaRow R50 protocol.

Changes:
- Added merach_novarow_r50 setting to qzsettings.h/cpp (default: false)
- Added "Merach Rower Options" section in settings.qml under "Rower Options"
- Added "Merach NovaRow R50" switch in the new section
- Updated trxappgateusbrower to load setting from QSettings in constructor
- Removed auto-detection logic from btinit() method
- Device will still be detected via Bluetooth but protocol selection now
  depends on the user-configurable setting

Related to issue #3593

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 10:34:15 +01:00
Roberto Viola
137036efd7 Nordictrack SE7i Init v2 (#3976)
* Nordictrack SE7i Init v2

* Update nordictrackelliptical.cpp

* Update nordictrackelliptical.cpp

* Revert "Update nordictrackelliptical.cpp"

This reverts commit ed4c451869.

* trying to align to ifit frames

* fixing init

* Update bluetooth.cpp

* Revert "Update bluetooth.cpp"

This reverts commit dd27d98e1d.

* Revert "fixing init"

This reverts commit 22d9e643bc.
2026-01-20 14:21:24 +01:00
48 changed files with 1827 additions and 251 deletions

View File

@@ -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'

View File

@@ -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;

Binary file not shown.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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"))) {

View File

@@ -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();

View File

@@ -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;
}

View 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();
}
}

View 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

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();
}
});
}
}

View File

@@ -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;

View File

@@ -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)) {

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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},

View File

@@ -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.

View File

@@ -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

View File

@@ -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) +

View File

@@ -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

View 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";
}

View 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

View File

@@ -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 \