Compare commits

..

144 Commits

Author SHA1 Message Date
Roberto Viola
e5998087e4 Update gpiotreadmill.cpp 2024-04-18 15:17:44 +02:00
Roberto Viola
252a45565e Update gpiotreadmill.cpp 2024-04-17 09:28:37 +02:00
Roberto Viola
05719c1be0 adding start, stop and frequency change 2024-04-15 12:15:18 +02:00
Roberto Viola
15d0c82eae trying to read 1 register at a time 2024-04-09 08:51:11 +02:00
Roberto Viola
50a189b066 Merge branch 'master' into Danfoss-VLT-Micro---176F7330 2024-04-08 09:01:47 +02:00
Roberto Viola
7628457ffa Update main.cpp 2024-04-08 09:00:47 +02:00
Roberto Viola
daaf7882f5 first commit 2024-04-02 15:16:11 +02:00
Roberto Viola
e2feffaf48 fixing includes 2024-02-29 17:57:28 +01:00
Roberto Viola
2cba38c900 Merge branch 'zwift-api-windows' into treadmill-modbus 2024-02-29 17:23:04 +01:00
Roberto Viola
8bcacf5b95 Merge branch 'master' into treadmill-modbus 2024-02-29 17:21:10 +01:00
Roberto Viola
23c8628e4b Merge branch 'master' into zwift-api-windows 2024-02-29 17:15:37 +01:00
Roberto Viola
70fd6f9097 Update main.yml 2024-02-29 15:57:27 +01:00
Roberto Viola
d4c5853561 Update qdomyos-zwift.pri 2024-02-29 14:37:48 +01:00
Roberto Viola
6b825c643a Update main.yml 2024-02-29 13:55:52 +01:00
Roberto Viola
97856b752c Update trainprogram.cpp 2024-02-29 11:01:04 +01:00
Roberto Viola
5465e30d3d Merge branch 'master' into zwift-api-windows 2024-02-29 10:53:20 +01:00
Roberto Viola
d0f2ab2700 Update main.yml 2024-02-28 08:59:07 +01:00
Roberto Viola
24bf95a039 Update main.yml 2024-02-28 08:21:03 +01:00
Roberto Viola
4c6401e7b9 Update trainprogram.cpp 2024-02-27 17:37:14 +01:00
Roberto Viola
571fd08d8c starting to port the same on linux 2024-02-27 17:33:25 +01:00
Roberto Viola
c667974aa2 Update main.yml 2024-02-27 17:24:21 +01:00
Roberto Viola
4bb2012049 Update trainprogram.cpp 2024-02-27 16:52:18 +01:00
Roberto Viola
4ac43dc166 protobuf 2024-02-27 16:20:30 +01:00
Roberto Viola
d5e795f5d6 Update main.yml 2024-02-27 15:37:36 +01:00
Roberto Viola
eb52fcf6de Update main.yml 2024-02-27 15:26:18 +01:00
Roberto Viola
9f4ab19e28 include removing, using vcpkg 2024-02-27 15:15:42 +01:00
Roberto Viola
1cdfb1edef Update qdomyos-zwift.pri 2024-02-27 14:42:35 +01:00
Roberto Viola
d865887311 Update qdomyos-zwift.pri 2024-02-27 14:02:28 +01:00
Roberto Viola
598a2a6d3a Update main.yml 2024-02-27 13:40:34 +01:00
Roberto Viola
d1f51c47fb using vcpkg 2024-02-27 13:25:43 +01:00
Roberto Viola
58b4ddd07c Update main.yml 2024-02-27 12:46:37 +01:00
Roberto Viola
f6bdbfa40d Update main.yml 2024-02-27 12:41:30 +01:00
Roberto Viola
d36b28e8fe Update main.yml 2024-02-27 12:31:23 +01:00
Roberto Viola
f0541b7bbc Update main.yml 2024-02-27 12:25:36 +01:00
Roberto Viola
1beb7f69f3 vcpkg 2024-02-27 12:19:43 +01:00
Roberto Viola
cd0040cee3 adding submodule 2024-02-27 12:12:22 +01:00
Roberto Viola
b5f90bd8f7 adding vcpkg 2024-02-27 12:06:36 +01:00
Roberto Viola
f23dda50f0 Update qdomyos-zwift.pri 2024-02-27 11:34:19 +01:00
Roberto Viola
6603195996 building absl inside protobuf for debug 2024-02-27 11:13:00 +01:00
Roberto Viola
dadaece5c3 adding absl on msvc 2024-02-27 10:35:43 +01:00
Roberto Viola
183e7dc0b6 Revert "Update qdomyos-zwift.pri"
This reverts commit 7e12ca0476.
2024-02-27 09:25:26 +01:00
Roberto Viola
7e12ca0476 Update qdomyos-zwift.pri 2024-02-27 07:20:20 +01:00
Roberto Viola
cbf0c91a4b Update main.yml 2024-02-27 07:07:04 +01:00
Roberto Viola
187dd9afc6 Update main.yml 2024-02-27 06:53:34 +01:00
Roberto Viola
24078a9dc8 JLL T550 #2161 2024-02-26 18:14:13 +01:00
Roberto Viola
487671293f again msvc first 2024-02-26 18:07:31 +01:00
Roberto Viola
323d051c7c Merge branch 'master' into zwift-api-windows 2024-02-26 18:05:34 +01:00
Roberto Viola
49f6fa9694 trying to put the protobuf lib on the same src dir 2024-01-31 11:26:14 +01:00
Roberto Viola
f804c01fde Update qdomyos-zwift.pri 2024-01-31 10:59:31 +01:00
Roberto Viola
737f1faf47 trying to msvc first 2024-01-31 10:29:19 +01:00
Roberto Viola
a3c0bec761 Merge branch 'master' into zwift-api-windows 2024-01-31 10:26:24 +01:00
Roberto Viola
6621c83452 Update main.yml 2024-01-16 15:34:11 +01:00
Roberto Viola
efe0cbcb4c trying to fix mingw build 2024-01-16 15:02:24 +01:00
Roberto Viola
194ca721ab required lib for libabsl 2024-01-16 12:27:44 +01:00
Roberto Viola
9de579ffc7 builds on mingw 2024-01-16 12:23:59 +01:00
Roberto Viola
9f758ddbec Update main.yml 2024-01-04 09:50:50 +01:00
Roberto Viola
b059dd681b adding mingw libabsl 2024-01-04 09:40:03 +01:00
Roberto Viola
e4e49524b8 adding mingw64 binary 2024-01-04 09:12:11 +01:00
Roberto Viola
9413349277 Update main.yml 2024-01-04 08:49:18 +01:00
Roberto Viola
af0abb30a9 Update main.yml 2024-01-04 08:48:10 +01:00
Roberto Viola
0e8bab3f34 Update main.yml 2024-01-04 08:42:32 +01:00
Roberto Viola
cae67268a6 Update main.yml 2024-01-04 08:38:13 +01:00
Roberto Viola
c5e53a920c Update main.yml 2024-01-04 08:25:39 +01:00
Roberto Viola
06123b5347 Update main.yml 2024-01-04 08:22:34 +01:00
Roberto Viola
6663b2c057 Update main.yml 2024-01-03 16:58:00 +01:00
Roberto Viola
9b3462441a Update main.yml 2024-01-03 16:26:48 +01:00
Roberto Viola
58270e7bf9 Update main.yml 2024-01-03 16:25:49 +01:00
Roberto Viola
c3b04dcccc building protobuf 2024-01-03 16:24:50 +01:00
Roberto Viola
ffeb51eecc absl from mingw 2024-01-03 15:20:24 +01:00
Roberto Viola
8dd29e2f5f adding absl 2024-01-03 15:03:20 +01:00
Roberto Viola
5e5ee11b37 Update main.yml 2024-01-01 12:54:34 +01:00
Roberto Viola
5057ddb3ed Update main.yml 2024-01-01 12:52:34 +01:00
Roberto Viola
128812b5cd adding protobuf to src folder 2024-01-01 12:36:49 +01:00
Roberto Viola
10f7387cfb Update main.yml 2024-01-01 12:27:39 +01:00
Roberto Viola
6afc73f6fb Update qdomyos-zwift.pri 2024-01-01 12:06:07 +01:00
Roberto Viola
eeae2ee587 mingw-w64-x86_64-protobuf 2024-01-01 12:05:01 +01:00
Roberto Viola
c45c842f20 Update qdomyos-zwift.pri 2024-01-01 11:55:30 +01:00
Roberto Viola
6dd8f6c831 Update main.yml 2024-01-01 11:12:52 +01:00
Roberto Viola
8043b2807d first test on CI 2024-01-01 09:00:39 +01:00
Roberto Viola
6fda282cf7 Merge branch 'master' into treadmill-modbus 2023-12-23 09:04:41 +00:00
Roberto Viola
b02c9de46f Merge branch 'master' into treadmill-modbus 2023-12-23 10:02:27 +01:00
Roberto Viola
d23115ea8c modbus error handling 2023-12-17 17:45:46 +01:00
Roberto Viola
1b13d8d9fd Update homeform.cpp 2023-12-17 17:18:32 +01:00
Roberto Viola
0409ade3f4 key pressed 2023-12-17 17:03:01 +01:00
Roberto Viola
6153f421cd Update gpiotreadmill.cpp 2023-12-17 16:42:55 +01:00
Roberto Viola
a1c8b9b363 Merge branch 'treadmill-modbus' of https://github.com/cagnulein/qdomyos-zwift into treadmill-modbus 2023-12-17 16:41:19 +01:00
Roberto Viola
064b34ddb3 minstepspeed to 0.1 2023-12-17 16:41:13 +01:00
Roberto Viola
62df4cc48f close gpiotreadmill.cpp 2023-12-16 21:30:26 +00:00
Roberto Viola
550b84ef98 Update gpiotreadmill.h 2023-12-16 21:17:06 +00:00
Roberto Viola
9b8e92b838 Update gpiotreadmill.h 2023-12-16 21:10:53 +00:00
Roberto Viola
a3aa6f9eeb speeding up the change 2023-12-16 21:03:59 +01:00
Roberto Viola
d279bc916b crash fixed 2023-12-16 19:56:04 +01:00
Roberto Viola
b2d666b06d Update gpiotreadmill.cpp 2023-12-16 08:20:22 +01:00
Roberto Viola
4210e2bbef fixing init and forcing gpio treadmill 2023-12-15 23:17:47 +01:00
Roberto Viola
f41944ff05 adapting for http://www.chinalctech.com/m/view.php?aid=490&fbclid=IwAR05CXn37H8ZkMY6d7Er63bgMpsNeyfsRFYyU_SH4f7uphDEmOwLThXlE-E 2023-12-15 22:22:15 +01:00
Roberto Viola
871b403b89 Update gpiotreadmill.cpp 2023-12-15 21:48:32 +01:00
Roberto Viola
a06f9f7cc0 Update main.yml 2023-12-15 16:41:53 +01:00
Roberto Viola
39a6c7402e Update main.yml 2023-12-15 16:40:08 +01:00
Roberto Viola
536f8766f2 Update main.yml 2023-12-15 14:17:49 +01:00
Roberto Viola
a80b66b23d Update qdomyos-zwift.pri 2023-12-15 13:42:31 +01:00
Roberto Viola
af73de3889 Update qdomyos-zwift.pri 2023-12-15 13:31:22 +01:00
Roberto Viola
6ca55b32a8 Update qdomyos-zwift.pri 2023-12-15 13:16:41 +01:00
Roberto Viola
7d7fd41172 removing consolereader 2023-12-15 13:15:57 +01:00
Roberto Viola
b44e482e8e fixing linker 2023-12-15 13:08:07 +01:00
Roberto Viola
76bc2bd1ff debug restored 2023-12-15 11:38:23 +01:00
Roberto Viola
5b59e5d2be removed debug 2023-12-15 11:32:12 +01:00
Roberto Viola
3b15f078e5 Update gpiotreadmill.h 2023-12-15 11:18:13 +01:00
Roberto Viola
57c951ccdc fixing android build 2023-12-15 11:16:16 +01:00
Roberto Viola
4a91c37d9f Update qdomyos-zwift.pri 2023-12-15 11:10:12 +01:00
Roberto Viola
6761b1bd0a fixing CI for the other archs 2023-12-15 11:08:59 +01:00
Roberto Viola
9e916bdf04 Update bluetooth.cpp 2023-12-15 10:56:12 +01:00
Roberto Viola
f33f06d184 modbus porting completed 2023-12-15 10:52:51 +01:00
Roberto Viola
d136b72604 Update qdomyos-zwift.pri 2023-12-15 10:13:36 +01:00
Roberto Viola
5b0bd28fe0 Merge branch 'master' into treadmill-modbus 2023-12-15 10:13:14 +01:00
Roberto Viola
d9034eff3f Local input for headless mode #938
https://github.com/cagnulein/qdomyos-zwift/issues/938#issuecomment-1256805749
2022-09-26 11:53:00 +02:00
Roberto Viola
94f5c37667 disabling qml too if the no-gui selector is enabled 2022-09-19 08:44:19 +02:00
Roberto Viola
c9b10026e0 workaround 2022-09-17 18:11:34 +02:00
Roberto Viola
d6937433da Local input for headless mode #938 2022-09-16 10:20:53 +02:00
Roberto Viola
6a16672ab1 fixing inclination calls #938 2022-09-15 08:23:42 +02:00
Roberto Viola
daa8dca053 Update main.cpp 2022-09-13 20:00:39 +02:00
Roberto Viola
21349a6e06 Rename consolereader.cpp to ConsoleReader.cpp 2022-09-13 09:34:42 +02:00
Roberto Viola
70dff0b78e Rename consolereader.h to ConsoleReader.h 2022-09-13 09:34:21 +02:00
Roberto Viola
7d88f9b211 Merge branch 'treadmill-gpio' of https://github.com/cagnulein/qdomyos-zwift into treadmill-gpio 2022-09-12 16:36:10 +02:00
Roberto Viola
7a58c776f9 Local input for headless mode #938 2022-09-12 16:35:52 +02:00
Roberto Viola
4f3051671e Merge branch 'master' into treadmill-gpio 2022-09-12 16:31:11 +02:00
Roberto Viola
ad4514a7e3 Local input for headless mode #938 2022-09-12 16:29:14 +02:00
Roberto Viola
f8f8e6c7ba Local input for headless mode #938 2022-09-12 16:28:08 +02:00
Roberto Viola
f195432020 Update bluetooth.cpp 2022-07-28 09:29:42 +02:00
Roberto Viola
283c1015f2 Merge branch 'master' into treadmill-gpio 2022-07-27 10:52:05 +02:00
Roberto Viola
8f41a2c8d8 added connectedAndDiscovered(); to gpiotreadmill
#525
2022-07-27 10:49:51 +02:00
Roberto Viola
6aacfe3cb0 Merge pull request #627 from december-soul/treadmill-gpio 2022-01-27 20:02:51 +01:00
decembersoul
477f58cf03 use semaphore to avoid press simultaneously the buttons 2022-01-27 19:13:00 +01:00
decembersoul
9d2ef5822d Merge branch 'treadmill-gpio' of https://github.com/december-soul/qdomyos-zwift into treadmill-gpio 2022-01-25 14:31:39 +01:00
decembersoul
8a56aab0ce add descructor to clean up thread 2022-01-25 14:26:18 +01:00
decembersoul
5e93ea947d Merge branch 'cagnulein:treadmill-gpio' into treadmill-gpio 2022-01-25 13:35:18 +01:00
decembersoul
8539fe91ea move GPIO worker into thread 2022-01-25 13:29:01 +01:00
Roberto Viola
4e81c60370 Merge pull request #587 from december-soul/treadmill-gpio 2022-01-16 18:32:04 +01:00
patrick
5aaa37ee02 Improve the setting of the speed and the slope 2022-01-16 18:08:04 +01:00
Roberto Viola
0aa1284898 fix gpio delay #525 2022-01-14 10:42:53 +01:00
Roberto Viola
d7e6cf51e9 fix build issue #525 2022-01-14 10:19:10 +01:00
Roberto Viola
ab8fe4f4a6 fix build issue #525 2022-01-14 09:58:59 +01:00
Roberto Viola
5c344a5a98 fixed zwift interface on gpiotreadmill 2022-01-13 19:41:57 +01:00
Roberto Viola
963becad7c first very raw release #525 2022-01-11 14:41:22 +01:00
Roberto Viola
5dd9547121 starting to create the gpio module 2022-01-05 23:52:49 +01:00
1149 changed files with 25752 additions and 135454 deletions

