mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
14 Commits
Mobi-Rower
...
ios17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f859473eb3 | ||
|
|
db0251d78e | ||
|
|
c1a613d8f4 | ||
|
|
ca8fa34d85 | ||
|
|
de92737b87 | ||
|
|
61621bd4f3 | ||
|
|
b354c48998 | ||
|
|
d7c499a009 | ||
|
|
5d9e28715f | ||
|
|
6f3c386915 | ||
|
|
f80550dbb8 | ||
|
|
c5ca080dd8 | ||
|
|
b6bd3bdb2c | ||
|
|
26325aa62e |
1501
.github/workflows/main.yml
vendored
1501
.github/workflows/main.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||
|
||||
13
.gitmodules
vendored
13
.gitmodules
vendored
@@ -5,14 +5,11 @@
|
||||
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
|
||||
[submodule "src/qthttpserver"]
|
||||
path = src/qthttpserver
|
||||
url = https://github.com/qt-labs/qthttpserver
|
||||
[submodule "zwiftplay"]
|
||||
path = zwiftplay
|
||||
url = https://github.com/cagnulein/zwiftplay.git
|
||||
branch = lib
|
||||
branch = tags/release-1.12.1
|
||||
|
||||
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -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
374
CLAUDE.md
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
131
README.md
131
README.md
@@ -7,125 +7,58 @@ Zwift bridge for Treadmills and Bike!
|
||||
[<img src="docs/img/app_store.png">](https://apps.apple.com/app/id1543684531?fbclid=IwAR10H6y3mEgwkTlGJON3e8voYOh2wt3kLFOpFzoIXaYZ_N0y0pDvKxHMUaM)
|
||||
<a href="https://www.buymeacoffee.com/cagnulein" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="icons/AppScreen/iOS%20Phones%20-%206.5_/screenshot1.jpeg" style="height: 400px !important; box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
||||
</td>
|
||||
<td>
|
||||
<img src="icons/AppScreen/iOS%20Phones%20-%206.5_/screenshot2.jpeg" style="height: 400px !important; box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
||||
</td>
|
||||
<td>
|
||||
<img src="icons/AppScreen/iOS%20Phones%20-%206.5_/screenshot3.jpeg" style="height: 400px !important; box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
||||
</td>
|
||||
<td>
|
||||
<img src="icons/AppScreen/iOS%20Phones%20-%206.5_/screenshot4.jpeg" style="height: 400px !important; box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
||||
</td>
|
||||
<td>
|
||||
<img src="icons/AppScreen/iOS%20Phones%20-%206.5_/screenshot5.jpeg" style="height: 400px !important; box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||

|
||||
|
||||
[](https://www.youtube.com/watch?v=GgG3dMhmo2Y)
|
||||
|
||||

|
||||

|
||||
|
||||
UI on Linux
|
||||
|
||||

|
||||
|
||||
UI on MacOS
|
||||
|
||||
### Features
|
||||
|
||||
# UI Features
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|Tiles Customization|X|X|X|X|Order and visibility of each tile|
|
||||
|Profiles|X|X|X|X|Different user or different fitness device profiles|
|
||||
|UI Zoom Customization|X|X|X|X||
|
||||
|
||||
# Peloton Features
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|Bike metrics on the peloton app|X||X|||
|
||||
|Power zone with auto resistance|X|||||
|
||||
|Peloton real-time resistance conversion|X||X||with the possibility to customize it|
|
||||
|Peloton real-time auto-resistance|X||X||with the possibility to customize it|
|
||||
|Peloton auto speed and auto inclination||X|X||with the possibility to customize it|
|
||||
|
||||
# Heart Rate Features
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|Heart Rate support|X|X|X|X|Apple Watch, ANT+ devices and Bluetooth devices|
|
||||
|Heart Rate Zones Customizations|X|X|X|X||
|
||||
|Ability to calculate Wattage from HR and Cadence|X||||for the bikes that doesn't have a power sensor|
|
||||
|
||||
# 3rd Apps Compatibility
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|Zwift Compatibility|X|X|X|X||
|
||||
|Zwift Auto resistance|X||X|||
|
||||
|Zwift Auto inclination and speed||X|X||https://www.youtube.com/watch?v=KTQ2n7yeDbo|
|
||||
|Wahoo RGT Compatibility|X|X|X|X||
|
||||
|VzFit Compatibility|X|X|X|X||
|
||||
|Rouvy Compatibility|X|X|X|X||
|
||||
|IFIT app Compatibility|X|||||
|
||||
|Echelon app Compatibility|X|||||
|
||||
|Wahoo Dircon Compatibility|X|X|X|X|in order to send data to Zwift or RGT with Wifi only!|
|
||||
|One device only support for Zwift and Wahoo RGT|X|X|X|X|using Wahoo Dircon https://www.youtube.com/watch?v=gYYUXNWFAok|
|
||||
|BitGym Compatibility|X|X|X|X||
|
||||
|
||||
# Training Program
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|Builtin video support (Kinomap like)|X|X|X|X|Files could be local or on the cloud!|
|
||||
|GPX auto following|X|X|X|X||
|
||||
|2D/3D maps for GPX|X|X|X|X||
|
||||
|ZWO (Zwift workout file) compatibility|X|X|X|X||
|
||||
|XML Workout file compatibility|X|X|X|X||
|
||||
|Auto follow workout based on your heart rate|X|X|X|X||
|
||||
|Random workout|X|X|X|X||
|
||||
|
||||
|
||||
# Statistics
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|E-Mail report|X|X|X|X|at the end of the workout|
|
||||
|Strava integration|X|X|X|X|press stop at the end of the workout to auto upload it|
|
||||
|
||||
# Misc
|
||||
|
||||
|Feature|Bike|Treadmill|Elliptical|Rower|Notes|
|
||||
|:---|:---:|:---:|:---:|:---:|---:|
|
||||
|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||
|
||||
1. Domyos compatible
|
||||
2. Toorx TRX Route Key compatible
|
||||
3. Echelon Connect Sport compatible
|
||||
4. Zwift compatible
|
||||
5. Create, load and save train programs
|
||||
6. Measure distance, elevation gain and watts
|
||||
7. Gpx import (with difficulty slider)
|
||||
8. Realtime Charts
|
||||
|
||||

|
||||
|
||||
### Installation
|
||||
|
||||
You can install it on multiple platforms.
|
||||
Read the [installation procedure](docs/10_Installation.md)
|
||||
You can install 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.
|
||||
|
||||
### No GUI version
|
||||
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 comes from [flaticon.com](https://www.flaticon.com)
|
||||
|
||||
### Blog
|
||||
|
||||
=> Related Blog: [Roberto Viola's Blog](https://robertoviola.cloud)
|
||||
https://robertoviola.cloud
|
||||
|
||||
@@ -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: "🤩")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,3 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "swiftDebug.h"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,17 +53,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_accessibility_support_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bodymovin_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bootstrap_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_clipboard_support_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_devicediscovery_support_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_edid_support_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_eventdispatcher_support_private.pri \
|
||||
@@ -76,9 +71,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_gui_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_httpserver.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_macextras.pri \
|
||||
@@ -103,8 +95,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioning_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmldebug_private.pri \
|
||||
@@ -117,16 +107,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmlworkerscript_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qtmultimediaquicktools_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2_private.pri \
|
||||
@@ -140,21 +120,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_remoteobjects_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sslserver.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_testlib.pri \
|
||||
@@ -165,8 +136,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uiplugin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel_private.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_websockets.pri \
|
||||
@@ -225,26 +194,15 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtiff.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmedia_audioengine.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmultimedia_m3u.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpassthrucanbus.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpeakcanbus.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_cl.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_positionpoll.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_plugin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_shakeplugin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_generic.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_ios.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qttinycanbus.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtuiotouchplugin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualcanbus.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_hangul.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_openwnn.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_pinyin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_tcime.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_thai.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboardplugin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtwebview_darwin.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwbmp.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebgl.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebp.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_scene2d.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qt_functions.prf \
|
||||
@@ -261,9 +219,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/features/default_pre.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/default_pre.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_pre.prf \
|
||||
../defaults.pri \
|
||||
../src/purchasing/purchasing.pri \
|
||||
../src/qdomyos-zwift.pri \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/resolve_config.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/resolve_config.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/default_post.prf \
|
||||
@@ -271,9 +226,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_post.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/macx-ios-clang/features/default_post.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/objective_c.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qmltypes.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/metatypes.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/ltcg.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qml_debug.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/mac.prf \
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/bitcode.prf \
|
||||
@@ -312,18 +264,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/lib/libqtharfbuzz_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Core_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libqtpcre2_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfmediaplayer_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_esri_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_itemsoverlay_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapbox_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapboxgl_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_nokia_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_osm_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/webview/libqtwebview_darwin_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfcamera_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqtmedia_audioengine_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/audio/libqtaudio_coreaudio_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/playlistformats/libqtmultimedia_m3u_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqgif_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqicns_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqico_debug.prl \
|
||||
@@ -348,51 +288,32 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_server_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_tcp_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/bearer/libqgenericbearer_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios_debug.prl \
|
||||
../../Qt/5.15.2/ios/plugins/sqldrivers/libqsqlite_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5HttpServer_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5SslServer_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Charts_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Widgets_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Location_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5PositioningQuick_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5QuickControls2_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Quick_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Multimedia_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5WebView_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Bluetooth_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Xml_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Positioning_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5QmlModels_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Qml_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5NetworkAuth_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5WebSockets_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Network_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5TextToSpeech_debug.prl \
|
||||
../../Qt/5.15.2/ios/lib/libQt5Concurrent_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick.2/libqtquick2plugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/libqtquickcontrols2plugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQml/libqmlplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Templates.2/libqtquicktemplates2plugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQml/Models.2/libmodelsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQml/WorkerScript.2/libworkerscriptplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtWebView/libdeclarative_webview_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/libdialogplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtPositioning/libdeclarative_positioning_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtLocation/libdeclarative_location_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private/libdialogsprivateplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls/libqtquickcontrolsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets/libwidgetsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtMultimedia/libdeclarative_multimedia_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion/libqtquickcontrols2fusionstyleplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal/libqtquickcontrols2universalstyleplugin_debug.prl \
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine/libqtquickcontrols2imaginestyleplugin_debug.prl
|
||||
@@ -440,17 +361,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_accessibility_support_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bluetooth_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bodymovin_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_bootstrap_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_charts_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_clipboard_support_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_concurrent_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_core_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_datavisualization_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_devicediscovery_support_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_edid_support_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_eventdispatcher_support_private.pri:
|
||||
@@ -463,9 +379,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_gui_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_help_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_httpserver_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_httpserver.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_location_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_macextras.pri:
|
||||
@@ -490,8 +403,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioning_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_positioningquick_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_purchasing_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qml_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmldebug_private.pri:
|
||||
@@ -504,16 +415,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qmlworkerscript_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_qtmultimediaquicktools_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3d_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dassetimport_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3drender_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3druntimerender_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick3dutils_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quick_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_quickcontrols2_private.pri:
|
||||
@@ -527,21 +428,12 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_remoteobjects_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_repparser_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_script_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scripttools_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_scxml_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sensors_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_serialbus_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sql_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules-inst/qt_lib_sslserver_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_sslserver.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_svg_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_testlib.pri:
|
||||
@@ -552,8 +444,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uiplugin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_uitools_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_virtualkeyboard_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_webchannel_private.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_lib_websockets.pri:
|
||||
@@ -612,26 +502,15 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtiff.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmedia_audioengine.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtmultimedia_m3u.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpassthrucanbus.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtpeakcanbus.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_cl.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtposition_positionpoll.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_plugin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensorgestures_shakeplugin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_generic.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtsensors_ios.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qttinycanbus.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtuiotouchplugin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualcanbus.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_hangul.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_openwnn.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_pinyin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_tcime.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboard_thai.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtvirtualkeyboardplugin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qtwebview_darwin.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwbmp.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebgl.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_qwebp.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/modules/qt_plugin_scene2d.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qt_functions.prf:
|
||||
@@ -648,9 +527,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/features/default_pre.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/default_pre.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_pre.prf:
|
||||
../defaults.pri:
|
||||
../src/purchasing/purchasing.pri:
|
||||
../src/qdomyos-zwift.pri:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/resolve_config.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/resolve_config.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/default_post.prf:
|
||||
@@ -658,9 +534,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/default_post.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/macx-ios-clang/features/default_post.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/objective_c.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qmltypes.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/metatypes.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/ltcg.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/qml_debug.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/mac/mac.prf:
|
||||
../../Qt/5.15.2/ios/mkspecs/features/uikit/bitcode.prf:
|
||||
@@ -699,18 +572,6 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/lib/libqtharfbuzz_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Core_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libqtpcre2_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfmediaplayer_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_esri_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_itemsoverlay_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapbox_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_mapboxgl_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_nokia_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/geoservices/libqtgeoservices_osm_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/webview/libqtwebview_darwin_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqavfcamera_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/mediaservice/libqtmedia_audioengine_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/audio/libqtaudio_coreaudio_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/playlistformats/libqtmultimedia_m3u_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqgif_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqicns_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/imageformats/libqico_debug.prl:
|
||||
@@ -735,51 +596,32 @@ qdomyoszwift.xcodeproj/project.pbxproj: ../src/qdomyos-zwift.pro ../../Qt/5.15.2
|
||||
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_server_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/qmltooling/libqmldbg_tcp_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/bearer/libqgenericbearer_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios_debug.prl:
|
||||
../../Qt/5.15.2/ios/plugins/sqldrivers/libqsqlite_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5HttpServer_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5SslServer_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Charts_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Widgets_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Location_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5PositioningQuick_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5QuickControls2_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Quick_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Multimedia_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5WebView_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Bluetooth_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Xml_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Positioning_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5QmlModels_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Qml_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5NetworkAuth_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5WebSockets_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Network_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5TextToSpeech_debug.prl:
|
||||
../../Qt/5.15.2/ios/lib/libQt5Concurrent_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick.2/libqtquick2plugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/libqtquickcontrols2plugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQml/libqmlplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Templates.2/libqtquicktemplates2plugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQml/Models.2/libmodelsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQml/WorkerScript.2/libworkerscriptplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Window.2/libwindowplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Material/libqtquickcontrols2materialstyleplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtWebView/libdeclarative_webview_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtCharts/libqtchartsqml2_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/libdialogplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtPositioning/libdeclarative_positioning_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtLocation/libdeclarative_location_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/folderlistmodel/libqmlfolderlistmodelplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/settings/libqmlsettingsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Dialogs/Private/libdialogsprivateplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls/libqtquickcontrolsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/PrivateWidgets/libwidgetsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/libqtgraphicaleffectsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/Qt/labs/platform/libqtlabsplatformplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtMultimedia/libdeclarative_multimedia_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtGraphicalEffects/private/libqtgraphicaleffectsprivate_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Layouts/libqquicklayoutsplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Fusion/libqtquickcontrols2fusionstyleplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Universal/libqtquickcontrols2universalstyleplugin_debug.prl:
|
||||
../../Qt/5.15.2/ios/qml/QtQuick/Controls.2/Imagine/libqtquickcontrols2imaginestyleplugin_debug.prl:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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))")
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,17 +159,9 @@ 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)!,
|
||||
HKSampleType.quantityType(forIdentifier: .runningPower)!,
|
||||
HKSampleType.quantityType(forIdentifier: .runningSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .runningStrideLength)!,
|
||||
HKSampleType.quantityType(forIdentifier: .runningVerticalOscillation)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingSpeed)!,
|
||||
HKSampleType.quantityType(forIdentifier: .walkingStepLength)!,
|
||||
HKSampleType.quantityType(forIdentifier: .flightsClimbed)!,
|
||||
HKSampleType.workoutType()
|
||||
])
|
||||
} else {
|
||||
@@ -189,8 +172,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 +190,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 +210,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 +242,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 +258,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 +300,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 +310,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)
|
||||
@@ -477,131 +335,64 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if(sport == 0) {
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
|
||||
doubleValue: WorkoutTracking.power)
|
||||
|
||||
if(WorkoutTracking.lastDateMetric.distance(to: Date()) < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let powerType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingPower) else {
|
||||
return
|
||||
}
|
||||
let wattPerIntervalSample = HKQuantitySample(type: powerType,
|
||||
quantity: wattPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([wattPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
let cadencePerInterval = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.second()),
|
||||
doubleValue: WorkoutTracking.cadence / 60.0)
|
||||
|
||||
guard let cadenceType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingCadence) else {
|
||||
return
|
||||
}
|
||||
let cadencePerIntervalSample = HKQuantitySample(type: cadenceType,
|
||||
quantity: cadencePerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([cadencePerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
|
||||
doubleValue: WorkoutTracking.speed * 0.277778)
|
||||
|
||||
guard let speedType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingSpeed) else {
|
||||
return
|
||||
}
|
||||
let speedPerIntervalSample = HKQuantitySample(type: speedType,
|
||||
quantity: speedPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([speedPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
} else if(sport == 1) {
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
|
||||
doubleValue: WorkoutTracking.power)
|
||||
|
||||
if(WorkoutTracking.lastDateMetric.distance(to: Date()) < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let powerType = HKQuantityType.quantityType(
|
||||
forIdentifier: .runningPower) else {
|
||||
return
|
||||
}
|
||||
let wattPerIntervalSample = HKQuantitySample(type: powerType,
|
||||
quantity: wattPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([wattPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
|
||||
doubleValue: WorkoutTracking.power)
|
||||
|
||||
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
|
||||
doubleValue: WorkoutTracking.speed * 0.277778)
|
||||
|
||||
guard let speedType = HKQuantityType.quantityType(
|
||||
forIdentifier: .runningSpeed) else {
|
||||
if(WorkoutTracking.lastDateMetric.distance(to: Date()) < 1) {
|
||||
return
|
||||
}
|
||||
let speedPerIntervalSample = HKQuantitySample(type: speedType,
|
||||
quantity: speedPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([speedPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
} else if(sport == 2) {
|
||||
if #available(watchOSApplicationExtension 10.0, *) {
|
||||
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
|
||||
doubleValue: WorkoutTracking.speed * 0.277778)
|
||||
|
||||
guard let speedType = HKQuantityType.quantityType(
|
||||
forIdentifier: .walkingSpeed) else {
|
||||
return
|
||||
}
|
||||
let speedPerIntervalSample = HKQuantitySample(type: speedType,
|
||||
quantity: speedPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([speedPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
|
||||
guard let powerType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingPower) else {
|
||||
return
|
||||
}
|
||||
let wattPerIntervalSample = HKQuantitySample(type: powerType,
|
||||
quantity: wattPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([wattPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
let cadencePerInterval = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()),
|
||||
doubleValue: WorkoutTracking.cadence)
|
||||
|
||||
guard let cadenceType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingCadence) else {
|
||||
return
|
||||
}
|
||||
let cadencePerIntervalSample = HKQuantitySample(type: cadenceType,
|
||||
quantity: cadencePerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([cadencePerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
|
||||
doubleValue: WorkoutTracking.speed * 0.277778)
|
||||
|
||||
guard let speedType = HKQuantityType.quantityType(
|
||||
forIdentifier: .cyclingSpeed) else {
|
||||
return
|
||||
}
|
||||
let speedPerIntervalSample = HKQuantitySample(type: speedType,
|
||||
quantity: speedPerInterval,
|
||||
start: WorkoutTracking.lastDateMetric,
|
||||
end: Date())
|
||||
workoutBuilder.add([speedPerIntervalSample]) {(success, error) in
|
||||
if let error = error {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
WorkoutTracking.lastDateMetric = Date()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
docker build -t qdomyos-zwift-vnc .
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
docker build -t qdomyos-zwift-webgl .
|
||||
@@ -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"]
|
||||
|
||||
@@ -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/
|
||||
@@ -28,21 +28,22 @@ $ sudo ./qdomyos-zwift
|
||||
You will need to (at a minimum) to install the xcode Command Line Tools (CLI) thanks to @richardwait
|
||||
https://developer.apple.com/download/more/?=xcode
|
||||
|
||||
Download and install https://download.qt.io/archive/qt/5.12/5.12.12/qt-opensource-mac-x64-5.12.12.dmg and simply run the qdomyos-zwift release for MacOs
|
||||
Download and install http://download.qt.io/official_releases/qt/5.12/5.12.9/qt-opensource-mac-x64-5.12.9.dmg and simply run the qdomyos-zwift release for MacOs
|
||||
|
||||
## On Raspberry Pi Zero W
|
||||
|
||||

|
||||
|
||||
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 equipement.
|
||||
|
||||
`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`
|
||||
|
||||

|
||||
|
||||
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 raspeberry 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.
|
||||
|
||||
@@ -18,7 +18,7 @@ Please refer to this article for more information under [QML Operations](https:/
|
||||
|
||||
## Configuration in NativeQT mode
|
||||
|
||||
This is the list of settings available in the application. These settings need to be appended to the binary command line.
|
||||
This is the list of settings available in the application. These settings needs to be appended to the binary command line.
|
||||
*Example :* `sudo ./qdomyos-zwift -no-gui` for disabling any graphical interface.
|
||||
|
||||
| **Option** | **Type** | **Default** | **Function** |
|
||||
@@ -35,8 +35,8 @@ This is the list of settings available in the application. These settings need t
|
||||
| -heart-service | Boolean | True | Simulate HR service (required for applications not reading FTMS) |
|
||||
| -only-virtualbike | Boolean | False | |
|
||||
| -only-virtualtreadmill | Boolean | False | |
|
||||
| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipment if enabled |
|
||||
| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipment |
|
||||
| -no-reconnection | Boolean | False | QZ will not try to reconnect your fitness equipement if enabled |
|
||||
| -bluetooth-relaxed | Boolean | False | In case of deconnections from QZ to your fitness equipement |
|
||||
| -bike-cadence-sensor | Boolean | False | |
|
||||
| -bike-power-sensor | Boolean | False | |
|
||||
| -battery-service | Boolean | False | |
|
||||
@@ -45,7 +45,7 @@ This is the list of settings available in the application. These settings need t
|
||||
| -run-cadence-sensor | Boolean | False | |
|
||||
| -nordictrack-10-treadmill | Boolean | False | Enable NordicTrack compatibility mode |
|
||||
| -train | String | | Force training program |
|
||||
| -name | String | | Force bluetooth device name (if QZ struggles to find your fitness equipment) |
|
||||
| -name | String | | Force bluetooth device name (if QZ struggles finding your fitness equipment) |
|
||||
| -poll-device-time | Int | 200 (ms) | Frequency to refresh information from QZ to Fitness equipment |
|
||||
| -bike-resistance-gain | Int | | Adjust resistance from the fitness application |
|
||||
| -bike-resistance-offset | Int | | Set another resistance point than default |
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
# QDomyos-Zwift Guide to Writing Unit Tests
|
||||
|
||||
## About
|
||||
|
||||
The testing project tst/qdomyos-zwift-tests.pro contains test code that uses the Google Test library.
|
||||
|
||||
## Adding a new device
|
||||
|
||||
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.
|
||||
|
||||
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(...).
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
## Tools in the Test Framework
|
||||
|
||||
### TestSettings
|
||||
|
||||
The detection of many devices depends on settings that are accessed programmatically using the QSettings class and the constants in the QZSettings namespace. The TestSettings class stores a QSettings object with what is intended to be a unique application and organisation name, to keep the configuration it represents seperate from others in the system. It also makes the stored QSettings object the default by setting the QCoreApplication's organisation and application names to those of the QSettings object. The original values are restored by calling the deactivate() function or on object destruction.
|
||||
|
||||
i.e. a test will
|
||||
* apply a configuration from a TestSettings object
|
||||
* perform device detection
|
||||
* use the TestSettings object to restore the previous settings either directly or by letting its destructor be called.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
### Recognition by Name
|
||||
|
||||
Consider the detection code for the Domyos Bike:
|
||||
|
||||
```
|
||||
} else if (b.name().startsWith(QStringLiteral("Domyos-Bike")) &&
|
||||
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
|
||||
|
||||
```
|
||||
|
||||
Reading this, to identify this device:
|
||||
- bluetooth name should start with "Domyos-Bike" using a case sensitive comparison
|
||||
- bluetooth name should NOT start with "DomyosBridge", also using a case sensitive comparison
|
||||
- there should not have been a device using the corresponding device class detected already (i.e. domyos)
|
||||
- filter has not been activated (this isn't tested)
|
||||
|
||||
In this case, we are not testing the last two, but can test the first two.
|
||||
|
||||
In deviceindex.h:
|
||||
|
||||
```
|
||||
static const QString DomyosBike;
|
||||
```
|
||||
|
||||
In deviceindex.cpp:
|
||||
|
||||
```
|
||||
DEFINE_DEVICE(DomyosBike, "Domyos Bike");
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
The computrainer_serialport QZSettings key should be registered in devicediscoveryinfo.cpp
|
||||
|
||||
In devicediscoveryinfo.cpp:
|
||||
```
|
||||
void InitializeTrackedSettings() {
|
||||
|
||||
...
|
||||
|
||||
trackedSettings.insert(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport);
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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 ""
|
||||
|
||||
DeviceTestDataIndex::Initialize():
|
||||
|
||||
```
|
||||
// Computrainer Bike
|
||||
RegisterNewDeviceTestData(DeviceIndex::ComputrainerBike)
|
||||
->expectDevice<computrainerbike>()
|
||||
->acceptDeviceName("", DeviceNameComparison::StartsWithIgnoreCase)
|
||||
->configureSettingsWith(QZSettings::computrainer_serialport, "COMX", "");
|
||||
```
|
||||
|
||||
|
||||
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);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
A more complicated example is the Pafers Treadmill. It involves a name match, but also some configuration settings obtained earlier...
|
||||
|
||||
```
|
||||
bool pafers_treadmill = settings.value(QZSettings::pafers_treadmill, QZSettings::default_pafers_treadmill).toBool();
|
||||
...
|
||||
|
||||
bool pafers_treadmill_bh_iboxster_plus =
|
||||
settings
|
||||
.value(QZSettings::pafers_treadmill_bh_iboxster_plus, QZSettings::default_pafers_treadmill_bh_iboxster_plus)
|
||||
.toBool();
|
||||
...
|
||||
|
||||
} else if (b.name().toUpper().startsWith(QStringLiteral("PAFERS_")) && !pafersTreadmill &&
|
||||
(pafers_treadmill || pafers_treadmill_bh_iboxster_plus) && filter) {
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
```
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
// 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;
|
||||
}
|
||||
|
||||
int key=0;
|
||||
info.DeviceInfo()->setManufacturerData(key++, hex2bytes("02010639009F00000000000000000014008001"));
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
// Stages Bike General
|
||||
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
|
||||
|
||||
//
|
||||
// ... other stages bike variants
|
||||
//
|
||||
|
||||
// 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));
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
In this case, it populates the vector with the single enabling configuration if that's what's been requested, otherwise 3 disabling ones.
|
||||
|
||||
### Exclusions
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
// Echelon Connect Sport Bike
|
||||
RegisterNewDeviceTestData(DeviceIndex::EchelonConnectSportBike)
|
||||
->expectDevice<echelonconnectsport>()
|
||||
->acceptDeviceName("ECH", DeviceNameComparison::StartsWith)
|
||||
->excluding<echelonrower>()
|
||||
->excluding<echelonstride>();
|
||||
|
||||
```
|
||||
|
||||
### When a single test data object can't cover all the conditions
|
||||
|
||||
Detection code:
|
||||
|
||||
```
|
||||
QString powerSensorName =
|
||||
settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name).toString();
|
||||
...
|
||||
} else if ((b.name().toUpper().startsWith(QStringLiteral("STAGES ")) ||
|
||||
(b.name().toUpper().startsWith("TACX SATORI")) ||
|
||||
((b.name().toUpper().startsWith("KICKR CORE")) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && deviceHasService(b, QBluetoothUuid((quint16)0x1818))) ||
|
||||
(b.name().toUpper()==QStringLiteral("QD")) ||
|
||||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
|
||||
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
|
||||
!stagesBike && !ftmsBike && filter) {
|
||||
```
|
||||
|
||||
This presents 3 scenarios for the current test framework.
|
||||
1. Match names only (starts with:"STAGES ", starts with: "TACX SATORI", equals: "QD")
|
||||
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.
|
||||
|
||||
In the example of the Stages Bike test data, the exclusions, which apply to all situations, are implemented in an array of type ids:
|
||||
|
||||
|
||||
```
|
||||
// Stages Bike General
|
||||
auto stagesBikeExclusions = { GetTypeId<ftmsbike>() };
|
||||
```
|
||||
|
||||
The name-match only in one test data instance:
|
||||
|
||||
```
|
||||
// Stages Bike
|
||||
RegisterNewDeviceTestData(DeviceIndex::StagesBike)
|
||||
->expectDevice<stagesbike>()
|
||||
->acceptDeviceNames({"STAGES ", "TACX SATORI"}, DeviceNameComparison::StartsWithIgnoreCase)
|
||||
->acceptDeviceName("QD", DeviceNameComparison::IgnoreCase)
|
||||
->excluding(stagesBikeExclusions);
|
||||
```
|
||||
|
||||
The name and setting match in another instance:
|
||||
|
||||
```
|
||||
// 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 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));
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
143
helpers/winbt.py
143
helpers/winbt.py
@@ -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))
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
QMAKE_PRL_BUILD_DIR = C:/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth
|
||||
QMAKE_PRO_INPUT = bluetooth.pro
|
||||
QMAKE_PRL_TARGET = Qt5Bluetooth.lib
|
||||
QMAKE_PRL_CONFIG = lex yacc depend_includepath testcase_targets import_plugins import_qpa_plugin windows prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on release link_prl flat debug_and_release precompile_header autogen_precompile_source embed_manifest_dll embed_manifest_exe shared shared release no_plugin_manifest win32 msvc copy_dir_files sse2 aesni sse3 ssse3 sse4_1 sse4_2 avx avx2 avx512f avx512bw avx512cd avx512dq avx512er avx512ifma avx512pf avx512vbmi avx512vl compile_examples f16c force_debug_info largefile precompile_header rdrnd rdseed shani x86SimdAlways prefix_build force_independent utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions release ReleaseBuild Release build_pass c++11 generated_privates relative_qt_rpath target_qt c++11 strict_c++ c++14 c++1z qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake skip_target_version_ext release ReleaseBuild Release build_pass have_target dll exclusive_builds debug_info no_autoqmake thread moc resources
|
||||
QMAKE_PRL_VERSION = 5.15.2
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
QMAKE_PRL_BUILD_DIR = C:/qt-everywhere-src-5.15.2/qtconnectivity/src/bluetooth
|
||||
QMAKE_PRO_INPUT = bluetooth.pro
|
||||
QMAKE_PRL_TARGET = Qt5Bluetoothd.lib
|
||||
QMAKE_PRL_CONFIG = lex yacc debug depend_includepath testcase_targets import_plugins import_qpa_plugin windows prepare_docs qt_docs_targets qt_build_extra file_copies qmake_use qt warn_on link_prl flat debug_and_release precompile_header autogen_precompile_source embed_manifest_dll embed_manifest_exe shared shared no_plugin_manifest win32 msvc copy_dir_files sse2 aesni sse3 ssse3 sse4_1 sse4_2 avx avx2 avx512f avx512bw avx512cd avx512dq avx512er avx512ifma avx512pf avx512vbmi avx512vl compile_examples f16c force_debug_info largefile precompile_header rdrnd rdseed shani x86SimdAlways prefix_build force_independent utf8_source create_prl link_prl no_private_qt_headers_warning QTDIR_build qt_example_installs exceptions_off testcase_exceptions debug DebugBuild Debug build_pass c++11 generated_privates relative_qt_rpath target_qt c++11 strict_c++ c++14 c++1z qt_install_headers need_fwd_pri qt_install_module debug_and_release build_all create_cmake skip_target_version_ext debug DebugBuild Debug build_pass have_target dll no_plist exclusive_builds debug_info no_autoqmake thread moc resources
|
||||
QMAKE_PRL_VERSION = 5.15.2
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,25 +0,0 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Loader {
|
||||
id: chartFooterLoader
|
||||
sourceComponent: ChartFooterInnerJS
|
||||
anchors.fill: parent
|
||||
active: false
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
source: CHARTJS ? "ChartFooterInnerJS.qml":"ChartFooterInnerNoJS.qml"
|
||||
onLoaded: {
|
||||
if(CHARTJS) {
|
||||
chartFooterLoader.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
import QtWebView 1.1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
property int chart_display_mode: 0
|
||||
}
|
||||
WebView {
|
||||
id: webView
|
||||
anchors.fill: parent
|
||||
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/chartjs/chartlive.htm"
|
||||
visible: rootItem.chartFooterVisible
|
||||
onLoadingChanged: {
|
||||
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: {
|
||||
console.log("onVisibleChanged" + visible)
|
||||
if(visible === true) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 + ");
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.0
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Settings {
|
||||
id: settings
|
||||
}
|
||||
}
|
||||
@@ -902,7 +902,6 @@ int Computrainer::rawWrite(uint8_t *bytes, int size) // unix!!
|
||||
b[i] = bytes[i];
|
||||
env->SetByteArrayRegion(d, 0, size, b);
|
||||
QAndroidJniObject::callStaticMethod<void>("org/cagnulen/qdomyoszwift/Usbserial", "write", "([B)V", d);
|
||||
env->DeleteLocalRef(d);
|
||||
#elif defined(WIN32)
|
||||
DWORD cBytes;
|
||||
rc = WriteFile(devicePort, bytes, size, &cBytes, NULL);
|
||||
@@ -948,38 +947,12 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
|
||||
QAndroidJniEnvironment env;
|
||||
int timeout = 0;
|
||||
int maxRetries = 100; // Maximum number of retries (100 * 50ms = 5 seconds timeout)
|
||||
int retryCount = 0;
|
||||
|
||||
while (fullLen < size && retryCount < maxRetries) {
|
||||
// Push a new local frame to automatically manage JNI references
|
||||
// This prevents local reference table overflow by cleaning up refs at the end of each iteration
|
||||
if (env->PushLocalFrame(16) < 0) {
|
||||
qDebug() << "Failed to push local frame";
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (fullLen < size) {
|
||||
QAndroidJniObject dd =
|
||||
QAndroidJniObject::callStaticObjectMethod("org/cagnulen/qdomyoszwift/Usbserial", "read", "()[B");
|
||||
jint len = QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/Usbserial", "readLen", "()I");
|
||||
jbyteArray d = dd.object<jbyteArray>();
|
||||
jbyte *b = env->GetByteArrayElements(d, 0);
|
||||
|
||||
// Check if we got any data
|
||||
if (len <= 0) {
|
||||
// No data available, release memory and retry after a short sleep
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
qDebug() << "No data available, retry" << retryCount + 1 << "of" << maxRetries;
|
||||
CTsleeper::msleep(50); // Sleep for 50ms before retrying
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset retry counter when we get data
|
||||
retryCount = 0;
|
||||
|
||||
if (len + fullLen > size) {
|
||||
QByteArray tmpDebug;
|
||||
qDebug() << "buffer overflow! Truncate from" << len + fullLen << "requested" << size;
|
||||
@@ -997,10 +970,6 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
qDebug() << len + fullLen - size << "bytes to the rxBuf" << tmpDebug.toHex(' ');
|
||||
qDebug() << size << QByteArray((const char *)b, size).toHex(' ');
|
||||
|
||||
// Release JNI memory before returning
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
return size;
|
||||
}
|
||||
for (int i = fullLen; i < len + fullLen; i++) {
|
||||
@@ -1008,16 +977,6 @@ int Computrainer::rawRead(uint8_t bytes[], int size) {
|
||||
}
|
||||
qDebug() << len << QByteArray((const char *)b, len).toHex(' ');
|
||||
fullLen += len;
|
||||
|
||||
// Release JNI memory after processing
|
||||
env->ReleaseByteArrayElements(d, b, 0);
|
||||
env->PopLocalFrame(NULL); // Pop frame to release all local refs created in this iteration
|
||||
}
|
||||
|
||||
// Check if we timed out
|
||||
if (retryCount >= maxRetries) {
|
||||
qDebug() << "rawRead timeout: no data after" << maxRetries << "retries";
|
||||
return -1; // Timeout error
|
||||
}
|
||||
|
||||
qDebug() << "FULL BUFFER RX: << " << fullLen << QByteArray((const char *)bytes, size).toHex(' ');
|
||||
@@ -37,9 +37,12 @@
|
||||
#include <QThread>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#include <windef.h>
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winbase.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/ioctl.h>
|
||||
#include <termios.h> // unix!!
|
||||
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
framework module ConnectIQ {
|
||||
umbrella header "ConnectIQ.h"
|
||||
export *
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
Binary file not shown.
@@ -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.
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "IQDevice.h"
|
||||
#import "IQAppStatus.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user