Compare commits

...

605 Commits

Author SHA1 Message Date
Roberto Viola
14d0f4f527 Merge branch 'master' into Kettler-Racer-S 2026-02-09 15:24:47 +01:00
Roberto Viola
8816fd105a Revert "Add EW-ST- Bluetooth name prefix for iconsole rower detection (#4289)"
This reverts commit 4fb046d9dc.
2026-02-09 08:18:14 +01:00
Roberto Viola
4fb046d9dc Add EW-ST- Bluetooth name prefix for iconsole rower detection (#4289) 2026-02-08 20:08:00 +01:00
Roberto Viola
1578e25aca Add virtual rower support to cscbike (#4274) 2026-02-08 17:24:28 +01:00
Roberto Viola
bceabd916a Deerun treadmill handle Inclination writing (#4277)
* Deerun treadmill handle Inclination writing

* Update deerruntreadmill.cpp

* Update deerruntreadmill.cpp

* Update deerruntreadmill.cpp

* Update deerruntreadmill.cpp
2026-02-06 16:02:54 +01:00
Roberto Viola
009c806189 Remove garmin_device_serial and garmin_email from debug logs (#4280)
These settings contain sensitive user information and should not be
written to debug logs, similar to how password, token, and username
are already filtered out.

https://claude.ai/code/session_01Bb3K9KzcJGewehcn2x5RXe

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 15:03:27 +01:00
Roberto Viola
c22ee74ff4 Fix Garmin Connect IQ INCOMING_MESSAGE crash - v2.20.26 (#4276)
* Fix Garmin Connect IQ INCOMING_MESSAGE crash - v2.20.26

Fixes crash caused by Garmin Connect Mobile app update (v5.20.1, Jan 15, 2026).

The IQMessageReceiverWrapper was not handling the INCOMING_MESSAGE broadcast
action, causing IllegalArgumentException when deserializing IQDevice Parcelable.

Changes:
- Added INCOMING_MESSAGE action handler in IQMessageReceiverWrapper.java
- Updated version to 2.20.26 (build 1274) in all required files:
  * AndroidManifest.xml
  * main.qml
  * qdomyos-zwift.pri
- Updated CLAUDE.md with version update instructions

Error fixed: java.lang.IllegalArgumentException: 8 > -1218333925

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Add comprehensive diagnostic logging to Garmin wrapper

This logging will help diagnose the crash by showing:
- All Intent extras and their types
- Parcelable deserialization attempts
- Exact point where exceptions occur
- Full stack traces

This will confirm if the INCOMING_MESSAGE fix is sufficient or if
there are other underlying issues causing the crash.

* ciq library 2.2.0

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 10:34:35 +01:00
Roberto Viola
0772026b7b Add speed and inclination +/- buttons to floating template (#4258)
* Add speed and inclination +/- buttons to floating template

Add new control tiles for speed and inclination in the floating
template (both vertical and horizontal variants). These tiles display
the current value with +/- buttons for adjustment, useful for treadmill
users who want to control speed and incline from the floating overlay.

Changes:
- Add speedcontrol and inclinecontrol tiles to floating.htm and hfloating.htm
- Add SpeedPlus/SpeedMinus and InclinationPlus/InclinationMinus JavaScript functions
- Add WebSocket message handlers (speed_plus, speed_minus, inclination_plus, inclination_minus)
- Add corresponding signals and slots in templateinfosenderbuilder and homeform
- Wire up to existing Plus/Minus functions which handle speed and inclination changes

https://claude.ai/code/session_01QgLyRenM7sSWqqGU1Esy3s

* Integrate speed/inclination buttons into existing rows

Replace AVG/MAX labels with -/+ buttons on speed and inclination rows
instead of having separate control tiles. This provides a cleaner UI
where users can see all metrics (avg, current, max) while also having
control buttons on the same row.

Layout: [-] AVG_VALUE | CURRENT_VALUE | [+] MAX_VALUE

https://claude.ai/code/session_01QgLyRenM7sSWqqGU1Esy3s

* Add AVG/MAX labels under values in speed/inclination rows

Add small labels under the avg and max values so users know what
the values represent. Layout is now:

[-] avg_value | current_value | [+] max_value
      AVG                            MAX

https://claude.ai/code/session_01QgLyRenM7sSWqqGU1Esy3s

* Show speed/inclination +/- buttons only for treadmills

The +/- control buttons on speed and inclination rows are now
only visible when connected to a treadmill device. For other
device types (bike, elliptical, etc.), the standard AVG/MAX
labels are shown instead.

Uses deviceType and TREADMILL_TYPE from workout data to detect
the device type and toggle visibility via CSS classes.

https://claude.ai/code/session_01QgLyRenM7sSWqqGU1Esy3s

* Fix table layout consistency for non-treadmill devices

Wrap the <br> and AVG/MAX label elements together in a span with the
treadmill-only class so they are both hidden for non-treadmill devices.
This ensures the table layout is identical to the original for bikes
and other non-treadmill equipment.

https://claude.ai/code/session_01QgLyRenM7sSWqqGU1Esy3s

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 09:08:18 +01:00
Roberto Viola
5bd74260ab Horizon Treadmill FW Update Prevents Connection (Issue #4270) 2026-02-04 13:46:00 +01:00
Roberto Viola
35f7ab636e Update project.pbxproj 2026-02-04 09:45:11 +01:00
Roberto Viola
4d8fd1ce1a 2.20.25 2026-02-04 09:20:09 +01:00
Roberto Viola
b2a28d71e4 DMASUN bike watt HR based 2026-02-04 08:54:21 +01:00
Roberto Viola
2db5683dd2 2.20.24 2026-02-04 08:34:16 +01:00
Roberto Viola
3b81b6d4ee Fix Garmin ConnectIQ crash with malformed messages (#4269)
Fixes app crash when receiving corrupted or malformed messages from
Garmin ConnectIQ devices that cause IllegalArgumentException or
BufferUnderflowException during deserialization.

Changes:
- IQMessageReceiverWrapper: Changed catch block from specific exception
  types to generic Exception to handle all deserialization errors
  including wrapped exceptions
- Garmin: Refactored initialization to create wrapped context BEFORE
  getInstance() call, ensuring ALL BroadcastReceivers registered by
  the SDK (including during getInstance) pass through the wrapper

Root cause: The SDK was receiving the original context in getInstance()
before the wrapper was created, so some receivers bypassed the wrapper's
exception handling. Now the wrapped context is created first and passed
to both getInstance() and initialize().

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 08:31:23 +01:00
Roberto Viola
2e0bd25a4a SunnyFit Stepper (#4245) 2026-02-04 05:00:53 +01:00
Roberto Viola
323c169067 Add average speed to Garmin FIT session fields (#4268) 2026-02-03 22:05:05 +01:00
Roberto Viola
1ff9da34db Fix event propagation in draggable map container (#4227)
* Fix iOS QML scrolling issue when interacting with metrics box

The metrics container drag/resize functionality was causing page scrolling
on iOS within the QML WebView instead of allowing box manipulation.

Changes:
- Add { passive: false } to touchstart event listeners to ensure preventDefault() works on iOS
- Add stopPropagation() to startDrag() and handleMove() to prevent event bubbling
- Add CSS rules to body/html to disable scrolling and overscroll behavior
- Prevent default touch actions on the entire viewport

This ensures that touch events on the metrics box are properly captured
and don't trigger page scrolling on iOS devices.

* Remove overly aggressive CSS that blocked map interaction

The previous fix prevented page scrolling but also disabled
Cesium map interaction (pan, zoom, rotate). The touch-action: none
on metricsContainer combined with passive: false listeners and
stopPropagation() should be sufficient to prevent scrolling only
when interacting with the metrics box.

* Add z-index and chart following functionality for metrics box

Changes:
- Add z-index: 1000 to metricsContainer and 999 to chartContainer to ensure
  they appear above the Cesium map and properly capture touch events
- Add touch-action: none to inner .metrics div and chartContainer for better
  touch event handling on iOS/Android
- Implement updateChartPosition() to make the elevation chart follow the
  metrics box when dragging or resizing
- Chart maintains 10px left offset and bottom alignment with metrics box
- Chart position updates during drag, resize, and on page load

This ensures the metrics box and chart can be interacted with on mobile
devices without interfering with the Cesium map underneath.

* Fix touch event handling to prevent Cesium interference

The previous implementation had critical issues:
1. Global document touchmove listeners were always active, capturing
   all touch events including those on the Cesium map
2. Resize handle detection using e.target was unreliable on touch devices

Changes:
- Remove permanently-active global listeners on document
- Add/remove global listeners dynamically only when drag/resize starts/ends
- Implement isInResizeHandle() function using coordinate-based detection
  instead of e.target checking (checks if touch is in bottom-right 20x20px)
- Consolidate mousedown/touchstart handlers on container to use the same
  coordinate-based logic for determining drag vs resize

This ensures:
- Cesium map remains fully interactive when not touching metrics box
- Resize handle works reliably on touch devices
- No event leakage between UI elements and the map

* Improve touch handling with larger resize handle and better event checks

Changes to fix touch interaction issues:

1. Resize Handle Improvements (50x50px instead of 20x20px):
   - Increased size from 20x20 to 50x50 pixels for easier touch targeting
   - Added visible resize icon (⤡) with better contrast
   - More opaque background for better visibility

2. Event Listener Safety:
   - Added isInsideContainer() check to verify touch starts in container
   - Added early return in handleMove() if not dragging/resizing
   - Always remove listeners in endDragOrResize() to prevent leaks
   - Added touchcancel event handling for system interruptions
   - Fixed logic to check wasResizing before accessing it

3. Better State Management:
   - Always reset isDragging/isResizing before removing listeners
   - Only save state if actually was dragging or resizing
   - Prevents stale listener references

This should fix:
- Difficulty grabbing resize handle on touch devices
- Box moving when interacting with Cesium map
- Event listeners remaining active when they shouldn't

* Update project.pbxproj

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 15:22:13 +01:00
Roberto Viola
361874f1ea Yesoul T3S Plus treadmill 2026-02-02 15:14:52 +01:00
Roberto Viola
33b686bf3e Add weight unit preference for miles users (#4261)
* Add weight unit toggle for UK users (kg with miles)

Adds a new setting "Use kg for weight" that allows users to use
kilograms for weight even when miles is selected for distance.
This is useful for UK users who typically use miles for distance
but kg for body weight.

The toggle appears only when "Use Miles unit in UI" is enabled.
Updated Player Weight, Bike Weight in settings.qml and Wizard.qml.

https://claude.ai/code/session_01B4HhW9pAva8fC7EtgQ8jRo

* Update Wizard.qml

* Update Wizard.qml

* Update settings.qml

* Add weight unit toggle for UK users (kg with miles)

Adds a new setting "Use kg for weight" that allows users to use
kilograms for weight even when miles is selected for distance.
This is useful for UK users who typically use miles for distance
but kg for body weight.

The toggle appears only when "Use Miles unit in UI" is enabled.
Updated Player Weight, Bike Weight in settings.qml and Wizard.qml.

https://claude.ai/code/session_01B4HhW9pAva8fC7EtgQ8jRo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 14:21:10 +01:00
Roberto Viola
74e1aba909 horizon gr7 power data bug (Issue #4250) 2026-02-02 12:03:33 +01:00
Roberto Viola
bf75b2bda0 Add Thinkrider VS200 controller support for gear shifting (#4242)
* Add Thinkrider VS200 controller support for gear shifting

Implements support for the Thinkrider VS200 remote controller,
enabling gear up/down functionality similar to Zwift Click.
Uses service UUID 0000fea0 and detects button patterns for
shift up (f3050301fc) and shift down (f3050300fb).

https://claude.ai/code/session_01DK5qQY9wKyHTKfYhAkGECS

* Update allSettingsCount to 857 for thinkrider_controller setting

https://claude.ai/code/session_01DK5qQY9wKyHTKfYhAkGECS

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Add device discovery wait for Thinkrider and create separate settings section

- Add thinkriderDeviceAvaiable() function for discovery wait logic
- Add thinkriderDeviceFound checks in bluetooth constructor and deviceDiscovered
- Create separate "Thinkrider Options" accordion section in settings.qml
- Remove Thinkrider from Zwift Devices Options section

https://claude.ai/code/session_01DK5qQY9wKyHTKfYhAkGECS

* Update project.pbxproj

* Update bluetooth.cpp

* Update project.pbxproj

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-31 21:23:42 +01:00
Roberto Viola
80faa062e1 Add Garmin server selection and debug logging (#4235)
* Add Garmin server selection and debug logging

Introduces a ComboBox in settings to select between global and China Garmin servers, prompting for app restart when changed. Adds debug logging in garminconnect.cpp to trace domain and API URLs, and logs the loaded domain from settings.

* Add verbose debug logging for GarminConnect responses

Introduces a DEBUG_GARMIN_VERBOSE flag to enable detailed logging of HTTP responses and ticket extraction attempts in the GarminConnect authentication flow. This aids in troubleshooting login and MFA issues by providing more insight into response contents and extraction logic.

* Detect MFA via page title and handle CSRF

Instead of scanning the entire response body for "MFA", detect MFA by parsing the HTML <title> (matching the Python garth approach) to avoid false positives from bodies that contain "MFA" text. Extract the page title early, check for "MFA" case-insensitively, and if detected update m_lastError, refresh cookies, extract a new CSRF token using two regex patterns, emit mfaRequired (unless suppressed), and abort the login flow. Also adjust the success check to rely on the title == "Success" and remove the legacy body-based MFA detection block. Added debugging logs for the title, CSRF token, and MFA signal paths.

* Update garminconnect.h

* popup not needed
2026-01-31 20:19:05 +01:00
Roberto Viola
51808cc8a4 Set RepetitionNum to lap_index
In src/qfit.cpp (qfit::save) for JUMPROPE laps, use lap_index when calling lapMesg.SetRepetitionNum instead of session.at(i - 1).inclination. This makes the repetition number reflect the lap index and avoids relying on session data that could be incorrect or out-of-range.
2026-01-31 08:00:07 +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
Roberto Viola
e5e2851d45 Update project.pbxproj 2026-01-20 13:47:37 +01:00
Roberto Viola
cadfaa8be1 Cherry-pick changes from domyos-ftms-rower-condition branch (excluding ftmsrower) (#4193)
This commit includes changes from the claude/domyos-ftms-rower-condition-01WfjA42DLkhxrY6KW2StycC branch
after it was merged with master, excluding modifications to ftmsrower files.

Changes include:
- Bluetooth device detection improvements
- Domyos rower enhancements

Files modified:
- src/devices/bluetooth.cpp
- src/devices/domyosrower/domyosrower.cpp

Files excluded (as requested):
- src/devices/ftmsrower/ftmsrower.cpp
- src/devices/ftmsrower/ftmsrower.h

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 13:45:54 +01:00
Roberto Viola
6df4acf35c Fix Dircon Rouvy compatibility to use ID 1234 when dircon_id is 0 (#4190) 2026-01-19 19:57:19 +01:00
Roberto Viola
2ccbbfb07b Add touch-based resize and drag functionality for metrics div in Google Maps view (#4185)
* Add touch-based resize and drag functionality for metrics div in Google Maps view

Implemented interactive metrics container with the following features:
- Touch/mouse draggable metrics box - touch anywhere on the box to move it
- Touch/mouse resizable with handle in bottom-right corner
- Dynamic font scaling that adjusts based on container size (8-24px range)
- Persistent state using localStorage to remember position and size
- Minimum size constraint of 100x100px
- Visual resize handle with gradient indicator

The text size automatically scales proportionally with the container dimensions,
maintaining readability across different box sizes.

* Optimize font sizing with binary search algorithm and debouncing

Replace linear scaling with binary search algorithm to find the maximum
font size that perfectly fits the metrics container. This ensures optimal
space utilization regardless of container dimensions.

Key improvements:
- Binary search finds maximum font size (6-48px range) that fits both
  vertically and horizontally without overflow
- Debounced updates every 500ms during continuous resize to reduce CPU usage
- Immediate font size calculation when resize completes
- More accurate text fitting compared to proportional scaling
- Better handling of varying content length and emoji sizes

The algorithm converges in ~6-8 iterations, providing excellent performance
while maximizing readability across all container sizes.

* Force reflow in font size calculation to ensure all lines are visible

Add forced reflow (void metricsText.offsetHeight) after each font size
change in the binary search loop. This ensures the browser recalculates
layout dimensions before checking scrollHeight/scrollWidth, guaranteeing
accurate overflow detection and that all text lines remain visible.

Fixes issue where some lines could be cut off due to browser layout
timing.

* Fix font size calculation to ensure all lines are always visible

Improved overflow detection with:
- Multiple forced reflows to guarantee browser layout updates
- Stricter comparison using < instead of <= with 1px tolerance for sub-pixel rendering
- Final verification step that reduces font size by 1px if overflow detected
- Ensures all 11 metric lines remain visible without clipping

This fixes the issue where only 3 lines were visible due to font being too large.

* Add verification loop to ensure last line is always visible

After binary search completes, added a final verification loop that:
- Checks if any content overflows (scrollHeight/scrollWidth vs clientHeight/clientWidth)
- Reduces font size by 1px and rechecks until all content fits
- Performs up to 10 iterations with multiple reflows per iteration
- Ensures the last metric line is never cut off

This guarantees all 11 metric lines remain completely visible regardless
of container size or browser rendering variations.

* Replace scrollHeight check with line-based height measurement

Completely rewrite font size detection to ensure all lines are visible:
- Count actual number of lines by counting <br> tags
- Create temporary test element to measure single line height
- Calculate total height needed: lineHeight × lineCount
- Compare estimated height vs available height instead of relying on scrollHeight
- Much more reliable detection that ensures last line is never cut off

This fixes the issue where only 5-6 lines were visible out of 11 total lines,
as scrollHeight was not correctly reporting overflow in all cases.

* Simplify font sizing with strict overflow checks and safety margin

Remove complex line-height measurement in favor of simpler approach:
- Use scrollHeight directly with strict less-than comparison (<)
- Add 3px safety margin to font size after binary search
- Increase verification loop to 15 iterations
- Use multiple forced reflows for reliable dimension updates
- Stricter contentFits() check ensures no content is cut off

The 3px safety margin compensates for any browser rendering inconsistencies
and ensures the last line (Elevation) is always fully visible.

* Reduce safety margin from 3px to 1px to allow larger font sizes

Modified overflow detection to be less conservative:
- Changed comparison from < to <= (allows equal size)
- Reduced safety margin from 3px to 1px
- Reduced verification loop from 15 to 10 iterations
- This allows font to be larger while still ensuring all lines are visible

The previous 3px margin was too conservative and kept fonts unnecessarily small.
The 1px margin provides sufficient safety while maximizing font size.

* Fix font sizing with mathematical line-height calculation

Completely rewrite font size detection using mathematical approach:
- Count actual number of lines by counting <br> tags
- Get computed lineHeight from CSS
- Calculate total height needed: numLines × lineHeight
- Subtract padding from available height for accurate comparison
- Remove unreliable scrollHeight checks that weren't working

This fixes the persistent issue where only 5-6 lines were visible.
The mathematical approach is precise and doesn't rely on browser
scrollHeight reporting which was inconsistent with overflow:hidden.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 15:58:01 +01:00
Roberto Viola
7d4775c0ea Sprintbok treadmill 2026-01-19 15:26:49 +01:00
Roberto Viola
9baa9a0f23 Speed/Cadence not working with Garmin watch #4149 (#4179) 2026-01-19 05:03:36 +01:00
Roberto Viola
f3fc0a5212 Update project.pbxproj 2026-01-18 18:08:31 +01:00
Roberto Viola
eb29ec5dfd Support Zipro Rave: FTMS resistance control not working #4182 2026-01-18 18:07:48 +01:00
Roberto Viola
d29b312554 Fix GPX distance calculation for non-looping routes in preview (#4176)
* Fix GPX distance calculation for non-looping routes in preview

Problem:
Non-looping GPX routes (where start and end points are >300m apart)
were showing incorrect distance in the preview. For example,
"Pittenweem to St Andrews.gpx" showed 42.5km instead of the actual
27.71km. Looping routes displayed correctly.

Root Cause:
The gpx_loop setting, when enabled, causes non-looping routes to
have all points appended in reverse order (creating a "there and
back" route), which approximately doubles the distance. This behavior
was affecting the preview display even though the preview should
always show the actual GPX route as-is.

Solution:
- Added optional 'forceNoLoop' parameter to gpx::open() method
- When true, this parameter forces gpx_loop to be false regardless
  of user settings
- Modified gpxpreview_open_clicked() in homeform.cpp to pass
  forceNoLoop=true
- This ensures the preview always shows the actual route distance
- Actual workout loading still respects the gpx_loop user setting

Testing:
Verified that:
- Pittenweem to St Andrews.gpx (27.71km, non-looping) now shows correct distance
- Box Hill.gpx (16.78km, looping) continues to work correctly
- Existing gpx.open() calls for workout loading remain unchanged

* Use gpx.cpp calculated distance instead of QML QGeoPath.length()

The issue was that GPXList.qml was using pathController.geopath.length()
to calculate distance, which was producing incorrect results for
non-looping routes (e.g., Pittenweem to St Andrews showed 42.5km
instead of 27.71km).

Solution:
- Add totalDistance member to gpx class to store correctly calculated distance
- Add distance property to PathController to expose it to QML
- Update GPXList.qml to use pathController.distance instead of
  pathController.geopath.length() / 1000.0

The distance calculation in gpx.cpp is correct and now properly exposed
to the QML layer.

* Fix: Set distance before setGeoPath to ensure QML has correct value

When setGeoPath() is called, it triggers onGeopathChanged in QML which
immediately calls loadPath(). If setDistance() is called after setGeoPath(),
the QML will read the old/stale distance value.

Moving setDistance() before setGeoPath() ensures the distance is already
updated when the QML onGeopathChanged handler runs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 17:35:59 +01:00
Roberto Viola
56f3916387 Support Zipro Rave: FTMS resistance control not working #4182 2026-01-18 16:55:31 +01:00
Roberto Viola
bbb2d6fe90 Update project.pbxproj 2026-01-18 16:01:26 +01:00
Roberto Viola
34be7d451f 2.20.22 2026-01-18 07:10:18 +01:00
Roberto Viola
83bbb56ad8 Fix MyWhoosh compatibility condition in dircon processor (#4178) 2026-01-18 06:32:38 +01:00
Roberto Viola
87846d9dd2 Update Garmin device UNIT ID instructions
Expanded the label text to clarify that leaving the default Unit ID allows viewing Acute load in Garmin Connect.
2026-01-17 15:48:22 +01:00
Roberto Viola
94a70a56db adding Bowflex T6 2026-01-16 21:21:04 +01:00
Roberto Viola
563c7a1445 FTMSBIKE Refactor KCal calculation logic in characteristicChanged
mail: InCondi 150i Bluetooth connection
From: Benjaminas R. 16/01/2026

Commented out the original KCal calculation from characteristic data and moved the watts-based KCal calculation outside the else block. This ensures KCal is always updated based on watts when available, regardless of the expEnergy flag.
2026-01-16 09:26:56 +01:00
Roberto Viola
6e23d0c743 Add Garmin Epix series support to device settings (#4168) 2026-01-16 07:05:45 +01:00
Roberto Viola
60d5880081 Update project.pbxproj 2026-01-15 16:52:54 +01:00
Roberto Viola
fb56c58046 Update project.pbxproj 2026-01-15 16:52:08 +01:00
Roberto Viola
27759c14ee Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2026-01-15 16:51:49 +01:00
Roberto Viola
19bee8ee9b Add Tacx device support for Garmin device settings (#4166) 2026-01-15 16:49:16 +01:00
Roberto Viola
d25ecb176c No watts Gymstick GX6.0 crosstrainer (Issue #4164) 2026-01-15 13:36:20 +01:00
Roberto Viola
da92d8711f use tss and training load separetely in the qfit 2026-01-15 10:33:30 +01:00
Roberto Viola
e6cbc43e3b Add acute training load (TRIMP) calculation to FIT files (#4159) 2026-01-15 08:47:17 +01:00
Roberto Viola
ad628b58d6 Fix Bluetooth device detection for Schwinn 510T and add Speed Race S (#4160) 2026-01-15 07:23:12 +01:00
Roberto Viola
7ddbc984dc Fix Domyos-TC horizon treadmill service discovery (#4158) 2026-01-14 20:22:34 +01:00
Roberto Viola
bb16ecc80d Fix Garmin Connect consecutive upload SSL error (#4154)
Fixes #4135 - Users reported that only the first FIT file upload
to Garmin Connect succeeded, while consecutive uploads failed with:
"SSL routines:ssl3_read_bytes:sslv3 alert bad record mac"

Root cause: QNetworkAccessManager was reusing stale SSL connections
between consecutive uploads, causing the server to reject the second
request due to connection state inconsistency.

Solution: Added "Connection: close" header to both uploadFitFile()
and uploadActivity() methods to force connection closure after each
upload, preventing SSL connection reuse issues.

This ensures clean SSL handshakes for every upload request.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 10:48:22 +01:00
Roberto Viola
77d18da7a1 Add virtual rower support to sportsplusrower (#4153) 2026-01-13 21:36:13 +01:00
Roberto Viola
5c008dba20 Add inCondi S150i FTMS bike support (#4152) 2026-01-13 21:14:32 +01:00
Roberto Viola
1fae044590 Add Life Fitness IC8 condition to Technogym bike detection (#4104) 2026-01-13 21:07:01 +01:00
Roberto Viola
fc09ea3de2 Add Timestamp Correlation record to FIT files (#4146)
Added TimestampCorrelationMesg to correlate UTC timestamp, system timestamp,
and local timestamp at session start. This provides better timezone handling
for fitness data syncing with companion apps.

- Added fit_timestamp_correlation_mesg.hpp include
- Created timestamp correlation record with three timestamp fields:
  * Timestamp: UTC timestamp at session start
  * SystemTimestamp: Same as timestamp (session start)
  * LocalTimestamp: User's local time at session start

References: https://github.com/cagnulein/QZCompanionGarmin/issues/9#issuecomment-3739254877

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 12:13:35 +01:00
Roberto Viola
332b3efe59 Revert SDMChannelController speed TX calculation changes (#4129)
* Revert SDMChannelController speed TX calculation changes

Reverts the speed encoding changes from commits 6f166d2 and eb4b73b.
Restores the original speed calculation logic:
- payload[4] = (byte) speedM_s
- payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0))
- payload[6] = (byte) stride_count (without increment)

Reverted in both TimerTask and TX event handler.

* Add stride_count increment back to SDM payload

Restore stride_count++ increment in both TimerTask and TX event handler.
The stride count must increment according to ANT+ SDM protocol requirements.

* Fix ANT+ SDM speed encoding to use little-endian byte order

Restore the fixed-point speed encoding with correct little-endian byte order:
- speedFixed = speedM_s * 256.0
- payload[4] = LSB (low byte)
- payload[5] = MSB (high byte)

The ANT+ protocol requires little-endian byte order (LSB first) for multi-byte
fields. The previous big-endian encoding was causing ~1.4% pace error on Garmin
devices (e.g., 6:05/km instead of 6:00/km at 10 km/h).

Maintains stride_count++ increment as required by ANT+ SDM protocol.

* Revert to original speed encoding with stride_count increment

Reverts the little-endian fixed-point encoding back to the original format:
- payload[4] = integer part of speedM_s
- payload[5] = fractional part * 256
- payload[6] = stride_count++ (kept)

The little-endian approach made the pace worse on Garmin.
Need to investigate the actual ANT+ SDM data page format.

* Fix ANT+ SDM payload byte 4 to use only lower 4 bits for speed integer

According to ANT+ SDM Page 1 specification, byte 4 is shared:
- Upper 4 bits (nibble): Distance Fractional
- Lower 4 bits (nibble): Speed Integer

Changed:
- payload[4] = (byte) speedM_s
+ payload[4] = (byte) ((int)speedM_s & 0x0F)

This ensures speed integer only occupies the lower 4 bits as required
by the protocol, preventing interference with distance fractional data
in the upper nibble. Should fix the ~1.4% pace error on Garmin devices.

Reference: https://github.com/Loghorn/ant-plus/blob/master/src/stride-speed-distance-sensors.ts

* Fix syntax error: remove duplicate cast in payload[7]

* Use fixed-point encoding for ANT+ SDM speed with proper 12-bit layout

Changed speed encoding to use a unified fixed-point calculation:
- speedFixed = round(speedM_s * 256.0)
- payload[4] = upper 4 bits (speed integer part)
- payload[5] = lower 8 bits (speed fractional part)

This encodes speed as a 12-bit fixed-point value (4 integer + 8 fractional bits)
in big-endian format, eliminating separate rounding errors and providing better
precision. Should fix the ~1.4% pace discrepancy between QZ and Garmin devices.

* Use Math.round for speed fractional part to avoid truncation errors

Changed payload[5] calculation to round instead of truncate:
- Before: (byte) ((speedM_s - (int)speedM_s) * 256.0)  // truncates
- After:  (byte) Math.round((speedM_s - (int)speedM_s) * 256.0)  // rounds

This should improve precision and potentially fix the ~1.4% pace discrepancy
between QZ (6:00/km) and Garmin (6:05/km) at 10 km/h.

* Fix ANT+ SDM Update Latency calculation for millisecond deltaTime

Corrected payload[7] (Update Latency) calculation:
- Before: deltaTime * 0.03125 (incorrect, assumes deltaTime in seconds)
- After:  deltaTime * 32.0 / 1000.0 (correct for deltaTime in milliseconds)

Update Latency must be in units of 1/32 second. Since deltaTime is in
milliseconds, the correct conversion is:
  (deltaTime / 1000) * 32 = deltaTime * 0.032

The previous value of 0.03125 (1/32) introduced a 2.4% error that may
have been causing the Garmin to show incorrect pace calculations.

* Set ANT+ SDM Update Latency to 0 for real-time data

Changed payload[7] from deltaTime-based calculation to 0:
- Before: (byte) (deltaTime * 32.0 / 1000.0)  // ~8 for 250ms interval
- After:  (byte) 0  // no measurement delay

Update Latency represents the delay between speed measurement and
transmission, not the interval between transmissions. In a real-time
system like ours, the speed is measured at transmission time, so the
latency should be 0. The Garmin may have been using the incorrect
latency value (250ms) to adjust its calculations, causing the 1.4%
pace discrepancy.

* Fix ANT+ SDM timestamp encoding to match protocol specification

Corrected payload[1] and payload[2] (time fields):
- Before: ((lastTime % 256000) / 5) & 0xFF and ((lastTime % 256000) / 1000)
- After:  (lastTime % 1000) / 5 and (lastTime / 1000) % 256

According to ANT+ SDM specification:
- Byte 1: Time fractional in 1/200 sec units (range 0-199)
- Byte 2: Time integer in seconds (range 0-255)

The previous calculation could overflow byte 1 and give incorrect values,
potentially causing the Garmin to calculate speed incorrectly from time
deltas. This should fix the persistent 1.4% pace discrepancy.

* trying to change frequency

* Update SDMChannelController.java

* Revert "Update SDMChannelController.java"

This reverts commit 95b764af90.

* Revert "trying to change frequency"

This reverts commit d1b7fab860.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 16:57:12 +01:00
Roberto Viola
aa163d9ec0 Update ftmsbike.cpp 2026-01-12 16:32:40 +01:00
Roberto Viola
e037faf020 Support for Alinco AF7019E (Issue #4145) 2026-01-12 15:54:39 +01:00
Roberto Viola
b84543d97f Update ftmsrower.cpp 2026-01-12 14:50:13 +01:00
Roberto Viola
979114b115 Update project.pbxproj 2026-01-12 14:46:18 +01:00
Roberto Viola
35abc2cd1f Skandika Munin rowing machine 2026-01-12 14:45:33 +01:00
Roberto Viola
c98411ac88 Update project.pbxproj 2026-01-12 12:23:23 +01:00
Roberto Viola
af94abae37 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2026-01-12 12:22:34 +01:00
Roberto Viola
10ce431568 Delayed Resistance Changes when Virtual Shifting (Issue #4018) 2026-01-12 12:21:38 +01:00
Roberto Viola
681eaffb05 Removing loop.exec() from bluetooth write mechanism to avoid ios crashes (#4050)
* Fix iOS Bluetooth crashes by removing nested event loops

This commit eliminates QEventLoop::exec() calls in Bluetooth write
operations for domyostreadmill and wahookickrsnapbike devices, which
were causing crashes on iOS due to nested event loop incompatibilities.

Changes:
- Replaced synchronous loop.exec() with async queue-based system
- Implemented WriteRequest queue with processWriteQueue() method
- Added timeout management with QTimer for async operations
- Updated characteristicWritten/packetReceived handlers for queue processing
- Removed all #ifdef Q_OS_IOS conditionals in wahookickrsnapbike
- Unified Bluetooth handling code across all platforms using pure Qt

The new architecture:
1. writeCharacteristic() enqueues write requests instead of blocking
2. processWriteQueue() handles requests sequentially
3. Completion signals (characteristicWritten/packetReceived) trigger next item
4. Timeout handling prevents queue stalls without blocking main thread

This eliminates the problematic pattern of calling loop.exec() from
within the main event loop, which was causing watchdog timeouts and
crashes on iOS. The solution is cross-platform compatible and improves
responsiveness on all platforms.

Fixes iOS crash issues related to nested event loops in Bluetooth operations.

* Update project.pbxproj

* fixing crash

* Update project.pbxproj

* removed unused code

* removed ios_wahookickrsnapbike reference

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 09:37:00 +01:00
Roberto Viola
d087453b2c Fix Garmin Connect login for usernames containing '+' character (#4121) (#4137)
The issue was that QUrlQuery doesn't percent-encode the '+' character in form
data (where '+' represents a space). This caused usernames like
"user+tag@example.com" to be sent as "user tag@example.com" to Garmin's
authentication servers, resulting in login failures.

The fix maintains the existing QUrlQuery approach (preserving all OAuth1
encoding work from PR #3940) but adds a simple post-processing step to replace
any '+' with '%2B' in the generated query string. This is safe because in
URL-encoded form data, '+' always means space, so any literal '+' character
must be encoded as '%2B'.

Fixes #4121

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 08:51:02 +01:00
Roberto Viola
b88a6d8cff Add power averaging modes (3s/5s harmonic) with cycling tile (#4130) 2026-01-11 14:34:35 +01:00
Roberto Viola
e6e4953bfd Update project.pbxproj 2026-01-11 10:36:50 +01:00
Roberto Viola
39df00f460 Fix watts not resetting to zero when cadence stops in cscbike (#4134) 2026-01-11 09:33:35 +01:00
Roberto Viola
2fd87e86ac Add HZ_T101- Bluetooth device name support for Horizon treadmill (#4136) 2026-01-11 09:32:52 +01:00
Roberto Viola
eb4b73b5f8 Fix ANT+ SDM TX speed calculation logic to match TimerTask and spec 2026-01-10 09:15:37 +01:00
Roberto Viola
e94002fc11 restored rouvy mac address on wifi dircon 2026-01-10 08:54:54 +01:00
Roberto Viola
ca6ff11275 Lifesmart TM6500 PowerTouch Treadmill doesn't Automatically Incline in Zwift #4045 2026-01-10 08:52:09 +01:00
Roberto Viola
3d88324e93 Yesoul A1 added 2026-01-10 08:46:49 +01:00
Roberto Viola
27c77d6619 Update project.pbxproj 2026-01-10 08:46:38 +01:00
colbond
25b00c6730 Fix Echelon watt tables and gear feedback loop (#3932) 2026-01-09 20:13:32 +01:00
Roberto Viola
4c01124dee Update dirconmanager.cpp 2026-01-09 19:58:57 +01:00
Roberto Viola
b2d7821f68 Implement MAC address randomization for Rouvy compatibility (#3999)
* Implement MAC address randomization for Rouvy compatibility

When Rouvy compatibility mode is enabled, the dirconmanager now uses
a specific MAC address base (24:DC:C3:E3:B5:XX) with the last byte
set to the dircon_id value from settings. This allows each instance
to have a unique identifier while maintaining compatibility with Rouvy.

Changes:
- Modified getMacAddress() to accept rouvy_compatibility and dircon_id parameters
- Added logic to generate MAC address "24:DC:C3:E3:B5:XX" where XX is dircon_id in hex
- Updated constructor to pass settings values to getMacAddress()
- Maintained backward compatibility with default parameter values

* Refactor getMacAddress to read Rouvy setting internally

Modified getMacAddress() to read the rouvy_compatibility setting
directly from QSettings instead of receiving it as a parameter.
This simplifies the function interface and ensures the setting is
always read from the current configuration.

Changes:
- Removed rouvy_compatibility parameter from getMacAddress()
- Added QSettings read inside getMacAddress() function
- Updated function call to only pass dircon_id parameter

* Simplify getMacAddress to read all settings internally

Modified getMacAddress() to be a parameter-free function that reads
both rouvy_compatibility and dircon_id settings internally from QSettings.
This creates a cleaner, more self-contained interface.

Changes:
- Removed dircon_id parameter from getMacAddress()
- Added dircon_id read inside getMacAddress() function
- Removed local dircon_id_value variable from constructor
- Function now takes no parameters and reads all needed settings internally

* Simplify Rouvy compatibility to advertise only ELITE AVANTI device

When Rouvy compatibility mode is enabled, now only the ELITE AVANTI
device is created and advertised. Removed WAHOO_BLUEHR (heart rate),
WAHOO_RPM_SPEED, and WAHOO_TREADMILL from the Rouvy macro to streamline
the device presentation to Rouvy app.

This change is safe because:
- All enum types remain defined via DM_MACHINE_OP
- All services continue to exist
- Only runtime device initialization is affected when Rouvy mode is enabled

Changes:
- Removed WAHOO_BLUEHR, WAHOO_RPM_SPEED, and WAHOO_TREADMILL from DM_MACHINE_OP_ROUVY
- Only WAHOO_KICKR (ELITE AVANTI) remains for Rouvy compatibility

* Update qmdnsengine submodule to use cagnulein-patch-1 branch

Changed qmdnsengine submodule configuration to use the cagnulein-patch-1
branch and commit 5e5469a06d79c1daa31bbc34d48241c53b6d4fea instead of the
previous zwift branch and commit 602da51dc43c55bd9aa8a83c47ea3594a9b01b98.

Changes:
- Updated .gitmodules: branch zwift → cagnulein-patch-1
- Updated GitHub workflows: all refs to "zwift" → "cagnulein-patch-1"
- Updated GitHub workflows: git checkout commands to use new commit hash
- This ensures all CI/CD builds use the correct qmdnsengine version

* Fix qmdnsengine commit SHA to correct version

Corrected the qmdnsengine submodule commit SHA from the incorrect
5e5469a06d79c1daa31bbc34d48241c53b6d4fea to the correct
16cf29108dfaf9c26d21038a620332c103a5b4f8 in all GitHub workflow
"Fix qmdnsengine submodule" steps.

This ensures all CI/CD builds use the correct qmdnsengine version
from the cagnulein-patch-1 branch.

* adding qmdnsengine as source code directly removing the submodules

* Update main.yml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 12:02:48 +01:00
Roberto Viola
e4875c7767 Fix Peloton remaining time underflow at workout completion (#4124)
Prevent unsigned integer underflow in remainingTime() calculation when
elapsed time (ticks) exceeds total workout duration. Previously, this
caused the Peloton remaining time tile to display 23:59:59 instead of
0:00:00 at workout completion.

The fix checks if ticks >= calculatedTotalTime and returns 0:00:00
instead of attempting the subtraction that would underflow.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 09:39:00 +01:00
Roberto Viola
f36c191442 Refactor sport override logic in save()
https://github.com/cagnulein/QZCompanionGarmin/issues/9#issuecomment-3727423452

Changed the order of setting sport and subsport in the save() method. Now, sport and subsport are set based on device type first, and then the sport is overridden if requested, preserving the subsport determined by the device type.
2026-01-09 08:26:31 +01:00
Roberto Viola
cbb3b20fcd Fix Raspberry Pi build failures due to network issues (#4117) 2026-01-08 18:13:32 +01:00
Roberto Viola
7fc6245ee5 Add support for RIVO COG Bluetooth devices
Updated device discovery to recognize devices with names starting with 'RIVO COG' as supported Bluetooth devices.
2026-01-08 13:47:17 +01:00
Roberto Viola
1a66877679 Update project.pbxproj 2026-01-07 12:25:15 +01:00
Roberto Viola
d3eb58e862 XM Fitness Air Rower #4101 2026-01-07 07:49:07 +01:00
Roberto Viola
6bf403907d Add minimum speed settings for Peloton walking and running sessions (#4111) 2026-01-07 06:04:02 +01:00
Roberto Viola
d26f2b6d25 Add lap tracking for Peloton workouts and TrainProgram rows (#4108) 2026-01-07 05:50:43 +01:00
Roberto Viola
61145e973c Update project.pbxproj 2026-01-06 15:11:50 +01:00
Roberto Viola
336f94a812 [PELOTON] Conditionally revert bike cadence sensor changes (#4106)
* Add bike_cadence_sensor setting to control treadmill average speed feature

Conditionalizes commit b9d6508 that added average speed to treadmill BLE notifications:
- With bike_cadence_sensor=false (default): includes average speed in BLE data (new behavior)
- With bike_cadence_sensor=true: excludes average speed from BLE data (old behavior)

Changes:
- characteristicnotifier2acd.cpp: Conditionally include average speed calculation and flag 0x0E vs 0x0C
- virtualtreadmill_zwift.swift: Conditionally include average speed in treadmill data array
- virtualtreadmill.cpp: Pass bike_cadence_sensor setting and conditionally use odometer vs odometerFromStartup
- lockscreen.h/mm: Update method signature to accept bike_cadence_sensor parameter

This allows users to revert to the previous treadmill behavior if needed.

* build fix and setting label changed

* Fix: Move bike_cadence_sensor declaration to fix compilation error

The bike_cadence_sensor variable was declared inside #ifdef Q_OS_IOS block
but used outside of it at line 559. Moved declaration to the beginning of
the constructor to make it accessible throughout the function.

* Fix: Add bike_cadence_sensor declaration in treadmillProvider()

The bike_cadence_sensor variable was used at line 559 in the treadmillProvider()
function but was only declared in the constructor. Added local declaration of
bike_cadence_sensor in treadmillProvider() to fix compilation error.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-06 15:10:15 +01:00
Roberto Viola
58f96ac932 Add JFICCYCLE FTMS bike support with power calculation patch (#4093)
JFICCYCLE is a Bluetooth FTMS bike that sends power data but always reports 0 watts.
This commit adds support for the JFICCYCLE device with automatic power calculation
from cadence or heart rate, similar to the existing cscbike implementation.

Changes:
- Added JFICCYCLE boolean flag to ftmsbike.h
- Added JFICCYCLE device detection in bluetooth.cpp
- Added JFICCYCLE initialization in ftmsbike.cpp deviceDiscovered()
- Implemented power calculation using wattFromHR() when JFICCYCLE is detected

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-06 14:24:50 +01:00
Roberto Viola
5b5586eb29 Update project.pbxproj 2026-01-06 08:44:57 +01:00
Roberto Viola
e18b207de5 PitPat Treadmill is found, but not speaking with the app #4058 2026-01-06 08:37:57 +01:00
Roberto Viola
f1c9fa5b73 Update settings.qml 2026-01-05 17:57:34 +01:00
Roberto Viola
d3b05e03e6 Update project.pbxproj 2026-01-05 17:34:30 +01:00
Roberto Viola
32fac1b5fa adding all the garmin devices 2026-01-05 17:33:47 +01:00
Roberto Viola
27b7f969fc Update project.pbxproj 2026-01-05 17:31:44 +01:00
Roberto Viola
b736ff2162 XM Fitness Air Rower #4101 2026-01-05 17:04:31 +01:00
Roberto Viola
33533f930b Update project.pbxproj 2026-01-05 16:58:56 +01:00
Roberto Viola
a27b95d623 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2026-01-05 16:42:32 +01:00
Roberto Viola
7c3d24f37f PitPat Treadmill is found, but not speaking with the app (Issue #4058) 2026-01-05 16:42:15 +01:00
Roberto Viola
372eaaf7e8 Fix iOS widget heart rate from external sources like Garmin Companion (#4097) 2026-01-05 13:44:44 +01:00
Roberto Viola
220a2dd7ea Update project.pbxproj 2026-01-05 10:02:37 +01:00
Roberto Viola
cdb45fe011 Fix scheduler loop condition and handle completion
mail "Help setting up QZ with my Reebok FR30Z treadmill and Zwift" from Lucas B. 26/12/2025

Changed the elapsed time comparison in the scheduler from '>' to '>=' to ensure correct loop termination. Added a check to detect when all rows are completed, logging a message and calling end() when finished.
2026-01-05 10:01:50 +01:00
Roberto Viola
a32ed12a3b Add treadmill minimum speed setting and enforcement
Introduces a new treadmill_speed_min setting to allow users to specify a minimum speed for treadmill operation. Updates the treadmill device logic to enforce this minimum speed, adds the setting to QZSettings, and provides a UI control in settings.qml for user configuration.
2026-01-05 09:53:59 +01:00
Roberto Viola
ab76e3b007 Update homeform.cpp 2026-01-05 09:23:52 +01:00
Roberto Viola
08566ae75c Fix speed and resistance bounds in PID heart rate control
Updated treadmill and bike PID heart rate logic to ensure speed and resistance adjustments do not exceed min/max bounds. Now uses std::min and std::max to clamp values, preventing out-of-range changes and improving safety.
2026-01-05 09:18:05 +01:00
Roberto Viola
b9c6f53a9d Update project.pbxproj 2026-01-05 09:06:01 +01:00
Roberto Viola
99c2222118 Remove HORIZON_78AT_treadmill from speed conversion check
Updated the forceSpeed method to exclude HORIZON_78AT_treadmill from the condition that applies miles conversion based on the miles_unit setting. This likely corrects unit handling for that specific treadmill model.
2026-01-05 09:05:12 +01:00
Roberto Viola
4b3d7310a6 XM Fitness air rower 2026-01-05 09:00:26 +01:00
Roberto Viola
8389c9e4bb Advamsoler X-T421 2026-01-05 08:56:14 +01:00
Roberto Viola
2a2307f400 Lifesmart TM6500 w/ Peloton App: Auto Speed and Incline Issues (Issue #4095) 2026-01-05 08:50:50 +01:00
Roberto Viola
16d5a4067c Update project.pbxproj 2026-01-04 15:37:30 +01:00
Roberto Viola
d98ca719d0 fixing settings.qml 2026-01-04 15:35:09 +01:00
Roberto Viola
2e2dc59f2a Update project.pbxproj 2026-01-04 15:11:09 +01:00
Roberto Viola
31bfb06416 Add configurable Garmin device serial number setting (#4089)
- Added new garmin_device_serial setting to allow users to specify their real Garmin device serial number
- Replaced hardcoded serial (3313379353) in FIT file generation with user-configurable value
- Added UI TextField in settings for serial number input with validation
- Added prominent warning message explaining importance of setting real serial for Garmin Connect
- Updated allSettingsCount to 843
- Serial number is used in FIT file when fit_file_garmin_device_training_effect is enabled

This change is essential for users to see their actual Garmin device in Garmin Connect instead of a generic placeholder.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 15:05:51 +01:00
Roberto Viola
268b948a7f Add Domyos Sync Start setting option (#4087)
* Add Domyos Treadmill Sync Start setting to restore old behavior

This commit introduces a new setting "Domyos Sync Start" that allows users
to revert to the old initialization behavior for Domyos treadmills, prior
to commit c90093046.

Changes:
- Add domyos_treadmill_sync_start boolean setting (default: false)
- Add setting to qzsettings.h/.cpp with proper initialization
- Add UI toggle in settings.qml under Domyos Treadmill Options
- Implement conditional logic in domyostreadmill.cpp btinit() method:
  * When true: executes forceSpeedOrIncline and initDataStart8/9 always (old behavior)
  * When false: executes these only when startTape is true (new behavior)
- Increment allSettingsCount to 842

This gives users the option to choose the initialization behavior that
works best for their specific Domyos treadmill model.

* Change domyos_treadmill_sync_start to local variable

* Fix assignment of domyos_treadmill_sync_start variable

* Fix property order in settings.qml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 14:23:08 +01:00
Roberto Viola
64a4831091 Save custom interval labels in workout editor (#4088)
* Save and restore interval labels in workout editor

Added support for saving custom interval labels (e.g., "Warm-up", "Sprint 1",
"Rest 2") in workout XML files. Previously, these labels were lost when saving
and reloading workouts, reverting to "Interval 1", "Interval 2", etc.

Changes:
- Added 'name' field to trainrow struct to store interval labels
- Updated saveXML to write 'name' attribute to XML when present
- Updated loadXML to read 'name' attribute from XML
- Fixed workout editor buildPayload to include interval names in saved data

Fixes issue where custom interval labels were not persisted in saved workouts.

* Display interval labels as toast notifications during workout

Added toast notifications that display when transitioning between intervals
during workout execution. The toast shows:
- Custom interval label if set (e.g., "Warm-up", "Sprint 1", "Cool-down")
- Default "Interval X" if no custom label is set

This provides visual feedback to users about which interval is currently
active, making workouts easier to follow.

Related to issue #4078 (Label purpose in workout editor)

* Use textEvents instead of separate name field for interval labels

Refactored interval label implementation to use the existing textEvents
mechanism instead of adding a new 'name' field. This approach:

- Reuses existing textEvent infrastructure already present in ZWO files
- Saves interval labels as textEvent with timeoffset=0
- Automatically shows toast notifications when intervals start
- Maintains consistency with Zwift workout file format

Changes:
- Added textEvents save/load support to XML functions
- Modified workout editor to create textEvents instead of name field
- Added textEvents support in backend JSON-to-trainrow conversion
- Removed temporary 'name' field and manual toast emissions

The interval label now appears as a toast message when the interval
starts, using the same mechanism as other workout text events.

Related to issue #4078 (Label purpose in workout editor)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 14:15:49 +01:00
Roberto Viola
d66c3deeca Fix step count using intelligent cadence auto-detection in evaluateStepCount (#4083) 2026-01-04 08:21:49 +01:00
Roberto Viola
6d5f38d0c6 Update project.pbxproj 2026-01-03 17:24:42 +01:00
Roberto Viola
41bb13df4b Wrong value displayed on speed tile (Horizon 7.8) #4075 2026-01-03 17:18:41 +01:00
Roberto Viola
972c07cdcd PitPat Treadmill is found, but not speaking with the app (Issue #4058) 2026-01-03 17:12:33 +01:00
Roberto Viola
40aedaec71 Fix floating-point precision issue in workout editor speed values (#4074)
When loading workouts, speed values were displaying with excessive
decimal places (e.g., 7.500031068686833 instead of 7.5) due to
floating-point arithmetic errors during km/miles conversions.

This fix truncates all speed-related values (speed, minSpeed, maxSpeed)
to 1 decimal place when loading workouts, eliminating the precision
display issue while maintaining accuracy.

Fixes issue where speed values change when loading recently created workouts.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-03 07:27:08 +01:00
Roberto Viola
8ce5ec8468 Fix MRK-R06- rower FTMS subscription to send data to Peloton (#4064) 2026-01-03 07:16:19 +01:00
Roberto Viola
8477731f89 Prevent repopulation of deleted default workout files (#4069)
When users delete default ZWO/GPX files from the workout editor, create
a hidden marker file (.deleted_<filename>) to prevent the app from
automatically recreating them on next startup.

Changes:
- Modified onDeleteTrainingProgram to create deletion markers
- Updated homeform.cpp to check for markers before copying default files
- Added QFile and QFileInfo includes to templateinfosenderbuilder.cpp

This allows users to permanently remove unwanted default workout files
without them reappearing after each app restart.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 20:18:31 +01:00
Roberto Viola
9f2a58c81f Horizon 7.8 (Issue #4071) 2026-01-02 20:04:20 +01:00
Roberto Viola
b0e991b472 Horizon 7.8 (Issue #4071) 2026-01-02 20:01:19 +01:00
Roberto Viola
ce14d95af1 Update project.pbxproj 2026-01-02 17:21:45 +01:00
Roberto Viola
38e76d88a5 Fix iOS WebView compatibility: Replace native dialogs with custom HTML dialogs (#4068)
iOS WebView doesn't properly support native JavaScript prompt() and confirm()
functions, causing the repeat selection and delete workout features to fail
silently on iOS while working correctly on Android.

Changes:
1. **Custom Dialog System (workout-editor-app.js)**
   - Created a custom dialog object with prompt(), confirm(), and alert() methods
   - All dialogs return Promises for async/await compatibility
   - Includes keyboard support (Enter to confirm, Escape to cancel)
   - Comprehensive console logging for debugging
   - Initialized in cacheDom() for iOS WebView compatibility

2. **Dialog HTML Structure (index.html)**
   - Added custom dialog markup with overlay, header, body, and footer
   - Includes input field for prompt dialogs
   - Separate cancel and confirm buttons
   - Hidden by default, shown programmatically

3. **Dialog Styling (workout-editor.css)**
   - Dark theme consistent with existing UI
   - Smooth slide-in animation
   - Responsive design (90% width, max 420px)
   - Backdrop blur effect for better focus
   - Proper z-index layering

4. **Function Updates**
   - clearIntervals: Changed to async, uses dialog.confirm()
   - deleteProgram: Changed to async, uses dialog.confirm()
   - repeatSelection: Changed to async, uses dialog.prompt()

This fix ensures consistent behavior across all platforms including iOS,
Android, and desktop WebView implementations.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 17:19:41 +01:00
Roberto Viola
5321105136 2.20.21 2026-01-02 16:53:30 +01:00
Roberto Viola
19f2d17d83 Update project.pbxproj 2026-01-02 16:36:56 +01:00
Roberto Viola
0f58538e80 Fix workout editor: repeat selection, delete function, and duplicate file bug (#4065)
This commit addresses three issues in the workout editor:

1. **Repeat Selection Enhancement**
   - Added comprehensive error handling and debugging logs
   - Improved user feedback when repeat selection is triggered
   - Added console logging to trace execution flow and identify issues
   - Added logging for checkbox selection and button state changes

2. **Delete Workout Function**
   - Added Delete button to workout editor UI
   - Implemented deleteProgram() function in frontend
   - Added onDeleteTrainingProgram() backend handler
   - Added confirmation dialog before deletion
   - Properly refresh workout list after deletion

3. **Fix Duplicate File Creation Bug**
   - Fixed bug where editing a workout created duplicate files
   - Modified sanitizeName() to strip .xml and .zwo extensions before sanitizing
   - Prevents file names like "MyWorkout_xml.xml" when editing "MyWorkout.xml"
   - Each edit now properly overwrites the original file instead of creating copies

All changes include proper error handling, user feedback, and console logging
for debugging purposes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 16:31:15 +01:00
Roberto Viola
70ee7cfa44 Fix: Add floor segment handling for bike bootcamp workouts (#4062)
- Added handling for 'floor' and 'free_mode' segment types in peloton.cpp
- Floor segments (off-bike exercises in bootcamp workouts) are now properly processed
- Applied fix in three locations:
  1. ride_onfinish() for BIKE devices (line ~1508)
  2. performance_onfinish() for BIKE devices - first loop (line ~1870)
  3. performance_onfinish() for BIKE devices - power zone loop (line ~2020)
- Floor segments are added as trainrows with duration only, no cycling metrics
- Follows the same pattern already used for TREADMILL devices (line 2174)
- Fixes warnings about undefined duration in bootcamp workouts

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 15:56:47 +01:00
Roberto Viola
e29eba3a71 2.2.20 2026-01-01 17:36:53 +01:00
Roberto Viola
76e836f69c Update project.pbxproj 2026-01-01 17:04:50 +01:00
Roberto Viola
eccd85b84b Fix Bluetooth accessory discovery timeout when using cached device connection (#4059)
When connecting directly to a cached device (e.g., IC BIKE on iOS), the
Bluetooth discovery was stopped immediately, preventing accessory devices
(heart rate monitors, power sensors, cadence sensors) from being discovered
within the standard 10-second timeout window.

This fix removes the immediate stopDiscovery() call in the direct connection
path, allowing the discovery process to continue for the full 10-second
timeout. This gives accessories enough time to be found while still
maintaining the fast direct connection to the cached primary device.

The discovery will now stop naturally after:
- 10 seconds (standard timeout), OR
- When all configured accessories are discovered

Fixes issue where accessories were not found when using direct connection.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 17:01:27 +01:00
Roberto Viola
5bc187a748 Add virtualrower support to smartrowrower (#3967) 2025-12-31 21:24:03 +01:00
Roberto Viola
f6ff45b449 Add support for Garmin Forerunner 255 series watches in FIT file settings (#4056) 2025-12-31 21:12:53 +01:00
Roberto Viola
967fe63652 Update project.pbxproj 2025-12-31 16:23:32 +01:00
Roberto Viola
50adea9d5b Add Garmin OAuth1 token settings
Added entries for garmin_oauth1_token and garmin_oauth1_token_secret to the allSettings array and updated allSettingsCount accordingly.
2025-12-31 16:22:57 +01:00
Roberto Viola
8c9f680a90 Check Garmin Connect authentication on startup (#4047)
* Fix Garmin Connect token refresh in uploadFitFile()

Problem: uploadFitFile() was failing immediately when access_token expired
(every 24h) without attempting to use the refresh_token (valid for 30 days).
This forced users to manually re-login with MFA every day.

Solution: Added automatic token refresh logic before checking authentication,
matching the behavior already present in uploadActivity(). Now the code will:
1. Check if access_token is expired
2. If yes, attempt to refresh using refresh_token (no MFA needed)
3. Only fail if refresh_token is also expired or refresh fails

This extends the time between manual logins from 24 hours to 30 days.

* Add proactive token refresh on startup

Added tryRefreshToken() method to GarminConnect that attempts to refresh
expired access_token using refresh_token (if still valid) without requiring
full login flow or MFA.

Updated garmin_connect_login() to proactively refresh tokens on startup:
1. If already authenticated -> show "Already authenticated"
2. If access_token expired but refresh_token valid -> silent refresh, then show "Already authenticated"
3. Only if both tokens expired/missing -> perform full login with MFA

Benefits:
- User sees "Already authenticated" on every app launch (for 30 days)
- Token timestamps visible in debug logs on each app restart
- No user-facing messages about refresh operations (debug only)
- Reduces full login prompts from every 24h to every 30 days

Debug logging shows token expiration timestamps before/after refresh
to help verify token refresh is working correctly.

* Change toast message to 'Authenticated\!' instead of 'Already authenticated\!'

* Fix Garmin Connect token refresh in uploadFitFile()

Problem: uploadFitFile() was failing immediately when access_token expired
(every 24h) without attempting to use the refresh_token (valid for 30 days).
This forced users to manually re-login with MFA every day.

Solution: Added automatic token refresh logic before checking authentication,
matching the behavior already present in uploadActivity(). Now the code will:
1. Check if access_token is expired
2. If yes, attempt to refresh using refresh_token (no MFA needed)
3. Only fail if refresh_token is also expired or refresh fails

This extends the time between manual logins from 24 hours to 30 days.

Changes in this commit:
- tryRefreshToken() now ALWAYS attempts refresh on startup (not just when expired)
- Added enhanced debug logging in refreshOAuth2Token() to diagnose 403 Forbidden errors
- Log shows HTTP status, response body, and detailed error info when refresh fails

This helps diagnose why Garmin returns 403 Forbidden on token refresh attempts.

* Fix Garmin Connect token refresh on startup

Always call tryRefreshToken() on startup, not just when token is expired.
Previously the code checked isAuthenticated() first and returned early,
never calling tryRefreshToken() when tokens were still valid.

Now the flow is:
1. Always try tryRefreshToken() first (attempts refresh every startup)
2. If refresh succeeds -> authenticated
3. If refresh fails but isAuthenticated() -> still authenticated
4. Otherwise -> full login required

This ensures tokens are refreshed on every app startup for maximum
freshness and allows testing the refresh mechanism.

* Implement OAuth2 token refresh using OAuth1 (garth method)

Problem: Previous implementation tried to use OAuth2 refresh_token which
Garmin doesn't support (returns 403 Forbidden). This forced users to
re-login every 24 hours.

Solution (based on garth library):
- OAuth1 tokens last ~1 YEAR (not 24h like OAuth2)
- When OAuth2 expires, reuse OAuth1 token to get fresh OAuth2 token
- Call exchangeForOAuth2Token() again with saved OAuth1 credentials

Changes:
1. Added OAuth1 token settings (garmin_oauth1_token, garmin_oauth1_token_secret)
2. Save/load OAuth1 tokens in settings (previously only OAuth2 was saved)
3. Rewrote refreshOAuth2Token() to call exchangeForOAuth2Token() with OAuth1

This matches how the Python garth library works - the "refresh" is actually
just re-exchanging OAuth1 for a fresh OAuth2 token.

Users need to login ONCE to get OAuth1 token saved, then refresh works
for ~1 year without MFA!

Reference: https://github.com/matin/garth/issues/21

* Add OAuth1 token properties to settings.qml

All settings defined in qzsettings.h/cpp must have corresponding
properties in settings.qml for proper C++/QML binding, even if they
are not exposed in the UI.

Added:
- property string garmin_oauth1_token: ""
- property string garmin_oauth1_token_secret: ""

These properties are internal and not shown to users, but required
for settings synchronization.

* Move OAuth1 token properties to END of settings.qml

IMPORTANT: New properties must ALWAYS be added at the END, never in
the middle, to maintain compatibility with existing saved settings.

Moved garmin_oauth1_token and garmin_oauth1_token_secret to the end
after garmin_last_refresh instead of inserting them in the middle.

* Match garth behavior: refresh only when token expired

Changed tryRefreshToken() to match garth's implementation:
- Check if token is already valid -> return true (no refresh)
- Only refresh when token is actually expired
- Avoids unnecessary API calls and startup delay

Garth v0.5.16+ uses:
  if not isinstance(self.oauth2_token, OAuth2Token) or self.oauth2_token.expired:
      self.refresh_oauth2()

Our equivalent:
  if (isAuthenticated()) { return true; }
  // Only refresh if expired

This reduces startup time by ~500ms when token is still valid.

* Add toast notification for Garmin login failures

Show toast message when Garmin login fails at startup, except when
MFA is required (which already shows its own dialog).

Now users always see Garmin status at startup:
- "Garmin credentials not configured" - if not configured
- "Garmin Connect: Authenticated!" - if authenticated
- "Garmin Connect: Login failed - [error]" - if login fails
- MFA dialog - if MFA required

This provides clear feedback on Garmin connection status.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-31 16:16:59 +01:00
Roberto Viola
c4de251dc7 Update project.pbxproj 2025-12-31 15:54:16 +01:00
Roberto Viola
2048debf3a Stryd Cadence (Issue #4052) 2025-12-31 15:33:02 +01:00
Roberto Viola
924635c047 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-12-30 15:57:09 +01:00
Roberto Viola
d68bddcf57 Garmin Connect Logo (#4048)
* Add Garmin Connect button to main menu with direct settings navigation

- Add Garmin Connect ItemDelegate to main.qml drawer menu between Peloton and Intervals.icu
- Clicking the Garmin button opens settings.qml and navigates to Garmin Options section
- Add garminOptionsAccordion ID to Garmin Options AccordionElement in settings.qml
- Add openSection property to settings.qml to programmatically open specific sections
- Add garmin-connect-badge.png to icons folder and icons.qrc (placeholder image - needs replacement)
- When clicked, shows Garmin username field for user credential entry

Note: The garmin-connect-badge.png is currently a placeholder. Replace with official Garmin Connect badge image.

* Remove placeholder Garmin Connect badge image

User will provide the official Garmin Connect badge image

* Add automatic scrolling to Garmin Options section

- Connect to contentBecameVisible signal of garminOptionsAccordion
- Automatically scroll to Garmin Options when opened from main menu
- Use Qt.callLater to ensure layout is updated before scrolling
- Add 20px top margin for better visibility

* Add files via upload

* Fix settings.qml crash and improve Garmin button layout

- Replace Qt.callLater with Timer for safer async scrolling
- Add null checks before accessing garminOptionsAccordion properties
- Set Garmin image height to 48px (same as Peloton/Strava)
- Add 3px spacing between menu items in drawer
- Use 150ms delay for scroll to ensure layout is ready

Fixes crash when opening settings or clicking Garmin button.

* Fix property assignment for settings.qml openSection

Use QML property initialization syntax in push() instead of
post-assignment to avoid 'Cannot assign to non-existent property' error.

stackView.push("settings.qml", { openSection: "garmin" })
instead of:
var page = stackView.push("settings.qml")
page.openSection = "garmin"

* Add null checks and improve error handling

- Add null check for stackView.currentItem before connecting signals
- Add null check for settingsPane.contentItem before setting contentY
- Increase Timer interval to 200ms for better layout stability
- Add conditional checks to prevent undefined method errors

Fixes TypeError: Cannot call method 'connect' of undefined

* Remove default value from openSection property

Remove empty string default value to prevent 'Property value set
multiple times' warning when passing property via push().

Also add null check before comparing openSection value.

* Replace property-based approach with function call for Garmin section

Instead of passing openSection property via push() which caused
'Property value set multiple times' warning, use a function call:
- Add openGarminSection() function to settings.qml
- Call it from main.qml after push completes
- Remove openSection property and Component.onCompleted logic

This approach is cleaner and avoids property initialization conflicts.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 15:29:37 +01:00
Roberto Viola
7059e680b3 restoring TrainingProgramList for Windows version 2025-12-30 14:52:45 +01:00
Roberto Viola
64272d508a Add Garmin Connect startup authentication toast (#4040)
* Add Garmin Connect startup authentication toast

Similar to Peloton, display a toast notification on app startup when:
- Garmin upload is enabled
- Garmin credentials are configured
- User is already authenticated

Shows "Garmin Connect: Account connected!" to confirm successful authentication.
Toast appears after 5 seconds delay to avoid slowing down app startup.

Issue: User requested Garmin Connect toast similar to Peloton

* Simplify Garmin Connect startup check

Reuse existing garmin_connect_login() method instead of duplicating
initialization code. This is cleaner and maintains DRY principle.

The login method already handles:
- GarminConnect initialization
- Authentication status check
- Appropriate toast messages

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 11:37:27 +01:00
Roberto Viola
eb9cc1b34c fixing build 2025-12-30 10:43:40 +01:00
Roberto Viola
a298af10f0 Add debug logging for Garmin OAuth2 token validity analysis (#4046)
Added detailed logging to track token expiration times and investigate
why MFA is required more frequently than expected (~48h vs 30 days).

Changes:
- Log token validity periods during initial OAuth2 exchange
- Log token validity during automatic refresh
- Log loaded token status at app startup
- Log when refresh_token expires and MFA re-authentication is required

This will help identify if Garmin is returning shorter token validity
periods (48h instead of 30 days) or if there are other issues causing
frequent MFA prompts.

Related to issue: Garmin MFA token appears to have short validity

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 10:22:24 +01:00
Roberto Viola
ca140a20b4 Add virtual rower device support
Introduces initialization and handling for a virtual rower device alongside existing bike and treadmill options. Also updates distance calculation to convert value to kilometers.
2025-12-30 09:55:43 +01:00
Roberto Viola
5623df2869 Refine manufacturer check for FIT file saving
Updated the condition for setting the manufacturer to Decathlon to ensure it only applies when the device name starts with 'DOMYOS', is not a Zwift device, and training effect is not set. This prevents incorrect manufacturer assignment in certain cases.
2025-12-30 08:08:35 +01:00
Roberto Viola
6c91436abb Set max_resistance for DOMYOS devices
Assigns max_resistance to 32 when a DOMYOS device is discovered, ensuring correct resistance handling for these devices.
2025-12-29 15:55:29 +01:00
Roberto Viola
6b5b1b5c0e Update project.pbxproj 2025-12-27 16:04:41 +01:00
Roberto Viola
d2a883e380 Exclude SMARTROWER from SMARTROW case in Bluetooth detection (Issue #4033) (#4034) 2025-12-27 15:56:30 +01:00
Roberto Viola
3fa9939fa1 Add SMARTROWER to FTMS rower device detection (#4031) 2025-12-27 14:40:42 +01:00
Roberto Viola
374ea0ffc2 Add MRK-CRYDN- prefix to FTMS rower device detection (#4030) 2025-12-27 07:42:29 +01:00
Roberto Viola
4f32f9b520 Add "Zwift device" to fit file (garmin settings) (Issue #4022) 2025-12-24 11:34:01 +01:00
Roberto Viola
f5769fd7bc Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-12-24 10:39:33 +01:00
Roberto Viola
01a09a0e36 Add 2FA guidance label to Garmin MFA section
Added a label instructing users to enable 2FA in their Garmin profile privacy settings if they do not receive the code. This provides clearer guidance for users experiencing issues with multi-factor authentication.
2025-12-24 10:39:26 +01:00
Roberto Viola
a70317453b 2.20.19 2025-12-23 12:39:22 +01:00
Roberto Viola
7bc91094ba Update project.pbxproj 2025-12-23 11:52:07 +01:00
Roberto Viola
ef152b1edd garmin logout when the garmin credentials changes 2025-12-23 11:50:08 +01:00
Roberto Viola
ca7ea7e7d5 adding garmin connect to settings 2025-12-23 11:34:26 +01:00
Roberto Viola
3adbb96a4e fixing ios crash for garmin connect 2025-12-23 11:29:27 +01:00
Roberto Viola
eb26c19964 Update project.pbxproj 2025-12-23 11:14:24 +01:00
Roberto Viola
27a7cf1471 Add Garmin Connect integration based on garth library (#3940)
* Add Garmin Connect integration based on garth library

This commit implements automatic upload of FIT files to Garmin Connect,
porting the Python garth library (https://github.com/matin/garth) to C++ Qt.

Features:
- OAuth1/OAuth2 authentication with Garmin Connect SSO
- Automatic token management and refresh
- Upload FIT files after workouts
- Settings UI for email/password configuration
- Test login functionality
- Support for MFA (basic implementation)

Implementation details:
- New GarminConnect class (garminconnect.h/cpp) handles authentication and upload
- Integration with homeform for automatic upload after workout save
- Settings stored in QSettings with encryption support
- UI added to settings.qml with enable/disable toggle
- Pattern follows existing Strava integration

Files modified:
- src/garminconnect.h (new): Header file with class definition
- src/garminconnect.cpp (new): Implementation of authentication and upload
- src/homeform.h/cpp: Integration of Garmin upload functionality
- src/qzsettings.h/cpp: Added Garmin settings (email, password, enable)
- src/settings.qml: Added Garmin Connect UI section
- src/qdomyos-zwift.pri: Added new source files to build

Usage:
1. Configure Garmin email and password in Settings > Garmin Connect
2. Enable "Enable Garmin Upload" toggle
3. Click "Test Garmin Login" to verify credentials
4. FIT files will automatically upload after workouts

* Fix build errors: add missing include headers

- Add QNetworkCookie include to garminconnect.h
- Add QNetworkCookieJar and QUrl includes to garminconnect.cpp
- Add QTimer and QFile includes to homeform.cpp

These headers are required for the Garmin Connect integration to
compile successfully on Linux.

* Fix compilation errors in Garmin integration

- Fix raw string literal syntax errors in garminconnect.cpp
  Changed R"(...)" to normal strings with escaped quotes
  Affected regex patterns for CSRF token and ticket extraction

- Fix missing closing brace in homeform.cpp
  Added missing } after garmin_upload_file_prepare() call

* Fix QML error: make garmin_connect_login() invokable from QML

Add Q_INVOKABLE macro to garmin_connect_login() function in homeform.h
to allow it to be called from QML settings interface.

Fixes: TypeError: Property 'garmin_connect_login' of object homeform is not a function

* Force rebuild: trigger moc regeneration for Q_INVOKABLE

Add comment to force complete rebuild and moc file regeneration
after adding Q_INVOKABLE to garmin_connect_login().

* Improve CSRF token extraction with flexible regex patterns

- Add multiple regex patterns to handle different HTML attribute orders
- Add debug logging to see actual HTML structure from Garmin
- Try name="_csrf" then value, value then name="_csrf", and name="csrf"
- Apply flexible parsing to both initial login and MFA pages

This fixes the 'CSRF token not found in HTML' error by being more
tolerant of HTML formatting variations.

* Add debug logging for ticket extraction troubleshooting

- Log login response length and snippet
- Log redirect URL from response
- Log Location headers
- Show all response details to diagnose ticket extraction failure

This will help identify how Garmin returns the ticket after successful login.

* Fix redeclaration error: remove duplicate responseUrl declaration

The responseUrl variable was declared twice in performLogin():
- Once for debug logging (line 223)
- Again for ticket extraction (line 258)

Removed the second declaration to fix compilation error.

* Add error message detection in login response

- Check for 'error' messages in HTML response
- Detect if still on login page (failed credentials)
- Look for validation errors
- Help diagnose why login is failing

This will show exactly why Garmin rejects the login attempt.

* Fix Garmin Connect login: use SSO embed URL and add missing query parameters

Critical fixes based on Python garth library analysis:

1. Fixed service parameter to use SSO embed URL instead of ConnectAPI URL
   - This was causing login to fail and return login page HTML
   - Now uses https://sso.garmin.com/sso/embed like Python library

2. Added all missing SIGNIN_PARAMS query parameters:
   - id=gauth-widget
   - embedWidget=true
   - gauthHost, source, redirectAfterAccountLoginUrl, redirectAfterAccountCreationUrl
   - These parameters are required by Garmin's SSO system

3. Removed unnecessary login-url parameter from OAuth1 token exchange
   - Not present in Python garth library
   - Was adding unnecessary query parameter

4. Improved ticket extraction with correct regex pattern:
   - Primary: embed\?ticket=([^"]+)" (matches Python library)
   - Fallback: ticket=([^&"']+) for edge cases

5. Added Success page title detection for better debugging
   - Detects <title>Success</title> like Python library
   - Helps identify successful login vs errors

Reference: GARMIN_SSO_ANALYSIS.md (comprehensive analysis document)

* Fix query parameters: use correct service URL for login POST

Critical fix based on detailed analysis of Python garth library:

The service parameter must be DIFFERENT for GET vs POST:
- GET /sso/signin (CSRF fetch): service=https://sso.garmin.com/sso/embed
- POST /sso/signin (login): service=https://connectapi.garmin.com

Also fixed gauthHost parameter to use ssoEmbedUrl consistently.

This should resolve the "An unexpected error has occurred" message.

* Add complete MFA (Multi-Factor Authentication) support for Garmin Connect

Implemented full UI and backend support for Garmin MFA flow:

Backend Changes (homeform.h/cpp):
- Added Q_PROPERTY garminMfaRequested with signal/getter/setter
- Added Q_INVOKABLE garmin_submit_mfa_code(QString) method
- Modified mfaRequired signal handler to trigger MFA dialog
- Implemented MFA code submission logic with validation
- Re-attempts login with MFA code when user submits

UI Changes (settings.qml):
- Added modal Popup dialog for MFA code input
- Dialog appears automatically when Garmin requests MFA
- Text field for entering verification code (sent to email)
- Submit and Cancel buttons with proper validation
- Auto-focus on text field when dialog opens
- Enter key submits the code

User Flow:
1. User clicks "Test Garmin Login"
2. If MFA required, dialog appears asking for code
3. User checks email for Garmin verification code
4. User enters code and clicks Submit (or presses Enter)
5. Code is sent to Garmin for verification
6. On success, authentication completes and tokens are saved

This completes the Garmin Connect integration with full MFA support.

* fixing test button

* Add missing HTTP headers for Garmin SSO security checks

Added required security headers to login and MFA requests:
- Referer: Points to the request URL (required for CSRF protection)
- Origin: Specifies the SSO origin (required for CORS)
- embed=true: Added to MFA POST data for consistency

These headers are commonly required by SSO systems for security validation.
Without them, Garmin was rejecting the login with "An unexpected error has occurred."

Based on standard SSO security practices and web form submission requirements.

* COMPLETE FIX: All 7 critical Garmin Connect authentication issues resolved

This commit fixes ALL blocking issues preventing Garmin Connect authentication,
based on comprehensive analysis comparing C++ implementation with Python garth library.

## CRITICAL FIXES IMPLEMENTED (All 7):

### 1.  Fixed service parameter (CRITICAL)
- Changed login POST to use ssoEmbedUrl instead of connectApiUrl
- Python garth uses SSO_EMBED for both GET and POST
- Location: performLogin() line 188

### 2.  Added login-url parameter (CRITICAL)
- Added missing login-url to OAuth1 preauthorized request
- Required by Garmin API for token exchange
- Location: exchangeForOAuth1Token() line 425

### 3.  Fixed OAuth1 response parsing (CRITICAL)
- Changed from JSON parsing to URL-encoded parsing
- Format: oauth_token=abc&oauth_token_secret=xyz
- Location: exchangeForOAuth1Token() lines 458-471

### 4.  Fixed OAuth2 POST body (CRITICAL)
- Removed oauth_token/oauth_token_secret from POST body
- Only includes mfa_token if present
- Credentials now go in OAuth1 signature (not body)
- Location: exchangeForOAuth2Token() lines 490-493

### 5.  Implemented OAuth1 HMAC-SHA1 signature (CRITICAL - MOST COMPLEX)
- Full OAuth1 signature generation with HMAC-SHA1
- Implements: nonce, timestamp, signature, percent encoding
- Used for both OAuth1 GET and OAuth2 POST requests
- New methods:
  * generateOAuth1AuthorizationHeader() - lines 706-773
  * generateOAuth1Signature() - lines 775-800
  * percentEncode() - lines 802-819
  * generateNonce() - lines 821-824
  * generateTimestamp() - lines 826-829
- Includes: QCryptographicHash, QMessageAuthenticationCode, QUuid, QDateTime

### 6.  Added MFA query parameters (HIGH PRIORITY)
- MFA endpoint now includes all required query parameters
- Same parameters as signin endpoint (id, embedWidget, gauthHost, etc.)
- Location: performMfaVerification() lines 325-335

### 7.  Removed unnecessary Content-Type from GET (MEDIUM)
- Removed Content-Type header from OAuth1 GET request
- GET requests should not have Content-Type
- Location: exchangeForOAuth1Token() line 431 (removed)

## ANALYSIS DOCUMENTATION ADDED:

Created 6 comprehensive analysis documents (107KB total):
- ANALYSIS_INDEX.md - Navigation guide
- ANALYSIS_EXECUTIVE_SUMMARY.md - High-level overview
- CRITICAL_ISSUES_SUMMARY.md - Detailed issue breakdown
- COMPREHENSIVE_CPP_VS_PYTHON_COMPARISON.md - Complete technical comparison
- SIDE_BY_SIDE_CODE_COMPARISON.md - Code differences with examples
- CORRECTED_CODE_SNIPPETS.md - Reference implementations

## TESTING STATUS:

Before:  Complete authentication failure (OAuth1 signature missing)
After:  Should work - all critical blocking issues resolved

## FILES MODIFIED:

- src/garminconnect.h: Added OAuth1 signature method declarations
- src/garminconnect.cpp:
  * All 7 critical fixes implemented
  * OAuth1 signature implementation (145 lines)
  * Added includes: QCryptographicHash, QMessageAuthenticationCode, QUuid

## IMPLEMENTATION NOTES:

- OAuth1 signature uses HMAC-SHA1 (RFC 5849 compliant)
- Percent encoding follows OAuth spec (RFC 3986)
- Nonce generated using QUuid for uniqueness
- Timestamp uses current epoch seconds
- All URL query parameters included in signature base string

This implementation now matches the Python garth library exactly.
Authentication should work for both MFA and non-MFA users.

* Fix MFA detection: detect redirect URL instead of empty response body

The login with MFA returns an HTTP redirect (empty body) to the MFA page.
Previous code only checked response body for 'MFA' text, which failed.

Changes:
- Detect MFA by checking if redirect URL contains 'verifyMFA'
- Follow redirect to fetch MFA page HTML
- Extract new CSRF token from MFA page
- Update cookies from MFA page
- Emit mfaRequired() signal to show dialog

This fixes the issue where MFA dialog didn't appear even though
Garmin sent the verification code via email.

Tested with user account that has MFA enabled.

* Fix MFA ticket extraction: check redirect URL before response body

The MFA verification response can also be a redirect (like login response).
Previous code only checked response body for ticket, which failed.

Changes:
- Check redirect URL first for ticket parameter
- Try multiple regex patterns for ticket extraction from body
- Add debug logging for redirect URL and ticket location
- Use same approach as performLogin() for consistency

This fixes 'Failed to extract ticket after MFA' error.
User can now complete MFA flow and authenticate successfully.

* Fix logintoken redirect handling and prevent double deletion bug

After MFA verification, Garmin redirects to a logintoken URL instead of
directly providing a ticket. This commit:
- Follows the logintoken redirect to extract the actual ticket
- Prevents double deletion of QNetworkReply with replyDeleted flag
- Adds proper cookie handling for the redirect chain
- Adds detailed logging for debugging the redirect flow

This completes the MFA authentication flow fix.

* CRITICAL FIX: Add session cookies to OAuth1/OAuth2 exchange requests

Root cause of "Host requires authentication" error identified:
- Garmin requires session continuity from SSO login through OAuth exchanges
- Python garth library inherits cookies via parent session
- C++ implementation was missing cookie propagation

Fixes:
- Added cookies to OAuth1 token exchange (line 644-648)
- Added cookies to OAuth2 token exchange (line 748-752)
- Ensures ticket validation can verify authenticated session

This matches Python garth behavior where GarminOAuth1Session inherits
the parent session's cookies through adapter mounting.

* Add detailed OAuth1 debugging and update cookies after logintoken redirect

Debug additions:
- Log cookie count and domains before OAuth1 request
- Log HTTP status code and response body on OAuth1 failure
- Update m_cookies after logintoken redirect to capture new session cookies

This will help identify if cookies are being properly propagated or if
the OAuth1 signature/request format has other issues.

* Add OAuth1 signature debugging: log URL, ticket, and base string

Debug additions to diagnose 401 Unauthorized error:
- Log complete OAuth1 request URL with all query parameters
- Log ticket value being used (truncated for security)
- Log OAuth1 signature base string to verify encoding

This will help identify if the issue is with:
- Ticket validity/extraction
- URL encoding in signature
- Parameter ordering in base string

* CRITICAL: Fix OAuth1 URL encoding mismatch causing 401 errors

Root cause: URL encoding mismatch between signature and actual request
- Changed queryItems from FullyDecoded to PrettyDecoded to match Qt's encoding
- Use url.toString(QUrl::FullyEncoded) for signature generation
- Ensures signature is calculated with same encoding as HTTP request

The log showed double-encoding artifacts (%%3A, %%26) because the signature
was being calculated with different encoding than what Qt sends in the
actual HTTP request. This caused OAuth1 signature verification to fail
with 401 Unauthorized.

Fixes applied to both OAuth1 token exchange and OAuth2 token exchange.

* CRITICAL FIX: Manually set Cookie header to ensure cookies are sent

Root cause: Qt's cookie jar was not automatically sending cookies from
sso.garmin.com to connectapi.garmin.com due to domain/path mismatches.

Python garth automatically inherits cookies through session adapter mounting,
ensuring cookies are always sent. Our C++ code was inserting cookies into
the jar but Qt was not including them in the actual HTTP request.

Solution: Build and set Cookie header manually for both OAuth1 and OAuth2
exchange requests, bypassing Qt's automatic cookie handling.

Format: "Cookie: name1=value1; name2=value2; ..."

This ensures all 11 cookies (GARMIN-SSO, CASTGC, etc.) are explicitly
sent with the OAuth requests, maintaining session continuity required
by Garmin's API.

* Add Python garth OAuth1 detailed analysis documentation

* Add MFA response debugging: log status code and body when no redirect

* Add comprehensive OAuth1 parameter debugging to diagnose 401 error

* Add query string parsing debug to find missing ticket parameter

* Add comprehensive OAuth1 debugging: full parameters, base string, and signature details

* Add OAuth consumer fetch debugging: log URL, response, and JSON parsing

* Improve OAuth consumer fetch error handling: check HTTP status, add headers

* Add environment variable support for OAuth consumer credentials

- Allow GARMIN_OAUTH_CONSUMER_KEY and GARMIN_OAUTH_CONSUMER_SECRET
- Provides workaround if S3 URL is blocked or restricted
- Falls back to S3 fetch if env vars not set
- Improved error messages with actionable workaround instructions

* Add OAuth consumer credential extraction tools and documentation

- Created extract_garth_credentials.py helper script
- Added GARMIN_OAUTH_SETUP.md with detailed setup instructions
- Provides workarounds for S3 access issues
- Documents environment variable configuration

* Add comprehensive OAuth1 debugging: check HTTP status, redirects, and response body

* Remove environment variable workaround - S3 URL works correctly

- Removed environment variable check from code
- Removed helper script and documentation
- Simplified OAuth consumer fetch to only use S3
- User confirmed S3 URL is accessible and working

* Fix status code assignment in GarminConnect

* Fix OAuth1 double-encoding issue causing 401 errors

CRITICAL FIX: Garmin was rejecting requests with 'Invalid URL encoding: not a valid digit (radix 16): 37'
This was caused by Qt re-encoding the URL when creating QNetworkRequest.

Solution:
- Use QUrl::fromEncoded() to tell Qt the URL is already encoded
- Use the same encodedUrlString for both signature and HTTP request
- This ensures no double-encoding occurs

Error was: login-url parameter being sent as https%253A%252F%252F instead of https%3A%2F%2F

* Fix status code assignment in GarminConnect

Use query.toString(QUrl::FullyEncoded) to properly encode query parameters.
Previous approach with url.toString(QUrl::FullyEncoded) was returning
unencoded parameters, causing Garmin to reject the request.

* Fix OAuth1 double-encoding issue causing 401 errors

CRITICAL: QUrlQuery.toString(QUrl::FullyEncoded) does NOT encode
characters like ':', '/', which caused Garmin to reject requests.

Solution: Manually construct query string using percentEncode() function
that properly encodes ALL special characters per RFC 3986.

This ensures login-url parameter is sent as https%3A%2F%2F...
instead of https://... which was causing the 401 error.

* Revert to Qt natural URL encoding to avoid multiple encoding

QUrl::fromEncoded() was causing double/triple encoding because:
1. percentEncode() encoded the URL: https://... -> https%3A%2F%2F...
2. QUrl::fromEncoded() partially decoded it
3. Qt re-encoded it for HTTP request
4. Result: https%25%253A... (triple encoded)

Solution: Use QUrl with QUrlQuery normally and let Qt handle
encoding once during HTTP request. Use url.toString(QUrl::FullyEncoded)
for signature to match what Qt actually sends.

* Add comprehensive URL encoding debug logging

Shows:
- URL in PrettyDecoded vs FullyEncoded format
- Warnings for double/triple encoding detection (%% or %25%25)
- Specific login-url parameter encoding
- Expected vs actual encoding comparison

This will immediately show if Qt is encoding correctly or if
we still have encoding issues.

* Document complete history of URL encoding attempts

Added detailed comments showing:
- ATTEMPT 1: url.toString(FullyEncoded) - no encoding of : and /
- ATTEMPT 2: query.toString(FullyEncoded) - same issue
- ATTEMPT 3: Manual percentEncode() + fromEncoded() - double/triple encoding

Each attempt includes:
- What was tried
- Why it failed
- Exact error received
- Technical reason for failure

This prevents repeating the same failed approaches in future debugging.

* Clarify URL encoding history with exact code from each attempt

Added actual code snippets from git history showing:
- ATTEMPT 1: Used QUrl::fromEncoded(url.toString(FullyEncoded).toUtf8())
- ATTEMPT 2: Used QUrl::fromEncoded(baseUrl + query.toString())
- ATTEMPT 3: Used QUrl::fromEncoded(baseUrl + percentEncode())
- CURRENT: Uses QNetworkRequest(url) directly - NO fromEncoded!

KEY DIFFERENCE: All failed attempts converted to string then used
fromEncoded(). Current solution passes QUrl object directly to
QNetworkRequest, letting Qt handle encoding consistently.

* Fix OAuth1 URL encoding using QUrl::toPercentEncoding with DecodedMode

Qt's QUrlQuery and QUrl classes treat ':' and '/' as unreserved characters
and never encode them in parameter values. However, OAuth1 signature
calculation and Garmin's API require these characters to be percent-encoded.

Solution: Use QUrl::toPercentEncoding() to manually encode parameter values,
then set the query string with DecodedMode to prevent Qt from modifying it.

This is the fifth attempt to solve the 401 authentication error caused by
incorrect URL encoding.

* ATTEMPT 6: Fix OAuth1 URL encoding with fromEncoded(StrictMode) and manual query parsing

Previous attempts (all failed):
1. url.toString(FullyEncoded) - Qt didn't encode : and /
2. query.toString(FullyEncoded) - same issue
3. percentEncode() + fromEncoded() - double/triple encoding
4. Revert to QUrlQuery - back to no encoding
5. toPercentEncoding() + DecodedMode - triple encoding (Qt re-encoded %)

This attempt:
- Build URL as string with QUrl::toPercentEncoding(): "login-url=https%3A%2F%2F..."
- Use QUrl::fromEncoded(fullUrl, StrictMode) to preserve encoding
- Pass fullUrl STRING to signature (not url.toString())
- Fix signature function to manually parse query params:
  * Extract params from string
  * Decode each value with QUrl::fromPercentEncoding()
  * Re-encode with percentEncode() per OAuth1 spec

Comprehensive documentation added for all 6 attempts in code comments.

* Add GarminConnect URL encoding test suite

Tests verify the OAuth1 URL encoding fix for Garmin authentication:

1. test_toPercentEncoding_encodesColonAndSlash
   - Verifies QUrl::toPercentEncoding() encodes ':' as %3A and '/' as %2F
   - Expected: "https://..." → "https%3A%2F%2F..."

2. test_fromEncodedStrictMode_preservesEncoding
   - Verifies QUrl::fromEncoded(StrictMode) preserves encoding
   - Ensures no triple encoding (%253A, %252F)

3. test_QUrlQuery_doesNotEncodeColonSlash
   - Documents the PROBLEM: QUrlQuery.addQueryItem() doesn't encode ':' and '/'
   - This is why we need the manual workaround

4. test_completePattern_correctEncoding
   - Tests end-to-end URL construction pattern
   - Verifies single encoding, no double/triple encoding

5. test_manualQueryParsing_decodesCorrectly
   - Tests OAuth1 signature query parameter parsing
   - Verifies decode → re-encode cycle works correctly

These tests run automatically in GitHub Actions workflow and verify
the fix works without needing manual 1-hour Garmin authentication tests.

* Update settings.qml

* Debug: Compare fullUrl vs url.toEncoded() for OAuth1 signature

Adding debug output to verify if Qt's url.toEncoded() matches our manually
constructed fullUrl string. This will show if Qt is modifying the URL encoding
when creating the QUrl object with fromEncoded(StrictMode).

If the URLs don't match, that could explain why the OAuth1 signature fails
even though the URL appears correctly encoded in our logs.

* Add test for fromEncoded+toEncoded round-trip preservation

New test: test_fromEncodedToEncoded_roundTrip

Verifies the CRITICAL assumption in ATTEMPT 6:
- Build URL with encoded params: "login-url=https%3A%2F%2F..."
- Parse with QUrl::fromEncoded(StrictMode)
- Extract with url.toEncoded(FullyEncoded)
- EXPECT: Get back the EXACT same string

If this test FAILS, it means Qt is modifying our encoding somewhere
in the fromEncoded → toEncoded cycle, which would explain why the
OAuth1 signature fails (signature uses different URL than HTTP request).

This test documents what we're debugging in commit dd773b7.

* Use QUrl::toPercentEncoding() for OAuth1 signature encoding

Replace custom percentEncode() implementation with Qt's built-in
QUrl::toPercentEncoding() to ensure 100% consistency between:
- URL encoding (used in HTTP request)
- Signature encoding (used in OAuth1 signature calculation)

This eliminates any potential differences in encoding behavior
that could cause signature mismatch.

Previous custom implementation used manual char-by-char encoding
with toUpper(). Now using Qt's standard implementation ensures
identical encoding across all OAuth1 components.

* Add OAuth2 exchange debugging: log HTTP status and response body

OAuth1 now works perfectly (HTTP 200)! 🎉

Next problem: OAuth2 exchange fails with "Host requires authentication".
Adding debug to see what Garmin actually responds:
- HTTP status code
- Full response body
- Better error messages

This will help understand why OAuth2 exchange is failing.

* Fix OAuth2 signature: include POST body params per OAuth1 RFC 5849

PROBLEM:
OAuth2 token exchange was failing with HTTP 401 "Invalid signature for
signature method HMAC-SHA1". The OAuth1 signature calculation was not
including POST body parameters (mfa_token).

ROOT CAUSE:
Per OAuth1 specification (RFC 5849, Section 3.4.1.3.1), when Content-Type
is "application/x-www-form-urlencoded", POST body parameters MUST be
included in the signature base string along with URL query parameters.

Our implementation was only including URL query parameters and OAuth
protocol parameters, but NOT the POST body parameters.

SOLUTION:
1. Modified generateOAuth1AuthorizationHeader() to accept POST body params
2. Added POST body params to the signature calculation params map
3. Updated exchangeForOAuth2Token() to pass POST body params to signature

CHANGES:
- src/garminconnect.h: Added postBodyParams parameter (optional, default empty)
- src/garminconnect.cpp:
  * Updated function signature
  * Added POST body params to signature calculation (lines 1247-1257)
  * Updated OAuth2 exchange to pass mfa_token to signature (lines 902-920)

TESTING:
This should resolve the OAuth2 401 error and allow successful token exchange.

REFERENCE:
OAuth 1.0 RFC 5849, Section 3.4.1.3.1
"The parameters from the following sources are collected into a single list
of name/value pairs:
- The query component of the HTTP request URI
- The OAuth HTTP "Authorization" header field parameters
- The HTTP request entity-body, but only if the following conditions are met:
  * The entity-body is single-part
  * The entity-body follows the encoding requirements of the
    "application/x-www-form-urlencoded" content-type"

* Add OAuth2 exchange debugging: log HTTP status and response body

This commit adds two critical improvements to Garmin Connect integration:

1. FIX: Multiple MFA dialog issue
   - Problem: When user submitted MFA code, the system called login() again
     which triggered the entire authentication flow from the beginning,
     causing mfaRequired() signal to be emitted multiple times
   - Solution: Only emit mfaRequired() if we don't already have an MFA code
     (src/garminconnect.cpp:66-70)
   - This prevents showing MFA dialog multiple times during retry

2. FEATURE: FIT file upload to Garmin Connect
   - Implemented uploadFitFile() method using multipart/form-data POST
   - Uses OAuth2 Bearer token for authentication
   - Uploads to: https://connectapi.garmin.com/upload-service/upload/.fit
   - Parses response JSON and checks for failures in detailedImportResult
   - Emits uploadSucceeded() or uploadFailed() signals appropriately
   - Added required includes: QHttpMultiPart, QFileInfo

3. UI Integration
   - Updated garmin_upload_file_prepare() in homeform.cpp
   - Replaced non-existent uploadActivity() call with new uploadFitFile()
   - Simplified code by passing file path instead of reading file content
   - Added user feedback with toast messages for upload status

CHANGES:
- src/garminconnect.h:
  * Added uploadFitFile() public method declaration
- src/garminconnect.cpp:
  * Fixed MFA dialog duplication (line 66-70)
  * Implemented uploadFitFile() with multipart upload (lines 40-161)
  * Added QHttpMultiPart and QFileInfo includes
- src/homeform.cpp:
  * Updated garmin_upload_file_prepare() to use new uploadFitFile() method
  * Added upload status toast messages

TESTING:
- MFA dialog should now only appear once per authentication attempt
- FIT files should upload successfully to Garmin Connect after workouts
- Users should see "Uploading to Garmin Connect..." and success/failure messages

REFERENCE:
Based on Python garth library upload implementation
https://github.com/matin/garth

* Fix: Add missing QJson includes for FIT upload compilation

* Fix: Prevent multiple MFA dialogs by suppressing signal on retry

PROBLEM:
MFA dialog was appearing multiple times when user submitted MFA code.
Even though login() was modified to not emit mfaRequired when retrying
with an MFA code, performLogin() was still emitting it unconditionally
when detecting MFA redirect (lines 463 and 492).

ROOT CAUSE:
The performLogin() method always emitted mfaRequired() when it detected
an MFA redirect, regardless of whether we were already retrying with an
MFA code. This caused the dialog to show again even though the user had
already entered the code.

SOLUTION:
1. Added 'suppressMfaSignal' parameter to performLogin()
2. When login() calls performLogin() with an MFA code, it passes
   suppressMfaSignal=true to prevent the signal emission
3. performLogin() checks this flag before emitting mfaRequired()

FLOW NOW:
First attempt (no MFA code):
  - performLogin() detects MFA → emits mfaRequired() → dialog shows
  - User enters code

Retry attempt (with MFA code):
  - performLogin() detects MFA → signal SUPPRESSED (no dialog)
  - Continues to performMfaVerification()
  - Success!

CHANGES:
- src/garminconnect.h:
  * Added suppressMfaSignal parameter to performLogin()
- src/garminconnect.cpp:
  * Updated performLogin() signature
  * Added conditional emit at lines 462-467 and 492-494
  * Updated login() call to pass suppressMfaSignal=!mfaCode.isEmpty()

TESTING:
MFA dialog should now appear only ONCE per authentication flow.
No more infinite MFA dialogs when submitting the code.

* Refactor: Eliminate duplicate MFA emails and unnecessary login calls

PROBLEMS FIXED:
1. MFA flow caused 2 Garmin emails (2 login attempts)
2. FIT upload triggered unnecessary login even when authenticated

ROOT CAUSES:
1. garmin_submit_mfa_code() called login() again, restarting entire flow:
   - Fetch cookies (triggers new MFA email from Garmin)
   - Fetch CSRF
   - Perform login (triggers another MFA request)
   - Then finally submit MFA code

2. garmin_connect_login() ALWAYS called login() even if authenticated
3. garmin_upload_file_prepare() called login when already authenticated

SOLUTIONS:
1. Created submitMfaCode() public method that continues from MFA state:
   - Calls performMfaVerification() directly
   - Skips cookies/CSRF/login steps
   - No duplicate MFA email!

2. Added authentication check in garmin_connect_login():
   - Check isAuthenticated() before calling login()
   - Skip login if already have valid OAuth2 tokens

3. Improved upload authentication check:
   - Only call login if not authenticated
   - Reuse existing valid tokens

CHANGES:
- src/garminconnect.h:
  * Added submitMfaCode() public method
  * Added m_pendingEmail/m_pendingPassword private members (for future use)
  * Added completeOAuthFlow() private method declaration

- src/garminconnect.cpp:
  * Implemented submitMfaCode() - continues MFA flow without restarting
  * Updated login() to store pending credentials

- src/homeform.cpp:
  * garmin_submit_mfa_code() now calls submitMfaCode() instead of login()
  * garmin_connect_login() checks isAuthenticated() before calling login()
  * garmin_upload_file_prepare() improved authentication checks

RESULT:
-  Only 1 Garmin MFA email per authentication
-  Upload uses existing tokens when available
-  No unnecessary login attempts
-  Faster authentication flow

* Security: Remove sensitive data from debug logs for production release

SENSITIVE DATA REMOVED:
1. OAuth consumer_secret (was logging first 200 chars of response)
2. OAuth1 tokens (oauth_token, oauth_token_secret, mfa_token - full response)
3. OAuth2 tokens (access_token, refresh_token - full JSON response)
4. URL query parameter values (tickets, login-urls)
5. POST body parameter values (mfa_token)
6. Complete OAuth signature parameter strings
7. Complete OAuth signature base strings

CHANGES:
- Replaced full response body logs with length-only logs
- Added "NOTE: Contains sensitive data - not logging" comments
- Changed parameter value logs to show only keys and value lengths
- Kept essential debugging info (HTTP status, lengths, counts)

PRODUCTION READY:
All sensitive authentication data is now protected while maintaining
useful debugging information for troubleshooting.

Files modified: src/garminconnect.cpp

* Decrease allSettingsCount from 832 to 831

* removed docs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 10:30:07 +01:00
Roberto Viola
7c865da169 Update project.pbxproj 2025-12-23 09:46:50 +01:00
Roberto Viola
2cbe92e525 2.20.18 2025-12-22 11:59:27 +01:00
Roberto Viola
2deb37ae83 Fix workout editor issues and add average pace tile (#4007)
* Fix workout editor issues and add average pace tile

This commit addresses three user-reported issues and feature requests:

1. **Auto-close workout editor after "Save & Start"**
   - After successfully saving and starting a workout, the workout editor
     now automatically closes and returns to the main screen
   - Added navigation intercept mechanism using custom URL scheme
   - Modified WorkoutEditor.qml to emit closeRequested signal
   - Updated main.qml to handle signal and pop the stack
   - Improved UX flow for starting workouts quickly

2. **Auto-calculate duration/distance/speed in workout editor**
   - Implemented smart field synchronization: "last changed wins"
   - When user modifies duration, distance, or speed/pace, the third field
     is automatically calculated using the relationship: duration = distance / speed
   - Added __lastModified tracking to intervals for intelligent updates
   - Works for treadmill workouts with enabled speed and distance fields
   - Eliminates manual calculation errors and improves workflow

3. **New "Average Pace" tile**
   - Added dedicated tile to display average pace in large font
   - Available for treadmills, rowers, stairclimbers, jumpropes, and ellipticals
   - Shows average pace as primary value with max pace in secondary line
   - Fully integrated with settings UI for enable/disable and ordering
   - Default order: 76, enabled by default
   - Addresses user request for better visibility of average pace during workouts

Files modified:
- src/homeform.h/cpp: Added avg_pace DataObject and update logic
- src/qzsettings.h/cpp: Added tile_avg_pace settings (2 new settings)
- src/settings-tiles.qml: Added UI control for average pace tile
- src/inner_templates/workouteditor/workout-editor-app.js: Auto-calc and auto-close
- src/WorkoutEditor.qml: Navigation intercept and signal emission
- src/main.qml: Close signal handling

* Remove delay from workout editor auto-close and add avg_pace properties to settings.qml

* Fix workout editor issues and add average pace tile

Changes in this commit:

1. Set avg_pace tile to disabled by default
   - Changed default_tile_avg_pace_enabled from true to false
   - Updated default values in qzsettings.h, settings.qml, and settings-tiles.qml

2. Fix onNavigationRequested error with QtWebView
   - QtWebView doesn't support onNavigationRequested
   - Switched to C++ signal-based approach
   - Added workoutStartedFromEditor() signal to homeform
   - Template builder emits signal when workout starts successfully
   - WorkoutEditor.qml connects to signal via Connections block
   - Removed URL navigation attempt from JavaScript
   - Auto-close now works correctly via Qt signal/slot mechanism

Technical details:
- homeform.h: Added workoutStartedFromEditor() signal
- templateinfosenderbuilder.cpp: Emit signal after successful workout start
- WorkoutEditor.qml: Removed onNavigationRequested, added Connections to homeform
- workout-editor-app.js: Removed window.location.href navigation

* Fix workout editor auto-close and distance/duration logic

This commit addresses the remaining issues in the workout editor:

1. Fixed "Save & Start" auto-close mechanism:
   - Removed failed attempt to use onNavigationRequested (not available in QtWebView)
   - Now using existing trainprogram_autostart_requested signal pattern
   - WorkoutEditor.qml listens for trainprogram_autostart_requested via Connections
   - Matches the pattern used by training browser (trainprogram_open_clicked + trainprogram_autostart_requested)

2. Corrected distance/duration relationship:
   - Distance and duration are mutually exclusive fields (user sets one OR the other)
   - Removed all auto-calculate logic that modified fields as user typed
   - Calculation now only happens in buildChartPayload() for chart rendering
   - For treadmill with distance enabled: duration = distance / speed * 3600 (chart display only)
   - User's input fields remain independent and unmodified

3. Cleaned up unused C++ signal code:
   - Removed workoutStartedFromEditor() signal from homeform.h
   - Removed corresponding emit statement from templateinfosenderbuilder.cpp
   - No longer needed since we're using existing trainprogram_autostart_requested pattern

All changes follow existing patterns in the codebase and avoid creating new mechanisms.

* Make duration/distance mutually exclusive and fix Save & Start timing

This commit fixes two issues in the workout editor:

1. Duration and distance are now mutually exclusive for treadmill:
   - Removed 'duration' from the list of non-toggleable fields
   - Added logic to disable duration when distance is enabled and vice versa
   - Set default state: duration enabled, distance disabled for new intervals
   - Only applies to treadmill device type as per requirements

2. Fixed "workout file not ready" error on Save & Start:
   - Changed verification from state.programs to state.programFiles (more reliable)
   - Added 300ms retry mechanism if file not found immediately after save
   - Refreshes program list again before second attempt
   - Provides better error messages with console logging for debugging

The mutual exclusion logic ensures only one of duration/distance can be active
at a time for treadmill workouts, preventing confusion and ensuring correct
chart rendering based on whichever field is enabled.

* Add detailed logging for Save & Start debugging

* Fix WorkoutEditor auto-close by handling signal in main.qml

The issue was that WorkoutEditor.qml cannot access the trainprogram_autostart_requested
signal from stackView because it's inside the stack.

Solution:
- Removed invalid Connections block from WorkoutEditor.qml
- Added connection in main.qml when WorkoutEditor is opened
- When trainprogram_autostart_requested is emitted, trigger closeRequested() on the editor
- This properly closes the editor after "Save & Start"

The flow is now:
1. JS sends trainprogram_autostart_requested message
2. C++ backend emits trainprogram_autostart_requested signal
3. main.qml receives signal and calls editorPage.closeRequested()
4. closeRequested triggers stackView.pop()

* Fix file lookup by checking for .xml/.zwo extensions

The backend saves files with .xml or .zwo extensions, but the payload
only contains the base name without extension. This caused the file
lookup to fail even though the file existed in the list.

Solution:
- Try to find file with exact name first
- If not found, try with .xml extension
- If not found, try with .zwo extension
- Apply same logic in retry attempt

This fixes the "workout file not ready" error when the file was actually
present in the list but with a different extension.

Example:
- User saves: "ffff"
- Backend creates: "ffff.xml"
- Old code looked for: "ffff" (not found)
- New code looks for: "ffff", "ffff.xml", "ffff.zwo" (found!)

* Fix signal routing: emit QML signal instead of calling slot directly

The issue was that templateinfosenderbuilder was calling the homeform slot
directly, bypassing the QML signal. This meant main.qml never received the
signal to close the WorkoutEditor.

Changes:
1. Added getEngine() method to homeform.h to access QML engine
2. Modified templateinfosenderbuilder to emit signal on QML stack instead
3. Now emits trainprogram_autostart_requested on the QML ApplicationWindow
4. This allows main.qml to intercept the signal and close the editor

Flow now works correctly:
- JS sends message -> C++ backend receives it
- C++ emits signal on QML stack (not homeform)
- main.qml receives signal via connection
- main.qml calls editorPage.closeRequested()
- Editor closes with stackView.pop()

* fixing proprieties on settings

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 09:46:13 +01:00
Roberto Viola
a8f4cc899e training program random setting fixed 2025-12-22 09:10:45 +01:00
Roberto Viola
44cab34f38 Update treadmill.h 2025-12-22 08:51:23 +01:00
Roberto Viola
9dab1cb357 Proform treadmill sport 3.0 (#3966) 2025-12-22 08:45:03 +01:00
Roberto Viola
98e58f5f17 Add YS_A6_ FTMS bike support (#4010) 2025-12-21 07:37:09 +01:00
Roberto Viola
5c8835cd38 Add THERUN T15 support to FTMS treadmill (Horizon) detection (#4008) 2025-12-20 14:55:12 +01:00
Roberto Viola
3b3dd9dbe7 Add support for TRUE TREADMILL XXXX devices to FTMS treadmill (#4004) 2025-12-20 08:44:14 +01:00
Roberto Viola
a449fdd09d Update project.pbxproj 2025-12-19 17:04:02 +01:00
Roberto Viola
6d226dd592 Add FTMS treadmill support for TM4800- device (#4002) 2025-12-19 17:01:20 +01:00
Roberto Viola
7f7aac4cd5 Add new service UUID for Nautilus treadmill (#3998) 2025-12-19 09:54:51 +01:00
Roberto Viola
dc3f3f5d21 Update project.pbxproj 2025-12-18 08:25:58 +01:00
Roberto Viola
6ec3c71ac4 Add cadence sensor support to Bkool bike CSC characteristic (#3996)
* Add cadence sensor support to Bkool bike CSC characteristic

Added check for cadence_sensor_name setting in CSC Measurement (0x2A5B)
characteristic handler, matching the behavior of other bike classes like
echelonconnectsport. This allows users to use an external cadence sensor
instead of the bike's built-in cadence data.

The bike will now only use its internal cadence from the CSC characteristic
when no external cadence sensor is configured (cadence_sensor_name is "Disabled").

* Improve cadence sensor handling with early return in CSC characteristic

Changed approach from conditional processing to early return when external
cadence sensor is configured. This is cleaner and more efficient:

- If external cadence sensor configured: return immediately, ignore all CSC data
- If no external sensor: process CSC data normally from bike
- Removed redundant conditional check around cadence calculation
- Prevents unnecessary processing of oldCrankRevs, Speed, Distance, etc.

This matches the pattern used by other bike classes and avoids wasting CPU
cycles processing data that won't be used.

* Fix cadence sensor handling: keep Speed/Distance/KCal calculations

Corrected previous implementation. When external cadence sensor is configured:
- Skip parsing CSC data from internal bike (CrankRevsRead, oldCrankRevs, etc.)
- BUT still calculate Speed, Distance, Resistance, KCal using Cadence.value()
  from the external sensor

The CSC characteristic handler now:
1. If no external sensor: parse CSC data and update Cadence from bike
2. If external sensor: skip CSC parsing, Cadence comes from external sensor
3. Always: calculate Speed/Distance/Resistance/KCal using current Cadence.value()

This matches the pattern used by echelonconnectsport and other bike classes.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 21:40:23 +01:00
Roberto Viola
f9f940b0a5 Update project.pbxproj 2025-12-17 12:10:54 +01:00
Roberto Viola
8cef05fb2d Fix watts reset for ProForm Treadmill Sport 3.0
Updated the condition to exclude proform_treadmill_sport_3_0 from resetting watts when the value exceeds 3000, ensuring correct distance handling for this model.
2025-12-17 11:46:48 +01:00
Roberto Viola
b45ca3e596 Yesoul Walking Pad steps showing as 0 in QZ (Issue #3924) (#3925)
* Yesoul Walking Pad steps showing as 0 in QZ (Issue #3924)

* Update treadmill.h
2025-12-17 08:34:20 +01:00
Andrew Dauncey
8eca1d6fd6 Issue #3902. Added binary installation. (#3993) 2025-12-17 06:22:22 +01:00
Roberto Viola
b1bce39c4a Lifespan Treadmill (#3990) 2025-12-16 18:00:59 +01:00
Roberto Viola
4ea5152a63 Update project.pbxproj 2025-12-16 14:55:36 +01:00
Roberto Viola
4826f75788 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-12-16 14:52:27 +01:00
Roberto Viola
dc3f5baf23 PID Pushing fixed and Add detailed debug logging to PID heart rate control
Introduces extensive qDebug statements throughout the PID heart rate control logic for treadmill, bike, and rowing devices. These logs provide insight into current values, decision branches, and actions taken (or not taken), aiding in troubleshooting and understanding control flow during exercise sessions.
2025-12-16 14:52:21 +01:00
Roberto Viola
9fb19bb5e3 Add negative inclination tile and inclination to FIT files (#3989)
* Add negative inclination tile and inclination to FIT files

This commit implements two improvements:

1. New negative inclination tile:
   - Displays current negative inclination (downhill) percentage
   - Only shows values when inclination is negative (< 0)
   - Otherwise displays "0.0"
   - Configurable via settings (tile_negative_inclination_enabled/order)
   - Updates across all device types (treadmill, bike, elliptical, stairclimber, jumprope)

2. Inclination data in FIT files:
   - Added SetGrade() call to write inclination/grade data to FIT records
   - Uses native FIT field for proper compatibility with fitness apps
   - Includes both positive and negative inclination values

Files modified:
- src/homeform.h: Added negative_inclination DataObject declaration
- src/homeform.cpp: Implemented tile initialization, sorting, and value updates
- src/qzsettings.h/cpp: Added settings for negative inclination tile
- src/settings-tiles.qml: Added QML properties for the new tile
- src/qfit.cpp: Added SetGrade() to write inclination to FIT records

* Add negative elevation gain (descent) tracking and FIT support

This commit implements proper negative elevation gain tracking:

1. Negative Elevation Gain Metric:
   - Added negativeElevationAcc metric to bluetoothdevice
   - Tracks total descent separately from ascent
   - Calculated when inclination < 0 in both bluetoothdevice.cpp and treadmill.cpp
   - Properly reset when clearing stats

2. FIT File Integration:
   - Removed incorrect SetGrade() addition (inclination already handled elsewhere)
   - Added native FIT fields SetTotalAscent() and SetTotalDescent() to session message
   - Uses standard FIT protocol fields instead of custom developer fields
   - Properly tracks both positive elevation gain and negative elevation gain (descents)

3. Session Data Updates:
   - Added negativeElevationGain field to SessionLine class
   - Updated constructor to include negativeElevationGain parameter
   - Updated all SessionLine creation calls in homeform.cpp
   - Ensures negative elevation data flows to FIT file export

Files modified:
- src/devices/bluetoothdevice.h/cpp: Added negativeElevationAcc metric and calculation
- src/devices/treadmill.cpp: Added negative elevation calculation
- src/sessionline.h/cpp: Added negativeElevationGain field and constructor parameter
- src/homeform.cpp: Updated SessionLine constructor calls
- src/qfit.cpp: Added SetTotalAscent/SetTotalDescent to FIT session, removed SetGrade

* Fix negative inclination tile to show descent total and update settings count

Changes:
1. Tile now shows total negative elevation gain (descent in meters/feet)
   instead of current negative inclination percentage
2. Tile renamed from "Neg. Incline (%)" to "Descent (m/ft)" for clarity
3. Consistent with "Elev. Gain" tile behavior
4. Updated allSettingsCount from 824 to 826 (added 2 settings)

This makes the tile much more useful as it shows cumulative descent
rather than instantaneous negative slope.

* Add negative inclination tile properties to settings.qml

* Move negative inclination tile properties to end of settings.qml

Following CLAUDE.md guidelines: 'Always add new properties at the END
of the properties list in settings.qml'

* Move negative_inclination tile to end of device type blocks

Moved all dataList.append(negative_inclination) calls to the end of each
device type block in sortTiles() method, following the guideline that new
tile appends should always be placed at the end of their respective blocks.

Updated device type blocks:
- TREADMILL: Positioned after coreTemperature tile
- STAIRCLIMBER: Positioned after coreTemperature tile
- ELLIPTICAL: Positioned after coreTemperature tile

* Update mainwindow.cpp

* Add UI element for negative inclination tile in settings-tiles.qml

Added AccordionCheckElement for the negative inclination (descent) tile
in the tiles settings UI, allowing users to enable/disable the tile and
configure its display order.

The UI includes:
- AccordionCheckElement with toggle and order configuration
- ComboBox for order index selection
- Descriptive label explaining the tile displays total descent

* Update settings-tiles.qml

* Add second line to negative elevation tile and fix elevation rate bug

1. Added setSecondLine to negative_inclination tile for all device types
   (TREADMILL, STAIRCLIMBER, BIKE, ELLIPTICAL) showing descent rate per minute

2. Fixed bug where elevation gain second line would retain last value when
   speed or inclination returned to zero - now properly clears when conditions
   are not met (speed > 0 and inclination != 0)

3. Added negative_inclination tile support for BIKE device type, which was
   previously missing despite BIKE supporting elevation gain tracking

All second lines now conditionally display:
- For elevation gain: only when speed > 0 AND inclination > 0
- For negative elevation (descent): only when speed > 0 AND inclination < 0

* Unify elevation and negative elevation code for all device types

Removed duplicated code from TREADMILL, STAIRCLIMBER, BIKE, and ELLIPTICAL
sections and created a single common block that handles both elevation gain
and negative elevation gain (descent) for all device types.

Benefits:
- Eliminates 4x code duplication (120+ lines reduced to 40 lines)
- Uses virtual methods from base bluetoothdevice class (no device-specific casts)
- Future modifications only need to be made in one place
- Maintains exact same functionality with cleaner architecture

The unified code block checks device type and applies to TREADMILL,
STAIRCLIMBER, BIKE, and ELLIPTICAL, all of which support elevation tracking.

* Remove device type check and remaining duplicated elevation code

- Removed 2 remaining duplicated negative_inclination->setValue() calls:
  * BIKE section (inside if (!pelotoncadence) block)
  * JUMPROPE section

- Removed device type check from unified elevation block
  * Now applies to ALL device types (TREADMILL, STAIRCLIMBER, BIKE,
    ELLIPTICAL, ROWING, JUMPROPE)
  * Virtual methods in base class return 0 for unsupported devices
  * Cleaner, more maintainable code

The elevation and negative elevation code is now truly unified with no
device-specific checks needed, leveraging polymorphism properly.

* Update qzsettings.cpp

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 14:13:52 +01:00
Roberto Viola
94189368d0 Fix timing update for characteristicChanged event
(Issue #3987)

Moved the update of lastRefreshCharacteristicChanged to occur only when KCal is updated, preventing unnecessary timestamp updates. This ensures more accurate tracking of characteristic change events.
2025-12-16 09:38:07 +01:00
Roberto Viola
3c10869db2 Update project.pbxproj 2025-12-12 12:45:55 +01:00
Roberto Viola
ee46a9b9be Mywhoosh dircon (#3979)
* Mywhoosh Dircon Works!

* works also in this way!

* Update dirconpacket.cpp

* Update settings.qml

* it works, with mywhoosh settings enable works also rouvy and zwift

* no setting required, all the 3 platform works!

* Update settings.qml

* fixing zwift "get gears from zwift setting"
2025-12-12 12:39:39 +01:00
Roberto Viola
2ffa08848e Rouvy Dircon Apple TV works! (#3977)
* Rouvy Dircon Apple TV works!

now i have to refine it

* let's remove constraint

* Apply power_avg_5s setting to virtualbike and virtualrower (#3980)

* setting added
2025-12-12 12:04:07 +01:00
Roberto Viola
4151c500be Add VoiceOver accessibility support for iOS homeform tiles (#3982)
* Add VoiceOver accessibility support for iOS homeform tiles

Implemented comprehensive VoiceOver support for the home screen tiles and main UI controls to improve accessibility for visually impaired users on iOS.

Changes include:
- Added Accessible.* properties to all tile elements in GridView delegate
- Added accessibility support for adjustable tiles (+ and - buttons)
- Added accessibility support for large button tiles
- Added accessibility properties to main control buttons (Start, Stop, Lap)
- Added accessibility properties to Bluetooth connection indicator
- Tiles now properly announce their name, current value, and adjustability status
- All interactive elements are now focusable and have descriptive labels

These changes enable VoiceOver users to:
- Navigate through all tiles using standard VoiceOver gestures
- Understand the purpose and current value of each tile
- Interact with adjustable tiles and action buttons
- Monitor Bluetooth connection status

* Add VoiceOver accessibility support for iOS homeform tiles

Implemented comprehensive VoiceOver support for the home screen tiles and main UI controls to improve accessibility for visually impaired users on iOS.

Changes include:

**Tile Accessibility:**
- Added Accessible.* properties to main tile Item delegate with dynamic roles (Button/Pane/StaticText)
- Tiles announce their name and current value to VoiceOver
- Added Accessible.ignored to decorative elements (Text, Image, Rectangle) to prevent interference
- Only interactive elements (Item container and +/- buttons) are now accessible

**Button Accessibility:**
- Added accessibility support for adjustable tiles (+ and - buttons)
- Added accessibility support for large button tiles
- Added accessibility properties to main control buttons (Start, Stop, Lap)
- Added accessibility properties to Bluetooth connection indicator
- Removed redundant Accessible.onPressAction (already handled by onClicked)

**Grid Accessibility:**
- Added GridView accessibility with role=List
- Added descriptive navigation hints for VoiceOver users

These changes enable VoiceOver users to:
- Navigate through all tiles using standard VoiceOver gestures without confusion
- Understand the purpose and current value of each tile clearly
- Interact with adjustable tiles and action buttons efficiently
- Monitor Bluetooth connection status
- Experience a clean navigation without duplicate/interfering accessibility elements

* Fix VoiceOver navigation: restore onPressAction and configure containers

Fixed the main VoiceOver issues identified during testing:

**Main Issues Fixed:**
1. VoiceOver was selecting the entire ApplicationWindow instead of individual elements
2. Missing Accessible.onPressAction on interactive buttons (incorrectly removed)
3. StackView was blocking navigation to child elements

**Changes:**
- **ApplicationWindow**: Added Accessible.role=Window to allow child navigation
- **StackView**: Added Accessible.ignored=true to prevent it from becoming a focus target
- **Buttons (+/-/large)**: Restored Accessible.onPressAction for proper VoiceOver activation
- All interactive buttons now properly respond to double-tap gestures

**How it works:**
- ApplicationWindow declares itself as Window (allows child navigation)
- StackView is ignored (doesn't interfere with children)
- Individual tiles and buttons are focusable and actionable
- VoiceOver can now navigate through tiles using swipe gestures

This should resolve the "big rectangle on the whole window" issue where VoiceOver
was selecting the root window instead of navigating to individual UI elements.

* Fix VoiceOver: ignore containers, configure Page correctly

Fixed the Qt warning and VoiceOver "big rectangle" issue by properly
configuring accessibility on correct element types.

**Root Cause:**
- ApplicationWindow is NOT an Item, cannot have Accessible properties (was causing Qt warning)
- Background Rectangle was capturing all VoiceOver focus
- Container elements (Item, Row) were interfering with navigation

**Solution:**
- Removed Accessible from ApplicationWindow (not an Item, causes warning)
- Removed Accessible from StackView (let children handle accessibility)
- Added Accessible.ignored=true to background Rectangle in Home.qml
- Configured Page with Accessible.role=Pane to allow child navigation
- Added Accessible.ignored=true to container elements (topBar Item, Row)

**Result:**
VoiceOver should now navigate directly to interactive elements (buttons, tiles)
instead of selecting the entire window as one big rectangle.

**Hierarchy:**
- ApplicationWindow (no Accessible - not an Item)
  └── StackView (no Accessible - transparent)
      └── Page (Pane, navigable)
          ├── Background Rectangle (IGNORED)
          ├── TopBar containers (IGNORED)
          └── Interactive elements (ACCESSIBLE)

* Fix VoiceOver ApplicationWindow capture: add wrapper Item

The issue was that ApplicationWindow itself was becoming the single
VoiceOver-accessible element, reading "qdomyos-zwift" everywhere.

**Root Cause:**
ApplicationWindow in Qt Quick on iOS has default accessibility behavior
that makes the entire window a single accessible element. Since
ApplicationWindow is not an Item, we cannot set Accessible properties
on it directly to prevent this.

**Solution:**
Wrapped the entire content (StackView) in a transparent Item with
Accessible.ignored=true. This prevents the ApplicationWindow from
capturing all accessibility focus while allowing child elements to
remain accessible.

**Additional Changes:**
- Set Page.Accessible.ignored=true (not the source of the problem)
- Added Accessible.ignored=true to all decorative Rectangle containers
- Added Accessible.ignored=true to decorative Image elements
- Removed Accessible from topBar Item and Row (let them be transparent)

**Hierarchy:**
ApplicationWindow (default behavior)
└── Item wrapper (IGNORED - breaks window capture)
    └── StackView (transparent)
        └── Page (IGNORED)
            ├── Decorative elements (IGNORED)
            └── Interactive elements (ACCESSIBLE)

This should finally allow VoiceOver to navigate to individual buttons
and tiles instead of selecting the entire window.

* Update main.qml

* Update Home.qml

* Update main.qml

* it works on ios!

* Revert "Fix VoiceOver ApplicationWindow capture: add wrapper Item"

This reverts commit 079390a1ac.

* Reapply "Fix VoiceOver ApplicationWindow capture: add wrapper Item"

This reverts commit c7e8b84937.

* Update project.pbxproj

* Update project.pbxproj

* Update project.pbxproj

* Fix gear change logic in inclination update

Mail from: Aaron B.
Subject: Something broke w the last beta update
Date: 11/12/2025

Refines the condition for updating inclination to ensure it only triggers when the gear value changes and the requestInclination is not -100. This prevents unnecessary updates and improves the logic for bikes without resistance.

* AsViva S18 bike peloton resistance supported

* Update project.pbxproj

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-12 11:24:22 +01:00
Roberto Viola
326bba8106 Domyos Bike 500 and mywhoosh (Issue #3895) (#3973) 2025-12-11 18:25:14 +01:00
Roberto Viola
bf512f3841 Support WriteNoResponse for FTMS Control Point
Mail: Resistance
From: Laura D.
Date: 11/12/2025

Updated characteristic property check to allow WriteNoResponse in addition to Write for the FTMS Control Point. This improves compatibility with devices that use WriteNoResponse for control point operations.
2025-12-11 15:38:25 +01:00
Roberto Viola
a1dd201bee Update project.pbxproj 2025-12-11 11:40:53 +01:00
Roberto Viola
d522dcb61b Handle invalid instantPace value in speed calculation
Speed is now set to 0 when instantPace is 0 or 65535, addressing cases where the pace value may be invalid or uninitialized.

Mail: Problems Christopeit ET 6
From: Tom
Date: 11/12/2025
2025-12-11 11:40:22 +01:00
Roberto Viola
044a06f3cf 2.20.17 2025-12-11 10:32:51 +01:00
Roberto Viola
c90093046c Domyos T900C not start paused (Issue #3890) (#3896) 2025-12-11 10:02:08 +01:00
Roberto Viola
ddc01d1ae0 Concept2>QZ: Rower Distance Discrepancy #3872
https://github.com/cagnulein/qdomyos-zwift/issues/3872#issuecomment-3640850756
2025-12-11 09:45:55 +01:00
Roberto Viola
6f54194e43 Proform treadmill sport 3.0 (#3966) 2025-12-11 09:33:05 +01:00
Roberto Viola
b4478812dc Proform treadmill sport 3.0 (#3966)
Mail from Christian P.
Subject: Proform sport 3 debug
Date: 9/12/2025
2025-12-11 09:29:40 +01:00
Roberto Viola
3a4d01f886 Proform treadmill sport 3.0 (#3966)
* proform_treadmill_sport_3_0

* Update proformtreadmill.cpp
2025-12-11 09:25:15 +01:00
Roberto Viola
3e50bf1f92 Fix workout editor distance unit conversion (#3975)
* Fix workout editor distance unit conversion

The workout editor was saving speed values in miles when the user had
miles enabled, but XML files should always store speed in km/h.

This commit fixes both saving and loading:
- When saving: convert mph to km/h by multiplying by 1.60934
- When loading: convert km/h to mph by dividing by 1.60934

This ensures XML files always contain km/h values regardless of user
settings, while users see their preferred unit in the editor.

Affects speed, minSpeed, and maxSpeed fields (all fields with unitKey === 'speed').

* Add distance field and fix unit conversion for all distance-based fields

Added support for the distance field in the workout editor and fixed
unit conversion to ensure all distance-based fields are properly
converted between miles and kilometers.

Changes:
- Added 'distance' field to FIELD_DEFS with unitKey: 'distance'
- Added distance to DEFAULT_DISABLED_VALUES (-1)
- Updated resolveFieldLabel() to show correct units (mi/km) for distance
- Updated convertRow() to convert both distance and speed from km to miles
- Updated buildPayload() to convert both distance and speed from miles to km

Now all fields with unitKey === 'distance' or unitKey === 'speed' are:
- Loaded from XML (km) and displayed in user's preferred unit
- Saved to XML in km regardless of user's unit preference

This ensures XML files always store distance in km and speed in km/h,
while users can work with their preferred units (miles/mph or km/km/h).

* Fix unit conversion issues for disabled fields and chart displays

This commit addresses three critical issues with unit conversion:

1. Skip conversion for disabled fields (-1 values):
   - Check if value is disabled BEFORE converting
   - Prevents -1 from being converted to -0.621371
   - Affects distance and speed fields in workout editor

2. Fix duplicate unit labels in workout editor charts:
   - Removed units from series labels (SERIES_DEFS)
   - Let updateLegend() add units automatically
   - Fixed "Speed (mph) (mph)" duplication issue

3. Add miles/km support to chart displays:
   - chartjs/dochart.js: Convert speed values and add unit to label
   - previewchart/dochart.js: Full miles_unit support added
     * Added miles variable and getsettings integration
     * Convert speed, distance, speed_avg, speed_max
     * Update all summary text labels with correct units

Now all charts and summaries show correct units based on user preference.

* Add miles/km support to all remaining chart displays

This commit adds complete miles/km unit support to the remaining chart
files that were still showing speed only in km/h:

1. workoutpreview/preview.html:
   - Added miles variable (default 1 for km)
   - Convert speed values when displaying charts
   - Update speed labels to show correct unit (km/h or mph)
   - Support miles_unit from both rootItem and setWorkoutData()
   - Applies to both primary and fallback speed displays

2. dochartlive.js:
   - Convert speed values by multiplying by miles factor
   - Already had miles_unit setting support, just missing conversion

3. dotreadmillchartlive.js (complete implementation):
   - Added miles variable
   - Added getsettings call to get miles_unit preference
   - Convert speed and target_speed in both initial and live data
   - Update speed labels to show correct unit
   - Update speed_max calculations to account for conversion
   - Applies to both process_arr() and process_workout()

Now ALL chart displays correctly show speed in user's preferred unit.

* Fix workoutpreview not receiving miles_unit setting

The workoutpreview was always showing speed in km/h even when the user
had miles_unit enabled. This was because:

1. TrainingProgramsListJS.qml was not passing miles_unit to setWorkoutData()
2. homeform.h was not exposing miles_unit as a Q_PROPERTY for rootItem

Changes:
- Added miles_unit to the data object passed to setWorkoutData()
  in TrainingProgramsListJS.qml
- Added Q_PROPERTY(bool miles_unit READ miles_unit) to homeform.h
- Implemented miles_unit() getter that reads from QSettings

Now the workoutpreview correctly receives miles_unit setting via both:
- window.rootItem.miles_unit (WebChannel approach)
- setWorkoutData({ miles_unit }) (direct call approach)

This ensures speed is displayed in the correct unit (mph or km/h).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 09:23:21 +01:00
Roberto Viola
b08fb0687c Zwift Ride with ERG mode in Mywhoosh
mail: Cannot connect to MyWhoosh
from Nick W.
date: 9/12/2025
2025-12-10 10:34:08 +01:00
Roberto Viola
477804da82 Workouts not showing up on history and when they do, they are empty g… (#3972)
* Workouts not showing up on history and when they do, they are empty graphs (Issue #3964)

* Update fitdatabaseprocessor.cpp

* Update project.pbxproj
2025-12-10 09:28:33 +01:00
Roberto Viola
e4d536ea2d Fix iOS Live Activities not closing when app is killed (#3959)
* Fix iOS Live Activities not closing when app is killed

Implemented inactivity timer that auto-closes Live Activity after 10 seconds without updates.

How it works:
- Timer starts when Live Activity is created
- Every update resets the timer back to 10 seconds
- If no updates received for 10 seconds, Live Activity auto-closes
- Timer uses RunLoop.common mode to work in background

This handles scenarios:
✓ App crashes or loses connection → auto-closes after 10 seconds
✓ Bluetooth disconnects → auto-closes after 10 seconds
✓ App in background continues workout → keeps updating, stays open
✓ Normal workout stop → closes immediately via explicit endActivity()

LIMITATION - Force-kill from app switcher:
When user force-kills app from app switcher, iOS terminates the process immediately.
No code can execute, including timers. In this case:
- Live Activity will NOT auto-close (iOS limitation)
- Stale date (15 seconds) will mark it visually as outdated
- User must manually dismiss from Lock Screen/Dynamic Island

This is still a major improvement: handles crashes, disconnections, and normal termination.
Force-kill scenario would require push notifications from a backend server to fix completely.

* fixing build

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 11:25:52 +01:00
Roberto Viola
464b126db8 2.20.16 2025-12-09 11:10:13 +01:00
Roberto Viola
c8ca7af1dc Add pace input (min/km or min/mi) to treadmill workout editor (#3956)
- Add pace conversion utility functions (speedToPace, paceToSpeed, formatPaceInput)
- Add pace field to FIELD_DEFS with syncWith relationship to speed
- Display both speed and pace fields for treadmill workouts
- Implement bidirectional sync: editing speed updates pace, editing pace updates speed
- Force mm:ss formatting for pace input
- Add +/- buttons for pace field (increment/decrement by 5 seconds)
- Respect miles setting: show min/mi when miles=true, min/km when miles=false
- Ensure saved workout files only contain speed (pace is not saved)
- Pace field has no checkbox (always synced with speed's enabled state)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 10:39:47 +01:00
Roberto Viola
7919319955 ftmsrower: Call update_hr_from_external only when heart rate belt is disabled
mail from martin b. "debug qz file rower" from 9/12/2025

Moved the call to update_hr_from_external() inside the condition that checks if the heart rate belt name starts with 'Disabled'. This prevents redundant calls and ensures heart rate is updated from external sources only when appropriate.
2025-12-09 08:29:45 +01:00
Roberto Viola
a6fd4cf4cb Add external browser auth support for Intervals.icu (#3960)
Intervals.icu now uses the same strava_auth_external_webbrowser setting
to control authentication method:
- When true (or on Windows/macOS desktop): opens external browser
- When false (default on iOS/Android): uses embedded WebView

This provides users the same flexibility as Strava/Peloton authentication.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-08 20:29:49 +01:00
Roberto Viola
2900c5f4fa fixing ios build 2025-12-06 16:26:59 +01:00
Roberto Viola
fd611c1bea Add heart_ignore_builtin support to Schwinn bikes (#3955) 2025-12-06 16:11:50 +01:00
Roberto Viola
7f1a702021 fixing crash mediabuttonevent (#3927) 2025-12-04 19:48:03 +01:00
Roberto Viola
0cc33e0c2b Add support for NordicTrack Elliptical SE7i (#3900) 2025-12-04 15:54:34 +01:00
Roberto Viola
b8fc355ea7 Concept2>QZ: Rower Distance Discrepancy (Issue #3872) 2025-12-04 09:26:13 +01:00
Roberto Viola
4922019a32 Add Intervals.icu integration with dual-mode authentication (#3884) 2025-12-03 04:42:35 +01:00
Roberto Viola
2eefcab2c8 Add support for NordicTrack Elliptical SE7i (#3900) 2025-12-02 09:26:37 +01:00
Roberto Viola
3da7906a8b Add support for NordicTrack Elliptical SE7i (#3900) 2025-12-02 08:52:32 +01:00
Roberto Viola
83c6b2ceb9 Workout Editor (#3760)
* Workout Editor

* fixing

* fixing

* save and start fixed

* Add auto-start confirmation for training programs

Introduces an auto-start confirmation dialog when opening a workout from the training programs list. Adds a new signal and handler to trigger automatic workout start, including logic to handle device state. Updates QML and C++ to support this workflow.

* Add JS-based training program browser with preview

Introduces a new QML component (TrainingProgramsListJS.qml) and supporting HTML templates for a modern training program browser using Chart.js for workout previews. Updates backend logic to support directory navigation, file filtering, and workout preview data for both XML and ZWO files. Integrates new signals and backend handlers for previewing and opening workouts, and conditionally loads the new browser based on the CHARTJS flag.

* fixing preview?

* Refactor layout and fix contentHeight in training lists

Replaces Row with Column for better vertical alignment in TrainingProgramsList.qml and TrainingProgramsListJS.qml. Sets fixed height for chart/webview container and updates contentHeight calculation to use a constant value, improving scroll behavior and layout consistency.

* Update TrainingProgramsListJS.qml

* Refactor workout preview UI and add treadmill inclination chart

Replaces RowLayout with SplitView for improved layout flexibility in TrainingProgramsList.qml and TrainingProgramsListJS.qml. Refactors chart and preview sections to use ColumnLayout and updates the chart rendering logic. Adds support for displaying inclination as a secondary metric for treadmill workouts in the preview chart (preview.html), including dynamic axis labeling and coloring.

* fixing layout

* Update TrainingProgramsListJS.qml

* fixing split view and loading program in the editor

* new way

* Update TrainingProgramsListJS.qml

* forcespeed

* Fix workout editor default values handling and device type detection

- Add DEFAULT_DISABLED_VALUES map to identify fields that should not be enabled
- Update convertRow to skip enabling fields with default values (speed=-1, cadence=-1, inclination=-200, etc.)
- Improve detectDevice to ignore default values when determining device type
- Fix device type detection in TrainingProgramsListJS with priority-based logic:
  * Priority 1: resistance → bike (regardless of inclination)
  * Priority 2: speed/inclination → treadmill
  * Priority 3: power/cadence → bike
- Ensure programs with only inclination show treadmill chart instead of bike
- Handle forcespeed field correctly when speed is enabled/disabled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Filter workout fields based on device type compatibility

- Add isFieldValidForDevice() helper to check field compatibility with device type
- Modify buildPayload() to only save fields valid for selected device type
- Prevent treadmill workouts from saving cadence/resistance/power
- Prevent bike workouts from saving speed/inclination
- Uses existing FIELD_DEFS devices mapping for validation

This ensures XML files only contain appropriate fields for each device type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* trying to fix other folder issue

* fixing other folders

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 17:01:33 +01:00
Roberto Viola
42c139b881 Lifesmart Treadmill TM6500 2025-12-01 15:47:40 +01:00
Roberto Viola
377c6df085 build fix 2025-12-01 15:09:42 +01:00
Roberto Viola
c462124128 build fix 2025-12-01 14:39:38 +01:00
Roberto Viola
9f85ea84aa Update project.pbxproj 2025-12-01 14:30:34 +01:00
Roberto Viola
b9d65081d5 RUNNA: Add average speed to treadmill BLE notifications
Enhanced treadmill BLE characteristic notifications to include average speed, updating both C++ and Swift implementations. Also fixed distance calculation in virtualtreadmill to use odometerFromStartup for consistency.
2025-12-01 14:28:51 +01:00
Roberto Viola
fdb359a89d Add signal for Zwift auth token result and show toast
Introduces a tokenReceived signal in AuthToken to notify when the Zwift authentication token is received. Updates trainprogram to connect this signal and display a toast message indicating success or failure, improving user feedback on login attempts.
2025-12-01 14:20:11 +01:00
Roberto Viola
6f166d2760 Impossible to connect to treadmill BH S7Ti (Issue #1800) (#1808)
* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* fixing crash on startup

d3c8441717

* Update toorxtreadmill.cpp

* Revert "Update toorxtreadmill.cpp"

This reverts commit 3228633cd8.

* Update toorxtreadmill.cpp

* trying to add the frame missing

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* trying to do the same of #2732

https://github.com/cagnulein/qdomyos-zwift/pull/2732

* Update iconceptbike.cpp

* Revert "Update iconceptbike.cpp"

This reverts commit 37e63737bb.

* fixing doubling devices and init

* Update bluetooth.cpp

* Update toorxtreadmill.h

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

* Update toorxtreadmill.cpp

https://github.com/cagnulein/qdomyos-zwift/issues/2985#issuecomment-3311500996

* ant treadmill speed fix

* miles speed handled

* Add treadmill max speed setting to limit maximum speed

Implemented a new treadmill_speed_max setting (default: 100 km/h) that allows users to cap the maximum speed their treadmill can reach. This is useful for safety and to prevent excessive speeds.

Changes:
- Added treadmill_speed_max setting to qzsettings.h/.cpp
- Updated allSettingsCount from 805 to 806
- Added UI controls in settings.qml for max speed configuration
- Implemented speed limiting in treadmill::changeSpeed() method
- Speed check follows same pattern as treadmill_incline_max

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add unit conversion for treadmill max speed setting

The treadmill max speed field now displays and accepts input in either km/h or mph based on the selected unit. The label and description have been updated to reflect the units, and conversions are handled when saving the value.

* Update qzsettings.cpp

* Pafer treadmill (Issue #2985)

https://github.com/cagnulein/qdomyos-zwift/issues/2985#issuecomment-3457781383

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 09:48:49 +01:00
Roberto Viola
2e077e9268 Allow cadence >256 when using wheel revs fallback
Mail from Gabriel C. 28/11/2025

Modified cadence calculation to permit values above 256 when using wheel revolutions as a fallback (i.e., when _CrankRevs == 0). This ensures accurate cadence reporting in scenarios where crank data is unavailable.
2025-11-28 15:17:10 +01:00
Roberto Viola
28c7de4608 Add FTMS command tracking to kettlerusbbike
Introduces tracking of the last FTMS command received via a new ftmsCharacteristicChanged slot. Slope control is now only enabled if the last FTMS command was FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, improving protocol compliance and device behavior.
2025-11-28 14:49:55 +01:00
Roberto Viola
19b204ff2d Revert "Kettler E7 USB power and slope collision"
This reverts commit 76d6ebceeb.
2025-11-28 14:38:16 +01:00
Roberto Viola
76d6ebceeb Kettler E7 USB power and slope collision 2025-11-28 10:22:21 +01:00
Roberto Viola
a945fa6314 Add support for Sunny Fitness Treadmill devices
Updated device discovery logic to recognize devices with names starting with 'SF-T' as Sunny Fitness Treadmills. Also added a missing check for 'horizonTreadmill' in the elliptical device filter to prevent incorrect device selection.
2025-11-28 09:48:20 +01:00
Roberto Viola
6eed563655 Update project.pbxproj 2025-11-27 14:53:02 +01:00
Roberto Viola
f0ac2da4f9 Add FITSHOW device support to FTMS rower
Email Problems Christopeit ET 6 from Tom 27/11/2025

Introduces detection and handling for FITSHOW devices by adding a FITSHOW flag and updating logic to treat FITSHOW similarly to ICONSOLE_PLUS for distance calculation. This enhances compatibility with additional rowing machine models.
2025-11-27 14:52:02 +01:00
Roberto Viola
6863ebcbfe Exclude SS2K from forcePower resistance logic
Updated the condition in ftmsbike::forcePower to exclude SS2K devices from the resistance level mode logic, ensuring correct handling for SS2K.
2025-11-27 14:38:58 +01:00
Roberto Viola
9a4c368492 Christopeit ET 6 bike
mail from Tom 26/11/2025
2025-11-27 13:33:57 +01:00
Roberto Viola
4af83bd51b Update project.pbxproj 2025-11-27 12:26:13 +01:00
Roberto Viola
a8136f2cbc Christopeit ET 6 bike
mail from Tom 26/11/2025
2025-11-27 11:59:25 +01:00
Roberto Viola
49fbf8acec Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-27 10:28:15 +01:00
Roberto Viola
9fa6eb2a48 Handle NaN values in peloton resistance calculation
Added checks to prevent assigning NaN to m_pelotonResistance. If the calculated resistance is NaN and cadence is zero, set resistance to 0; otherwise, retain the last valid value. This improves stability when sensor data is invalid or missing.
2025-11-27 10:28:04 +01:00
Roberto Viola
be12057c51 Add flights climbed metric to Apple Health for treadmill workouts (#3909)
* Add flights climbed metric to Apple Health for treadmill workouts

Implement calculation and tracking of flights climbed in Apple Health for
treadmill workouts on both iOS and watchOS, using the treadmill's inclination
data.

Changes:
- Add flightsClimbed static variable to WorkoutTracking class to track accumulated flights
- Add inclination parameter to addMetrics() function with default value of 0
- Calculate flights climbed based on inclination and distance delta:
  * Vertical gain = distance * sin(atan(inclination/100))
  * Flights = vertical gain / 3.048 meters (10 feet per flight)
- Add HealthKit authorization for flightsClimbed quantity type
- Write flights climbed data to HealthKit in stopWorkOut() for walking/running workouts
- Reset flights climbed counter at workout start and end
- Pass inclination from lockscreen.mm to WorkoutTracking Swift code
- Convert inclination from centesimal int16 to percentage double

The implementation only tracks flights for treadmill workouts (sport types 0 and 1
for walking and running) when inclination is greater than 0.

* Add flights climbed metric to watchOS for treadmill workouts

Extend Apple Health flights climbed tracking to watchOS companion app
for treadmill workouts using inclination data.

Changes to watchkit Extension/WatchWorkoutTracking.swift:
- Add flightsClimbed, inclination, and previousDistance static variables
- Add HealthKit authorization for flightsClimbed quantity type
- Add updateMetrics() method to calculate flights climbed in real-time:
  * Vertical gain = distance delta * sin(atan(inclination/100))
  * Flights = vertical gain / 3.048 meters (10 feet per flight)
- Write flights climbed data to HealthKit in stopWorkOut() for walking/running
- Reset flights climbed counter at workout start and end
- Combine steps, distance, and flights into single sample array

The implementation tracks flights only for walking/running workouts (sport
types 1 and 2) when inclination is greater than 0, matching iOS behavior.

* Refactor flights climbed to use QZ's existing elevationGain

Replace manual elevation calculation with QZ's built-in elevationGain metric
for improved accuracy and efficiency.

Changes:
- Add elevationGain parameter to virtualtreadmill_updateFTMS() signature
- Pass elevationGain (meters) from virtualtreadmill.cpp to iOS/watchOS
- Remove manual calculation in WorkoutTracking.swift and WatchWorkoutTracking.swift
- Simplify code by using pre-calculated metric from treadmill.cpp
- Remove unnecessary variables (previousDistance, inclination)

Benefits:
- More accurate: uses QZ's time-aware calculation with deltaTime
- More efficient: eliminates duplicate elevation calculations
- Cleaner code: reduces complexity and improves maintainability
- Consistent: aligns with QZ's existing metrics infrastructure

The elevationGain is calculated by treadmill::update_metrics() as:
  elevationAcc += (speed / 3600.0) * 1000.0 * (inclination / 100.0) * deltaTime

Flights climbed = elevationGain (meters) / 3.048 (10 feet per flight)

* Implement WatchConnectivity bridge for flights climbed on watchOS

Complete the watchOS integration by implementing WatchConnectivity bridge
to pass elevationGain from iOS to Apple Watch, enabling flights climbed
tracking on the watch.

iOS changes:
- Add elevationGain static variable to WatchKitConnection.swift
- Include elevationGain in WatchConnectivity replyHandler
- Add @objc setElevationGain() method in AppDelegate.swift
- Declare and implement setElevationGain() in lockscreen.h/mm
- Call setElevationGain() from virtualtreadmill.cpp with elevationGain value

watchOS changes:
- Add elevationGain static variable to WatchKitConnection.swift
- Extract elevationGain from iOS message in replyHandler
- Calculate flights climbed (elevationGain / 3.048) and update WorkoutTracking
- Remove unused updateMetrics() method from WatchWorkoutTracking.swift

Flow:
1. C++ treadmill.cpp calculates elevationGain from speed/inclination/deltaTime
2. virtualtreadmill.cpp passes elevationGain to iOS via lockscreen bridge
3. iOS stores elevationGain in WatchKitConnection static variable
4. When watch requests data, iOS includes elevationGain in reply message
5. Watch receives elevationGain, calculates flights climbed, updates HealthKit

Benefits:
- watchOS now receives accurate elevation data from QZ's calculations
- Flights climbed synced to Apple Health from both iOS and watchOS
- Consistent implementation across platforms
- Removes duplicate/unused code

* Fix compilation error: add previousDistance variable for distance delta calculations

Add missing previousDistance variable to WorkoutTracking class. This variable
is needed to calculate distance deltas for cycling and rowing metrics, which
is separate from the flights climbed calculation.

The previousDistance is used in:
- Cycling distance delta calculation (line 622)
- Rowing distance delta calculation (line 664)

Initialize previousDistance to 0 at workout start and update it at the end
of each addMetrics() call.

* Fix Swift static member access: use WorkoutTracking.previousDistance

Correct the static member access syntax. Since previousDistance is a static
variable, it must be accessed via the class name WorkoutTracking.previousDistance
rather than as a local variable.

Fixed in two locations:
- Line 624: cycling distance delta calculation
- Line 666: rowing distance delta calculation

* Add missing elevationGain parameter to addMetrics calls

Add elevationGain:0 parameter to three addMetrics calls in lockscreen.mm:
- workoutTrackingUpdate(): general workout tracking (line 211)
- virtualbike_updateFTMS(): bike workout tracking (line 269)
- virtualrower_updateFTMS(): rower workout tracking (line 279)

These devices don't have elevation gain relevant for flights climbed
calculation, so passing 0 is appropriate. The treadmill-specific call
in virtualtreadmill_updateFTMS() already correctly passes the actual
elevationGain value.

* Update project.pbxproj

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 10:09:03 +01:00
Roberto Viola
390471abb7 Add timeout and retry logic to rawRead method (#3922) 2025-11-26 20:45:02 +01:00
Roberto Viola
f0188aa9b1 Update project.pbxproj 2025-11-26 16:02:58 +01:00
Roberto Viola
9b784d935b Adding support for - HOP-SPORT HS-090H iConsole+ (Issue #2082) 2025-11-26 15:54:49 +01:00
Roberto Viola
cd07e46ff0 Update peloton.h 2025-11-26 10:51:23 +01:00
Roberto Viola
0393488c69 Walking Tread Bootcamp (Issue #3856) 2025-11-26 10:16:45 +01:00
Roberto Viola
29613b97fa Kettler USB Slope Implementation (#3917) 2025-11-25 20:03:51 +01:00
Roberto Viola
c3ff3c2e06 Update bluetooth.cpp 2025-11-25 09:35:18 +01:00
Roberto Viola
60e23c731b Update settings.qml 2025-11-24 20:22:33 +01:00
Roberto Viola
7d33d87f04 Update project.pbxproj 2025-11-24 13:14:37 +01:00
Roberto Viola
b15055e914 Add THINK_X check to characteristicChanged logic
Updated the conditional in characteristicChanged to include a THINK_X flag, ensuring that crank revolutions are only incremented when THINK_X is false. This accommodates devices where THINK_X sends crank revs in the power characteristic.
2025-11-24 12:43:33 +01:00
Roberto Viola
5ddb5f08cd Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-24 12:34:53 +01:00
Roberto Viola
13161cd894 Fix resistance value formatting in update method
Ensures the resistance value is formatted as a floating-point number with zero decimal places when updating the UI.
2025-11-24 12:34:47 +01:00
Roberto Viola
3089dc8a1c Revert "Format resistance values as strings with no decimals"
This reverts commit fdbc6e94e1.
2025-11-24 12:34:12 +01:00
Roberto Viola
f38652f7b2 Add support for NordicTrack Elliptical SE7i (#3900)
* Add support for NordicTrack Elliptical SE7i

Introduces device-specific initialization, polling, and control logic for the NordicTrack Elliptical SE7i in proformelliptical. Adds a new settings flag and UI switch for enabling SE7i support. Updates QZSettings and settings.qml to include the new option and default value.

* Update proformelliptical.h

* Update proformelliptical.cpp

* moved to right module, and init fixed

* build fix

* Update nordictrackelliptical.cpp

* Update settings.qml

* Update settings.qml
2025-11-24 12:02:53 +01:00
Roberto Viola
fdbc6e94e1 Format resistance values as strings with no decimals
Changed resistance and peloton_resistance assignments to use QString::number with zero decimal places, ensuring consistent string formatting for display.
2025-11-24 12:00:21 +01:00
Roberto Viola
025815fe99 Treadmill NYMAN PLUS min step inclination to 1 2025-11-24 08:40:00 +01:00
Roberto Viola
e2a93cde72 QZ app does not record cadence or resistance after connecting with SmartSpin2k (Issue #3887) 2025-11-21 14:08:14 +01:00
Roberto Viola
a44002c924 Change Kettler USB baudrate from 9600 to 57600 (#3899)
* Change Kettler USB baudrate from 9600 to 57600

Update baudrate configuration for Kettler USB bike support across all platforms:
- Android: Update JNI call parameter to 57600
- Linux/Mac: Change cfsetspeed to B57600
- Windows: Change BaudRate to CBR_57600

* Add baudrate selection setting for Kettler USB

- Add kettler_usb_baud_57600 setting (default: true for 57600 baud)
- Add baudrate parameter to KettlerUSB constructor
- Update openPort() to use configured baudrate for all platforms (Android, Linux/Mac, Windows)
- Add UI ComboBox in Kettler USB Bike Options to choose between 9600 and 57600 baud
- Update settings infrastructure (qzsettings.h/cpp, settings.qml)
- Allow users to switch between 9600 and 57600 baudrate via UI

* Change Kettler USB baudrate setting from bool to int

- Replace kettler_usb_baud_57600 (bool) with kettler_usb_baudrate (int)
- Change default from 57600 to 9600 as requested
- Update KettlerUSB constructor to accept int baudrate parameter
- Add switch statements to convert int to platform-specific constants:
  * Linux/Mac: Convert to speed_t (B9600, B57600)
  * Windows: Convert to CBR constants (CBR_9600, CBR_57600)
  * Android: Use int value directly
- Update settings.qml to use int property and parseInt()
- Future-proof: Can easily add more baudrate options without new settings

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-21 14:00:21 +01:00
Roberto Viola
3bcd4d0ee4 Improve KettlerUSB status parsing and serial buffer handling
Added validation to ignore non-status and malformed responses in parseStatusResponse. Introduced flushSerialBuffer to clear serial buffers after initialization. Reduced polling interval for faster response and improved handling of power command responses.
2025-11-21 11:40:27 +01:00
Roberto Viola
e15e8ebf9e Concept2>QZ: Rower Distance Discrepancy #3872 2025-11-20 15:38:54 +01:00
Roberto Viola
fba48cb7da Update project.pbxproj 2025-11-20 14:39:36 +01:00
Roberto Viola
daacf806bf Track valid cadence from 0x2AD2 characteristic
email "QZ compatibility smart Trainer" from Niklas H. on 20/11/2025

Introduces a static flag to ensure cadence from the 0x2A5B characteristic is only processed until a valid cadence is received from 0x2AD2. This prevents duplicate or conflicting cadence data.
2025-11-20 13:39:50 +01:00
Roberto Viola
4c21b01903 Set ergModeSupported to false for JFBK5.0 devices
mail from Darren K. on 20/11/2025

When discovering devices with names starting with JFBK5.0 or JFBK7.0, explicitly set ergModeSupported to false to reflect their capabilities.
2025-11-20 13:32:15 +01:00
Roberto Viola
59228197ac Update horizontreadmill.cpp (#3842) 2025-11-18 13:17:59 +01:00
Roberto Viola
f7b514c623 Add Kettler USB bike support for Android (#3878) 2025-11-17 20:07:28 +01:00
Roberto Viola
088208ff57 Update project.pbxproj 2025-11-17 15:29:23 +01:00
Roberto Viola
a5a4b93407 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-17 11:59:25 +01:00
Roberto Viola
47696c24ad Add support for NordicTrack Series 7 treadmill
Introduces detection, initialization, and polling logic for the NordicTrack Series 7 treadmill in proformtreadmill. Updates QZSettings and settings UI to include the new model, allowing users to select and configure it.
2025-11-17 11:59:19 +01:00
Roberto Viola
ba9da36087 Update project.pbxproj 2025-11-17 10:24:03 +01:00
Roberto Viola
8fcc9b6725 Update fakerower.cpp 2025-11-17 10:23:16 +01:00
Roberto Viola
d065dd5bd1 2.20.15 2025-11-17 10:13:46 +01:00
Roberto Viola
6f42a0d2cc Fix: Apply volume gear patch only when volume_change_gears is enabled (#3881) 2025-11-17 06:16:17 +01:00
Roberto Viola
14e2e16595 Fix iOS Live Activity updates for ellipticals with builtin heart rate (#3879) 2025-11-16 21:42:14 +01:00
Roberto Viola
025a757c35 2.20.14 2025-11-15 07:52:34 +01:00
Roberto Viola
292a5600c9 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-14 20:08:06 +01:00
Roberto Viola
468bc8f87b Concept2>QZ: Rower Distance Discrepancy (Issue #3872) 2025-11-14 20:07:51 +01:00
Roberto Viola
b0e011fd34 Update project.pbxproj 2025-11-14 12:50:21 +01:00
Roberto Viola
2ef0a3c5a7 kinomap and exr compatibility on android added 2025-11-14 10:45:48 +01:00
Roberto Viola
6d3e1eae7d Merge branch 'master' into Kettler-Racer-S 2025-11-14 09:46:39 +01:00
Roberto Viola
e0dbbb9431 gears in all the places 2025-11-14 08:44:33 +00:00
Roberto Viola
019b3c8abb Schwinn 411/510e cadence issue 2025-11-14 08:55:05 +01:00
Roberto Viola
317116f2d5 Update fakerower.cpp 2025-11-14 08:04:05 +01:00
Roberto Viola
fe005a2f00 SMARTBIKE added 2025-11-13 10:04:59 +01:00
Roberto Viola
08c1e26d3b mqtt settings in command line 2025-11-12 13:53:42 +01:00
Roberto Viola
e98820601a Update project.pbxproj 2025-11-12 11:02:21 +01:00
Roberto Viola
c499092460 Skandika wiry not working correct in qz app (Discussion #3860) 2025-11-12 08:18:17 +01:00
Roberto Viola
e3d50bda7c Add setting for Skandika X-2000 protocol selection
Introduces a new setting to enable or disable the X-2000 protocol for Skandika Wiri bikes. Updates the device discovery logic to respect this setting and adds a corresponding option in the settings UI, allowing users to select the appropriate protocol for their bike model.
2025-11-11 08:36:09 +01:00
Roberto Viola
c060e8b24a height setting fixed for miles units 2025-11-11 08:32:10 +01:00
Roberto Viola
f15f841860 Revert "Remove MLKit integration (#3859)"
This reverts commit 54a8b2619a.
2025-11-10 12:07:48 +01:00
Roberto Viola
15010b27dd Walking Tread Bootcamp (Issue #3856) 2025-11-10 10:03:03 +01:00
Roberto Viola
88c6091e21 Fix division by zero in speed calculation
Added a check to set Speed to 0 when instantPace is zero, preventing a division by zero error during speed calculation.
2025-11-10 08:27:59 +01:00
Roberto Viola
4a6df1c020 handleurl build fix for android 2025-11-09 12:04:34 +01:00
Roberto Viola
3d24e7c1a0 2.20.13 2025-11-09 11:52:18 +01:00
Roberto Viola
54a8b2619a Remove MLKit integration (#3859)
* Remove MLKit integration

- Remove OCR UI elements from settings.qml (Peloton and Zwift auto-sync features)
- Remove MLKit meta-data from AndroidManifest.xml
- Remove MLKit dependencies from build.gradle (both Amazon and Google Play versions)
- Keep underlying properties and qzsettings intact, only UI elements removed

* Restore Zwift OCR settings for Windows only

- Restored zwift_ocr, zwift_ocr_climb_portal, zwift_workout_ocr UI elements
- These settings are visible only on Windows (Qt.platform.os === "windows")
- Windows uses PaddleOCR for screen reading, not MLKit
- Android/iOS OCR features remain removed (they used MLKit)
- Updated labels to clarify these use PaddleOCR, not MLKit

* Remove MLKit OCR code from ScreenCaptureService

- Removed all MLKit imports (InputImage, Text, TextRecognition, etc.)
- Removed TextRecognizer field initialization
- Removed OCR processing code from onImageAvailable method
- Service kept for compatibility but OCR functionality disabled
- Fixes compilation errors after MLKit dependency removal

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-09 11:50:04 +01:00
Roberto Viola
038c4a6165 Update project.pbxproj 2025-11-07 08:14:27 +01:00
Roberto Viola
0831a4ed20 Comment out total distance calculation logic
email "debug qz file rower" from Martin B. on 6/11/2025

The code for calculating total distance from characteristic data has been commented out. Instead, distance is now incremented based on speed and elapsed time, possibly for testing or to address an issue with the original calculation.
2025-11-07 08:04:03 +01:00
Roberto Viola
74fc5f660c problem with Bkool smart bike 1.0 conection #3851 2025-11-07 07:59:22 +01:00
Roberto Viola
ae5dd54738 zwo message commands to TTS (Issue #3823) (#3824) 2025-11-05 14:46:38 +01:00
Roberto Viola
c0299b16ac Support for Proform Trainer 9.0 (PFTL69921-INT.4)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3491211756
2025-11-05 14:41:48 +01:00
Roberto Viola
6401a66f4c Update project.pbxproj 2025-11-05 14:18:05 +01:00
Roberto Viola
ba064c2acd Support for Proform Trainer 9.0 (PFTL69921-INT.4)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3490770427
2025-11-05 13:26:37 +01:00
Roberto Viola
9375f15207 Update AndroidManifest.xml 2025-11-05 10:40:17 +01:00
Roberto Viola
24183a4968 added all the android library aligned to 16k 2025-11-05 10:39:36 +01:00
Roberto Viola
9e1537caad 2.20.12 2025-11-05 10:07:38 +01:00
Roberto Viola
9fe72d13c0 Support for Proform Trainer 9.0 (PFTL69921-INT.4) (Issue #144)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3490007042
2025-11-05 09:58:09 +01:00
Roberto Viola
df5e80a5be Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-05 09:39:30 +01:00
Roberto Viola
3b751d44e6 Treadmill incline multiplied on QZ output [BUG] #2511
BH Vanquish II no regula la velocidad automáticamente #3839
2025-11-05 09:39:24 +01:00
Roberto Viola
3815e45107 PitPat-T01 treadmill #3589 (#3737)
* PitPat-T01 treadmill #3589

* fixing

* wait for a packet and init from 0

* Update deerruntreadmill.cpp

* Update deerruntreadmill.cpp

* new xor

* stop command handled

* minspeedstep handled

* start and stop?

* start and stop

* Update deerruntreadmill.cpp
2025-11-05 08:25:12 +01:00
Roberto Viola
580eb3f092 Create libc++_shared.so 2025-11-04 17:02:41 +01:00
Roberto Viola
aba59cd136 Update peloton.h 2025-11-04 17:02:20 +01:00
Roberto Viola
369fbc4bc0 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-04 17:01:42 +01:00
Roberto Viola
871e704852 resistance for elliptical in the summary must be not the peloton resistance 2025-11-04 14:44:03 +01:00
Roberto Viola
b574e86804 cadence for elliptical in apple health must divide by 2 2025-11-04 14:43:44 +01:00
Roberto Viola
91735a714b Update project.pbxproj 2025-11-04 12:48:51 +01:00
Roberto Viola
da3b5b168e Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 12:47:47 +01:00
Roberto Viola
d339cd461d Cadence and Wattage no responding the right way in Zwift (using a Elite Drivo 2) (Issue #3767) 2025-11-04 12:05:03 +01:00
Roberto Viola
f5ac438905 Update project.pbxproj 2025-11-04 11:42:42 +01:00
Roberto Viola
073b331535 Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 11:40:49 +01:00
Roberto Viola
184c99ff6c Bkool smart bike (Issue #3774) 2025-11-04 11:35:06 +01:00
Roberto Viola
b25f7acf20 Support for Proform Trainer 9.0 (PFTL69921-INT.4)
https://github.com/cagnulein/QZCompanionNordictrackTreadmill/issues/144#issuecomment-3482194626
2025-11-04 11:11:33 +01:00
Roberto Viola
cb0df3ae27 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-04 10:56:20 +01:00
Roberto Viola
7f9ffb7e0e heart rate monitors battery levels (Issue #3833) 2025-11-04 10:55:49 +01:00
Roberto Viola
dd104da06d Update project.pbxproj 2025-11-04 10:37:54 +01:00
Roberto Viola
9440089d05 Trying to figure out how to best use this, monitor vs control (Discussion #3834) 2025-11-04 10:35:51 +01:00
Roberto Viola
39b6ad7463 Peloton and Yesoul resistance numbers the same #3729 2025-11-04 09:46:13 +01:00
Roberto Viola
fdc548fda7 Matrix FTMS Bikes not showing any data (Issue #3837) 2025-11-04 09:40:57 +01:00
Roberto Viola
cc85cbba4f Update project.pbxproj 2025-11-03 16:03:23 +01:00
Roberto Viola
7576a77cd8 Add sanity check for cadence calculation
Cadence is now only updated if the 5-second average is below 200, preventing unrealistic values from being set due to erroneous stride data.
2025-11-03 15:53:48 +01:00
Roberto Viola
61c633474a iOS Apple Health: elliptical as cycling speed and distance 2025-11-03 14:44:29 +01:00
Roberto Viola
69aefc0b30 Domyos 900 + garmin fenix = ANT+ FTMS profile ( smart trainer bike) (Issue #3835) 2025-11-03 14:01:32 +01:00
Roberto Viola
19ca844968 Update qdomyos-zwift.pri 2025-11-03 13:40:40 +01:00
Roberto Viola
5bb3a808a1 ftmselliptical: Refactor cadence calculation logic
Introduces an instantCadence metric to store the immediate cadence value and updates Cadence to use a 5-second average. This improves the accuracy and stability of cadence reporting in ypooelliptical.
2025-11-03 13:15:06 +01:00
Roberto Viola
63cedd457d Update project.pbxproj 2025-11-03 12:11:31 +01:00
Roberto Viola
d9925ac780 SCH_411_510E cadence overflow 2025-11-03 12:11:03 +01:00
Roberto Viola
e4a71e2940 Improve cadence calculation using stride count
Email: Schwinn 411/510e compatibility 03/11/2025

Cadence is now calculated from stride count differences when no external cadence sensor is present, providing more accurate RPM values. Added tracking for last stride count and timestamp to support this calculation.
2025-11-03 12:06:22 +01:00
Roberto Viola
d66d2fd915 Add support for FS-YK Bluetooth bike model
Recognizes devices with names starting with 'FS-YK-' and sets up model-specific flags and behavior. FS-YK bikes are now detected and marked as not supporting ERG mode natively.
2025-11-03 11:37:17 +01:00
Roberto Viola
d20f651672 Add option to force virtual treadmill mode
Introduces a new setting 'virtual_device_force_treadmill' allowing rower devices to be presented as treadmills to client apps. Updates the settings UI, QZSettings class, and device logic to support this feature.
2025-11-03 10:03:18 +01:00
Roberto Viola
19a564d832 Domyos bike 900 disconnecting[BUG] (Issue #3829) 2025-11-03 08:40:15 +01:00
Roberto Viola
5e100f8857 Enable manual workflow dispatch for nordictrack-build job
Modified the nordictrack-build job condition to allow manual triggering
in addition to scheduled runs.

Changes:
- Updated job condition from 'schedule' only to 'schedule || workflow_dispatch'
- This allows users to manually trigger the nordictrack build from GitHub Actions UI

Now the nordictrack-build job can be triggered:
1. Automatically: Every night at midnight (cron schedule)
2. Manually: Via GitHub Actions "Run workflow" button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:16:46 +01:00
Roberto Viola
5491007a2a Add NordicTrack Rower variant to GitHub Actions workflow
Extended the nordictrack-build job to include a third variant for rower
devices, in addition to existing treadmill and bike variants.

Changes:
- Added rower variant to matrix strategy with proform_rower_ip setting
- Updated artifact list to include nordictrack-rower-android-trial APK
- Added rower description in release notes section

The workflow will now build three specialized APKs:
- android-debug-nordictrack-treadmill.apk (nordictrack_2950_ip)
- android-debug-nordictrack-bike.apk (tdf_10_ip)
- android-debug-nordictrack-rower.apk (proform_rower_ip) [NEW]

All three variants use the same nordictrack-build-grpc branch (PR #3478)
and set their respective IP settings to "localhost" for gRPC communication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:12:57 +01:00
Giuseppe Macario
028b5b4c4a typos and grammar (#3768)
* Update HomeForm.ui.qml

grammar

* Update settings.qml

typos
2025-11-02 15:19:33 +01:00
Roberto Viola
8eb0083897 Add SCH_411_510E to power divisor exception list
SCH_411_510E devices now use a divisor of 1.0 for instant power calculation, aligning with other listed models. This change ensures correct power readings for SCH_411_510E ellipticals.
2025-11-02 14:52:52 +01:00
Roberto Viola
89ed87ecb1 Update project.pbxproj 2025-11-02 14:19:15 +01:00
Roberto Viola
eac18d7a51 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-11-02 14:18:15 +01:00
Roberto Viola
d9a50973cd Improve cadence and power calculation for SCH_411_510E
Cadence is now calculated from speed for SCH_411_510E devices when step count is unavailable and the cadence sensor is disabled. Instant power calculation logic is updated to always process when the flag is set, and SCH_411_510E is excluded from the DOMYOS power calculation branch.
2025-11-02 14:18:11 +01:00
Roberto Viola
39c33f3ebc Make SCH_290R behave like hammer_racer_s in FTMS bike
Add SCH_290R device support to FTMS bike with same behavior as hammer_racer_s.
The device will subscribe only to FTMS service and use the same initialization logic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 12:15:28 +01:00
Roberto Viola
2858468a04 Update project.pbxproj 2025-11-01 21:16:52 +01:00
Roberto Viola
1bbbda4efb Technogym ride 2025-11-01 21:16:21 +01:00
Roberto Viola
49fc672538 Technogym ride 2025-11-01 21:08:18 +01:00
Roberto Viola
d93107e286 Update project.pbxproj 2025-11-01 20:00:38 +01:00
Roberto Viola
08af7d61a5 rouvy dircon fixed! 2025-11-01 19:07:54 +01:00
Roberto Viola
e25dfc2354 Add debug logging and update slope change handling
Added debug output for requested slope changes in BLEPeripheralManagerTreadmillZwift and corrected indentation. In virtualtreadmill.cpp, restored lastSlopeChanged assignment after heart rate update to ensure proper slope change timing.
2025-11-01 15:28:07 +01:00
Roberto Viola
a4a4d1b9c5 App closes after Bluetooth connection (Issue #3807) 2025-10-31 20:37:34 +01:00
Roberto Viola
5761865916 Only able to control resistance or incline at once (Issue #3798) (#3813) 2025-10-31 13:22:23 +01:00
Roberto Viola
5be7b8530e Add SCH_290R to recognized Bluetooth devices
Included support for devices with names starting with SCH_290R in the device discovery logic to ensure proper recognition and handling.
2025-10-31 10:40:29 +01:00
Roberto Viola
fe44490ad9 Update project.pbxproj 2025-10-31 08:19:52 +01:00
Roberto Viola
97138d8492 Add FTMS bike filter to device discovery
Device discovery now checks if the FTMS bike list contains the default FTMS bike before proceeding. This adds an additional filter to improve device selection accuracy.
2025-10-31 08:14:12 +01:00
Roberto Viola
76845d5507 Toputure TEB1 (Issue #3814)
Updated forceResistance and deviceDiscovered methods to handle SPORT01 devices. Resistance is now initialized to 1 for SPORT01, ensuring correct behavior on device discovery and resistance setting.
2025-10-30 14:20:23 +01:00
Roberto Viola
28bc5670c4 Update project.pbxproj 2025-10-30 13:24:55 +01:00
Roberto Viola
e0b84cb4a3 Check SCH_411_510E before processing instant power
Added a condition to ensure instant power is only processed when SCH_411_510E is not set. This prevents unintended behavior for devices with SCH_411_510E.
2025-10-30 13:23:27 +01:00
Roberto Viola
4cdf23d544 Toputure TEB1 #3814 2025-10-30 11:22:34 +01:00
Roberto Viola
7beb1aed6f set watchos to 6.0
https://developer.apple.com/forums/thread/805538
2025-10-30 09:00:59 +01:00
Roberto Viola
91841a1ff7 Find minimum start offset in target metrics
mail from Christian P. 29/10/2025 "qdomyos-zwift"

Replaces logic that assumed the first element was chronologically first with a search for the minimum 'start' offset among all target metrics. This ensures the correct offset is used even if the data is unordered.
2025-10-30 08:54:07 +01:00
Roberto Viola
886310497c Update project.pbxproj 2025-10-29 15:36:01 +01:00
Roberto Viola
527a270ade Update kettlerracersbike.cpp 2025-10-29 14:19:42 +01:00
Roberto Viola
5e96d3cff3 Update ypooelliptical.cpp 2025-10-29 13:59:20 +01:00
Roberto Viola
d66af32eed Update project.pbxproj 2025-10-29 13:55:41 +01:00
Roberto Viola
982318326f Elliptical FlowFitness 2Xi (Issue #3811) 2025-10-29 13:54:37 +01:00
Roberto Viola
2ad3640377 Update inclination logic with gear offset
Sets currentSlopePercent to grade in forceInclination and applies gear offset to inclination when specific conditions are met in update(). This improves the handling of gear changes affecting slope simulation.
2025-10-29 10:56:33 +01:00
Roberto Viola
2d680b9c4c Update project.pbxproj 2025-10-29 09:47:19 +01:00
Roberto Viola
17e2d211c1 Update watt calculation for SCH_411_510E device
Modified the power calculation logic to exclude SCH_411_510E from the divisor adjustment and to handle watt calculation for SCH_411_510E similarly to DOMYOS. Also moved the debug emit for current watt outside conditional blocks for consistent logging.
2025-10-29 09:41:14 +01:00
Roberto Viola
e1020be250 Bkool smart bike #3774 2025-10-29 09:17:22 +01:00
Roberto Viola
ce219a790a Sportstech sbike lite autoresistance in Rouvy #3803 2025-10-28 14:51:41 +01:00
Roberto Viola
eaed85ab7d Update kettlerracersbike.cpp 2025-10-28 14:47:51 +01:00
Roberto Viola
7e55ded95d QZ Android Sends Data Even Without Pedaling (Issue #3763) 2025-10-28 12:31:45 +01:00
Roberto Viola
f70c6e8feb Add FIT_BK to 2-byte resistance protocol in ftmsbike
This ensures FIT_BK bikes receive resistance values multiplied by 10 using the 2-byte protocol, consistent with other similar bikes (JFBK5_0, DIRETO_XR, YPBM).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:53:36 +01:00
Roberto Viola
ced1e06a12 Merge branch 'master' into Kettler-Racer-S 2025-10-27 15:47:43 +01:00
Roberto Viola
12ef2bd6ab slope with power 2025-10-27 15:46:57 +01:00
Roberto Viola
bdfaaecc83 Update WorkoutTracking.swift 2025-10-27 15:34:42 +01:00
Roberto Viola
4622fd6df2 Update project.pbxproj 2025-10-27 15:24:43 +01:00
Roberto Viola
fa5691fa2a Fix cadence handling for SCH_411_510E 2025-10-27 15:12:54 +01:00
Roberto Viola
2b995f8396 adding rowing, elliptical and walking on ios apple health 2025-10-27 14:55:34 +01:00
Roberto Viola
e044dc69bc fixing github ios CI 2025-10-27 11:45:25 +01:00
Roberto Viola
26bf095f19 Update project.pbxproj 2025-10-27 10:41:21 +01:00
Roberto Viola
2ba66d9625 writing ios heart rate without using apple watch and air pods 3 2025-10-27 10:40:45 +01:00
Roberto Viola
621440f981 writing ios heart rate without using apple watch and air pods 3 2025-10-27 09:47:56 +01:00
Roberto Viola
a0c1efce9c right device type in apple health from ios directly 2025-10-26 15:53:22 +01:00
Roberto Viola
861f916eb4 Update Windows build job to windows-2022
Changed the GitHub Actions workflow to use 'windows-2022' instead of 'windows-latest' for the window-build job. Also updated the build step to use Windows path separators and removed the explicit msys2 shell specification.
2025-10-26 14:23:28 +01:00
Roberto Viola
8ae1c59b41 Update project.pbxproj 2025-10-26 13:51:13 +01:00
Roberto Viola
d213b5dffe Add support for SCH411/510E elliptical device 2025-10-26 13:49:03 +01:00
Roberto Viola
099531be72 Después de cargar el archivo gpx, no se visualiza el mapa #3795 2025-10-26 13:40:54 +01:00
Roberto Viola
b7e92ab33c Fix Windows MinGW build after GitHub Actions runner update
GitHub migrated windows-latest from Windows Server 2022 to 2025, which
changed how PowerShell inherits PATH from MSYS2. The qthttpserver build
step now explicitly uses the msys2 shell to access MinGW tools (g++, make).

Changes:
- Add shell: msys2 {0} to Build qthttpserver step
- Fix path separator from backslash to forward slash for MSYS2 compatibility

Fixes: "Project ERROR: Cannot run compiler 'g++'"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 13:30:28 +01:00
Roberto Viola
0720d431db ios github build fixed 2025-10-26 13:16:39 +01:00
Roberto Viola
62c7b7b9df Add FTMS auto-detection for Domyos-Bike
- Check ftms_bike is disabled before connecting to domyosbike in bluetooth.cpp
- Auto-switch to FTMS bike when main service not found but FTMS service available
- Show toast notification to restart app when FTMS service detected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 08:38:43 +02:00
Roberto Viola
4eed4be958 Update project.pbxproj 2025-10-24 17:55:13 +02:00
Roberto Viola
eb0dc48d24 Add support for SCH411/510E elliptical device
Added SCH411/510E device support to ypooelliptical class following the same FTMS pattern used for other devices like SCH_590E, E35, and KETTLER.

Changes:
- Added SCH_411_510E boolean flag to ypooelliptical.h
- Added device detection in bluetooth.cpp
- Updated all FTMS conditional checks to include SCH_411_510E

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 17:27:16 +02:00
Roberto Viola
4ff8e8335a GTABikeV ios compatibility 2025-10-24 08:42:05 +02:00
Roberto Viola
ec263a402d Update project.pbxproj 2025-10-23 21:14:47 +02:00
Roberto Viola
0ffb06cc79 Bkool smart bike #3774 (#3792) 2025-10-23 21:08:53 +02:00
Roberto Viola
302526000f Enable continuous gear shifting by decoupling from system volume limits on Android (Issue #3775) (#3779) 2025-10-23 13:34:33 +02:00
Roberto Viola
e62c0dfa93 SCommand added 2025-10-22 16:37:24 +02:00
Roberto Viola
8e47c7d07d Revert "Parse and handle Kettler power packets"
This reverts commit 4b2cb6b148.
2025-10-22 13:32:32 +02:00
Roberto Viola
ff630c2491 Revert "Update kettlerracersbike.cpp"
This reverts commit 47c1b19a7a.
2025-10-22 13:32:28 +02:00
Roberto Viola
47c1b19a7a Update kettlerracersbike.cpp 2025-10-22 10:21:46 +02:00
Roberto Viola
4b2cb6b148 Parse and handle Kettler power packets
Implemented parsing of power value from Kettler packets, converting deciWatts to Watts and updating the power sensor. Ignored power packets from CyclingPowerMeasurement, logging them for debugging instead.
2025-10-22 09:34:31 +02:00
Roberto Viola
76891d41e2 Request : add support for Proform Sport 7.0 treadmill #2635
mail Re: I'm having problems with a Proform 7.0 Sport treadmill. The data is refreshed every 5 seconds, so it becomes impossible to uy application... in fact, with Kinomaps it ends up taking me out of the training due to many disconnections... for Kinomaps, when it stays at 0 km/h it pauses... thank you very much in advance and congratulations for this magnificent application 21/10/2025
2025-10-22 09:03:09 +02:00
Roberto Viola
bb3f9fe216 GTABikeV ios compatibility 2025-10-22 08:17:47 +02:00
Roberto Viola
dd0ce73260 Update trxappgateusbbike.cpp 2025-10-21 17:06:57 +02:00
Roberto Viola
ea98da894f Revert "fixing wattage?"
This reverts commit 407062ba12.
2025-10-21 13:38:41 +02:00
Roberto Viola
206fa06049 Adjust packet length check for TOORX_SRX_500
Changed the minimum packet length for TOORX_SRX_500 from 21 to 19 in characteristicChanged. This ensures short packets are correctly ignored for this bike type.
2025-10-21 08:51:44 +02:00
Roberto Viola
3985eecfe6 Update trxappgateusbbike.cpp 2025-10-21 08:50:40 +02:00
Roberto Viola
407062ba12 fixing wattage? 2025-10-20 15:15:06 +02:00
Roberto Viola
97a7b5c27c Think A102-0063521 2025-10-20 15:05:24 +02:00
Roberto Viola
9804e79144 Increase allSettingsCount to 811
Updated the allSettingsCount from 809 to 811 to reflect new settings.
2025-10-20 11:26:35 +02:00
Roberto Viola
7ec9568218 Merge branch 'master' into Kettler-Racer-S 2025-10-20 11:26:06 +02:00
Roberto Viola
29ec816ab9 max and min gears and inclination from the very beggining 2025-10-20 11:25:03 +02:00
Roberto Viola
02c7063655 Update project.pbxproj 2025-10-20 10:59:18 +02:00
Roberto Viola
0067a728a4 iOS live activity continues after app closes (Issue #3783) 2025-10-20 09:34:23 +02:00
Roberto Viola
2204801f6a gear handlind 2025-10-17 12:43:34 +02:00
Roberto Viola
3ec15253d0 issues connecting zwift play with thinkrider max 2 (Issue #3758) (#3759)
* issues connecting zwift play with thinkrider max 2 (Issue #3758)

* Revert "issues connecting zwift play with thinkrider max 2 (Issue #3758)"

This reverts commit c657127675.

* avoiding char 0xFFF4 for cadence increment

* Update tacxneo2.cpp

* Update tacxneo2.cpp
2025-10-16 15:53:07 +02:00
Roberto Viola
09772d0968 ICSE patching
mail from Jean - Connectivity from 16/10/2025
2025-10-16 15:37:57 +02:00
Roberto Viola
6496e0cf7c Update project.pbxproj 2025-10-16 13:43:30 +02:00
Roberto Viola
b6ef01c59a ICSE patching
mail from Jean - Connectivity from 16/10/2025
2025-10-16 13:41:20 +02:00
Roberto Viola
522ea54ff2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-15 10:24:55 +02:00
Roberto Viola
ba4cc11196 Walking speed surges/spikes #3757 2025-10-15 10:24:49 +02:00
Roberto Viola
95622182c9 Update project.pbxproj 2025-10-13 17:08:55 +02:00
Roberto Viola
0349dfcbaf Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-13 17:08:13 +02:00
Roberto Viola
9fc9d9cfe9 patching ftmsbike for multiple connectedAndDiscovered.
email from Jean-Noel - Connectivity from 13/10/2025
2025-10-13 16:12:53 +02:00
Roberto Viola
5ea16d0869 fixing git for live activities 2025-10-13 09:15:25 +02:00
Roberto Viola
57b259fcba ICSE reset to 15 seconds
mail "Connectivity" from Jean 13/10/2025
2025-10-13 08:05:34 +02:00
Roberto Viola
b78cf1fca5 fixing crash on ios 2025-10-11 07:39:04 +02:00
Roberto Viola
acbdbdd3f2 handling gears 2025-10-10 15:08:12 +02:00
Roberto Viola
77b71e56de Update project.pbxproj 2025-10-10 14:00:25 +02:00
Roberto Viola
c8e3d370a1 fixing build on ios 2025-10-10 13:54:27 +02:00
Roberto Viola
ab8a325da2 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-10 13:43:24 +02:00
Roberto Viola
a3158514af Update project.pbxproj 2025-10-10 13:42:54 +02:00
Roberto Viola
2804d4686a Proform Rower (Proform 750R) cannot change resistance from app (Issue #3746) 2025-10-10 13:39:06 +02:00
Roberto Viola
5833bfb642 fix power notifications 2025-10-10 12:20:34 +02:00
Roberto Viola
51556df3e1 adding power notifications 2025-10-10 10:20:41 +02:00
Roberto Viola
cf0dc2d00d nordictrack integration broken #3731 2025-10-10 10:12:08 +02:00
Roberto Viola
164aa38cb1 adding horizon treadmill XP
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 20:53:30 +02:00
Roberto Viola
34af895940 fixing notifications 2025-10-09 16:12:58 +02:00
Roberto Viola
791cabbc9f fixing handle 2025-10-09 14:13:54 +02:00
Roberto Viola
14998d0f25 TOPUTURE TP1 2025-10-09 13:29:40 +02:00
Roberto Viola
77c66083ab Handle direct read before timeout check
Moved sDirectReadPending check to the beginning of onCharacteristicRead
before any handle lookup or timeout checks. This prevents Qt from
discarding the response as "late" when it finds an existing handle
with expired timeout.

The data now arrives ([109, 50]) and will be forwarded to Qt.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 12:03:37 +02:00
Roberto Viola
ca74fe7ccd fixing wattage reset on the elliptical class 2025-10-09 09:55:38 +02:00
Roberto Viola
facba11bae miles on live activity 2025-10-09 08:00:17 +02:00
Giuseppe Macario
8b8302fb53 typos (#3744) 2025-10-09 04:59:43 +02:00
Roberto Viola
7053050685 Fix JAR with all synthetic access methods
Recompiled from source with all inner classes to generate
proper synthetic access$ methods. Previous compilation was
missing access100 causing NoSuchMethodError.

Verified: all access/bin/bash00 through access800 methods present.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 15:47:57 +02:00
Roberto Viola
22cadd38a1 Make direct read callback generic for any characteristic
Instead of hardcoding Kettler UUID, use sDirectReadPending flag:
- Set flag when readCharacteristicDirectly() is called
- In onCharacteristicRead(), if foundHandle == -1 and flag is set,
  forward data to Qt via leCharacteristicRead()
- Clear flag after handling

This makes the solution reusable for any device needing direct reads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:39:45 +02:00
Roberto Viola
eaea4bf8b8 Recently I managed to decode the Rowing data from Merach NovaRow R50, are any guideline I can follow in order to add it to the app? #3593 2025-10-08 14:01:24 +02:00
Roberto Viola
9d39f3ac5b Fix Kettler characteristic UUID - use READ not WRITE
Corrected characteristic UUID from 638a1105 (WRITE) to 638a1104 (READ).
We need to READ the handshake seed, not write to it.

- 638a1104 = Handshake seed (READ)
- 638a1105 = Handshake key (WRITE)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:29:40 +02:00
Roberto Viola
117257db45 Fix Kettler service UUID typo
Corrected service UUID from 638a1100 to 638af000.
The characteristic UUID 638a1105 belongs to service 638af000,
not 638a1100.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 12:28:44 +02:00
Roberto Viola
3d9c3e4103 Recently I managed to decode the Rowing data from Merach NovaRow R50, are any guideline I can follow in order to add it to the app? (Discussion #3593) 2025-10-08 12:00:02 +02:00
Roberto Viola
b0c3f52945 Add extensive debug logging to readCharacteristicDirectly
Added detailed logging to trace:
- When sConnectedInstance is set/unset
- Static method call parameters
- UUID parsing and service/characteristic lookup
- All available services and characteristics if not found
- JNI exception checking in C++

This will help identify why readCharacteristic fails.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 10:44:24 +02:00
Roberto Viola
cdf026f46d Add custom readCharacteristicDirectly method for Kettler
Instead of fixing Qt's buggy executeReadJob, added a custom method:
- readCharacteristicDirectly() bypasses Qt's job queue
- readCharacteristicDirectlyStatic() static wrapper for JNI calls
- sConnectedInstance tracks active BluetoothGatt connection
- kettlerracersbike.cpp uses JNI on Android to call the new method

This preserves Qt's original logic while fixing Kettler seed reads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 20:27:26 +02:00
Roberto Viola
09f1724417 Delete tst/Makefile.qdomyos-zwift-tests 2025-10-07 15:18:10 +02:00
Roberto Viola
363fae46f4 Delete src/qdomyos-zwift_metatypes.json 2025-10-07 15:17:58 +02:00
Roberto Viola
429fc32551 Delete src/Makefile.qdomyos-zwift-lib 2025-10-07 15:17:41 +02:00
Roberto Viola
393b4534f9 Delete src/Makefile.qdomyos-zwift 2025-10-07 15:17:28 +02:00
Roberto Viola
f8ae14af55 Delete Aaa.log 2025-10-07 15:17:15 +02:00
Roberto Viola
20ea1f6dd6 Delete .qmake.stash 2025-10-07 15:17:03 +02:00
Roberto Viola
e840d7b3e9 Update bkoolbike.cpp 2025-10-07 15:03:27 +02:00
Roberto Viola
3a248ad2c5 Update project.pbxproj 2025-10-07 15:01:35 +02:00
Roberto Viola
5912d7df2d Airpods pro 3 heart rate (#3718)
* Airpods Pro 3 Heart Rate

* Update project.pbxproj
2025-10-07 14:53:48 +02:00
Roberto Viola
94842114e6 BKOOL Bike V 1 2025-10-07 14:47:16 +02:00
Roberto Viola
48dceb4d96 Fix QtBluetoothLE executeReadJob in pre-patched JAR
Applied patch to remove "return true; //rviola" bug and uncomment
the actual GATT read implementation in both:
- QtAndroidBluetooth.jar (compiled .class)
- QtBluetoothLE.java (source file for reference)

The executeReadJob method now properly executes readCharacteristic()
and readDescriptor() calls instead of immediately returning true.

This fixes Kettler Racer S handshake where readCharacteristic() was
always failing with CharacteristicReadError on Android.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 14:16:07 +02:00
Roberto Viola
0e41520326 Remove QtBluetoothLE patch step temporarily
Removed the runtime JAR patching step that was causing
"Unknown module(s) in QT" errors. The patching will be done
offline and the pre-patched JAR committed to qt-patches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 14:11:43 +02:00
Roberto Viola
ff7929f662 Add Android SDK to QtBluetoothLE compilation classpath
Include android.jar via -bootclasspath to resolve android.bluetooth
package imports during compilation of the patched QtBluetoothLE.java.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 13:57:44 +02:00
Roberto Viola
33da32aea7 Fix QtBluetoothLE executeReadJob in GitHub workflow
Patch Qt's executeReadJob method that was returning true immediately
instead of executing GATT read operations. The workflow now:
1. Copies the source from qt-patches
2. Removes the "return true; //rviola" line
3. Uncomments the actual read implementation
4. Recompiles and updates the JAR

This fixes Kettler Racer S handshake failures on Android where
readCharacteristic() was always returning CharacteristicReadError.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 13:45:21 +02:00
Roberto Viola
b98869579e Replace KettlerReadHelper with Qt JAR patch in workflow
Remove experimental reflection-based helper and add automated Qt patching:

Changes:
1. Remove src/android/src/KettlerReadHelper.java (reflection approach didn't work)
2. Remove JNI code from kettlerracersbike.cpp
3. Add workflow step to patch QtAndroidBluetooth.jar:
   - Extract JAR
   - Remove "return true; //rviola" at line 1527
   - Uncomment executeReadJob implementation (lines 1528-1558)
   - Recompile and update JAR

This fixes Qt's characteristic read bug at the source during CI build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 13:20:13 +02:00
Roberto Viola
49698c7102 Add KettlerReadHelper to attempt direct BluetoothGatt read via reflection
Attempt to work around Qt's executeReadJob bug by:
1. Creating Java helper that uses reflection to find active BluetoothGatt
2. Calling it from C++ via JNI before falling back to Qt read
3. Using QLog for proper logging integration

This is experimental and likely won't work without finding the QtBluetoothLE
instance, but will provide diagnostic logs to understand Android internals.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 12:22:17 +02:00
Roberto Viola
84557164b2 csc and virtualbike after the read 2025-10-07 09:20:09 +02:00
Roberto Viola
d83df0ba5a Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-07 08:54:33 +02:00
Roberto Viola
0764fb50b2 Peloton Walking Pace Targets at bottom of range (Issue #3738) 2025-10-07 08:54:26 +02:00
Roberto Viola
d538cbcdde Serialize GATT service discovery to avoid concurrent operations
Discover CSC service details AFTER Kettler service is fully discovered instead
of calling discoverDetails() on both services simultaneously. This prevents
concurrent GATT operations that cause readCharacteristic to fail on Android.

Key changes:
- Remove immediate CSC discoverDetails() call in serviceScanDone()
- Trigger CSC discoverDetails() only after Kettler reaches ServiceDiscovered state
- Ensures GATT operations are serialized (similar to Java queue implementation)

This addresses the CharacteristicReadError when reading handshake seed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 15:25:10 +02:00
Roberto Viola
2ac02cd398 Fix handshake seed read timing and GATT operation conflicts
Changes:
1. Move CSC notification subscription to subscribeKettlerNotifications() to prevent
   premature activation before handshake completion
2. Add 500ms delay before requesting handshake seed to avoid GATT operation conflicts
   with concurrent descriptor writes (similar to Java 1000ms delay)
3. Request handshake directly from stateChanged instead of relying only on update() loop
4. Existing 1-second retry mechanism in update() provides automatic fallback if needed

This addresses CharacteristicReadError issues caused by concurrent GATT operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 13:41:05 +02:00
Roberto Viola
38050084fb Fix Kettler handshake flow to respect proper sequence
Move handshake completion logic to characteristicWritten callback to ensure correct sequence:
1. Read handshake seed
2. Write handshake response
3. Wait for write confirmation
4. Enable notifications
5. Prime notifications with 00 00 write
6. Emit connectedAndDiscovered

This fixes premature notification activation before handshake completion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 12:17:35 +02:00
Roberto Viola
bb5de868ab Live Actions iOS (#3735)
* Add iOS Live Activity support at startup

- Created LiveActivityManager.swift for managing fitness metrics Live Activities
- Added Objective-C++ bridge (ios_liveactivity.h/mm) for Qt integration
- Updated Info.plist with NSSupportsLiveActivities flag
- Initialized LiveActivityManager in AppDelegate on app startup (iOS 16.1+)
- Added new files to qdomyos-zwift.pri build configuration

Live Activities display real-time fitness metrics (speed, cadence, power, heart rate, distance, calories) on Lock Screen and Dynamic Island.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update project.pbxproj

* Update AppDelegate.swift

* fixing build error

* let's see

* qzwidget

* distance fixed in live activities

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-05 14:34:17 +02:00
Roberto Viola
2b8fe6c28d Schwinn CABLE concerns (Issue #3727) 2025-10-04 06:50:45 +02:00
Roberto Viola
0153e09f0d TOPUTURE TP1 treadmill 2025-10-03 15:19:09 +02:00
Roberto Viola
61209307a0 removing android native code 2025-10-03 14:02:22 +02:00
Roberto Viola
dc44433d7c Update project.pbxproj 2025-10-03 12:45:32 +02:00
Roberto Viola
8bdefdb331 Auto inclination not working when using an Android tablet (Issue #3730) 2025-10-03 12:44:01 +02:00
Roberto Viola
c7f5e320fc Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-10-02 15:35:10 +02:00
Roberto Viola
c1582cc763 When using Watt Gain < 1 , unable to fulfil the requested power in Workouts (ERG) (Issue #3728) 2025-10-02 15:34:58 +02:00
Roberto Viola
b2fad2589d virtualbike on android 2025-10-02 11:30:24 +02:00
Roberto Viola
f2f0f7a793 Update project.pbxproj 2025-10-01 17:07:23 +02:00
Roberto Viola
3d665e397e BKOOL Bike V 1 2025-10-01 17:06:13 +02:00
Roberto Viola
194f8686f3 Update project.pbxproj 2025-10-01 15:49:33 +02:00
Roberto Viola
fb79d0ddd6 SportsTech sWalk Walking Pad 2025-10-01 15:39:55 +02:00
Roberto Viola
cd1a108b4c Update KettlerHandshakeReader.java 2025-10-01 10:15:50 +02:00
Roberto Viola
d7e0a4e441 stop workout confirmation 2025-10-01 09:54:44 +02:00
Roberto Viola
465123a156 KUBIsport BC91EK 2025-10-01 09:30:48 +02:00
Roberto Viola
88d01562b1 DMASUN Bikes 2025-09-30 15:34:15 +02:00
Roberto Viola
7cc8e67d19 trying to read power 2025-09-30 15:30:49 +02:00
Roberto Viola
feee99f806 fixing crash 2025-09-30 13:00:46 +02:00
Roberto Viola
1dc3a8fe84 fixing crash 2025-09-30 09:34:31 +02:00
Roberto Viola
85421f41b8 KUBIsport BC91EK 2025-09-30 08:11:11 +02:00
Roberto Viola
0a6a3381c7 invokable fix 2025-09-29 18:19:51 +02:00
Roberto Viola
bf9b0c39ee fixing crash 2025-09-29 15:09:37 +02:00
Roberto Viola
a67cb10633 Update project.pbxproj 2025-09-29 13:35:41 +02:00
Roberto Viola
f00a161fc1 Merge branch 'master' of https://github.com/cagnulein/qdomyos-zwift 2025-09-29 13:33:10 +02:00
Roberto Viola
c071c56eb7 Toorx BRX R100 ergo bicycle 2025-09-29 13:33:03 +02:00
Roberto Viola
78dd681198 trying to fix crash 2025-09-29 13:27:31 +02:00
Roberto Viola
fc39bb0000 Update kettlerracersbike.cpp 2025-09-29 11:01:10 +02:00
Roberto Viola
ea9f69f076 Update KettlerHandshakeReader.java 2025-09-29 09:03:08 +02:00
Roberto Viola
b39f769423 Update project.pbxproj 2025-09-28 08:41:58 +02:00
Roberto Viola
dde526c059 Merach MRK-T25-EF79 (T25) discovered but no tiles report any activity (Issue #3720) 2025-09-28 08:38:39 +02:00
Roberto Viola
666c3d30fc fixing crash 2025-09-27 14:49:13 +02:00
Roberto Viola
c223d6e81d ORLAUF ARES device added 2025-09-27 14:44:58 +02:00
Roberto Viola
83938b54a2 power reading 2025-09-26 15:42:01 +02:00
Roberto Viola
19238be7b5 Update kettlerracersbike.cpp 2025-09-26 15:17:22 +02:00
Roberto Viola
8930795ce7 from apk 2025-09-26 10:44:53 +00:00
Roberto Viola
3e8473261e Update KettlerHandshakeReader.java 2025-09-26 09:07:03 +02:00
Roberto Viola
d531a1d313 ios adb debug 2025-09-26 09:05:07 +02:00
Roberto Viola
b0722cc827 ios adb log 2025-09-26 08:46:06 +02:00
Roberto Viola
863a61c5de Update KettlerHandshakeReader.java 2025-09-25 16:13:10 +02:00
Roberto Viola
5a364bb9e9 Update build.gradle 2025-09-25 14:57:51 +02:00
Roberto Viola
c153a7d9bd legacy 2025-09-25 14:21:14 +02:00
Roberto Viola
990c9b670b Revert "trying sdk33"
This reverts commit 8c19e98511.
2025-09-25 14:17:56 +02:00
Roberto Viola
8c19e98511 trying sdk33 2025-09-25 13:32:08 +02:00
Roberto Viola
13fdf6bc47 Update KettlerHandshakeReader.java 2025-09-25 12:40:00 +02:00
Roberto Viola
eaa5992020 Update KettlerHandshakeReader.java 2025-09-25 11:08:26 +02:00
Roberto Viola
2e534abfbb Update lockscreen.mm 2025-09-24 16:23:48 +02:00
Roberto Viola
6d3ca9877a Update project.pbxproj 2025-09-24 16:08:07 +02:00
Roberto Viola
f477cb32ab Proform CSX210 2025-09-24 12:10:34 +02:00
Roberto Viola
51b79ed413 Wattage gain max value increase #3709 2025-09-24 09:32:56 +02:00
Roberto Viola
7537a0580d native android implementation 2025-09-22 15:19:40 +02:00
Roberto Viola
ad99405a95 Update kettlerracersbike.cpp 2025-09-22 08:57:55 +02:00
Roberto Viola
fa78f03f0a D500 trainer added 2025-09-22 08:16:39 +02:00
Roberto Viola
a40fec4082 adding LSApplicationCategoryType on iOS 2025-09-21 07:38:50 +02:00
Roberto Viola
8ddd75de13 Update kettlerracersbike.cpp 2025-09-20 07:44:19 +02:00
Roberto Viola
5aeba1a605 Update kettlerracersbike.cpp 2025-09-19 15:31:23 +02:00
Roberto Viola
c2279eb886 keep trying 2025-09-19 14:25:04 +02:00
Roberto Viola
a867f17999 testing 2025-09-19 12:44:40 +02:00
Roberto Viola
308e6c6ee0 decrypting 2025-09-18 17:02:47 +02:00
Roberto Viola
f6a9d8ca4e removed gears gain changes from Wizard.qml 2025-09-18 16:30:25 +02:00
Roberto Viola
5378b0c1ab fixing 2025-09-18 15:05:48 +02:00
Roberto Viola
e2d34082cc Kettler Racer S 2025-09-18 12:23:06 +02:00
Roberto Viola
dd2bfc4e1b Update proformbike.cpp 2025-09-18 12:09:24 +02:00
Roberto Viola
06fd78378e Proform CSX210 2025-09-18 11:38:11 +02:00
Roberto Viola
f28574245c Update project.pbxproj 2025-09-18 09:58:38 +02:00
Roberto Viola
b964c523dd How to Make 10s Intervals Work with Virtual Shifting #3603 2025-09-18 09:50:06 +02:00
Roberto Viola
0721bc3ec5 Update project.pbxproj 2025-09-17 17:00:58 +02:00
Roberto Viola
3f783305b2 How to Make 10s Intervals Work with Virtual Shifting #3603 2025-09-17 17:00:09 +02:00
Roberto Viola
be29180e48 Update project.pbxproj 2025-09-17 12:22:46 +02:00
Roberto Viola
19c65d7d90 Taurua IC90 (#3697) 2025-09-17 12:20:21 +02:00
399 changed files with 34758 additions and 2865 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(tshark:*)",
"Bash(find:*)",
"Bash(python3:*)",
"Bash(git log:*)"
]
}
}

View File

@@ -21,7 +21,7 @@ on:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
window-build:
runs-on: windows-latest
runs-on: windows-2022
strategy:
matrix:
config:
@@ -37,14 +37,6 @@ jobs:
path: "src/smtpclient/"
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
with:
repository: cagnulein/qmdnsengine
path: "src/qmdnsengine/"
ref: "zwift"
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
@@ -117,7 +109,7 @@ jobs:
- name: Build qthttpserver
run: |
cd src\qthttpserver
cd src\qthttpserver
qmake
make -j8
make install
@@ -132,6 +124,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
@@ -270,13 +264,6 @@ jobs:
# path: "src/smtpclient/"
# ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
#
# - uses: actions/checkout@v2
# - name: Checkout submodule repo
# uses: actions/checkout@v2
# with:
# repository: cagnulein/qmdnsengine
# path: "src/qmdnsengine/"
# ref: "zwift"
#
# - uses: msys2/setup-msys2@v2
# with:
@@ -385,14 +372,6 @@ jobs:
path: "src/smtpclient/"
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
with:
repository: cagnulein/qmdnsengine
path: "src/qmdnsengine/"
ref: "zwift"
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
@@ -449,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'
@@ -544,12 +532,6 @@ jobs:
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -603,6 +585,11 @@ jobs:
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
- name: Patch QtBluetoothLE executeReadJob
run: |
# Fix Qt's executeReadJob bug - just copy pre-patched JAR
echo "Using pre-patched QtAndroidBluetooth.jar from qt-patches (no compilation needed)"
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
@@ -622,9 +609,11 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
cd ..
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
@@ -852,14 +841,6 @@ jobs:
path: "src/smtpclient/"
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
with:
repository: cagnulein/qmdnsengine
path: "src/qmdnsengine/"
ref: "zwift"
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
@@ -899,6 +880,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
qmake CONFIG+=debug CONFIG+=iphonesimulator && make -j4
@@ -928,14 +911,6 @@ jobs:
path: "src/smtpclient/"
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
with:
repository: cagnulein/qmdnsengine
path: "src/qmdnsengine/"
ref: "zwift"
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
@@ -1010,6 +985,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
@@ -1131,14 +1108,6 @@ jobs:
path: "src/smtpclient/"
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
with:
repository: cagnulein/qmdnsengine
path: "src/qmdnsengine/"
ref: "zwift"
- uses: actions/checkout@v2
- name: Checkout submodule repo
uses: actions/checkout@v2
@@ -1197,9 +1166,11 @@ jobs:
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
cd ..
- name: Clone vcpkg
run: git clone https://github.com/microsoft/vcpkg.git
@@ -1293,6 +1264,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
@@ -1303,8 +1276,8 @@ jobs:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql &&
for i in 1 2 3; do apt-get update && break || sleep 5; done &&
for i in 1 2 3; do apt-get install -y --fix-missing build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql && break || sleep 5; done &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
@@ -1352,6 +1325,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
@@ -1362,8 +1337,8 @@ jobs:
args: >
bash -c "
set -ex &&
apt-get update &&
apt-get install -y build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql &&
for i in 1 2 3; do apt-get update && break || sleep 5; done &&
for i in 1 2 3; do apt-get install -y --fix-missing build-essential git cmake qtbase5-dev qtbase5-private-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools libqt5svg5-dev qtmultimedia5-dev libqt5charts5-dev qtpositioning5-dev qtconnectivity5-dev libqt5websockets5-dev libqt5texttospeech5-dev libqt5bluetooth5 libqt5networkauth5-dev qml-module-qtlocation qml-module-qtpositioning qtlocation5-dev libqt5quickcontrols2-5 qtquickcontrols2-5-dev qml-module-qtquick-controls2 qtbase5-dev libqt5sql5-sqlite libqt5sql5 libqt5sql5-mysql libqt5sql5-psql && break || sleep 5; done &&
export QT_SELECT=qt5 &&
export PATH=/usr/lib/qt5/bin:$PATH &&
cd /github/workspace &&
@@ -1459,6 +1434,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
cd ..
@@ -1573,7 +1550,7 @@ jobs:
nordictrack-build:
# The type of runner that the job will run on
runs-on: ubuntu-22.04
if: github.event_name == 'schedule'
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
strategy:
matrix:
@@ -1584,6 +1561,9 @@ jobs:
- name: bike
setting_key: tdf_10_ip
setting_value: localhost
- name: rower
setting_key: proform_rower_ip
setting_value: localhost
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
@@ -1604,12 +1584,6 @@ jobs:
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -1634,6 +1608,11 @@ jobs:
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
- name: Patch QtBluetoothLE executeReadJob
run: |
# Fix Qt's executeReadJob bug - just copy pre-patched JAR
echo "Using pre-patched QtAndroidBluetooth.jar from qt-patches (no compilation needed)"
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
@@ -1653,6 +1632,8 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
# Set variant-specific IP setting
@@ -1709,12 +1690,6 @@ jobs:
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -1739,6 +1714,11 @@ jobs:
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
- name: Patch QtBluetoothLE executeReadJob
run: |
# Fix Qt's executeReadJob bug - just copy pre-patched JAR
echo "Using pre-patched QtAndroidBluetooth.jar from qt-patches (no compilation needed)"
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
@@ -1758,9 +1738,11 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
cd ..
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
@@ -1812,12 +1794,6 @@ jobs:
git submodule init
git submodule update --init --recursive
- name: Fix qmdnsengine submodule
run: |
cd src/qmdnsengine
git fetch
git checkout 602da51dc43c55bd9aa8a83c47ea3594a9b01b98
- name: Install packages required to run QZ inside workflow
run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev
@@ -1842,6 +1818,11 @@ jobs:
- name: patching qt for bluetooth
run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/
- name: Patch QtBluetoothLE executeReadJob
run: |
# Fix Qt's executeReadJob bug - just copy pre-patched JAR
echo "Using pre-patched QtAndroidBluetooth.jar from qt-patches (no compilation needed)"
- name: download 3rd party files for qthttpserver
run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/
@@ -1861,9 +1842,11 @@ jobs:
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
echo "#define LICENSE" >> secret.h
cd ..
cd ..
ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK
rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393
@@ -1933,6 +1916,7 @@ jobs:
- **fdroid-android-trial**: Android build
- **nordictrack-treadmill-android-trial**: Nordictrack Treadmill build for iFIT2 Tablets
- **nordictrack-bike-android-trial**: Nordictrack Bike build for iFIT2 Tablets
- **nordictrack-rower-android-trial**: Nordictrack Rower build for iFIT2 Tablets
- **peloton-bike-plus-android-trial**: Peloton Bike Plus build with Grupetto backend
- **peloton-bike-android-trial**: Peloton Bike build with Grupetto backend
- **raspberry-pi-binary**: Raspberry Pi build
@@ -1949,6 +1933,7 @@ jobs:
fdroid-android-trial/android-debug.apk
nordictrack-treadmill-android-trial/android-debug-nordictrack-treadmill.apk
nordictrack-bike-android-trial/android-debug-nordictrack-bike.apk
nordictrack-rower-android-trial/android-debug-nordictrack-rower.apk
peloton-bike-plus-android-trial/android-debug-peloton-bike-plus.apk
peloton-bike-android-trial/android-debug-peloton-bike.apk
raspberry-pi-binary/qdomyos-zwift-32bit

4
.gitmodules vendored
View File

@@ -5,10 +5,6 @@
path = src/smtpclient
url = https://github.com/cagnulein/SmtpClient-for-Qt.git
branch = cagnulein-patch-2
[submodule "src/qmdnsengine"]
path = src/qmdnsengine
url = https://github.com/cagnulein/qmdnsengine.git
branch = zwift
[submodule "tst/googletest"]
path = tst/googletest
url = https://github.com/google/googletest.git

View File

@@ -368,7 +368,55 @@ The ProForm 995i implementation serves as the reference example:
- Test device detection thoroughly using the existing test infrastructure
- Consider platform differences when adding new features
## Updating Version Numbers
When releasing a new version of QDomyos-Zwift, you must update the version number in **3 files**:
### 1. Android Manifest
**File**: `src/android/AndroidManifest.xml`
Update both `versionName` and `versionCode`:
```xml
<manifest ... android:versionName="X.XX.XX" android:versionCode="XXXX" ...>
```
- `versionName`: The human-readable version (e.g., "2.20.26")
- `versionCode`: Integer build number that must be incremented (e.g., 1274)
### 2. Main QML File
**File**: `src/main.qml`
Update the version text displayed in the UI (around line 938):
```qml
ItemDelegate {
text: "version X.XX.XX"
width: parent.width
}
```
### 3. Qt Project Include File
**File**: `src/qdomyos-zwift.pri`
Update the VERSION variable (around line 1011):
```pri
VERSION = X.XX.XX
```
### Version Numbering Convention
- **Major.Minor.Patch** format (e.g., 2.20.26)
- **Build number** must always increment, never reuse
- Update all 3 files together to keep versions synchronized
### iOS Version (Optional)
iOS version is managed through Xcode project variables:
- `MARKETING_VERSION` in project.pbxproj (corresponds to versionName)
- `CURRENT_PROJECT_VERSION` in project.pbxproj (corresponds to versionCode)
These are typically updated via Xcode IDE rather than manually editing files.
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list
* Add the property at the END of the properties list

556
Makefile Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,84 @@
//
// QZWidget.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "😀")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "😀")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
// func relevances() async -> WidgetRelevances<Void> {
// // Generate a list containing the contexts this widget is relevant in.
// }
}
struct SimpleEntry: TimelineEntry {
let date: Date
let emoji: String
}
struct QZWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)
Text("Emoji:")
Text(entry.emoji)
}
}
}
struct QZWidget: Widget {
let kind: String = "QZWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
QZWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
QZWidgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
#Preview(as: .systemSmall) {
QZWidget()
} timeline: {
SimpleEntry(date: .now, emoji: "😀")
SimpleEntry(date: .now, emoji: "🤩")
}

View File

@@ -0,0 +1,18 @@
//
// QZWidgetBundle.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import WidgetKit
import SwiftUI
@main
struct QZWidgetBundle: WidgetBundle {
var body: some Widget {
QZWidget()
QZWidgetControl()
QZWidgetLiveActivity()
}
}

View File

@@ -0,0 +1,54 @@
//
// QZWidgetControl.swift
// QZWidget
//
// Created by Roberto Viola on 04/10/25.
//
import AppIntents
import SwiftUI
import WidgetKit
struct QZWidgetControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "org.cagnulein.qdomyoszwift.QZWidget",
provider: Provider()
) { value in
ControlWidgetToggle(
"Start Timer",
isOn: value,
action: StartTimerIntent()
) { isRunning in
Label(isRunning ? "On" : "Off", systemImage: "timer")
}
}
.displayName("Timer")
.description("A an example control that runs a timer.")
}
}
extension QZWidgetControl {
struct Provider: ControlValueProvider {
var previewValue: Bool {
false
}
func currentValue() async throws -> Bool {
let isRunning = true // Check if the timer is running
return isRunning
}
}
}
struct StartTimerIntent: SetValueIntent {
static let title: LocalizedStringResource = "Start a timer"
@Parameter(title: "Timer is running")
var value: Bool
func perform() async throws -> some IntentResult {
// Start / stop the timer based on `value`.
return .result()
}
}

View File

@@ -0,0 +1,174 @@
//
// QZWidgetLiveActivity.swift
// QDomyos-Zwift Live Activity Widget
//
// Displays workout metrics on Dynamic Island and Lock Screen
//
import ActivityKit
import WidgetKit
import SwiftUI
// QZWorkoutAttributes is defined in QZWorkoutAttributes.swift (shared file)
// MARK: - Live Activity Widget
@available(iOS 16.1, *)
struct QZWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: QZWorkoutAttributes.self) { context in
// Lock screen/banner UI
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading, spacing: 4) {
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
Label("\(Int(speed)) \(speedUnit)", systemImage: "speedometer")
.font(.caption)
Label("\(context.state.heartRate) bpm", systemImage: "heart.fill")
.font(.caption)
.foregroundColor(.red)
}
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing, spacing: 4) {
Label("\(Int(context.state.power)) W", systemImage: "bolt.fill")
.font(.caption)
.foregroundColor(.yellow)
Label("\(Int(context.state.cadence)) rpm", systemImage: "arrow.clockwise")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.center) {
// Empty or can add more info
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
let distanceKm = context.state.distance / 1000.0
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
Label(String(format: "%.2f \(distanceUnit)", distance), systemImage: "map")
Spacer()
Label("\(Int(context.state.kcal)) kcal", systemImage: "flame.fill")
.foregroundColor(.orange)
}
.font(.caption)
.padding(.horizontal)
}
} compactLeading: {
// Compact leading (left side of Dynamic Island)
HStack(spacing: 2) {
Image(systemName: "heart.fill")
.foregroundColor(.red)
Text("\(context.state.heartRate)")
.font(.caption2)
}
} compactTrailing: {
// Compact trailing (right side of Dynamic Island)
HStack(spacing: 2) {
Image(systemName: "bolt.fill")
.foregroundColor(.yellow)
Text("\(Int(context.state.power))")
.font(.caption2)
}
} minimal: {
// Minimal view (when multiple activities)
Image(systemName: "figure.run")
}
}
}
}
// MARK: - Lock Screen View
@available(iOS 16.1, *)
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<QZWorkoutAttributes>
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "figure.indoor.cycle")
.foregroundColor(.blue)
Text(context.attributes.deviceName)
.font(.headline)
Spacer()
}
HStack(spacing: 16) {
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
MetricView(icon: "speedometer", value: String(format: "%.1f", speed), unit: speedUnit)
MetricView(icon: "heart.fill", value: "\(context.state.heartRate)", unit: "bpm", color: .red)
MetricView(icon: "bolt.fill", value: "\(Int(context.state.power))", unit: "W", color: .yellow)
}
HStack(spacing: 16) {
let distanceKm = context.state.distance / 1000.0
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
MetricView(icon: "arrow.clockwise", value: "\(Int(context.state.cadence))", unit: "rpm")
MetricView(icon: "map", value: String(format: "%.2f", distance), unit: distanceUnit)
MetricView(icon: "flame.fill", value: "\(Int(context.state.kcal))", unit: "kcal", color: .orange)
}
}
.padding()
}
}
// MARK: - Metric View Component
struct MetricView: View {
let icon: String
let value: String
let unit: String
var color: Color = .primary
var body: some View {
VStack(spacing: 2) {
Image(systemName: icon)
.foregroundColor(color)
.font(.caption)
Text(value)
.font(.system(.body, design: .rounded))
.fontWeight(.semibold)
Text(unit)
.font(.caption2)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Preview
@available(iOS 16.1, *)
struct QZWidgetLiveActivity_Previews: PreviewProvider {
static let attributes = QZWorkoutAttributes(deviceName: "QZ Bike", useMiles: false)
static let contentState = QZWorkoutAttributes.ContentState(
speed: 25.5,
cadence: 85,
power: 200,
heartRate: 145,
distance: 12500, // meters (will be displayed as 12.50 km or 7.77 mi)
kcal: 320,
useMiles: false
)
static var previews: some View {
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Island Compact")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Island Expanded")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
attributes
.previewContext(contentState, viewKind: .content)
.previewDisplayName("Lock Screen")
}
}

View File

@@ -0,0 +1,41 @@
//
// QZWorkoutAttributes.swift
// QDomyos-Zwift
//
// Shared attributes for Live Activities
// MUST be included in both qdomyoszwift and QZWidget targets
//
import Foundation
import ActivityKit
@available(iOS 16.1, *)
public struct QZWorkoutAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
public var speed: Double
public var cadence: Double
public var power: Double
public var heartRate: Int
public var distance: Double
public var kcal: Double
public var useMiles: Bool
public init(speed: Double, cadence: Double, power: Double, heartRate: Int, distance: Double, kcal: Double, useMiles: Bool) {
self.speed = speed
self.cadence = cadence
self.power = power
self.heartRate = heartRate
self.distance = distance
self.kcal = kcal
self.useMiles = useMiles
}
}
public var deviceName: String
public var useMiles: Bool
public init(deviceName: String, useMiles: Bool) {
self.deviceName = deviceName
self.useMiles = useMiles
}
}

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -130,6 +130,7 @@
87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; };
870A5DB32CEF8FB100839641 /* moc_technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */; };
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB42CEF8FD200839641 /* technogymbike.cpp */; };
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870C72632E91565E00DC8A84 /* ios_liveactivity.mm */; };
8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; };
8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; };
8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; };
@@ -303,7 +304,6 @@
87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */; };
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */; };
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */; };
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */; };
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */; };
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */; };
8768C8BA2BBC11C80099DBE1 /* file_sync_client.c in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8768C89C2BBC11C70099DBE1 /* file_sync_client.c */; };
@@ -374,7 +374,6 @@
8772A0E625E43ADB0080718C /* trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */; };
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; };
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */; };
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */; };
877350F72D1C08E60070CBD8 /* SmartControl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877350F62D1C08E50070CBD8 /* SmartControl.cpp */; };
8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */; };
8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */; };
@@ -402,7 +401,6 @@
8785D5432B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */; };
8785D5442B3DD105005A2EB7 /* moc_zwift_client_auth.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */; };
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */; };
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */; };
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */; };
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; };
@@ -461,6 +459,8 @@
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A4B75F25AF27CB0027EF3C /* metric.cpp */; };
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */; };
87A6825D2CE3AB4000586A2A /* sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */; };
87A892562F0C12EB00811D95 /* deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */; };
87A892582F0C173600811D95 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A892572F0C173600811D95 /* sportsplusrower.cpp */; };
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */; };
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */; };
87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; };
@@ -522,6 +522,8 @@
87C5F0D926285E7E0067A1B5 /* moc_mimeattachment.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C5F0CE26285E7E0067A1B5 /* moc_mimeattachment.cpp */; };
87C7074227E4CF5300E79C46 /* moc_keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */; };
87C7074327E4CF5900E79C46 /* keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7073F27E4CF4500E79C46 /* keepbike.cpp */; };
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */; };
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */; };
87CC3B9D25A08812001EC5A8 /* moc_domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */; };
87CC3B9E25A08812001EC5A8 /* moc_elliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */; };
87CC3BA325A0885F001EC5A8 /* domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */; };
@@ -555,6 +557,10 @@
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */; };
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */; };
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */; };
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */; };
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
@@ -597,6 +603,12 @@
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EFC5652E918D35005BB573 /* WidgetKit.framework */; };
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; };
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */; };
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */; };
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
@@ -704,6 +716,13 @@
remoteGlobalIDString = 876E4E102594747F00BD5714;
remoteInfo = watchkit;
};
87EFC5742E918D38005BB573 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 6DB9C3763D02B1415CD9D565 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 87EFC5632E918D35005BB573;
remoteInfo = QZWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -729,6 +748,17 @@
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
99542592E9780B9225F24AA8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -974,6 +1004,8 @@
87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = "<group>"; };
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymbike.cpp; sourceTree = "<group>"; };
870A5DB42CEF8FD200839641 /* technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = technogymbike.cpp; path = ../src/devices/technogymbike/technogymbike.cpp; sourceTree = SOURCE_ROOT; };
870C72622E91565E00DC8A84 /* ios_liveactivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_liveactivity.h; path = ../src/ios/ios_liveactivity.h; sourceTree = SOURCE_ROOT; };
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_liveactivity.mm; path = ../src/ios/ios_liveactivity.mm; sourceTree = SOURCE_ROOT; };
8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = "<group>"; };
8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = "<group>"; };
8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = "<group>"; };
@@ -1244,8 +1276,6 @@
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = elitesquarecontroller.h; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.h; sourceTree = SOURCE_ROOT; };
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = elitesquarecontroller.cpp; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.cpp; sourceTree = SOURCE_ROOT; };
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesquarecontroller.cpp; sourceTree = "<group>"; };
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_wahookickrsnapbike.h; path = ../src/ios/ios_wahookickrsnapbike.h; sourceTree = SOURCE_ROOT; };
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_wahookickrsnapbike.mm; path = ../src/ios/ios_wahookickrsnapbike.mm; sourceTree = SOURCE_ROOT; };
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_zwiftclickremote.h; path = ../src/ios/ios_zwiftclickremote.h; sourceTree = SOURCE_ROOT; };
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_zwiftclickremote.mm; path = ../src/ios/ios_zwiftclickremote.mm; sourceTree = SOURCE_ROOT; };
8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor.cpp; path = ../src/characteristics/characteristicwriteprocessor.cpp; sourceTree = "<group>"; };
@@ -1359,7 +1389,6 @@
8772A0E525E43ADA0080718C /* trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = trxappgateusbbike.cpp; path = ../src/devices/trxappgateusbbike/trxappgateusbbike.cpp; sourceTree = "<group>"; };
8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_trxappgateusbbike.cpp; sourceTree = "<group>"; };
8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_deerruntreadmill.cpp; sourceTree = "<group>"; };
8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = deerruntreadmill.cpp; sourceTree = "<group>"; };
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = deerruntreadmill.h; path = ../src/devices/deeruntreadmill/deerruntreadmill.h; sourceTree = SOURCE_ROOT; };
877350F52D1C08E50070CBD8 /* SmartControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SmartControl.h; path = ../src/devices/kineticinroadbike/SmartControl.h; sourceTree = SOURCE_ROOT; };
877350F62D1C08E50070CBD8 /* SmartControl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = SmartControl.cpp; path = ../src/devices/kineticinroadbike/SmartControl.cpp; sourceTree = SOURCE_ROOT; };
@@ -1401,7 +1430,6 @@
8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_PlayerStateWrapper.cpp; sourceTree = "<group>"; };
8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_zwift_client_auth.cpp; sourceTree = "<group>"; };
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF22D09AADF005F86B4 /* sportsplusrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportsplusrower.h; path = ../src/devices/sportsplusrower/sportsplusrower.h; sourceTree = SOURCE_ROOT; };
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inclinationresistancetable.h; path = ../src/inclinationresistancetable.h; sourceTree = SOURCE_ROOT; };
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inclinationresistancetable.cpp; sourceTree = "<group>"; };
@@ -1496,6 +1524,8 @@
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sramAXSController.cpp; sourceTree = "<group>"; };
87A6825B2CE3AB4000586A2A /* sramAXSController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sramAXSController.h; path = ../src/devices/sramAXSController/sramAXSController.h; sourceTree = SOURCE_ROOT; };
87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sramAXSController.cpp; path = ../src/devices/sramAXSController/sramAXSController.cpp; sourceTree = SOURCE_ROOT; };
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = deerruntreadmill.cpp; path = ../src/devices/deeruntreadmill/deerruntreadmill.cpp; sourceTree = SOURCE_ROOT; };
87A892572F0C173600811D95 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sportsplusrower.cpp; path = ../src/devices/sportsplusrower/sportsplusrower.cpp; sourceTree = SOURCE_ROOT; };
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = androidstatusbar.h; path = ../src/androidstatusbar.h; sourceTree = SOURCE_ROOT; };
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = androidstatusbar.cpp; path = ../src/androidstatusbar.cpp; sourceTree = SOURCE_ROOT; };
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_androidstatusbar.cpp; sourceTree = "<group>"; };
@@ -1586,6 +1616,9 @@
87C7073F27E4CF4500E79C46 /* keepbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = keepbike.cpp; path = ../src/devices/keepbike/keepbike.cpp; sourceTree = "<group>"; };
87C7074027E4CF4500E79C46 /* keepbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = keepbike.h; path = ../src/devices/keepbike/keepbike.h; sourceTree = "<group>"; };
87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_keepbike.cpp; sourceTree = "<group>"; };
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = garminconnect.h; path = ../src/garminconnect.h; sourceTree = SOURCE_ROOT; };
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = garminconnect.cpp; path = ../src/garminconnect.cpp; sourceTree = SOURCE_ROOT; };
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_garminconnect.cpp; sourceTree = "<group>"; };
87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_domyoselliptical.cpp; sourceTree = "<group>"; };
87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elliptical.cpp; sourceTree = "<group>"; };
87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = domyoselliptical.cpp; path = ../src/devices/domyoselliptical/domyoselliptical.cpp; sourceTree = "<group>"; };
@@ -1631,6 +1664,12 @@
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = thinkridercontroller.h; path = ../src/devices/thinkridercontroller/thinkridercontroller.h; sourceTree = SOURCE_ROOT; };
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = thinkridercontroller.cpp; path = ../src/devices/thinkridercontroller/thinkridercontroller.cpp; sourceTree = SOURCE_ROOT; };
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_thinkridercontroller.cpp; sourceTree = "<group>"; };
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sunnyfitstepper.cpp; sourceTree = "<group>"; };
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sunnyfitstepper.h; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.h; sourceTree = SOURCE_ROOT; };
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sunnyfitstepper.cpp; path = ../src/devices/sunnyfitstepper/sunnyfitstepper.cpp; sourceTree = SOURCE_ROOT; };
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
@@ -1693,6 +1732,11 @@
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QZWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
87EFC5652E918D35005BB573 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveActivityBridge.swift; path = ../src/ios/LiveActivityBridge.swift; sourceTree = SOURCE_ROOT; };
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QZWidgetExtension.entitlements; sourceTree = "<group>"; };
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QZWorkoutAttributes.swift; path = QZWidget/QZWorkoutAttributes.swift; sourceTree = "<group>"; };
87EFE45727A518F5006EA1C3 /* nautiluselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautiluselliptical.h; path = ../src/devices/nautiluselliptical/nautiluselliptical.h; sourceTree = "<group>"; };
87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautiluselliptical.cpp; path = ../src/devices/nautiluselliptical/nautiluselliptical.cpp; sourceTree = "<group>"; };
87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nautiluselliptical.cpp; sourceTree = "<group>"; };
@@ -1929,6 +1973,20 @@
FF5BDAB0076F3391B219EA52 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = "<absolute>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
87EFC5682E918D35005BB573 /* QZWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = QZWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
876E4E172594748000BD5714 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -1937,6 +1995,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5612E918D35005BB573 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */,
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D1C883685E82D5676953459A /* Link Binary With Libraries */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -2278,6 +2345,18 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
87DBD5EC2F2CF22100342F2B /* moc_thinkridercontroller.cpp */,
87DBD5D92F2CEE1900342F2B /* thinkridercontroller.h */,
87DBD5DA2F2CEE1900342F2B /* thinkridercontroller.cpp */,
87A892572F0C173600811D95 /* sportsplusrower.cpp */,
87A892552F0C12EB00811D95 /* deerruntreadmill.cpp */,
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */,
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */,
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
@@ -2307,8 +2386,6 @@
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */,
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */,
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */,
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */,
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */,
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */,
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */,
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */,
@@ -2351,7 +2428,6 @@
875CA9482D0C742500667EE6 /* kineticinroadbike.cpp */,
875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */,
87873AF22D09AADF005F86B4 /* sportsplusrower.h */,
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */,
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */,
870A5DB42CEF8FD200839641 /* technogymbike.cpp */,
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */,
@@ -2397,7 +2473,6 @@
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */,
87A083062C73361C00567A4E /* characteristicnotifier2ad9.h */,
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */,
8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */,
8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */,
877758B52C98629B00BB1697 /* sportstechelliptical.cpp */,
877758B42C98629B00BB1697 /* sportstechelliptical.h */,
@@ -2821,6 +2896,9 @@
87F1BD652DBFBCE700416506 /* android_antbike.h */,
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
87DBD6612F333E5700342F2B /* moc_sunnyfitstepper.cpp */,
87DBD6622F333E5700342F2B /* sunnyfitstepper.h */,
87DBD6632F333E5700342F2B /* sunnyfitstepper.cpp */,
);
name = Sources;
sourceTree = "<group>";
@@ -3131,6 +3209,7 @@
4D765E1B1EA6C757220C63E7 /* CoreFoundation.framework */,
FCC237CA5AD60B9BA4447615 /* Foundation.framework */,
344F66310C19536DB4886D8F /* qtpcre2 */,
87EFC5652E918D35005BB573 /* WidgetKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -3393,6 +3472,7 @@
E8C543AB96796ECAA2E65C57 /* qdomyoszwift */ = {
isa = PBXGroup;
children = (
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */,
8768C8CE2BBC12170099DBE1 /* adb */,
87BAC3BE2BA497160003E925 /* PrivacyInfo.xcprivacy */,
8745B2752AFCB4A300991A39 /* android */,
@@ -3403,6 +3483,7 @@
74B182DB50CB5611B5C1C297 /* Supporting Files */,
876E4E122594747F00BD5714 /* watchkit */,
876E4E1E2594748000BD5714 /* watchkit Extension */,
87EFC5682E918D35005BB573 /* QZWidget */,
AF39DD055C3EF8226FBE929D /* Frameworks */,
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
FE0A091FDBFB3E9C31B7A1BD /* Products */,
@@ -3417,6 +3498,7 @@
040B10E2EF2CEF79F2205FE2 /* qdomyoszwift.app */,
876E4E112594747F00BD5714 /* watchkit.app */,
876E4E1A2594748000BD5714 /* watchkit Extension.appex */,
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -3433,11 +3515,13 @@
30414803F31797EB689AE508 /* Copy Bundle Resources */,
99542592E9780B9225F24AA8 /* Embed Frameworks */,
876E4E332594748100BD5714 /* Embed Watch Content */,
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
876E4E312594748100BD5714 /* PBXTargetDependency */,
87EFC5752E918D38005BB573 /* PBXTargetDependency */,
);
name = qdomyoszwift;
packageProductDependencies = (
@@ -3483,6 +3567,28 @@
productReference = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */;
productType = "com.apple.product-type.watchkit2-extension";
};
87EFC5632E918D35005BB573 /* QZWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */;
buildPhases = (
87EFC5602E918D35005BB573 /* Sources */,
87EFC5612E918D35005BB573 /* Frameworks */,
87EFC5622E918D35005BB573 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
87EFC5682E918D35005BB573 /* QZWidget */,
);
name = QZWidgetExtension;
packageProductDependencies = (
);
productName = QZWidgetExtension;
productReference = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -3490,7 +3596,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
LastSwiftUpdateCheck = 1220;
LastSwiftUpdateCheck = 2600;
TargetAttributes = {
799833E5566DEFFC37E4BF1E = {
DevelopmentTeam = 6335M7T29D;
@@ -3506,6 +3612,9 @@
DevelopmentTeam = 6335M7T29D;
ProvisioningStyle = Automatic;
};
87EFC5632E918D35005BB573 = {
CreatedOnToolsVersion = 26.0.1;
};
};
};
buildConfigurationList = DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */;
@@ -3527,6 +3636,7 @@
799833E5566DEFFC37E4BF1E /* qdomyoszwift */,
876E4E102594747F00BD5714 /* watchkit */,
876E4E192594748000BD5714 /* watchkit Extension */,
87EFC5632E918D35005BB573 /* QZWidgetExtension */,
);
};
/* End PBXProject section */
@@ -3573,6 +3683,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5622E918D35005BB573 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -3590,10 +3707,19 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5602E918D35005BB573 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F7E50F631C51CD5B5DC0BC43 /* Compile Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
@@ -3627,7 +3753,6 @@
871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */,
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */,
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */,
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */,
20A50533946A39CBD2C89104 /* bluetoothdevice.cpp in Compile Sources */,
87C5F0D126285E7E0067A1B5 /* moc_stagesbike.cpp in Compile Sources */,
873824E927E647A8004F1B46 /* mdns.cpp in Compile Sources */,
@@ -3737,7 +3862,6 @@
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */,
A4BD6DF51CFFF867B7B5AED4 /* fit_developer_field_definition.cpp in Compile Sources */,
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */,
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */,
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */,
3015F9B9FF4CA6D653D46CCA /* fit_developer_field_description.cpp in Compile Sources */,
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */,
@@ -3753,6 +3877,7 @@
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
87A892582F0C173600811D95 /* sportsplusrower.cpp in Compile Sources */,
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
873CD22327EF8E18000131BC /* inappstoreqmltype.cpp in Compile Sources */,
@@ -3780,10 +3905,10 @@
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */,
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */,
876E50F52B701C050080FAAF /* moc_zwiftclickremote.cpp in Compile Sources */,
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */,
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
87DBD5ED2F2CF22100342F2B /* moc_thinkridercontroller.cpp in Compile Sources */,
87097D2F275EA9A30020EE6F /* sportsplusbike.cpp in Compile Sources */,
333C629F93DB3941862924F7 /* fit_field_base.cpp in Compile Sources */,
87473A9827ECAA0500C203F5 /* moc_proformrower.cpp in Compile Sources */,
@@ -3842,6 +3967,7 @@
87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */,
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */,
ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */,
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */,
6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */,
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */,
87C5F0D026285E7E0067A1B5 /* moc_smtpclient.cpp in Compile Sources */,
@@ -3924,6 +4050,8 @@
872088EB2CE6543C008C2C17 /* moc_mqttpublisher.cpp in Compile Sources */,
872088EC2CE6543C008C2C17 /* moc_qmqttclient.cpp in Compile Sources */,
875CA94C2D130F8100667EE6 /* moc_osc.cpp in Compile Sources */,
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */,
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */,
872088ED2CE6543C008C2C17 /* moc_qmqttmessage.cpp in Compile Sources */,
872088EE2CE6543C008C2C17 /* moc_qmqttsubscription.cpp in Compile Sources */,
872088EF2CE6543C008C2C17 /* moc_qmqttconnection_p.cpp in Compile Sources */,
@@ -3953,6 +4081,7 @@
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */,
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
@@ -4022,6 +4151,8 @@
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
87DBD6642F333E5700342F2B /* sunnyfitstepper.cpp in Compile Sources */,
87DBD6652F333E5700342F2B /* moc_sunnyfitstepper.cpp in Compile Sources */,
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
877758B62C98629B00BB1697 /* sportstechelliptical.cpp in Compile Sources */,
8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */,
@@ -4031,6 +4162,7 @@
E8B499F921FB0AB55C7A8A8B /* moc_gpx.cpp in Compile Sources */,
87E6A85825B5C88E00371D28 /* moc_flywheelbike.cpp in Compile Sources */,
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */,
87A892562F0C12EB00811D95 /* deerruntreadmill.cpp in Compile Sources */,
878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */,
873824B727E64707004F1B46 /* moc_characteristicwriteprocessor.cpp in Compile Sources */,
87310B1F266FBB59008BA0D6 /* homefitnessbuddy.cpp in Compile Sources */,
@@ -4085,6 +4217,7 @@
874D272029AFA11F0007C079 /* apexbike.cpp in Compile Sources */,
8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */,
87C5F0B626285E5F0067A1B5 /* quotedprintable.cpp in Compile Sources */,
87DBD5DB2F2CEE1900342F2B /* thinkridercontroller.cpp in Compile Sources */,
87310B23266FBB78008BA0D6 /* moc_smartrowrower.cpp in Compile Sources */,
EE29228550794460E7654533 /* moc_trxappgateusbtreadmill.cpp in Compile Sources */,
3DB7B5F0CE1E2390CEFFC1E8 /* moc_virtualbike.cpp in Compile Sources */,
@@ -4132,6 +4265,11 @@
target = 876E4E102594747F00BD5714 /* watchkit */;
targetProxy = 876E4E302594748100BD5714 /* PBXContainerItemProxy */;
};
87EFC5752E918D38005BB573 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
targetProxy = 87EFC5742E918D38005BB573 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -4455,7 +4593,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4633,6 +4771,7 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = "";
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
@@ -4655,7 +4794,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -4836,6 +4975,7 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = _debug;
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
@@ -4891,7 +5031,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4987,7 +5127,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5079,7 +5219,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5195,7 +5335,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1161;
CURRENT_PROJECT_VERSION = 1279;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5267,6 +5407,184 @@
};
name = Release;
};
87EFC5792E918D38005BB573 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1279;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
87EFC57A2E918D38005BB573 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1279;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -5297,6 +5615,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
87EFC5792E918D38005BB573 /* Debug */,
87EFC57A2E918D38005BB573 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -29,6 +29,7 @@ class WatchKitConnection: NSObject {
public static var cadence = 0.0
public static var power = 0.0
public static var steps = 0
public static var elevationGain = 0.0
weak var delegate: WatchKitConnectionDelegate?
private override init() {
@@ -85,6 +86,13 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
let iSteps = Int(stepsDouble)
WatchKitConnection.steps = iSteps
}
if let elevationGainDouble = result["elevationGain"] as? Double {
WatchKitConnection.elevationGain = elevationGainDouble
// Calculate flights climbed and update WorkoutTracking
let flightsClimbed = elevationGainDouble / 3.048 // One flight = 10 feet = 3.048 meters
WorkoutTracking.flightsClimbed = flightsClimbed
print("WatchKitConnection: Received elevation gain: \(elevationGainDouble)m, flights: \(flightsClimbed)")
}
}, errorHandler: { (error) in
print(error)
})

View File

@@ -37,17 +37,18 @@ class WorkoutTracking: NSObject {
public static var steps = Int()
public static var cadence = Double()
public static var lastDateMetric = Date()
public static var flightsClimbed = Double()
var sport: Int = 0
let healthStore = HKHealthStore()
let configuration = HKWorkoutConfiguration()
var workoutSession: HKWorkoutSession!
var workoutBuilder: HKLiveWorkoutBuilder!
weak var delegate: WorkoutTrackingDelegate?
override init() {
super.init()
}
}
}
extension WorkoutTracking {
@@ -177,6 +178,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .runningVerticalOscillation)!,
HKSampleType.quantityType(forIdentifier: .walkingSpeed)!,
HKSampleType.quantityType(forIdentifier: .walkingStepLength)!,
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
HKSampleType.workoutType()
])
} else {
@@ -188,6 +190,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
HKSampleType.workoutType()
])
}
@@ -206,6 +209,8 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
func startWorkOut() {
WorkoutTracking.lastDateMetric = Date()
// Reset flights climbed for new workout
WorkoutTracking.flightsClimbed = 0
print("Start workout")
configWorkout()
workoutSession.startActivity(with: Date())
@@ -354,7 +359,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
}
} else {
// Guard to check if steps quantity type is available
guard let quantityTypeSteps = HKQuantityType.quantityType(
forIdentifier: .stepCount) else {
@@ -362,7 +367,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
// Create a sample for total steps
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
@@ -370,55 +375,59 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
start: startDate,
end: Date())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
// Guard to check if distance quantity type is available
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
}
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
end: Date())
// Create flights climbed sample if available
var samplesToAdd: [HKCumulativeQuantitySeriesSample] = [sampleSteps, sampleDistance]
if WorkoutTracking.flightsClimbed > 0 {
if let quantityTypeFlights = HKQuantityType.quantityType(forIdentifier: .flightsClimbed) {
let flightsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: WorkoutTracking.flightsClimbed)
let sampleFlights = HKCumulativeQuantitySeriesSample(
type: quantityTypeFlights,
quantity: flightsQuantity,
start: startDate,
end: Date())
samplesToAdd.append(sampleFlights)
print("WatchWorkoutTracking: Adding flights climbed to workout: \(WorkoutTracking.flightsClimbed)")
}
}
// Add all samples to the workout builder
workoutBuilder.add(samplesToAdd) { (success, error) in
if let error = error {
print(error)
}
// End the data collection
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
// Finish the workout and save total steps
// Finish the workout and save metrics
self.workoutBuilder.finishWorkout { (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(stepsQuantity, forKey: "totalSteps")
}
}
}
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
}
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
if let error = error {
print(error)
}
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
self.workoutBuilder.finishWorkout{ (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(quantityMiles, forKey: "totalDistance")
// Set total energy burned on the workout
let totalEnergy = WorkoutTracking.totalKcal > 0 ? WorkoutTracking.totalKcal : activeEnergyBurned
let totalEnergyQuantity = HKQuantity(unit: unit, doubleValue: totalEnergy)
workout?.setValue(totalEnergyQuantity, forKey: "totalEnergyBurned")
// Reset flights climbed for next workout
WorkoutTracking.flightsClimbed = 0
}
}
}
@@ -433,7 +442,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
let startOfDay = Calendar.current.startOfDay(for: Date())
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
let query = HKStatisticsQuery(quantityType: stepCounts, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] (_, result, error) in
guard let weakSelf = self else {
return
@@ -443,7 +452,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
print("Failed to fetch steps rate")
return
}
if let sum = result.sumQuantity() {
resultCount = sum.doubleValue(for: HKUnit.count())
weakSelf.delegate?.didReceiveHealthKitStepCounts(resultCount)

78
compile-qt-jar.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Script to compile QtBluetoothLE.java and update the JAR
# Usage: ./compile-qt-jar.sh
set -e # Exit on error
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
QT_PATCHES_DIR="$SCRIPT_DIR/qt-patches/android/5.15.0"
JAR_FILE="$QT_PATCHES_DIR/jar/QtAndroidBluetooth.jar"
SOURCE_FILE="$QT_PATCHES_DIR/qtconnectivity/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java"
WORK_DIR="/tmp/qt-jar-compile-$$"
ANDROID_JAR="/tmp/android.jar"
echo "=== QtBluetoothLE JAR Compiler ==="
echo ""
# Check if source file exists
if [ ! -f "$SOURCE_FILE" ]; then
echo "ERROR: Source file not found: $SOURCE_FILE"
exit 1
fi
# Download Android SDK jar if not present
if [ ! -f "$ANDROID_JAR" ]; then
echo "Downloading Android SDK jar..."
curl -L -o "$ANDROID_JAR" "https://github.com/Sable/android-platforms/raw/master/android-31/android.jar"
fi
# Create backup of original JAR
echo "Creating backup of original JAR..."
cp "$JAR_FILE" "$JAR_FILE.backup"
# Create work directory
echo "Creating work directory..."
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
# Extract original JAR (to get all other classes and META-INF)
echo "Extracting original JAR..."
jar xf "$JAR_FILE.backup"
# Copy modified source
echo "Copying modified source..."
cp "$SOURCE_FILE" org/qtproject/qt5/android/bluetooth/
# Compile
echo "Compiling QtBluetoothLE.java..."
javac -source 1.8 -target 1.8 \
-bootclasspath "$ANDROID_JAR" \
-cp "$JAR_FILE.backup" \
org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
if [ $? -ne 0 ]; then
echo "ERROR: Compilation failed!"
cd "$SCRIPT_DIR"
rm -rf "$WORK_DIR"
exit 1
fi
# Create new JAR
echo "Creating new JAR..."
jar cf QtAndroidBluetooth-new.jar org/ META-INF/
# Replace old JAR
echo "Replacing JAR file..."
cp QtAndroidBluetooth-new.jar "$JAR_FILE"
# Cleanup
echo "Cleaning up..."
cd "$SCRIPT_DIR"
rm -rf "$WORK_DIR"
echo ""
echo "=== SUCCESS ==="
echo "JAR compiled and updated: $JAR_FILE"
echo "Backup saved as: $JAR_FILE.backup"
echo ""

View File

@@ -9,7 +9,7 @@ These instructions build the app itself, not the test project.
## On a Linux System (from source)
```buildoutcfg
$ sudo apt update && sudo apt upgrade # this is very important on raspberry pi: you need the bluetooth firmware updated!
$ sudo apt update && sudo apt upgrade # this is very important on Raspberry Pi: you need the bluetooth firmware updated!
$ sudo apt install git qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qml-module* libqt5texttospeech5-dev libqt5texttospeech5 libqt5location5-plugins qtlocation5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
@@ -34,16 +34,15 @@ Download and install https://download.qt.io/archive/qt/5.12/5.12.12/qt-opensourc
![raspi](../docs/img/raspi-bike.jpg)
This guide will walk you through steps to setup an autonomous, headless raspberry bridge.
This guide will walk you through steps to setup an autonomous, headless Raspberry Pi bridge.
### Initial System Preparation
You can install a lightweight version of embedded OS to speed up your raspberry booting time.
You can install a lightweight version of embedded OS to speed up your Raspberry booting time.
#### Prepare your SD Card
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, the Raspberry lite OS version.
Boot on the raspberry (default credentials are pi/raspberry)
Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and install, on a SD card, [`Raspberry Pi OS Lite 64bit`](https://www.raspberrypi.com/software/operating-systems/). Boot up the Raspberry Pi (default credentials are pi/raspberry)
#### Change default credentials
@@ -56,7 +55,7 @@ Boot on the raspberry (default credentials are pi/raspberry)
`System Options` > `Wireless LAN`
Enter an SSID and your wifi password.
Your raspberry will fetch a DHCP address at boot time, which can be painful :
Your Raspberry will fetch a DHCP address at boot time, which can be painful :
- The IP address might change at every boot
- This process takes approximately 10 seconds at boot time.
@@ -77,7 +76,7 @@ Apply the changes `sudo systemctl restart dhcpcd.service` and ensure you have in
#### Enable SSH access
You might want to access your raspberry remotely while it is attached to your fitness equipment.
You might want to access your Raspberry remotely while it is attached to your fitness equipment.
`sudo raspi-config` > `Interface Options` > `SSH`
@@ -86,15 +85,17 @@ You might want to access your raspberry remotely while it is attached to your fi
This option allows a faster boot. `sudo raspi-config` > `System Options` > `Network at boot` > `No`
#### Reboot and test connectivity
Reboot your raspberry `sudo reboot now`
Reboot your Raspberry `sudo reboot now`
Congratulations !
Your raspberry should be reachable from your local network via SSH.
Your Raspberry should be reachable from your local network via SSH.
### QDOMYOS-ZWIFT installation
#### Update your raspberry (mandatory !)
Qdomyos-zwift can be compiled from source (hard), or using a binary (easy). **Only one is required**.
#### Update your Raspberry (mandatory !)
Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
@@ -103,7 +104,7 @@ Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
This operation takes a moment to complete.
#### Install qdomyos-zwift from sources
#### Option 1. Install qdomyos-zwift from sources
```bash
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtbase5-private-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 qt5-assistant libqt5networkauth5-dev libqt5websockets5-dev qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qtlocation5-dev qtquickcontrols2-5-dev libqt5texttospeech5-dev libqt5texttospeech5 g++ make qtbase5-dev libqt5sql5 libqt5sql5-mysql libqt5sql5-psql
@@ -126,20 +127,117 @@ Please note :
- Don't build the application with `-j4` option (this will fail)
- Build operation is circa 45 minutes (subsequent builds are faster)
#### Option 2. Install qdomyos-zwift from binary
Ensure you're logged in to GitHub and download `https://github.com/cagnulein/qdomyos-zwift/actions/runs/19521021942/artifacts/4622513957`. Extract the zip file and copy the QZ binary to the Raspberry Pi Zero 2 W. If you get a 404 Not Found you might have to login to GitHub first.
Make it executable:
```
chmod +x qdomyos-zwift-64bit
```
Install required libraries and dependencies for headless mode:
```
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64
```
If you are running Raspberry Pi Desktop OS, and you want to run the QZ UI, additonally add the qml libraries.
```
sudo apt install libqt5charts5 libqt5multimedia5 libqt5bluetooth5 libqt5xml5t64 libqt5positioning5 libqt5networkauth5 libqt5websockets5 libqt5texttospeech5 libqt5sql5t64 *qml*
```
#### Unblock Bluetooth (if using Bluetooth)
Unblock Bluetooth:
```
sudo rfkill unblock bluetooth
```
Troubleshooting Bluetooth not working:
Errors:
```
Fri Nov 21 18:05:07 2025 1763708707500 Debug: Bluez 5 detected.
qt.bluetooth.bluez: Aborting device discovery due to offline Bluetooth Adapter
Fri Nov 21 18:05:07 2025 1763708707540 Debug: Aborting device discovery due to offline Bluetooth Adapter
^C"SIGINT"
Fri Nov 21 18:05:21 2025 1763708721033 Debug: devices/bluetooth.cpp virtual bool bluetooth::handleSignal(int) "SIGINT"
```
Check if Bluetooth is blocked/down:
```
$ rfkill list
0: hci0: Bluetooth
Soft blocked: yes
Hard blocked: no
1: phy0: Wireless LAN
Soft blocked: no
Hard blocked: no
```
```
$ hciconfig -a
hci0: Type: Primary Bus: UART
BD Address: B8:27:EB:A2:85:70 ACL MTU: 1021:8 SCO MTU: 64:1
DOWN
RX bytes:3629 acl:0 sco:0 events:280 errors:0
TX bytes:48392 acl:0 sco:0 commands:280 errors:0
Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87
Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
Link policy: RSWITCH SNIFF
Link mode: PERIPHERAL ACCEPT
```
Unblock Bluetooth:
```
sudo rfkill unblock bluetooth
```
#### Test your installation
It is now time to check everything's fine
`./qdomyos-zwift -no-gui -heart-service`
`sudo ./qdomyos-zwift-64bit -no-gui -heart-service`
![initial setup](../docs/img/raspi_initial-startup.png)
Test your access from your fitness device.
Check logs to see if it's running:
```
journalctl -u qz.service -f
```
#### Update QZ config file
Running headless you need to update `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` with specific settings for your set up. If you already have it working on an iPhone/Android, follow this guide to deploy QZ with the UI, replicate the settings in the UI, check everything works, then take a copy of `/root/.config/'Roberto Viola'/qDomyos-Zwift.conf` to use with the headless deployment.
For my set up, I add:
Nordictrack C1650:
```
norditrack_s25_treadmill=true
proformtreadmillip=172.31.2.36
```
Zwift specific options (auto inclination not there yet in the Raspberry Pi version):
```
zwift_api_autoinclination=true
zwift_inclination_gain=1
zwift_inclination_offset=0
zwift_username=user@myemail.com
zwift_password=Password1
```
Check it works:
```
sudo ./qdomyos-zwift-64bit -no-gui -no-console -no-log
```
#### Automate QDOMYOS-ZWIFT at startup
You might want to have QDOMYOS-ZWIFT to start automatically at boot time.
Let's create a systemd service that we'll enable at boot sequence.
Let's create a systemd service that we'll enable at boot sequence. **Update ExecStart with the path and full name with commandline options for your qz binary. Update ExecStop with the full name of the binary.**
`sudo vi /lib/systemd/system/qz.service`
@@ -325,7 +423,7 @@ sudo tail -f /var/log/qz-treadmill-monitor.log
### (optional) Enable overlay FS
Once that everything is working as expected, and if you dedicate your Raspberry pi to this usage, you might want to enable the read-only overlay FS.
Once that everything is working as expected, and if you dedicate your Raspberry Pi to this usage, you might want to enable the read-only overlay FS.
By enabling the overlay read-only system, your SD card will be read-only only and every file written will be to RAM.
Then at each reboot the RAM is erased and you'll revert to the initial status of the overlay file-system.
@@ -350,7 +448,19 @@ Reboot immediately.
## Other tricks
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my raspberry to my bike.
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the raspberry.
I use some [3m magic scratches](https://www.amazon.fr/Command-Languettes-Accrochage-Tableaux-Larges/dp/B00X7792IE/ref=sr_1_5?dchild=1&keywords=accroche+tableau&qid=1616515278&sr=8-5) to attach my Raspberry to my bike.
I use the USB port from the bike console (always powered as long as the bike is plugged to main), maximum power is 500mA and this is enough for the Raspberry.
You can easily remove the Raspberry Pi from the bike if required.
## Trouobleshooting QZ on RPI
Run qz as root
For Zwift, check Zwift detects QZ. Check bluetooth
If Zwift isn't detecting speed from your exercise device, double check your .conf is correct. If you're not sure, Check the setup works using iPhone/Android phone, then replicate the settings by using Raspberry Pi Desktop OS and qz -qml to view the QZ UI. Change settings to match working iPhone/Android.
You can easily remove the raspberry pi from the bike if required.

25
docs/workout-editor.md Normal file
View File

@@ -0,0 +1,25 @@
# Workout Editor
The Workout Editor lets you create multi-device training sessions without leaving QZ.
## Open the Editor
- Drawer → Workout Editor
- Select the target device profile (treadmill, bike, elliptical, rower).
## Build Intervals
- Every interval exposes the parameters supported by the selected device.
- Use **Add Interval**, **Copy**, **Up/Down**, or **Del** to manage the timeline.
- Select a block of consecutive intervals and hit **Repeat Selection** to clone it quickly (perfect for repeat sets like work/rest pairs).
- Toggle **Show advanced parameters** to edit cadence targets, Peloton levels, heart-rate limits, GPS metadata, etc.
- The Chart.js preview updates automatically while you edit.
## Load or Save Programs
- **Load** imports any `.xml` plan from `training/`.
- **Save** writes the XML back into the same folder (name is sanitised automatically).
- **Save & Start** persists the file and immediately queues it for playback.
- Existing files trigger an overwrite confirmation.
## Tips
- Durations must follow `hh:mm:ss` format.
- Speed/incline units follow the global miles setting.
- Saved workouts appear inside the regular “Open Train Program” list.

View File

@@ -76,6 +76,8 @@ public class QtBluetoothLE {
private boolean mLeScanRunning = false;
private BluetoothGatt mBluetoothGatt = null;
private static QtBluetoothLE sConnectedInstance = null; // For direct read access
private static boolean sDirectReadPending = false; // Flag for direct read bypass
private HandlerThread mHandlerThread = null;
private Handler mHandler = null;
private Constructor mCharacteristicConstructor = null;
@@ -287,6 +289,36 @@ public class QtBluetoothLE {
android.bluetooth.BluetoothGattCharacteristic characteristic,
int status)
{
// Log for direct read debugging
Log.d(TAG, "onCharacteristicRead: UUID=" + characteristic.getUuid() + " status=" + status +
" value=" + java.util.Arrays.toString(characteristic.getValue()));
// Check if this is a direct read (bypass queue) - handle BEFORE timeout check
if (sDirectReadPending && status == 0) {
Log.d(TAG, "Direct read detected, getting handle for UUID: " + characteristic.getUuid());
// Get the proper handle so Qt can find the characteristic and emit the signal
int handle = handleForCharacteristic(characteristic);
Log.d(TAG, "Direct read: obtained handle = " + handle + " (Qt handle will be " + (handle + 1) + ")");
byte[] value = characteristic.getValue();
if (value != null && value.length > 0) {
// Call Qt's native callback with proper handle so the signal fires in C++
leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(),
handle + 1, characteristic.getUuid().toString(),
characteristic.getProperties(), value);
}
// Clear the flag after handling
sDirectReadPending = false;
//unlock the queue for next item
synchronized (readWriteQueue) {
pendingJob = null;
}
performNextIO();
return;
}
int foundHandle = -1;
synchronized (this) {
foundHandle = handleForCharacteristic(characteristic);
@@ -665,6 +697,14 @@ public class QtBluetoothLE {
}
}
// Save instance for Kettler direct read access
if (mBluetoothGatt != null) {
sConnectedInstance = this;
Log.d("QtBluetoothGatt", "connect(): mBluetoothGatt is valid, sConnectedInstance SET to " + this);
} else {
Log.w("QtBluetoothGatt", "connect(): mBluetoothGatt is NULL, sConnectedInstance NOT SET");
}
return mBluetoothGatt != null;
}
@@ -1558,6 +1598,86 @@ public class QtBluetoothLE {
return false;*/
}
// Custom method for direct characteristic read
// This bypasses Qt's job queue and directly calls BluetoothGatt.readCharacteristic()
public boolean readCharacteristicDirectly(String serviceUuid, String characteristicUuid) {
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: START - service=" + serviceUuid + " char=" + characteristicUuid);
if (mBluetoothGatt == null) {
Log.w("QtBluetoothGatt", "readCharacteristicDirectly: mBluetoothGatt is NULL!");
return false;
}
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: mBluetoothGatt OK, parsing UUIDs");
try {
java.util.UUID svcUUID = java.util.UUID.fromString(serviceUuid);
java.util.UUID charUUID = java.util.UUID.fromString(characteristicUuid);
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: UUIDs parsed, looking for service");
android.bluetooth.BluetoothGattService service = mBluetoothGatt.getService(svcUUID);
if (service == null) {
Log.w("QtBluetoothGatt", "readCharacteristicDirectly: service NOT FOUND for UUID " + serviceUuid);
// List all available services
java.util.List<android.bluetooth.BluetoothGattService> services = mBluetoothGatt.getServices();
Log.d("QtBluetoothGatt", "Available services count: " + services.size());
for (android.bluetooth.BluetoothGattService s : services) {
Log.d("QtBluetoothGatt", " - Service: " + s.getUuid().toString());
}
return false;
}
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: service FOUND, looking for characteristic");
android.bluetooth.BluetoothGattCharacteristic characteristic = service.getCharacteristic(charUUID);
if (characteristic == null) {
Log.w("QtBluetoothGatt", "readCharacteristicDirectly: characteristic NOT FOUND for UUID " + characteristicUuid);
// List all available characteristics in this service
java.util.List<android.bluetooth.BluetoothGattCharacteristic> chars = service.getCharacteristics();
Log.d("QtBluetoothGatt", "Available characteristics count: " + chars.size());
for (android.bluetooth.BluetoothGattCharacteristic c : chars) {
Log.d("QtBluetoothGatt", " - Characteristic: " + c.getUuid().toString());
}
return false;
}
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: characteristic FOUND, calling readCharacteristic()");
// Set flag to indicate this is a direct read (bypass queue check)
sDirectReadPending = true;
boolean result = mBluetoothGatt.readCharacteristic(characteristic);
Log.d("QtBluetoothGatt", "readCharacteristicDirectly: readCharacteristic() returned " + result);
if (!result) {
// Failed to initiate read, clear flag
sDirectReadPending = false;
}
return result;
} catch (Exception e) {
Log.e("QtBluetoothGatt", "readCharacteristicDirectly: EXCEPTION occurred", e);
sDirectReadPending = false;
return false;
}
}
// Static wrapper for calling readCharacteristicDirectly on the connected instance
public static boolean readCharacteristicDirectlyStatic(String serviceUuid, String characteristicUuid) {
Log.d("QtBluetoothGatt", "readCharacteristicDirectlyStatic called with service: " + serviceUuid + " char: " + characteristicUuid);
if (sConnectedInstance == null) {
Log.w("QtBluetoothGatt", "readCharacteristicDirectlyStatic: sConnectedInstance is NULL!");
return false;
}
Log.d("QtBluetoothGatt", "readCharacteristicDirectlyStatic: sConnectedInstance found, calling instance method");
boolean result = sConnectedInstance.readCharacteristicDirectly(serviceUuid, characteristicUuid);
Log.d("QtBluetoothGatt", "readCharacteristicDirectlyStatic: returning " + result);
return result;
}
/*
* Modifies and returns the given \a handle such that the job
* \a type is encoded into the returned handle. Hereby we take advantage of the fact that

View File

@@ -25,6 +25,11 @@ ColumnLayout {
Layout.fillWidth: true
height: 48
Accessible.role: Accessible.Button
Accessible.name: title
Accessible.description: expanded ? "Expanded" : "Collapsed"
Accessible.onPressAction: toggle()
Rectangle {
id: indicatRect
x: 16; y: 20

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

@@ -92,7 +92,7 @@ class BluetoothHandler : public QObject
void onKeyPressed(int keyCode)
{
qDebug() << "Key pressed:" << keyCode;
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == bluetoothdevice::BIKE) {
if (m_bluetooth && m_bluetooth->device() && m_bluetooth->device()->deviceType() == BIKE) {
if (keyCode == 115) // up
((bike*)m_bluetooth->device())->setGears(((bike*)m_bluetooth->device())->gears() + 1);
else if (keyCode == 114) // down

View File

@@ -246,7 +246,7 @@ ColumnLayout {
elevationGain = elevationGain + (pathController.geopath.coordinateAt(i).altitude - pathController.geopath.coordinateAt(i-1).altitude)
lines[i] = pathController.geopath.coordinateAt(i)
}
distance.text = "Distance " + (pathController.geopath.length() / 1000.0).toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
distance.text = "Distance " + pathController.distance.toFixed(1) + " km Elevation Gain: " + elevationGain.toFixed(1) + " meters"
return lines;
}

View File

@@ -14,6 +14,10 @@ HomeForm {
width: parent.fill
height: parent.fill
color: settings.theme_background_color
// VoiceOver accessibility - ignore decorative background
Accessible.role: Accessible.Pane
Accessible.ignored: true
}
signal start_clicked;
signal stop_clicked;
@@ -72,7 +76,19 @@ HomeForm {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("New lap started!")
}
}
}
}
MessageDialog {
id: stopConfirmationDialog
text: qsTr("Stop Workout")
informativeText: qsTr("Do you really want to stop the current workout?")
buttons: (MessageDialog.Yes | MessageDialog.No)
onYesClicked: {
close();
inner_stop();
}
onNoClicked: close()
}
Timer {
@@ -141,7 +157,11 @@ HomeForm {
start.onClicked: { start_clicked(); }
stop.onClicked: {
inner_stop();
if (rootItem.confirmStopEnabled()) {
stopConfirmationDialog.open();
} else {
inner_stop();
}
}
lap.onClicked: { lap_clicked(); popupLap.open(); popupLapAutoClose.running = true; }
@@ -169,6 +189,8 @@ HomeForm {
gridView.leftMargin = (parent.width % cellWidth) / 2;
}
Accessible.ignored: true
delegate: Item {
id: id1
width: 170 * settings.ui_zoom / 100
@@ -177,6 +199,12 @@ HomeForm {
visible: visibleItem
Component.onCompleted: console.log("completed " + objectName)
// VoiceOver accessibility support
Accessible.role: largeButton ? Accessible.Button : (writable ? Accessible.Pane : Accessible.StaticText)
Accessible.name: name + (largeButton ? "" : (": " + value))
Accessible.description: largeButton ? largeButtonLabel : (secondLine !== "" ? secondLine : (writable ? qsTr("Adjustable. Current value: ") + value : qsTr("Current value: ") + value))
Accessible.focusable: true
Behavior on x {
enabled: id1.state != "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
@@ -210,6 +238,9 @@ HomeForm {
border.color: (settings.theme_tile_shadow_enabled ? settings.theme_tile_shadow_color : settings.theme_tile_background_color)
color: settings.theme_tile_background_color
id: rect
// Ignore for VoiceOver - decorative background only
Accessible.ignored: true
}
DropShadow {
@@ -240,6 +271,9 @@ HomeForm {
height: 48 * settings.ui_zoom / 100
source: icon
visible: settings.theme_tile_icon_enabled && !largeButton
// Ignore for VoiceOver - decorative only
Accessible.ignored: true
}
Text {
objectName: "value"
@@ -254,6 +288,9 @@ HomeForm {
font.pointSize: valueFontSize * settings.ui_zoom / 100
font.bold: true
visible: !largeButton
// Ignore for VoiceOver - parent Item handles accessibility
Accessible.ignored: true
}
Text {
objectName: "secondLine"
@@ -269,6 +306,9 @@ HomeForm {
font.pointSize: settings.theme_tile_secondline_textsize * settings.ui_zoom / 100
font.bold: false
visible: !largeButton
// Ignore for VoiceOver - parent Item handles accessibility
Accessible.ignored: true
}
Text {
id: myText
@@ -283,6 +323,9 @@ HomeForm {
anchors.leftMargin: 55 * settings.ui_zoom / 100
anchors.topMargin: 20 * settings.ui_zoom / 100
visible: !largeButton
// Ignore for VoiceOver - parent Item handles accessibility
Accessible.ignored: true
}
RoundButton {
objectName: minusName
@@ -295,6 +338,13 @@ HomeForm {
anchors.leftMargin: 2
width: 48 * settings.ui_zoom / 100
height: 48 * settings.ui_zoom / 100
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: qsTr("Decrease ") + name
Accessible.description: qsTr("Decrease the value of ") + name
Accessible.focusable: true
Accessible.onPressAction: { minus_clicked(objectName) }
}
RoundButton {
autoRepeat: true
@@ -307,6 +357,13 @@ HomeForm {
anchors.rightMargin: 2
width: 48 * settings.ui_zoom / 100
height: 48 * settings.ui_zoom / 100
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: qsTr("Increase ") + name
Accessible.description: qsTr("Increase the value of ") + name
Accessible.focusable: true
Accessible.onPressAction: { plus_clicked(objectName) }
}
RoundButton {
autoRepeat: true
@@ -320,6 +377,13 @@ HomeForm {
radius: 20
}
font.pointSize: 20 * settings.ui_zoom / 100
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: largeButtonLabel
Accessible.description: name + ": " + largeButtonLabel
Accessible.focusable: true
Accessible.onPressAction: { largeButton_clicked(objectName) }
}
}
}

View File

@@ -9,6 +9,9 @@ Page {
title: qsTr("QZ Fitness")
id: page
// VoiceOver accessibility - ignore Page itself, only children are accessible
Accessible.ignored: true
property alias start: start
property alias stop: stop
property alias lap: lap
@@ -39,6 +42,8 @@ Page {
width: 50
height: row.height
color: settings.theme_background_color
Accessible.ignored: true
Column {
id: column
anchors.horizontalCenter: parent.horizontalCenter
@@ -47,10 +52,13 @@ Page {
height: row.height
spacing: 0
padding: 0
Accessible.ignored: true
Rectangle {
width: 50
height: row.height
color: settings.theme_background_color
Accessible.ignored: true
Image {
anchors.verticalCenter: parent.verticalCenter
@@ -60,6 +68,12 @@ Page {
source: "icons/icons/bluetooth-icon.png"
enabled: rootItem.device
smooth: true
// VoiceOver accessibility
Accessible.role: Accessible.Indicator
Accessible.name: qsTr("Bluetooth connection")
Accessible.description: rootItem.device ? qsTr("Device connected") : qsTr("Device not connected")
Accessible.focusable: true
}
ColorOverlay {
anchors.fill: treadmill_connection
@@ -74,6 +88,7 @@ Page {
height: row.height - 76
source: rootItem.signal
smooth: true
Accessible.ignored: true
}
}
}
@@ -82,6 +97,8 @@ Page {
width: 120
height: row.height
color: settings.theme_background_color
Accessible.ignored: true
RoundButton {
icon.source: rootItem.startIcon
icon.height: row.height - 54
@@ -91,6 +108,12 @@ Page {
id: start
width: 120
height: row.height - 4
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: rootItem.startText
Accessible.description: qsTr("Start workout")
Accessible.focusable: true
}
ColorOverlay {
anchors.fill: start
@@ -104,6 +127,7 @@ Page {
width: 120
height: row.height
color: settings.theme_background_color
Accessible.ignored: true
RoundButton {
icon.source: rootItem.stopIcon
@@ -114,6 +138,12 @@ Page {
id: stop
width: 120
height: row.height - 4
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: rootItem.stopText
Accessible.description: qsTr("Stop workout")
Accessible.focusable: true
}
ColorOverlay {
anchors.fill: stop
@@ -128,6 +158,8 @@ Page {
width: 50
height: row.height
color: settings.theme_background_color
Accessible.ignored: true
RoundButton {
anchors.verticalCenter: parent.verticalCenter
id: lap
@@ -138,6 +170,12 @@ Page {
icon.height: 48
enabled: rootItem.lap
smooth: true
// VoiceOver accessibility
Accessible.role: Accessible.Button
Accessible.name: qsTr("Lap")
Accessible.description: qsTr("Record a new lap")
Accessible.focusable: true
}
ColorOverlay {
anchors.fill: lap
@@ -165,7 +203,7 @@ Page {
width: parent.width
anchors.top: row1.bottom
anchors.topMargin: 30
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) bluetooth and bluetooth permission MUST be on<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
text: "This app should automatically connect to your bike/treadmill/rower. <b>If it doesn't, please check</b>:<br>1) your Echelon/Domyos App MUST be closed while qdomyos-zwift is running;<br>2) both Bluetooth and Bluetooth permissions MUST be enabled<br>3) your bike/treadmill/rower should be turned on BEFORE starting this app<br>4) try to restart your device<br><br>If your bike/treadmill disconnects every 30 seconds try to disable the 'virtual device' setting on the left bar.<br><br>In case of issues, please feel free to contact me at roberto.viola83@gmail.com.<br><br><b>Have a nice ride!</b><br/ ><i>QZ specifically disclaims liability for<br>incidental or consequential damages and assumes<br>no responsibility or liability for any loss<br>or damage suffered by any person as a result of<br>the use or misuse of the app.</i><br><br>Roberto Viola"
wrapMode: Label.WordWrap
visible: rootItem.labelHelp
}

View File

@@ -22,6 +22,11 @@ ColumnLayout {
Layout.fillWidth: true;
height: 48
Accessible.role: Accessible.Button
Accessible.name: title
Accessible.description: expanded ? "Expanded" : "Collapsed"
Accessible.onPressAction: toggle()
Rectangle{
id:indicatRect
x: 16; y: 20

View File

@@ -42,11 +42,27 @@ class PathController : public QObject {
void centerChanged() W_SIGNAL(centerChanged)
double distance() const {
return mDistance;
}
void setDistance(double distance) {
if (qFuzzyCompare(distance, mDistance)) {
return;
}
mDistance = distance;
emit distanceChanged();
}
void distanceChanged() W_SIGNAL(distanceChanged)
private : QGeoPath mGeoPath;
QGeoCoordinate mCenter;
double mDistance = 0.0;
W_PROPERTY(QGeoPath, geopath READ geoPath WRITE setGeoPath NOTIFY geopathChanged)
W_PROPERTY(QGeoCoordinate, center READ center WRITE setCenter NOTIFY centerChanged)
W_PROPERTY(double, distance READ distance WRITE setDistance NOTIFY distanceChanged)
};
#endif // APPLICATION_PATHCONTROLLER_H

View File

@@ -74,12 +74,12 @@ ColumnLayout {
id: filterField
onTextChanged: updateFilter()
}
Button {
anchors.left: mainRect.right
anchors.leftMargin: 5
text: "←"
onClicked: folderModel.folder = folderModel.parentFolder
}
Button {
anchors.left: mainRect.right
anchors.leftMargin: 5
text: "←"
onClicked: folderModel.folder = folderModel.parentFolder
}
}
ListView {
@@ -95,10 +95,10 @@ ColumnLayout {
id: folderModel
nameFilters: ["*.xml", "*.zwo"]
folder: "file://" + rootItem.getWritableAppDir() + 'training'
showDotAndDotDot: false
showDotAndDotDot: false
showDirs: true
sortField: "Name"
showDirsFirst: true
sortField: "Name"
showDirsFirst: true
}
model: folderModel
delegate: Component {
@@ -106,7 +106,7 @@ ColumnLayout {
property alias textColor: fileTextBox.color
width: parent.width
height: 40
color: Material.backgroundColor
color: Material.backgroundColor
z: 1
Item {
id: root
@@ -145,12 +145,12 @@ ColumnLayout {
console.log('onclicked ' + index+ " count "+list.count);
if (index == list.currentIndex) {
let fileUrl = folderModel.get(list.currentIndex, 'fileUrl') || folderModel.get(list.currentIndex, 'fileURL');
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
if (fileUrl && !folderModel.isFolder(list.currentIndex)) {
trainprogram_open_clicked(fileUrl);
popup.open()
} else {
folderModel.folder = fileURL
}
} else {
folderModel.folder = fileURL
}
}
else {
if (list.currentItem)

View File

@@ -0,0 +1,349 @@
import QtQuick 2.7
import Qt.labs.folderlistmodel 2.15
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.0
import QtQuick.Dialogs 1.0
import Qt.labs.settings 1.0
import Qt.labs.platform 1.1
import QtWebView 1.1
ColumnLayout {
signal trainprogram_open_clicked(url name)
signal trainprogram_open_other_folder(url name)
signal trainprogram_preview(url name)
signal trainprogram_autostart_requested()
property url pendingWorkoutUrl: ""
Settings {
id: settings
property real ftp: 200.0
}
property var selectedFileUrl: ""
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
id: fileDialog
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
var chosenFile = fileDialog.fileUrl || fileDialog.file || (fileDialog.fileUrls && fileDialog.fileUrls.length > 0 ? fileDialog.fileUrls[0] : "")
console.log("You chose: " + chosenFile)
selectedFileUrl = chosenFile
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(chosenFile)
} else {
trainprogram_open_clicked(chosenFile)
}
close()
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
fileDialogLoader.active = false
}
}
}
}
StackView {
id: stackView
Layout.fillWidth: true
Layout.fillHeight: true
initialItem: masterView
// MASTER VIEW - Lista Workout
Component {
id: masterView
ColumnLayout {
spacing: 5
Row {
Layout.fillWidth: true
spacing: 5
Text {
text: "Filter"
color: "white"
verticalAlignment: Text.AlignVCenter
}
TextField {
id: filterField
Layout.fillWidth: true
function updateFilter() {
var text = filterField.text
var filter = "*"
for(var i = 0; i<text.length; i++)
filter+= "[%1%2]".arg(text[i].toUpperCase()).arg(text[i].toLowerCase())
filter+="*"
folderModel.nameFilters = [filter + ".zwo", filter + ".xml"]
}
onTextChanged: updateFilter()
}
Button {
text: "←"
onClicked: folderModel.folder = folderModel.parentFolder
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.vertical: ScrollBar {}
id: list
FolderListModel {
id: folderModel
nameFilters: ["*.xml", "*.zwo"]
folder: "file://" + rootItem.getWritableAppDir() + 'training'
showDotAndDotDot: false
showDirs: true
sortField: "Name"
showDirsFirst: true
}
model: folderModel
delegate: Component {
Rectangle {
width: ListView.view.width
height: 50
color: ListView.isCurrentItem ? Material.color(Material.Green, Material.Shade800) : Material.backgroundColor
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
Text {
id: fileIcon
text: folderModel.isFolder(index) ? "📁" : "📄"
font.pixelSize: 24
}
Text {
id: fileName
Layout.fillWidth: true
text: !folderModel.isFolder(index) ?
folderModel.get(index, "fileName").substring(0, folderModel.get(index, "fileName").length-4) :
folderModel.get(index, "fileName")
color: folderModel.isFolder(index) ? Material.color(Material.Orange) : "white"
font.pixelSize: 16
elide: Text.ElideRight
}
Text {
text: ""
font.pixelSize: 24
color: Material.color(Material.Grey)
visible: !ListView.isCurrentItem
}
}
MouseArea {
anchors.fill: parent
onClicked: {
list.currentIndex = index
let fileUrl = folderModel.get(index, 'fileUrl') || folderModel.get(index, 'fileURL');
if (folderModel.isFolder(index)) {
// Navigate to folder
folderModel.folder = fileUrl
} else if (fileUrl) {
// Load preview and show detail view
console.log('Loading preview for: ' + fileUrl);
trainprogram_preview(fileUrl)
pendingWorkoutUrl = fileUrl
// Wait for preview to load then push detail view
detailViewTimer.restart()
}
}
}
}
}
focus: true
}
Button {
Layout.fillWidth: true
height: 50
text: "Other folders"
onClicked: {
fileDialogLoader.active = true
}
}
// Timer to push detail view after preview loads
Timer {
id: detailViewTimer
interval: 300
repeat: false
onTriggered: {
stackView.push(detailView)
}
}
}
}
// DETAIL VIEW - Anteprima Workout
Component {
id: detailView
ColumnLayout {
spacing: 10
// Header con pulsanti
RowLayout {
Layout.fillWidth: true
Layout.margins: 5
spacing: 10
Button {
text: "← Back"
onClicked: stackView.pop()
}
Item { Layout.fillWidth: true }
Button {
text: "Start Workout"
highlighted: true
Material.background: Material.Green
onClicked: {
trainprogram_open_clicked(pendingWorkoutUrl)
trainprogram_autostart_requested()
stackView.pop()
}
}
}
// Descrizione workout
Text {
Layout.fillWidth: true
Layout.margins: 10
text: rootItem.previewWorkoutDescription
font.pixelSize: 14
font.bold: true
color: "white"
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
Text {
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
text: rootItem.previewWorkoutTags
font.pixelSize: 12
wrapMode: Text.WordWrap
color: Material.color(Material.Grey, Material.Shade400)
horizontalAlignment: Text.AlignHCenter
}
// WebView con grafico
WebView {
id: previewWebView
Layout.fillWidth: true
Layout.fillHeight: true
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/workoutpreview/preview.html"
Component.onCompleted: {
// Update workout after a short delay to ensure data is loaded
updateTimer.restart()
}
Timer {
id: updateTimer
interval: 400
repeat: false
onTriggered: previewWebView.updateWorkout()
}
function updateWorkout() {
if (!rootItem.preview_workout_points) return;
// Build arrays for the workout data
var watts = [];
var speed = [];
var inclination = [];
var resistance = [];
var cadence = [];
var hasWatts = false;
var hasSpeed = false;
var hasInclination = false;
var hasResistance = false;
var hasCadence = false;
for (var i = 0; i < rootItem.preview_workout_points; i++) {
if (rootItem.preview_workout_watt && rootItem.preview_workout_watt[i] !== undefined && rootItem.preview_workout_watt[i] > 0) {
watts.push({ x: i, y: rootItem.preview_workout_watt[i] });
hasWatts = true;
}
if (rootItem.preview_workout_speed && rootItem.preview_workout_speed[i] !== undefined && rootItem.preview_workout_speed[i] > 0) {
speed.push({ x: i, y: rootItem.preview_workout_speed[i] });
hasSpeed = true;
}
if (rootItem.preview_workout_inclination && rootItem.preview_workout_inclination[i] !== undefined && rootItem.preview_workout_inclination[i] > -200) {
inclination.push({ x: i, y: rootItem.preview_workout_inclination[i] });
hasInclination = true;
}
if (rootItem.preview_workout_resistance && rootItem.preview_workout_resistance[i] !== undefined && rootItem.preview_workout_resistance[i] >= 0) {
resistance.push({ x: i, y: rootItem.preview_workout_resistance[i] });
hasResistance = true;
}
if (rootItem.preview_workout_cadence && rootItem.preview_workout_cadence[i] !== undefined && rootItem.preview_workout_cadence[i] > 0) {
cadence.push({ x: i, y: rootItem.preview_workout_cadence[i] });
hasCadence = true;
}
}
// Determine device type based on available data
var deviceType = 'bike'; // default
// Priority 1: If has resistance, it's a bike (regardless of inclination)
if (hasResistance) {
deviceType = 'bike';
}
// Priority 2: If has speed or inclination (without resistance), it's a treadmill
else if (hasSpeed || hasInclination) {
deviceType = 'treadmill';
}
// Priority 3: If has power or cadence (bike metrics), it's a bike
else if (hasWatts || hasCadence) {
deviceType = 'bike';
}
// Call JavaScript function in the WebView
var data = {
points: rootItem.preview_workout_points,
watts: watts,
speed: speed,
inclination: inclination,
resistance: resistance,
cadence: cadence,
deviceType: deviceType,
miles_unit: settings.value("miles_unit", false)
};
runJavaScript("if(window.setWorkoutData) window.setWorkoutData(" + JSON.stringify(data) + ");");
}
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtQuick.Dialogs 1.0
import QtGraphicalEffects 1.12
import Qt.labs.settings 1.0
import QtMultimedia 5.15
import QtQuick.Layouts 1.3
import QtWebView 1.1
Item {
anchors.fill: parent
height: parent.height
width: parent.width
visible: true
WebView {
anchors.fill: parent
height: parent.height
width: parent.width
visible: !rootItem.generalPopupVisible
url: rootItem.getIntervalsICUAuthUrl
}
Popup {
id: popupIntervalsICUConnectedWeb
parent: Overlay.overlay
enabled: rootItem.generalPopupVisible
onEnabledChanged: { if(rootItem.generalPopupVisible) popupIntervalsICUConnectedWeb.open() }
onClosed: { rootItem.generalPopupVisible = false; }
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
width: 380
height: 120
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: 370
height: 120
text: qsTr("Your Intervals.icu account is now connected!<br><br>When you will press STOP on QZ a file<br>will be automatically uploaded to Intervals.icu!")
}
}
}
}

View File

@@ -34,6 +34,7 @@ Page {
property string heart_rate_belt_name: "Disabled"
property bool garmin_companion: false
property string filter_device: "Disabled"
property bool weight_kg_unit: false
}
background: Rectangle {
@@ -845,7 +846,6 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 0.5;
stackViewLocal.push(finalStepComponent);
}
}
@@ -904,7 +904,6 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 1;
stackViewLocal.push(finalStepComponent);
}
}
@@ -1183,7 +1182,7 @@ Page {
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Weight (" + (settings.miles_unit ? "lbs" : "kg") + ")")
text: qsTr("Weight (" + ((settings.miles_unit && !settings.weight_kg_unit) ? "lbs" : "kg") + ")")
font.pixelSize: 20
color: "white"
}
@@ -1191,13 +1190,13 @@ Page {
SpinBox {
id: weightSpinBox
Layout.alignment: Qt.AlignHCenter
from: settings.miles_unit ? 660 : 300 // 66.0 lbs or 30.0 kg
to: settings.miles_unit ? 4400 : 2000 // 440.0 lbs or 200.0 kg
value: settings.miles_unit ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
from: (settings.miles_unit && !settings.weight_kg_unit) ? 660 : 300 // 66.0 lbs or 30.0 kg
to: (settings.miles_unit && !settings.weight_kg_unit) ? 4400 : 2000 // 440.0 lbs or 200.0 kg
value: (settings.miles_unit && !settings.weight_kg_unit) ? (settings.weight * 2.20462 * 10).toFixed(0) : (settings.weight * 10)
stepSize: 1
editable: true
property real realValue: settings.miles_unit ? value / 22.0462 : value / 10
property real realValue: (settings.miles_unit && !settings.weight_kg_unit) ? value / 22.0462 : value / 10
textFromValue: function(value, locale) {
return Number(value / 10).toLocaleString(locale, 'f', 1)

61
src/WorkoutEditor.qml Normal file
View File

@@ -0,0 +1,61 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import Qt.labs.settings 1.0
import QtWebView 1.1
Item {
id: root
property string title: qsTr("Workout Editor")
property bool pageLoaded: false
signal closeRequested()
Settings {
id: settings
}
Timer {
id: portPoller
interval: 500
repeat: true
running: !root.pageLoaded
onTriggered: {
var port = settings.value("template_inner_QZWS_port", 0)
if (!port) {
return
}
var targetUrl = "http://localhost:" + port + "/workouteditor/index.html"
if (webView.url !== targetUrl) {
webView.url = targetUrl
}
}
}
WebView {
id: webView
anchors.fill: parent
visible: root.pageLoaded
onLoadingChanged: {
if (loadRequest.status === WebView.LoadSucceededStatus) {
root.pageLoaded = true
busy.visible = false
busy.running = false
portPoller.stop()
} else if (loadRequest.status === WebView.LoadFailedStatus) {
root.pageLoaded = false
busy.visible = true
busy.running = true
portPoller.start()
}
}
}
BusyIndicator {
id: busy
anchors.centerIn: parent
visible: !root.pageLoaded
running: !root.pageLoaded
}
Component.onCompleted: portPoller.start()
}

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.11" android:versionCode="1155" 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.26" android:versionCode="1274" 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

@@ -57,6 +57,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.60'
implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
implementation("com.garmin.connectiq:ciq-companion-app-sdk:2.2.0@aar")
}
import org.apache.tools.ant.taskdefs.condition.Os
@@ -127,7 +128,7 @@ android {
defaultConfig {
resConfig "en"
compileSdkVersion 33
compileSdkVersion 33
minSdkVersion = 21
targetSdkVersion = 36
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -73,17 +73,27 @@ public class Garmin {
}
public static void init(Context c) {
if (connectIqReady || connectIqInitializing) {
QLog.d(TAG, "Garmin already initialized or initializing");
return;
}
connectIqInitializing = true;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
connectIQ = ConnectIQ.getInstance(c, ConnectIQ.IQConnectType.WIRELESS);
// Create wrapped context BEFORE getInstance to ensure all SDK operations use it
context = createWrappedContext(c);
connectIQ = ConnectIQ.getInstance(context, ConnectIQ.IQConnectType.WIRELESS);
// init a wrapped SDK with fix for "Cannot cast to Long" issue viz https://forums.garmin.com/forum/developers/connect-iq/connect-iq-bug-reports/158068-?p=1278464#post1278464
context = initializeConnectIQWrapped(c, connectIQ, false, new ConnectIQ.ConnectIQListener() {
initializeConnectIQWithContext(connectIQ, false, new ConnectIQ.ConnectIQListener() {
@Override
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
QLog.e(TAG, errStatus.toString());
connectIqInitializing = false;
connectIqReady = false;
}
@@ -151,12 +161,8 @@ public class Garmin {
connectIQ.sendMessage(getDevice(), getApp(), message, listener);
}
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
if (connectIQ instanceof ConnectIQAdbStrategy) {
connectIQ.initialize(context, autoUI, listener);
return context;
}
Context wrappedContext = new ContextWrapper(context) {
private static Context createWrappedContext(Context context) {
return new ContextWrapper(context) {
private HashMap<BroadcastReceiver, BroadcastReceiver> receiverToWrapper = new HashMap<>();
@Override
@@ -183,6 +189,18 @@ public class Garmin {
if (wrappedReceiver != null) super.unregisterReceiver(wrappedReceiver);
}
};
}
private static void initializeConnectIQWithContext(ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
connectIQ.initialize(context, autoUI, listener);
}
private static Context initializeConnectIQWrapped(Context context, ConnectIQ connectIQ, boolean autoUI, ConnectIQ.ConnectIQListener listener) {
if (connectIQ instanceof ConnectIQAdbStrategy) {
connectIQ.initialize(context, autoUI, listener);
return context;
}
Context wrappedContext = createWrappedContext(context);
connectIQ.initialize(wrappedContext, autoUI, listener);
return wrappedContext;
}

View File

@@ -20,32 +20,73 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
QLog.d(TAG, "onReceive intent " + intent.getAction());
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
}
try {
QLog.d(TAG, "=== GARMIN INTENT DEBUG START ===");
QLog.d(TAG, "Action: " + intent.getAction());
// Log all extras in the intent
if (intent.getExtras() != null) {
QLog.d(TAG, "Extras bundle: " + intent.getExtras());
try {
for (String key : intent.getExtras().keySet()) {
Object value = intent.getExtras().get(key);
QLog.d(TAG, " Extra[" + key + "] = " + value + " (type: " + (value != null ? value.getClass().getName() : "null") + ")");
}
} catch (Exception e) {
QLog.e(TAG, "Error iterating extras: " + e.toString());
}
} else {
QLog.d(TAG, "No extras in intent");
}
// Process known actions
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
QLog.d(TAG, "Processing SEND_MESSAGE_STATUS");
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
QLog.d(TAG, "Processing OPEN_APPLICATION");
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_OPEN_APPLICATION_DEVICE");
} else if ("com.garmin.android.connectiq.DEVICE_STATUS".equals(intent.getAction())) {
QLog.d(TAG, "Processing DEVICE_STATUS");
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else if ("com.garmin.android.connectiq.INCOMING_MESSAGE".equals(intent.getAction())) {
QLog.d(TAG, "Processing INCOMING_MESSAGE");
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else {
QLog.d(TAG, "Unknown action, no processing");
}
QLog.d(TAG, "Calling wrapped receiver.onReceive()");
receiver.onReceive(context, intent);
} catch (IllegalArgumentException | BufferUnderflowException e) {
QLog.d(TAG, e.toString());
QLog.d(TAG, "=== GARMIN INTENT DEBUG END (success) ===");
} catch (Exception e) {
QLog.e(TAG, "=== EXCEPTION in wrapper (BEFORE or DURING receiver call) ===");
QLog.e(TAG, "Exception type: " + e.getClass().getName());
QLog.e(TAG, "Exception message: " + e.getMessage());
QLog.e(TAG, "Stack trace:", e);
QLog.e(TAG, "=== GARMIN INTENT DEBUG END (error) ===");
}
}
private static void replaceIQDeviceById(Intent intent, String extraName) {
try {
QLog.d(TAG, " Attempting to get Parcelable for extra: " + extraName);
IQDevice device = intent.getParcelableExtra(extraName);
if (device != null) {
// Logger.logDebug("Replacing " + device.describeContents() + " " + device.getFriendlyName() + " by " + device.getDeviceIdentifier() );
intent.putExtra(extraName, device.getDeviceIdentifier());
QLog.d(TAG, " Found IQDevice: " + device.getFriendlyName() + " (ID: " + device.getDeviceIdentifier() + ")");
long deviceId = device.getDeviceIdentifier();
intent.putExtra(extraName, deviceId);
QLog.d(TAG, " Replaced IQDevice with Long ID: " + deviceId);
} else {
QLog.d(TAG, " Extra '" + extraName + "' is null or not an IQDevice");
}
} catch (ClassCastException e) {
QLog.d(TAG, e.toString());
// It's already a long, i.e. on the simulator.
QLog.d(TAG, " ClassCastException for '" + extraName + "': " + e.toString());
QLog.d(TAG, " (Extra is already a Long, probably on simulator)");
} catch (Exception e) {
QLog.e(TAG, " Unexpected exception in replaceIQDeviceById: " + e.toString());
QLog.e(TAG, " Stack trace:", e);
}
}

View File

@@ -7,9 +7,13 @@ import android.content.IntentFilter;
import android.media.AudioManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
public class MediaButtonReceiver extends BroadcastReceiver {
private static MediaButtonReceiver instance;
private static final int TARGET_VOLUME = 7; // Middle volume value for infinite gear changes
private static boolean restoringVolume = false; // Flag to prevent recursion
@Override
public void onReceive(Context context, Intent intent) {
@@ -21,8 +25,30 @@ public class MediaButtonReceiver extends BroadcastReceiver {
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Previous: " + previousVolume + ", Max: " + maxVolume + ", Restoring: " + restoringVolume);
// If we're restoring volume, skip processing and reset flag
if (restoringVolume) {
QLog.d("MediaButtonReceiver", "Volume restore completed");
restoringVolume = false;
return;
}
// Process the gear change
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
// Auto-restore volume to middle value after a short delay to enable infinite gear changes
if (currentVolume != TARGET_VOLUME) {
final AudioManager am = audioManager;
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
QLog.d("MediaButtonReceiver", "Auto-restoring volume to: " + TARGET_VOLUME);
restoringVolume = true;
am.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
}
}, 100); // 100ms delay to ensure gear change is processed first
}
}
}
@@ -54,7 +80,25 @@ public class MediaButtonReceiver extends BroadcastReceiver {
}
}
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
// Initialize volume to target value for gear control
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager != null) {
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (currentVolume != TARGET_VOLUME) {
QLog.d("MediaButtonReceiver", "Initializing volume to: " + TARGET_VOLUME);
restoringVolume = true;
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, TARGET_VOLUME, 0);
// Reset flag after initialization
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
restoringVolume = false;
}
}, 200);
}
}
} catch (IllegalArgumentException e) {
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
} catch (Exception e) {

View File

@@ -208,13 +208,13 @@ public class SDMChannelController {
byte[] payload = new byte[8];
payload[0] = (byte) 0x01;
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
payload[2] = (byte) ((lastTime % 256000) / 1000);
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
payload[3] = (byte) 0x00;
payload[4] = (byte) speedM_s;
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
payload[6] = (byte) stride_count++; // bad but it works on zwift
payload[7] = (byte) ((double)deltaTime * 0.03125);
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
payload[6] = (byte) stride_count++;
payload[7] = (byte) 0; // update latency: no delay in real-time system
if (mIsOpen) {
try {
@@ -257,13 +257,13 @@ public class SDMChannelController {
byte[] payload = new byte[8];
payload[0] = (byte) 0x01;
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
payload[2] = (byte) ((lastTime % 256000) / 1000);
payload[1] = (byte) ((lastTime % 1000) / 5); // time fractional: 0-199 in 1/200 sec units
payload[2] = (byte) ((lastTime / 1000) % 256); // time integer: seconds mod 256
payload[3] = (byte) 0x00;
payload[4] = (byte) speedM_s;
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
payload[6] = (byte) stride_count;
payload[7] = (byte) ((double)deltaTime * 0.03125);
payload[4] = (byte) ((int)speedM_s & 0x0F); // speed integer in lower 4 bits only
payload[5] = (byte) Math.round((speedM_s - (double)((int)speedM_s)) * 256.0);
payload[6] = (byte) stride_count++;
payload[7] = (byte) 0; // update latency: no delay in real-time system
if (mIsOpen) {
try {

View File

@@ -43,7 +43,11 @@ public class Usbserial {
static int lastReadLen = 0;
public static void open(Context context) {
QLog.d("QZ","UsbSerial open");
open(context, 2400); // Default baud rate for Computrainer
}
public static void open(Context context, int baudRate) {
QLog.d("QZ","UsbSerial open with baud rate: " + baudRate);
// Find all available drivers from attached devices.
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
@@ -98,13 +102,12 @@ public class Usbserial {
port = driver.getPorts().get(0); // Most devices have just one port (port 0)
try {
port.open(connection);
port.setParameters(2400, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
port.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
QLog.d("QZ","UsbSerial port opened successfully at " + baudRate + " baud");
}
catch (IOException e) {
// Do something here
QLog.d("QZ","UsbSerial port open failed: " + e.getMessage());
}
QLog.d("QZ","UsbSerial port opened");
}
public static void write (byte[] bytes) {

View File

@@ -0,0 +1,6 @@
#ifndef BLUETOOTHDEVICETYPE_H
#define BLUETOOTHDEVICETYPE_H
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
#endif // BLUETOOTHDEVICETYPE_H

View File

@@ -5,7 +5,7 @@ CharacteristicNotifier2A53::CharacteristicNotifier2A53(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2a53, parent), Bike(Bike) {}
int CharacteristicNotifier2A53::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
value.append(0x02); // total distance
uint16_t speed = Bike->currentSpeed().value() / 3.6 * 256;
uint32_t distance = Bike->odometer() * 10000.0;

View File

@@ -4,11 +4,11 @@ CharacteristicNotifier2A63::CharacteristicNotifier2A63(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2a63, parent), Bike(Bike) {}
int CharacteristicNotifier2A63::notify(QByteArray &value) {
double normalizeWattage = Bike->wattsMetric().value();
double normalizeWattage = Bike->wattsMetricforUI();
if (normalizeWattage < 0)
normalizeWattage = 0;
if (Bike->deviceType() == bluetoothdevice::BIKE) {
if (Bike->deviceType() == BIKE) {
/*
// set measurement
measurement[2] = power & 0xFF;

View File

@@ -7,9 +7,16 @@ CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
int CharacteristicNotifier2ACD::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
value.append(0x0C); // Inclination available and distance for peloton
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == TREADMILL || dt == ELLIPTICAL) {
QSettings settings;
bool bike_cadence_sensor = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
if (bike_cadence_sensor) {
value.append(0x0C); // Inclination and distance available (old behavior)
} else {
value.append(0x0E); // Inclination, distance and average speed available
}
//value.append((char)0x01); // heart rate available
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
@@ -19,7 +26,33 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
QByteArray speedBytes;
speedBytes.append(b);
speedBytes.append(a);
// average speed in 0.01 km/h (distance from startup / elapsed time)
double elapsed_time_seconds = 0.0;
uint16_t averageSpeed = 0;
QByteArray averageSpeedBytes;
if (!bike_cadence_sensor) {
QTime sessionElapsedTime = Bike->elapsedTime();
elapsed_time_seconds = (double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
if (elapsed_time_seconds > 0) {
double distance_m = Bike->odometerFromStartup() * 1000.0;
double avg_kmh = (distance_m * 3.6) / elapsed_time_seconds;
averageSpeed = (uint16_t)qRound(avg_kmh * 100.0);
}
averageSpeedBytes.append(static_cast<char>(averageSpeed & 0xFF));
averageSpeedBytes.append(static_cast<char>((averageSpeed >> 8) & 0xFF));
} else {
elapsed_time_seconds = 0.0;
QTime sessionElapsedTime = Bike->elapsedTime();
elapsed_time_seconds = (double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
}
// peloton wants the distance from the qz startup to handle stacked classes
// https://github.com/cagnulein/qdomyos-zwift/issues/2018
uint32_t normalizeDistance = (uint32_t)qRound(Bike->odometerFromStartup() * 1000);
@@ -34,7 +67,6 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
uint16_t normalizeIncline = 0;
QSettings settings;
bool real_inclination_to_virtual_treamill_bridge = settings.value(QZSettings::real_inclination_to_virtual_treamill_bridge, QZSettings::default_real_inclination_to_virtual_treamill_bridge).toBool();
double inclination = ((treadmill *)Bike)->currentInclination().value();
if(real_inclination_to_virtual_treamill_bridge) {
@@ -46,7 +78,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclination /= gain;
}
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
normalizeIncline = (uint32_t)qRound(inclination * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
@@ -54,29 +86,25 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclineBytes.append(b);
inclineBytes.append(a);
double ramp = 0;
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
ramp = qRadiansToDegrees(qAtan(inclination / 100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
int16_t normalizeRamp = (int16_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;
QByteArray rampBytes;
rampBytes.append(b);
rampBytes.append(a);
// Get session elapsed time - makes Runna calculations work
QTime sessionElapsedTime = Bike->elapsedTime();
double elapsed_time_seconds =
(double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
QByteArray elapsedBytes;
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
value.append(speedBytes); // Actual value.
if (!bike_cadence_sensor) {
value.append(averageSpeedBytes); // Average speed value.
}
value.append(distanceBytes); // Actual value.
value.append(inclineBytes); // incline

View File

@@ -8,23 +8,23 @@ CharacteristicNotifier2AD2::CharacteristicNotifier2AD2(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2ad2, parent), Bike(Bike) {}
int CharacteristicNotifier2AD2::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
QSettings settings;
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
bool rowerAsABike = !virtual_device_rower && dt == bluetoothdevice::ROWING;
bool rowerAsABike = !virtual_device_rower && dt == ROWING;
bool double_cadence = settings.value(QZSettings::powr_sensor_running_cadence_double, QZSettings::default_powr_sensor_running_cadence_double).toBool();
double cadence_multiplier = 2.0;
if (double_cadence)
cadence_multiplier = 1.0;
double normalizeWattage = Bike->wattsMetric().value();
double normalizeWattage = Bike->wattsMetricforUI();
if (normalizeWattage < 0)
normalizeWattage = 0;
if (dt == bluetoothdevice::BIKE || rowerAsABike) {
if (dt == BIKE || rowerAsABike) {
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
@@ -44,7 +44,7 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
value.append(char(Bike->currentHeart().value())); // Actual value.
value.append((char)0); // Bkool FTMS protocol HRM offset 1280 fix
return CN_OK;
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL || dt == bluetoothdevice::ROWING) {
} else if (dt == TREADMILL || dt == ELLIPTICAL || dt == ROWING) {
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
value.append((char)0x64); // speed, inst. cadence, resistance lvl, instant power
value.append((char)0x02); // heart rate
@@ -53,11 +53,11 @@ int CharacteristicNotifier2AD2::notify(QByteArray &value) {
value.append((char)(normalizeSpeed >> 8) & 0xFF); // speed
uint16_t cadence = 0;
if (dt == bluetoothdevice::ELLIPTICAL)
if (dt == ELLIPTICAL)
cadence = ((elliptical *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::TREADMILL)
else if (dt == TREADMILL)
cadence = ((treadmill *)Bike)->currentCadence().value();
else if (dt == bluetoothdevice::ROWING)
else if (dt == ROWING)
cadence = ((rower *)Bike)->currentCadence().value();
value.append((char)((uint16_t)(cadence * cadence_multiplier) & 0xFF)); // cadence

View File

@@ -10,7 +10,7 @@ CharacteristicWriteProcessor::CharacteristicWriteProcessor(double bikeResistance
void CharacteristicWriteProcessor::changePower(uint16_t power) { Bike->changePower(power); }
void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr, uint8_t cw) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
@@ -64,7 +64,7 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
qDebug() << "changeSlope CRR = " << fCRR << CRR_offset << "CW = " << fCW;
if (dt == bluetoothdevice::BIKE) {
if (dt == BIKE) {
// if the bike doesn't have the inclination by hardware, i'm simulating inclination with the value received
// from Zwift
@@ -82,9 +82,9 @@ void CharacteristicWriteProcessor::changeSlope(int16_t iresistance, uint8_t crr,
Bike->changeResistance((resistance_t)(round(resistance * bikeResistanceGain)) + bikeResistanceOffset + 1 +
CRR_offset + CW_offset); // resistance start from 1
}
} else if (dt == bluetoothdevice::TREADMILL) {
} else if (dt == TREADMILL) {
emit changeInclination(grade, percentage);
} else if (dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == ELLIPTICAL) {
bool inclinationAvailableByHardware = ((elliptical *)Bike)->inclinationAvailableByHardware();
qDebug() << "inclinationAvailableByHardware" << inclinationAvailableByHardware << "erg_mode" << erg_mode;
emit changeInclination(grade, percentage);

View File

@@ -239,11 +239,11 @@ int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArra
changeSlope(slopefloat, 0 /* TODO */, 0 /* TODO */);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->wattsMetricforUI(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
Bike->wattsMetricforUI(),
calculateUnknown1(Bike->wattsMetricforUI()),
0
);
notifier0002->addAnswer(reply);
@@ -284,15 +284,15 @@ int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArra
QByteArray::fromHex("05") + power);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->wattsMetricforUI(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
Bike->wattsMetricforUI(),
calculateUnknown1(Bike->wattsMetricforUI()),
0
);
notifier0002->addAnswer(reply);
changePower(Power.value);
}
else if (receivedData.startsWith(expectedHexArray9)) {

View File

@@ -13,8 +13,8 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
if (data.size()) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::BIKE || dt == bluetoothdevice::ROWING) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == BIKE || dt == ROWING) {
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
@@ -82,7 +82,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
reply.append((quint8)cmd);
reply.append((quint8)FTMS_NOT_SUPPORTED);
}
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
char a, b;
if ((char)data.at(0) == 0x02) {
// Set Target Speed
@@ -91,7 +91,7 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
uint16_t uspeed = a + (((uint16_t)b) << 8);
double requestSpeed = (double)uspeed / 100.0;
if (dt == bluetoothdevice::TREADMILL) {
if (dt == TREADMILL) {
((treadmill *)Bike)->changeSpeed(requestSpeed);
}
qDebug() << QStringLiteral("new requested speed ") + QString::number(requestSpeed);
@@ -103,10 +103,10 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
int16_t sincline = a + (((int16_t)b) << 8);
double requestIncline = (double)sincline / 10.0;
if (dt == bluetoothdevice::TREADMILL)
if (dt == TREADMILL)
((treadmill *)Bike)->changeInclination(requestIncline, requestIncline);
// Resistance as incline on Sole E95s Elliptical #419
else if (dt == bluetoothdevice::ELLIPTICAL) {
else if (dt == ELLIPTICAL) {
if(((elliptical *)Bike)->inclinationAvailableByHardware())
((elliptical *)Bike)->changeInclination(requestIncline, requestIncline);
else

View File

@@ -13,8 +13,8 @@ CharacteristicWriteProcessorE005::CharacteristicWriteProcessorE005(double bikeRe
int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
if (data.size()) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::BIKE) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == BIKE) {
char cmd = data.at(0);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(), data);
if (cmd == wahookickrsnapbike::_setSimMode && data.count() >= 7) {
@@ -35,7 +35,7 @@ int CharacteristicWriteProcessorE005::writeProcess(quint16 uuid, const QByteArra
qDebug() << "erg mode" << watts;
changePower(watts);
}
} else if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
} else if (dt == TREADMILL || dt == ELLIPTICAL) {
}
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)data.at(0));

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
activiotreadmill::activiotreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
antbike::antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -14,7 +14,7 @@ using namespace std::chrono_literals;
apexbike::apexbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -19,7 +19,7 @@ using namespace std::chrono_literals;
bhfitnesselliptical::bhfitnesselliptical(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -3,6 +3,7 @@
#include "qdebugfixup.h"
#include "homeform.h"
#include <QSettings>
#include <cmath>
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
@@ -195,6 +196,16 @@ void bike::setGears(double gears) {
}
}
double bike::maxGears() {
QSettings settings;
return settings.value(QZSettings::gears_max, QZSettings::default_gears_max).toDouble();
}
double bike::minGears() {
QSettings settings;
return settings.value(QZSettings::gears_min, QZSettings::default_gears_min).toDouble();
}
double bike::currentCrankRevolutions() { return CrankRevs; }
uint16_t bike::lastCrankEventTime() { return LastCrankEventTime; }
metric bike::lastRequestedResistance() { return RequestedResistance; }
@@ -211,7 +222,7 @@ resistance_t bike::resistanceFromPowerRequest(uint16_t power) { return power / 1
void bike::cadenceSensor(uint8_t cadence) { Cadence.setValue(cadence); }
void bike::powerSensor(uint16_t power) { m_watt.setValue(power, false); }
bluetoothdevice::BLUETOOTH_TYPE bike::deviceType() { return bluetoothdevice::BIKE; }
BLUETOOTH_TYPE bike::deviceType() { return BIKE; }
void bike::clearStats() {
@@ -381,6 +392,8 @@ uint8_t bike::metrics_override_heartrate() {
bool bike::inclinationAvailableByHardware() { return false; }
bool bike::inclinationAvailableBySoftware() { return false; }
uint16_t bike::wattFromHR(bool useSpeedAndCadence) {
QSettings settings;
double watt = 0;
@@ -466,7 +479,136 @@ double bike::gearsZwiftRatio() {
case 23:
return 5.14;
case 24:
return 5.49;
return 5.49;
}
return 1;
}
// Sim mode support: Physics-based power calculation from slope gradient
double bike::computeSlopeTargetPower(double gradePercent, double speedKmh) {
QSettings settings;
const double riderWeight = settings.value(QZSettings::weight, QZSettings::default_weight).toDouble();
const double bikeWeight = settings.value(QZSettings::bike_weight, QZSettings::default_bike_weight).toDouble();
const double rollingCoeff = settings.value(QZSettings::rolling_resistance, QZSettings::default_rolling_resistance).toDouble();
double totalMass = riderWeight + bikeWeight;
if (!std::isfinite(totalMass) || totalMass < 1.0) {
totalMass = 75.0 + 10.0; // fallback to reasonable defaults
}
double speedMs = speedKmh / 3.6; // convert km/h to m/s
if (!std::isfinite(speedMs) || speedMs < 0.0) {
speedMs = 0.0;
}
// Calculate slope angle components
const double slope = gradePercent / 100.0;
const double denom = std::sqrt(1.0 + slope * slope);
const double sinTheta = (denom > 0.0) ? (slope / denom) : 0.0;
const double cosTheta = (denom > 0.0) ? (1.0 / denom) : 1.0;
const double g = 9.80665; // m/s² - gravitational acceleration
// 1. Gravitational resistance (climbing/descending)
double powerGravity = totalMass * g * speedMs * sinTheta;
// 2. Rolling resistance
double powerRolling = totalMass * g * rollingCoeff * speedMs * cosTheta;
// 3. Aerodynamic resistance
const double airDensity = 1.204; // kg/m³ at 20°C
const double dragCoefficient = 0.4; // Cd - typical cycling position
const double frontalArea = 1.0; // m² - approximate frontal area
double cda = dragCoefficient * frontalArea;
double powerAerodynamic = 0.5 * airDensity * cda * std::pow(std::max(0.0, speedMs), 3);
// Total power required
double totalPower = powerGravity + powerRolling + powerAerodynamic;
if (!std::isfinite(totalPower)) {
totalPower = 0.0;
}
if (totalPower < 0.0) {
totalPower = 0.0;
}
qDebug() << "computeSlopeTargetPower grade%:" << gradePercent
<< "speedKmh:" << speedKmh
<< "powerGravity:" << powerGravity
<< "powerRolling:" << powerRolling
<< "powerAero:" << powerAerodynamic
<< "total:" << totalPower;
return totalPower;
}
// Helper: get current speed for slope calculations with fallback to cadence-based estimation
double bike::getCurrentSpeedForSlope() {
double speedKmh = Speed.value();
if (!std::isfinite(speedKmh) || speedKmh < 0.0) {
speedKmh = 0.0;
}
// If speed is very low, estimate from cadence
if (speedKmh < 5.0) {
double cadence = Cadence.value();
if (std::isfinite(cadence) && cadence > 0.0) {
// Rough approximation: 90 RPM ≈ 27 km/h
speedKmh = std::max(0.5, cadence * 0.3);
}
}
return speedKmh;
}
// Update power target based on current slope and speed
void bike::updateSlopeTargetPower(bool force) {
qDebug() << "updateSlopeTargetPower called - force:" << force
<< "autoRes:" << autoResistance()
<< "slopeEnabled:" << m_slopeControlEnabled
<< "currentGrade:" << m_currentSlopePercent;
if (!autoResistance()) {
qDebug() << "updateSlopeTargetPower skipped: auto resistance disabled";
return;
}
if (!m_slopeControlEnabled && !force) {
qDebug() << "updateSlopeTargetPower skipped: slope control inactive";
return;
}
// Apply gear offset to grade (0.5 scaling factor)
double grade = m_currentSlopePercent + (gears() / 2.0);
// Get current speed (with fallback to cadence-based estimation)
double speedKmh = getCurrentSpeedForSlope();
// Compute required power using physics model
double targetPower = computeSlopeTargetPower(grade, speedKmh);
int powerValue = static_cast<int>(std::round(targetPower));
powerValue = qBound(0, powerValue, 2000);
// Hysteresis: avoid too frequent changes
if (!force) {
if (!m_slopePowerTimer.isValid()) {
m_slopePowerTimer.start();
}
if (m_slopePowerTimer.elapsed() < 500 &&
m_lastSlopeTargetPower >= 0 &&
std::abs(powerValue - m_lastSlopeTargetPower) < 3) {
qDebug() << "updateSlopeTargetPower skipped: within hysteresis"
<< powerValue << "vs" << m_lastSlopeTargetPower;
return;
}
}
// Apply power change
m_lastSlopeTargetPower = powerValue;
m_slopePowerTimer.restart();
m_slopePowerChangeInProgress = true;
qDebug() << "updateSlopeTargetPower -> changePower:" << powerValue;
changePower(powerValue);
m_slopePowerChangeInProgress = false;
}

View File

@@ -4,6 +4,7 @@
#include "devices/bluetoothdevice.h"
#include "virtualdevices/virtualbike.h"
#include <QObject>
#include <QElapsedTimer>
class bike : public bluetoothdevice {
@@ -24,14 +25,14 @@ class bike : public bluetoothdevice {
uint16_t lastCrankEventTime() override;
bool connected() override;
double defaultMaxGears() { return 9999.0; }
virtual double maxGears() { return defaultMaxGears(); }
virtual double minGears() { return -9999.0; }
virtual double maxGears();
virtual double minGears();
virtual uint16_t watts();
virtual resistance_t pelotonToBikeResistance(int pelotonResistance);
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance);
virtual bool ergManagedBySS2K() { return false; }
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
BLUETOOTH_TYPE deviceType() override;
metric pelotonResistance();
void clearStats() override;
void setLap() override;
@@ -51,6 +52,7 @@ class bike : public bluetoothdevice {
*/
metric currentSteeringAngle() { return m_steeringAngle; }
virtual bool inclinationAvailableByHardware();
virtual bool inclinationAvailableBySoftware();
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
@@ -110,6 +112,25 @@ class bike : public bluetoothdevice {
double m_speedLimit = 0;
// Sim mode support: convert inclination to power for devices without native inclination
bool m_slopeControlEnabled = false;
double m_currentSlopePercent = 0.0;
int m_lastSlopeTargetPower = -1;
bool m_slopePowerChangeInProgress = false;
QElapsedTimer m_slopePowerTimer;
// Physics-based power calculation from slope
virtual double computeSlopeTargetPower(double gradePercent, double speedKmh);
// Update power based on current slope and speed (called periodically)
virtual void updateSlopeTargetPower(bool force = false);
// Check if device supports native inclination control
virtual bool supportsNativeInclination() const { return true; }
// Helper: get current speed for slope calculations
double getCurrentSpeedForSlope();
uint16_t wattFromHR(bool useSpeedAndCadence);
};

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
bkoolbike::bkoolbike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
@@ -49,7 +49,12 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
}
writeBuffer = new QByteArray((const char *)data, data_len);
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
if (gattWriteCharCustomId.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCustomService->writeCharacteristic(gattWriteCharCustomId, *writeBuffer);
}
if (!disable_log) {
emit debug(QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
@@ -61,19 +66,28 @@ void bkoolbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStri
void bkoolbike::changePower(int32_t power) {
RequestedPower = power;
/*
if (power < 0)
power = 0;
uint8_t p[] = {0xa4, 0x09, 0x4e, 0x05, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x02, 0x00};
p[10] = (uint8_t)((power * 4) & 0xFF);
p[11] = (uint8_t)((power * 4) >> 8);
for (uint8_t i = 0; i < sizeof(p) - 1; i++) {
p[12] ^= p[i]; // the last byte is a sort of a checksum
}
writeCharacteristic(p, sizeof(p), QStringLiteral("changePower"), false, false);*/
if (power < 0) {
power = 0;
}
qDebug() << QStringLiteral("Changepower not implemented");
forcePower(power);
}
void bkoolbike::forcePower(int32_t power) {
// FE-C "Set Target Power" command (page 0x31)
// Power is sent in 1/4 watt units (0.25W resolution)
// Bytes: [0x31][0x25][0xFF][0xFF][0xFF][0xFF][power_low][power_high]
uint16_t power_quarter_watts = (uint16_t)(power * 4);
uint8_t power_cmd[] = {0x31, 0x25, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00};
power_cmd[6] = (uint8_t)(power_quarter_watts & 0xFF); // Low byte
power_cmd[7] = (uint8_t)((power_quarter_watts >> 8) & 0xFF); // High byte
writeCharacteristic(power_cmd, sizeof(power_cmd),
QStringLiteral("forcePower ") + QString::number(power) + QStringLiteral("W"),
false, false);
}
void bkoolbike::forceInclination(double inclination) {
@@ -115,13 +129,27 @@ void bkoolbike::update() {
uint8_t init1[] = {0x30, 0x25, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
uint8_t init2[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x1e, 0x7f, 0x00};
uint8_t init3[] = {0x33, 0x25, 0xff, 0xff, 0xff, 0x20, 0x4e, 0x00};
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
writeCharacteristic(init1, sizeof(init1), QStringLiteral("init1"), false, false);
writeCharacteristic(init2, sizeof(init2), QStringLiteral("init2"), false, false);
writeCharacteristic(init3, sizeof(init3), QStringLiteral("init3"), false, false);
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
if (bkool_fitness_bike) {
// BKOOLFITNESSBIKE specific init packets
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
uint8_t init6[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
uint8_t init7[] = {0x32, 0x25, 0xff, 0xff, 0xff, 0x25, 0x7f, 0x00};
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
writeCharacteristic(init6, sizeof(init6), QStringLiteral("init6"), false, true);
writeCharacteristic(init7, sizeof(init7), QStringLiteral("init7"), false, false);
} else {
// BKOOLSMARTPRO init packets
uint8_t init4[] = {0x37, 0x4c, 0x1d, 0xff, 0x80, 0x0c, 0x46, 0x21};
uint8_t init5[] = {0x37, 0xee, 0x16, 0xff, 0x80, 0x0c, 0x46, 0x21};
writeCharacteristic(init4, sizeof(init4), QStringLiteral("init4"), false, true);
writeCharacteristic(init5, sizeof(init5), QStringLiteral("init5"), false, true);
}
} else if (bluetoothDevice.isValid() &&
m_control->state() == QLowEnergyController::DiscoveredState //&&
@@ -137,6 +165,13 @@ void bkoolbike::update() {
// updateDisplay(elapsed);
}
// Send poll command for BKOOLFITNESSBIKE
/*
if (bkool_fitness_bike) {
uint8_t poll[] = {0x37, 0xc8, 0x19, 0xff, 0xe0, 0x0a, 0x46, 0x21};
writeCharacteristic(poll, sizeof(poll), QStringLiteral("poll"), false, false);
}*/
if (requestResistance != -1) {
if (requestResistance != currentResistance().value() || lastGearValue != gears()) {
emit debug(QStringLiteral("writing resistance ") + QString::number(requestResistance));
@@ -146,10 +181,15 @@ void bkoolbike::update() {
requestInclination = requestResistance / 10.0;
}
// forceResistance(requestResistance);;
}
lastGearValue = gears();
}
requestResistance = -1;
}
if(lastGearValue != gears() && requestInclination == -100) {
// if only gears changed, we need to update the inclination to match the gears
requestInclination = lastRawRequestedInclinationValue;
}
if (requestInclination != -100) {
emit debug(QStringLiteral("writing inclination ") + QString::number(requestInclination));
forceInclination(requestInclination + gears()); // since this bike doesn't have the concept of resistance,
@@ -157,6 +197,8 @@ void bkoolbike::update() {
requestInclination = -100;
}
lastGearValue = gears();
if (requestPower != -1) {
changePower(requestPower);
requestPower = -1;
@@ -207,60 +249,65 @@ void bkoolbike::characteristicChanged(const QLowEnergyCharacteristic &characteri
if (characteristic.uuid() == QBluetoothUuid((quint16)0x2A5B)) {
lastPacket = newValue;
uint8_t index = 1;
// Only parse and update cadence from internal CSC if no external cadence sensor configured
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
uint8_t index = 1;
if (newValue.at(0) == 0x02 && newValue.length() < 4) {
emit debug(QStringLiteral("Crank revolution data present with wrong bytes ") +
QString::number(newValue.length()));
return;
} else if (newValue.at(0) == 0x01 && newValue.length() < 6) {
emit debug(QStringLiteral("Wheel revolution data present with wrong bytes ") +
QString::number(newValue.length()));
return;
} else if (newValue.at(0) == 0x00) {
emit debug(QStringLiteral("Cadence sensor notification without datas ") +
QString::number(newValue.length()));
return;
}
if (newValue.at(0) == 0x02) {
CrankRevsRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
} else if (newValue.at(0) == 0x03) {
index += 6;
CrankRevsRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
} else {
return;
// CrankRevsRead = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
// ((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)));
}
if (newValue.at(0) == 0x01) {
index += 4;
} else {
index += 2;
}
uint16_t LastCrankEventTimeRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime;
if (deltaT < 0) {
deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime;
}
if (CrankRevsRead != oldCrankRevs && deltaT) {
double cadence = (((double)CrankRevsRead - (double)oldCrankRevs) / (double)deltaT) * 1024.0 * 60.0;
if (cadence >= 0 && cadence < 255) {
Cadence = cadence;
if (newValue.at(0) == 0x02 && newValue.length() < 4) {
emit debug(QStringLiteral("Crank revolution data present with wrong bytes ") +
QString::number(newValue.length()));
return;
} else if (newValue.at(0) == 0x01 && newValue.length() < 6) {
emit debug(QStringLiteral("Wheel revolution data present with wrong bytes ") +
QString::number(newValue.length()));
return;
} else if (newValue.at(0) == 0x00) {
emit debug(QStringLiteral("Cadence sensor notification without datas ") +
QString::number(newValue.length()));
return;
}
lastGoodCadence = now;
} else if (lastGoodCadence.msecsTo(now) > 2000) {
Cadence = 0;
}
oldLastCrankEventTime = LastCrankEventTimeRead;
oldCrankRevs = CrankRevsRead;
if (newValue.at(0) == 0x02) {
CrankRevsRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
} else if (newValue.at(0) == 0x03) {
index += 6;
CrankRevsRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
} else {
return;
// CrankRevsRead = (((uint32_t)((uint8_t)newValue.at(index + 3)) << 24) |
// ((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)));
}
if (newValue.at(0) == 0x01) {
index += 4;
} else {
index += 2;
}
uint16_t LastCrankEventTimeRead =
(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) | (uint16_t)((uint8_t)newValue.at(index)));
int16_t deltaT = LastCrankEventTimeRead - oldLastCrankEventTime;
if (deltaT < 0) {
deltaT = LastCrankEventTimeRead + 65535 - oldLastCrankEventTime;
}
if (CrankRevsRead != oldCrankRevs && deltaT) {
double cadence = (((double)CrankRevsRead - (double)oldCrankRevs) / (double)deltaT) * 1024.0 * 60.0;
if (cadence >= 0 && cadence < 255) {
Cadence = cadence;
}
lastGoodCadence = now;
} else if (lastGoodCadence.msecsTo(now) > 2000) {
Cadence = 0;
}
oldLastCrankEventTime = LastCrankEventTimeRead;
oldCrankRevs = CrankRevsRead;
}
Speed = Cadence.value() *
settings.value(QZSettings::cadence_sensor_speed_ratio, QZSettings::default_cadence_sensor_speed_ratio)
@@ -677,6 +724,12 @@ void bkoolbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
{
bluetoothDevice = device;
// Check if this is BKOOLFITNESSBIKE model
if (device.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) {
bkool_fitness_bike = true;
emit debug(QStringLiteral("BKOOLFITNESSBIKE model detected"));
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &bkoolbike::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &bkoolbike::serviceScanDone);

View File

@@ -45,6 +45,7 @@ class bkoolbike : public bike {
bool wait_for_response = false);
void startDiscover();
void forceInclination(double inclination);
void forcePower(int32_t power);
uint16_t watts() override;
double bikeResistanceToPeloton(double resistance);
@@ -70,6 +71,8 @@ class bkoolbike : public bike {
bool noWriteResistance = false;
bool noHeartService = false;
bool bkool_fitness_bike = false;
uint16_t pollCounter = 0;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;

View File

@@ -201,12 +201,15 @@ void bluetooth::finished() {
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
if ((!heartRateBeltFound && !heartRateBeltAvaiable()) || (!ftmsAccessoryFound && !ftmsAccessoryAvaiable()) ||
(!cscFound && !cscSensorAvaiable()) || (!powerSensorFound && !powerSensorAvaiable()) ||
(!eliteRizerFound && !eliteRizerAvaiable()) || (!eliteSterzoSmartFound && !eliteSterzoSmartAvaiable()) ||
(!fitmetriaFanfitFound && !fitmetriaFanfitAvaiable()) ||
(!zwiftDeviceFound && !zwiftDeviceAvaiable()) ||
(!sramDeviceFound && !sramDeviceAvaiable())) {
(!sramDeviceFound && !sramDeviceAvaiable()) ||
(!thinkriderDeviceFound && !thinkriderDeviceAvaiable())) {
// force heartRateBelt off
forceHeartBeltOffForTimeout = true;
@@ -336,6 +339,16 @@ bool bluetooth::sramDeviceAvaiable() {
return false;
}
bool bluetooth::thinkriderDeviceAvaiable() {
Q_FOREACH (QBluetoothDeviceInfo b, devices) {
if (b.name().toUpper().startsWith("THINK VS") || b.name().toUpper().startsWith("THINKRIDER")) {
return true;
}
}
return false;
}
bool bluetooth::powerSensorAvaiable() {
@@ -437,6 +450,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bool sramDeviceFound = !settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool();
bool zwiftDeviceFound =
!settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool() && !settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool();
bool thinkriderDeviceFound = !settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool();
bool fitmetriaFanfitFound =
!settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
bool toorx_ftms = settings.value(QZSettings::toorx_ftms, QZSettings::default_toorx_ftms).toBool();
@@ -450,7 +464,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool() ||
settings.value(QZSettings::hop_sport_hs_090h_bike, QZSettings::default_hop_sport_hs_090h_bike).toBool() ||
settings.value(QZSettings::toorx_bike_srx_500, QZSettings::default_toorx_bike_srx_500).toBool() ||
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool()) &&
settings.value(QZSettings::hertz_xr_770, QZSettings::default_hertz_xr_770).toBool() ||
settings.value(QZSettings::taurua_ic90, QZSettings::default_taurua_ic90).toBool()) &&
!toorx_ftms;
bool snode_bike = settings.value(QZSettings::snode_bike, QZSettings::default_snode_bike).toBool();
bool fitplus_bike = settings.value(QZSettings::fitplus_bike, QZSettings::default_fitplus_bike).toBool() ||
@@ -467,6 +482,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
bool hammerRacerS = settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool();
bool flywheel_life_fitness_ic8 =
settings.value(QZSettings::flywheel_life_fitness_ic8, QZSettings::default_flywheel_life_fitness_ic8).toBool();
bool life_fitness_ic5 =
settings.value(QZSettings::life_fitness_ic5, QZSettings::default_life_fitness_ic5).toBool();
bool technogym_bike =
settings.value(QZSettings::technogym_bike, QZSettings::default_technogym_bike).toBool();
QString powerSensorName =
settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name).toString();
QString eliteRizerName =
@@ -500,6 +519,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
QString computrainerSerialPort =
settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
QString kettlerUsbSerialPort =
settings.value(QZSettings::kettler_usb_serialport, QZSettings::default_kettler_usb_serialport).toString();
QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString();
QString csafeellipticalSerialPort =
settings.value(QZSettings::csafe_elliptical_port, QZSettings::default_csafe_elliptical_port).toString();
@@ -542,6 +563,10 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
sramDeviceFound = sramDeviceAvaiable();
}
if(!thinkriderDeviceFound) {
thinkriderDeviceFound = thinkriderDeviceAvaiable();
}
if (!ftmsAccessoryFound) {
ftmsAccessoryFound = ftmsAccessoryAvaiable();
@@ -576,7 +601,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
!b.compare(settings.value(QZSettings::filter_device, QZSettings::default_filter_device).toString()) &&
(b.toUpper().startsWith("IC BIKE") || b.toUpper().startsWith("C7-"))) {
this->stopDiscovery();
// Don't stop discovery here to allow time for accessories (heart rate, power, cadence sensors) to be found
// Discovery will stop naturally after 10 seconds or when all configured accessories are discovered
schwinnIC4Bike = new schwinnic4bike(noWriteResistance, noHeartService);
// stateFileRead();
QBluetoothDeviceInfo bt;
@@ -673,7 +699,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
#endif
bool searchDevices = (heartRateBeltFound && ftmsAccessoryFound && cscFound && powerSensorFound && eliteRizerFound &&
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound) ||
eliteSterzoSmartFound && fitmetriaFanfitFound && zwiftDeviceFound && sramDeviceFound && thinkriderDeviceFound) ||
forceHeartBeltOffForTimeout;
if (searchDevices) {
@@ -684,7 +710,20 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
filter = (b.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
}
if (b.name().startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
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;
for (int idx = 2; idx < deviceName.length(); ++idx) {
if (!deviceName.at(idx).isDigit()) {
isTrxAppGateUsbBikeTC = false;
break;
}
}
}
if (deviceName.startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
if (m3ibike::isCorrectUnit(b)) {
this->setLastBluetoothDevice(b);
@@ -806,6 +845,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(computrainerBike);
} else if (!kettlerUsbSerialPort.isEmpty() && !kettlerUsbBike) {
this->stopDiscovery();
kettlerUsbBike =
new kettlerusbbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
emit deviceConnected(b);
connect(kettlerUsbBike, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
connect(kettlerUsbBike, &kettlerusbbike::debug, this, &bluetooth::debug);
kettlerUsbBike->deviceDiscovered(b);
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(kettlerUsbBike);
} else if (!csaferowerSerialPort.isEmpty() && !csafeRower) {
this->stopDiscovery();
csafeRower = new csaferower(noWriteResistance, noHeartService, false);
@@ -934,7 +986,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();
@@ -966,6 +1019,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((((power_as_treadmill && b.name().startsWith(powerSensorName))) ||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (deviceHasService(b, QBluetoothUuid((quint16)0x1814)))) ||
(b.name().toUpper().startsWith(QStringLiteral("S10")) && deviceHasService(b, QBluetoothUuid((quint16)0x1814))) ||
(b.name().toUpper().startsWith(QStringLiteral("NOHRD SPRINTBOK"))) ||
b.name().toUpper().startsWith(QStringLiteral("ZWIFT RUNPOD"))) &&
!powerTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -983,7 +1037,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(powerTreadmill);
} else if (b.name().toUpper().startsWith(QStringLiteral("DOMYOS-ROW")) &&
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && filter) {
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosRower && ftms_rower.contains(QZSettings::default_ftms_rower) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
domyosRower = new domyosrower(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
@@ -1000,7 +1054,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(domyosRower);
} else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool())) &&
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
@@ -1015,7 +1069,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(domyosBike);
} else if (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();
@@ -1069,6 +1124,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(domyosElliptical);
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH411/510E")) ||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
b.name().toUpper().startsWith(QStringLiteral("FEIER-EM-")) ||
b.name().toUpper().startsWith(QStringLiteral("MX-AS ")) ||
@@ -1078,7 +1134,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("CARDIOPOWER EEGO")) ||
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical) ||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && filter) {
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && !horizonTreadmill && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
ypooElliptical =
@@ -1229,6 +1285,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(soleElliptical);
} else if (b.name().startsWith(QStringLiteral("Domyos")) &&
!b.name().startsWith(QStringLiteral("DomyosBr")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKE-")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKING-")) && !domyos && !domyosElliptical && b.name().compare(ftms_treadmill, Qt::CaseInsensitive) &&
!domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill &&
(!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) &&
@@ -1344,7 +1401,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
if (this->discoveryAgent && !this->discoveryAgent->isActive())
emit searchingStop();
this->signalBluetoothDeviceConnected(shuaA5Treadmill);
} else if ((b.name().toUpper().startsWith(QStringLiteral("TRUE")) ||
} else if (((b.name().toUpper().startsWith(QStringLiteral("TRUE")) &&
!(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) && b.name().length() == 19)) ||
b.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL ")) ||
(b.name().toUpper().startsWith(QStringLiteral("WDWAY")) && b.name().length() == 8) || // WdWay179
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && !gem_module_inclination && !deviceHasService(b, QBluetoothUuid((quint16)0x1814)) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
@@ -1445,6 +1503,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(lifefitnessTreadmill);
} else if ((b.name().toUpper().startsWith(QStringLiteral("HORIZON")) ||
b.name().toUpper().startsWith(QStringLiteral("HZ_T101-")) ||
b.name().toUpper().startsWith(QStringLiteral("HZ_7.0AT-")) ||
b.name().toUpper().startsWith(QStringLiteral("AFG SPORT")) ||
b.name().toUpper().startsWith(QStringLiteral("WLT2541")) ||
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && (gem_module_inclination || deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) ||
@@ -1477,24 +1537,31 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("XTERRA TR")) ||
b.name().toUpper().startsWith(QStringLiteral("T118_")) ||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
b.name().toUpper().startsWith(QStringLiteral("YS_T")) ||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
b.name().toUpper().startsWith(QStringLiteral("BFX_T9_")) ||
b.name().toUpper().startsWith(QStringLiteral("BFX_T")) ||
(b.name().toUpper().startsWith("3G PRO ")) ||
(b.name().toUpper().startsWith("3G ELITE ")) ||
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415
(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) &&
b.name().length() == 19) || // TRUE TREADMILL followed by 4 digits (e.g. TRUE TREADMILL 0000)
(b.name().toUpper().startsWith(QStringLiteral("FIT-")) && !b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) || // FIT-1596 and sports tech f37s treadmill #2412
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
(b.name().toUpper().startsWith("SCHWINN 810")) ||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
(b.name().toUpper().startsWith("SF-T")) || // Sunny Fitness Treadmill
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) ||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
@@ -1503,6 +1570,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TM4800-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
@@ -1510,12 +1578,18 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("CITYSPORTS-LINKER")) ||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15 && ftms_bike.contains(QZSettings::default_ftms_bike)) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("S77")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS
(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("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);
@@ -1649,7 +1723,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((b.name().toUpper().startsWith(QStringLiteral(">CABLE")) ||
(b.name().toUpper().startsWith(QStringLiteral("MD")) && b.name().length() == 7) ||
// BIKE 1, BIKE 2, BIKE 3...
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && flywheel_life_fitness_ic8 == false &&
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && (flywheel_life_fitness_ic8 || life_fitness_ic5) == false && !technogym_bike &&
b.name().length() == 6)) &&
!npeCableBike && filter) {
this->setLastBluetoothDevice(b);
@@ -1675,8 +1749,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("YS_G1_")) || // Yesoul S3
(b.name().toUpper().startsWith("YS_G1MPLUS")) || // Yesoul G1M Plus
(b.name().toUpper().startsWith("YS_G1MMAX")) || // Yesoul G1M Max
(b.name().toUpper().startsWith("YS_A")) || // Yesoul A6 and A1
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
(b.name().toUpper().startsWith("3G CARDIO ")) ||
(b.name().toUpper().startsWith("ZWIFT HUB")) ||
((b.name().toUpper().startsWith("MAGNUS ")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
@@ -1709,6 +1783,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("KETTLERBLE")) ||
(b.name().toUpper().startsWith("JAS_C3")) ||
(b.name().toUpper().startsWith("SCH_190U")) ||
(b.name().toUpper().startsWith("SCH_290R")) ||
(b.name().toUpper().startsWith("RAVE WHITE")) ||
(b.name().toUpper().startsWith("DOMYOS-BIKING-")) ||
(b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool()) ||
@@ -1726,6 +1801,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("JUSTO")) ||
(b.name().toUpper().startsWith("MYCYCLE ")) ||
(b.name().toUpper().startsWith("T2 ")) ||
(b.name().toUpper().startsWith("S18")) ||
(b.name().toUpper().startsWith("RC-MAX-")) ||
(b.name().toUpper().startsWith("TPS-SPBIKE-2.0")) ||
(b.name().toUpper().startsWith("NEO BIKE SMART")) ||
@@ -1735,6 +1811,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("JFBK5.0")) ||
(b.name().toUpper().startsWith("NEO 3M ")) ||
(b.name().toUpper().startsWith("JFBK7.0")) ||
(b.name().toUpper().startsWith("SPEED RACE S")) ||
(b.name().toUpper().startsWith("SPEEDRACEX")) ||
(b.name().toUpper().startsWith("POOBOO")) ||
(b.name().toUpper().startsWith("ZYCLE ZPRO")) ||
@@ -1753,21 +1830,32 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("MRK-S26S-")) ||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
(b.name().toUpper().startsWith("ROBX")) ||
(b.name().toUpper().startsWith("ORLAUF_ARES")) ||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
(b.name().toUpper().startsWith("XCX-")) ||
(b.name().toUpper().startsWith("SMARTBIKE-")) ||
(b.name().toUpper().startsWith("D500V2")) ||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
(b.name().toUpper().startsWith("DMASUN-") && b.name().toUpper().endsWith("-BIKE")) ||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
(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-")) ||
(b.name().toUpper().startsWith(QStringLiteral("HT")) && (b.name().length() == 10)) ||
(b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) ||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
(b.name().toUpper().startsWith("INCONDI")) || // inCondi S150i
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
ftms_rower.contains(QZSettings::default_ftms_rower) &&
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
this->setLastBluetoothDevice(b);
@@ -1798,7 +1886,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(wahooKickrSnapBike, &wahookickrsnapbike::debug, this, &bluetooth::debug);
wahooKickrSnapBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(wahooKickrSnapBike);
} else if (((b.name().toUpper().startsWith("BIKE ") && b.name().midRef(5).toInt() > 0) ||
} else if (((b.name().toUpper().startsWith("BIKE ") && (flywheel_life_fitness_ic8 || life_fitness_ic5) == false && technogym_bike && b.name().midRef(5).toInt() > 0) ||
b.name().toUpper().startsWith("MYCYCLING")) &&
!technogymBike && filter) {
this->setLastBluetoothDevice(b);
@@ -1840,9 +1928,17 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
//connect(kineticInroadBike, &kineticinroadbike::debug, this, &bluetooth::debug);
kineticInroadBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(kineticInroadBike);
} else if (b.name().toUpper().startsWith("RACER S") && !kettlerRacerSBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
kettlerRacerSBike = new kettlerracersbike(noWriteResistance, noHeartService);
emit deviceConnected(b);
connect(kettlerRacerSBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
connect(kettlerRacerSBike, &kettlerracersbike::debug, this, &bluetooth::debug);
kettlerRacerSBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(kettlerRacerSBike);
} else if ((b.name().toUpper().startsWith(QStringLiteral("STAGES ")) ||
(b.name().toUpper().startsWith("TACX SATORI")) ||
(b.name().toUpper().startsWith("RACER S")) ||
((b.name().toUpper().startsWith("KU")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith("ELITETRAINER")) ||
(b.name().toUpper().startsWith("TOUR 600")) ||
@@ -1851,6 +1947,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith(QStringLiteral("RM")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DR")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) ||
(b.name().toUpper().startsWith(QStringLiteral("THINK A")) && b.name().length() == 18) ||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
!stagesBike && !ftmsBike && filter) {
@@ -1866,7 +1963,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// connect(stagesBike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
stagesBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(stagesBike);
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !smartrowRower && filter) {
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) && ftms_rower.contains(QZSettings::default_ftms_rower) && !smartrowRower && filter) { // Issue #4033
this->setLastBluetoothDevice(b);
this->stopDiscovery();
smartrowRower =
@@ -1904,8 +2001,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("S4 COMMS")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-WLT")) || // KS-WLT-W1
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)
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
@@ -1941,6 +2042,19 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(echelonStairclimber, &echelonstairclimber::inclinationChanged, this, &bluetooth::inclinationChanged);
echelonStairclimber->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(echelonStairclimber);
} else if (b.name().toUpper().startsWith(QLatin1String("SF-S")) &&
!sunnyfitStepper && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
sunnyfitStepper = new sunnyfitstepper(this->pollDeviceTime, noConsole, noHeartService);
emit deviceConnected(b);
connect(sunnyfitStepper, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
connect(sunnyfitStepper, &sunnyfitstepper::debug, this, &bluetooth::debug);
connect(sunnyfitStepper, &sunnyfitstepper::speedChanged, this, &bluetooth::speedChanged);
connect(sunnyfitStepper, &sunnyfitstepper::inclinationChanged, this, &bluetooth::inclinationChanged);
sunnyfitStepper->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(sunnyfitStepper);
} else if ((b.name().toUpper().startsWith(QLatin1String("ECH-STRIDE")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-UK-")) ||
b.name().toUpper().startsWith(QLatin1String("ECH-FR-")) ||
@@ -2005,7 +2119,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(ziproTreadmill, &ziprotreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
ziproTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(ziproTreadmill);
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN-TM"))) && !lifespanTreadmill && filter) {
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN"))) && !lifespanTreadmill && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
lifespanTreadmill = new lifespantreadmill(this->pollDeviceTime, noConsole, noHeartService);
@@ -2019,9 +2133,9 @@ 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().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();
@@ -2063,7 +2177,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
apexBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(apexBike);
} else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) {
} else if ((b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) ||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFBIKE")) ||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) && !bkoolBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
bkoolBike = new bkoolbike(noWriteResistance, noHeartService);
@@ -2119,7 +2235,9 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// SLOT(inclinationChanged(double)));
schwinnIC4Bike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(schwinnIC4Bike);
} else if (b.name().toUpper().startsWith(QStringLiteral("EW-BK")) && !sportsTechBike && filter) {
} else if ((b.name().toUpper().startsWith(QStringLiteral("EW-BK")) ||
(b.name().toUpper().startsWith(QStringLiteral("AF")) && b.name().length() == 7)) // AF7019E
&& !sportsTechBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
sportsTechBike = new sportstechbike(noWriteResistance, noHeartService, bikeResistanceOffset,
@@ -2169,8 +2287,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// SLOT(inclinationChanged(double)));
sportsPlusBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(sportsPlusBike);
} else if ((b.name().toUpper().contains(QStringLiteral("CARE")) &&
b.name().length() >= 13) // CARE968300122
} else if (((b.name().toUpper().contains(QStringLiteral("CARE")) && b.name().length() >= 13) || // CARE968300122
(b.name().toUpper().startsWith(QStringLiteral("VMAX"))))
&& !sportsPlusRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -2345,7 +2463,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(nautilusTreadmill);
} else if ((b.name().startsWith(QStringLiteral("Flywheel")) ||
// BIKE 1, BIKE 2, BIKE 3...
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && flywheel_life_fitness_ic8 == true &&
(b.name().toUpper().startsWith(QStringLiteral("BIKE")) && (flywheel_life_fitness_ic8 || life_fitness_ic5) == true &&
b.name().length() == 6)) &&
!flywheelBike && filter) {
this->setLastBluetoothDevice(b);
@@ -2391,7 +2509,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
toorx->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(toorx);
} else if (((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT")) && !b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT TREAD"))) ||
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike &&
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike && !toorx &&
!iconcept_elliptical && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -2443,15 +2561,15 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug);
activioTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(activioTreadmill);
} else if (((b.name().startsWith(QStringLiteral("TOORX"))) ||
(b.name().startsWith(QStringLiteral("V-RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("K80_"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) ||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
} else if (((deviceName.startsWith(QStringLiteral("TOORX"))) ||
(deviceName.startsWith(QStringLiteral("V-RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("K80_"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("ICONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-RUNNING"))) ||
(upperDeviceName.startsWith(QStringLiteral("DKN RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("ADIDAS "))) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK")))) &&
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) &&
ftms_bike.contains(QZSettings::default_ftms_bike) &&
filter) {
@@ -2465,24 +2583,26 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug);
trxappgateusb->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(trxappgateusb);
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
b.name().toUpper().startsWith(QStringLiteral("FIT HI WAY")) ||
b.name().toUpper().startsWith(QStringLiteral("BIKZU_")) ||
b.name().toUpper().startsWith(QStringLiteral("PASYOU-")) ||
b.name().toUpper().startsWith(QStringLiteral("VIRTUFIT")) ||
b.name().toUpper().startsWith(QStringLiteral("IBIKING+")) ||
((b.name().startsWith(QStringLiteral("TOORX")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("VIFHTR2.1")) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK"))) ||
b.name().toUpper().contains(QStringLiteral("CR011R")) ||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
} else if (isTrxAppGateUsbBikeTC ||
(upperDeviceName.startsWith(QStringLiteral("TUN ")) ||
upperDeviceName.startsWith(QStringLiteral("FITHIWAY")) ||
upperDeviceName.startsWith(QStringLiteral("FIT HI WAY")) ||
upperDeviceName.startsWith(QStringLiteral("BIKZU_")) ||
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("ICONSOLE+")) ||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
upperDeviceName.contains(QStringLiteral("CR011R")) ||
(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)) {
!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 =
@@ -2586,7 +2706,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 && filter) {
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && !trxappgateusbRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
fitPlusBike =
@@ -2733,11 +2853,11 @@ void bluetooth::connectedAndDiscovered() {
settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
// only at the first very connection, setting the user default resistance
if (device() && firstConnected && device()->deviceType() == bluetoothdevice::BIKE &&
if (device() && firstConnected && device()->deviceType() == BIKE &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() != 1) {
qobject_cast<bike *>(device())->changeResistance(
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt());
} else if (device() && firstConnected && device()->deviceType() == bluetoothdevice::ELLIPTICAL &&
} else if (device() && firstConnected && device()->deviceType() == ELLIPTICAL &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() !=
1) {
qobject_cast<elliptical *>(device())->changeResistance(
@@ -2905,7 +3025,7 @@ void bluetooth::connectedAndDiscovered() {
#else
settings.setValue(QZSettings::power_sensor_address, b.deviceUuid().toString());
#endif
if (device() && device()->deviceType() == bluetoothdevice::BIKE) {
if (device() && device()->deviceType() == BIKE) {
powerSensor = new stagesbike(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2914,7 +3034,7 @@ void bluetooth::connectedAndDiscovered() {
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
&bluetoothdevice::cadenceSensor);
powerSensor->deviceDiscovered(b);
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
} else if (device() && device()->deviceType() == TREADMILL) {
powerSensorRun = new strydrunpowersensor(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2971,7 +3091,7 @@ void bluetooth::connectedAndDiscovered() {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().startsWith(eliteSterzoSmartName))) && !eliteSterzoSmart &&
!eliteSterzoSmartName.startsWith(QStringLiteral("Disabled")) && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
settings.setValue(QZSettings::elite_sterzo_smart_lastdevice_name, b.name());
#ifndef Q_OS_IOS
@@ -2993,7 +3113,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SRAM "))) && !sramAXSController && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
sramAXSController = new sramaxscontroller();
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -3012,7 +3132,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("ZWIFT CLICK"))) && !zwiftClickRemote && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 9. is it? " << int(b.manufacturerData(2378).at(0));
@@ -3034,10 +3154,28 @@ void bluetooth::connectedAndDiscovered() {
}
}
if(settings.value(QZSettings::thinkrider_controller, QZSettings::default_thinkrider_controller).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("THINK VS")) || (b.name().toUpper().startsWith("THINKRIDER"))) && !thinkriderController && this->device() &&
this->device()->deviceType() == BIKE) {
thinkriderController = new thinkridercontroller(this->device());
connect(thinkriderController, &thinkridercontroller::debug, this, &bluetooth::debug);
connect(thinkriderController, &thinkridercontroller::plus, (bike*)this->device(), &bike::gearUp);
connect(thinkriderController, &thinkridercontroller::minus, (bike*)this->device(), &bike::gearDown);
thinkriderController->deviceDiscovered(b);
if(homeform::singleton())
homeform::singleton()->setToastRequested("Thinkrider Controller Connected!");
break;
}
}
}
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
eliteSquareController = new elitesquarecontroller(this->device());
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -3057,7 +3195,7 @@ void bluetooth::connectedAndDiscovered() {
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() &&
this->device()->deviceType() == bluetoothdevice::BIKE) {
this->device()->deviceType() == BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0));
@@ -3100,8 +3238,8 @@ void bluetooth::connectedAndDiscovered() {
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
device()->deviceType() == bluetoothdevice::TREADMILL ||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
device()->deviceType() == TREADMILL ||
device()->deviceType() == ELLIPTICAL,
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool(),
settings.value(QZSettings::technogym_group_cycle, QZSettings::default_technogym_group_cycle).toBool(),
settings.value(QZSettings::ant_bike_device_number, QZSettings::default_ant_bike_device_number).toInt(),
@@ -3534,6 +3672,11 @@ void bluetooth::restart() {
delete echelonStairclimber;
echelonStairclimber = nullptr;
}
if (sunnyfitStepper) {
delete sunnyfitStepper;
sunnyfitStepper = nullptr;
}
if (octaneTreadmill) {
delete octaneTreadmill;
@@ -3694,6 +3837,11 @@ void bluetooth::restart() {
delete computrainerBike;
computrainerBike = nullptr;
}
if (kettlerUsbBike) {
delete kettlerUsbBike;
kettlerUsbBike = nullptr;
}
if (csafeRower) {
delete csafeRower;
@@ -3958,6 +4106,8 @@ bluetoothdevice *bluetooth::device() {
return echelonStride;
} else if (echelonStairclimber) {
return echelonStairclimber;
} else if (sunnyfitStepper) {
return sunnyfitStepper;
} else if (octaneTreadmill) {
return octaneTreadmill;
} else if (ziproTreadmill) {
@@ -4036,6 +4186,8 @@ bluetoothdevice *bluetooth::device() {
return horizonGr7Bike;
} else if (kineticInroadBike) {
return kineticInroadBike;
} else if (kettlerRacerSBike) {
return kettlerRacerSBike;
} else if (renphoBike) {
return renphoBike;
} else if (pafersBike) {
@@ -4049,6 +4201,8 @@ bluetoothdevice *bluetooth::device() {
#ifndef Q_OS_IOS
} else if (computrainerBike) {
return computrainerBike;
} else if (kettlerUsbBike) {
return kettlerUsbBike;
} else if (csafeRower) {
return csafeRower;
} else if (csafeElliptical) {
@@ -4114,7 +4268,7 @@ void bluetooth::stateFileUpdate() {
if (!device()) {
return;
}
if (device()->deviceType() != bluetoothdevice::TREADMILL) {
if (device()->deviceType() != TREADMILL) {
return;
}

View File

@@ -34,6 +34,7 @@
#include "devices/coresensor/coresensor.h"
#ifndef Q_OS_IOS
#include "devices/computrainerbike/computrainerbike.h"
#include "devices/kettlerusbbike/kettlerusbbike.h"
#include "devices/csaferower/csaferower.h"
#include "devices/csafeelliptical/csafeelliptical.h"
#endif
@@ -111,6 +112,7 @@
#include "signalhandler.h"
#include "devices/skandikawiribike/skandikawiribike.h"
#include "devices/smartrowrower/smartrowrower.h"
#include "devices/sunnyfitstepper/sunnyfitstepper.h"
#include "devices/smartspin2k/smartspin2k.h"
#include "devices/snodebike/snodebike.h"
#include "devices/strydrunpowersensor/strydrunpowersensor.h"
@@ -127,6 +129,7 @@
#include "devices/sportstechelliptical/sportstechelliptical.h"
#include "devices/sramAXSController/sramAXSController.h"
#include "devices/stagesbike/stagesbike.h"
#include "devices/kettlerracersbike/kettlerracersbike.h"
#include "devices/renphobike/renphobike.h"
#include "devices/tacxneo2/tacxneo2.h"
@@ -153,6 +156,7 @@
#include "zwift_play/zwiftPlayDevice.h"
#include "zwift_play/zwiftclickremote.h"
#include "devices/thinkridercontroller/thinkridercontroller.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
@@ -192,6 +196,7 @@ class bluetooth : public QObject, public SignalHandler {
focustreadmill *focusTreadmill = nullptr;
#ifndef Q_OS_IOS
computrainerbike *computrainerBike = nullptr;
kettlerusbbike *kettlerUsbBike = nullptr;
csaferower *csafeRower = nullptr;
csafeelliptical *csafeElliptical = nullptr;
#endif
@@ -258,6 +263,7 @@ class bluetooth : public QObject, public SignalHandler {
mcfbike *mcfBike = nullptr;
npecablebike *npeCableBike = nullptr;
stagesbike *stagesBike = nullptr;
kettlerracersbike *kettlerRacerSBike = nullptr;
solebike *soleBike = nullptr;
soleelliptical *soleElliptical = nullptr;
solef80treadmill *soleF80 = nullptr;
@@ -267,6 +273,7 @@ class bluetooth : public QObject, public SignalHandler {
echelonrower *echelonRower = nullptr;
ftmsrower *ftmsRower = nullptr;
smartrowrower *smartrowRower = nullptr;
sunnyfitstepper *sunnyfitStepper = nullptr;
echelonstride *echelonStride = nullptr;
echelonstairclimber *echelonStairclimber = nullptr;
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;
@@ -304,6 +311,7 @@ class bluetooth : public QObject, public SignalHandler {
QList<eliteariafan *> eliteAriaFan;
QList<zwiftclickremote* > zwiftPlayDevice;
zwiftclickremote* zwiftClickRemote = nullptr;
thinkridercontroller* thinkriderController = nullptr;
sramaxscontroller* sramAXSController = nullptr;
elitesquarecontroller* eliteSquareController = nullptr;
QString filterDevice = QLatin1String("");
@@ -341,6 +349,7 @@ class bluetooth : public QObject, public SignalHandler {
bool fitmetriaFanfitAvaiable();
bool zwiftDeviceAvaiable();
bool sramDeviceAvaiable();
bool thinkriderDeviceAvaiable();
bool fitmetria_fanfit_isconnected(QString name);
#ifdef Q_OS_WIN

View File

@@ -20,7 +20,7 @@ bluetoothdevice::~bluetoothdevice() {
}
}
bluetoothdevice::BLUETOOTH_TYPE bluetoothdevice::deviceType() { return bluetoothdevice::UNKNOWN; }
BLUETOOTH_TYPE bluetoothdevice::deviceType() { return UNKNOWN; }
void bluetoothdevice::start() { requestStart = 1; lastStart = QDateTime::currentMSecsSinceEpoch(); }
void bluetoothdevice::stop(bool pause) {
requestStop = 1;
@@ -177,7 +177,18 @@ bool bluetoothdevice::changeFanSpeed(uint8_t speed) {
}
bool bluetoothdevice::connected() { return false; }
metric bluetoothdevice::elevationGain() { return elevationAcc; }
void bluetoothdevice::heartRate(uint8_t heart) { Heart.setValue(heart); }
metric bluetoothdevice::negativeElevationGain() { return negativeElevationAcc; }
void bluetoothdevice::heartRate(uint8_t heart) {
Heart.setValue(heart);
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
// Write heart rate from Bluetooth to Apple Health during workout
lockscreen h;
if(heart > 0)
h.setHeartRate(heart);
#endif
#endif
}
void bluetoothdevice::coreBodyTemperature(double coreBodyTemperature) { CoreBodyTemperature.setValue(coreBodyTemperature); }
void bluetoothdevice::skinTemperature(double skinTemperature) { SkinTemperature.setValue(skinTemperature); }
void bluetoothdevice::heatStrainIndex(double heatStrainIndex) { HeatStrainIndex.setValue(heatStrainIndex); }
@@ -237,7 +248,7 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
!power_as_bike && !power_as_treadmill)
watt_calc = false;
if(deviceType() == bluetoothdevice::BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
if(deviceType() == BIKE && !from_accessory) // append only if it's coming from the bike, not from the power sensor
_ergTable.collectData(Cadence.value(), m_watt.value(), Resistance.value());
if (!_firstUpdate && !paused) {
@@ -276,9 +287,14 @@ void bluetoothdevice::update_metrics(bool watt_calc, const double watts, const b
METS = calculateMETS();
if (currentInclination().value() > 0)
elevationAcc += (currentSpeed().value() / 3600.0) * 1000.0 * (currentInclination().value() / 100.0) * deltaTime;
else if (currentInclination().value() < 0)
negativeElevationAcc += (currentSpeed().value() / 3600.0) * 1000.0 * fabs(currentInclination().value() / 100.0) * deltaTime;
_lastTimeUpdate = current;
_firstUpdate = false;
// Update iOS Live Activity with throttling
update_ios_live_activity();
}
void bluetoothdevice::update_hr_from_external() {
@@ -326,15 +342,29 @@ void bluetoothdevice::update_hr_from_external() {
}
#endif
}
// Note: workoutTrackingUpdate is now called from update_ios_live_activity() with throttling
}
void bluetoothdevice::update_ios_live_activity() {
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
lockscreen h;
double kcal = calories().value();
if(kcal < 0)
kcal = 0;
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value());
static QDateTime lastUpdate;
QDateTime current = QDateTime::currentDateTime();
// Throttle updates: only update if at least 1 second has passed since last update
if (!lastUpdate.isValid() || lastUpdate.msecsTo(current) >= 1000) {
QSettings settings;
lockscreen h;
double kcal = calories().value();
if(kcal < 0)
kcal = 0;
bool useMiles = settings.value(QZSettings::miles_unit, QZSettings::default_miles_unit).toBool();
h.workoutTrackingUpdate(Speed.value(), Cadence.value(), (uint16_t)m_watt.value(), kcal, StepCount.value(), deviceType(), odometer() * 1000.0, totalCalories().value(), useMiles, (uint8_t)Heart.value());
lastUpdate = current;
}
#endif
#endif
#endif
}
void bluetoothdevice::clearStats() {
@@ -350,6 +380,7 @@ void bluetoothdevice::clearStats() {
Heart.clear(false);
m_jouls.clear(true);
elevationAcc = 0;
negativeElevationAcc = 0;
m_watt.clear(false);
m_rawWatt.clear(false);
WeightLoss.clear(false);

View File

@@ -1,6 +1,7 @@
#ifndef BLUETOOTHDEVICE_H
#define BLUETOOTHDEVICE_H
#include "bluetoothdevicetype.h"
#include "definitions.h"
#include "metric.h"
#include "qzsettings.h"
@@ -234,9 +235,12 @@ class bluetoothdevice : public QObject {
*/
double wattsMetricforUI() {
QSettings settings;
bool power3s = settings.value(QZSettings::power_avg_3s, QZSettings::default_power_avg_3s).toBool();
bool power5s = settings.value(QZSettings::power_avg_5s, QZSettings::default_power_avg_5s).toBool();
if (power5s)
return wattsMetric().average5s();
if (power3s)
return wattsMetric().average3sHarmonic();
else if (power5s)
return wattsMetric().average5sHarmonic();
else
return wattsMetric().value();
}
@@ -252,6 +256,11 @@ class bluetoothdevice : public QObject {
*/
virtual metric elevationGain();
/**
* @brief negativeElevationGain Gets a metric object to get and set the negative elevation gain (descents). Units: ?
*/
virtual metric negativeElevationGain();
/**
* @brief clearStats Clear the statistics.
*/
@@ -451,7 +460,6 @@ class bluetoothdevice : public QObject {
*/
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
/**
@@ -628,6 +636,11 @@ class bluetoothdevice : public QObject {
*/
metric elevationAcc;
/**
* @brief negativeElevationAcc The negative elevation gain (descents). Units: meters
*/
metric negativeElevationAcc;
/**
* @brief m_watt Metric to get and set the power read from the trainer or from the power sensor Unit: watts
*/
@@ -787,6 +800,11 @@ class bluetoothdevice : public QObject {
*/
void update_hr_from_external();
/**
* @brief update_ios_live_activity Updates iOS Live Activity with throttling (max 1 update per second)
*/
void update_ios_live_activity();
/**
* @brief calculateMETS Calculate the METS (Metabolic Equivalent of Tasks)
* Units: METs (1 MET is approximately 3.5mL of Oxygen consumed per kg of body weight per minute)

View File

@@ -23,7 +23,7 @@ bowflext216treadmill::bowflext216treadmill(uint32_t pollDeviceTime, bool noConso
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -23,7 +23,7 @@ bowflextreadmill::bowflextreadmill(uint32_t pollDeviceTime, bool noConsole, bool
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
//#include <QtBluetooth/private/qlowenergyserviceprivate_p.h>
chronobike::chronobike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
t_timeout = new QTimer(this);

View File

@@ -902,6 +902,7 @@ int Computrainer::rawWrite(uint8_t *bytes, int size) // unix!!
b[i] = bytes[i];
env->SetByteArrayRegion(d, 0, size, b);
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/Usbserial", "write", "([B)V", d);
env->DeleteLocalRef(d);
#elif defined(WIN32)
DWORD cBytes;
rc = WriteFile(devicePort, bytes, size, &cBytes, NULL);
@@ -947,12 +948,38 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
}
QAndroidJniEnvironment env;
while (fullLen < size) {
int timeout = 0;
int maxRetries = 100; // Maximum number of retries (100 * 50ms = 5 seconds timeout)
int retryCount = 0;
while (fullLen < size && retryCount < maxRetries) {
// Push a new local frame to automatically manage JNI references
// This prevents local reference table overflow by cleaning up refs at the end of each iteration
if (env->PushLocalFrame(16) < 0) {
qDebug() << "Failed to push local frame";
return -1;
}
QAndroidJniObject dd =
QAndroidJniObject::callStaticObjectMethod("org/cagnulen/qdomyoszwift/Usbserial", "read", "()[B");
jint len = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/Usbserial", "readLen", "()I");
jbyteArray d = dd.object<jbyteArray>();
jbyte *b = env->GetByteArrayElements(d, 0);
// Check if we got any data
if (len <= 0) {
// No data available, release memory and retry after a short sleep
env->ReleaseByteArrayElements(d, b, 0);
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
qDebug() << "No data available, retry" << retryCount + 1 << "of" << maxRetries;
CTsleeper::msleep(50); // Sleep for 50ms before retrying
retryCount++;
continue;
}
// Reset retry counter when we get data
retryCount = 0;
if (len + fullLen > size) {
QByteArray tmpDebug;
qDebug() << "buffer overflow! Truncate from" << len + fullLen << "requested" << size;
@@ -970,6 +997,10 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
}
qDebug() << len + fullLen - size << "bytes to the rxBuf" << tmpDebug.toHex(' ');
qDebug() << size << QByteArray((const char *)b, size).toHex(' ');
// Release JNI memory before returning
env->ReleaseByteArrayElements(d, b, 0);
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
return size;
}
for (int i = fullLen; i < len + fullLen; i++) {
@@ -977,6 +1008,16 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
}
qDebug() << len << QByteArray((const char *)b, len).toHex(' ');
fullLen += len;
// Release JNI memory after processing
env->ReleaseByteArrayElements(d, b, 0);
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
}
// Check if we timed out
if (retryCount >= maxRetries) {
qDebug() << "rawRead timeout: no data after" << maxRetries << "retries";
return -1; // Timeout error
}
qDebug() << "FULL BUFFER RX: << " << fullLen << QByteArray((const char *)bytes, size).toHex(' ');

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
computrainerbike::computrainerbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
QSettings settings;
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
target_watts.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -20,7 +20,7 @@
using namespace std::chrono_literals;
concept2skierg::concept2skierg(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -23,7 +23,7 @@ crossrope::crossrope(uint32_t pollDeviceTime, bool noConsole, bool noHeartServic
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

@@ -4,7 +4,7 @@ using namespace std::chrono_literals;
csafeelliptical::csafeelliptical(bool noWriteResistance, bool noHeartService, bool noVirtualDevice,
int8_t bikeResistanceOffset, double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -3,7 +3,7 @@
using namespace std::chrono_literals;
csaferower::csaferower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -1,5 +1,6 @@
#include "cscbike.h"
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualrower.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
@@ -17,7 +18,7 @@
using namespace std::chrono_literals;
cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -78,7 +79,12 @@ void cscbike::update() {
double rpm = currentCadence().value();
m_watt = 0.000602337 * pow(rpm, 3.11762) + 32.6404;
} else {
m_watt = wattFromHR(false);
// When cadence is zero, watts should be zero regardless of HR
if (currentCadence().value() == 0) {
m_watt = 0;
} else {
m_watt = wattFromHR(false);
}
}
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
@@ -207,6 +213,22 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
emit debug(QStringLiteral("Current Crank Event Time: ") + QString::number(_LastCrankEventTime));
}
// CSC Combo Sensor Fallback Logic
//
// Some combo sensors (e.g., Giant Combo) advertise both speed and cadence capabilities
// by setting CrankPresent=true in the CSC flags byte, but only transmit valid wheel data
// while sending crank data as zeros. This happens when:
// 1. The sensor supports dual mode (speed+cadence) but only speed sensor is mounted
// 2. The cadence sensor is not activated/calibrated
// 3. Firmware always sets CrankPresent flag regardless of actual crank sensor status
//
// In these cases, we use wheel revolutions as a fallback to calculate cadence.
// This works when the wheel circumference is set to a small value (e.g., 20cm for
// indoor trainers), which effectively converts wheel RPM to a cadence-like metric.
//
// Note: When using wheel revs as cadence, the calculated RPM can exceed 256 (the
// typical limit for real crank cadence). For example, with 20cm wheel circumference
// at 12.5 km/h, wheel RPM ≈ 1000. The validation logic below accounts for this.
if ((!CrankPresent || _CrankRevs == 0) && WheelPresent) {
CrankRevs = _WheelRevs;
LastCrankEventTime = _LastWheelEventTime;
@@ -222,7 +244,23 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
if (CrankRevs != oldCrankRevs && deltaT) {
double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * 1024 * 60;
if ((cadence >= 0 && cadence < 256 && CrankPresent) || (!CrankPresent && WheelPresent))
// Cadence Validation Logic
//
// Normal cadence validation applies a 256 RPM limit for real crank sensors (no human
// can pedal faster than 256 RPM). However, when using wheel revs as fallback
// (_CrankRevs == 0), we bypass this limit because:
// - Wheel RPM with small circumferences (e.g., 20cm) can legitimately exceed 256
// - Example: 12.5 km/h with 20cm circumference = ~1042 wheel RPM
// - This high RPM represents wheel rotation rate, not actual pedaling cadence
//
// The condition breakdown:
// Part 1: (cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent)
// - For real crank data: applies 256 RPM limit
// - For wheel fallback: no limit when _CrankRevs == 0
// Part 2: (!CrankPresent && WheelPresent)
// - Pure speed sensors with no crank capability
if ((cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent) || (!CrankPresent && WheelPresent))
Cadence = cadence;
lastGoodCadence = now;
} else if (lastGoodCadence.msecsTo(now) > 2000) {
@@ -422,6 +460,8 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
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 =
@@ -436,11 +476,17 @@ void cscbike::stateChanged(QLowEnergyService::ServiceState state) {
#endif
#endif
if (virtual_device_enabled) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
if (virtual_device_rower) {
emit debug(QStringLiteral("creating virtual rower interface..."));
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::ALTERNATIVE);
} else {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike, &virtualbike::changeInclination, this, &cscbike::changeInclination);
// connect(virtualBike,&virtualbike::debug ,this,&cscbike::debug);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
cycleopsphantombike::cycleopsphantombike(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
deerruntreadmill::deerruntreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -75,9 +75,22 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
}
}
void deerruntreadmill::waitForAPacket() {
QEventLoop loop;
QTimer timeout;
connect(this, &deerruntreadmill::packetReceived, &loop, &QEventLoop::quit);
timeout.singleShot(3000, &loop, SLOT(quit()));
loop.exec();
}
void deerruntreadmill::writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log) {
QEventLoop loop;
QTimer timeout;
if(!unlock_service) {
qDebug() << "ERROR! Unlock service not found!";
return;
}
connect(unlock_service, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
@@ -122,22 +135,80 @@ uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
return result;
}
uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
uint8_t result = 0;
if (size < 5) {
qDebug() << QStringLiteral("array too small for PitPat checksum");
return 0;
}
// For PitPat protocol:
// 1. XOR from byte 5 to byte (size - 3) for long messages (>= 7 bytes)
// or from byte 2 to byte (size - 3) for short messages (< 7 bytes)
// 2. XOR the result with byte 1
size_t startIdx = (size < 7) ? 2 : 5;
for (size_t i = startIdx; i <= size - 3; i++) {
result ^= arr[i];
}
// XOR with byte 1 (command byte)
result ^= arr[1];
return result;
}
void deerruntreadmill::forceSpeedAndInclination(double requestSpeed, double requestInclination) {
QSettings settings;
if (pitpat) {
// PitPat speed template
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x01, 0x08, 0x64, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x7a, 0x67, 0x96, 0x43};
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
uint16_t incline = (uint16_t)(requestInclination);
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
writeSpeed[7] = speed & 0xFF; // Low byte
writeSpeed[9] = incline & 0xFF; // Low byte
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed) + QStringLiteral(" incline=") + QString::number(requestInclination), false, true);
} else if (superun_ba04) {
// Superun BA04 speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed BA04 speed=") + QString::number(requestSpeed), false, false);
} else {
// Default speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
}
}
void deerruntreadmill::forceSpeed(double requestSpeed) {
QSettings settings;
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
forceSpeedAndInclination(requestSpeed, currentInclination().value());
}
void deerruntreadmill::forceIncline(double requestIncline) {
forceSpeedAndInclination(currentSpeed().value(), requestIncline);
}
void deerruntreadmill::changeInclinationRequested(double grade, double percentage) {
@@ -216,8 +287,7 @@ void deerruntreadmill::update() {
}
if (pitpat) {
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
forceSpeed(1.0);
} else {
// should be:
// 0x49 = inited
@@ -240,13 +310,16 @@ void deerruntreadmill::update() {
emit tapeStarted();
} else if (requestStop != -1) {
emit debug(QStringLiteral("stopping... ") + paused);
/*if (lastState == PAUSED) {
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
true);
} else*/ {
if (pitpat) {
uint8_t stop[] = {
0x6a, 0x17, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00,
0x8a, 0x00, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x12, 0x2e,
0x0c, 0xaa, 0x43};
writeCharacteristic(gattWriteCharacteristic, stop, sizeof(stop), QStringLiteral("stop"), false, true);
} else {
uint8_t stop[] = {0x4d, 0x00, 0x48, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd6, 0x43};
stop[2] = pollCounter;
@@ -318,6 +391,9 @@ void deerruntreadmill::characteristicChanged(const QLowEnergyCharacteristic &cha
speed = ((double)((value[3] << 8) | ((uint8_t)value[4])) / 1000.0);
}
double incline = 0.0;
if(pitpat) {
incline = (double)(value[11] & 0xFF);
}
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
@@ -392,15 +468,37 @@ void deerruntreadmill::btinit(bool startTape) {
// PitPat treadmill initialization sequence
uint8_t initData1[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("pitpat init 1"), false, true);
uint8_t unlockData[] = {0x6b, 0x05, 0x9d, 0x98, 0x43};
writeUnlockCharacteristic(unlockData, sizeof(unlockData), QStringLiteral("pitpat unlock"), false);
uint8_t initData2[] = {0x6a, 0x05, 0xd7, 0xd2, 0x43};
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("pitpat init 2"), false, true);
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
} else if (superun_ba04) {
// Superun BA04 treadmill initialization sequence
// Wait for initial packet from treadmill before sending init
emit debug(QStringLiteral("BA04: waiting for initial packet..."));
waitForAPacket();
// Init 1: pollCounter = 0
uint8_t initData1[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 0; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("BA04 init 1"), false, true);
uint8_t initData2[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 1; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("BA04 init 2"), false, true);
// Init 2: pollCounter = 1
uint8_t initData3[] = {0x4d, 0x00, 0x01, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0x7c, 0x43};
initData3[2] = 2; // pollCounter = 1
writeCharacteristic(gattWriteCharacteristic, initData3, sizeof(initData3), QStringLiteral("BA04 init 3"), false, true);
// Start pollCounter from 2 after init
pollCounter = 3;
}
initDone = true;
}
@@ -413,7 +511,10 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
QBluetoothUuid _superunWriteCharacteristicId((quint16)0xff01);
QBluetoothUuid _superunNotifyCharacteristicId((quint16)0xff02);
QBluetoothUuid _unlockCharacteristicId((quint16)0x2b2a);
QBluetoothUuid _unlockCharacteristicId2((quint16)0x2b11);
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
@@ -427,11 +528,16 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
qDebug() << QStringLiteral("unlock char uuid") << c.uuid() << QStringLiteral("handle") << c.handle()
<< c.properties();
}
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId);
if (unlock_characteristic.isValid()) {
emit debug(QStringLiteral("unlock characteristic found"));
} else {
qDebug() << "unlock char not found, let's try the other one";
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId2);
}
qDebug() << "unlock_characteristic" << unlock_characteristic.isValid();
return;
}
@@ -445,6 +551,9 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
if (pitpat) {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_pitpatWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_pitpatNotifyCharacteristicId);
} else if (superun_ba04) {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_superunWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_superunNotifyCharacteristicId);
} else {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
@@ -488,7 +597,9 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
void deerruntreadmill::serviceScanDone(void) {
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
QBluetoothUuid _superunServiceId((quint16)0xffff);
QBluetoothUuid _unlockServiceId((quint16)0x1801);
QBluetoothUuid _unlockServiceId2((quint16)0x1910);
emit debug(QStringLiteral("serviceScanDone"));
auto services_list = m_control->services();
@@ -497,17 +608,42 @@ void deerruntreadmill::serviceScanDone(void) {
emit debug(s.toString());
}
// Check if this is a pitpat treadmill by looking for the 0xfba0 service
if (services_list.contains(_pitpatServiceId)) {
// Try to create service objects for each variant
// On iOS, services_list.contains() doesn't work reliably, so we try to create the service directly
QLowEnergyService* pitpat_service = m_control->createServiceObject(_pitpatServiceId);
QLowEnergyService* superun_service = m_control->createServiceObject(_superunServiceId);
QLowEnergyService* default_service = m_control->createServiceObject(_gattCommunicationChannelServiceId);
// Check which service was successfully created
if (pitpat_service) {
pitpat = true;
emit debug(QStringLiteral("Detected pitpat treadmill variant"));
gattCommunicationChannelService = m_control->createServiceObject(_pitpatServiceId);
gattCommunicationChannelService = pitpat_service;
unlock_service = m_control->createServiceObject(_unlockServiceId);
} else {
if(!unlock_service) {
qDebug() << "unlock service not found, let's try with another one";
unlock_service = m_control->createServiceObject(_unlockServiceId2);
}
qDebug() << "unlock service " << unlock_service;
// Clean up unused services
if (superun_service) delete superun_service;
if (default_service) delete default_service;
} else if (superun_service) {
superun_ba04 = true;
pitpat = false;
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
emit debug(QStringLiteral("Detected Superun BA04 treadmill variant"));
gattCommunicationChannelService = superun_service;
// Clean up unused services
if (default_service) delete default_service;
} else if (default_service) {
pitpat = false;
emit debug(QStringLiteral("Detected default treadmill variant"));
gattCommunicationChannelService = default_service;
}
if (gattCommunicationChannelService) {
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
&deerruntreadmill::stateChanged);
@@ -515,7 +651,7 @@ void deerruntreadmill::serviceScanDone(void) {
} else {
emit debug(QStringLiteral("error on find Service"));
}
if (pitpat && unlock_service) {
connect(unlock_service, &QLowEnergyService::stateChanged, this,
&deerruntreadmill::stateChanged);

View File

@@ -41,16 +41,20 @@ class deerruntreadmill : public treadmill {
double forceInitSpeed = 0.0, double forceInitInclination = 0.0);
bool connected() override;
double minStepInclination() override;
double minStepSpeed() override { return 0.1; }
private:
void forceSpeed(double requestSpeed);
void forceIncline(double requestIncline);
void forceSpeedAndInclination(double requestSpeed, double requestInclination);
void btinit(bool startTape);
void writeCharacteristic(const QLowEnergyCharacteristic characteristic, uint8_t *data, uint8_t data_len,
const QString &info, bool disable_log = false, bool wait_for_response = false);
void writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false);
void waitForAPacket();
void startDiscover();
uint8_t calculateXOR(uint8_t arr[], size_t size);
uint8_t calculatePitPatChecksum(uint8_t arr[], size_t size);
bool noConsole = false;
bool noHeartService = false;
uint32_t pollDeviceTime = 200;
@@ -70,8 +74,9 @@ class deerruntreadmill : public treadmill {
QLowEnergyService *unlock_service = nullptr;
QLowEnergyCharacteristic unlock_characteristic;
bool pitpat = false;
bool superun_ba04 = false;
bool initDone = false;
bool initRequest = false;

View File

@@ -27,6 +27,9 @@ using namespace std::chrono_literals;
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
#define DM_MACHINE_OP_ROUVY(OP, P1, P2, P3) \
OP(WAHOO_KICKR, "ELITE AVANTI $uuid_hex$ W", DM_MACHINE_TYPE_TREADMILL | DM_MACHINE_TYPE_BIKE, P1, P2, P3)
#define DP_PROCESS_WRITE_0003() (zwift_play_emulator ? writeP0003 : 0)
#define DP_PROCESS_WRITE_2AD9() writeP2AD9
#define DP_PROCESS_WRITE_2AD9T() writeP2AD9
@@ -43,7 +46,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0xE005, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_E005, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD2, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD3, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x00\x01"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
@@ -51,7 +54,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2ACD, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
P3) \
@@ -127,13 +130,17 @@ enum {
} \
} \
if (P2.size()) { \
QString dircon_id = QString("%1").arg(settings.value(QZSettings::dircon_id, \
QZSettings::default_dircon_id).toInt(), 4, 10, QChar('0')); \
int dircon_id_int = settings.value(QZSettings::dircon_id, \
QZSettings::default_dircon_id).toInt(); \
if (rouvy_compatibility && dircon_id_int == 0) { \
dircon_id_int = 1234; \
} \
QString dircon_id = QString("%1").arg(dircon_id_int, rouvy_compatibility ? 5 : 4, 10, QChar('0')); \
DirconProcessor *processor = new DirconProcessor( \
P2, \
QString(QStringLiteral(NAME)) \
.replace(QStringLiteral("$uuid_hex$"), dircon_id), \
server_base_port + DM_MACHINE_##DESC, QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
server_base_port + DM_MACHINE_##DESC, rouvy_compatibility ? dircon_id : QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
this); \
QString servdesc; \
foreach (DirconProcessorService *s, P2) { servdesc += *s + QStringLiteral(","); } \
@@ -146,6 +153,24 @@ enum {
}
QString DirconManager::getMacAddress() {
QSettings settings;
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
int dircon_id = settings.value(QZSettings::dircon_id, QZSettings::default_dircon_id).toInt();
// When Rouvy compatibility is enabled and dircon_id is 0, use 1234 instead
if (rouvy_compatibility && dircon_id == 0) {
dircon_id = 1234;
}
// When Rouvy compatibility is enabled, use a specific MAC address with the last byte set to dircon_id
if (rouvy_compatibility) {
// Use base MAC address "24:DC:C3:E3:B5:XX" where XX is the dircon_id
// Ensure dircon_id is in the valid range 0-255
int last_byte = dircon_id & 0xFF;
return QString("24:DC:C3:E3:B5:%1").arg(last_byte, 2, 16, QChar('0')).toUpper();
}
// Default behavior: get MAC address from network interfaces
QString addr;
foreach (QNetworkInterface netInterface, QNetworkInterface::allInterfaces()) {
// Return only the first non-loopback MAC Address
@@ -171,15 +196,16 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QSettings settings;
DirconProcessorService *service;
QList<DirconProcessorService *> services, proc_services;
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
BLUETOOTH_TYPE dt = Bike->deviceType();
bt = Bike;
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
uint8_t type = dt == TREADMILL || dt == ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
: DM_MACHINE_TYPE_BIKE;
qDebug() << "Building Dircom Manager";
uint16_t server_base_port =
settings.value(QZSettings::dircon_server_base_port, QZSettings::default_dircon_server_base_port).toUInt();
bool bike_wheel_revs = settings.value(QZSettings::bike_wheel_revs, QZSettings::default_bike_wheel_revs).toBool();
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_BUILD_OP, Bike, 0, 0)
@@ -209,7 +235,11 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider);
QString mac = getMacAddress();
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
if (rouvy_compatibility) {
DM_MACHINE_OP_ROUVY(DM_MACHINE_INIT_OP, services, proc_services, type)
} else {
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
}
if (zwift_play_emulator || settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
bikeTimer.start(50ms);
@@ -236,7 +266,7 @@ double DirconManager::currentGear() {
QSettings settings;
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool() && writeP0003)
return writeP0003->currentGear();
else if(bt && bt->deviceType() == bluetoothdevice::BIKE)
else if(bt && bt->deviceType() == BIKE)
return ((bike*)bt)->gears();
return 0;
}

View File

@@ -96,11 +96,11 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
} else
return DPKT_PARSE_ERROR - rembuf;
} else if (this->Identifier == DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS) {
if (this->Length == 16 || this->Length == 17) {
if (this->Length >= 16) {
quint16 uuid = ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH8)) << 8;
uuid |= ((quint16)buf.at(DPKT_MESSAGE_HEADER_LENGTH + DPKT_POS_SH0)) & 0x00FF;
this->uuid = uuid;
if (this->Length == 17) {
if (this->Length >= 17) {
this->isRequest = true;
this->additional_data = buf.mid(DPKT_MESSAGE_HEADER_LENGTH + 16, 1);
}
@@ -117,6 +117,12 @@ int DirconPacket::parse(const QByteArray &buf, int last_seq_number) {
return rembuf;
} else
return DPKT_PARSE_ERROR - rembuf;
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
if (this->Length == 0) {
this->isRequest = this->checkIsRequest(last_seq_number);
return DPKT_MESSAGE_HEADER_LENGTH;
} else
return DPKT_PARSE_ERROR - rembuf;
} else
return DPKT_PARSE_ERROR - rembuf;
} else
@@ -182,6 +188,10 @@ QByteArray DirconPacket::encode(int last_seq_number) {
}
}
}
} else if (this->Identifier == DPKT_MSGID_UNKNOWN_0x07) {
// Unknown message 0x07 - always respond with empty payload
this->Length = 0;
byteout.append(2, 0);
} else if (this->Identifier == DPKT_MSGID_DISCOVER_CHARACTERISTICS && !this->isRequest) {
this->Length = 16 + this->uuids.size() * 17;
byteout.append((char)(this->Length >> 8)).append((char)(this->Length));

View File

@@ -25,6 +25,7 @@
#define DPKT_MSGID_WRITE_CHARACTERISTIC 0x04
#define DPKT_MSGID_ENABLE_CHARACTERISTIC_NOTIFICATIONS 0x05
#define DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION 0x06
#define DPKT_MSGID_UNKNOWN_0x07 0x07
#define DPKT_RESPCODE_SUCCESS_REQUEST 0x00
#define DPKT_RESPCODE_UNKNOWN_MESSAGE_TYPE 0x01
#define DPKT_RESPCODE_UNEXPECTED_ERROR 0x02

View File

@@ -9,6 +9,8 @@ DirconProcessor::DirconProcessor(const QList<DirconProcessorService *> &my_servi
: QObject(parent), services(my_services), mac(my_mac), serverPort(serv_port), serialN(serv_sn),
serverName(serv_name) {
qDebug() << "In the constructor of dircon processor for" << serverName;
QSettings settings;
rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
foreach (DirconProcessorService *my_service, my_services) { my_service->setParent(this); }
}
@@ -33,7 +35,8 @@ bool DirconProcessor::initServer() {
}
if (!server->isListening()) {
qDebug() << "Dircon TCP Server trying to listen" << serverPort;
return server->listen(QHostAddress::Any, serverPort);
// Listen only on IPv4 for Apple TV/Windows compatibility (like Elite Avanti) when Rouvy compatibility is enabled
return server->listen(rouvy_compatibility ? QHostAddress::AnyIPv4 : QHostAddress::Any, serverPort);
} else
return true;
}
@@ -59,20 +62,39 @@ void DirconProcessor::initAdvertising() {
mdnsHostname = new QMdnsEngine::Hostname(mdnsServer, serverName.toUtf8() + QByteArrayLiteral("H"), this);
mdnsProvider = new QMdnsEngine::Provider(mdnsServer, mdnsHostname, this);
QMdnsEngine::Service mdnsService;
mdnsService.setType("_wahoo-fitness-tnp._tcp.local.");
mdnsService.setType(rouvy_compatibility ? "_wahoo-fitness-tnp._tcp.local" : "_wahoo-fitness-tnp._tcp.local.");
mdnsService.setName(serverName.toUtf8());
mdnsService.addAttribute(QByteArrayLiteral("mac-address"), mac.toUtf8());
mdnsService.addAttribute(QByteArrayLiteral("serial-number"), serialN.toUtf8());
QString ble_uuids;
int i = 0;
foreach (DirconProcessorService *service, services) {
if(service->uuid == ZWIFT_PLAY_ENUM_VALUE) {
ble_uuids += ZWIFT_PLAY_UUID_STRING +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
} else {
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
if (rouvy_compatibility) {
QStringList uuid_list;
foreach (DirconProcessorService *service, services) {
// Filter: only advertise 0x1826 for KICKR (skip 0x1818, 0x1816)
if(service->uuid == 0x1818 || service->uuid == 0x1816) {
continue;
}
if(service->uuid == ZWIFT_PLAY_ENUM_VALUE) {
uuid_list.append(ZWIFT_PLAY_UUID_STRING);
} else {
// Use short format with 0x prefix (Apple TV/Windows compatibility)
uuid_list.append(QString(QStringLiteral("0x%1"))
.arg(service->uuid, 4, 16, QLatin1Char('0')));
}
}
ble_uuids = uuid_list.join(",");
} else {
int i = 0;
foreach (DirconProcessorService *service, services) {
if(service->uuid == ZWIFT_PLAY_ENUM_VALUE) {
ble_uuids += ZWIFT_PLAY_UUID_STRING +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
} else {
ble_uuids += QString(QStringLiteral(DP_BASE_UUID))
.replace("u", QString(QStringLiteral("%1")).arg(service->uuid, 4, 16, QLatin1Char('0'))) +
((i++ < services.size() - 1) ? QStringLiteral(",") : QStringLiteral(""));
}
}
}
mdnsService.addAttribute(QByteArrayLiteral("ble-service-uuids"), ble_uuids.toUtf8());
@@ -101,6 +123,21 @@ void DirconProcessor::tcpNewConnection() {
connect(socket, SIGNAL(readyRead()), this, SLOT(tcpDataAvailable()));
DirconProcessorClient *client = new DirconProcessorClient(socket);
clientsMap.insert(socket, client);
if (rouvy_compatibility) {
// Send initial notification for 0x2AD2 (Indoor Bike Data) - Apple TV/Windows compatibility
// Elite Avanti sends this immediately after connection
DirconPacket initPkt;
initPkt.isRequest = false;
initPkt.Identifier = DPKT_MSGID_UNSOLICITED_CHARACTERISTIC_NOTIFICATION;
initPkt.ResponseCode = DPKT_RESPCODE_SUCCESS_REQUEST;
initPkt.uuid = 0x2AD2;
initPkt.additional_data = QByteArray(29, 0x00); // Empty data for now
QByteArray initData = initPkt.encode(0);
socket->write(initData);
socket->flush();
qDebug() << "Sent initial notification for 0x2AD2 to" << socket->peerAddress().toString();
}
}
void DirconProcessor::tcpDisconnected() {
@@ -187,7 +224,7 @@ DirconPacket DirconProcessor::processPacket(DirconProcessorClient *client, const
foreach (cc, service->chars) {
if (cc->uuid == pkt.uuid) {
cfound = true;
if (cc->type & DPKT_CHAR_PROP_FLAG_NOTIFY) {
if (cc->type & (DPKT_CHAR_PROP_FLAG_NOTIFY | DPKT_CHAR_PROP_FLAG_INDICATE)) {
int idx;
char notif = pkt.additional_data.at(0);
out.uuid = pkt.uuid;
@@ -208,6 +245,9 @@ DirconPacket DirconProcessor::processPacket(DirconProcessorClient *client, const
}
if (!cfound)
out.ResponseCode = DPKT_RESPCODE_CHARACTERISTIC_NOT_FOUND;
} else if (pkt.Identifier == DPKT_MSGID_UNKNOWN_0x07) {
// Unknown message 0x07 - respond with success
out.ResponseCode = DPKT_RESPCODE_SUCCESS_REQUEST;
}
}
return out;
@@ -225,7 +265,8 @@ bool DirconProcessor::sendCharacteristicNotification(quint16 uuid, const QByteAr
pkt.uuid = uuid;
for (QHash<QTcpSocket *, DirconProcessorClient *>::iterator i = clientsMap.begin(); i != clientsMap.end(); ++i) {
client = i.value();
/*if (client->char_notify.indexOf(uuid) >= 0 || !settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool())*/ {
if (!settings.value(QZSettings::wahoo_rgt_dircon, QZSettings::default_wahoo_rgt_dircon).toBool() ||
client->char_notify.indexOf(uuid) >= 0) {
socket = i.key();
rvs = socket->write(pkt.encode(0)) < 0;
if (rvs)

View File

@@ -81,6 +81,7 @@ class DirconProcessor : public QObject {
QMdnsEngine::Provider *mdnsProvider = 0;
QMdnsEngine::Hostname *mdnsHostname = 0;
QHash<QTcpSocket *, DirconProcessorClient *> clientsMap;
bool rouvy_compatibility = false;
bool initServer();
void initAdvertising();
DirconPacket processPacket(DirconProcessorClient *client, const DirconPacket &pkt);

View File

@@ -3,6 +3,8 @@
#include "keepawakehelper.h"
#endif
#include "virtualdevices/virtualbike.h"
#include "homeform.h"
#include "qzsettings.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
@@ -15,7 +17,7 @@ using namespace std::chrono_literals;
domyosbike::domyosbike(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
@@ -302,6 +304,7 @@ void domyosbike::characteristicChanged(const QLowEnergyCharacteristic &character
// so this simply condition will match all the cases, excluding the 20byte packet of the T900.
if (newValue.length() != 20) {
qDebug() << QStringLiteral("packetReceived!");
initPacketRecv = true;
emit packetReceived();
}
@@ -498,22 +501,98 @@ void domyosbike::btinit_changyow(bool startTape) {
0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x14, 0x01, 0xff, 0xff};
uint8_t initDataStart13[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbd};
init_reset:
initPacketRecv = false;
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "init 1 not received, retrying...";
goto init_reset;
}
init_data2:
initPacketRecv = false;
writeCharacteristic(initData2, sizeof(initData2), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "init 2 not received, retrying...";
goto init_data2;
}
init_start:
initPacketRecv = false;
writeCharacteristic(initDataStart, sizeof(initDataStart), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart not received, retrying...";
goto init_start;
}
init_start2:
initPacketRecv = false;
writeCharacteristic(initDataStart2, sizeof(initDataStart2), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart2 not received, retrying...";
goto init_start2;
}
init_start3:
initPacketRecv = false;
writeCharacteristic(initDataStart3, sizeof(initDataStart3), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart3 not received, retrying...";
goto init_start3;
}
init_start4:
initPacketRecv = false;
writeCharacteristic(initDataStart4, sizeof(initDataStart4), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart4 not received, retrying...";
goto init_start4;
}
init_start5:
initPacketRecv = false;
writeCharacteristic(initDataStart5, sizeof(initDataStart5), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart5 not received, retrying...";
goto init_start5;
}
init_start6_7:
initPacketRecv = false;
writeCharacteristic(initDataStart6, sizeof(initDataStart6), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart7, sizeof(initDataStart7), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart6/7 not received, retrying...";
goto init_start6_7;
}
init_start8_9:
initPacketRecv = false;
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart8/9 not received, retrying...";
goto init_start8_9;
}
init_start10_11:
initPacketRecv = false;
writeCharacteristic(initDataStart10, sizeof(initDataStart10), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart11, sizeof(initDataStart11), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart10/11 not received, retrying...";
goto init_start10_11;
}
if (startTape) {
init_start12_13:
initPacketRecv = false;
writeCharacteristic(initDataStart12, sizeof(initDataStart12), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart13, sizeof(initDataStart13), QStringLiteral("init"), false, true);
if (!initPacketRecv) {
qDebug() << "initDataStart12/13 not received, retrying...";
goto init_start12_13;
}
}
initDone = true;
@@ -593,6 +672,22 @@ void domyosbike::serviceScanDone(void) {
QBluetoothUuid _gattCommunicationChannelServiceId(QStringLiteral("49535343-fe7d-4ae5-8fa9-9fafd205e455"));
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);
if(!gattCommunicationChannelService) {
// Main service not found, check if FTMS service is available
QBluetoothUuid ftmsServiceId((quint16)0x1826);
QLowEnergyService *ftmsService = m_control->createServiceObject(ftmsServiceId);
if(ftmsService) {
QSettings settings;
settings.setValue(QZSettings::ftms_bike, bluetoothDevice.name());
qDebug() << "forcing FTMS bike since it has FTMS service but not the main domyos service";
if(homeform::singleton())
homeform::singleton()->setToastRequested("FTMS bike found, restart the app to apply the change");
delete ftmsService;
}
return;
}
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &domyosbike::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
@@ -601,6 +696,8 @@ void domyosbike::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
qDebug() << QStringLiteral("domyosbike::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString();
m_control->disconnectFromDevice();
}
void domyosbike::error(QLowEnergyController::Error err) {

View File

@@ -71,6 +71,7 @@ class domyosbike : public bike {
volatile bool incompletePackets = false;
bool initDone = false;
bool initRequest = false;
bool initPacketRecv = false;
bool noWriteResistance = false;
bool noHeartService = false;
bool testResistance = false;

View File

@@ -17,7 +17,7 @@ using namespace std::chrono_literals;
domyoselliptical::domyoselliptical(bool noWriteResistance, bool noHeartService, bool testResistance,
int8_t bikeResistanceOffset, double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

@@ -6,6 +6,8 @@
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualrower.h"
#include "virtualdevices/virtualtreadmill.h"
#include "homeform.h"
#include "qzsettings.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
@@ -18,7 +20,7 @@ using namespace std::chrono_literals;
domyosrower::domyosrower(bool noWriteResistance, bool noHeartService, bool testResistance, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
@@ -299,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 =
@@ -641,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;
@@ -655,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;
}
@@ -883,6 +885,18 @@ void domyosrower::serviceScanDone(void) {
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &domyosrower::stateChanged);
gattCommunicationChannelService->discoverDetails();
} else {
// Main service not found, check if FTMS service is available
QBluetoothUuid ftmsServiceId((quint16)0x1826);
QLowEnergyService *ftmsService = m_control->createServiceObject(ftmsServiceId);
if(ftmsService) {
QSettings settings;
settings.setValue(QZSettings::ftms_rower, bluetoothDevice.name());
qDebug() << "forcing FTMS rower since it has FTMS service but not the main domyos service";
if(homeform::singleton())
homeform::singleton()->setToastRequested("FTMS rower found, restart the app to apply the change");
delete ftmsService;
}
ftmsRower = true;
auto services_list = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services_list)) {

View File

@@ -56,7 +56,7 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -73,50 +73,98 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
initDone = false;
connect(refresh, &QTimer::timeout, this, &domyostreadmill::update);
refresh->start(pollDeviceTime);
// Initialize write timeout timer
writeTimeoutTimer = new QTimer(this);
writeTimeoutTimer->setSingleShot(true);
connect(writeTimeoutTimer, &QTimer::timeout, this, [this]() {
qDebug() << QStringLiteral("writeCharacteristic timeout - processing next in queue");
isWriting = false;
currentWriteWaitingForResponse = false;
processWriteQueue();
});
// Connect packetReceived signal to handle wait_for_response = true case
connect(this, &domyostreadmill::packetReceived, this, [this]() {
// Only process if we were waiting for a response
if (currentWriteWaitingForResponse && isWriting) {
// Stop timeout timer
writeTimeoutTimer->stop();
// Mark writing as complete and process next item in queue
isWriting = false;
currentWriteWaitingForResponse = false;
processWriteQueue();
}
});
}
void domyostreadmill::writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log,
bool wait_for_response) {
QEventLoop loop;
QTimer timeout;
// Create write request and add to queue
WriteRequest request;
request.data = QByteArray((const char *)data, data_len);
request.info = info;
request.disable_log = disable_log;
request.wait_for_response = wait_for_response;
if (wait_for_response) {
connect(this, &domyostreadmill::packetReceived, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
} else {
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicWritten, &loop, &QEventLoop::quit);
timeout.singleShot(300ms, &loop, &QEventLoop::quit);
writeQueue.enqueue(request);
// Start processing if not already writing
processWriteQueue();
}
void domyostreadmill::processWriteQueue() {
// If already writing or queue is empty, do nothing
if (isWriting || writeQueue.isEmpty()) {
return;
}
if (gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
// Check connection state
if (!gattCommunicationChannelService ||
gattCommunicationChannelService->state() != QLowEnergyService::ServiceState::ServiceDiscovered ||
m_control->state() == QLowEnergyController::UnconnectedState) {
qDebug() << QStringLiteral("writeCharacteristic error because the connection is closed");
// Clear the queue on disconnection
writeQueue.clear();
isWriting = false;
return;
}
if (!gattWriteCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattWriteCharacteristic is invalid");
// Clear the queue on invalid characteristic
writeQueue.clear();
isWriting = false;
return;
}
// Get next request from queue
WriteRequest request = writeQueue.dequeue();
isWriting = true;
currentWriteWaitingForResponse = request.wait_for_response;
// Update write buffer
if (writeBuffer) {
delete writeBuffer;
}
writeBuffer = new QByteArray((const char *)data, data_len);
writeBuffer = new QByteArray(request.data);
// Write the characteristic
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (!disable_log) {
if (!request.disable_log) {
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ')
<< QStringLiteral(" // ") + info;
<< QStringLiteral(" // ") + request.info;
}
loop.exec();
// Start timeout timer (300ms as before)
writeTimeoutTimer->start(300);
if (timeout.isActive() == false) {
qDebug() << QStringLiteral(" exit for timeout");
}
// Note: The actual completion will be signaled by:
// - characteristicWritten (if wait_for_response = false)
// - packetReceived (if wait_for_response = true)
// which will call processWriteQueue() again to process the next item
}
void domyostreadmill::updateDisplay(uint16_t elapsed) {
@@ -409,6 +457,7 @@ void domyostreadmill::characteristicChanged(const QLowEnergyCharacteristic &char
bool domyos_treadmill_buttons =
settings.value(QZSettings::domyos_treadmill_buttons, QZSettings::default_domyos_treadmill_buttons).toBool();
bool domyos_treadmill_t900a = settings.value(QZSettings::domyos_treadmill_t900a, QZSettings::default_domyos_treadmill_t900a).toBool();
domyos_treadmill_sync_start = settings.value(QZSettings::domyos_treadmill_sync_start, QZSettings::default_domyos_treadmill_sync_start).toBool();
Q_UNUSED(characteristic);
QByteArray value = newValue;
@@ -761,13 +810,28 @@ void domyostreadmill::btinit(bool startTape) {
writeCharacteristic(initDataStart4, sizeof(initDataStart4), QStringLiteral("init"), false, true);
writeCharacteristic(initDataStart5, sizeof(initDataStart5), QStringLiteral("init"), false, true);
// writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
// writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
forceSpeedOrIncline(lastSpeed, lastInclination);
// Old behavior (before commit c90093046): these lines were always executed
// New behavior (after commit c90093046): these lines are only executed if startTape is true
if (domyos_treadmill_sync_start) {
// Old behavior: always execute these lines
forceSpeedOrIncline(lastSpeed, lastInclination);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
}
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
if (startTape) {
// writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
// writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
if (!domyos_treadmill_sync_start) {
// New behavior: only execute if startTape is true
forceSpeedOrIncline(lastSpeed, lastInclination);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
}
writeCharacteristic(initDataStart10, sizeof(initDataStart10), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart11, sizeof(initDataStart11), QStringLiteral("init"), false, true);
writeCharacteristic(initDataStart12, sizeof(initDataStart12), QStringLiteral("init"), false, false);
@@ -822,6 +886,17 @@ void domyostreadmill::characteristicWritten(const QLowEnergyCharacteristic &char
const QByteArray &newValue) {
Q_UNUSED(characteristic);
emit debug(QStringLiteral("characteristicWritten ") + newValue.toHex(' '));
// If the current write is NOT waiting for a response, we can process the next one
if (!currentWriteWaitingForResponse) {
// Stop timeout timer
writeTimeoutTimer->stop();
// Mark writing as complete and process next item in queue
isWriting = false;
processWriteQueue();
}
// Otherwise, we need to wait for packetReceived signal
}
void domyostreadmill::serviceScanDone(void) {

View File

@@ -23,6 +23,7 @@
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QtCore/qqueue.h>
#include <QDateTime>
#include <QObject>
@@ -43,6 +44,13 @@ class domyostreadmill : public treadmill {
bool changeFanSpeed(uint8_t speed) override;
private:
// Structure for async write queue
struct WriteRequest {
QByteArray data;
QString info;
bool disable_log;
bool wait_for_response;
};
bool sendChangeFanSpeed(uint8_t speed);
double GetSpeedFromPacket(const QByteArray &packet);
double GetInclinationFromPacket(const QByteArray &packet);
@@ -53,10 +61,12 @@ class domyostreadmill : public treadmill {
void btinit(bool startTape);
void writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
bool wait_for_response = false);
void processWriteQueue();
void startDiscover();
volatile bool incompletePackets = false;
bool noConsole = false;
bool noHeartService = false;
bool domyos_treadmill_sync_start = false;
uint32_t pollDeviceTime = 200;
bool searchStopped = false;
uint8_t sec1Update = 0;
@@ -75,6 +85,12 @@ class domyostreadmill : public treadmill {
bool initDone = false;
bool initRequest = false;
// Async write queue management
QQueue<WriteRequest> writeQueue;
bool isWriting = false;
bool currentWriteWaitingForResponse = false;
QTimer *writeTimeoutTimer = nullptr;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif

View File

@@ -23,7 +23,7 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT);
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -272,8 +272,10 @@ void echelonconnectsport::characteristicChanged(const QLowEnergyCharacteristic &
int8_t g = gears();
g += (res - qRound(Resistance.value()));
qDebug() << QStringLiteral("gears_from_bike APPLIED") << gears() << g;
lastRawRequestedResistanceValue = -1; // in order to avoid to change resistance with the setGears
resistance_t savedRawValue = lastRawRequestedResistanceValue;
lastRawRequestedResistanceValue = -1; // temporarily prevent setGears from re-applying resistance
setGears(g);
lastRawRequestedResistanceValue = savedRawValue; // restore for future checks
}
}
Resistance = res;
@@ -645,7 +647,7 @@ uint16_t echelonconnectsport::wattsFromResistance(double resistance) {
const double Epsilon = 4.94065645841247E-324;
const int wattTableFirstDimension = 33;
const int wattTableSecondDimension = 11;
double wattTable[wattTableFirstDimension][wattTableSecondDimension] = {
static const double wattTable[wattTableFirstDimension][wattTableSecondDimension] = {
{Epsilon, 1.0, 2.2, 4.8, 9.5, 13.6, 16.7, 22.6, 26.3, 29.2, 47.0},
{Epsilon, 1.0, 2.2, 4.8, 9.5, 13.6, 16.7, 22.6, 26.3, 29.2, 47.0},
{Epsilon, 1.3, 3.0, 5.4, 10.4, 14.5, 18.5, 24.6, 27.6, 33.5, 49.5},
@@ -680,7 +682,7 @@ uint16_t echelonconnectsport::wattsFromResistance(double resistance) {
{Epsilon, 12.5, 48.0, 99.3, 162.2, 232.9, 310.4, 400.3, 435.5, 530.5, 589.0},
{Epsilon, 13.0, 53.0, 102.0, 170.3, 242.0, 320.0, 427.9, 475.2, 570.0, 625.0}};
double wattTable_mgarcea[wattTableFirstDimension][wattTableSecondDimension] = {
static const double wattTable_mgarcea[wattTableFirstDimension][wattTableSecondDimension] = {
{Epsilon, 1.0, 2.2, 4.8, 9.5, 13.6, 16.7, 22.6, 26.3, 29.2, 47.0},
{Epsilon, 1.0, 2.2, 4.8, 9.5, 13.6, 16.7, 22.6, 26.3, 29.2, 47.0},
{Epsilon, 1.3, 3.0, 5.4, 10.4, 14.5, 18.5, 24.6, 27.6, 33.5, 49.5},
@@ -722,7 +724,7 @@ uint16_t echelonconnectsport::wattsFromResistance(double resistance) {
if (level >= wattTableFirstDimension) {
level = wattTableFirstDimension - 1;
}
double *watts_of_level;
const double *watts_of_level;
QSettings settings;
if (!settings.value(QZSettings::echelon_watttable, QZSettings::default_echelon_watttable)
.toString()

Some files were not shown because too many files have changed in this diff Show More