File diff suppressed because it is too large Load Diff

3
.gitignore vendored
View File

@@ -1,5 +1,3 @@
src/qdomyos-zwift.pro.user
.idea/
src/Makefile
@@ -52,4 +50,3 @@ src/inner_templates/googlemaps/cesium-key.js
.vscode/settings.json
/tst/Devices/.vs
src/inner_templates/googlemaps/cesium-key.js
src/qdomyos-zwift.pro.user.49de507

6
.gitmodules vendored
View File

@@ -5,10 +5,14 @@
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
tag = release-1.12.1
branch = tags/release-1.12.1
[submodule "src/qthttpserver"]
path = src/qthttpserver
url = https://github.com/qt-labs/qthttpserver

16
.vscode/launch.json vendored
View File

@@ -1,16 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.exe",
"symbolSearchPath": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/qdomyos-zwift.pdb",
"sourceFileMap": {
"d:/a/qdomyos-zwift/qdomyos-zwift": "c:/work/qdomyos-zwift/",
"compiled_source_path": "C://Users//violarob//Downloads//windows-msvc2019-binary-no-python (1)//output/"
}
}
]
}

374
CLAUDE.md
View File

@@ -1,374 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
QDomyos-Zwift is a Qt-based application that bridges fitness equipment (treadmills, bikes, ellipticals, rowers) with virtual training platforms like Zwift. It acts as a Bluetooth intermediary, connecting physical equipment to fitness apps while providing enhanced features like Peloton integration, power zone training, and workout programs.
## Build System & Commands
### Build Commands
```bash
# Build entire project (use subdirs TEMPLATE)
qmake
make
# Build specific configurations
qmake -r # Recursive build
make debug # Debug build
make release # Release build
# Clean build
make clean
make distclean
```
### Platform-Specific Builds
```bash
# Android
qmake -spec android-clang
make
# iOS
qmake -spec macx-ios-clang
make
# Windows (MinGW)
qmake -spec win32-g++
make
```
### Testing
```bash
# Build and run tests (requires main app built first)
cd tst
qmake
make
./qdomyos-zwift-tests
# Run with XML output for CI
GTEST_OUTPUT=xml:test-results/ GTEST_COLOR=1 ./qdomyos-zwift-tests
```
### No-GUI Mode
```bash
# Run application without GUI
sudo ./qdomyos-zwift -no-gui
```
## Architecture Overview
### Device Architecture
The application follows a hierarchical device architecture:
1. **Base Class**: `bluetoothdevice` - Abstract base for all fitness devices
- Manages Bluetooth connectivity via Qt's QLowEnergyController
- Defines common metrics (speed, cadence, heart rate, power, distance)
- Integrates with virtual devices for app connectivity
2. **Device Type Classes**: Inherit from `bluetoothdevice`
- `bike` - Bike-specific features (resistance, gears, power zones)
- `treadmill` - Treadmill features (speed control, inclination, pace)
- `elliptical` - Combined bike/treadmill features
- `rower` - Rowing metrics (stroke count, 500m pace)
- `stairclimber` - Step counting and climbing metrics
- `jumprope` - Jump sequence tracking
3. **Concrete Implementations**: Inherit from device type classes
- Located in `src/devices/[devicename]/` folders
- Examples: `domyosbike`, `pelotonbike`, `ftmsbike`
### Virtual Device System
- `virtualdevice` - Abstract base for virtual representations
- `virtualbike`, `virtualtreadmill`, etc. - Advertise to external apps
- Enables bidirectional communication between physical and virtual devices
### Bluetooth Management
- `bluetooth` class acts as device factory and connection manager
- `discoveryoptions` configures device discovery process
- Supports multiple connection types (Bluetooth LE, TCP, UDP)
## Key Development Areas
### Adding New Device Support
1. Create device folder in `src/devices/[devicename]/`
2. Implement device class inheriting from appropriate base type
3. Add device detection logic to `bluetooth.cpp`
4. Update `qdomyos-zwift.pri` with new source files
5. Add tests in `tst/Devices/` following existing patterns
### Characteristics & Protocols
- Bluetooth characteristics handlers in `src/characteristics/`
- FTMS (Fitness Machine Service) protocol support
- ANT+ integration for sensors
- Custom protocol implementations for specific brands
### UI & QML
- QML-based UI with Qt Quick Controls 2
- Main QML files in `src/` (main.qml, settings.qml, etc.)
- Platform-specific UI adaptations (iOS, Android, desktop)
### Integration Features
- Peloton workout/resistance integration (`peloton.cpp`)
- Zwift workout parsing (`zwiftworkout.cpp`)
- GPX file support for route following (`gpx.cpp`)
- Training program support (ZWO, XML formats)
## Platform-Specific Notes
### iOS
- Swift bridge files in `src/ios/`
- Apple Watch integration via `WatchKitConnection.swift`
- HealthKit integration for fitness data
- ConnectIQ SDK for Garmin devices
### Android
- Java bridge files in `src/android/src/`
- ANT+ integration via Android ANT SDK
- Foreground service for background operation
- USB serial support for wired connections
### Windows
- ADB integration for Nordic Track iFit devices
- PaddleOCR integration for Zwift workout detection
- Windows-specific networking features
## File Structure Patterns
### Device Files
```
src/devices/[devicename]/
├── [devicename].h # Header file
├── [devicename].cpp # Implementation
└── README.md # Device-specific documentation (optional)
```
### Test Files
```
tst/Devices/
├── DeviceTestData.h # Test data definitions
├── Test[DeviceName].h # Device-specific test cases
└── TestBluetooth.cpp # Main device detection test suite
```
## Testing Framework
- Uses Google Test (gtest) with Google Mock
- Comprehensive device detection testing
- Configuration-based test scenarios
- XML output support for CI/CD integration
- Tests must be built after main application (links against libqdomyos-zwift.a)
## Configuration & Settings
- Settings managed via `qzsettings.cpp` (QSettings wrapper)
- Platform-specific configuration paths
- Profile system for multiple users/devices
- Extensive customization options for device behavior
## External Dependencies
- Qt 5.15.2+ (Bluetooth, WebSockets, Charts, Quick, etc.)
- Google Test (submodule for testing)
- Platform SDKs (Android ANT+, iOS HealthKit, Windows ADB)
- Protocol Buffers for Zwift API integration
- MQTT client for IoT integration
- Various fitness platform APIs (Strava, Garmin Connect, etc.)
## Adding New ProForm Treadmill Models
This section provides a complete guide for adding new ProForm treadmill models to the codebase, based on the ProForm 995i implementation.
### Prerequisites
1. **Bluetooth Frame Capture File**: A file containing raw Bluetooth frames from the target treadmill
2. **Frame Analysis**: Understanding of which frames are initialization vs. sendPoll frames
3. **BLE Header Knowledge**: Each frame has an 11-byte BLE header that must be removed
### Step-by-Step Implementation Process
#### 1. Process Bluetooth Frames
First, process the raw Bluetooth frames by removing the first 11 bytes (BLE header) from each frame:
```bash
# Example: if you have "proform_model.c" with raw frames
# Process each frame by removing first 11 bytes
# Separate initialization frames from sendPoll frames
```
**Key Requirements:**
- Remove exactly 11 bytes from each frame (BLE header)
- Identify the boundary between initialization and sendPoll frames
- Initialization frames come first, sendPoll frames follow
- Document which packet number starts the sendPoll sequence
#### 2. Add Boolean Flag to Header File
Add the new model flag to `src/devices/proformtreadmill/proformtreadmill.h`:
```cpp
// Add before #ifdef Q_OS_IOS section
bool proform_treadmill_newmodel = false;
```
#### 3. Add Settings Support
Update the following files for settings integration:
**In `src/qzsettings.h`:**
```cpp
static const QString proform_treadmill_newmodel;
static constexpr bool default_proform_treadmill_newmodel = false;
```
**In `src/qzsettings.cpp`:**
```cpp
const QString QZSettings::proform_treadmill_newmodel = QStringLiteral("proform_treadmill_newmodel");
```
* Update the `allSettingsCount` in `qzsettings.cpp`
#### 4. Update QML Settings UI
**In `src/settings.qml`:**
1. Add property at the END of properties list:
```qml
property bool proform_treadmill_newmodel: false
```
2. Update ComboBox model array:
```qml
model: ["Disabled", "Proform New Model", ...]
```
3. Add case selection logic (find next available case number):
```qml
currentIndex: settings.proform_treadmill_newmodel ? XX : 0;
```
4. Add reset logic:
```qml
settings.proform_treadmill_newmodel = false;
```
5. Add switch case:
```qml
case XX: settings.proform_treadmill_newmodel = true; break;
```
#### 5. Implement Device Logic
**In `src/devices/proformtreadmill/proformtreadmill.cpp`:**
1. **Load Settings** (in constructor):
```cpp
proform_treadmill_newmodel = settings.value(QZSettings::proform_treadmill_newmodel, QZSettings::default_proform_treadmill_newmodel).toBool();
```
2. **Add Initialization Case** (in `btinit()` method):
```cpp
} else if (proform_treadmill_newmodel) {
// ALL initialization frames go here
uint8_t initData1[] = {0x00, 0xfe, 0x02, 0x08, 0x02};
writeCharacteristic(initData1, sizeof(initData1), QStringLiteral("init"), false, true);
// ... continue with ALL init frames from capture file
// Use frames from beginning until sendPoll boundary
}
```
3. **Add SendPoll Case** (in `sendPoll()` method):
```cpp
} else if (proform_treadmill_newmodel) {
switch (counterPoll) {
case 0:
// First sendPoll frame
break;
case 1:
// Second sendPoll frame
break;
// ... continue with pattern from sendPoll frames
default:
// Reset counter and cycle
counterPoll = -1;
break;
}
}
```
4. **Update Force Functions** - Add flag to conditional checks in `forceIncline()` and `forceSpeed()`:
```cpp
} else if (proform_treadmill_8_0 || ... || proform_treadmill_newmodel) {
write[14] = write[11] + write[12] + 0x12;
}
```
### Implementation Requirements
#### Frame Processing Rules
- **Exactly 11 bytes** must be removed from each frame (BLE header)
- **All initialization frames** must be included in the btinit() case
- **All sendPoll frames** must be included in the sendPoll() switch statement
- **Frame order** must be preserved exactly as captured
#### Settings Integration Rules
- **Property placement**: Always add new properties at the END of the properties list in settings.qml
- **Case numbering**: Find the next available case number in the ComboBox switch statement
- **Naming convention**: Use descriptive names following existing patterns
#### Code Organization Rules
- **Initialization**: All init frames go in btinit() method
- **Communication**: All sendPoll frames go in sendPoll() method with switch/case structure
- **Force functions**: Add new model flag to existing conditional chains
### Common Pitfalls and Solutions
#### Incorrect Byte Removal
- **Problem**: Removing wrong number of bytes (12 instead of 11)
- **Solution**: Always remove exactly 11 bytes (BLE header)
#### Wrong SendPoll Boundary
- **Problem**: Using initialization frames in sendPoll logic
- **Solution**: Identify exact packet number where sendPoll starts
#### Incomplete Initialization
- **Problem**: Missing initialization frames
- **Solution**: Include ALL frames from start until sendPoll boundary
#### Settings Placement
- **Problem**: Adding property in wrong location in settings.qml
- **Solution**: Always add at END of properties list
### Verification Checklist
- [ ] All 11 bytes removed from each frame
- [ ] Initialization frames correctly identified and included
- [ ] SendPoll frames correctly identified and implemented
- [ ] Settings properly integrated in all required files
- [ ] ComboBox updated with new model option
- [ ] Force functions updated with new model flag
- [ ] Property added at END of settings.qml properties list
### Example Reference
The ProForm 995i implementation serves as the reference example:
- 25 initialization frames (pkt4658-pkt4756)
- 33 sendPoll frames (pkt4761-pkt4897)
- 6-case sendPoll switch statement with cycling logic
- Complete settings integration across all required files
## Development Tips
- Use Qt Creator for development with proper project file support
- The project uses Qt's signal/slot mechanism extensively
- Device implementations should follow existing patterns for consistency
- Add comprehensive logging using the project's logging framework
- Test device detection thoroughly using the existing test infrastructure
- Consider platform differences when adding new features
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list

View File

@@ -1,53 +0,0 @@
#include <NimBLEDevice.h>
#define INDOOR_BIKE_DATA_UUID "00002AD2-0000-1000-8000-00805f9b34fb"
#define CUSTOM_SERVICE_UUID "ce060000-43e5-11e4-916c-0800200c9a66"
NimBLEServer* pServer = nullptr;
NimBLECharacteristic* pIndoorBikeDataChar = nullptr;
class ServerCallbacks: public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) {
Serial.println("Client connected");
};
void onDisconnect(NimBLEServer* pServer) {
Serial.println("Client disconnected");
}
};
void setup() {
Serial.begin(115200);
Serial.println("Starting NimBLE Server");
NimBLEDevice::init("PM5 431431183 Row");
pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
NimBLEService* pFtmService = pServer->createService("1826");
//NimBLEService* pCustomService = pServer->createService(CUSTOM_SERVICE_UUID);
pIndoorBikeDataChar = pFtmService->createCharacteristic(
INDOOR_BIKE_DATA_UUID,
NIMBLE_PROPERTY::READ |
NIMBLE_PROPERTY::NOTIFY
);
pFtmService->start();
//pCustomService->start();
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(pFtmService->getUUID());
//pAdvertising->addServiceUUID(CUSTOM_SERVICE_UUID);
const std::string data = { 0x01, 0x10, 0x00 }; // Imposta i valori desiderati
pAdvertising->setServiceData(pFtmService->getUUID(), data);
pAdvertising->start();
Serial.println("Advertising started");
}
void loop() {
// Metti qui il tuo codice principale, da eseguire ripetutamente
// Ad esempio, potresti aggiornare il valore della caratteristica Indoor Bike Data
}

View File

@@ -96,36 +96,34 @@ Zwift bridge for Treadmills and Bike!
|:---|:---:|:---:|:---:|:---:|---:|
|Resistance shifting with bluetooth remote|X||X|||
|TTS support|X|X|X|X||
|Zwift Play & Click support|X|||||
|MQTT integration|X|X|X|X||
|OpenSoundControl integration|X|X|X|X||
### Installation
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
You can install it on multiple platforms.
Read the [installation procedure](docs/10_Installation.md)
### Tested on
The QDomyos-Zwift application can run on [Macintosh or Linux devices](docs/10_Installation.md) iOS, and Android.
It supports any [FTMS-compatible application](docs/20_supported_devices_and_applications.md) software and most [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
You can run the app on [Macintosh or Linux devices](docs/10_Installation.md). IOS and Android are also supported.
QDomyos-Zwift works on every [FTMS-compatible application](docs/20_supported_devices_and_applications.md), and virtually any [bluetooth enabled device](docs/20_supported_devices_and_applications.md).
### No GUI version
run as
$ sudo ./qdomyos-zwift -no-gui
$ sudo ./qdomyos-zwift -no-gui
### Reference
=> GitHub Repository: [QDomyos-Zwift on GitHub](https://github.com/ProH4Ck/treadmill-bridge)
https://github.com/ProH4Ck/treadmill-bridge
=> Treadmill Incline Reference: [What Is 10 Degrees in Incline on a Treadmill?](https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/)
https://www.livestrong.com/article/422012-what-is-10-degrees-in-incline-on-a-treadmill/
=> Icon Attribution: Icons used in this documentation are from [Flaticon.com](https://www.flaticon.com)
Icons used in this documentation come from [flaticon.com](https://www.flaticon.com)
### Blog
=> Related Blog: [Roberto Viola's Blog](https://robertoviola.cloud)
https://robertoviola.cloud

View File

@@ -1,84 +0,0 @@
//
// 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

@@ -1,18 +0,0 @@
//
// 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

@@ -1,54 +0,0 @@
//
// 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

@@ -1,174 +0,0 @@
//
// 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

@@ -1,41 +0,0 @@
//
// 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

@@ -107,7 +107,6 @@ extension MainController: WorkoutTrackingDelegate {
WorkoutTracking.speed = WatchKitConnection.speed
WorkoutTracking.power = WatchKitConnection.power
WorkoutTracking.cadence = WatchKitConnection.cadence
WorkoutTracking.steps = WatchKitConnection.steps
if Locale.current.measurementSystem != "Metric" {
self.distanceLabel.setText("Distance \(String(format:"%.2f", WorkoutTracking.distance))")

View File

@@ -23,13 +23,10 @@ class WatchKitConnection: NSObject {
static let shared = WatchKitConnection()
public static var distance = 0.0
public static var kcal = 0.0
public static var totalKcal = 0.0
public static var stepCadence = 0
public static var speed = 0.0
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() {
@@ -72,9 +69,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.distance = dDistance
let dKcal = Double(result["kcal"] as! Double)
WatchKitConnection.kcal = dKcal
if let totalKcalDouble = result["totalKcal"] as? Double {
WatchKitConnection.totalKcal = totalKcalDouble
}
let dSpeed = Double(result["speed"] as! Double)
WatchKitConnection.speed = dSpeed
@@ -82,17 +76,6 @@ extension WatchKitConnection: WatchKitConnectionProtocol {
WatchKitConnection.power = dPower
let dCadence = Double(result["cadence"] as! Double)
WatchKitConnection.cadence = dCadence
if let stepsDouble = result["steps"] as? Double {
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

@@ -28,27 +28,24 @@ class WorkoutTracking: NSObject {
static let shared = WorkoutTracking()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var cadenceTimeStamp = NSDate().timeIntervalSince1970
public static var cadenceLastSteps = Double()
public static var cadenceSteps = 0
public static var speed = Double()
public static var power = Double()
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 {
@@ -56,26 +53,20 @@ extension WorkoutTracking {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .distanceCycling):
let distanceUnit = HKUnit.mile()
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: distanceUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitDistanceCycling(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
let energyUnit = HKUnit.kilocalorie()
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: energyUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitActiveEnergyBurned(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
guard let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) else {
return
}
let roundedValue = Double( round( 1 * value ) / 1 )
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
delegate?.didReceiveHealthKitHeartRate(roundedValue)
case HKQuantityType.quantityType(forIdentifier: .stepCount):
@@ -168,7 +159,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -178,7 +168,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .runningVerticalOscillation)!,
HKSampleType.quantityType(forIdentifier: .walkingSpeed)!,
HKSampleType.quantityType(forIdentifier: .walkingStepLength)!,
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
HKSampleType.workoutType()
])
} else {
@@ -189,8 +178,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
HKSampleType.workoutType()
])
}
@@ -209,8 +196,6 @@ 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())
@@ -231,30 +216,23 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutSession.stopActivity(with: Date())
workoutSession.end()
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
guard let quantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: workoutSession.startDate!,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
}
}
workoutBuilder.add([sample]) {(success, error) in}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance
let quantityMiles = HKQuantity(unit: unitDistance,
@@ -270,7 +248,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -286,148 +264,34 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
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")
}
}
}
} else if(sport == 4) { // Rowing
// Guard to check if steps quantity type is available
guard let quantityTypeSteps = HKQuantityType.quantityType(
forIdentifier: .stepCount) else {
return
}
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
// Create a sample for total steps
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
quantity: stepsQuantity,
start: startDate,
end: Date())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
if let error = error {
print(error)
}
}
// Per il rowing, HealthKit utilizza un tipo specifico di distanza
// Se non esiste un tipo specifico per il rowing, possiamo usare un tipo generico di distanza
var quantityTypeDistance: HKQuantityType?
// In watchOS 10 e versioni successive, possiamo usare un tipo specifico se disponibile
if #available(watchOSApplicationExtension 10.0, *) {
// Verifica se esiste un tipo specifico per il rowing, altrimenti utilizza un tipo generico
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)
} else {
// Nelle versioni precedenti, usa il tipo generico
quantityTypeDistance = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)
}
guard let typeDistance = quantityTypeDistance else {
return
}
let sampleDistance = HKCumulativeQuantitySeriesSample(type: typeDistance,
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")
}
}
}
} else {
// Guard to check if steps quantity type is available
guard let quantityTypeSteps = HKQuantityType.quantityType(
forIdentifier: .stepCount) else {
return
}
let stepsQuantity = HKQuantity(unit: HKUnit.count(), doubleValue: Double(WorkoutTracking.steps))
// Create a sample for total steps
let sampleSteps = HKCumulativeQuantitySeriesSample(
type: quantityTypeSteps,
quantity: stepsQuantity, // Use your steps quantity here
start: startDate,
end: Date())
// Guard to check if distance quantity type is available
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
return
}
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.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
workoutBuilder.add([sampleDistance]) {(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 metrics
self.workoutBuilder.finishWorkout { (workout, error) in
self.workoutBuilder.finishWorkout{ (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(stepsQuantity, forKey: "totalSteps")
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
}
}
}
@@ -442,7 +306,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
@@ -452,7 +316,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)
@@ -538,7 +402,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 1) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
doubleValue: WorkoutTracking.power)
@@ -581,7 +445,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 2) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
doubleValue: WorkoutTracking.speed * 0.277778)

View File

@@ -1,4 +1,4 @@
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia sql
QT += gui bluetooth widgets xml positioning quick networkauth websockets texttospeech location multimedia
QTPLUGIN += qavfmediaplayer
QT+= charts

View File

@@ -1,96 +0,0 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
tigervnc-standalone-server \
tigervnc-tools \
libgl1-mesa-dri \
xfonts-base \
x11-xserver-utils \
tigervnc-common \
net-tools \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# VNC configuration
RUN mkdir -p ~/.vnc && \
echo "securepassword" | vncpasswd -f > ~/.vnc/passwd && \
chmod 600 ~/.vnc/passwd
# .Xauthority configuration
RUN touch /root/.Xauthority
ENV DISPLAY=:99
# Start VNC server with authentication
CMD vncserver :99 -depth 24 -localhost no -xstartup qdomyos-zwift && \
sleep infinity

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker build -t qdomyos-zwift-vnc .

View File

@@ -1,10 +0,0 @@
services:
qdomyos-zwift-vnc:
image: qdomyos-zwift-vnc
container_name: qdomyos-zwift-vnc
privileged: true # Required for Bluetooth functionality
network_mode: "host" # Used to access host Bluetooth and D-Bus
volumes:
- /dev:/dev # Forward host devices (for Bluetooth)
- /run/dbus:/run/dbus # Forward D-Bus for Bluetooth interaction
restart: "no" # Do not restart the container automatically

View File

@@ -1,95 +0,0 @@
# Define build image
FROM ubuntu:latest AS build
# Install essential build dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
git \
ca-certificates \
qtquickcontrols2-5-dev \
qtconnectivity5-dev \
qtbase5-private-dev \
qtpositioning5-dev \
libqt5charts5-dev \
libqt5networkauth5-dev \
libqt5websockets5-dev \
qml-module* \
libqt5texttospeech5-dev \
qtlocation5-dev \
qtmultimedia5-dev \
g++ \
make \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Define runtime image
FROM ubuntu:latest AS runtime
# Install essential runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt upgrade -y \
&& apt install --no-install-recommends -y \
libqt5bluetooth5 \
libqt5widgets5 \
libqt5positioning5 \
libqt5xml5 \
libqt5charts5 \
qt5-assistant \
libqt5networkauth5 \
libqt5websockets5 \
qml-module* \
libqt5texttospeech5 \
libqt5location5-plugins \
libqt5multimediawidgets5 \
libqt5multimedia5-plugins \
libqt5multimedia5 \
qml-module-qtquick-controls2 \
libqt5location5 \
bluez \
dbus \
&& rm -rf /var/lib/apt/lists/*
# Stage 1: Build
FROM build AS builder
# Define variables for Qt versions
ARG QT_VERSION=5.15
ARG QT_SUBVERSION=5.15.13
ARG QT_WEBPLUGIN_NAME=qtwebglplugin-everywhere-opensource-src
# Build WebGL plugin
WORKDIR /usr/local/src
RUN wget https://download.qt.io/official_releases/qt/${QT_VERSION}/${QT_SUBVERSION}/submodules/${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& unzip ${QT_WEBPLUGIN_NAME}-${QT_SUBVERSION}.zip \
&& mv *-${QT_SUBVERSION} qtwebglplugin-everywhere \
&& cd qtwebglplugin-everywhere \
&& qmake \
&& make
# Clone the project and build it
WORKDIR /usr/local/src
RUN git clone --recursive https://github.com/cagnulein/qdomyos-zwift.git
WORKDIR /usr/local/src/qdomyos-zwift
RUN git submodule update --init src/smtpclient/ \
&& git submodule update --init src/qmdnsengine/ \
&& git submodule update --init tst/googletest/
WORKDIR /usr/local/src/qdomyos-zwift/src
RUN qmake qdomyos-zwift.pro \
&& make -j4
# Stage 2: Runtime
FROM runtime
# Copy the built binary to /usr/local/bin
COPY --from=builder /usr/local/src/qdomyos-zwift/src/qdomyos-zwift /usr/local/bin/qdomyos-zwift
# Copy WebGL plugin to the appropriate location
COPY --from=builder /usr/local/src/qtwebglplugin-everywhere/plugins/platforms/libqwebgl.so /usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqwebgl.so
# Set the default command to run the application with WebGL
CMD ["qdomyos-zwift", "-qml", "-platform", "webgl:port=8080"]

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker build -t qdomyos-zwift-webgl .

View File

@@ -1,19 +0,0 @@
services:
qdomyos-zwift-webgl:
image: qdomyos-zwift-webgl
container_name: qdomyos-zwift-webgl
privileged: true
network_mode: "host"
environment:
- DISPLAY=${DISPLAY}
volumes:
- /dev:/dev
- /run/dbus:/run/dbus
- ./.config:/root/.config
- /tmp/.X11-unix:/tmp/.X11-unix
stdin_open: true
tty: true
restart: "no"
# command: qdomyos-zwift -qml -platform webgl:port=8080
# command: ["qdomyos-zwift", "-no-gui"]

View File

@@ -9,8 +9,8 @@ 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 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
$ 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 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
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
$ git submodule update --init src/smtpclient/
@@ -34,15 +34,16 @@ 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 Pi bridge.
This guide will walk you through steps to setup an autonomous, headless raspberry 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, [`Raspberry Pi OS Lite 64bit`](https://www.raspberrypi.com/software/operating-systems/). Boot up the Raspberry Pi (default credentials are pi/raspberry)
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)
#### Change default credentials
@@ -55,7 +56,7 @@ Get the latest [Raspberry Pi Imager](https://www.raspberrypi.org/software/) and
`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.
@@ -76,7 +77,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`
@@ -85,17 +86,15 @@ 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
Qdomyos-zwift can be compiled from source (hard), or using a binary (easy). **Only one is required**.
#### Update your Raspberry (mandatory !)
#### Update your raspberry (mandatory !)
Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
@@ -104,10 +103,10 @@ Before installing qdomyos-zwift, let's ensure we have an up-to-date system.
This operation takes a moment to complete.
#### Option 1. Install qdomyos-zwift from sources
#### 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
sudo apt install git libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-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
git clone https://github.com/cagnulein/qdomyos-zwift.git
cd qdomyos-zwift
git submodule update --init src/smtpclient/
@@ -118,126 +117,24 @@ qmake qdomyos-zwift.pro
make
```
If you need GUI also do a
```
apt install qml-module*
```
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
`sudo ./qdomyos-zwift-64bit -no-gui -heart-service`
`./qdomyos-zwift -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. **Update ExecStart with the path and full name with commandline options for your qz binary. Update ExecStop with the full name of the binary.**
Let's create a systemd service that we'll enable at boot sequence.
`sudo vi /lib/systemd/system/qz.service`
@@ -275,155 +172,10 @@ If everything is working as expected, **enable your service at boot time** :
Then reboot to check operations (`sudo reboot`)
### (optional) Treadmill Auto-Detection and Service Management
This section provides a reliable way to manage the QZ service based on the treadmill's power state. Using a `bluetoothctl`-based Bash script, this solution ensures the QZ service starts when the treadmill is detected and stops when it is not.
- **Bluetooth Discovery**: Monitors treadmill availability via `bluetoothctl`.
- **Service Control**: Automatically starts and stops the QZ service.
- **Logging**: Tracks treadmill status and actions in a log file.
**Notes:**
- Ensure `bluetoothctl` is installed and working on your system.
- Replace `I_TL` in the script with your treadmill's Bluetooth name. You can find your device name via `bluetoothctl scan on`
- Adjust the sleep interval (`sleep 30`) in the script as needed for your use case.
Step 1: Save the following script as `/root/qz-treadmill-monitor.sh`:
```bash
#!/bin/bash
LOG_FILE="/var/log/qz-treadmill-monitor.log"
TARGET_DEVICE="I_TL"
SCAN_INTERVAL=30 # Time in seconds between checks
SERVICE_NAME="qz"
DEBUG_LOG_DIR="/var/log" # Directory where QZ debug logs are stored
ERROR_MESSAGE="BTLE stateChanged InvalidService"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
is_service_running() {
systemctl is-active --quiet "$SERVICE_NAME"
return $?
}
scan_for_device() {
log "Starting Bluetooth scan for $TARGET_DEVICE..."
# Run bluetoothctl scan in the background and capture output
bluetoothctl scan on &>/dev/null &
SCAN_PID=$!
# Allow some time for devices to appear
sleep 5
# Check if the target device appears in the list
bluetoothctl devices | grep -q "$TARGET_DEVICE"
DEVICE_FOUND=$?
# Stop scanning
kill "$SCAN_PID"
bluetoothctl scan off &>/dev/null
if [ $DEVICE_FOUND -eq 0 ]; then
log "Device '$TARGET_DEVICE' found."
return 0
else
log "Device '$TARGET_DEVICE' not found."
return 1
fi
}
restart_qz_on_error() {
# Get the current date
CURRENT_DATE=$(date '+%a_%b_%d')
# Find the latest QZ debug log file for today
LATEST_LOG=$(ls -t "$DEBUG_LOG_DIR"/debug-"$CURRENT_DATE"_*.log 2>/dev/null | head -n 1)
if [ -z "$LATEST_LOG" ]; then
log "No QZ debug log found for today."
return 0
fi
log "Checking latest log file: $LATEST_LOG for errors..."
# Search the latest log for the error message
if grep -q "$ERROR_MESSAGE" "$LATEST_LOG"; then
log "***** Error detected in QZ log: $ERROR_MESSAGE *****"
log "Restarting QZ service..."
systemctl restart "$SERVICE_NAME"
else
log "No errors detected in $LATEST_LOG."
fi
}
manage_service() {
local device_found=$1
if $device_found; then
if ! is_service_running; then
log "***** Starting QZ service... *****"
systemctl start "$SERVICE_NAME"
else
log "QZ service is already running."
restart_qz_on_error # Check the log for errors when QZ is already running
fi
else
if is_service_running; then
log "***** Stopping QZ service... *****"
systemctl stop "$SERVICE_NAME"
else
log "QZ service is already stopped."
fi
fi
}
while true; do
log "Checking for treadmill status..."
if scan_for_device; then
manage_service true
else
manage_service false
fi
log "Waiting for $SCAN_INTERVAL seconds before next check..."
sleep "$SCAN_INTERVAL"
done
```
Step2: To ensure the script runs continuously, create a systemd service file at `/etc/systemd/system/qz-treadmill-monitor.service`
```bash
[Unit]
Description=QZ Treadmill Monitor Service
After=bluetooth.service
[Service]
Type=simple
ExecStart=/root/qz-treadmill-monitor.sh
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
```
Step 3: Enable and Start the Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable qz-treadmill-monitor
sudo systemctl start qz-treadmill-monitor
```
Monitor logs are written to `/var/log/qz-treadmill-monitor.log`. Use the following command to check logs in real-time:
```bash
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.
@@ -448,19 +200,7 @@ 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.
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.
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.

View File

@@ -8,21 +8,23 @@ The testing project tst/qdomyos-zwift-tests.pro contains test code that uses the
New devices are added to the main QZ application by creating or modifying a subclass of the bluetoothdevice class.
At minimum, each device has a corresponding BluetoothDeviceTestData object constructed in the DeviceTestDataIndex class in the test project, which is coded to provide information to the test framework to generate tests for device detection and potentially other things.
At minimum, each device has a corresponding BluetoothDeviceTestData subclass in the test project, which is coded to provide information to the test framework to generate tests for device detection and potentially other things.
In the test project
* add a new device name constant to the DeviceIndex class.
* locate the implementation of DeviceTestDataindex::Initialize and build the test data from a call to DeviceTestDataIndex::RegisterNewDeviceTestData(...)
* pass the device name constant defined in the DeviceIndex class to the call to DeviceTestDataIndex::RegisterNewDeviceTestData(...).
* create a new folder for the device under tst/Devices. This is for anything you define for testing this device.
* add a new class with header file and optionally .cpp file to the project in that folder. Name the class DeviceNameTestData, substituting an appropriate name in place of "DeviceName".
* edit the header file to inherit the class from the BluetoothDeviceTestData abstract subclass appropriate to the device type, i.e. BikeTestData, RowerTestData, EllipticalTestData, TreadmillTestData.
* have this new subclass' constructor pass a unique test name to its superclass.
The tests are not organised around real devices that are handled, but the bluetoothdevice subclass that handles them - the "driver" of sorts.
You need to provide the following:
- patterns for valid names (e.g. equals a value, starts with a value, case sensitivity, specific length)
- invalid names to ensure the device is not identified when the name is invalid
- configuration settings that are required for the device to be detected, including bluetooth device information configuration
- configuration settings that are required for the device to be detected
- invalid configurations to test that the device is not detected, e.g. when it's disabled in the settings, but the name is correct
- exclusion devices: for example if a device with the same name but of a higher priority type is detected, this device should not be detected
- exclusion devices: if a device with the same name but of a higher priority type is detected, this device should not be detected
- valid and invalid QBluetoothDeviceInfo configurations, e.g. to check the device is only detected when the manufacturer data is set correctly, or certain services are available or not.
## Tools in the Test Framework
@@ -37,18 +39,16 @@ i.e. a test will
### DeviceDiscoveryInfo
This class:
* stores values for a specific subset of the QZSettings keys.
* provides methods to read and write the values it knows about from and to a QSettings object.
* provides a QBluetoothDeviceInfo object configured with the device name currently being tested.
This class contains a set of fields that store strongly typed QSettings values.
It also provides methods to read and write the values it knows about from and to a QSettings object.
It is used in conjunction with a TestSettings object to write a configuration during a test.
## Writing a device detection test
Because of the way the BluetoothDeviceTestDataBuilder currently works, it may be necessary to define multiple test data objects to cover the various cases.
For example, if any of a list of names is enough to identify a device, or another group of names but with a certain service in the bluetooth device info, that will require multiple test data objects.
Because of the way the TestData classes currently work, it may be necessary to define multiple test data classes to cover the various cases.
For example, if any of a list of names is enough to identify a device, or another group of names but with a certain service in the bluetooth device info, that will require multiple classes.
### Recognition by Name
@@ -68,82 +68,132 @@ Reading this, to identify this device:
In this case, we are not testing the last two, but can test the first two.
In deviceindex.h:
```
static const QString DomyosBike;
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/domyosbike/domyosbike.h"
class DomyosBikeTestData : public BikeTestData {
public:
DomyosBikeTestData() : BikeTestData("Domyos Bike") {
this->addDeviceName("Domyos-Bike", comparison::StartsWith);
this->addInvalidDeviceName("DomyosBridge", comparison::StartsWith);
}
// not used yet
deviceType get_expectedDeviceType() const override { return deviceType::DomyosBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<domyosbike*>(detectedDevice)!=nullptr;
}
};
```
In deviceindex.cpp:
The constructor adds a valid device name, and an invalid one. Various overloads of these methods and other members of the comparison enumeration provide other capabilities for specifying test data. If you add a valid device name that says the name should start with a value, additional names will be added automatically to the valid list with additional characters to test that it is in fact a "starts with" relationship. Also, valid and invalid names will be generated base on whether the comparison is case sensitive or not.
```
DEFINE_DEVICE(DomyosBike, "Domyos Bike");
```
The get_expectedDeviceType() function is not actually used and is part of an unfinished refactoring of the device detection code, whereby the bluetoothdevice object doesn't actually get created intially. You could add a new value to the deviceType enum and return that, but it's not used yet. There's always deviceType::None.
This pair adds the "friendly name" for the device as a constant, and also adds the key/value pair to an index.
In DeviceTestDataIndex::Initialize():
```
// Domyos bike
RegisterNewDeviceTestData(DeviceIndex::DomyosBike)
->expectDevice<domyosbike>()
->acceptDeviceName("Domyos-Bike", DeviceNameComparison::StartsWith)
->rejectDeviceName("DomyosBridge", DeviceNameComparison::StartsWith);
```
This set of instructions adds a valid device name, and an invalid one. Various overloads of these methods, other methods, and other members of the comparison enumeration provide other capabilities for specifying test data. If you add a valid device name that says the name should start with a value, additional names will be added automatically to the valid list with additional characters to test that it is in fact a "starts with" relationship. Also, valid and invalid names will be generated based on whether the comparison is case sensitive or not.
The get_isExpectedDevice(bluetoothdevice *) function must be overridden to indicate if the specified object is of the type expected for this test data.
### Configuration Settings
Consider the CompuTrainer bike. This device is not detected by name, but only by whether or not it is enabled in the settings.
To specify this in the test data, we use one of the BluetoothDeviceTestData::configureSettingsWith(...) methods, the one for the simple case where there is a single QZSetting with a specific enabling and disabling value.
Consider the CompuTrainerTestData. This device is not detected by name, but only by whether or not it is enabled in the settings.
To specify this in the test data, we override one of the configureSettings methods, the one for the simple case where there is a single valid and a single invalid configuration.
Settings from QSettings that contribute to tests should be put into the DeviceDiscoveryInfo class.
For example, for the Computrainer Bike, the "computrainer_serialport" value from the QSettings determines if the bike should be detected or not.
For example, for the Computrainer Bike, the "computrainer_serial_port" value from the QSettings determines if the bike should be detected or not.
The computrainer_serialport QZSettings key should be registered in devicediscoveryinfo.cpp
In devicediscoveryinfo.cpp:
```
void InitializeTrackedSettings() {
class DeviceDiscoveryInfo {
public :
...
QString computrainer_serial_port = nullptr;
...
}
```
trackedSettings.insert(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport);
The getValues and setValues methods should be updated to include the addition(s):
```
void DeviceDiscoveryInfo::setValues(QSettings &settings, bool clear) const {
if(clear) settings.clear();
...
settings.setValue(QZSettings::computrainer_serialport, this->computrainer_serial_port);
...
}
void DeviceDiscoveryInfo::getValues(QSettings &settings){
...
this->computrainer_serial_port = settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
...
}
```
For this test data,
* if enabling configurations are requested, the computrainer_serialport setting will be populated with "COMX"
* if disabling configurations are requested, the computrainer_serialport setting will be populated with ""
In the following example, the DeviceDiscoveryInfo class has been updated to contain the device's configuration setting (computrainer_serial_port).
- if an enabling configuration is requested (enable==true) a string that is known to be accepted is supplied
- if a disabling configuration is requested (enable==false) an empty string is supplied.
DeviceTestDataIndex::Initialize():
This example uses the simpler of 2 configureSettings methods returns true/false to indicate if the configuration should be used for the test.
```
// Computrainer Bike
RegisterNewDeviceTestData(DeviceIndex::ComputrainerBike)
->expectDevice<computrainerbike>()
->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::computrainer_serialport, "COMX", "");
```
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/computrainerbike/computrainerbike.h"
class CompuTrainerTestData : public BikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
info.computrainer_serial_port = enable ? "X":QString();
return true;
}
public:
CompuTrainerTestData() : BikeTestData("CompuTrainer Bike") {
// any name
this->addDeviceName("", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::CompuTrainerBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<computrainerbike*>(detectedDevice)!=nullptr;
}
};
```
Similarly, the Pafers Bike has a simple configuration setting:
```
// Pafers Bike
RegisterNewDeviceTestData(DeviceIndex::PafersBike)
->expectDevice<pafersbike>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::pafers_treadmill,false);
```
#include "Devices/Bike/biketestdata.h"
#include "devices/pafersbike/pafersbike.h"
In that case, ```configureSettingsWith(QZSettings::pafers_treadmill,false)``` indicates that the pafers_treadmill setting will be false for enabling configurations and true for disabling ones.
class PafersBikeTestData : public BikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
// the treadmill is given priority
info.pafers_treadmill = !enable;
return true;
}
public:
PafersBikeTestData() : BikeTestData("Pafers Bike") {
this->addDeviceName("PAFERS_", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::PafersBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<pafersbike*>(detectedDevice)!=nullptr;
}
};
```
A more complicated example is the Pafers Treadmill. It involves a name match, but also some configuration settings obtained earlier...
@@ -162,60 +212,76 @@ bool pafers_treadmill_bh_iboxster_plus =
```
Here the device could be activated due to a name match and various combinations of settings.
For this, the configureSettingsWith(...) function that takes a lambda function which consumes a vector of DeviceDiscoveryInfo objects which is populated with configurations that lead to the specified result (enable = detected, !enable=not detected).
For this, the configureSettings function that takes a vector of DeviceDiscoveryInfo objects which is populated with configurations that lead to the specified result (enable = detected, !enable=not detected). Instead of returning a boolean to indicate if a configuration has been supplied, it populates a vector of DeviceDiscoveryInfo objects.
```
// Pafers Treadmill
RegisterNewDeviceTestData(DeviceIndex::PafersTreadmill)
->expectDevice<paferstreadmill>()
->acceptDeviceName("PAFERS_", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith( [](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void {
DeviceDiscoveryInfo config(info);
#pragma once
if (enable) {
for(int x = 1; x<=3; x++) {
config.setValue(QZSettings::pafers_treadmill, x & 1);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, x & 2);
configurations.push_back(config);
}
} else {
config.setValue(QZSettings::pafers_treadmill, false);
config.setValue(QZSettings::pafers_treadmill_bh_iboxster_plus, false);
configurations.push_back(config);
}
});
#include "Devices/Treadmill/treadmilltestdata.h"
#include "devices/paferstreadmill/paferstreadmill.h"
class PafersTreadmillTestData : public TreadmillTestData {
protected:
void configureSettings(const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations) const override {
DeviceDiscoveryInfo config(info);
if (enable) {
for(int x = 1; x<=3; x++) {
config.pafers_treadmill = x & 1;
config.pafers_treadmill_bh_iboxster_plus = x & 2;
configurations.push_back(config);
}
} else {
config.pafers_treadmill = false;
config.pafers_treadmill_bh_iboxster_plus = false;
configurations.push_back(config);
}
}
public:
PafersTreadmillTestData() : TreadmillTestData("Pafers Treadmill") {
this->addDeviceName("PAFERS_", comparison::StartsWithIgnoreCase);
}
deviceType get_expectedDeviceType() const override { return deviceType::PafersTreadmill; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<paferstreadmill*>(detectedDevice)!=nullptr;
}
};
```
### Considering Extra QBluetoothDeviceInfo Content
Detection of some devices requires some specific bluetooth device information.
Supplying enabling and disabling QBluetoothDeviceInfo objects is done by accessing the QBluetoothDeviceInfo member of the DeviceDiscoveryInfo object.
For example, the M3iBike requires specific manufacturer information, using the simpler of the lambda functions accepted by the configureSettingsWith function.
Supplying enabling and disabling QBluetoothDeviceInfo objects is done using a similar pattern to the multiple configurations scenario.
For example, the M3iBike requires specific manufacturer information.
```
// M3I Bike
RegisterNewDeviceTestData(DeviceIndex::M3IBike)
->expectDevice<m3ibike>()
->acceptDeviceName("M3", DeviceNameComparison::StartsWith)
->configureSettingsWith(
[](DeviceDiscoveryInfo& info, bool enable)->void
{
// The M3I bike detector looks into the manufacturer data.
if(!enable) {
info.DeviceInfo()->setManufacturerData(1, QByteArray("Invalid manufacturer data."));
return;
}
void M3IBikeTestData::configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const {
// The M3I bike detector looks into the manufacturer data.
int key=0;
info.DeviceInfo()->setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
});
QBluetoothDeviceInfo result = info;
if(!enable) {
result.setManufacturerData(1, QByteArray("Invalid manufacturer data."));
bluetoothDeviceInfos.push_back(result);
return;
}
int key=0;
result.setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
bluetoothDeviceInfos.push_back(result);
}
```
The test framework populates the incoming QBluetoothDeviceInfo object with a UUID and the name (generated from the acceptDeviceName and rejectDeviceName calls) currently being tested.
This is expected to have nothing else defined.
Another example is one of the test data definitions for detecting a device that uses the stagesbike class:
The test framework populates the incoming QBluetoothDeviceInfo object with a name and a UUID. This is expected to have nothing else defined.
Another example is one of the test data classes for detecting a device that uses the statesbike class:
Detection code from bluetooth.cpp:
@@ -223,49 +289,37 @@ Detection code from bluetooth.cpp:
((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
```
This condition is actually extracted from a more complicated example where the BluetoothDeviceTestData class can't cover all the detection criteria with one instance.
This condition is actually extracted from a more complicated example where the current test data classes can't cover all the detection criteria in one implementation. This is why this class inherits from StagesBikeTestData rather than BikeTestData directly.
```
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
class StagesBike3TestData : public StagesBikeTestData {
protected:
void configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const override {
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
//
// ... other stages bike variants
//
if(enable) {
QBluetoothDeviceInfo result = info;
result.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818)}));
bluetoothDeviceInfos.push_back(result);
} else {
QBluetoothDeviceInfo hasInvalid = info;
hasInvalid.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1826)}));
QBluetoothDeviceInfo hasBoth = hasInvalid;
hasBoth.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818),QBluetoothUuid((quint16)0x1826)}));
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
bluetoothDeviceInfos.push_back(info); // has neither
bluetoothDeviceInfos.push_back(hasInvalid);
bluetoothDeviceInfos.push_back(hasBoth);
}
}
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
public:
StagesBike3TestData() : StagesBikeTestData("Stages Bike (KICKR CORE)") {
this->addDeviceName("KICKR CORE", comparison::StartsWithIgnoreCase);
}
};
```
In this case, it populates the vector with the single enabling configuration if that's what's been requested, otherwise 3 disabling ones.
@@ -274,7 +328,7 @@ In this case, it populates the vector with the single enabling configuration if
Sometimes there might be ambiguity when multiple devices are available, and the detection code may specify that if the other conditions match, but certain specific kinds of devices (the exclusion devices) have already been detected, the newly matched device should be ignored.
The test data object can be made to cover this by calling the excluding(...) functions to add type identifiers for the bluetoothdevice classes for the exclusion devices to the object's internal list of exclusions.
The TestData class can be made to cover this by overriding the configureExclusions() method to add instances of the TestData classes for the exclusion devices to the object's internal list of exclusions.
Detection code:
@@ -282,19 +336,39 @@ Detection code:
} else if (b.name().startsWith(QStringLiteral("ECH")) && !echelonRower && !echelonStride &&
!echelonConnectSport && filter) {
```
The excluding<T>() template function is called to specify the exclusion device type. Note that the test for a previously detected device of the same type is not included.
The configureExclusions code is overridden to specify the exclusion test data objects. Note that the test for a previously detected device of the same type is not included.
```
// Echelon Connect Sport Bike
RegisterNewDeviceTestData(DeviceIndex::EchelonConnectSportBike)
->expectDevice<echelonconnectsport>()
->acceptDeviceName("ECH", DeviceNameComparison::StartsWith)
->excluding<echelonrower>()
->excluding<echelonstride>();
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "Devices/EchelonRower/echelonrowertestdata.h"
#include "Devices/EchelonStrideTreadmill/echelonstridetreadmilltestdata.h"
#include "devices/echelonconnectsport/echelonconnectsport.h"
class EchelonConnectSportBikeTestData : public BikeTestData {
public:
EchelonConnectSportBikeTestData() : BikeTestData("Echelon Connect Sport Bike") {
this->addDeviceName("ECH", comparison::StartsWith);
}
void configureExclusions() override {
this->exclude(new EchelonRowerTestData());
this->exclude(new EchelonStrideTreadmillTestData());
}
deviceType get_expectedDeviceType() const override { return deviceType::EchelonConnectSport; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<echelonconnectsport*>(detectedDevice)!=nullptr;
}
};
```
### When a single test data object can't cover all the conditions
### When a single TestData Class Can't Cover all the Conditions
Detection code:
@@ -316,81 +390,116 @@ This presents 3 scenarios for the current test framework.
2. Match the name "KICKR CORE", presence and absence of specific service ids
3. Match the name "ASSIOMA" and the power sensor name setting starts with "Disabled"
The framework is not currently capable of specifying all these scenarios in a single test data object, without checking the name of the supplied QBluetoothDeviceInfo object against name conditions specified and constructing extra configurations based on that.
The generated test data is approximately the combinations of these lists: names * settings * exclusions.
If a combination should not exist, separate test data objects should be used.
The framework is not currently capable of specifying all these scenarios in a single class.
The generated test data is approximately the combinations of these lists: names * settings * bluetoothdeviceInfo * exclusions.
If a combination should not exist, a separate class should be used.
In the example of the Stages Bike test data, the exclusions, which apply to all situations, are implemented in an array of type ids:
In the example of the StagesBikeTestData classes, the exclusions, which apply to all situations, are implemented in the superclass StagesBikeTestData,
```
// Stages Bike General
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
#pragma once
#include "Devices/Bike/biketestdata.h"
#include "devices/stagesbike/stagesbike.h"
#include "Devices/FTMSBike/ftmsbiketestdata.h"
class StagesBikeTestData : public BikeTestData {
protected:
StagesBikeTestData(std::string testName): BikeTestData(testName) {
}
void configureExclusions() override {
this->exclude(new FTMSBike1TestData());
this->exclude(new FTMSBike2TestData());
}
public:
deviceType get_expectedDeviceType() const override { return deviceType::StagesBike; }
bool get_isExpectedDevice(bluetoothdevice * detectedDevice) const override {
return dynamic_cast<stagesbike*>(detectedDevice)!=nullptr;
}
};
```
The name-match only in one test data instance:
The name-match only in one subclass:
```
// Stages Bike
RegisterNewDeviceTestData(DeviceIndex::StagesBike)
->expectDevice<stagesbike>()
->acceptDeviceNames({"STAGES ", "TACX SATORI"}, DeviceNameComparison::StartsWithIgnoreCase)
->acceptDeviceName("QD", DeviceNameComparison::IgnoreCase)
->excluding(stagesBikeExclusions);
```
class StagesBike1TestData : public StagesBikeTestData {
The name and setting match in another instance:
public:
StagesBike1TestData() : StagesBikeTestData("Stages Bike") {
this->addDeviceName("STAGES ", comparison::StartsWithIgnoreCase);
this->addDeviceName("TACX SATORI", comparison::StartsWithIgnoreCase);
}
};
```
// Stages Bike Stages Bike (Assioma / Power Sensor disabled
RegisterNewDeviceTestData(DeviceIndex::StagesBike_Assioma_PowerSensorDisabled)
->expectDevice<stagesbike>()
->acceptDeviceName("ASSIOMA", DeviceNameComparison::StartsWithIgnoreCase)
->configureSettingsWith(QZSettings::power_sensor_name, "DisabledX", "XDisabled")
->excluding( stagesBikeExclusions);
The name and setting match in another subclass:
```
class StagesBike2TestData : public StagesBikeTestData {
protected:
bool configureSettings(DeviceDiscoveryInfo& info, bool enable) const override {
info.powerSensorName = enable ? "Disabled":"Roberto";
return true;
}
public:
StagesBike2TestData() : StagesBikeTestData("Stages Bike (Assioma / Power Sensor disabled") {
this->addDeviceName("ASSIOMA", comparison::StartsWithIgnoreCase);
}
};
```
The name and bluetooth device info configurations in another:
```
// Stages Bike (KICKR CORE)
RegisterNewDeviceTestData(DeviceIndex::StagesBike_KICKRCORE)
->expectDevice<stagesbike>()
->acceptDeviceName("KICKR CORE", DeviceNameComparison::StartsWithIgnoreCase)
->excluding(stagesBikeExclusions)
->configureSettingsWith(
[](const DeviceDiscoveryInfo& info, bool enable, std::vector<DeviceDiscoveryInfo>& configurations)->void
{
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
if(enable) {
DeviceDiscoveryInfo result = info;
result.addBluetoothService(QBluetoothUuid((quint16)0x1818));
result.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
configurations.push_back(result);
} else {
DeviceDiscoveryInfo hasNeither = info;
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1818));
hasNeither.removeBluetoothService(QBluetoothUuid((quint16)0x1826));
class StagesBike3TestData : public StagesBikeTestData {
protected:
void configureBluetoothDeviceInfos(const QBluetoothDeviceInfo& info, bool enable, std::vector<QBluetoothDeviceInfo>& bluetoothDeviceInfos) const override {
// The condition, if the name is acceptable, is:
// !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818)))
DeviceDiscoveryInfo hasInvalid = info;
hasInvalid.addBluetoothService(QBluetoothUuid((quint16)0x1826));
DeviceDiscoveryInfo hasBoth = hasInvalid;
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1818));
hasBoth.addBluetoothService(QBluetoothUuid((quint16)0x1826));
if(enable) {
QBluetoothDeviceInfo result = info;
result.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818)}));
bluetoothDeviceInfos.push_back(result);
} else {
QBluetoothDeviceInfo hasInvalid = info;
hasInvalid.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1826)}));
QBluetoothDeviceInfo hasBoth = hasInvalid;
hasBoth.setServiceUuids(QVector<QBluetoothUuid>({QBluetoothUuid((quint16)0x1818),QBluetoothUuid((quint16)0x1826)}));
configurations.push_back(info); // has neither
configurations.push_back(hasInvalid);
configurations.push_back(hasBoth);
}
});
bluetoothDeviceInfos.push_back(info); // has neither
bluetoothDeviceInfos.push_back(hasInvalid);
bluetoothDeviceInfos.push_back(hasBoth);
}
}
public:
StagesBike3TestData() : StagesBikeTestData("Stages Bike (KICKR CORE)") {
this->addDeviceName("KICKR CORE", comparison::StartsWithIgnoreCase);
}
};
```
## Telling Google Test Where to Look
The BluetoothDeviceTestSuite configuration specifies that the test data will be obtained from the DeviceTestDataIndex class, so there's nothing more to do.
To register your test data class(es) with Google Test:
- open tst/Devices/devices.h
- add a #include for your new header file(s)
- add your new classes to the BluetoothDeviceTestDataTypes list.
This will add tests for your new device class to test runs of the tests in the BluetoothDeviceTestSuite class, which are about detecting, and not detecting devices in circumstances generated from the TestData classes.

View File

@@ -1,25 +0,0 @@
# 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

@@ -1,188 +0,0 @@
from dataclasses import dataclass
from typing import List, Optional, Tuple
import re
@dataclass
class HubRidingData:
power: Optional[int] = None
cadence: Optional[int] = None
speed_x100: Optional[int] = None
hr: Optional[int] = None
unknown1: Optional[int] = None
unknown2: Optional[int] = None
def __str__(self):
return (f"Power={self.power}W Cadence={self.cadence}rpm "
f"Speed={self.speed_x100/100 if self.speed_x100 else 0:.1f}km/h "
f"HR={self.hr}bpm Unknown1={self.unknown1} Unknown2={self.unknown2}")
def parse_protobuf_varint(data: bytes, offset: int = 0) -> Tuple[int, int]:
result = 0
shift = 0
while offset < len(data):
byte = data[offset]
result |= (byte & 0x7F) << shift
offset += 1
if not (byte & 0x80):
break
shift += 7
return result, offset
def parse_hub_riding_data(data: bytes) -> Optional[HubRidingData]:
try:
riding_data = HubRidingData()
offset = 0
while offset < len(data):
key, new_offset = parse_protobuf_varint(data, offset)
wire_type = key & 0x07
field_number = key >> 3
offset = new_offset
if wire_type == 0:
value, offset = parse_protobuf_varint(data, offset)
if field_number == 1:
riding_data.power = value
elif field_number == 2:
riding_data.cadence = value
elif field_number == 3:
riding_data.speed_x100 = value
elif field_number == 4:
riding_data.hr = value
elif field_number == 5:
riding_data.unknown1 = value
elif field_number == 6:
riding_data.unknown2 = value
return riding_data
except Exception as e:
print(f"Error parsing protobuf: {e}")
return None
@dataclass
class DirconPacket:
message_version: int = 1
identifier: int = 0xFF
sequence_number: int = 0
response_code: int = 0
length: int = 0
uuid: int = 0
uuids: List[int] = None
additional_data: bytes = b''
is_request: bool = False
riding_data: Optional[HubRidingData] = None
def __str__(self):
uuids_str = ','.join(f'{u:04x}' for u in (self.uuids or []))
base_str = (f"vers={self.message_version} Id={self.identifier} sn={self.sequence_number} "
f"resp={self.response_code} len={self.length} req?={self.is_request} "
f"uuid={self.uuid:04x} dat={self.additional_data.hex()} uuids=[{uuids_str}]")
if self.riding_data:
base_str += f"\nRiding Data: {self.riding_data}"
return base_str
def parse_dircon_packet(data: bytes, offset: int = 0) -> Tuple[Optional[DirconPacket], int]:
if len(data) - offset < 6:
return None, 0
packet = DirconPacket()
packet.message_version = data[offset]
packet.identifier = data[offset + 1]
packet.sequence_number = data[offset + 2]
packet.response_code = data[offset + 3]
packet.length = (data[offset + 4] << 8) | data[offset + 5]
total_length = 6 + packet.length
if len(data) - offset < total_length:
return None, 0
curr_offset = offset + 6
if packet.identifier == 0x01: # DPKT_MSGID_DISCOVER_SERVICES
if packet.length == 0:
packet.is_request = True
elif packet.length % 16 == 0:
packet.uuids = []
while curr_offset + 16 <= offset + total_length:
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
packet.uuids.append(uuid)
curr_offset += 16
elif packet.identifier == 0x02: # DPKT_MSGID_DISCOVER_CHARACTERISTICS
if packet.length >= 16:
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
if packet.length == 16:
packet.is_request = True
elif (packet.length - 16) % 17 == 0:
curr_offset += 16
packet.uuids = []
packet.additional_data = b''
while curr_offset + 17 <= offset + total_length:
uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
packet.uuids.append(uuid)
packet.additional_data += bytes([data[curr_offset + 16]])
curr_offset += 17
elif packet.identifier in [0x03, 0x04, 0x05, 0x06]: # READ/WRITE/NOTIFY characteristics
if packet.length >= 16:
packet.uuid = (data[curr_offset + 2] << 8) | data[curr_offset + 3]
if packet.length > 16:
packet.additional_data = data[curr_offset + 16:offset + total_length]
if packet.uuid == 0x0002:
packet.riding_data = parse_hub_riding_data(packet.additional_data)
if packet.identifier != 0x06:
packet.is_request = True
return packet, total_length
def extract_bytes_from_c_array(content: str) -> List[Tuple[str, bytes]]:
packets = []
pattern = r'static const unsigned char (\w+)\[\d+\] = \{([^}]+)\};'
matches = re.finditer(pattern, content)
for match in matches:
name = match.group(1)
hex_str = match.group(2)
hex_values = []
for line in hex_str.split('\n'):
line = line.split('//')[0]
values = re.findall(r'0x[0-9a-fA-F]{2}', line)
hex_values.extend(values)
byte_data = bytes([int(x, 16) for x in hex_values])
packets.append((name, byte_data))
return packets
def get_tcp_payload(data: bytes) -> bytes:
ip_header_start = 14 # Skip Ethernet header
ip_header_len = (data[ip_header_start] & 0x0F) * 4
tcp_header_start = ip_header_start + ip_header_len
tcp_header_len = ((data[tcp_header_start + 12] >> 4) & 0x0F) * 4
payload_start = tcp_header_start + tcp_header_len
return data[payload_start:]
def parse_file(filename: str):
with open(filename, 'r') as f:
content = f.read()
packets = extract_bytes_from_c_array(content)
for name, data in packets:
print(f"\nPacket {name}:")
payload = get_tcp_payload(data)
print(f"Dircon payload: {payload.hex()}")
offset = 0
while offset < len(payload):
packet, consumed = parse_dircon_packet(payload, offset)
if packet is None or consumed == 0:
break
print(f"Frame: {packet}")
offset += consumed
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python script.py <input_file>")
sys.exit(1)
parse_file(sys.argv[1])

View File

@@ -1,331 +0,0 @@
import sys
import logging
import asyncio
import threading
import random
import struct
import binascii
import time
from typing import Any, Union
# Verificare che siamo su macOS
if sys.platform != 'darwin':
print("Questo script è progettato specificamente per macOS!")
sys.exit(1)
# Importare bless
try:
from bless import (
BlessServer,
BlessGATTCharacteristic,
GATTCharacteristicProperties,
GATTAttributePermissions,
)
except ImportError:
print("Errore: impossibile importare bless. Installarlo con: pip install bless")
sys.exit(1)
# Configurazione logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Trigger per eventi
trigger = threading.Event()
# Informazioni sul dispositivo
DEVICE_NAME = "Wahoo KICKR 51A6"
# UUID dei servizi standard
CYCLING_POWER_SERVICE = "00001818-0000-1000-8000-00805f9b34fb"
USER_DATA_SERVICE = "0000181c-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_SERVICE = "00001826-0000-1000-8000-00805f9b34fb"
# UUID dei servizi Wahoo personalizzati
WAHOO_SERVICE_1 = "a026ee01-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_3 = "a026ee03-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_6 = "a026ee06-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_SERVICE_B = "a026ee0b-0a7d-4ab3-97fa-f1500f9feb8b"
# UUID delle caratteristiche standard
CYCLING_POWER_MEASUREMENT = "00002a63-0000-1000-8000-00805f9b34fb"
CYCLING_POWER_FEATURE = "00002a65-0000-1000-8000-00805f9b34fb"
SENSOR_LOCATION = "00002a5d-0000-1000-8000-00805f9b34fb"
CYCLING_POWER_CONTROL_POINT = "00002a66-0000-1000-8000-00805f9b34fb"
WEIGHT = "00002a98-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_FEATURE = "00002acc-0000-1000-8000-00805f9b34fb"
TRAINING_STATUS = "00002ad3-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_CONTROL_POINT = "00002ad9-0000-1000-8000-00805f9b34fb"
FITNESS_MACHINE_STATUS = "00002ada-0000-1000-8000-00805f9b34fb"
INDOOR_BIKE_DATA = "00002ad2-0000-1000-8000-00805f9b34fb"
# UUID delle caratteristiche Wahoo personalizzate
WAHOO_CUSTOM_CP_CHAR = "a026e005-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_1 = "a026e002-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_2 = "a026e004-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_3 = "a026e00a-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_4 = "a026e023-0a7d-4ab3-97fa-f1500f9feb8b"
WAHOO_CHAR_5 = "a026e037-0a7d-4ab3-97fa-f1500f9feb8b"
# Stato dispositivo - variabili globali
current_power = 120
current_cadence = 85
current_speed = 25.0
current_resistance = 5
# Funzioni di callback
def read_request(characteristic, **kwargs):
logger.debug(f"Lettura: {characteristic.value}")
return characteristic.value
def write_request(characteristic, value, **kwargs):
uuid_str = str(characteristic.uuid).lower()
logger.info(f"Scrittura su caratteristica: {uuid_str}, valore: {binascii.hexlify(value)}")
# Gestione delle richieste di scrittura
if uuid_str == FITNESS_MACHINE_CONTROL_POINT.lower():
handle_ftms_control_point(value)
elif uuid_str == CYCLING_POWER_CONTROL_POINT.lower():
handle_cp_control_point(value)
elif uuid_str in [WAHOO_CHAR_1.lower(), WAHOO_CHAR_3.lower(), WAHOO_CHAR_4.lower(), WAHOO_CHAR_5.lower()]:
handle_wahoo_char_write(uuid_str, value)
characteristic.value = value
# Gestori di richieste di scrittura
def handle_ftms_control_point(data):
global current_power, current_resistance
if not data:
return
op_code = data[0]
logger.info(f"Comando FTMS Control Point: {op_code:#x}")
if op_code == 0x05: # Set Target Power (ERG mode)
if len(data) >= 3:
target_power = int.from_bytes(data[1:3], byteorder='little')
logger.info(f"Target power impostato: {target_power}W")
current_power = target_power
def handle_cp_control_point(data):
if not data:
return
op_code = data[0]
logger.info(f"Comando CP Control Point: {op_code:#x}")
def handle_wahoo_char_write(uuid_str, data):
logger.info(f"Scrittura su caratteristica Wahoo {uuid_str}: {binascii.hexlify(data)}")
# Funzioni per generare dati
def generate_cycling_power_data():
global current_power, current_cadence
# Varia leggermente i valori
current_power += random.randint(-3, 3)
current_power = max(0, min(2000, current_power))
current_cadence += random.randint(-1, 1)
current_cadence = max(0, min(200, current_cadence))
# Crea Cycling Power Measurement
power_data = bytearray(16)
power_data[0:2] = (0x0034).to_bytes(2, byteorder='little')
power_data[2:4] = (current_power).to_bytes(2, byteorder='little')
power_data[4:8] = (int(current_power * 10)).to_bytes(4, byteorder='little')
power_data[8:12] = (0).to_bytes(4, byteorder='little')
power_data[12:14] = (current_cadence).to_bytes(2, byteorder='little')
power_data[14:16] = (0xBAD8).to_bytes(2, byteorder='little')
return bytes(power_data)
def generate_indoor_bike_data():
global current_speed, current_cadence
# Varia leggermente i valori
current_speed += random.uniform(-0.2, 0.2)
current_speed = max(0, min(60.0, current_speed))
# Crea Indoor Bike Data
bike_data = bytearray(8)
bike_data[0:2] = (0x0044).to_bytes(2, byteorder='little')
bike_data[2:4] = (int(current_speed * 100)).to_bytes(2, byteorder='little')
bike_data[4:6] = (current_cadence).to_bytes(2, byteorder='little')
bike_data[6:8] = (0).to_bytes(2, byteorder='little')
return bytes(bike_data)
async def run():
# Crea server con minimo di parametri
server = BlessServer(name=DEVICE_NAME)
server.read_request_func = read_request
server.write_request_func = write_request
logger.info(f"Configurazione del simulatore {DEVICE_NAME}...")
# 1. Servizi standard
# Aggiungi Cycling Power Service
await server.add_new_service(CYCLING_POWER_SERVICE)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_MEASUREMENT,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_FEATURE,
GATTCharacteristicProperties.read,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
CYCLING_POWER_CONTROL_POINT,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
CYCLING_POWER_SERVICE,
WAHOO_CUSTOM_CP_CHAR,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Aggiungi Fitness Machine Service
await server.add_new_service(FITNESS_MACHINE_SERVICE)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
INDOOR_BIKE_DATA,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
FITNESS_MACHINE_CONTROL_POINT,
GATTCharacteristicProperties.write | GATTCharacteristicProperties.indicate,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
FITNESS_MACHINE_SERVICE,
FITNESS_MACHINE_FEATURE,
GATTCharacteristicProperties.read,
None,
GATTAttributePermissions.readable
)
# 2. Servizi Wahoo personalizzati
# Wahoo Service 1
await server.add_new_service(WAHOO_SERVICE_1)
await server.add_new_characteristic(
WAHOO_SERVICE_1,
WAHOO_CHAR_1,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
await server.add_new_characteristic(
WAHOO_SERVICE_1,
WAHOO_CHAR_2,
GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable
)
# Wahoo Service 3
await server.add_new_service(WAHOO_SERVICE_3)
await server.add_new_characteristic(
WAHOO_SERVICE_3,
WAHOO_CHAR_3,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Wahoo Service 6
await server.add_new_service(WAHOO_SERVICE_6)
await server.add_new_characteristic(
WAHOO_SERVICE_6,
WAHOO_CHAR_4,
GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
# Wahoo Service B
await server.add_new_service(WAHOO_SERVICE_B)
await server.add_new_characteristic(
WAHOO_SERVICE_B,
WAHOO_CHAR_5,
GATTCharacteristicProperties.read | GATTCharacteristicProperties.write_without_response | GATTCharacteristicProperties.notify,
None,
GATTAttributePermissions.readable | GATTAttributePermissions.writeable
)
logger.info("Configurazione dei servizi completata")
# Avvia il server
await server.start()
logger.info(f"{DEVICE_NAME} è ora in fase di advertising")
# Imposta i valori iniziali DOPO l'avvio del server
# Valori per servizi standard
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
server.get_characteristic(CYCLING_POWER_FEATURE).value = (0x08).to_bytes(4, byteorder='little')
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
server.get_characteristic(FITNESS_MACHINE_FEATURE).value = (0x02C6).to_bytes(4, byteorder='little')
# Valori per caratteristiche Wahoo
server.get_characteristic(WAHOO_CHAR_1).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_2).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_3).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_4).value = bytearray(1)
server.get_characteristic(WAHOO_CHAR_5).value = bytearray(1)
# Loop di aggiornamento
try:
counter = 0
while True:
# Aggiorna i dati principali
server.get_characteristic(INDOOR_BIKE_DATA).value = generate_indoor_bike_data()
server.get_characteristic(CYCLING_POWER_MEASUREMENT).value = generate_cycling_power_data()
# Invia notifiche
server.update_value(FITNESS_MACHINE_SERVICE, INDOOR_BIKE_DATA)
server.update_value(CYCLING_POWER_SERVICE, CYCLING_POWER_MEASUREMENT)
if counter % 10 == 0: # Log ogni 10 cicli
logger.info(f"Potenza: {current_power}W, Cadenza: {current_cadence}rpm, Velocità: {current_speed:.1f}km/h")
counter += 1
await asyncio.sleep(0.1)
except KeyboardInterrupt:
logger.info("Arresto richiesto dall'utente")
except Exception as e:
logger.error(f"Errore durante l'esecuzione: {e}")
finally:
await server.stop()
logger.info("Server arrestato")
if __name__ == "__main__":
print("=" * 80)
print(f"Wahoo KICKR 51A6 BLE Simulator per macOS (Versione completa)")
print("=" * 80)
print(f"Avvio della simulazione di {DEVICE_NAME}")
print("Premi Ctrl+C per terminare il server")
print("=" * 80)
try:
asyncio.run(run())
except KeyboardInterrupt:
print("\nSimulazione fermata dall'utente")
except Exception as e:
print(f"Errore: {e}")
print("Potrebbe essere necessario eseguire questo script con sudo")
sys.exit(1)

View File

@@ -1,143 +0,0 @@
import sys
import logging
import asyncio
import threading
import random
import struct
import binascii
from typing import Any, Union
from bless import (
BlessServer,
BlessGATTCharacteristic,
GATTCharacteristicProperties,
GATTAttributePermissions,
)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(name=__name__)
trigger: Union[asyncio.Event, threading.Event]
if sys.platform in ["darwin", "win32"]:
trigger = threading.Event()
else:
trigger = asyncio.Event()
def read_request(characteristic: BlessGATTCharacteristic, **kwargs) -> bytearray:
logger.debug(f"Reading {characteristic.value}")
return characteristic.value
def write_request(characteristic: BlessGATTCharacteristic, value: Any, **kwargs):
characteristic.value = value
logger.debug(f"Char value set to {characteristic.value}")
if characteristic.value == b"\x0f":
logger.debug("NICE")
trigger.set()
def generate_indoor_bike_data():
# Flags (16 bits)
flags = (1 << 2) | (1 << 6) # Instantaneous Cadence and Instantaneous Power present
speed = random.randint(0, 20000) # 0-20000
# Instantaneous Cadence (uint16, 0.5 rpm resolution)
cadence = random.randint(0, 400) # 0-200 rpm
# Instantaneous Power (sint16, watts)
power = random.randint(10, 50)
# Pack data into bytes
data = struct.pack("<HHHh", flags, speed, cadence, power)
return data
def generate_zwift_ride_data():
data_str = "2308ffbfffff0f1a04080010001a04080110001a04080210001a0408031000"
data = binascii.unhexlify(data_str)
return data
async def update_indoor_bike_data(server, service_uuid, char_uuid):
while True:
c = server.get_characteristic(char_uuid)
c.value = bytes(generate_indoor_bike_data())
server.update_value(service_uuid, char_uuid)
await asyncio.sleep(1)
async def update_zwift_ride_data(server, service_uuid, char_uuid):
while True:
c = server.get_characteristic(char_uuid)
c.value = bytes(generate_zwift_ride_data())
server.update_value(service_uuid, char_uuid)
await asyncio.sleep(1)
async def run(loop):
trigger.clear()
# Instantiate the server
server = BlessServer(name="FTMS Indoor Bike", loop=loop)
server.read_request_func = read_request
server.write_request_func = write_request
# Add Fitness Machine Service
ftms_uuid = "00001826-0000-1000-8000-00805f9b34fb"
await server.add_new_service(ftms_uuid)
# Add Indoor Bike Data Characteristic
indoor_bike_data_uuid = "00002ad2-0000-1000-8000-00805f9b34fb"
char_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.notify
)
permissions = GATTAttributePermissions.readable
await server.add_new_characteristic(
ftms_uuid, indoor_bike_data_uuid, char_flags, generate_indoor_bike_data(), permissions
)
zwift_ride_uuid = "00000001-19ca-4651-86e5-fa29dcdd09d1"
await server.add_new_service(zwift_ride_uuid)
syncRxChar = "00000003-19CA-4651-86E5-FA29DCDD09D1"
syncRx_flags = (
GATTCharacteristicProperties.write
)
syncRx_permissions = GATTAttributePermissions.writeable
syncTxChar = "00000004-19CA-4651-86E5-FA29DCDD09D1"
syncTx_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.indicate
)
syncTx_permissions = GATTAttributePermissions.readable
asyncChar = "00000002-19CA-4651-86E5-FA29DCDD09D1"
async_flags = (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.notify
)
async_permissions = GATTAttributePermissions.readable
await server.add_new_characteristic(
zwift_ride_uuid, syncRxChar, syncRx_flags, generate_indoor_bike_data(), syncRx_permissions
)
await server.add_new_characteristic(
zwift_ride_uuid, syncTxChar, syncTx_flags, generate_indoor_bike_data(), syncTx_permissions
)
await server.add_new_characteristic(
zwift_ride_uuid, asyncChar, async_flags, generate_zwift_ride_data(), async_permissions
)
logger.debug(server.get_characteristic(indoor_bike_data_uuid))
await server.start()
logger.debug("Advertising")
logger.info(f"FTMS Indoor Bike is now advertising")
# Start updating the indoor bike data
update_task = asyncio.create_task(update_indoor_bike_data(server, ftms_uuid, indoor_bike_data_uuid))
update_task_zwift_ride = asyncio.create_task(update_zwift_ride_data(server, zwift_ride_uuid, asyncChar))
await asyncio.sleep(99999999)
await server.stop()
loop = asyncio.get_event_loop()
loop.run_until_complete(run(loop))

View File

@@ -1,28 +0,0 @@
import json
def generate_code(hex_string, start_index):
hex_pairs = [hex_string[i:i+2] for i in range(0, len(hex_string), 2)]
output = ""
array_name = f"initData{start_index}"
array_elements = ', '.join([f"0x{hex_pair}" for hex_pair in hex_pairs])
output += f"uint8_t {array_name}[] = {{{array_elements}}};\n"
output += f'writeCharacteristic({array_name}, sizeof({array_name}), QStringLiteral("init"), false, false);\n'
output += "QThread::msleep(sleepms);\n\n"
return output
json_file_path = "C:\\Work\\qdomyos-zwift\\helpers\\tmp.json"
with open(json_file_path, 'r') as file:
# Carica i dati JSON
json_data = json.load(file)
line = 0
for item in json_data:
try:
if(item['_source']['layers']['btatt']['btatt.value_raw'][0] != ''):
line = line + 1
print(generate_code(item['_source']['layers']['btatt']['btatt.value_raw'][0], line))
except:
pass

View File

@@ -1,37 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.associations": {
"list": "cpp",
"chrono": "cpp",
"complex": "cpp",
"functional": "cpp",
"optional": "cpp",
"system_error": "cpp",
"type_traits": "cpp",
"xlocnum": "cpp",
"xtr1common": "cpp",
"qhttpserver": "cpp",
"array": "cpp",
"deque": "cpp",
"map": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"xstring": "cpp",
"algorithm": "cpp",
"xutility": "cpp",
"xlocale": "cpp",
"filesystem": "cpp",
"bitset": "cpp",
"iterator": "cpp",
"xhash": "cpp",
"xtree": "cpp",
"ostream": "cpp",
"locale": "cpp"
}
}
}

View File

@@ -10,92 +10,59 @@ ColumnLayout {
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentLoader.sourceComponent
// Signal emitted when content becomes visible
signal contentBecameVisible()
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true
Layout.fillWidth: true;
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
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
width: 8; height: 8
radius: 8
color: "white"
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x: 34; y: 13
x:34;y:13
color: "#FFFFFF"
text: rootElement.title
}
Image {
y: 13
anchors.right: parent.right
y:13
anchors.right: parent.right
anchors.rightMargin: 20
width: 30; height: 30
id: indicatImg
source: "qrc:/icons/arrow-collapse-vertical.png"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
rootElement.isOpen = !rootElement.isOpen
if(rootElement.isOpen) {
if(rootElement.isOpen)
{
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
} else {
}else{
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// Loader with enhanced visibility handling
Loader {
id: contentLoader
active: rootElement.isOpen
visible: false // Start invisible
Layout.fillWidth: true
asynchronous: false
onLoaded: {
if (item) {
item.Layout.fillWidth = true
visible = true
rootElement.contentBecameVisible()
}
}
// Handle visibility changes
onVisibleChanged: {
if (visible && status === Loader.Ready) {
rootElement.contentBecameVisible()
}
}
}
// Handle accordion closing
onIsOpenChanged: {
if (!isOpen) {
contentLoader.visible = false
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

View File

@@ -1,4 +0,0 @@
when you add a setting remember:
- you have to add always as the last settings declared in the settings.qml
- if you have to add a setting also on another qml file, you need also to declare it there always putting as the last one
- in the qzsettings.cpp there is a allsettingscount that must be updated if you add a setting

View File

@@ -9,7 +9,6 @@ ColumnLayout {
anchors.fill: parent
Settings {
id: settings
property int chart_display_mode: 0
}
WebView {
id: webView
@@ -20,9 +19,6 @@ ColumnLayout {
if (loadRequest.errorString) {
console.error(loadRequest.errorString);
console.error("port " + settings.value("template_inner_QZWS_port"));
} else if (loadRequest.status === WebView.LoadSucceededStatus) {
// Send chart display mode to the web view
sendDisplayModeToWebView();
}
}
onVisibleChanged: {
@@ -32,22 +28,4 @@ ColumnLayout {
}
}
}
// Watch for changes in chart display mode setting
Connections {
target: settings
function onChart_display_modeChanged() {
sendDisplayModeToWebView();
}
}
function sendDisplayModeToWebView() {
if (webView.loading === false) {
webView.runJavaScript("
if (window.setChartDisplayMode) {
window.setChartDisplayMode(" + settings.chart_display_mode + ");
}
");
}
}
}

View File

@@ -5,29 +5,26 @@
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>ConnectIQ.framework/ConnectIQ</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-armv7_arm64</string>
<key>LibraryPath</key>
<string>ConnectIQ.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>ConnectIQ.framework/ConnectIQ</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-i386_x86_64-simulator</string>
<key>LibraryPath</key>
<string>ConnectIQ.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>i386</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>

View File

@@ -6,7 +6,6 @@
//
#import <Foundation/Foundation.h>
#import "IQConstants.h"
#import "IQDevice.h"
#import "IQApp.h"
@@ -50,22 +49,9 @@ 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
@@ -102,11 +88,8 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
#pragma mark - INITIALIZATION
// --------------------------------------------------------------------------------
/// @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.
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
/// its operation.
///
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
/// Mobile is launched, it will return to the companion app by
@@ -116,60 +99,6 @@ 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
// --------------------------------------------------------------------------------
@@ -295,21 +224,6 @@ 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

@@ -13,9 +13,6 @@ extern int const IQSDKVersion;
/// @brief The bundle identifier for the Garmin Connect Mobile app.
extern NSString * const IQGCMBundle;
/// @brief The bundle identifier for the Garmin Connect Mobile Beta app.
extern NSString * const IQGCMInternalBetaBundle;
/// @brief The result of a SendMessage operation
typedef NS_ENUM(NSInteger, IQSendMessageResult){
///! @brief The message was sent successfully.

View File

@@ -42,9 +42,6 @@ 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.
@@ -54,17 +51,6 @@ 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,7 +6,6 @@
//
#import <Foundation/Foundation.h>
#import "IQConstants.h"
#import "IQDevice.h"
#import "IQApp.h"
@@ -50,22 +49,9 @@ 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
@@ -102,11 +88,8 @@ typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
#pragma mark - INITIALIZATION
// --------------------------------------------------------------------------------
/// @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.
/// @brief Initializes the ConnectIQ SDK with startup parameters necessary for
/// its operation.
///
/// @param urlScheme The URL scheme for this companion app. When Garmin Connect
/// Mobile is launched, it will return to the companion app by
@@ -116,60 +99,6 @@ 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
// --------------------------------------------------------------------------------
@@ -295,21 +224,6 @@ 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

@@ -13,9 +13,6 @@ extern int const IQSDKVersion;
/// @brief The bundle identifier for the Garmin Connect Mobile app.
extern NSString * const IQGCMBundle;
/// @brief The bundle identifier for the Garmin Connect Mobile Beta app.
extern NSString * const IQGCMInternalBetaBundle;
/// @brief The result of a SendMessage operation
typedef NS_ENUM(NSInteger, IQSendMessageResult){
///! @brief The message was sent successfully.

View File

@@ -42,9 +42,6 @@ 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.
@@ -54,17 +51,6 @@ 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>
oktDCwqbdQQg6rdcptAN5TGhUZs=
F1hICh90Ex4ADEjYLcSi0YPhrPA=
</data>
<key>Headers/IQApp.h</key>
<data>
CMQ9wDp2PKaw9dRd8NBYpX9xkzE=
R7+SmeArgBACIBWHRnEAugyFHKE=
</data>
<key>Headers/IQAppStatus.h</key>
<data>
@@ -18,19 +18,19 @@
</data>
<key>Headers/IQConstants.h</key>
<data>
z5FAXaGG7RDVUTai1Vvqs33zc98=
eI7keKSkaajUZACnuMhgtV1RuBA=
</data>
<key>Headers/IQDevice.h</key>
<data>
a4hkgIut7ETtkOJXPkn/nGElEYg=
bl545C/cu0mw2KlRmzojKmHPom0=
</data>
<key>Info.plist</key>
<data>
LeO8CbXcC4FrKgyl2zDm7R7nOj0=
sMY09qXRBL/m1OGNWejLjfNg04w=
</data>
<key>Modules/module.modulemap</key>
<data>
eEyhq/G44PBlD3KiydN8B1vbfCU=
SSRVAtIAdFmowQqE4HzOpWYLubg=
</data>
<key>ar.lproj/IQLocalizable.strings</key>
<dict>
@@ -298,20 +298,32 @@
<dict>
<key>Headers/ConnectIQ.h</key>
<dict>
<key>hash</key>
<data>
F1hICh90Ex4ADEjYLcSi0YPhrPA=
</data>
<key>hash2</key>
<data>
E2QDme6rWC+CJc/kKtxIVSpPzbE4ArUwNagnLG6Nxis=
ABtgvHbvmly4QpZO/KmmrwYkL0N+AqV3gXdPVrseysY=
</data>
</dict>
<key>Headers/IQApp.h</key>
<dict>
<key>hash</key>
<data>
R7+SmeArgBACIBWHRnEAugyFHKE=
</data>
<key>hash2</key>
<data>
KhyZorkoK2Qipuzee5aE5ENCarHR+Ni21GdxCV3FQ0s=
X4vXt0sO9gxQNzQalIaLqMpSGNRC9ue2USDcfjBYkec=
</data>
</dict>
<key>Headers/IQAppStatus.h</key>
<dict>
<key>hash</key>
<data>
WnybOSMMVqCKGns0rEz9C3EfQOg=
</data>
<key>hash2</key>
<data>
tg9qNXtTmFUvNoJtq7O/aEXBNngcGENVRhvxLJ8C/xo=
@@ -319,27 +331,43 @@
</dict>
<key>Headers/IQConstants.h</key>
<dict>
<key>hash</key>
<data>
eI7keKSkaajUZACnuMhgtV1RuBA=
</data>
<key>hash2</key>
<data>
qVLQDlPhVsyAAQ/LCGOCdEOUaabcgwTHijMQiuWbAXM=
bqDpm8yikc2FIqaSUHcLqPY6TPXLlXSUo+Dl9NUYwmA=
</data>
</dict>
<key>Headers/IQDevice.h</key>
<dict>
<key>hash</key>
<data>
bl545C/cu0mw2KlRmzojKmHPom0=
</data>
<key>hash2</key>
<data>
Xx+4dhu0JD6w2pd9UMvLXukYVQfKzaLJhU0paDUQyls=
4N4+64IHeb9iBwyziNxo0SMuCM75ez9Em4UfmtgtTHA=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash</key>
<data>
SSRVAtIAdFmowQqE4HzOpWYLubg=
</data>
<key>hash2</key>
<data>
6a9Ehz1N4Sm/6qBlTfQpHUqRlpzQr2JMF26AfW4xUtY=
lQGjVO5Q0wfztjETCwDkwAkQ7nZInCgWdStnHL3o6Co=
</data>
</dict>
<key>ar.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
1CDTE/Qaf1Z/HuhSt9CUnwitv4M=
</data>
<key>hash2</key>
<data>
CWyQue2TCS0heGoGbN4ffetM2QZSk7lqgc2Wer2fgTg=
@@ -349,6 +377,10 @@
</dict>
<key>cs.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
/jkyQ77G2Xd9wy6QptBphGNbtCY=
</data>
<key>hash2</key>
<data>
1mSn+EYeYcTV1dArgHz7PkmZrV6mHWfnuG5aDa6Y87E=
@@ -358,6 +390,10 @@
</dict>
<key>da.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
FYi0wjOu/Hw//Qe96yqxSb9yClc=
</data>
<key>hash2</key>
<data>
yLkvGzd+smkOjicvW/+Oe6wGGyirHS+/YfjuSzyVoMM=
@@ -367,6 +403,10 @@
</dict>
<key>de.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
MitzVbGhXhTLjPvw9vuWcQQa50Q=
</data>
<key>hash2</key>
<data>
DFHv7MWBJmyAkOj993NmSFKbS2t8/vtSev603sBUtjI=
@@ -376,6 +416,10 @@
</dict>
<key>el.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
n82gLcjjjHszaroTFeJUvSrrc0o=
</data>
<key>hash2</key>
<data>
i4FAK4mi+SgS6oZv8zM74kRZToakn49E8GD7FcJBLoQ=
@@ -385,6 +429,10 @@
</dict>
<key>en.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
hcxxLyrTI+aElXlPc5dwr7jdqwc=
</data>
<key>hash2</key>
<data>
vmBi9DFJzFcG0OwaWKSDjgklNi407U8u2pz3EnEENN4=
@@ -394,6 +442,10 @@
</dict>
<key>es.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
ff8DVQtNhO8pF7HFnXjh8foHXbo=
</data>
<key>hash2</key>
<data>
z6RjynaWjrRKHmv4sLirc4eXwKOtQdylzj5+TiHpaTc=
@@ -403,6 +455,10 @@
</dict>
<key>fi.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
R9cr8yqJmu91Xz31tGyprGR3t/s=
</data>
<key>hash2</key>
<data>
6BI0iPRVWaP63/XFdjLBz6z7DsvvuOoaEAS+mYzrx8E=
@@ -412,6 +468,10 @@
</dict>
<key>fr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
PwFmqFeRTcjdHmkXYrPzNVYoe5o=
</data>
<key>hash2</key>
<data>
geXjZzXre2CRiALecPFBGz4JSJA7MbkDnB4qrEMKNwk=
@@ -421,6 +481,10 @@
</dict>
<key>he.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
/jPUgFtYbbyELG5DZ3Sjoi/If9w=
</data>
<key>hash2</key>
<data>
47mcrSx16SFjWPIiN7guCAG0va8NiJ6I5s45tSVEHlY=
@@ -430,6 +494,10 @@
</dict>
<key>hr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
H2GtdTeORRPCnogvpWY69Dg9uME=
</data>
<key>hash2</key>
<data>
4bQvygPax6VBpoFlyS5by1N6otnDMliHu+bWsDaWSQc=
@@ -439,6 +507,10 @@
</dict>
<key>hu.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
QIimMhNyYmqp4ZW01hfj554WAMg=
</data>
<key>hash2</key>
<data>
0m2fIyz26vh3RlUqqSXvoNTLovxIixrUyJoL/IDSoVk=
@@ -448,6 +520,10 @@
</dict>
<key>id.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
2/54a0gkcVuk1I3m4ulDAXOLL5o=
</data>
<key>hash2</key>
<data>
hQf9SrG7d8aVWsXIbCIxkKEJjbnW1FLvS+MbOI1VtHQ=
@@ -457,6 +533,10 @@
</dict>
<key>it.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
hNIKYIcP/87e6g7AUP+zKRtJ52M=
</data>
<key>hash2</key>
<data>
XAbEWX6cicDxGzxGgSx3DhF4rjUHX4LV+dO0X3rUEqc=
@@ -466,6 +546,10 @@
</dict>
<key>ja.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
0iU2PbJ/3xgXMZ20ffsqaWpxKWc=
</data>
<key>hash2</key>
<data>
YOqOvZq0WEN4DCoSwc0lcTSRc4C812DqzjIsaid1SHg=
@@ -475,6 +559,10 @@
</dict>
<key>ko.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
ERH8oHR9H9jMHjP0EAgaTtVhnX4=
</data>
<key>hash2</key>
<data>
WJyaRCWn1KqmcDeajRnC41MdNrlpbI+1JbPkXhbKrKY=
@@ -484,6 +572,10 @@
</dict>
<key>ms.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
DkbQA2+v/qSgQWma/fg3647Bkqs=
</data>
<key>hash2</key>
<data>
gztYxa4Hn58HkKmcUIZI1jCz44IETZeMsqrpZSKxJvc=
@@ -493,6 +585,10 @@
</dict>
<key>nb.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
T3zFOvuvrJt5Vnmfqt2Mf/du8as=
</data>
<key>hash2</key>
<data>
Oy6UOwSN+/xPIrthAEvzV8PEn27kfsHpMMLU5w1rww0=
@@ -502,6 +598,10 @@
</dict>
<key>nl.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
t9PD5JEbfoSLaQ7f8M2cLghOReI=
</data>
<key>hash2</key>
<data>
XbijhSaZgmsW59Vo9ZEbhDuUQH18fHizWKzsLosiM0o=
@@ -511,6 +611,10 @@
</dict>
<key>pl.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
wfTnhBccAm6JfwH/JkZKNRKTUAU=
</data>
<key>hash2</key>
<data>
MQYgqA+Hl03JJ261Q19K5Lt64kSTBP+pfpD+jOVE3AU=
@@ -520,6 +624,10 @@
</dict>
<key>pt-PT.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
7yXkcZEpJ4UiRHAzhK+vw/Q857Y=
</data>
<key>hash2</key>
<data>
seINq3QazVameLGOW+pIAtGWLa6NDl5XWRtqnObxywo=
@@ -529,6 +637,10 @@
</dict>
<key>pt.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
tZPncsQs8weCDJa03AKLpijXSUw=
</data>
<key>hash2</key>
<data>
GnzdqEuQwORzVCih99bwr79UHIyzXm+zuN5b9m1NrKY=
@@ -538,6 +650,10 @@
</dict>
<key>ru.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
Ct+byJ3rWeigvg0q6rB/kQaR+yE=
</data>
<key>hash2</key>
<data>
yCN9s/JXYqsMNZ1icaH4hUwyMQ1NtxOmV6sIAtRd9pc=
@@ -547,6 +663,10 @@
</dict>
<key>sk.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
1yTM1nAsAYpSH7NrYU6/nFlqk5E=
</data>
<key>hash2</key>
<data>
OFHDtkGLLSfTuSx8GOTycKDCKOKmX0Wh2QG1CHhRz3I=
@@ -556,6 +676,10 @@
</dict>
<key>sv.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
i84z6vuHLrFpO0qZ2V0zYjixIws=
</data>
<key>hash2</key>
<data>
a3Gk+3USOT5uundOXrNCgnbcD0rDo2lkCO7b7+zg2Is=
@@ -565,6 +689,10 @@
</dict>
<key>th.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
oW5npy+pDJM1wUOgTkw9FY1Ave4=
</data>
<key>hash2</key>
<data>
qxGqAqRMwm0/dMd0W7DUsvbWb9x65GT+3d1zOQEql1w=
@@ -574,6 +702,10 @@
</dict>
<key>tr.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
76rD7PLrQMiT5YTlI8IjEFgsiU4=
</data>
<key>hash2</key>
<data>
Y6TnKQmqO/TAx+0KYqRRG6UOz7I/gM1YmbUwgSfZSQU=
@@ -583,6 +715,10 @@
</dict>
<key>zh-Hans.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
DNlMxUKypOvKArzi7ioJUiFfFXg=
</data>
<key>hash2</key>
<data>
BI3m4MTMHuPI4sQKPGeQnxIlBJJrXwgVuR7Ho1Q5o6Y=
@@ -592,6 +728,10 @@
</dict>
<key>zh-Hant.lproj/IQLocalizable.strings</key>
<dict>
<key>hash</key>
<data>
U6I+uL07KIv2b77w0c0glaJlhMg=
</data>
<key>hash2</key>
<data>
14dQnjX3pEz2Um4J/fOdQDRe/LSuXxqkg1hEkO8E5ys=

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