Compare commits

..

17 Commits

Author SHA1 Message Date
Roberto Viola
f48ee36ef3 Merge branch 'master' into android_5_fix 2024-12-01 15:16:11 +01:00
Roberto Viola
1e1e6cf4bf Merge branch 'master' into android_5_fix 2024-12-01 15:15:41 +01:00
Roberto Viola
19105f4c23 Merge branch 'master' into android_5_fix 2024-11-29 11:54:49 +01:00
Roberto Viola
148d8530aa removing TTS 2024-03-18 21:14:30 +01:00
Roberto Viola
b25334d669 Update InAppPurchase.java 2024-03-18 20:09:06 +01:00
Roberto Viola
e8b44ecb07 Update InAppPurchase.java 2024-03-18 19:45:12 +01:00
Roberto Viola
3692362ada Revert "trying qt 5.15.2"
This reverts commit cbb7845ec1.
2024-03-17 09:03:03 +01:00
Roberto Viola
6eddbf754b Revert "Update main.yml"
This reverts commit 587e2da456.
2024-03-17 09:03:00 +01:00
Roberto Viola
0178b8d28b Revert "Update main.yml"
This reverts commit 3f8226a7e9.
2024-03-17 09:02:49 +01:00
Roberto Viola
0e9556065c Revert "Update main.yml"
This reverts commit 9342b8018f.
2024-03-17 09:02:44 +01:00
Roberto Viola
9342b8018f Update main.yml 2024-03-16 17:37:09 +01:00
Roberto Viola
3f8226a7e9 Update main.yml 2024-03-16 17:25:41 +01:00
Roberto Viola
587e2da456 Update main.yml 2024-03-16 16:26:51 +01:00
Roberto Viola
cbb7845ec1 trying qt 5.15.2 2024-03-16 16:16:52 +01:00
Roberto Viola
8aa8ef7983 Update androidjni.cpp 2024-03-16 15:41:59 +01:00
Roberto Viola
ec107e42f5 Update androidjni.cpp 2024-03-15 22:06:02 +01:00
Roberto Viola
f4a010c540 Update androidjni.cpp 2024-03-15 21:53:19 +01:00
653 changed files with 11360 additions and 67689 deletions

File diff suppressed because it is too large Load Diff

3
.gitignore vendored
View File

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

2
.gitmodules vendored
View File

@@ -12,7 +12,7 @@
[submodule "tst/googletest"]
path = tst/googletest
url = https://github.com/google/googletest.git
tag = release-1.12.1
branch = tags/release-1.12.1
[submodule "src/qthttpserver"]
path = src/qthttpserver
url = https://github.com/qt-labs/qthttpserver

16
.vscode/launch.json vendored
View File

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

375
CLAUDE.md
View File

@@ -1,375 +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
- #usa le qdebug invece che le emit debug

View File

@@ -96,9 +96,6 @@ Zwift bridge for Treadmills and Bike!
|:---|:---:|:---:|:---:|:---:|---:|
|Resistance shifting with bluetooth remote|X||X|||
|TTS support|X|X|X|X||
|Zwift Play & Click support|X|||||
|MQTT integration|X|X|X|X||
|OpenSoundControl integration|X|X|X|X||
### Installation

View File

@@ -286,14 +286,8 @@
8752C0E92B15D85600C3D1A5 /* ios_eliteariafan.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8752C0E62B15D85600C3D1A5 /* ios_eliteariafan.mm */; };
87540FAD2848FD70005E0D44 /* libqtexttospeech_speechios.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */; };
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8754D24B27F786F0003D7054 /* virtualrower.swift */; };
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */; };
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8755E5D22E4E260B006A12E4 /* fontmanager.cpp */; };
87586A4125B8340E00A243C4 /* proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4025B8340E00A243C4 /* proformbike.cpp */; };
87586A4325B8341B00A243C4 /* moc_proformbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87586A4225B8341B00A243C4 /* moc_proformbike.cpp */; };
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */; };
875CA9492D0C742500667EE6 /* kineticinroadbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9482D0C742500667EE6 /* kineticinroadbike.cpp */; };
875CA94C2D130F8100667EE6 /* moc_osc.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA94B2D130F8100667EE6 /* moc_osc.cpp */; };
875CA9552D130FBC00667EE6 /* osc.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875CA9502D130FBC00667EE6 /* osc.cpp */; };
875F69B926342E8D0009FD78 /* spirittreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875F69B826342E8D0009FD78 /* spirittreadmill.cpp */; };
875F69BB26342E9A0009FD78 /* moc_spirittreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 875F69BA26342E9A0009FD78 /* moc_spirittreadmill.cpp */; };
8762D50F2601F7EA00F6F049 /* M3iNS.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8762D50B2601F7EA00F6F049 /* M3iNS.mm */; };
@@ -301,10 +295,6 @@
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8762D5112601F89500F6F049 /* scanrecordresult.cpp */; };
87646C2027B5064600F82131 /* bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */; };
87646C2227B5065100F82131 /* moc_bhfitnesselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */; };
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */; };
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */; };
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */; };
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */; };
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */; };
8768C8BA2BBC11C80099DBE1 /* file_sync_client.c in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8768C89C2BBC11C70099DBE1 /* file_sync_client.c */; };
8768C8BB2BBC11C80099DBE1 /* protocol.txt in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 8768C89D2BBC11C70099DBE1 /* protocol.txt */; };
@@ -342,8 +332,6 @@
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9927BE35C4001D7645 /* bowflext216treadmill.cpp */; };
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */; };
876BFCA127BE35D8001D7645 /* moc_bowflext216treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */; };
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */; };
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */; };
876E4E142594748000BD5714 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 876E4E132594748000BD5714 /* Assets.xcassets */; };
876E4E1B2594748000BD5714 /* watchkit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
876E4E202594748000BD5714 /* qdomyoszwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876E4E1F2594748000BD5714 /* qdomyoszwiftApp.swift */; };
@@ -375,7 +363,6 @@
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772A0E725E43AE70080718C /* moc_trxappgateusbbike.cpp */; };
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */; };
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */; };
877350F72D1C08E60070CBD8 /* SmartControl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877350F62D1C08E50070CBD8 /* SmartControl.cpp */; };
8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008129E876F7008E48B7 /* iconceptelliptical.cpp */; };
8775008529E87713008E48B7 /* moc_iconceptelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */; };
877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 877758B22C98627300BB1697 /* moc_sportstechelliptical.cpp */; };
@@ -392,22 +379,14 @@
8781908526150C8E0085E656 /* libqtlabsplatformplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 8781908126150B490085E656 /* libqtlabsplatformplugin.a */; };
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */; };
8783153C25E8DAFD0007817C /* sportstechbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */; };
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */; };
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */; };
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */; };
878531642711A3E1004B153D /* fakebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531602711A3E0004B153D /* fakebike.cpp */; };
878531652711A3E1004B153D /* activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531612711A3E1004B153D /* activiotreadmill.cpp */; };
878531682711A3EC004B153D /* moc_activiotreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531662711A3EB004B153D /* moc_activiotreadmill.cpp */; };
878531692711A3EC004B153D /* moc_fakebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878531672711A3EB004B153D /* moc_fakebike.cpp */; };
8785D5432B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */; };
8785D5442B3DD105005A2EB7 /* moc_zwift_client_auth.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */; };
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */; };
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */; };
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */; };
878A331A25AB4FF800BD13E1 /* yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331725AB4FF800BD13E1 /* yesoulbike.cpp */; };
878A331D25AB50C300BD13E1 /* moc_yesoulbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */; };
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */; };
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */; };
878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */; };
878C9E6B28B77E9800669129 /* moc_nordictrackifitadbbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */; };
878D83742A1F33C600D7F004 /* bkoolbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 878D83732A1F33C600D7F004 /* bkoolbike.cpp */; };
@@ -427,7 +406,6 @@
87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87958F1A27628D5400124B24 /* moc_elitesterzosmart.cpp */; };
8798C8872733E103003148B3 /* strydrunpowersensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8798C8862733E103003148B3 /* strydrunpowersensor.cpp */; };
8798C8892733E10E003148B3 /* moc_strydrunpowersensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8798C8882733E10E003148B3 /* moc_strydrunpowersensor.cpp */; };
8798FDC52D66075B00CF8EE8 /* OSXBtManagerInternal.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8798FDC32D66075B00CF8EE8 /* OSXBtManagerInternal.mm */; };
879A38C8281BD83300F78B2A /* characteristicnotifier2ad9.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 879A38C7281BD83300F78B2A /* characteristicnotifier2ad9.cpp */; };
879E5AA8289C057E00FEA38A /* proformwifitreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 879E5AA6289C057E00FEA38A /* proformwifitreadmill.cpp */; };
879E5AAA289C05A500FEA38A /* moc_proformwifitreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 879E5AA9289C05A500FEA38A /* moc_proformwifitreadmill.cpp */; };
@@ -450,19 +428,13 @@
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A18F052660D5C0002D7C96 /* ftmsrower.cpp */; };
87A18F092660D5D9002D7C96 /* moc_ftmsrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A18F082660D5D9002D7C96 /* moc_ftmsrower.cpp */; };
87A2E0222B2B053E00E6168F /* swiftDebug.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A2E0212B2B053E00E6168F /* swiftDebug.mm */; };
87A33F1A2D611D8400BFFF29 /* moc_logwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A33F192D611D8400BFFF29 /* moc_logwriter.cpp */; };
87A33F1D2D611D9500BFFF29 /* logwriter.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A33F1C2D611D9500BFFF29 /* logwriter.cpp */; };
87A3BC222656429600D302E3 /* rower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3BC1F2656429400D302E3 /* rower.cpp */; };
87A3BC232656429600D302E3 /* echelonrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3BC202656429400D302E3 /* echelonrower.cpp */; };
87A3BC26265642A300D302E3 /* moc_rower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3BC24265642A200D302E3 /* moc_rower.cpp */; };
87A3BC27265642A300D302E3 /* moc_echelonrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3BC25265642A200D302E3 /* moc_echelonrower.cpp */; };
87A3DD9B2D3413790060BAEB /* moc_lifespantreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3DD9A2D3413790060BAEB /* moc_lifespantreadmill.cpp */; };
87A3DD9C2D3413790060BAEB /* lifespantreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A3DD992D3413790060BAEB /* lifespantreadmill.cpp */; };
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A4B75F25AF27CB0027EF3C /* metric.cpp */; };
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */; };
87A6825D2CE3AB4000586A2A /* sramAXSController.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */; };
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */; };
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */; };
87ADD2BB27634C1500B7A0AB /* technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */; };
87ADD2BD27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */; };
87AE0CB227760DCB00E547E9 /* virtualtreadmill_zwift.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87AE0CB127760DCB00E547E9 /* virtualtreadmill_zwift.swift */; };
@@ -488,7 +460,6 @@
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BE6FDD272D2A3E00C35795 /* moc_horizongr7bike.cpp */; };
87BF116D298E28CA00B5B6E7 /* pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */; };
87BF116F298E28EC00B5B6E7 /* moc_pelotonbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */; };
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */; };
87C424262BC1294000503687 /* moc_treadmillErgTable.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */; };
87C481FA26DFA7C3006211AD /* eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481F926DFA7C3006211AD /* eliterizer.cpp */; };
87C481FC26DFA7D1006211AD /* moc_eliterizer.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C481FB26DFA7D1006211AD /* moc_eliterizer.cpp */; };
@@ -540,12 +511,6 @@
87D5DC4228230496008CCDE7 /* moc_truetreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D5DC4128230496008CCDE7 /* moc_truetreadmill.cpp */; };
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F992800B9970026D43C /* proformwifibike.cpp */; };
87D91F9C2800B9B90026D43C /* moc_proformwifibike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */; };
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */; };
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */; };
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */; };
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */; };
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */; };
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */; };
87DA76502848F98200A71B64 /* libQt5TextToSpeech.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */; };
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8464284933D200B550E9 /* fakeelliptical.cpp */; };
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DA8466284933DE00B550E9 /* moc_fakeelliptical.cpp */; };
@@ -555,12 +520,6 @@
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */; };
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */; };
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */; };
87DED80627D1273900BE4FBB /* filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80427D1273800BE4FBB /* filedownloader.cpp */; };
87DED80827D1274600BE4FBB /* moc_filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */; };
87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DF68B625E2673600FCDA46 /* eslinkertreadmill.cpp */; };
@@ -578,8 +537,6 @@
87E5D2C825E69F4700BDBE6C /* moc_horizontreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87E5D2C725E69F4700BDBE6C /* moc_horizontreadmill.cpp */; };
87E6A85825B5C88E00371D28 /* moc_flywheelbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87E6A85725B5C88E00371D28 /* moc_flywheelbike.cpp */; };
87E6A85B25B5C8B900371D28 /* flywheelbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87E6A85925B5C8B900371D28 /* flywheelbike.cpp */; };
87EAC3D32D1D8D1D004FE975 /* moc_pitpatbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EAC3D22D1D8D1D004FE975 /* moc_pitpatbike.cpp */; };
87EAC3D62D1D8D34004FE975 /* pitpatbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EAC3D52D1D8D34004FE975 /* pitpatbike.cpp */; };
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917427EE5FB3002535E1 /* nautilusbike.cpp */; };
87EB918227EE5FE7002535E1 /* moc_inapptransaction.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917727EE5FE7002535E1 /* moc_inapptransaction.cpp */; };
87EB918327EE5FE7002535E1 /* moc_inappstore.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917827EE5FE7002535E1 /* moc_inappstore.cpp */; };
@@ -589,12 +546,6 @@
87EB918A27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */; };
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */; };
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */; };
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */; };
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A52D39214E00348B15 /* workoutmodel.cpp */; };
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */; };
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */; };
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */; };
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
@@ -602,10 +553,6 @@
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */; };
87F1179E26A5FBDE00541B3A /* libqtwebview_darwin.a in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87420DF7269D7CE1000C5EC6 /* libqtwebview_darwin.a */; };
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */; };
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD662DBFBCE700416506 /* android_antbike.cpp */; };
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD702DC0D59600416506 /* moc_coresensor.cpp */; };
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F1BD6F2DC0D59600416506 /* coresensor.cpp */; };
87F4FB5A29D550C00061BB4A /* schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */; };
87F4FB5C29D550E00061BB4A /* moc_schwinn170bike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */; };
87F527BE28EEB5AA00A9F8D5 /* qzsettings.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F527BC28EEB5AA00A9F8D5 /* qzsettings.cpp */; };
@@ -614,10 +561,6 @@
87FA11AB27C5ECD1008AC5D1 /* ultrasportbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FA11AA27C5ECD1008AC5D1 /* ultrasportbike.cpp */; };
87FA11AD27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FA11AC27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp */; };
87FA94672B6B89FD00B6AB9A /* SwiftUI.framework in Link Binary With Libraries */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
87FC40BC2D2E74F9008BA736 /* cycleopsphantombike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FC40BA2D2E74F9008BA736 /* cycleopsphantombike.cpp */; };
87FC40BD2D2E74F9008BA736 /* moc_cycleopsphantombike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FC40BB2D2E74F9008BA736 /* moc_cycleopsphantombike.cpp */; };
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */; };
87FE06842D170D5600CDAAF6 /* trxappgateusbrower.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */; };
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FE5BAD2692F3130056EFC8 /* tacxneo2.cpp */; };
87FE5BB12692F31E0056EFC8 /* moc_tacxneo2.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FE5BB02692F31E0056EFC8 /* moc_tacxneo2.cpp */; };
87FFA13727BBE3FF00924E4E /* solebike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87FFA13527BBE3FE00924E4E /* solebike.cpp */; };
@@ -1211,24 +1154,9 @@
8752C0E72B15D85600C3D1A5 /* ios_eliteariafan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ios_eliteariafan.h; path = ../src/ios/ios_eliteariafan.h; sourceTree = "<group>"; };
87540FAC2848FD70005E0D44 /* libqtexttospeech_speechios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtexttospeech_speechios.a; path = ../../Qt/5.15.2/ios/plugins/texttospeech/libqtexttospeech_speechios.a; sourceTree = "<group>"; };
8754D24B27F786F0003D7054 /* virtualrower.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = virtualrower.swift; path = ../src/ios/virtualrower.swift; sourceTree = "<group>"; };
8755E5D12E4E260B006A12E4 /* fontmanager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fontmanager.h; path = ../src/fontmanager.h; sourceTree = SOURCE_ROOT; };
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fontmanager.cpp; path = ../src/fontmanager.cpp; sourceTree = SOURCE_ROOT; };
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fontmanager.cpp; sourceTree = "<group>"; };
87586A3F25B8340D00A243C4 /* proformbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformbike.h; path = ../src/devices/proformbike/proformbike.h; sourceTree = "<group>"; };
87586A4025B8340E00A243C4 /* proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformbike.cpp; path = ../src/devices/proformbike/proformbike.cpp; sourceTree = "<group>"; };
87586A4225B8341B00A243C4 /* moc_proformbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformbike.cpp; sourceTree = "<group>"; };
875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kineticinroadbike.cpp; sourceTree = "<group>"; };
875CA9472D0C742500667EE6 /* kineticinroadbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = kineticinroadbike.h; path = ../src/devices/kineticinroadbike/kineticinroadbike.h; sourceTree = SOURCE_ROOT; };
875CA9482D0C742500667EE6 /* kineticinroadbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = kineticinroadbike.cpp; path = ../src/devices/kineticinroadbike/kineticinroadbike.cpp; sourceTree = SOURCE_ROOT; };
875CA94B2D130F8100667EE6 /* moc_osc.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_osc.cpp; sourceTree = "<group>"; };
875CA94D2D130FBC00667EE6 /* client.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = client.hpp; path = ../src/oscpp/client.hpp; sourceTree = SOURCE_ROOT; };
875CA94E2D130FBC00667EE6 /* error.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = error.hpp; path = ../src/oscpp/error.hpp; sourceTree = SOURCE_ROOT; };
875CA94F2D130FBC00667EE6 /* osc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = osc.h; path = ../src/osc.h; sourceTree = SOURCE_ROOT; };
875CA9502D130FBC00667EE6 /* osc.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = osc.cpp; path = ../src/osc.cpp; sourceTree = SOURCE_ROOT; };
875CA9512D130FBC00667EE6 /* print.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = print.hpp; path = ../src/oscpp/print.hpp; sourceTree = SOURCE_ROOT; };
875CA9522D130FBC00667EE6 /* server.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = server.hpp; path = ../src/oscpp/server.hpp; sourceTree = SOURCE_ROOT; };
875CA9532D130FBC00667EE6 /* types.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = types.hpp; path = ../src/oscpp/types.hpp; sourceTree = SOURCE_ROOT; };
875CA9542D130FBC00667EE6 /* util.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = util.hpp; path = ../src/oscpp/util.hpp; sourceTree = SOURCE_ROOT; };
875F69B726342E8D0009FD78 /* spirittreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = spirittreadmill.h; path = ../src/devices/spirittreadmill/spirittreadmill.h; sourceTree = "<group>"; };
875F69B826342E8D0009FD78 /* spirittreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = spirittreadmill.cpp; path = ../src/devices/spirittreadmill/spirittreadmill.cpp; sourceTree = "<group>"; };
875F69BA26342E9A0009FD78 /* moc_spirittreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_spirittreadmill.cpp; sourceTree = "<group>"; };
@@ -1241,13 +1169,6 @@
87646C1E27B5064500F82131 /* bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bhfitnesselliptical.cpp; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.cpp; sourceTree = "<group>"; };
87646C1F27B5064500F82131 /* bhfitnesselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bhfitnesselliptical.h; path = ../src/devices/bhfitnesselliptical/bhfitnesselliptical.h; sourceTree = "<group>"; };
87646C2127B5065100F82131 /* moc_bhfitnesselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bhfitnesselliptical.cpp; sourceTree = "<group>"; };
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = elitesquarecontroller.h; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.h; sourceTree = SOURCE_ROOT; };
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = elitesquarecontroller.cpp; path = ../src/devices/elitesquarecontroller/elitesquarecontroller.cpp; sourceTree = SOURCE_ROOT; };
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elitesquarecontroller.cpp; sourceTree = "<group>"; };
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_wahookickrsnapbike.h; path = ../src/ios/ios_wahookickrsnapbike.h; sourceTree = SOURCE_ROOT; };
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_wahookickrsnapbike.mm; path = ../src/ios/ios_wahookickrsnapbike.mm; sourceTree = SOURCE_ROOT; };
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_zwiftclickremote.h; path = ../src/ios/ios_zwiftclickremote.h; sourceTree = SOURCE_ROOT; };
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_zwiftclickremote.mm; path = ../src/ios/ios_zwiftclickremote.mm; sourceTree = SOURCE_ROOT; };
8767EF1D29448D6700810C0F /* characteristicwriteprocessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor.cpp; path = ../src/characteristics/characteristicwriteprocessor.cpp; sourceTree = "<group>"; };
8768C89C2BBC11C70099DBE1 /* file_sync_client.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = file_sync_client.c; path = ../src/ios/adb/adb/file_sync_client.c; sourceTree = "<group>"; };
8768C89D2BBC11C70099DBE1 /* protocol.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = protocol.txt; path = ../src/ios/adb/adb/protocol.txt; sourceTree = "<group>"; };
@@ -1300,9 +1221,6 @@
876BFC9B27BE35C5001D7645 /* proformelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformelliptical.h; path = ../src/devices/proformelliptical/proformelliptical.h; sourceTree = "<group>"; };
876BFC9E27BE35D8001D7645 /* moc_proformelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformelliptical.cpp; sourceTree = "<group>"; };
876BFC9F27BE35D8001D7645 /* moc_bowflext216treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_bowflext216treadmill.cpp; sourceTree = "<group>"; };
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitbackupwriter.h; path = ../src/fitbackupwriter.h; sourceTree = SOURCE_ROOT; };
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitbackupwriter.cpp; path = ../src/fitbackupwriter.cpp; sourceTree = SOURCE_ROOT; };
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitbackupwriter.cpp; sourceTree = "<group>"; };
876E4E112594747F00BD5714 /* watchkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchkit.app; sourceTree = BUILT_PRODUCTS_DIR; };
876E4E132594748000BD5714 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
876E4E152594748000BD5714 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -1361,8 +1279,6 @@
8772B7F32CB55E80004AB8E9 /* moc_deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_deerruntreadmill.cpp; sourceTree = "<group>"; };
8772B7F62CB55E98004AB8E9 /* deerruntreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = deerruntreadmill.cpp; sourceTree = "<group>"; };
8772B7F92CB5603A004AB8E9 /* deerruntreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = deerruntreadmill.h; path = ../src/devices/deeruntreadmill/deerruntreadmill.h; sourceTree = SOURCE_ROOT; };
877350F52D1C08E50070CBD8 /* SmartControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SmartControl.h; path = ../src/devices/kineticinroadbike/SmartControl.h; sourceTree = SOURCE_ROOT; };
877350F62D1C08E50070CBD8 /* SmartControl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = SmartControl.cpp; path = ../src/devices/kineticinroadbike/SmartControl.cpp; sourceTree = SOURCE_ROOT; };
8775008129E876F7008E48B7 /* iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = iconceptelliptical.cpp; path = ../src/devices/iconceptelliptical/iconceptelliptical.cpp; sourceTree = "<group>"; };
8775008229E876F7008E48B7 /* iconceptelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iconceptelliptical.h; path = ../src/devices/iconceptelliptical/iconceptelliptical.h; sourceTree = "<group>"; };
8775008429E87712008E48B7 /* moc_iconceptelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_iconceptelliptical.cpp; sourceTree = "<group>"; };
@@ -1386,10 +1302,6 @@
878225C234983ACB863D2D29 /* fit_nmea_sentence_mesg.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_nmea_sentence_mesg.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_nmea_sentence_mesg.hpp"; sourceTree = "<absolute>"; };
8783153A25E8D81E0007817C /* moc_sportstechbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportstechbike.cpp; sourceTree = "<group>"; };
87842E7E25AF88FB00321E69 /* secret.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = secret.h; path = ../src/secret.h; sourceTree = "<group>"; };
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libqtlabscalendarplugin.a; path = ../../Qt/5.15.2/ios/qml/Qt/labs/calendar/libqtlabscalendarplugin.a; sourceTree = "<group>"; };
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbrower.cpp; sourceTree = "<group>"; };
878521D22E44B26600922796 /* nordictrackifitadbrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbrower.h; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.h; sourceTree = SOURCE_ROOT; };
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbrower.cpp; path = ../src/devices/nordictrackifitadbrower/nordictrackifitadbrower.cpp; sourceTree = SOURCE_ROOT; };
878531602711A3E0004B153D /* fakebike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakebike.cpp; path = ../src/devices/fakebike/fakebike.cpp; sourceTree = "<group>"; };
878531612711A3E1004B153D /* activiotreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = activiotreadmill.cpp; path = ../src/devices/activiotreadmill/activiotreadmill.cpp; sourceTree = "<group>"; };
878531622711A3E1004B153D /* activiotreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = activiotreadmill.h; path = ../src/devices/activiotreadmill/activiotreadmill.h; sourceTree = "<group>"; };
@@ -1400,18 +1312,10 @@
8785D5402B3DD0EC005A2EB7 /* PlayerStateWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PlayerStateWrapper.h; path = "../src/zwift-api/PlayerStateWrapper.h"; sourceTree = "<group>"; };
8785D5412B3DD105005A2EB7 /* moc_PlayerStateWrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_PlayerStateWrapper.cpp; sourceTree = "<group>"; };
8785D5422B3DD105005A2EB7 /* moc_zwift_client_auth.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_zwift_client_auth.cpp; sourceTree = "<group>"; };
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sportsplusrower.cpp; sourceTree = "<group>"; };
87873AF22D09AADF005F86B4 /* sportsplusrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportsplusrower.h; path = ../src/devices/sportsplusrower/sportsplusrower.h; sourceTree = SOURCE_ROOT; };
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = inclinationresistancetable.h; path = ../src/inclinationresistancetable.h; sourceTree = SOURCE_ROOT; };
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inclinationresistancetable.cpp; sourceTree = "<group>"; };
8789DCDB6A4F681A76DF3F92 /* Qt5Widgets */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = Qt5Widgets; path = "/Users/cagnulein/Qt/5.15.2/ios/lib/libQt5Widgets$(QT_LIBRARY_SUFFIX).a"; sourceTree = "<absolute>"; };
878A331725AB4FF800BD13E1 /* yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = yesoulbike.cpp; path = ../src/devices/yesoulbike/yesoulbike.cpp; sourceTree = "<group>"; };
878A331825AB4FF800BD13E1 /* yesoulbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = yesoulbike.h; path = ../src/devices/yesoulbike/yesoulbike.h; sourceTree = "<group>"; };
878A331B25AB50C200BD13E1 /* moc_yesoulbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_yesoulbike.cpp; sourceTree = "<group>"; };
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_speraxtreadmill.cpp; sourceTree = "<group>"; };
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = speraxtreadmill.h; path = ../src/devices/speraxtreadmill/speraxtreadmill.h; sourceTree = SOURCE_ROOT; };
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = speraxtreadmill.cpp; path = ../src/devices/speraxtreadmill/speraxtreadmill.cpp; sourceTree = SOURCE_ROOT; };
878C9E6728B77E7B00669129 /* nordictrackifitadbbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nordictrackifitadbbike.h; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.h; sourceTree = "<group>"; };
878C9E6828B77E7B00669129 /* nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nordictrackifitadbbike.cpp; path = ../src/devices/nordictrackifitadbbike/nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
878C9E6A28B77E9800669129 /* moc_nordictrackifitadbbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nordictrackifitadbbike.cpp; sourceTree = "<group>"; };
@@ -1439,11 +1343,6 @@
8798C8852733E103003148B3 /* strydrunpowersensor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = strydrunpowersensor.h; path = ../src/devices/strydrunpowersensor/strydrunpowersensor.h; sourceTree = "<group>"; };
8798C8862733E103003148B3 /* strydrunpowersensor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = strydrunpowersensor.cpp; path = ../src/devices/strydrunpowersensor/strydrunpowersensor.cpp; sourceTree = "<group>"; };
8798C8882733E10E003148B3 /* moc_strydrunpowersensor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_strydrunpowersensor.cpp; sourceTree = "<group>"; };
8798FDC02D66075B00CF8EE8 /* osxbluetooth_p.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = osxbluetooth_p.h; path = ../src/ios/BluetoothPatch/osxbluetooth_p.h; sourceTree = SOURCE_ROOT; };
8798FDC12D66075B00CF8EE8 /* osxbtcentralmanager_p.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = osxbtcentralmanager_p.h; path = ../src/ios/BluetoothPatch/osxbtcentralmanager_p.h; sourceTree = SOURCE_ROOT; };
8798FDC22D66075B00CF8EE8 /* osxbtgcdtimer_p.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = osxbtgcdtimer_p.h; path = ../src/ios/BluetoothPatch/osxbtgcdtimer_p.h; sourceTree = SOURCE_ROOT; };
8798FDC32D66075B00CF8EE8 /* OSXBtManagerInternal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = OSXBtManagerInternal.mm; path = ../src/ios/BluetoothPatch/OSXBtManagerInternal.mm; sourceTree = SOURCE_ROOT; };
8798FDC42D66075B00CF8EE8 /* osxbtutility_p.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = osxbtutility_p.h; path = ../src/ios/BluetoothPatch/osxbtutility_p.h; sourceTree = SOURCE_ROOT; };
879A38C7281BD83300F78B2A /* characteristicnotifier2ad9.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier2ad9.cpp; path = ../src/characteristics/characteristicnotifier2ad9.cpp; sourceTree = "<group>"; };
879E5AA6289C057E00FEA38A /* proformwifitreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformwifitreadmill.cpp; path = ../src/devices/proformwifitreadmill/proformwifitreadmill.cpp; sourceTree = "<group>"; };
879E5AA7289C057E00FEA38A /* proformwifitreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformwifitreadmill.h; path = ../src/devices/proformwifitreadmill/proformwifitreadmill.h; sourceTree = "<group>"; };
@@ -1476,18 +1375,12 @@
87A18F082660D5D9002D7C96 /* moc_ftmsrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_ftmsrower.cpp; sourceTree = "<group>"; };
87A2E0202B2B024200E6168F /* swiftDebug.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = swiftDebug.h; path = ../src/ios/swiftDebug.h; sourceTree = "<group>"; };
87A2E0212B2B053E00E6168F /* swiftDebug.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = swiftDebug.mm; path = ../src/ios/swiftDebug.mm; sourceTree = "<group>"; };
87A33F192D611D8400BFFF29 /* moc_logwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_logwriter.cpp; sourceTree = "<group>"; };
87A33F1B2D611D9500BFFF29 /* logwriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = logwriter.h; path = ../src/logwriter.h; sourceTree = SOURCE_ROOT; };
87A33F1C2D611D9500BFFF29 /* logwriter.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = logwriter.cpp; path = ../src/logwriter.cpp; sourceTree = SOURCE_ROOT; };
87A3BC1E2656429300D302E3 /* echelonrower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = echelonrower.h; path = ../src/devices/echelonrower/echelonrower.h; sourceTree = "<group>"; };
87A3BC1F2656429400D302E3 /* rower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = rower.cpp; path = ../src/devices/rower.cpp; sourceTree = "<group>"; };
87A3BC202656429400D302E3 /* echelonrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = echelonrower.cpp; path = ../src/devices/echelonrower/echelonrower.cpp; sourceTree = "<group>"; };
87A3BC212656429400D302E3 /* rower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rower.h; path = ../src/devices/rower.h; sourceTree = "<group>"; };
87A3BC24265642A200D302E3 /* moc_rower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_rower.cpp; sourceTree = "<group>"; };
87A3BC25265642A200D302E3 /* moc_echelonrower.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_echelonrower.cpp; sourceTree = "<group>"; };
87A3DD982D3413790060BAEB /* lifespantreadmill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = lifespantreadmill.h; path = ../src/devices/lifespantreadmill/lifespantreadmill.h; sourceTree = SOURCE_ROOT; };
87A3DD992D3413790060BAEB /* lifespantreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = lifespantreadmill.cpp; path = ../src/devices/lifespantreadmill/lifespantreadmill.cpp; sourceTree = SOURCE_ROOT; };
87A3DD9A2D3413790060BAEB /* moc_lifespantreadmill.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_lifespantreadmill.cpp; sourceTree = "<group>"; };
87A3EBB925D2CFED0040EB4C /* sportstechbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sportstechbike.h; path = ../src/devices/sportstechbike/sportstechbike.h; sourceTree = "<group>"; };
87A3EBBA25D2CFED0040EB4C /* sportstechbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sportstechbike.cpp; path = ../src/devices/sportstechbike/sportstechbike.cpp; sourceTree = "<group>"; };
87A4B75F25AF27CB0027EF3C /* metric.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = metric.cpp; path = ../src/metric.cpp; sourceTree = "<group>"; };
@@ -1496,9 +1389,6 @@
87A682592CE3AB3100586A2A /* moc_sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sramAXSController.cpp; sourceTree = "<group>"; };
87A6825B2CE3AB4000586A2A /* sramAXSController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sramAXSController.h; path = ../src/devices/sramAXSController/sramAXSController.h; sourceTree = SOURCE_ROOT; };
87A6825C2CE3AB4000586A2A /* sramAXSController.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = sramAXSController.cpp; path = ../src/devices/sramAXSController/sramAXSController.cpp; sourceTree = SOURCE_ROOT; };
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = androidstatusbar.h; path = ../src/androidstatusbar.h; sourceTree = SOURCE_ROOT; };
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = androidstatusbar.cpp; path = ../src/androidstatusbar.cpp; sourceTree = SOURCE_ROOT; };
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_androidstatusbar.cpp; sourceTree = "<group>"; };
87ADD2B927634C1400B7A0AB /* technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = technogymmyruntreadmill.cpp; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
87ADD2BA27634C1400B7A0AB /* technogymmyruntreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = technogymmyruntreadmill.h; path = ../src/devices/technogymmyruntreadmill/technogymmyruntreadmill.h; sourceTree = "<group>"; };
87ADD2BC27634C2100B7A0AB /* moc_technogymmyruntreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymmyruntreadmill.cpp; sourceTree = "<group>"; };
@@ -1533,8 +1423,6 @@
87BF116B298E28CA00B5B6E7 /* pelotonbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pelotonbike.h; path = ../src/devices/pelotonbike/pelotonbike.h; sourceTree = "<group>"; };
87BF116C298E28CA00B5B6E7 /* pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = pelotonbike.cpp; path = ../src/devices/pelotonbike/pelotonbike.cpp; sourceTree = "<group>"; };
87BF116E298E28EC00B5B6E7 /* moc_pelotonbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_pelotonbike.cpp; sourceTree = "<group>"; };
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_echelonconnectsport.h; path = ../src/ios/ios_echelonconnectsport.h; sourceTree = SOURCE_ROOT; };
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_echelonconnectsport.mm; path = ../src/ios/ios_echelonconnectsport.mm; sourceTree = SOURCE_ROOT; };
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_treadmillErgTable.cpp; sourceTree = "<group>"; };
87C481F826DFA7C3006211AD /* eliterizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = eliterizer.h; path = ../src/devices/eliterizer/eliterizer.h; sourceTree = "<group>"; };
87C481F926DFA7C3006211AD /* eliterizer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = eliterizer.cpp; path = ../src/devices/eliterizer/eliterizer.cpp; sourceTree = "<group>"; };
@@ -1612,12 +1500,6 @@
87D91F982800B9970026D43C /* proformwifibike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformwifibike.h; path = ../src/devices/proformwifibike/proformwifibike.h; sourceTree = "<group>"; };
87D91F992800B9970026D43C /* proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformwifibike.cpp; path = ../src/devices/proformwifibike/proformwifibike.cpp; sourceTree = "<group>"; };
87D91F9B2800B9B90026D43C /* moc_proformwifibike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformwifibike.cpp; sourceTree = "<group>"; };
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0002.cpp; sourceTree = "<group>"; };
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicwriteprocessor0003.cpp; sourceTree = "<group>"; };
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0002.cpp; path = ../src/characteristics/characteristicnotifier0002.cpp; sourceTree = SOURCE_ROOT; };
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicwriteprocessor0003.cpp; path = ../src/characteristics/characteristicwriteprocessor0003.cpp; sourceTree = SOURCE_ROOT; };
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = characteristicnotifier0004.cpp; path = ../src/characteristics/characteristicnotifier0004.cpp; sourceTree = SOURCE_ROOT; };
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_characteristicnotifier0004.cpp; sourceTree = "<group>"; };
87DA764F2848F8F200A71B64 /* libQt5TextToSpeech.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libQt5TextToSpeech.a; path = ../../Qt/5.15.2/ios/lib/libQt5TextToSpeech.a; sourceTree = "<group>"; };
87DA8463284933D200B550E9 /* fakeelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fakeelliptical.h; path = ../src/devices/fakeelliptical/fakeelliptical.h; sourceTree = "<group>"; };
87DA8464284933D200B550E9 /* fakeelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = fakeelliptical.cpp; path = ../src/devices/fakeelliptical/fakeelliptical.cpp; sourceTree = "<group>"; };
@@ -1631,15 +1513,6 @@
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = stairclimber.cpp; path = ../src/devices/stairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_echelonstairclimber.cpp; sourceTree = "<group>"; };
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_stairclimber.cpp; sourceTree = "<group>"; };
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_moxy5sensor.cpp; sourceTree = "<group>"; };
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = moxy5sensor.h; path = ../src/devices/moxy5sensor/moxy5sensor.h; sourceTree = SOURCE_ROOT; };
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = moxy5sensor.cpp; path = ../src/devices/moxy5sensor/moxy5sensor.cpp; sourceTree = SOURCE_ROOT; };
87DED80427D1273800BE4FBB /* filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = filedownloader.cpp; path = ../src/filedownloader.cpp; sourceTree = "<group>"; };
87DED80527D1273900BE4FBB /* filedownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filedownloader.h; path = ../src/filedownloader.h; sourceTree = "<group>"; };
87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filedownloader.cpp; sourceTree = "<group>"; };
@@ -1665,9 +1538,6 @@
87E6A85725B5C88E00371D28 /* moc_flywheelbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_flywheelbike.cpp; sourceTree = "<group>"; };
87E6A85925B5C8B900371D28 /* flywheelbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = flywheelbike.cpp; path = ../src/devices/flywheelbike/flywheelbike.cpp; sourceTree = "<group>"; };
87E6A85A25B5C8B900371D28 /* flywheelbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = flywheelbike.h; path = ../src/devices/flywheelbike/flywheelbike.h; sourceTree = "<group>"; };
87EAC3D22D1D8D1D004FE975 /* moc_pitpatbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_pitpatbike.cpp; sourceTree = "<group>"; };
87EAC3D42D1D8D34004FE975 /* pitpatbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = pitpatbike.h; path = ../src/devices/pitpatbike/pitpatbike.h; sourceTree = SOURCE_ROOT; };
87EAC3D52D1D8D34004FE975 /* pitpatbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = pitpatbike.cpp; path = ../src/devices/pitpatbike/pitpatbike.cpp; sourceTree = SOURCE_ROOT; };
87EB917427EE5FB3002535E1 /* nautilusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautilusbike.cpp; path = ../src/devices/nautilusbike/nautilusbike.cpp; sourceTree = "<group>"; };
87EB917527EE5FB3002535E1 /* nautilusbike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautilusbike.h; path = ../src/devices/nautilusbike/nautilusbike.h; sourceTree = "<group>"; };
87EB917727EE5FE7002535E1 /* moc_inapptransaction.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inapptransaction.cpp; sourceTree = "<group>"; };
@@ -1681,15 +1551,6 @@
87EB917F27EE5FE7002535E1 /* qdomyoszwift_qmltyperegistrations.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = qdomyoszwift_qmltyperegistrations.cpp; sourceTree = "<group>"; };
87EB918027EE5FE7002535E1 /* moc_inappproductqmltype.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproductqmltype.cpp; sourceTree = "<group>"; };
87EB918127EE5FE7002535E1 /* moc_inappproduct.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_inappproduct.cpp; sourceTree = "<group>"; };
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fitdatabaseprocessor.h; path = ../src/fitdatabaseprocessor.h; sourceTree = SOURCE_ROOT; };
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = fitdatabaseprocessor.cpp; path = ../src/fitdatabaseprocessor.cpp; sourceTree = SOURCE_ROOT; };
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_fitdatabaseprocessor.cpp; sourceTree = "<group>"; };
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutloaderworker.cpp; sourceTree = "<group>"; };
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_workoutmodel.cpp; sourceTree = "<group>"; };
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutloaderworker.h; path = ../src/workoutloaderworker.h; sourceTree = SOURCE_ROOT; };
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutloaderworker.cpp; path = ../src/workoutloaderworker.cpp; sourceTree = SOURCE_ROOT; };
87EBB2A42D39214E00348B15 /* workoutmodel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = workoutmodel.h; path = ../src/workoutmodel.h; sourceTree = SOURCE_ROOT; };
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = workoutmodel.cpp; path = ../src/workoutmodel.cpp; sourceTree = SOURCE_ROOT; };
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
@@ -1699,12 +1560,6 @@
87F02E3E29178523000DB52C /* octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = octaneelliptical.cpp; path = ../src/devices/octaneelliptical/octaneelliptical.cpp; sourceTree = "<group>"; };
87F02E3F29178524000DB52C /* octaneelliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = octaneelliptical.h; path = ../src/devices/octaneelliptical/octaneelliptical.h; sourceTree = "<group>"; };
87F02E4129178545000DB52C /* moc_octaneelliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_octaneelliptical.cpp; sourceTree = "<group>"; };
87F1BD652DBFBCE700416506 /* android_antbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = android_antbike.h; path = ../src/devices/android_antbike/android_antbike.h; sourceTree = SOURCE_ROOT; };
87F1BD662DBFBCE700416506 /* android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = android_antbike.cpp; path = ../src/devices/android_antbike/android_antbike.cpp; sourceTree = SOURCE_ROOT; };
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_android_antbike.cpp; sourceTree = "<group>"; };
87F1BD6E2DC0D59600416506 /* coresensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = coresensor.h; path = ../src/devices/coresensor/coresensor.h; sourceTree = SOURCE_ROOT; };
87F1BD6F2DC0D59600416506 /* coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = coresensor.cpp; path = ../src/devices/coresensor/coresensor.cpp; sourceTree = SOURCE_ROOT; };
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_coresensor.cpp; sourceTree = "<group>"; };
87F4FB5829D550BF0061BB4A /* schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = schwinn170bike.cpp; path = ../src/devices/schwinn170bike/schwinn170bike.cpp; sourceTree = "<group>"; };
87F4FB5929D550BF0061BB4A /* schwinn170bike.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = schwinn170bike.h; path = ../src/devices/schwinn170bike/schwinn170bike.h; sourceTree = "<group>"; };
87F4FB5B29D550DF0061BB4A /* moc_schwinn170bike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_schwinn170bike.cpp; sourceTree = "<group>"; };
@@ -1718,13 +1573,7 @@
87FA11AC27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_ultrasportbike.cpp; sourceTree = "<group>"; };
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libSystem.B.tbd; path = usr/lib/libSystem.B.tbd; sourceTree = SDKROOT; };
87FC40B92D2E74F9008BA736 /* cycleopsphantombike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = cycleopsphantombike.h; path = ../src/devices/cycleopsphantombike/cycleopsphantombike.h; sourceTree = SOURCE_ROOT; };
87FC40BA2D2E74F9008BA736 /* cycleopsphantombike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = cycleopsphantombike.cpp; path = ../src/devices/cycleopsphantombike/cycleopsphantombike.cpp; sourceTree = SOURCE_ROOT; };
87FC40BB2D2E74F9008BA736 /* moc_cycleopsphantombike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_cycleopsphantombike.cpp; sourceTree = "<group>"; };
87FD05DDC378E9BAD82A818F /* fit_ohr_settings_mesg_listener.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = fit_ohr_settings_mesg_listener.hpp; path = "/Users/cagnulein/qdomyos-zwift/src/fit-sdk/fit_ohr_settings_mesg_listener.hpp"; sourceTree = "<absolute>"; };
87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_trxappgateusbrower.cpp; sourceTree = "<group>"; };
87FE06822D170D5600CDAAF6 /* trxappgateusbrower.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = trxappgateusbrower.h; path = ../src/devices/trxappgateusbrower/trxappgateusbrower.h; sourceTree = SOURCE_ROOT; };
87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = trxappgateusbrower.cpp; path = ../src/devices/trxappgateusbrower/trxappgateusbrower.cpp; sourceTree = SOURCE_ROOT; };
87FE5BAD2692F3130056EFC8 /* tacxneo2.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = tacxneo2.cpp; path = ../src/devices/tacxneo2/tacxneo2.cpp; sourceTree = "<group>"; };
87FE5BAE2692F3130056EFC8 /* tacxneo2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tacxneo2.h; path = ../src/devices/tacxneo2/tacxneo2.h; sourceTree = "<group>"; };
87FE5BB02692F31E0056EFC8 /* moc_tacxneo2.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_tacxneo2.cpp; sourceTree = "<group>"; };
@@ -1941,7 +1790,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
878521CD2E42552A00922796 /* libqtlabscalendarplugin.a in Link Binary With Libraries */,
8768C9282BBC13220099DBE1 /* libcrypto.a in Link Binary With Libraries */,
87FA94672B6B89FD00B6AB9A /* SwiftUI.framework in Link Binary With Libraries */,
879F74112893D5B8009A64C8 /* libqavfcamera.a in Link Binary With Libraries */,
@@ -2104,8 +1952,6 @@
25B08E2869634E9BCBA333A2 /* Generated Sources */ = {
isa = PBXGroup;
children = (
878895D92DD48AB100BF5162 /* inclinationresistancetable.h */,
878895DA2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp */,
87C4E5BC2C1C1D2600D0750E /* moc_crossrope.cpp */,
87C424252BC1294000503687 /* moc_treadmillErgTable.cpp */,
874823FD2B935ADA006F3CA1 /* moc_ergtable.cpp */,
@@ -2278,81 +2124,6 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
8755E5D12E4E260B006A12E4 /* fontmanager.h */,
8755E5D22E4E260B006A12E4 /* fontmanager.cpp */,
8755E5D32E4E260B006A12E4 /* moc_fontmanager.cpp */,
878521D12E44B26600922796 /* moc_nordictrackifitadbrower.cpp */,
878521D22E44B26600922796 /* nordictrackifitadbrower.h */,
878521D32E44B26600922796 /* nordictrackifitadbrower.cpp */,
87ACBE9B2E250F7D00F1B6EA /* androidstatusbar.h */,
87ACBE9C2E250F7D00F1B6EA /* androidstatusbar.cpp */,
87ACBE9D2E250F7D00F1B6EA /* moc_androidstatusbar.cpp */,
87EBB29D2D39214E00348B15 /* fitdatabaseprocessor.h */,
87EBB29E2D39214E00348B15 /* fitdatabaseprocessor.cpp */,
87EBB29F2D39214E00348B15 /* moc_fitdatabaseprocessor.cpp */,
87EBB2A02D39214E00348B15 /* moc_workoutloaderworker.cpp */,
87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */,
87EBB2A22D39214E00348B15 /* workoutloaderworker.h */,
87EBB2A32D39214E00348B15 /* workoutloaderworker.cpp */,
87EBB2A42D39214E00348B15 /* workoutmodel.h */,
87EBB2A52D39214E00348B15 /* workoutmodel.cpp */,
878C9DC62DF01C16001114D5 /* moc_speraxtreadmill.cpp */,
878C9DC72DF01C16001114D5 /* speraxtreadmill.h */,
878C9DC82DF01C16001114D5 /* speraxtreadmill.cpp */,
87F1BD6E2DC0D59600416506 /* coresensor.h */,
87F1BD6F2DC0D59600416506 /* coresensor.cpp */,
87F1BD702DC0D59600416506 /* moc_coresensor.cpp */,
8767CA5E2DA800590003001F /* ios_zwiftclickremote.h */,
8767CA5F2DA800590003001F /* ios_zwiftclickremote.mm */,
8767CA5B2DA7F5170003001F /* ios_wahookickrsnapbike.h */,
8767CA5C2DA7F5170003001F /* ios_wahookickrsnapbike.mm */,
87BFEA2D2CEDDEEE00BDD759 /* ios_echelonconnectsport.h */,
87BFEA2E2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm */,
8767CA522DA3C1FD0003001F /* elitesquarecontroller.h */,
8767CA532DA3C1FD0003001F /* elitesquarecontroller.cpp */,
8767CA542DA3C1FD0003001F /* moc_elitesquarecontroller.cpp */,
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */,
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */,
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */,
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */,
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */,
8798FDC02D66075B00CF8EE8 /* osxbluetooth_p.h */,
8798FDC12D66075B00CF8EE8 /* osxbtcentralmanager_p.h */,
8798FDC22D66075B00CF8EE8 /* osxbtgcdtimer_p.h */,
8798FDC32D66075B00CF8EE8 /* OSXBtManagerInternal.mm */,
8798FDC42D66075B00CF8EE8 /* osxbtutility_p.h */,
87A33F1B2D611D9500BFFF29 /* logwriter.h */,
87A33F1C2D611D9500BFFF29 /* logwriter.cpp */,
87A33F192D611D8400BFFF29 /* moc_logwriter.cpp */,
87A3DD982D3413790060BAEB /* lifespantreadmill.h */,
87A3DD992D3413790060BAEB /* lifespantreadmill.cpp */,
87A3DD9A2D3413790060BAEB /* moc_lifespantreadmill.cpp */,
87FC40B92D2E74F9008BA736 /* cycleopsphantombike.h */,
87FC40BA2D2E74F9008BA736 /* cycleopsphantombike.cpp */,
87FC40BB2D2E74F9008BA736 /* moc_cycleopsphantombike.cpp */,
87EAC3D42D1D8D34004FE975 /* pitpatbike.h */,
87EAC3D52D1D8D34004FE975 /* pitpatbike.cpp */,
87EAC3D22D1D8D1D004FE975 /* moc_pitpatbike.cpp */,
877350F52D1C08E50070CBD8 /* SmartControl.h */,
877350F62D1C08E50070CBD8 /* SmartControl.cpp */,
875CA94D2D130FBC00667EE6 /* client.hpp */,
875CA94E2D130FBC00667EE6 /* error.hpp */,
875CA94F2D130FBC00667EE6 /* osc.h */,
875CA9502D130FBC00667EE6 /* osc.cpp */,
875CA9512D130FBC00667EE6 /* print.hpp */,
875CA9522D130FBC00667EE6 /* server.hpp */,
875CA9532D130FBC00667EE6 /* types.hpp */,
875CA9542D130FBC00667EE6 /* util.hpp */,
875CA94B2D130F8100667EE6 /* moc_osc.cpp */,
875CA9472D0C742500667EE6 /* kineticinroadbike.h */,
875CA9482D0C742500667EE6 /* kineticinroadbike.cpp */,
875CA9452D0C740000667EE6 /* moc_kineticinroadbike.cpp */,
87873AF22D09AADF005F86B4 /* sportsplusrower.h */,
87873AF02D09A8CE005F86B4 /* sportsplusrower.cpp */,
87873AED2D09A8AA005F86B4 /* moc_sportsplusrower.cpp */,
870A5DB42CEF8FD200839641 /* technogymbike.cpp */,
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */,
8720891A2CE6567D008C2C17 /* mqttpublisher.cpp */,
@@ -2811,16 +2582,6 @@
87A2E0202B2B024200E6168F /* swiftDebug.h */,
8730A3912B404159007E336D /* zwift_protobuf_layer.swift */,
87B871922CE1E94D009B06CA /* zwifthubbike.swift */,
87FE06822D170D5600CDAAF6 /* trxappgateusbrower.h */,
87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */,
87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */,
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */,
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */,
87DC27E82D9BDB53007A1B9D /* stairclimber.h */,
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */,
87F1BD652DBFBCE700416506 /* android_antbike.h */,
87F1BD662DBFBCE700416506 /* android_antbike.cpp */,
87F1BD672DBFBCE700416506 /* moc_android_antbike.cpp */,
);
name = Sources;
sourceTree = "<group>";
@@ -2974,19 +2735,6 @@
name = AppleWatchToIpad;
sourceTree = "<group>";
};
8798FDCA2D6F338200CF8EE8 /* Recovered References */ = {
isa = PBXGroup;
children = (
87DA62A92D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp */,
87DA62A72D2305F5008ADA0F /* characteristicnotifier0002.cpp */,
87DA62A22D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp */,
87DA62A32D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp */,
87DA62AD2D2426F2008ADA0F /* characteristicnotifier0004.cpp */,
87DA62AE2D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp */,
);
name = "Recovered References";
sourceTree = "<group>";
};
87DF60DE337FB58864343E39 /* Resources */ = {
isa = PBXGroup;
children = (
@@ -2999,7 +2747,6 @@
AF39DD055C3EF8226FBE929D /* Frameworks */ = {
isa = PBXGroup;
children = (
878521CC2E42552A00922796 /* libqtlabscalendarplugin.a */,
8768C9262BBC12D10099DBE1 /* libcrypto.a */,
87FA94682B6B8A5A00B6AB9A /* libSystem.B.tbd */,
87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */,
@@ -3406,7 +3153,6 @@
AF39DD055C3EF8226FBE929D /* Frameworks */,
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
FE0A091FDBFB3E9C31B7A1BD /* Products */,
8798FDCA2D6F338200CF8EE8 /* Recovered References */,
);
name = qdomyoszwift;
sourceTree = "<group>";
@@ -3603,8 +3349,6 @@
873824C027E64707004F1B46 /* moc_dirconmanager.cpp in Compile Sources */,
2F0E70F826316F3600E11F3A /* virtualbike_zwift.swift in Compile Sources */,
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */,
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */,
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */,
87062646259480B200D06586 /* ViewController.swift in Compile Sources */,
87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */,
87BAFE482B8CA7AA00065FCD /* moc_focustreadmill.cpp in Compile Sources */,
@@ -3625,8 +3369,6 @@
873824BC27E64707004F1B46 /* moc_resolver.cpp in Compile Sources */,
87FA11AD27C5ECE4008AC5D1 /* moc_ultrasportbike.cpp in Compile Sources */,
871235BF26B297670012D0F2 /* kingsmithr1protreadmill.cpp in Compile Sources */,
87DA62AA2D2305F5008ADA0F /* characteristicwriteprocessor0003.cpp in Compile Sources */,
87DA62AB2D2305F5008ADA0F /* characteristicnotifier0002.cpp in Compile Sources */,
8772B7F72CB55E98004AB8E9 /* deerruntreadmill.cpp in Compile Sources */,
20A50533946A39CBD2C89104 /* bluetoothdevice.cpp in Compile Sources */,
87C5F0D126285E7E0067A1B5 /* moc_stagesbike.cpp in Compile Sources */,
@@ -3663,12 +3405,9 @@
87F02E4229178545000DB52C /* moc_octaneelliptical.cpp in Compile Sources */,
87E2F85D291ED308002BDC65 /* lifefitnesstreadmill.cpp in Compile Sources */,
8752C0E82B15D85600C3D1A5 /* eliteariafan.cpp in Compile Sources */,
8767CA602DA800590003001F /* ios_zwiftclickremote.mm in Compile Sources */,
87917A7328E768D200F8D9AC /* Browser.swift in Compile Sources */,
873CD20B27EF8D8A000131BC /* inapptransaction.cpp in Compile Sources */,
8727C7D52B3BF1E4005429EB /* moc_proformtelnetbike.cpp in Compile Sources */,
8767CA552DA3C1FD0003001F /* elitesquarecontroller.cpp in Compile Sources */,
8767CA562DA3C1FD0003001F /* moc_elitesquarecontroller.cpp in Compile Sources */,
873824EF27E647A9004F1B46 /* query.cpp in Compile Sources */,
876F45FF279350D9003CDA5A /* moc_concept2skierg.cpp in Compile Sources */,
BE93C6EF2C2A6BFEEC9EA565 /* fit_buffered_mesg_broadcaster.cpp in Compile Sources */,
@@ -3676,7 +3415,6 @@
87DAE16426E9FF3A00B0527E /* kingsmithr2treadmill.cpp in Compile Sources */,
87F93427278E0EC00088B596 /* domyosrower.cpp in Compile Sources */,
87B617EE25F25FED0094A1CB /* snodebike.cpp in Compile Sources */,
878895DB2DD48AB100BF5162 /* moc_inclinationresistancetable.cpp in Compile Sources */,
87C5F0B526285E5F0067A1B5 /* mimemessage.cpp in Compile Sources */,
8768C8C92BBC11C80099DBE1 /* adb_client.c in Compile Sources */,
873063C0259DF2C500DA0F44 /* moc_heartratebelt.cpp in Compile Sources */,
@@ -3737,21 +3475,14 @@
87BE6FDE272D2A3E00C35795 /* moc_horizongr7bike.cpp in Compile Sources */,
A4BD6DF51CFFF867B7B5AED4 /* fit_developer_field_definition.cpp in Compile Sources */,
87EB918B27EE5FE7002535E1 /* moc_inappproductqmltype.cpp in Compile Sources */,
87873AF12D09A8CE005F86B4 /* sportsplusrower.cpp in Compile Sources */,
8762D5132601F89500F6F049 /* scanrecordresult.cpp in Compile Sources */,
3015F9B9FF4CA6D653D46CCA /* fit_developer_field_description.cpp in Compile Sources */,
878521D42E44B26600922796 /* moc_nordictrackifitadbrower.cpp in Compile Sources */,
878521D52E44B26600922796 /* nordictrackifitadbrower.cpp in Compile Sources */,
87310B22266FBB78008BA0D6 /* moc_homefitnessbuddy.cpp in Compile Sources */,
87958F1B27628D5400124B24 /* moc_elitesterzosmart.cpp in Compile Sources */,
8768C8D82BBC12890099DBE1 /* centraldir.c in Compile Sources */,
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
87ACBE9E2E250F7D00F1B6EA /* moc_androidstatusbar.cpp in Compile Sources */,
87ACBE9F2E250F7D00F1B6EA /* androidstatusbar.cpp in Compile Sources */,
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
@@ -3780,7 +3511,6 @@
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */,
87DA8465284933D200B550E9 /* fakeelliptical.cpp in Compile Sources */,
876E50F52B701C050080FAAF /* moc_zwiftclickremote.cpp in Compile Sources */,
8767CA5D2DA7F5170003001F /* ios_wahookickrsnapbike.mm in Compile Sources */,
87FE5BAF2692F3130056EFC8 /* tacxneo2.cpp in Compile Sources */,
8718CBAC263063CE004BF4EE /* moc_tcpclientinfosender.cpp in Compile Sources */,
873824B527E64707004F1B46 /* moc_provider_p.cpp in Compile Sources */,
@@ -3794,9 +3524,7 @@
87E6A85B25B5C8B900371D28 /* flywheelbike.cpp in Compile Sources */,
87182A09276BBAF600141463 /* virtualrower.cpp in Compile Sources */,
87C424262BC1294000503687 /* moc_treadmillErgTable.cpp in Compile Sources */,
877350F72D1C08E60070CBD8 /* SmartControl.cpp in Compile Sources */,
873824ED27E647A9004F1B46 /* resolver.cpp in Compile Sources */,
875CA9552D130FBC00667EE6 /* osc.cpp in Compile Sources */,
878531652711A3E1004B153D /* activiotreadmill.cpp in Compile Sources */,
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */,
48BA9CE9D6F256A15E8FB25D /* fit_mesg.cpp in Compile Sources */,
@@ -3808,8 +3536,6 @@
8720890E2CE65451008C2C17 /* qmqttsubscriptionproperties.cpp in Compile Sources */,
8720890F2CE65451008C2C17 /* qmqttconnectionproperties.cpp in Compile Sources */,
872089102CE65451008C2C17 /* qmqttconnection.cpp in Compile Sources */,
878C9DC92DF01C16001114D5 /* speraxtreadmill.cpp in Compile Sources */,
878C9DCA2DF01C16001114D5 /* moc_speraxtreadmill.cpp in Compile Sources */,
872089112CE65451008C2C17 /* qmqttsubscription.cpp in Compile Sources */,
872089122CE65451008C2C17 /* qmqttclient.cpp in Compile Sources */,
872089132CE65451008C2C17 /* qmqtttopicfilter.cpp in Compile Sources */,
@@ -3819,12 +3545,10 @@
872089172CE65451008C2C17 /* qmqtttopicname.cpp in Compile Sources */,
872089182CE65451008C2C17 /* qmqttpublishproperties.cpp in Compile Sources */,
872089192CE65451008C2C17 /* qmqttauthenticationproperties.cpp in Compile Sources */,
87FE06842D170D5600CDAAF6 /* trxappgateusbrower.cpp in Compile Sources */,
87BCE6BD29F28F72001F70EB /* ypooelliptical.cpp in Compile Sources */,
87C7074327E4CF5900E79C46 /* keepbike.cpp in Compile Sources */,
87B871932CE1E94D009B06CA /* zwifthubbike.swift in Compile Sources */,
878C9E6928B77E7C00669129 /* nordictrackifitadbbike.cpp in Compile Sources */,
87A33F1D2D611D9500BFFF29 /* logwriter.cpp in Compile Sources */,
8798C8892733E10E003148B3 /* moc_strydrunpowersensor.cpp in Compile Sources */,
8781907E2615089D0085E656 /* peloton.cpp in Compile Sources */,
2B800DC34C91D8B080DEFBE8 /* fit_mesg_with_event_broadcaster.cpp in Compile Sources */,
@@ -3850,7 +3574,6 @@
7CF08714869DA569C2EA551C /* keepawakehelper.cpp in Compile Sources */,
87EB918727EE5FE7002535E1 /* moc_nautilusbike.cpp in Compile Sources */,
873824B827E64707004F1B46 /* moc_cache.cpp in Compile Sources */,
87873AEE2D09A8AA005F86B4 /* moc_sportsplusrower.cpp in Compile Sources */,
873824E427E647A8004F1B46 /* cache.cpp in Compile Sources */,
87A2E0222B2B053E00E6168F /* swiftDebug.mm in Compile Sources */,
87310B1E266FBB59008BA0D6 /* smartrowrower.cpp in Compile Sources */,
@@ -3869,12 +3592,6 @@
8768C9022BBC12B80099DBE1 /* socket_loopback_client.c in Compile Sources */,
87C5F0B926285E5F0067A1B5 /* mimehtml.cpp in Compile Sources */,
27E452D452B62D0948DF0755 /* sessionline.cpp in Compile Sources */,
87EBB2A62D39214E00348B15 /* moc_workoutloaderworker.cpp in Compile Sources */,
87EBB2A72D39214E00348B15 /* workoutmodel.cpp in Compile Sources */,
87EBB2A82D39214E00348B15 /* workoutloaderworker.cpp in Compile Sources */,
87EBB2A92D39214E00348B15 /* moc_fitdatabaseprocessor.cpp in Compile Sources */,
87EBB2AA2D39214E00348B15 /* fitdatabaseprocessor.cpp in Compile Sources */,
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */,
E40895A73216AC52D35083D9 /* signalhandler.cpp in Compile Sources */,
873CD22427EF8E18000131BC /* inappproductqmltype.cpp in Compile Sources */,
87DF68BF25E2675100FCDA46 /* moc_schwinnic4bike.cpp in Compile Sources */,
@@ -3892,7 +3609,6 @@
873824BB27E64707004F1B46 /* moc_prober_p.cpp in Compile Sources */,
877758B32C98627300BB1697 /* moc_sportstechelliptical.cpp in Compile Sources */,
8742C2B227C92C30007D3FA0 /* wahookickrsnapbike.cpp in Compile Sources */,
87BFEA2F2CEDDEEE00BDD759 /* ios_echelonconnectsport.mm in Compile Sources */,
87EB918327EE5FE7002535E1 /* moc_inappstore.cpp in Compile Sources */,
87CF516B293C87B000A7CABC /* moc_characteristicwriteprocessore005.cpp in Compile Sources */,
8780D949264FB8B800192D41 /* moc_smartspin2k.cpp in Compile Sources */,
@@ -3923,7 +3639,6 @@
873CD20727EF8D8A000131BC /* inappproduct.cpp in Compile Sources */,
872088EB2CE6543C008C2C17 /* moc_mqttpublisher.cpp in Compile Sources */,
872088EC2CE6543C008C2C17 /* moc_qmqttclient.cpp in Compile Sources */,
875CA94C2D130F8100667EE6 /* moc_osc.cpp in Compile Sources */,
872088ED2CE6543C008C2C17 /* moc_qmqttmessage.cpp in Compile Sources */,
872088EE2CE6543C008C2C17 /* moc_qmqttsubscription.cpp in Compile Sources */,
872088EF2CE6543C008C2C17 /* moc_qmqttconnection_p.cpp in Compile Sources */,
@@ -3934,8 +3649,6 @@
87440FBF2640292900E4DC0B /* moc_fitplusbike.cpp in Compile Sources */,
8768C8CA2BBC11C80099DBE1 /* sockets.c in Compile Sources */,
87B617EC25F25FED0094A1CB /* screencapture.cpp in Compile Sources */,
8755E5D42E4E260B006A12E4 /* moc_fontmanager.cpp in Compile Sources */,
8755E5D52E4E260B006A12E4 /* fontmanager.cpp in Compile Sources */,
876F9B5F275385C9006AE6FA /* fitmetria_fanfit.cpp in Compile Sources */,
FB2566376FE0FB17ED3DE94D /* FitDeveloperField.mm in Compile Sources */,
43FA2D5EA73D9C89F1A333B6 /* FitEncode.mm in Compile Sources */,
@@ -3952,14 +3665,10 @@
2B42755BF45173E11E2110CB /* FitFieldDefinition.mm in Compile Sources */,
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
872261F0289EA887006A6F75 /* moc_nordictrackelliptical.cpp in Compile Sources */,
873824E327E647A8004F1B46 /* bitmap.cpp in Compile Sources */,
87FE5BB12692F31E0056EFC8 /* moc_tacxneo2.cpp in Compile Sources */,
8798FDC52D66075B00CF8EE8 /* OSXBtManagerInternal.mm in Compile Sources */,
873824E827E647A8004F1B46 /* provider.cpp in Compile Sources */,
8791A8AA25C8603F003B50B2 /* moc_inspirebike.cpp in Compile Sources */,
03F49BBCF19B73B18385B13D /* FitMesgDefinition.mm in Compile Sources */,
@@ -3994,11 +3703,7 @@
1FBBC7C86C436CAAAFD37E56 /* moc_domyostreadmill.cpp in Compile Sources */,
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */,
873824BD27E64707004F1B46 /* moc_resolver_p.cpp in Compile Sources */,
87DA62A42D2305E4008ADA0F /* moc_characteristicnotifier0002.cpp in Compile Sources */,
87DA62A52D2305E4008ADA0F /* moc_characteristicwriteprocessor0003.cpp in Compile Sources */,
876ED21A25C3E9010065F3DC /* moc_material.cpp in Compile Sources */,
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */,
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */,
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */,
87D10552290996EA00B3935B /* mepanelbike.cpp in Compile Sources */,
9D9484EED654597C394345DE /* moc_echelonconnectsport.cpp in Compile Sources */,
@@ -4015,11 +3720,8 @@
E62DA5FF2436135448C94671 /* moc_toorxtreadmill.cpp in Compile Sources */,
87586A4325B8341B00A243C4 /* moc_proformbike.cpp in Compile Sources */,
87CC3BA325A0885F001EC5A8 /* domyoselliptical.cpp in Compile Sources */,
87A33F1A2D611D8400BFFF29 /* moc_logwriter.cpp in Compile Sources */,
87D105542909971100B3935B /* moc_mepanelbike.cpp in Compile Sources */,
87B617F325F260150094A1CB /* moc_snodebike.cpp in Compile Sources */,
87F1BD712DC0D59600416506 /* moc_coresensor.cpp in Compile Sources */,
87F1BD722DC0D59600416506 /* coresensor.cpp in Compile Sources */,
87DA8467284933DE00B550E9 /* moc_fakeelliptical.cpp in Compile Sources */,
87C5F0D726285E7E0067A1B5 /* moc_mimefile.cpp in Compile Sources */,
877FBA29276E684500F6C0C9 /* bowflextreadmill.cpp in Compile Sources */,
@@ -4027,7 +3729,6 @@
8762D5102601F7EA00F6F049 /* M3iNSQT.cpp in Compile Sources */,
872261EE289EA873006A6F75 /* nordictrackelliptical.cpp in Compile Sources */,
8718CBA3263063BD004BF4EE /* templateinfosender.cpp in Compile Sources */,
87EAC3D62D1D8D34004FE975 /* pitpatbike.cpp in Compile Sources */,
E8B499F921FB0AB55C7A8A8B /* moc_gpx.cpp in Compile Sources */,
87E6A85825B5C88E00371D28 /* moc_flywheelbike.cpp in Compile Sources */,
8754D24C27F786F0003D7054 /* virtualrower.swift in Compile Sources */,
@@ -4040,9 +3741,6 @@
879E5AA8289C057E00FEA38A /* proformwifitreadmill.cpp in Compile Sources */,
873824B227E64706004F1B46 /* moc_hostname.cpp in Compile Sources */,
873824EB27E647A8004F1B46 /* prober.cpp in Compile Sources */,
875CA9462D0C740000667EE6 /* moc_kineticinroadbike.cpp in Compile Sources */,
87DA62AF2D2426F2008ADA0F /* characteristicnotifier0004.cpp in Compile Sources */,
87DA62B02D2426F2008ADA0F /* moc_characteristicnotifier0004.cpp in Compile Sources */,
8767EF1E29448D6700810C0F /* characteristicwriteprocessor.cpp in Compile Sources */,
87B617F425F260150094A1CB /* moc_screencapture.cpp in Compile Sources */,
87B617ED25F25FED0094A1CB /* fitshowtreadmill.cpp in Compile Sources */,
@@ -4055,16 +3753,12 @@
8727C7D12B3BF1B8005429EB /* QTelnet.cpp in Compile Sources */,
8775008329E876F8008E48B7 /* iconceptelliptical.cpp in Compile Sources */,
87B187BD29B8C577007EEF9D /* moc_ziprotreadmill.cpp in Compile Sources */,
875CA9492D0C742500667EE6 /* kineticinroadbike.cpp in Compile Sources */,
877FBA2B276E684E00F6C0C9 /* moc_bowflextreadmill.cpp in Compile Sources */,
874D272229AFA13B0007C079 /* moc_apexbike.cpp in Compile Sources */,
8738248127E646C1004F1B46 /* characteristicnotifier2ad2.cpp in Compile Sources */,
87A6825A2CE3AB3100586A2A /* moc_sramAXSController.cpp in Compile Sources */,
87A0771029B641D600A368BF /* wahookickrheadwind.cpp in Compile Sources */,
87EAC3D32D1D8D1D004FE975 /* moc_pitpatbike.cpp in Compile Sources */,
8791A8AB25C861BD003B50B2 /* inspirebike.cpp in Compile Sources */,
87F1BD682DBFBCE700416506 /* moc_android_antbike.cpp in Compile Sources */,
87F1BD692DBFBCE700416506 /* android_antbike.cpp in Compile Sources */,
876BFC9D27BE35C5001D7645 /* bowflext216treadmill.cpp in Compile Sources */,
871235C126B297720012D0F2 /* moc_kingsmithr1protreadmill.cpp in Compile Sources */,
87061397286D8CFE00D2446E /* PathController.cpp in Compile Sources */,
@@ -4102,8 +3796,6 @@
87EB918827EE5FE7002535E1 /* moc_inappstoreqmltype.cpp in Compile Sources */,
87083D9626678EFA0072410D /* zwiftworkout.cpp in Compile Sources */,
87C5F0B826285E5F0067A1B5 /* stagesbike.cpp in Compile Sources */,
87FC40BC2D2E74F9008BA736 /* cycleopsphantombike.cpp in Compile Sources */,
87FC40BD2D2E74F9008BA736 /* moc_cycleopsphantombike.cpp in Compile Sources */,
87C5F0D526285E7E0067A1B5 /* moc_mimehtml.cpp in Compile Sources */,
871B9FD4265E6A9A00DB41F4 /* moc_powerzonepack.cpp in Compile Sources */,
87A0C4BF262329B500121A76 /* moc_cscbike.cpp in Compile Sources */,
@@ -4111,8 +3803,6 @@
8790FDE1277B0AC600247550 /* moc_nautilustreadmill.cpp in Compile Sources */,
8727A47927849EB200019B5D /* moc_paferstreadmill.cpp in Compile Sources */,
8783153B25E8D81E0007817C /* moc_sportstechbike.cpp in Compile Sources */,
87A3DD9B2D3413790060BAEB /* moc_lifespantreadmill.cpp in Compile Sources */,
87A3DD9C2D3413790060BAEB /* lifespantreadmill.cpp in Compile Sources */,
875F69BB26342E9A0009FD78 /* moc_spirittreadmill.cpp in Compile Sources */,
8768C8BF2BBC11C80099DBE1 /* transport.c in Compile Sources */,
);
@@ -4455,8 +4145,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1165;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
CURRENT_PROJECT_VERSION = 964;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4492,7 +4181,6 @@
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
../../Qt/5.15.2/ios/include/QtSql,
);
LIBRARY_SEARCH_PATHS = (
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
@@ -4538,9 +4226,8 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
OTHER_CFLAGS = (
"-pipe",
"-g",
@@ -4634,9 +4321,6 @@
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_INSTALL_OBJC_HEADER = YES;
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
@@ -4655,9 +4339,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1165;
CURRENT_PROJECT_VERSION = 964;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = 0;
@@ -4694,7 +4377,6 @@
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/private,
../../Qt/5.15.2/ios/include/QtCore/5.15.2,
../../Qt/5.15.2/ios/include/QtCore/5.15.2/QtCore/,
../../Qt/5.15.2/ios/include/QtSql,
);
LIBRARY_SEARCH_PATHS = (
/Users/cagnulein/Qt/5.15.2/ios/plugins/platforms,
@@ -4740,9 +4422,8 @@
/Users/cagnulein/Qt/5.15.2/ios/plugins/playlistformats,
/Users/cagnulein/Qt/5.15.2/ios/plugins/audio,
"/Users/cagnulein/qdomyos-zwift/src/ios/adb",
/Users/cagnulein/Qt/5.15.2/ios/qml/Qt/labs/calendar,
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"-pipe",
@@ -4837,9 +4518,6 @@
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = _debug;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_INSTALL_OBJC_HEADER = YES;
SWIFT_OBJC_BRIDGING_HEADER = "qdomyoszwift-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift2.h";
@@ -4891,7 +4569,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1165;
CURRENT_PROJECT_VERSION = 964;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4916,7 +4594,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -4987,7 +4665,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1165;
CURRENT_PROJECT_VERSION = 964;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5008,7 +4686,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -5079,7 +4757,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1166;
CURRENT_PROJECT_VERSION = 964;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5124,7 +4802,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -5144,8 +4822,6 @@
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
STRIP_SWIFT_SYMBOLS = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -5195,7 +4871,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1166;
CURRENT_PROJECT_VERSION = 964;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5236,7 +4912,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.20;
MARKETING_VERSION = 2.18;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "--generate-entitlement-der";
@@ -5256,8 +4932,6 @@
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
STRIP_INSTALLED_PRODUCT = NO;
STRIP_SWIFT_SYMBOLS = NO;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;

View File

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

View File

@@ -23,12 +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
weak var delegate: WatchKitConnectionDelegate?
private override init() {
@@ -71,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
@@ -81,10 +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
}
}, errorHandler: { (error) in
print(error)
})

View File

@@ -28,13 +28,11 @@ 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()
var sport: Int = 0
@@ -55,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):
@@ -167,7 +159,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .cyclingPower)!,
HKSampleType.quantityType(forIdentifier: .cyclingSpeed)!,
HKSampleType.quantityType(forIdentifier: .cyclingCadence)!,
@@ -187,7 +178,6 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
HKSampleType.quantityType(forIdentifier: .distanceCycling)!,
HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!,
HKSampleType.workoutType()
])
}
@@ -226,30 +216,23 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
workoutSession.stopActivity(with: Date())
workoutSession.end()
// Write active calories
guard let activeQuantityType = HKQuantityType.quantityType(
guard let quantityType = HKQuantityType.quantityType(
forIdentifier: .activeEnergyBurned) else {
return
}
let unit = HKUnit.kilocalorie()
let activeEnergyBurned = WorkoutTracking.kcal
let activeQuantity = HKQuantity(unit: unit,
doubleValue: activeEnergyBurned)
let totalEnergyBurned = WorkoutTracking.kcal
let quantity = HKQuantity(unit: unit,
doubleValue: totalEnergyBurned)
let startDate = workoutSession.startDate ?? WorkoutTracking.lastDateMetric
let sample = HKCumulativeQuantitySeriesSample(type: quantityType,
quantity: quantity,
start: workoutSession.startDate!,
end: Date())
let activeSample = HKCumulativeQuantitySeriesSample(type: activeQuantityType,
quantity: activeQuantity,
start: startDate,
end: Date())
workoutBuilder.add([activeSample]) {(success, error) in
if let error = error {
print("WatchWorkoutTracking active calories: \(error.localizedDescription)")
}
}
workoutBuilder.add([sample]) {(success, error) in}
let unitDistance = HKUnit.mile()
let miles = WorkoutTracking.distance
let quantityMiles = HKQuantity(unit: unitDistance,
@@ -265,7 +248,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -281,117 +264,11 @@ 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())
// Add the steps sample to workout builder
workoutBuilder.add([sampleSteps]) { (success, error) in
if let error = error {
print(error)
}
// End the data collection
self.workoutBuilder.endCollection(withEnd: Date()) { (success, error) in
if let error = error {
print(error)
}
// Finish the workout and save total steps
self.workoutBuilder.finishWorkout { (workout, error) in
if let error = error {
print(error)
}
workout?.setValue(stepsQuantity, forKey: "totalSteps")
}
}
}
guard let quantityTypeDistance = HKQuantityType.quantityType(
forIdentifier: .distanceWalkingRunning) else {
return
@@ -399,7 +276,7 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
let sampleDistance = HKCumulativeQuantitySeriesSample(type: quantityTypeDistance,
quantity: quantityMiles,
start: startDate,
start: workoutSession.startDate!,
end: Date())
workoutBuilder.add([sampleDistance]) {(success, error) in
@@ -415,10 +292,6 @@ 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")
}
}
}
@@ -529,7 +402,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 1) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let wattPerInterval = HKQuantity(unit: HKUnit.watt(),
doubleValue: WorkoutTracking.power)
@@ -572,7 +445,7 @@ extension WorkoutTracking: HKLiveWorkoutBuilderDelegate {
// Fallback on earlier versions
}
} else if(sport == 2) {
if #available(watchOSApplicationExtension 10.0, *) {
if #available(watchOSApplicationExtension 10.0, *) {
let speedPerInterval = HKQuantity(unit: HKUnit.meter().unitDivided(by: HKUnit.second()),
doubleValue: WorkoutTracking.speed * 0.277778)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ These instructions build the app itself, not the test project.
```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 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
$ git clone https://github.com/cagnulein/qdomyos-zwift.git
$ cd qdomyos-zwift
$ git submodule update --init src/smtpclient/
@@ -106,7 +106,7 @@ This operation takes a moment to complete.
#### 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 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
git clone https://github.com/cagnulein/qdomyos-zwift.git
cd qdomyos-zwift
git submodule update --init src/smtpclient/
@@ -177,151 +177,6 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,32 +13,22 @@ ColumnLayout {
signal trainprogram_open_clicked(url name)
signal trainprogram_open_other_folder(url name)
signal trainprogram_preview(url name)
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileUrl)
} else {
trainprogram_open_clicked(fileUrl)
}
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
FileDialog {
id: fileDialogTrainProgram
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
} else {
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
}
fileDialogTrainProgram.close()
}
onRejected: {
console.log("Canceled")
fileDialogTrainProgram.close()
}
}
@@ -273,8 +263,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'gpx')
// Create a fresh FileDialog instance
fileDialogLoader.active = true
fileDialogTrainProgram.visible = true
}
anchors {
bottom: parent.bottom

View File

@@ -1,20 +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 QtQuick.Dialogs 1.0
SwitchDelegate {
id: root
MouseArea {
anchors.fill: parent
onClicked: {
if (mouse.x > parent.width - parent.indicator.width) {
root.checked = !root.checked
root.clicked()
}
}
}
}

View File

@@ -1,26 +0,0 @@
#ifndef OAUTH2_H
#define OAUTH2_H
#include <QString>
#include <QTextStream>
struct OAuth2Parameter {
QString responseType = QStringLiteral("code");
QString approval_prompt = QStringLiteral("force");
inline bool isEmpty() const { return responseType.isEmpty() && approval_prompt.isEmpty(); }
QString toString() const {
QString msg;
QTextStream out(&msg);
out << QStringLiteral("OAuth2Parameter{\n") << QStringLiteral("responseType: ") << this->responseType
<< QStringLiteral("\n") << QStringLiteral("approval_prompt: ") << this->approval_prompt
<< QStringLiteral("\n");
return msg;
}
};
#define _STR(x) #x
#define STRINGIFY(x) _STR(x)
#endif // OAUTH2_H

View File

@@ -1,51 +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 {
signal popupclose()
id: column1
spacing: 10
anchors.fill: parent
Settings {
id: settings
}
WebView {
id: webView
anchors.fill: parent
url: "http://localhost:" + settings.value("template_inner_QZWS_port") + "/previewchart/chart.htm"
visible: true
onLoadingChanged: {
if (loadRequest.errorString) {
console.error(loadRequest.errorString);
console.error("port " + settings.value("template_inner_QZWS_port"));
}
}
}
Timer {
id: chartJscheckStartFromWeb
interval: 200; running: true; repeat: true
onTriggered: {if(rootItem.startRequested) {rootItem.startRequested = false; rootItem.stopRequested = false; stackView.pop(); }}
}
Button {
id: closeButton
height: 50
width: parent.width
text: "Close"
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
popupclose();
}
anchors {
bottom: parent.bottom
}
}
Component.onCompleted: {
headerToolbar.visible = true;
}
}

View File

@@ -7,32 +7,22 @@ import QtQuick.Dialogs 1.0
ColumnLayout {
signal loadSettings(url name)
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
loadSettings(fileUrl)
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
}
FileDialog {
id: fileDialogSettings
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogSettings.fileUrl)
loadSettings(fileDialogSettings.fileUrl)
fileDialogSettings.close()
}
onRejected: {
console.log("Canceled")
fileDialogSettings.close()
}
}
StaticAccordionElement {
AccordionElement {
title: qsTr("Settings folder")
indicatRectColor: Material.color(Material.Grey)
textColor: Material.color(Material.Grey)
@@ -116,8 +106,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'settings')
// Create a fresh FileDialog instance
fileDialogLoader.active = true
fileDialogSettings.visible = true
}
anchors {
bottom: parent.bottom

View File

@@ -1,68 +0,0 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
ColumnLayout {
id: rootElement
property bool isOpen: false
property string title: ""
property alias color: accordionHeader.color
property alias textColor: accordionText.color
property alias textFont: accordionText.font.family
property alias textFontSize: accordionText.font.pixelSize
property alias indicatRectColor: indicatRect.color
default property alias accordionContent: contentPlaceholder.data
spacing: 0
Layout.fillWidth: true;
Rectangle {
id: accordionHeader
color: "red"
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true;
height: 48
Rectangle{
id:indicatRect
x: 16; y: 20
width: 8; height: 8
radius: 8
color: "white"
}
Text {
id: accordionText
x:34;y:13
color: "#FFFFFF"
text: rootElement.title
}
Image {
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)
{
indicatImg.source = "qrc:/icons/arrow-expand-vertical.png"
}else{
indicatImg.source = "qrc:/icons/arrow-collapse-vertical.png"
}
}
}
}
// This will get filled with the content
ColumnLayout {
id: contentPlaceholder
visible: rootElement.isOpen
Layout.fillWidth: true;
}
}

View File

@@ -1,6 +1,4 @@
import QtQuick 2.0
import AndroidStatusBar 1.0
import QtQuick.Window 2.12
/**
* adapted from StackOverflow:
@@ -31,9 +29,7 @@ ListView {
z: Infinity
spacing: 5
anchors.fill: parent
anchors.bottomMargin: (Qt.platform.os === "android" && AndroidStatusBar.apiLevel >= 31) ?
((Screen.orientation === Qt.PortraitOrientation || Screen.orientation === Qt.InvertedPortraitOrientation) ?
AndroidStatusBar.navigationBarHeight + 10 : 10) : 10
anchors.bottomMargin: 10
verticalLayoutDirection: ListView.BottomToTop
interactive: false

View File

@@ -11,32 +11,22 @@ ColumnLayout {
signal trainprogram_open_clicked(url name)
signal trainprogram_open_other_folder(url name)
signal trainprogram_preview(url name)
Loader {
id: fileDialogLoader
active: false
sourceComponent: Component {
FileDialog {
title: "Please choose a file"
folder: shortcuts.home
visible: true
onAccepted: {
console.log("You chose: " + fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileUrl)
} else {
trainprogram_open_clicked(fileUrl)
}
close()
// Destroy and recreate the dialog for next use
fileDialogLoader.active = false
}
onRejected: {
console.log("Canceled")
close()
// Destroy the dialog
fileDialogLoader.active = false
}
FileDialog {
id: fileDialogTrainProgram
title: "Please choose a file"
folder: shortcuts.home
onAccepted: {
console.log("You chose: " + fileDialogTrainProgram.fileUrl)
if(OS_VERSION === "Android") {
trainprogram_open_other_folder(fileDialogTrainProgram.fileUrl)
} else {
trainprogram_open_clicked(fileDialogTrainProgram.fileUrl)
}
fileDialogTrainProgram.close()
}
onRejected: {
console.log("Canceled")
fileDialogTrainProgram.close()
}
}
@@ -306,8 +296,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
onClicked: {
console.log("folder is " + rootItem.getWritableAppDir() + 'training')
// Create a fresh FileDialog instance
fileDialogLoader.active = true
fileDialogTrainProgram.visible = true
}
anchors {
bottom: parent.bottom

View File

@@ -1,80 +0,0 @@
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12
import QtQuick.Dialogs 1.0
import QtGraphicalEffects 1.12
import Qt.labs.settings 1.0
import QtMultimedia 5.15
import QtQuick.Layouts 1.3
import QtWebView 1.1
Item {
id: pelotonAuthPage
anchors.fill: parent
height: parent.height
width: parent.width
visible: true
// Signal to notify the parent stack when we want to go back
signal goBack()
WebView {
anchors.fill: parent
height: parent.height
width: parent.width
visible: !rootItem.pelotonPopupVisible
url: rootItem.getPelotonAuthUrl
}
Popup {
id: popupPelotonConnectedWeb
parent: Overlay.overlay
enabled: rootItem.pelotonPopupVisible
onEnabledChanged: { if(rootItem.pelotonPopupVisible) popupPelotonConnectedWeb.open() }
onClosed: {
rootItem.pelotonPopupVisible = false;
// Attempt to go back to the previous view after the popup is closed
goBack();
}
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
width: 380
height: 120
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: 370
height: 120
text: qsTr("Your Peloton account is now connected!")
}
}
// Add a MouseArea to capture clicks anywhere on the popup
MouseArea {
anchors.fill: parent
onClicked: {
popupPelotonConnectedWeb.close();
}
}
}
// Component is being completed
Component.onCompleted: {
console.log("WebPelotonAuth loaded")
}
}

View File

@@ -5,7 +5,6 @@ import Qt.labs.settings 1.0
Page {
id: wizardPage
objectName: "wizardPage"
property int currentStep: 0
property var selectedOptions: ({})
@@ -336,7 +335,7 @@ Page {
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Connect to Peloton")
text: qsTr("Peloton Login")
font.pixelSize: 24
font.bold: true
color: "white"
@@ -344,38 +343,56 @@ Page {
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Click the button below to connect your Peloton account")
text: qsTr("Username")
font.pixelSize: 20
wrapMode: Text.WordWrap
Layout.fillWidth: true
width: stackViewLocal.width * 0.8
horizontalAlignment: Text.AlignHCenter
font.bold: true
color: "white"
}
Image {
TextField {
id: pelotonUsernameTextField
text: settings.peloton_username
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
source: "icons/icons/Button_Connect_Rect_DarkMode.png"
fillMode: Image.PreserveAspectFit
width: parent.width * 0.8
Layout.fillHeight: false
onAccepted: settings.peloton_username = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
MouseArea {
anchors.fill: parent
onClicked: {
stackViewLocal.push("WebPelotonAuth.qml")
stackViewLocal.currentItem.goBack.connect(function() {
stackViewLocal.pop();
stackViewLocal.push(pelotonDifficultyComponent)
})
peloton_connect_clicked()
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Password")
font.pixelSize: 20
font.bold: true
color: "white"
}
TextField {
id: pelotonPasswordTextField
text: settings.peloton_password
horizontalAlignment: Text.AlignHCenter
Layout.fillHeight: false
Layout.alignment: Qt.AlignHCenter
inputMethodHints: Qt.ImhHiddenText
echoMode: TextInput.PasswordEchoOnEdit
onAccepted: settings.peloton_password = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Item {
Layout.preferredHeight: 50
}
WizardButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Next")
onClicked: {
settings.peloton_username = pelotonUsernameTextField.text;
settings.peloton_password = pelotonPasswordTextField.text;
stackViewLocal.push(pelotonDifficultyComponent)
}
}
WizardButton {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Back")
@@ -845,6 +862,7 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 0.5;
stackViewLocal.push(finalStepComponent);
}
}
@@ -903,6 +921,7 @@ Page {
text: qsTr("Finish")
onClicked: {
settings.tile_gears_enabled = true;
settings.gears_gain = 1;
stackViewLocal.push(finalStepComponent);
}
}

View File

@@ -1,71 +0,0 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: root
property string workoutSource: "QZ"
property alias text: tagText.text
// Auto-size based on text
width: tagText.implicitWidth + 16
height: 24
radius: 12
// Color scheme based on workout source
color: {
switch(workoutSource.toUpperCase()) {
case "PELOTON": return "#ff6b35"
case "ZWIFT": return "#ff6900"
case "ERG": return "#8bc34a"
case "QZ": return "#2196f3"
case "MANUAL": return "#757575"
default: return "#9e9e9e"
}
}
// Subtle border for better definition
border.color: Qt.darker(color, 1.2)
border.width: 1
Text {
id: tagText
anchors.centerIn: parent
text: workoutSource.toUpperCase()
color: "white"
font.pixelSize: 10
font.bold: true
font.family: "Arial"
}
// Subtle shadow effect
Rectangle {
anchors.fill: parent
anchors.topMargin: 1
anchors.leftMargin: 1
radius: parent.radius
color: "#20000000"
z: -1
}
// Hover effect for interactivity feedback
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
}
}

View File

@@ -1,910 +0,0 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtCharts 2.15
import Qt.labs.calendar 1.0
Page {
id: workoutHistoryPage
// Signal for chart preview
signal fitfile_preview_clicked(var url)
// Helper function to wrap text with emoji font only on Android
function wrapEmoji(emoji) {
return Qt.platform.os === "android" ?
'<font face="' + fontManager.emojiFontFamily + '">' + emoji + '</font>' :
emoji;
}
// Sport type to icon mapping (using FIT_SPORT values)
function getSportIcon(sport) {
switch(parseInt(sport)) {
case 1: // FIT_SPORT_RUNNING
case 11: // FIT_SPORT_WALKING
return "🏃"; // Running/Walking
case 2: // FIT_SPORT_CYCLING
return "🚴"; // Cycling
case 4: // FIT_SPORT_FITNESS_EQUIPMENT (Elliptical)
return "⭕"; // Elliptical
case 15: // FIT_SPORT_ROWING
return "🚣"; // Rowing
case 84: // FIT_SPORT_JUMPROPE
return "🪢"; // Jump Rope
default:
return "💪"; // Generic workout
}
}
ColumnLayout {
anchors.fill: parent
spacing: 10
// Header
Rectangle {
Layout.fillWidth: true
height: 60
color: "#f5f5f5"
// Calendar Icon Button - positioned absolutely on the left
Button {
id: calendarButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
width: 48
height: 48
background: Rectangle {
radius: 8
color: calendarButton.pressed ? "#e0e0e0" : "#f0f0f0"
border.color: "#d0d0d0"
border.width: 1
}
contentItem: Text {
text: Qt.platform.os === "android" ?
wrapEmoji("📅") :
"📅"
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: 20
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
calendarPopup.open()
}
}
// Title with filter status - centered
Column {
anchors.centerIn: parent
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Workout History"
font.pixelSize: 24
font.bold: true
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: workoutModel && workoutModel.isDateFiltered ?
"Filtered: " + workoutModel.filteredDate.toLocaleDateString() : ""
font.pixelSize: 12
color: "#666666"
visible: workoutModel && workoutModel.isDateFiltered
}
}
// Clear Filter Button - positioned absolutely on the right
Button {
id: clearFilterButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 12
width: 100
height: 36
visible: workoutModel && workoutModel.isDateFiltered
background: Rectangle {
radius: 6
color: clearFilterButton.pressed ? "#ff6666" : "#ff8888"
border.color: "#ff4444"
border.width: 1
}
contentItem: Text {
text: "Clear Filter"
color: "white"
font.pixelSize: 12
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
workoutModel.clearDateFilter()
}
}
}
// Loading indicator
BusyIndicator {
id: loadingIndicator
Layout.alignment: Qt.AlignHCenter
visible: workoutModel ? (workoutModel.isLoading || workoutModel.isDatabaseProcessing) : false
running: visible
}
// Database processing message
Text {
Layout.alignment: Qt.AlignHCenter
visible: workoutModel ? workoutModel.isDatabaseProcessing : false
text: "Processing workout files...\nThis may take a few moments on first startup."
horizontalAlignment: Text.AlignHCenter
color: "#666666"
font.pixelSize: 16
}
// Workout List
ListView {
id: workoutListView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.bottomMargin: streakBanner.visible ? streakBanner.height + 10 : 10
model: workoutModel
spacing: 8
clip: true
onContentYChanged: {
// Hide banner when scrolling down, show when at top
streakBanner.visible = contentY <= 20
}
delegate: SwipeDelegate {
id: swipeDelegate
width: parent.width
height: 135
Component.onCompleted: {
console.log("Delegate data:", JSON.stringify({
sport: sport,
title: title,
date: date,
duration: duration,
distance: distance,
calories: calories,
id: id
}))
}
swipe.right: Rectangle {
width: parent.width
height: parent.height
color: "#FF4444"
clip: true
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 20
Text {
text: Qt.platform.os === "android" ?
wrapEmoji("🗑️") + " Delete" :
"🗑️ Delete"
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
color: "white"
font.pixelSize: 16
anchors.verticalCenter: parent.verticalCenter
}
}
}
swipe.onCompleted: {
// Show confirmation dialog
confirmDialog.workoutId = model.id
confirmDialog.workoutTitle = model.title
confirmDialog.open()
}
// Card-like container
Rectangle {
anchors.fill: parent
anchors.margins: 8
radius: 10
color: "white"
border.color: "#e0e0e0"
// Workout Type Tag - positioned absolutely in top-right
WorkoutTypeTag {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 12
workoutSource: workoutModel ? workoutModel.getWorkoutSource(model.id) : "QZ"
}
// Action buttons - positioned absolutely in bottom-right
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 12
spacing: 8
// Peloton URL button
Button {
width: 40
height: 45
visible: workoutModel && workoutModel.getWorkoutSource(model.id) === "PELOTON" &&
workoutModel.getPelotonUrl(model.id) !== ""
background: Rectangle {
color: parent.pressed ? "#ff8855" : "#ff6b35"
radius: 6
border.color: "#cc5529"
border.width: 1
}
contentItem: Text {
text: Qt.platform.os === "android" ?
wrapEmoji("🌐") :
"🌐"
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: 16
color: "white"
anchors.centerIn: parent
}
onClicked: {
workoutModel.openPelotonUrl(model.id)
}
}
// Training Program button
Button {
width: 40
height: 45
visible: workoutModel && workoutModel.hasTrainingProgram(model.id)
background: Rectangle {
color: parent.pressed ? "#1976d2" : "#2196f3"
radius: 6
border.color: "#1565c0"
border.width: 1
}
contentItem: Text {
text: Qt.platform.os === "android" ?
wrapEmoji("📋") :
"📋"
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: 16
color: "white"
anchors.centerIn: parent
}
onClicked: {
var success = workoutModel.loadTrainingProgram(model.id)
if (success) {
trainingProgramDialog.title = "Success"
trainingProgramDialog.message = "Training program loaded successfully!"
trainingProgramDialog.isSuccess = true
} else {
trainingProgramDialog.title = "Error"
trainingProgramDialog.message = "Failed to load training program. Please check if the file exists."
trainingProgramDialog.isSuccess = false
}
trainingProgramDialog.open()
}
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 16
// Sport icon
Column {
Layout.alignment: Qt.AlignVCenter
Text {
text: Qt.platform.os === "android" ?
wrapEmoji(getSportIcon(sport)) :
getSportIcon(sport)
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: 32
}
}
// Workout info
ColumnLayout {
Layout.fillWidth: true
spacing: 4
// Title row (without tag) with auto-scrolling
Rectangle {
Layout.fillWidth: true
Layout.rightMargin: 80 // Reserve space for tag
Layout.preferredHeight: 24
clip: true
color: "transparent"
Text {
id: titleText
text: title
font.bold: true
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
// Auto-scroll animation for long titles
SequentialAnimation on x {
running: titleText.contentWidth > titleText.parent.width
loops: Animation.Infinite
NumberAnimation {
from: 0
to: -(titleText.contentWidth - titleText.parent.width + 20)
duration: Math.max(3000, titleText.contentWidth * 30)
}
PauseAnimation { duration: 1500 }
NumberAnimation {
from: -(titleText.contentWidth - titleText.parent.width + 20)
to: 0
duration: Math.max(3000, titleText.contentWidth * 30)
}
PauseAnimation { duration: 2000 }
}
}
}
Text {
text: date
color: "#666666"
}
// Stats row
RowLayout {
spacing: 16
Text {
text: "⏱ " + duration
}
Text {
text: "📏 " + distance.toFixed(2) + " km"
}
}
RowLayout {
spacing: 16
Text {
text: Qt.platform.os === "android" ?
wrapEmoji("🔥") + " " + Math.round(calories) + " kcal" :
"🔥 " + Math.round(calories) + " kcal"
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
}
}
}
}
}
onClicked: {
console.log("Workout clicked, ID:", model.id)
// Get workout details from the model
var details = workoutModel.getWorkoutDetails(model.id)
console.log("Workout details:", JSON.stringify(details))
// Emit signal with file URL for chart preview - same pattern as profiles.qml
console.log("Emitting fitfile_preview_clicked with path:", details.filePath)
// Convert to URL like profiles.qml does with FolderListModel
var fileUrl = "file://" + details.filePath
console.log("Converted to URL:", fileUrl)
workoutHistoryPage.fitfile_preview_clicked(fileUrl)
// Push the ChartJsTest view
stackView.push("PreviewChart.qml")
}
}
}
}
// Confirmation Dialog
Dialog {
id: confirmDialog
property int workoutId
property string workoutTitle
title: "Delete Workout"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
x: (parent.width - width) / 2
y: (parent.height - height) / 2
Text {
text: "Are you sure you want to delete '" + confirmDialog.workoutTitle + "'?"
}
onAccepted: {
workoutModel.deleteWorkout(confirmDialog.workoutId)
swipeDelegate.swipe.close()
}
onRejected: {
swipeDelegate.swipe.close()
}
}
// Training Program Loading Dialog
Dialog {
id: trainingProgramDialog
property string message: ""
property bool isSuccess: true
modal: true
standardButtons: Dialog.Ok
x: (parent.width - width) / 2
y: (parent.height - height) / 2
background: Rectangle {
color: "white"
radius: 8
border.color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
border.width: 2
}
header: Rectangle {
height: 50
color: trainingProgramDialog.isSuccess ? "#4caf50" : "#f44336"
radius: 8
Text {
anchors.centerIn: parent
text: trainingProgramDialog.title
color: "white"
font.pixelSize: 18
font.bold: true
}
}
contentItem: ColumnLayout {
spacing: 16
Text {
Layout.margins: 20
Layout.preferredWidth: 300
Layout.preferredHeight: 120
text: Qt.platform.os === "android" ?
wrapEmoji("🔥") + " " +
wrapEmoji(trainingProgramDialog.isSuccess ? '✅' : '❌') +
" " + trainingProgramDialog.message :
"🔥 " + (trainingProgramDialog.isSuccess ? '✅ ' : '❌ ') + trainingProgramDialog.message
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 14
}
}
}
// Streak Banner at the bottom
Rectangle {
id: streakBanner
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 80
visible: workoutModel
Behavior on visible {
NumberAnimation {
properties: "opacity"
duration: 300
easing.type: Easing.InOutQuad
}
}
// Special pulsing effect for major milestones
SequentialAnimation on opacity {
running: workoutModel && workoutModel.currentStreak >= 30
loops: Animation.Infinite
NumberAnimation { from: 0.9; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
NumberAnimation { from: 1.0; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
}
gradient: Gradient {
GradientStop {
position: 0.0;
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFD700" :
workoutModel && (workoutModel.currentStreak >= 180) ? "#9932CC" :
workoutModel && (workoutModel.currentStreak >= 90) ? "#FF1493" :
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF4500" :
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF6347" : "#FF6B35"
}
GradientStop {
position: 1.0;
color: workoutModel && (workoutModel.currentStreak >= 365) ? "#FFA500" :
workoutModel && (workoutModel.currentStreak >= 180) ? "#8A2BE2" :
workoutModel && (workoutModel.currentStreak >= 90) ? "#DC143C" :
workoutModel && (workoutModel.currentStreak >= 30) ? "#FF6B35" :
workoutModel && (workoutModel.currentStreak >= 7) ? "#FF4500" : "#F7931E"
}
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#40FFFFFF" }
GradientStop { position: 1.0; color: "#00FFFFFF" }
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 4
// Current streak with count
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 15
// Fire emoji with animation
Text {
text: Qt.platform.os === "android" ? (
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("👑🔥") :
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🎖️🔥") :
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🦁🔥") :
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🎊🔥") :
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🏆🔥") : wrapEmoji("🔥")
) : (
workoutModel && workoutModel.currentStreak >= 365 ? "👑🔥" :
workoutModel && workoutModel.currentStreak >= 180 ? "🎖️🔥" :
workoutModel && workoutModel.currentStreak >= 90 ? "🦁🔥" :
workoutModel && workoutModel.currentStreak >= 30 ? "🎊🔥" :
workoutModel && workoutModel.currentStreak >= 7 ? "🏆🔥" : "🔥"
)
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: workoutModel && workoutModel.currentStreak >= 7 ? 28 : 24
SequentialAnimation on scale {
running: workoutModel && workoutModel.currentStreak > 0
loops: Animation.Infinite
NumberAnimation {
from: 1.0;
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
duration: workoutModel && workoutModel.currentStreak >= 365 ? 600 : 800;
easing.type: Easing.InOutSine
}
NumberAnimation {
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
to: 1.0;
duration: workoutModel && workoutModel.currentStreak >= 7 ? 600 : 800;
easing.type: Easing.InOutSine
}
}
// Special sparkle effect for year achievement
SequentialAnimation on rotation {
running: workoutModel && workoutModel.currentStreak >= 7
loops: Animation.Infinite
NumberAnimation { from: 0; to: 360; duration: 3000; easing.type: Easing.Linear }
}
}
// Current streak count
Text {
text: workoutModel ? workoutModel.currentStreak + " day" + (workoutModel.currentStreak !== 1 ? "s" : "") + " streak" : ""
font.pixelSize: 18
font.bold: true
color: "white"
visible: workoutModel
}
// Another fire emoji
Text {
text: Qt.platform.os === "android" ? (
workoutModel && workoutModel.currentStreak >= 365 ? wrapEmoji("🔥👑") :
workoutModel && workoutModel.currentStreak >= 180 ? wrapEmoji("🔥🎖️") :
workoutModel && workoutModel.currentStreak >= 90 ? wrapEmoji("🔥🦁") :
workoutModel && workoutModel.currentStreak >= 30 ? wrapEmoji("🔥🎊") :
workoutModel && workoutModel.currentStreak >= 7 ? wrapEmoji("🔥🏆") : wrapEmoji("🔥")
) : (
workoutModel && workoutModel.currentStreak >= 365 ? "🔥👑" :
workoutModel && workoutModel.currentStreak >= 180 ? "🔥🎖️" :
workoutModel && workoutModel.currentStreak >= 90 ? "🔥🦁" :
workoutModel && workoutModel.currentStreak >= 30 ? "🔥🎊" :
workoutModel && workoutModel.currentStreak >= 7 ? "🔥🏆" : "🔥"
)
textFormat: Qt.platform.os === "android" ? Text.RichText : Text.PlainText
font.pixelSize: workoutModel && workoutModel.currentStreak >= 365 ? 28 : 24
SequentialAnimation on scale {
running: workoutModel && workoutModel.currentStreak > 0
loops: Animation.Infinite
NumberAnimation {
from: 1.0;
to: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
easing.type: Easing.InOutSine
}
NumberAnimation {
from: workoutModel && workoutModel.currentStreak >= 7 ? 1.4 : 1.2;
to: 1.0;
duration: workoutModel && workoutModel.currentStreak >= 7 ? 700 : 1000;
easing.type: Easing.InOutSine
}
}
// Counter-rotation for variety
SequentialAnimation on rotation {
running: workoutModel && workoutModel.currentStreak >= 7
loops: Animation.Infinite
NumberAnimation { from: 0; to: -360; duration: 3500; easing.type: Easing.Linear }
}
}
}
// Motivational message
Text {
Layout.alignment: Qt.AlignHCenter
text: workoutModel ? workoutModel.streakMessage : ""
font.pixelSize: 14
font.italic: true
color: "white"
visible: workoutModel && workoutModel.streakMessage !== ""
opacity: 0.9
}
// Best streak (smaller text)
Text {
Layout.alignment: Qt.AlignHCenter
text: workoutModel ? "Personal best: " + workoutModel.longestStreak + " day" + (workoutModel.longestStreak !== 1 ? "s" : "") : ""
font.pixelSize: 12
color: "white"
visible: workoutModel && workoutModel.longestStreak > workoutModel.currentStreak && workoutModel.longestStreak > 0
opacity: 0.7
}
}
// Subtle shadow effect at the top
Rectangle {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 2
gradient: Gradient {
GradientStop { position: 0.0; color: "#40000000" }
GradientStop { position: 1.0; color: "#00000000" }
}
}
}
// Calendar Popup
Popup {
id: calendarPopup
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: Math.min(parent.width * 0.9, 400)
height: Math.min(parent.height * 0.8, 500)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
// Refresh workout dates when calendar opens
if (workoutModel) {
calendar.workoutDates = workoutModel.getWorkoutDates()
console.log("Calendar opened, refreshed workout dates:", JSON.stringify(calendar.workoutDates))
}
}
background: Rectangle {
color: "white"
radius: 12
border.color: "#d0d0d0"
border.width: 1
// Shadow effect
Rectangle {
anchors.fill: parent
anchors.topMargin: 2
anchors.leftMargin: 2
radius: parent.radius
color: "#40000000"
z: -1
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Calendar Header
RowLayout {
Layout.fillWidth: true
Button {
text: "<"
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() - 1, 1)
}
Text {
Layout.fillWidth: true
text: calendar.selectedDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
font.pixelSize: 18
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
Button {
text: ">"
onClicked: calendar.selectedDate = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 1)
}
}
// Calendar Grid
GridLayout {
id: calendar
Layout.fillWidth: true
Layout.fillHeight: true
columns: 7
property date selectedDate: new Date()
property var workoutDates: workoutModel ? workoutModel.getWorkoutDates() : []
// Debug: print workout dates when they change
onWorkoutDatesChanged: {
console.log("Calendar workout dates updated:", JSON.stringify(workoutDates))
}
// Day headers
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Text {
Layout.fillWidth: true
Layout.preferredHeight: 30
text: modelData
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: "#666666"
}
}
// Calendar days
Repeater {
model: getCalendarDays()
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 40
property date dayDate: modelData.date
property bool isCurrentMonth: modelData.currentMonth
property bool hasWorkout: modelData.hasWorkout
property bool isToday: dayDate.toDateString() === new Date().toDateString()
color: {
if (mouseArea.pressed) return "#e3f2fd"
if (isToday) return "#bbdefb"
if (!isCurrentMonth) return "#f5f5f5"
return "white"
}
border.color: isToday ? "#2196f3" : "#e0e0e0"
border.width: isToday ? 2 : 1
radius: 4
Column {
anchors.centerIn: parent
spacing: 2
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: dayDate.getDate()
color: isCurrentMonth ? "black" : "#cccccc"
font.pixelSize: 14
}
// Workout indicator dot
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 8
height: 8
radius: 4
color: "#ff6b35"
visible: hasWorkout
border.width: 1
border.color: "#cc5529"
// Debug: log when a dot should be visible
Component.onCompleted: {
if (hasWorkout) {
console.log("Workout dot visible for date:", dayDate.toDateString())
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: {
if (isCurrentMonth) {
var year = dayDate.getFullYear();
var month = dayDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
var day = dayDate.getDate();
var dateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
workoutModel.setDateFilter(dateString);
calendarPopup.close();
}
}
}
}
}
}
// Close button
Button {
Layout.alignment: Qt.AlignHCenter
text: "Close"
onClicked: calendarPopup.close()
}
}
}
// JavaScript functions for calendar
function getCalendarDays() {
var days = []
var firstDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth(), 1)
var lastDay = new Date(calendar.selectedDate.getFullYear(), calendar.selectedDate.getMonth() + 1, 0)
var startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - firstDay.getDay()) // Go back to start of week
var workoutDates = calendar.workoutDates || []
console.log("getCalendarDays: workoutDates received:", JSON.stringify(workoutDates))
// workoutDates is now a QStringList (array of strings in format "yyyy-MM-dd")
var workoutDateStrings = workoutDates || []
console.log("Final workout date strings:", JSON.stringify(workoutDateStrings))
for (var i = 0; i < 42; i++) { // 6 rows x 7 days
var currentDate = new Date(startDate)
currentDate.setDate(startDate.getDate() + i)
// Costruisci la stringa YYYY-MM-DD dai componenti della data locale per evitare problemi di fuso orario
var year = currentDate.getFullYear();
var month = currentDate.getMonth() + 1; // i mesi JS sono 0-indicizzati
var day = currentDate.getDate();
var localDateString = year + "-" + (month < 10 ? '0' + month : month) + "-" + (day < 10 ? '0' + day : day);
var hasWorkout = workoutDateStrings.indexOf(localDateString) !== -1;
if (hasWorkout) {
// Questo console.log ora utilizza la stringa della data locale corretta per la corrispondenza
console.log("Found workout match for:", localDateString);
}
var isCurrentMonth = currentDate.getMonth() === calendar.selectedDate.getMonth()
days.push({
date: currentDate,
currentMonth: isCurrentMonth,
hasWorkout: hasWorkout
})
}
console.log("getCalendarDays: returning", days.length, "days")
return days
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.11" android:versionCode="1155" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.18.8" android:versionCode="952" android:installLocation="auto">
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS -->
@@ -10,7 +10,7 @@
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.cagnulen.qdomyoszwift.CustomQtActivity" android:label="QZ" android:launchMode="singleTop">
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="QZ" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -106,16 +106,6 @@
android:name=".ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
<service android:name=".VirtualGearingService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/virtual_gearing_service_config" />
</service>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
@@ -130,7 +120,7 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="36" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>

View File

@@ -44,13 +44,13 @@ dependencies {
def appcompat_version = "1.3.1"
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "com.android.billingclient:billing:8.0.0"
implementation "com.android.billingclient:billing:6.0.1"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation files('libs/usb-serial-for-android-3.8.1.aar')
implementation 'com.github.mik3y:usb-serial-for-android:v3.4.6'
androidTestImplementation "com.android.support:support-annotations:28.0.0"
implementation 'com.google.android.gms:play-services-wearable:+'
@@ -129,7 +129,7 @@ android {
resConfig "en"
compileSdkVersion 33
minSdkVersion = 21
targetSdkVersion = 36
targetSdkVersion = 34
}
tasks.all { task ->

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="virtual_gearing_service_description">Virtual Gearing Service for QZ - Enables touch simulation for virtual shifting in cycling apps</string>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/virtual_gearing_service_description"
android:packageNames="@null"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true" />

View File

@@ -1,4 +1,5 @@
package org.cagnulen.qdomyoszwift;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.Activity;
@@ -22,151 +23,110 @@ import android.widget.Button;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.content.Intent;
public class Ant {
private ChannelService.ChannelServiceComm mChannelService = null;
private boolean mChannelServiceBound = false;
private final String TAG = "Ant";
public static Activity activity = null;
static boolean speedRequest = false;
static boolean heartRequest = false;
static boolean bikeRequest = false; // Added bike request flag
static boolean garminKey = false;
static boolean treadmill = false;
static boolean technoGymGroupCycle = false;
static int antBikeDeviceNumber = 0;
static int antHeartDeviceNumber = 0;
// Updated antStart method with BikeRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
QLog.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
bikeRequest = BikeRequest; // Set bike request flag
technoGymGroupCycle = TechnoGymGroupCycle;
antBikeDeviceNumber = AntBikeDeviceNumber;
antHeartDeviceNumber = AntHeartDeviceNumber;
activity = a;
if(a != null)
QLog.v(TAG, "antStart activity is valid");
else
{
QLog.v(TAG, "antStart activity is invalid");
return;
}
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill) {
Log.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
activity = a;
if(a != null)
Log.v(TAG, "antStart activity is valid");
else
{
Log.v(TAG, "antStart activity is invalid");
return;
}
if(!mChannelServiceBound) doBindChannelService();
}
private ServiceConnection mChannelServiceConnection = new ServiceConnection()
{
@Override
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
{
QLog.v(TAG, "mChannelServiceConnection.onServiceConnected...");
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
QLog.v(TAG, "...mChannelServiceConnection.onServiceConnected");
}
@Override
public void onServiceConnected(ComponentName name, IBinder serviceBinder)
{
Log.v(TAG, "mChannelServiceConnection.onServiceConnected...");
@Override
public void onServiceDisconnected(ComponentName arg0)
{
QLog.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
// Clearing and disabling when disconnecting from ChannelService
mChannelService = null;
QLog.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
}
};
mChannelService = (ChannelService.ChannelServiceComm) serviceBinder;
Log.v(TAG, "...mChannelServiceConnection.onServiceConnected");
}
@Override
public void onServiceDisconnected(ComponentName arg0)
{
Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected...");
// Clearing and disabling when disconnecting from ChannelService
mChannelService = null;
Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected");
}
};
private void doBindChannelService()
{
QLog.v(TAG, "doBindChannelService...");
// Binds to ChannelService. ChannelService binds and manages connection between the
// app and the ANT Radio Service
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection, Context.BIND_AUTO_CREATE);
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
doUnbindChannelService();
QLog.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
QLog.v(TAG, "...doBindChannelService");
}
Log.v(TAG, "doBindChannelService...");
// Binds to ChannelService. ChannelService binds and manages connection between the
// app and the ANT Radio Service
mChannelServiceBound = activity.bindService(new Intent(activity, ChannelService.class), mChannelServiceConnection , Context.BIND_AUTO_CREATE);
if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI
doUnbindChannelService();
Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound);
Log.v(TAG, "...doBindChannelService");
}
public void doUnbindChannelService()
{
QLog.v(TAG, "doUnbindChannelService...");
if(mChannelServiceBound)
{
activity.unbindService(mChannelServiceConnection);
mChannelServiceBound = false;
}
QLog.v(TAG, "...doUnbindChannelService");
}
Log.v(TAG, "doUnbindChannelService...");
if(mChannelServiceBound)
{
activity.unbindService(mChannelServiceConnection);
mChannelServiceBound = false;
}
Log.v(TAG, "...doUnbindChannelService");
}
public void setCadenceSpeedPower(float speed, int power, int cadence)
{
if(mChannelService == null)
return;
QLog.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
mChannelService.setSpeed(speed);
mChannelService.setPower(power);
mChannelService.setCadence(cadence);
if(mChannelService == null)
return;
Log.v(TAG, "setCadenceSpeedPower " + speed + " " + power + " " + cadence);
mChannelService.setSpeed(speed);
mChannelService.setPower(power);
mChannelService.setCadence(cadence);
}
public int getHeart()
{
if(mChannelService == null)
return 0;
QLog.v(TAG, "getHeart");
return mChannelService.getHeart();
}
if(mChannelService == null)
return 0;
// Added bike-related getter methods
public int getBikeCadence() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikeCadence");
return mChannelService.getBikeCadence();
}
public int getBikePower() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikePower");
return mChannelService.getBikePower();
}
public double getBikeSpeed() {
if(mChannelService == null)
return 0.0;
QLog.v(TAG, "getBikeSpeed");
return mChannelService.getBikeSpeed();
}
public long getBikeDistance() {
if(mChannelService == null)
return 0;
QLog.v(TAG, "getBikeDistance");
return mChannelService.getBikeDistance();
}
public boolean isBikeConnected() {
if(mChannelService == null)
return false;
QLog.v(TAG, "isBikeConnected");
return mChannelService.isBikeConnected();
}
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if(mChannelService == null)
return;
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
Log.v(TAG, "getHeart");
return mChannelService.getHeart();
}
}

View File

@@ -1,116 +0,0 @@
package org.cagnulen.qdomyoszwift;
import org.cagnulen.qdomyoszwift.QLog;
public class AppConfiguration {
private static final String TAG = "AppConfiguration";
public static class TouchCoordinate {
public final double xPercent;
public final double yPercent;
public TouchCoordinate(double xPercent, double yPercent) {
this.xPercent = xPercent;
this.yPercent = yPercent;
}
public int getX(int screenWidth) {
return (int) (screenWidth * xPercent);
}
public int getY(int screenHeight) {
return (int) (screenHeight * yPercent);
}
}
public static class AppConfig {
public final String appName;
public final String packageName;
public final TouchCoordinate shiftUp;
public final TouchCoordinate shiftDown;
public AppConfig(String appName, String packageName, TouchCoordinate shiftUp, TouchCoordinate shiftDown) {
this.appName = appName;
this.packageName = packageName;
this.shiftUp = shiftUp;
this.shiftDown = shiftDown;
}
}
// Predefined configurations based on SwiftControl
private static final AppConfig[] SUPPORTED_APPS = {
// MyWhoosh - coordinates from SwiftControl repository
new AppConfig(
"MyWhoosh",
"com.mywhoosh.whooshgame",
new TouchCoordinate(0.98, 0.94), // Shift Up - bottom right corner
new TouchCoordinate(0.80, 0.94) // Shift Down - more to the left
),
// IndieVelo / TrainingPeaks
new AppConfig(
"IndieVelo",
"com.indieVelo.client",
new TouchCoordinate(0.66, 0.74), // Shift Up - center right
new TouchCoordinate(0.575, 0.74) // Shift Down - center left
),
// Biketerra.com
new AppConfig(
"Biketerra",
"biketerra",
new TouchCoordinate(0.8, 0.5), // Generic coordinates for now
new TouchCoordinate(0.2, 0.5)
),
// Default configuration for unrecognized apps
new AppConfig(
"Default",
"*",
new TouchCoordinate(0.85, 0.9), // Conservative coordinates
new TouchCoordinate(0.15, 0.9)
)
};
public static AppConfig getConfigForPackage(String packageName) {
// Use custom coordinates from settings instead of hardcoded values
return getCurrentConfig();
}
// Get current configuration from user settings
public static AppConfig getCurrentConfig() {
try {
double shiftUpX = VirtualGearingBridge.getVirtualGearingShiftUpX();
double shiftUpY = VirtualGearingBridge.getVirtualGearingShiftUpY();
double shiftDownX = VirtualGearingBridge.getVirtualGearingShiftDownX();
double shiftDownY = VirtualGearingBridge.getVirtualGearingShiftDownY();
int appIndex = VirtualGearingBridge.getVirtualGearingApp();
String appName = "Custom";
if (appIndex >= 0 && appIndex < SUPPORTED_APPS.length) {
appName = SUPPORTED_APPS[appIndex].appName;
}
QLog.d(TAG, "Using custom coordinates: shiftUp(" + shiftUpX + "," + shiftUpY +
") shiftDown(" + shiftDownX + "," + shiftDownY + ") for " + appName);
return new AppConfig(
appName,
"*", // Package name not relevant for custom config
new TouchCoordinate(shiftUpX, shiftUpY),
new TouchCoordinate(shiftDownX, shiftDownY)
);
} catch (Exception e) {
QLog.e(TAG, "Error getting custom config, using fallback", e);
return getDefaultConfig();
}
}
public static AppConfig getDefaultConfig() {
return SUPPORTED_APPS[SUPPORTED_APPS.length - 1]; // Last element is the default
}
public static AppConfig[] getAllConfigs() {
return SUPPORTED_APPS;
}
}

View File

@@ -1,562 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.content.Context;
import org.cagnulen.qdomyoszwift.QLog;
import android.app.Activity;
// ANT+ Plugin imports
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IFitnessEquipmentStateReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IBikeDataReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.IGeneralFitnessEquipmentDataReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentState;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.EquipmentType;
import com.dsi.ant.plugins.antplus.pcc.AntPlusFitnessEquipmentPcc.HeartRateDataSource;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.IRawPowerOnlyDataReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikePowerPcc.ICalculatedPowerReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.CalculatedAccumulatedDistanceReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc;
import com.dsi.ant.plugins.antplus.pcc.AntPlusBikeCadencePcc.ICalculatedCadenceReceiver;
import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IDeviceStateChangeReceiver;
import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IPluginAccessResultReceiver;
import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
// Java imports
import java.math.BigDecimal;
import java.util.EnumSet;
public class BikeChannelController {
private static final String TAG = BikeChannelController.class.getSimpleName();
private Context context;
private AntPlusFitnessEquipmentPcc fePcc = null;
private PccReleaseHandle<AntPlusFitnessEquipmentPcc> releaseHandle = null;
private AntPlusBikePowerPcc powerPcc = null;
private PccReleaseHandle<AntPlusBikePowerPcc> powerReleaseHandle = null;
private AntPlusBikeSpeedDistancePcc speedCadencePcc = null;
private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> speedCadenceReleaseHandle = null;
private AntPlusBikeCadencePcc cadencePcc = null;
private PccReleaseHandle<AntPlusBikeCadencePcc> cadenceReleaseHandle = null;
private boolean isConnected = false;
private boolean isPowerConnected = false;
private boolean isSpeedCadenceConnected = false;
// Bike data fields - from fitness equipment
public int cadence = 0; // Current cadence in RPM
public int power = 0; // Current power in watts
public BigDecimal speed = new BigDecimal(0); // Current speed in m/s
public long distance = 0; // Total distance in meters
public long calories = 0; // Total calories burned
public EquipmentType equipmentType = EquipmentType.UNKNOWN;
public EquipmentState equipmentState = EquipmentState.ASLEEP_OFF;
public int heartRate = 0; // Heart rate from equipment
public HeartRateDataSource heartRateSource = HeartRateDataSource.UNKNOWN;
public BigDecimal elapsedTime = new BigDecimal(0); // Elapsed time in seconds
// Bike data fields - from dedicated sensors
public int powerSensorPower = 0; // Power from dedicated power sensor
public int speedSensorCadence = 0; // Cadence from speed/cadence sensor
public BigDecimal speedSensorSpeed = new BigDecimal(0); // Speed from speed/cadence sensor
public long speedSensorDistance = 0; // Distance from speed/cadence sensor
// Fitness equipment state receiver
private final IFitnessEquipmentStateReceiver mFitnessEquipmentStateReceiver =
new IFitnessEquipmentStateReceiver() {
@Override
public void onNewFitnessEquipmentState(long estTimestamp,
EnumSet<EventFlag> eventFlags,
EquipmentType type,
EquipmentState state) {
equipmentType = type;
equipmentState = state;
QLog.d(TAG, "Equipment type: " + type + ", State: " + state);
// Only subscribe to bike specific data if this is actually a bike
if (type == EquipmentType.BIKE && !isSubscribedToBikeData) {
subscribeToBikeSpecificData();
isSubscribedToBikeData = true;
}
}
};
public BikeChannelController(boolean technoGymGroupCycle, int antBikeDeviceNumber) {
this.context = Ant.activity;
if (technoGymGroupCycle) {
// For Technogym Group Cycle: disable openChannel, enable openPowerSensorChannel
openPowerSensorChannel(antBikeDeviceNumber);
} else {
// Standard behavior: enable openChannel, disable openPowerSensorChannel
openChannel();
}
//openSpeedCadenceSensorChannel();
}
public boolean openChannel() {
// Request access to first available fitness equipment device
// Using requestNewOpenAccess from the sample code
releaseHandle = AntPlusFitnessEquipmentPcc.requestNewOpenAccess(
(Activity)context,
context,
new IPluginAccessResultReceiver<AntPlusFitnessEquipmentPcc>() {
@Override
public void onResultReceived(AntPlusFitnessEquipmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
switch(resultCode) {
case SUCCESS:
fePcc = result;
isConnected = true;
QLog.d(TAG, "Connected to fitness equipment: " + result.getDeviceName());
subscribeToBikeEvents();
break;
case CHANNEL_NOT_AVAILABLE:
QLog.e(TAG, "Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
QLog.e(TAG, "ANT Adapter Not Available");
break;
case BAD_PARAMS:
QLog.e(TAG, "Bad request parameters");
break;
case OTHER_FAILURE:
QLog.e(TAG, "RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
QLog.e(TAG, "Dependency not installed");
break;
case USER_CANCELLED:
QLog.e(TAG, "User cancelled");
break;
default:
QLog.e(TAG, "Unrecognized result: " + resultCode);
break;
}
}
},
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isConnected = false;
}
}
},
mFitnessEquipmentStateReceiver
);
return isConnected;
}
public boolean openPowerSensorChannel(int deviceNumber) {
// Request access to power sensor device (deviceNumber = 0 means first available)
powerReleaseHandle = AntPlusBikePowerPcc.requestAccess((Activity)context, deviceNumber, 0,
new IPluginAccessResultReceiver<AntPlusBikePowerPcc>() {
@Override
public void onResultReceived(AntPlusBikePowerPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
switch(resultCode) {
case SUCCESS:
powerPcc = result;
isPowerConnected = true;
QLog.d(TAG, "Connected to power sensor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
subscribeToPowerSensorEvents();
break;
case CHANNEL_NOT_AVAILABLE:
QLog.e(TAG, "Power Sensor Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
QLog.e(TAG, "ANT Adapter Not Available for Power Sensor");
break;
case BAD_PARAMS:
QLog.e(TAG, "Bad request parameters for Power Sensor");
break;
case OTHER_FAILURE:
QLog.e(TAG, "Power Sensor RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
QLog.e(TAG, "Dependency not installed for Power Sensor");
break;
case USER_CANCELLED:
QLog.e(TAG, "User cancelled Power Sensor");
break;
default:
QLog.e(TAG, "Unrecognized power sensor result: " + resultCode);
break;
}
}
},
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Power Sensor State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isPowerConnected = false;
}
}
}
);
return isPowerConnected;
}
public boolean openSpeedCadenceSensorChannel() {
// Request access to first available speed/cadence sensor device
speedCadenceReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess((Activity)context, context,
new IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {
@Override
public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
switch(resultCode) {
case SUCCESS:
speedCadencePcc = result;
isSpeedCadenceConnected = true;
QLog.d(TAG, "Connected to speed/cadence sensor: " + result.getDeviceName());
subscribeToSpeedCadenceSensorEvents();
break;
case CHANNEL_NOT_AVAILABLE:
QLog.e(TAG, "Speed/Cadence Sensor Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
QLog.e(TAG, "ANT Adapter Not Available for Speed/Cadence Sensor");
break;
case BAD_PARAMS:
QLog.e(TAG, "Bad request parameters for Speed/Cadence Sensor");
break;
case OTHER_FAILURE:
QLog.e(TAG, "Speed/Cadence Sensor RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
QLog.e(TAG, "Dependency not installed for Speed/Cadence Sensor");
break;
case USER_CANCELLED:
QLog.e(TAG, "User cancelled Speed/Cadence Sensor");
break;
default:
QLog.e(TAG, "Unrecognized speed/cadence sensor result: " + resultCode);
break;
}
}
},
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Speed/Cadence Sensor State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isSpeedCadenceConnected = false;
}
}
}
);
return isSpeedCadenceConnected;
}
private void subscribeToBikeEvents() {
if (fePcc != null) {
// General fitness equipment data
fePcc.subscribeGeneralFitnessEquipmentDataEvent(new IGeneralFitnessEquipmentDataReceiver() {
@Override
public void onNewGeneralFitnessEquipmentData(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal elapsedTime, long cumulativeDistance,
BigDecimal instantaneousSpeed, boolean virtualInstantaneousSpeed,
int instantaneousHeartRate, HeartRateDataSource source) {
if (elapsedTime != null && elapsedTime.intValue() != -1) {
BikeChannelController.this.elapsedTime = elapsedTime;
}
if (cumulativeDistance != -1) {
distance = cumulativeDistance;
}
if (instantaneousSpeed != null && instantaneousSpeed.intValue() != -1) {
speed = instantaneousSpeed;
}
if (instantaneousHeartRate != -1) {
heartRate = instantaneousHeartRate;
heartRateSource = source;
}
QLog.d(TAG, "General Data - Time: " + elapsedTime + "s, Distance: " +
distance + "m, Speed: " + speed + "m/s, HR: " + heartRate + "bpm");
}
});
}
}
private boolean isSubscribedToBikeData = false;
private void subscribeToBikeSpecificData() {
if (fePcc != null) {
// Subscribe to bike specific data
fePcc.getBikeMethods().subscribeBikeDataEvent(new IBikeDataReceiver() {
@Override
public void onNewBikeData(long estTimestamp, EnumSet<EventFlag> eventFlags,
int instantaneousCadence, int instantaneousPower) {
if (instantaneousCadence != -1) {
cadence = instantaneousCadence;
}
if (instantaneousPower != -1) {
power = instantaneousPower;
}
QLog.d(TAG, "Bike Data - Cadence: " + cadence + "rpm, Power: " + power + "W");
}
});
}
}
private void subscribeToPowerSensorEvents() {
if (powerPcc != null) {
// Subscribe to raw power-only data events
powerPcc.subscribeRawPowerOnlyDataEvent(new IRawPowerOnlyDataReceiver() {
@Override
public void onNewRawPowerOnlyData(long estTimestamp, EnumSet<EventFlag> eventFlags,
long powerOnlyUpdateEventCount, int instantaneousPower,
long accumulatedPower) {
if (instantaneousPower != -1) {
powerSensorPower = instantaneousPower;
QLog.d(TAG, "Power Sensor Data - Power: " + powerSensorPower + "W");
}
}
});
// Also subscribe to calculated power events
powerPcc.subscribeCalculatedPowerEvent(new ICalculatedPowerReceiver() {
@Override
public void onNewCalculatedPower(long estTimestamp, EnumSet<EventFlag> eventFlags,
AntPlusBikePowerPcc.DataSource dataSource,
BigDecimal calculatedPower) {
if (calculatedPower != null && calculatedPower.intValue() != -1) {
powerSensorPower = calculatedPower.intValue();
QLog.d(TAG, "Power Sensor Calculated Data - Power: " + powerSensorPower + "W");
}
}
});
}
}
private void subscribeToSpeedCadenceSensorEvents() {
if (speedCadencePcc != null) {
// 2.095m circumference = average 700cx23mm road tire
BigDecimal wheelCircumference = new BigDecimal("2.095");
// Subscribe to calculated speed events
speedCadencePcc.subscribeCalculatedSpeedEvent(new CalculatedSpeedReceiver(wheelCircumference) {
@Override
public void onNewCalculatedSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal calculatedSpeed) {
if (calculatedSpeed != null && calculatedSpeed.doubleValue() > 0) {
speedSensorSpeed = calculatedSpeed;
QLog.d(TAG, "Speed Sensor Data - Speed: " + speedSensorSpeed + "m/s");
}
}
});
// Subscribe to calculated distance events
speedCadencePcc.subscribeCalculatedAccumulatedDistanceEvent(new CalculatedAccumulatedDistanceReceiver(wheelCircumference) {
@Override
public void onNewCalculatedAccumulatedDistance(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal calculatedAccumulatedDistance) {
if (calculatedAccumulatedDistance != null && calculatedAccumulatedDistance.longValue() > 0) {
speedSensorDistance = calculatedAccumulatedDistance.longValue();
QLog.d(TAG, "Speed Sensor Data - Distance: " + speedSensorDistance + "m");
}
}
});
// Subscribe to raw speed and distance data
speedCadencePcc.subscribeRawSpeedAndDistanceDataEvent(new IRawSpeedAndDistanceDataReceiver() {
@Override
public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal timestampOfLastEvent, long cumulativeRevolutions) {
QLog.d(TAG, "Speed/Distance Raw Data - Revs: " + cumulativeRevolutions + ", Time: " + timestampOfLastEvent);
}
});
// Check if this is a combined speed/cadence sensor
if (speedCadencePcc.isSpeedAndCadenceCombinedSensor()) {
// Connect to cadence functionality
cadenceReleaseHandle = AntPlusBikeCadencePcc.requestAccess(
(Activity)context, speedCadencePcc.getAntDeviceNumber(), 0, true,
new IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {
@Override
public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
if (resultCode == RequestAccessResult.SUCCESS) {
cadencePcc = result;
cadencePcc.subscribeCalculatedCadenceEvent(new ICalculatedCadenceReceiver() {
@Override
public void onNewCalculatedCadence(long estTimestamp, EnumSet<EventFlag> eventFlags,
BigDecimal calculatedCadence) {
if (calculatedCadence != null && calculatedCadence.intValue() > 0) {
speedSensorCadence = calculatedCadence.intValue();
QLog.d(TAG, "Cadence Sensor Data - Cadence: " + speedSensorCadence + "rpm");
}
}
});
}
}
},
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Cadence Sensor State Changed to: " + newDeviceState);
}
}
);
}
}
}
public void close() {
if (releaseHandle != null) {
releaseHandle.close();
releaseHandle = null;
}
if (powerReleaseHandle != null) {
powerReleaseHandle.close();
powerReleaseHandle = null;
}
if (speedCadenceReleaseHandle != null) {
speedCadenceReleaseHandle.close();
speedCadenceReleaseHandle = null;
}
if (cadenceReleaseHandle != null) {
cadenceReleaseHandle.close();
cadenceReleaseHandle = null;
}
fePcc = null;
powerPcc = null;
speedCadencePcc = null;
cadencePcc = null;
isConnected = false;
isPowerConnected = false;
isSpeedCadenceConnected = false;
QLog.d(TAG, "All Channels Closed");
}
// Getter methods for bike data with sensor reconciliation
public int getCadence() {
// Priority: 1) Fitness Equipment, 2) Speed/Cadence Sensor, 3) Power Sensor
if (isConnected && cadence > 0) {
return cadence; // From fitness equipment
} else if (isSpeedCadenceConnected && speedSensorCadence > 0) {
return speedSensorCadence; // From dedicated speed/cadence sensor
} else if (isPowerConnected && speedSensorCadence > 0) {
return speedSensorCadence; // From power sensor (if it provides cadence)
}
return 0;
}
public int getPower() {
// Priority: 1) Dedicated Power Sensor, 2) Fitness Equipment
if (isPowerConnected && powerSensorPower > 0) {
return powerSensorPower; // From dedicated power sensor (most accurate)
} else if (isConnected && power > 0) {
return power; // From fitness equipment
}
return 0;
}
public double getSpeedKph() {
// Convert from m/s to km/h
return getSpeedMps() * 3.6;
}
public double getSpeedMps() {
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
if (isSpeedCadenceConnected && speedSensorSpeed.doubleValue() > 0) {
return speedSensorSpeed.doubleValue(); // From dedicated speed sensor (most accurate)
} else if (isConnected && speed.doubleValue() > 0) {
return speed.doubleValue(); // From fitness equipment
}
return 0.0;
}
public long getDistance() {
// Priority: 1) Speed/Cadence Sensor, 2) Fitness Equipment
if (isSpeedCadenceConnected && speedSensorDistance > 0) {
return speedSensorDistance; // From dedicated speed sensor (most accurate)
} else if (isConnected && distance > 0) {
return distance; // From fitness equipment
}
return 0;
}
public long getCalories() {
return calories;
}
public int getHeartRate() {
return heartRate;
}
public BigDecimal getElapsedTime() {
return elapsedTime;
}
public EquipmentState getEquipmentState() {
return equipmentState;
}
public EquipmentType getEquipmentType() {
return equipmentType;
}
public boolean isConnected() {
return isConnected;
}
// Additional connection status methods
public boolean isPowerSensorConnected() {
return isPowerConnected;
}
public boolean isSpeedCadenceSensorConnected() {
return isSpeedCadenceConnected;
}
public boolean isAnyDeviceConnected() {
return isConnected || isPowerConnected || isSpeedCadenceConnected;
}
// Raw sensor data getters (for debugging/advanced use)
public int getRawFitnessEquipmentPower() {
return power;
}
public int getRawPowerSensorPower() {
return powerSensorPower;
}
public int getRawFitnessEquipmentCadence() {
return cadence;
}
public int getRawSpeedSensorCadence() {
return speedSensorCadence;
}
public double getRawFitnessEquipmentSpeed() {
return speed.doubleValue();
}
public double getRawSpeedSensorSpeed() {
return speedSensorSpeed.doubleValue();
}
public long getRawFitnessEquipmentDistance() {
return distance;
}
public long getRawSpeedSensorDistance() {
return speedSensorDistance;
}
}

View File

@@ -1,651 +0,0 @@
/*
* Copyright 2012 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
import com.dsi.ant.channel.IAntChannelEventHandler;
import com.dsi.ant.message.ChannelId;
import com.dsi.ant.message.ChannelType;
import com.dsi.ant.message.EventCode;
import com.dsi.ant.message.fromant.AcknowledgedDataMessage;
import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
import android.os.RemoteException;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.Random;
/**
* ANT+ Bike Transmitter Controller
* Follows exactly the same pattern as PowerChannelController but for Fitness Equipment
*/
public class BikeTransmitterController {
public static final int FITNESS_EQUIPMENT_SENSOR_ID = 0x9e3d4b67; // Different from power sensor
// The device type and transmission type to be part of the channel ID message
private static final int CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE = 17; // Fitness Equipment
private static final int CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE = 5;
// The period and frequency values the channel will be configured to
private static final int CHANNEL_FITNESS_EQUIPMENT_PERIOD = 8192; // 4 Hz for FE
private static final int CHANNEL_FITNESS_EQUIPMENT_FREQUENCY = 57;
private static final String TAG = BikeTransmitterController.class.getSimpleName();
// ANT+ Data Page IDs for Fitness Equipment
private static final byte DATA_PAGE_GENERAL_FE = 0x10;
private static final byte DATA_PAGE_BIKE_DATA = 0x19;
private static final byte DATA_PAGE_TRAINER_DATA = 0x1A;
private static final byte DATA_PAGE_GENERAL_SETTINGS = 0x11;
private static Random randGen = new Random();
// Current bike metrics to transmit
int currentCadence = 0; // Current cadence in RPM
int currentPower = 0; // Current power in watts
double currentSpeedKph = 0.0; // Current speed in km/h
long totalDistance = 0; // Total distance in meters
int currentHeartRate = 0; // Heart rate in BPM
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
int currentResistance = 0; // Current resistance level (0-100)
double currentInclination = 0.0; // Current inclination in percentage
// Control commands received from ANT+ devices
private int requestedResistance = -1; // Requested resistance from controller
private int requestedPower = -1; // Requested power from controller
private double requestedInclination = -100; // Requested inclination from controller
private AntChannel mAntChannel;
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
private boolean mIsOpen;
// Callbacks for control commands
public interface ControlCommandListener {
void onResistanceChangeRequested(int resistance);
void onPowerChangeRequested(int power);
void onInclinationChangeRequested(double inclination);
}
private ControlCommandListener controlListener = null;
public BikeTransmitterController(AntChannel antChannel) {
mAntChannel = antChannel;
openChannel();
}
/**
* Set the listener for control commands received from ANT+ devices
*/
public void setControlCommandListener(ControlCommandListener listener) {
this.controlListener = listener;
}
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type
ChannelId channelId = new ChannelId(FITNESS_EQUIPMENT_SENSOR_ID & 0xFFFF,
CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE, CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE);
try {
// Setting the channel event handler so that we can receive messages from ANT
mAntChannel.setChannelEventHandler(mChannelEventCallback);
// Performs channel assignment by assigning the type to the channel
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
// Configures the channel ID, messaging period and rf frequency after assigning,
// then opening the channel.
mAntChannel.setChannelId(channelId);
mAntChannel.setPeriod(CHANNEL_FITNESS_EQUIPMENT_PERIOD);
mAntChannel.setRfFrequency(CHANNEL_FITNESS_EQUIPMENT_FREQUENCY);
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened fitness equipment channel with device number: " + FITNESS_EQUIPMENT_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
// This will release, and therefore unassign if required
channelError("Open failed", e);
}
}
} else {
QLog.w(TAG, "No channel available");
}
return mIsOpen;
}
public boolean startTransmission() {
return openChannel();
}
public void stopTransmission() {
close();
}
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
StringBuilder logString;
if (e.getResponseMessage() != null) {
String initiatingMessageId = "0x" + Integer.toHexString(
e.getResponseMessage().getInitiatingMessageId());
String rawResponseCode = "0x" + Integer.toHexString(
e.getResponseMessage().getRawResponseCode());
logString = new StringBuilder(error)
.append(". Command ")
.append(initiatingMessageId)
.append(" failed with code ")
.append(rawResponseCode);
} else {
String attemptedMessageId = "0x" + Integer.toHexString(
e.getAttemptedMessageType().getMessageId());
String failureReason = e.getFailureReason().toString();
logString = new StringBuilder(error)
.append(". Command ")
.append(attemptedMessageId)
.append(" failed with reason ")
.append(failureReason);
}
QLog.e(TAG, logString.toString());
mAntChannel.release();
}
public void close() {
if (null != mAntChannel) {
mIsOpen = false;
// Releasing the channel to make it available for others.
// After releasing, the AntChannel instance cannot be reused.
mAntChannel.release();
mAntChannel = null;
}
QLog.e(TAG, "Fitness Equipment Channel Closed");
}
// Setter methods for updating bike metrics from the main application
public void setCadence(int cadence) {
this.currentCadence = Math.max(0, cadence);
}
public void setPower(int power) {
this.currentPower = Math.max(0, power);
}
public void setSpeedKph(double speedKph) {
this.currentSpeedKph = Math.max(0, speedKph);
}
public void setDistance(long distance) {
this.totalDistance = Math.max(0, distance);
}
public void setHeartRate(int heartRate) {
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
}
public void setElapsedTime(double timeSeconds) {
this.elapsedTimeSeconds = Math.max(0, timeSeconds);
}
public void setResistance(int resistance) {
this.currentResistance = Math.max(0, Math.min(100, resistance));
}
public void setInclination(double inclination) {
this.currentInclination = Math.max(-100, Math.min(100, inclination));
}
// Getter methods for the last requested control values
public int getRequestedResistance() {
return requestedResistance;
}
public int getRequestedPower() {
return requestedPower;
}
public double getRequestedInclination() {
return requestedInclination;
}
public void clearControlRequests() {
requestedResistance = -1;
requestedPower = -1;
requestedInclination = -100;
}
public boolean isTransmitting() {
return mIsOpen;
}
public String getTransmissionInfo() {
if (!mIsOpen) {
return "Transmission: STOPPED";
}
return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
currentCadence, currentPower, currentSpeedKph,
currentResistance, currentInclination);
}
/**
* Helper method to convert byte array to hex string for debugging
*/
private String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02X ", b & 0xFF));
}
return hex.toString().trim();
}
/**
* Implements the Channel Event Handler Interface following PowerChannelController pattern
*/
public class ChannelEventCallback implements IAntChannelEventHandler {
int cnt = 0;
int eventCount = 0;
int eventPowerCount = 0;
int cumulativeDistance = 0;
int cumulativeWatt = 0;
int accumulatedTorque32 = 0;
Timer carousalTimer = null;
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Fitness Equipment Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
byte[] payload = new byte[8];
// Start unsolicited transmission timer like PowerChannelController
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsolicited Fitness Equipment Data");
byte[] payload = new byte[8];
String debugString = "";
eventCount = (eventCount + 1) & 0xFF;
cumulativeDistance = (cumulativeDistance + (int)(currentSpeedKph / 3.6)) & 0xFFFF; // rough distance calc
cnt += 1;
// Cycle through different data pages like PowerChannelController
if (cnt % 5 == 0) {
// General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 5 == 1) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 2) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 3) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
}
}, 0, 250); // Every 250ms for 4Hz
}
// Switching on message type to handle different types of messages
switch (messageType) {
case BROADCAST_DATA:
// Rx Data
break;
case ACKNOWLEDGED_DATA:
// Handle control commands
payload = new AcknowledgedDataMessage(antParcel).getPayload();
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
handleControlCommand(payload);
break;
case CHANNEL_EVENT:
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
case TX:
cnt += 1;
String debugString = "";
// Cycle through different data pages like PowerChannelController
if (cnt % 16 == 1) {
// General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 16 == 5) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
} else if (cnt % 16 == 9) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 16 == 13) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
break;
case CHANNEL_COLLISION:
cnt += 1;
break;
case RX_SEARCH_TIMEOUT:
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:
case RX_FAIL_GO_TO_SEARCH:
case TRANSFER_RX_FAILED:
case TRANSFER_TX_COMPLETED:
case TRANSFER_TX_FAILED:
case TRANSFER_TX_START:
case UNKNOWN:
// TODO More complex communication will need to handle these events
break;
}
break;
case ANT_VERSION:
case BURST_TRANSFER_DATA:
case CAPABILITIES:
case CHANNEL_ID:
case CHANNEL_RESPONSE:
case CHANNEL_STATUS:
case SERIAL_NUMBER:
case OTHER:
// TODO More complex communication will need to handle these message types
break;
}
}
/**
* Build General Fitness Equipment Data Page (0x10) - Page 16
* Following Table 8-7 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildGeneralFEDataPage(byte[] payload) {
payload[0] = 0x10; // Data Page Number = 0x10 (Page 16)
// Byte 1: Equipment Type Bit Field (Refer to Table 8-8)
payload[1] = 0x19; // Equipment type: Bike (stationary bike = 0x19)
// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
payload[2] = (byte) elapsedTime025s;
// Byte 3: Distance Traveled (1 meter resolution, rollover at 256m)
int distanceMeters = (int) (totalDistance) & 0xFF;
payload[3] = (byte) distanceMeters;
// Bytes 4-5: Speed (0.001 m/s resolution, 0xFFFF = invalid)
int speedMms = (int) (currentSpeedKph / 3.6 * 1000);
if (speedMms > 65534) speedMms = 65534; // Max valid value
payload[4] = (byte) (speedMms & 0xFF); // Speed LSB
payload[5] = (byte) ((speedMms >> 8) & 0xFF); // Speed MSB
// Byte 6: Heart Rate (0xFF = invalid)
payload[6] = (byte) (currentHeartRate == 0 ? 0xFF : currentHeartRate);
// Byte 7: Capabilities Bit Field (4 bits 0:3) + FE State Bit Field (4 bits 4:7)
payload[7] = 0x00; // Set to 0x00 for now (refer to Tables 8-9 and 8-10)
// Create debug string
return String.format(Locale.US,
"General FE Data Page (0x10): " +
"Page=0x%02X, Equipment=0x%02X(Bike), " +
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
"Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF,
payload[2] & 0xFF, elapsedTimeSeconds,
payload[3] & 0xFF, distanceMeters,
payload[5] & 0xFF, payload[4] & 0xFF, currentSpeedKph,
payload[6] & 0xFF, currentHeartRate == 0 ? "Invalid" : currentHeartRate + "bpm",
payload[7] & 0xFF);
}
/**
* Build Specific Trainer/Stationary Bike Data Page (0x19) - Page 25
* Following Table 8-25 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildBikeDataPage(byte[] payload) {
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)
// Byte 1: Update Event Count (increments with each information update)
eventPowerCount = (eventPowerCount + 1) & 0xFF;
payload[1] = (byte) eventPowerCount;
// Byte 2: Instantaneous Cadence (RPM, 0xFF = invalid)
payload[2] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);
// Bytes 3-4: Accumulated Power (1 watt resolution, rollover at 65536W)
// This is cumulative power, not instantaneous
cumulativeWatt = (cumulativeWatt + currentPower);
payload[3] = (byte) (cumulativeWatt & 0xFF); // Accumulated Power LSB
payload[4] = (byte) ((cumulativeWatt >> 8) & 0xFF); // Accumulated Power MSB
// Bytes 5-6: Instantaneous Power (1.5 bytes, 0xFFF = invalid for both fields)
if (currentPower > 4094) {
// 0xFFF indicates BOTH instantaneous and accumulated power fields are invalid
payload[5] = (byte) 0xFF; // Instantaneous Power LSB
payload[6] = (byte) 0xFF; // Instantaneous Power MSB (bits 0-3) + Trainer Status (bits 4-7)
} else {
payload[5] = (byte) (currentPower & 0xFF); // Instantaneous Power LSB
payload[6] = (byte) ((currentPower >> 8) & 0x0F); // Instantaneous Power MSN (bits 0-3)
// Bits 4-7 of byte 6: Trainer Status Bit Field (refer to Table 8-27)
payload[6] |= 0x00; // Trainer status = 0 for now
}
// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
// Create debug string
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
String powerStr = currentPower > 4094 ? "Invalid" : currentPower + "W";
return String.format(Locale.US,
"Bike Data Page (0x19): " +
"Page=0x%02X, EventCount=0x%02X(%d), " +
"Cadence=0x%02X(%s), AccumPower=0x%02X%02X(%dW), " +
"InstPower=0x%X%02X(%s), Flags=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, eventCount,
payload[2] & 0xFF, cadenceStr,
payload[4] & 0xFF, payload[3] & 0xFF, cumulativeWatt,
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
payload[7] & 0xFF);
}
/**
* Build General Settings Page (0x11) - Page 17
* Following Table 8-11 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildGeneralSettingsPage(byte[] payload) {
payload[0] = 0x11; // Data Page Number = 0x11 (Page 17)
// Byte 1: Reserved (0xFF - Do not interpret)
payload[1] = (byte) 0xFF;
// Byte 2: Reserved (0xFF - Do not interpret)
payload[2] = (byte) 0xFF;
// Byte 3: Cycle length (0.01 meters resolution, 0xFF = invalid)
// Length of one 'cycle' - for bike this could be wheel circumference
int cycleLengthCm = 210; // 2.1m wheel circumference = 210cm
payload[3] = (byte) (cycleLengthCm & 0xFF);
// Bytes 4-5: Incline Percentage (signed integer, 0.01% resolution, 0x7FFF = invalid)
int inclinePercent001 = (int) (currentInclination * 100); // Convert to 0.01% units
if (inclinePercent001 < -10000) inclinePercent001 = -10000; // Min -100.00%
if (inclinePercent001 > 10000) inclinePercent001 = 10000; // Max +100.00%
payload[4] = (byte) (inclinePercent001 & 0xFF); // Incline LSB
payload[5] = (byte) ((inclinePercent001 >> 8) & 0xFF); // Incline MSB
// Byte 6: Resistance Level (0.5% resolution, percentage of maximum applicable resistance)
int resistanceLevel05 = (int) (currentResistance * 2); // Convert to 0.5% units
if (resistanceLevel05 > 200) resistanceLevel05 = 200; // Max 100% = 200 in 0.5% units
payload[6] = (byte) (resistanceLevel05 & 0xFF);
// Byte 7: Capabilities Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
// Create debug string
return String.format(Locale.US,
"General Settings Page (0x11): " +
"Page=0x%02X, Reserved1=0x%02X, Reserved2=0x%02X, " +
"CycleLength=0x%02X(%.2fm), Incline=0x%02X%02X(%.2f%%), " +
"Resistance=0x%02X(%d%%), Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, payload[2] & 0xFF,
payload[3] & 0xFF, cycleLengthCm / 100.0,
payload[5] & 0xFF, payload[4] & 0xFF, currentInclination,
payload[6] & 0xFF, currentResistance,
payload[7] & 0xFF);
}
/**
* Handle incoming control commands
*/
private void handleControlCommand(byte[] data) {
if (data.length < 8) return;
byte pageNumber = data[0];
QLog.d(TAG, "Received control command page: 0x" + String.format("%02X", pageNumber));
QLog.d(TAG, "Control Command HEX: " + bytesToHex(data));
// Handle control command pages
switch (pageNumber) {
case 0x30: // Basic Resistance
handleBasicResistanceCommand(data);
break;
case 0x31: // Target Power
handleTargetPowerCommand(data);
break;
case 0x33: // Track Resistance
handleTrackResistanceCommand(data);
break;
default:
QLog.d(TAG, "Unknown control page: 0x" + String.format("%02X", pageNumber));
break;
}
}
private void handleBasicResistanceCommand(byte[] data) {
int resistance = data[7] & 0xFF; // Resistance in 0.5% increments
double resistancePercent = resistance * 0.5;
QLog.d(TAG, String.format(Locale.US,
"Basic Resistance Command (0x30): Resistance=0x%02X(%.1f%%)",
resistance, resistancePercent));
if (resistancePercent != requestedResistance && controlListener != null) {
requestedResistance = (int) resistancePercent;
controlListener.onResistanceChangeRequested(requestedResistance);
}
}
private void handleTargetPowerCommand(byte[] data) {
int targetPower = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
targetPower = targetPower / 4;
QLog.d(TAG, String.format(Locale.US,
"Target Power Command (0x31): Power=0x%02X%02X(%dW)",
data[7] & 0xFF, data[6] & 0xFF, targetPower));
if (targetPower != requestedPower && controlListener != null) {
requestedPower = targetPower;
controlListener.onPowerChangeRequested(targetPower);
}
}
private void handleTrackResistanceCommand(byte[] data) {
// Grade is in 0.01% increments, signed 16-bit
int gradeRaw = ((data[6] & 0xFF) << 8) | (data[5] & 0xFF);
if (gradeRaw > 32767) gradeRaw -= 65536; // Convert to signed
double grade = (gradeRaw - 0x4E20) * 0.01;
QLog.d(TAG, String.format(Locale.US,
"Track Resistance Command (0x33): Grade=0x%02X%02X(%.2f%%)",
data[6] & 0xFF, data[5] & 0xFF, grade));
if (Math.abs(grade - requestedInclination) > 0.1 && controlListener != null) {
requestedInclination = grade;
controlListener.onInclinationChangeRequested(grade);
}
}
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.ContextWrapper;
import android.content.IntentFilter;
@@ -89,12 +89,12 @@ public class BleAdvertiser {
private static AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
QLog.d("BleAdvertiser", "Advertising started successfully");
Log.d("BleAdvertiser", "Advertising started successfully");
}
@Override
public void onStartFailure(int errorCode) {
QLog.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
Log.e("BleAdvertiser", "Advertising failed with error code: " + errorCode);
}
};
}

View File

@@ -25,7 +25,7 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class CSafeRowerUSBHID {
@@ -34,21 +34,21 @@ public class CSafeRowerUSBHID {
static int lastReadLen = 0;
public static void open(Context context) {
QLog.d("QZ","CSafeRowerUSBHID open");
Log.d("QZ","CSafeRowerUSBHID open");
hidBridge = new HidBridge(context, 0x0002, 0x17A4);
boolean ret = hidBridge.OpenDevice();
QLog.d("QZ","hidBridge.OpenDevice " + ret);
Log.d("QZ","hidBridge.OpenDevice " + ret);
if(ret == false) {
hidBridge = new HidBridge(context, 0x0001, 0x17A4);
ret = hidBridge.OpenDevice();
QLog.d("QZ","hidBridge.OpenDevice " + ret);
Log.d("QZ","hidBridge.OpenDevice " + ret);
}
hidBridge.StartReadingThread();
QLog.d("QZ","hidBridge.StartReadingThread");
Log.d("QZ","hidBridge.StartReadingThread");
}
public static void write (byte[] bytes) {
QLog.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
Log.d("QZ","CSafeRowerUSBHID writing " + new String(bytes, StandardCharsets.ISO_8859_1));
hidBridge.WriteData(bytes);
}
@@ -60,10 +60,10 @@ public class CSafeRowerUSBHID {
if(hidBridge.IsThereAnyReceivedData()) {
receiveData = hidBridge.GetReceivedDataFromQueue();
lastReadLen = receiveData.length;
QLog.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
Log.d("QZ","CSafeRowerUSBHID reading " + lastReadLen + new String(receiveData, StandardCharsets.ISO_8859_1));
return receiveData;
} else {
QLog.d("QZ","CSafeRowerUSBHID empty data");
Log.d("QZ","CSafeRowerUSBHID empty data");
lastReadLen = 0;
return null;
}

View File

@@ -34,7 +34,7 @@ import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.util.SparseArray;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -49,21 +49,15 @@ public class ChannelService extends Service {
private AntChannelProvider mAntChannelProvider = null;
private boolean mAllowAddChannel = false;
public static native void nativeSetResistance(int resistance);
public static native void nativeSetPower(int power);
public static native void nativeSetInclination(double inclination);
HeartChannelController heartChannelController = null;
PowerChannelController powerChannelController = null;
SpeedChannelController speedChannelController = null;
SDMChannelController sdmChannelController = null;
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
QLog.v(TAG, "onServiceConnected");
Log.v(TAG, "onServiceConnected");
// Must pass in the received IBinder object to correctly construct an AntService object
mAntRadioService = new AntService(service);
@@ -78,7 +72,7 @@ public class ChannelService extends Service {
// radio by attempting to acquire a channel.
boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse();
QLog.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
Log.v(TAG, "onServiceConnected mChannelAvailable=" + mChannelAvailable + " legacyInterfaceInUse=" + legacyInterfaceInUse);
// If there are channels OR legacy interface in use, allow adding channels
if (mChannelAvailable || legacyInterfaceInUse) {
@@ -91,7 +85,7 @@ public class ChannelService extends Service {
try {
openAllChannels();
} catch (ChannelNotAvailableException exception) {
QLog.e(TAG, "Channel not available!!");
Log.e(TAG, "Channel not available!!");
}
} catch (RemoteException e) {
// TODO Auto-generated catch block
@@ -123,20 +117,12 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.speed = speed;
}
// Update bike transmitter with speed data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setSpeedKph(speed);
}
}
void setPower(int power) {
if (null != powerChannelController) {
powerChannelController.power = power;
}
// Update bike transmitter with power data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setPower(power);
}
}
void setCadence(int cadence) {
@@ -149,164 +135,16 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.cadence = cadence;
}
// Update bike transmitter with cadence data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setCadence(cadence);
}
}
int getHeart() {
if (null != heartChannelController) {
QLog.v(TAG, "getHeart");
Log.v(TAG, "getHeart");
return heartChannelController.heart;
}
if (null != bikeChannelController) {
return bikeChannelController.getHeartRate();
}
return 0;
}
// Added getters for bike channel data
int getBikeCadence() {
if (null != bikeChannelController) {
return bikeChannelController.getCadence();
}
return 0;
}
int getBikePower() {
if (null != bikeChannelController) {
return bikeChannelController.getPower();
}
return 0;
}
double getBikeSpeed() {
if (null != bikeChannelController) {
return bikeChannelController.getSpeedKph();
}
return 0.0;
}
long getBikeDistance() {
if (null != bikeChannelController) {
return bikeChannelController.getDistance();
}
return 0;
}
boolean isBikeConnected() {
return (bikeChannelController != null && bikeChannelController.isConnected());
}
// ========== BIKE TRANSMITTER METHODS ==========
/**
* Start the bike transmitter (only available if not treadmill)
*/
boolean startBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.startBikeTransmitter");
if (Ant.treadmill) {
QLog.w(TAG, "Bike transmitter not available in treadmill mode");
return false;
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.startTransmission();
}
QLog.w(TAG, "Bike transmitter controller is null");
return false;
}
/**
* Stop the bike transmitter
*/
void stopBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.stopBikeTransmitter");
if (bikeTransmitterController != null) {
bikeTransmitterController.stopTransmission();
}
}
/**
* Check if bike transmitter is active (only if not treadmill)
*/
boolean isBikeTransmitterActive() {
if (Ant.treadmill) {
return false;
}
return (bikeTransmitterController != null && bikeTransmitterController.isTransmitting());
}
/**
* Update bike transmitter with extended metrics (only if not treadmill)
*/
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.setDistance(distanceMeters);
bikeTransmitterController.setHeartRate(heartRate);
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
bikeTransmitterController.setResistance(resistance);
bikeTransmitterController.setInclination(inclination);
}
}
/**
* Get the last requested resistance from ANT+ controller (only if not treadmill)
*/
int getRequestedResistanceFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedResistance();
}
return -1;
}
/**
* Get the last requested power from ANT+ controller (only if not treadmill)
*/
int getRequestedPowerFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedPower();
}
return -1;
}
/**
* Get the last requested inclination from ANT+ controller (only if not treadmill)
*/
double getRequestedInclinationFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedInclination();
}
return -100.0;
}
/**
* Clear any pending control requests (only if not treadmill)
*/
void clearAntControlRequests() {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.clearControlRequests();
}
}
/**
* Get transmission info for debugging (only if not treadmill)
*/
String getBikeTransmitterInfo() {
if (Ant.treadmill) {
return "Bike transmitter disabled in treadmill mode";
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.getTransmissionInfo();
}
return "Bike transmitter not initialized";
}
/**
* Closes all channels currently added.
*/
@@ -317,7 +155,7 @@ public class ChannelService extends Service {
public void openAllChannels() throws ChannelNotAvailableException {
if (Ant.heartRequest && heartChannelController == null)
heartChannelController = new HeartChannelController(Ant.antHeartDeviceNumber);
heartChannelController = new HeartChannelController();
if (Ant.speedRequest) {
if(Ant.treadmill && sdmChannelController == null) {
@@ -327,72 +165,6 @@ public class ChannelService extends Service {
speedChannelController = new SpeedChannelController(acquireChannel());
}
}
// Add initialization for BikeChannelController (receiver)
if (Ant.bikeRequest && bikeChannelController == null) {
bikeChannelController = new BikeChannelController(Ant.technoGymGroupCycle, Ant.antBikeDeviceNumber);
}
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
if (!Ant.treadmill && bikeTransmitterController == null) {
QLog.v(TAG, "Initializing BikeTransmitterController (not treadmill mode)");
try {
// Acquire channel like other controllers
AntChannel transmitterChannel = acquireChannel();
if (transmitterChannel != null) {
bikeTransmitterController = new BikeTransmitterController(transmitterChannel);
// Set up control command listener to handle requests from ANT+ devices
bikeTransmitterController.setControlCommandListener(new BikeTransmitterController.ControlCommandListener() {
@Override
public void onResistanceChangeRequested(int resistance) {
QLog.d(TAG, "ChannelService: ANT+ Resistance change requested: " + resistance);
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_RESISTANCE_CHANGE");
intent.putExtra("resistance", resistance);
nativeSetResistance(resistance);
sendBroadcast(intent);
}
@Override
public void onPowerChangeRequested(int power) {
QLog.d(TAG, "ChannelService: ANT+ Power change requested: " + power + "W");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_POWER_CHANGE");
intent.putExtra("power", power);
nativeSetPower(power);
sendBroadcast(intent);
}
@Override
public void onInclinationChangeRequested(double inclination) {
QLog.d(TAG, "ChannelService: ANT+ Inclination change requested: " + inclination + "%");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_INCLINATION_CHANGE");
intent.putExtra("inclination", inclination);
nativeSetInclination(inclination);
sendBroadcast(intent);
}
});
QLog.i(TAG, "BikeTransmitterController initialized successfully (bike mode)");
// Start the bike transmitter immediately after initialization
boolean transmissionStarted = bikeTransmitterController.startTransmission();
if (transmissionStarted) {
QLog.i(TAG, "BikeTransmitterController transmission started automatically");
} else {
QLog.w(TAG, "Failed to start BikeTransmitterController transmission");
}
} else {
QLog.e(TAG, "Failed to acquire channel for BikeTransmitterController");
}
} catch (Exception e) {
QLog.e(TAG, "Failed to initialize BikeTransmitterController: " + e.getMessage());
bikeTransmitterController = null;
}
}
}
private void closeAllChannels() {
@@ -404,18 +176,10 @@ public class ChannelService extends Service {
speedChannelController.close();
if (sdmChannelController != null)
sdmChannelController.close();
if (bikeChannelController != null) // Added closing bikeChannelController
bikeChannelController.close();
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
bikeTransmitterController.close(); // Use close() method like other controllers
}
heartChannelController = null;
powerChannelController = null;
speedChannelController = null;
sdmChannelController = null;
bikeChannelController = null; // Added nullifying bikeChannelController
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
}
AntChannel acquireChannel() throws ChannelNotAvailableException {
@@ -436,13 +200,13 @@ public class ChannelService extends Service {
else {
NetworkKey mNK = new NetworkKey(new byte[]{(byte) 0xb9, (byte) 0xa5, (byte) 0x21, (byte) 0xfb,
(byte) 0xbd, (byte) 0x72, (byte) 0xc3, (byte) 0x45});
QLog.v(TAG, mNK.toString());
Log.v(TAG, mNK.toString());
mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK);
}
} catch (RemoteException e) {
QLog.v(TAG, "ACP Remote Ex");
Log.v(TAG, "ACP Remote Ex");
} catch (UnsupportedFeatureException e) {
QLog.v(TAG, "ACP UnsupportedFeature Ex");
Log.v(TAG, "ACP UnsupportedFeature Ex");
}
}
return mAntChannel;
@@ -459,14 +223,14 @@ public class ChannelService extends Service {
private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
QLog.d(TAG, "onReceive");
Log.d(TAG, "onReceive");
if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) {
boolean update = false;
// Retrieving the data contained in the intent
int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0);
boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false);
QLog.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
Log.d(TAG, "onReceive" + mAllowAddChannel + " " + numChannels + " " + legacyInterfaceInUse);
if (mAllowAddChannel) {
// Was a acquire channel allowed
@@ -485,7 +249,7 @@ public class ChannelService extends Service {
try {
openAllChannels();
} catch (ChannelNotAvailableException exception) {
QLog.e(TAG, "Channel not available!!");
Log.e(TAG, "Channel not available!!");
}
}
}
@@ -494,7 +258,7 @@ public class ChannelService extends Service {
};
private void doBindAntRadioService() {
if (BuildConfig.DEBUG) QLog.v(TAG, "doBindAntRadioService");
if (BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService");
ContextCompat.registerReceiver(
this,
@@ -509,14 +273,14 @@ public class ChannelService extends Service {
}
private void doUnbindAntRadioService() {
if (BuildConfig.DEBUG) QLog.v(TAG, "doUnbindAntRadioService");
if (BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService");
// Stop listing for channel available intents
try {
unregisterReceiver(mChannelProviderStateChangedReceiver);
} catch (IllegalArgumentException exception) {
if (BuildConfig.DEBUG)
QLog.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver.");
}
if (mAntRadioServiceBound) {
@@ -551,7 +315,7 @@ public class ChannelService extends Service {
}
static void die(String error) {
QLog.e(TAG, "DIE: " + error);
Log.e(TAG, "DIE: " + error);
}
}
}

View File

@@ -4,20 +4,20 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class ContentHelper {
public static String getFileName(Context context, Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
QLog.d("ContentHelper", "content");
Log.d("ContentHelper", "content");
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
QLog.d("ContentHelper", "cursor " + cursor);
Log.d("ContentHelper", "cursor " + cursor);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
QLog.d("ContentHelper", "result " + result);
Log.d("ContentHelper", "result " + result);
}
} finally {
cursor.close();

View File

@@ -1,91 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.DisplayCutout;
import org.qtproject.qt5.android.bindings.QtActivity;
public class CustomQtActivity extends QtActivity {
private static final String TAG = "CustomQtActivity";
// Declare the native method that will be implemented in C++
private static native void onInsetsChanged(int top, int bottom, int left, int right);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: CustomQtActivity initialized");
// This tells the OS that we want to handle the display cutout area ourselves
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
// This is the core of the new solution. We set a listener on the main view.
// The OS will call this listener whenever the insets change (e.g., on rotation).
final View decorView = getWindow().getDecorView();
decorView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
final float density = getResources().getDisplayMetrics().density;
int top = 0;
int bottom = 0;
int left = 0;
int right = 0;
if (density > 0) {
// Use system window insets as primary source
top = Math.round(insets.getSystemWindowInsetTop() / density);
bottom = Math.round(insets.getSystemWindowInsetBottom() / density);
left = Math.round(insets.getSystemWindowInsetLeft() / density);
right = Math.round(insets.getSystemWindowInsetRight() / density);
// For API 28+, also check display cutout for additional padding
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
// Use the maximum between system window inset and cutout safe inset
left = Math.max(left, Math.round(cutout.getSafeInsetLeft() / density));
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
}
}
}
Log.d(TAG, "onApplyWindowInsets - Top:" + top + " Bottom:" + bottom + " Left:" + left + " Right:" + right);
Log.d(TAG, "Raw insets - SystemTop:" + insets.getSystemWindowInsetTop() +
" SystemBottom:" + insets.getSystemWindowInsetBottom() +
" SystemLeft:" + insets.getSystemWindowInsetLeft() +
" SystemRight:" + insets.getSystemWindowInsetRight());
Log.d(TAG, "Stable insets - StableTop:" + insets.getStableInsetTop() +
" StableBottom:" + insets.getStableInsetBottom() +
" StableLeft:" + insets.getStableInsetLeft() +
" StableRight:" + insets.getStableInsetRight());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
Log.d(TAG, "Cutout insets - Top:" + cutout.getSafeInsetTop() +
" Bottom:" + cutout.getSafeInsetBottom() +
" Left:" + cutout.getSafeInsetLeft() +
" Right:" + cutout.getSafeInsetRight());
}
}
// Push the new, correct inset values to the C++ layer
onInsetsChanged(top, bottom, left, right);
return v.onApplyWindowInsets(insets);
}
});
}
// This method is still needed for the QML check
public static int getApiLevel() {
return Build.VERSION.SDK_INT;
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -29,26 +29,32 @@ public class FloatingHandler {
static public int _width;
static public int _height;
static public int _alpha;
static public String _htmlPage = "floating.htm";
public static void show(Context context, int port, int width, int height, int transparency, String htmlPage) {
_context = context;
_port = port;
_width = width;
_height = height;
_alpha = transparency;
_htmlPage = htmlPage;
public static void show(Context context, int port, int width, int height, int transparency) {
_context = context;
_port = port;
_width = width;
_height = height;
_alpha = transparency;
if (checkOverlayDisplayPermission()) {
if (_intent == null)
_intent = new Intent(context, FloatingWindowGFG.class);
context.startService(_intent);
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
Activity a = (Activity) _context;
a.startActivityForResult(intent, -1);
}
}
// First it confirms whether the
// 'Display over other apps' permission in given
if (checkOverlayDisplayPermission()) {
if(_intent == null)
_intent = new Intent(context, FloatingWindowGFG.class);
// FloatingWindowGFG service is started
context.startService(_intent);
// The MainActivity closes here
//finish();
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + _context.getPackageName()));
// This method will start the intent. It takes two parameter, one is the Intent and the other is
// an requestCode Integer. Here it is -1.
Activity a = (Activity)_context;
a.startActivityForResult(intent, -1);
}
}
public static void hide() {
if(_intent != null)

View File

@@ -24,12 +24,8 @@ import android.widget.Toast;
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.WebViewClient;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.webkit.JavascriptInterface;
import androidx.constraintlayout.widget.ConstraintLayout;
public class FloatingWindowGFG extends Service {
@@ -41,14 +37,6 @@ public class FloatingWindowGFG extends Service {
private WindowManager.LayoutParams floatWindowLayoutParam;
private WindowManager windowManager;
private Button maximizeBtn;
private Handler handler;
private Runnable paddingTimeoutRunnable;
private boolean isDraggingEnabled = false;
private int originalHeight;
private boolean isExpanded = false;
private WebView webView;
private int originalMargin = 20; // in dp, matching the XML layout
private int reducedMargin = 2; // minimal margin when not dragging
// Retrieve the user preference node for the package com.mycompany
SharedPreferences sharedPreferences;
@@ -68,9 +56,6 @@ public class FloatingWindowGFG extends Service {
public void onCreate() {
super.onCreate();
// Initialize handler for timeout operations
handler = new Handler(Looper.getMainLooper());
// The screen height and width are calculated, cause
// the height and width of the floating window is set depending on this
/*DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
@@ -88,30 +73,23 @@ public class FloatingWindowGFG extends Service {
// inflate a new view hierarchy from the floating_layout xml
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
webView = (WebView)floatView.findViewById(R.id.webview);
webView.setWebViewClient(new WebViewClient(){
WebView wv = (WebView)floatView.findViewById(R.id.webview);
wv.setWebViewClient(new WebViewClient(){
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
WebSettings settings = webView.getSettings();
WebSettings settings = wv.getSettings();
settings.setJavaScriptEnabled(true);
// Add JavaScript interface for communication with HTML
webView.addJavascriptInterface(new WebAppInterface(), "Android");
webView.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/" + FloatingHandler._htmlPage);
webView.clearView();
webView.measure(100, 100);
webView.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
wv.loadUrl("http://localhost:" + FloatingHandler._port + "/floating/floating.htm");
wv.clearView();
wv.measure(100, 100);
wv.setAlpha(Float.valueOf(FloatingHandler._alpha) / 100.0f);
settings.setBuiltInZoomControls(true);
settings.setUseWideViewPort(true);
settings.setDomStorageEnabled(true);
QLog.d("QZ","loadurl");
// Initially set reduced margin for normal operation
setWebViewMargin(reducedMargin);
Log.d("QZ","loadurl");
// WindowManager.LayoutParams takes a lot of parameters to set the
@@ -138,18 +116,17 @@ public class FloatingWindowGFG extends Service {
// 5) Next parameter is Layout_Format. System chooses a format that supports
// translucency by PixelFormat.TRANSLUCENT
originalHeight = FloatingHandler._height;
floatWindowLayoutParam = new WindowManager.LayoutParams(
(int) (FloatingHandler._width ),
(int) (originalHeight ),
(int) (FloatingHandler._height ),
LAYOUT_TYPE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
// The Gravity of the Floating Window is set.
// Use TOP | LEFT for free positioning without constraints
floatWindowLayoutParam.gravity = Gravity.TOP | Gravity.LEFT;
// The Window will appear in the center of the screen
floatWindowLayoutParam.gravity = Gravity.CENTER;
// X and Y value of the window is set
floatWindowLayoutParam.x = 0;
@@ -168,86 +145,48 @@ public class FloatingWindowGFG extends Service {
// The window can be moved at any position on the screen.
floatView.setOnTouchListener(new View.OnTouchListener() {
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
int initialX;
int initialY;
float initialTouchX;
float initialTouchY;
boolean isDragging = false;
final int TOUCH_THRESHOLD = 10; // Threshold for distinguishing tap vs drag
double x;
double y;
double px;
double py;
@Override
public boolean onTouch(View v, MotionEvent event) {
QLog.d("QZ","onTouch action: " + event.getAction());
Log.d("QZ","onTouch");
switch (event.getAction()) {
// When the window will be touched,
// the x and y position of that position
// will be retrieved
case MotionEvent.ACTION_DOWN:
// Store initial positions
initialX = floatWindowLayoutUpdateParam.x;
initialY = floatWindowLayoutUpdateParam.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
isDragging = false;
// Enable dragging for 5 seconds
enableDraggingTemporarily();
break;
x = floatWindowLayoutUpdateParam.x;
y = floatWindowLayoutUpdateParam.y;
// returns the original raw X
// coordinate of this event
px = event.getRawX();
// returns the original raw Y
// coordinate of this event
py = event.getRawY();
break;
// When the window will be dragged around,
// it will update the x, y of the Window Layout Parameter
case MotionEvent.ACTION_MOVE:
// Calculate distance moved
float deltaX = event.getRawX() - initialTouchX;
float deltaY = event.getRawY() - initialTouchY;
// Check if we've moved enough to consider this a drag
if (!isDragging && (Math.abs(deltaX) > TOUCH_THRESHOLD || Math.abs(deltaY) > TOUCH_THRESHOLD)) {
isDragging = true;
}
// Only allow dragging if it's temporarily enabled
if (isDragging && isDraggingEnabled) {
// Get screen dimensions for boundary checking
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int screenWidth = displayMetrics.widthPixels;
int screenHeight = displayMetrics.heightPixels;
// Calculate new position
int newX = initialX + (int) deltaX;
int newY = initialY + (int) deltaY;
// Apply boundary constraints
// Keep window within screen bounds
int windowWidth = FloatingHandler._width;
int windowHeight = FloatingHandler._height;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + windowWidth > screenWidth) newX = screenWidth - windowWidth;
if (newY + windowHeight > screenHeight) newY = screenHeight - windowHeight;
// Update position
floatWindowLayoutUpdateParam.x = newX;
floatWindowLayoutUpdateParam.y = newY;
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
// Save position to preferences
SharedPreferences.Editor myEdit = sharedPreferences.edit();
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
myEdit.apply(); // Use apply() instead of commit() for better performance
SharedPreferences.Editor myEdit = sharedPreferences.edit();
myEdit.putInt(PREF_NAME_X, floatWindowLayoutUpdateParam.x);
myEdit.putInt(PREF_NAME_Y, floatWindowLayoutUpdateParam.y);
myEdit.commit();
// Apply updated parameter to the WindowManager
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
}
// updated parameter is applied to the WindowManager
windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
break;
case MotionEvent.ACTION_UP:
// If it wasn't a drag, it's a tap - let the WebView handle it
if (!isDragging) {
return false; // Let the event propagate to WebView
}
isDragging = false;
break;
}
return isDragging && isDraggingEnabled; // Consume the event only if we're dragging and dragging is enabled
return false;
}
});
}
@@ -261,107 +200,4 @@ public class FloatingWindowGFG extends Service {
// Window is removed from the screen
windowManager.removeView(floatView);
}
// Method to enable dragging temporarily for 5 seconds
private void enableDraggingTemporarily() {
isDraggingEnabled = true;
// Increase margin for better dragging experience
setWebViewMargin(originalMargin);
// Cancel any existing timeout
if (paddingTimeoutRunnable != null) {
handler.removeCallbacks(paddingTimeoutRunnable);
}
// Create new timeout runnable
paddingTimeoutRunnable = new Runnable() {
@Override
public void run() {
isDraggingEnabled = false;
// Restore reduced margin for normal operation
setWebViewMargin(reducedMargin);
QLog.d("QZ", "Dragging disabled after timeout, margin restored");
}
};
// Schedule timeout for 5 seconds
handler.postDelayed(paddingTimeoutRunnable, 5000);
}
// Method to expand window height dynamically
private void expandWindow(int additionalHeight) {
if (!isExpanded) {
isExpanded = true;
floatWindowLayoutParam.height = originalHeight + additionalHeight;
// Adjust Y position to keep window within screen bounds
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int screenHeight = displayMetrics.heightPixels;
if (floatWindowLayoutParam.y + floatWindowLayoutParam.height > screenHeight) {
floatWindowLayoutParam.y = screenHeight - floatWindowLayoutParam.height;
if (floatWindowLayoutParam.y < 0) {
floatWindowLayoutParam.y = 0;
}
}
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
QLog.d("QZ", "Window expanded to height: " + floatWindowLayoutParam.height);
}
}
// Method to restore original window height
private void restoreWindow() {
if (isExpanded) {
isExpanded = false;
floatWindowLayoutParam.height = originalHeight;
windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
QLog.d("QZ", "Window restored to original height: " + originalHeight);
}
}
// Method to set WebView margin dynamically
private void setWebViewMargin(int marginDp) {
if (webView != null) {
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) webView.getLayoutParams();
int marginPx = (int) (marginDp * getResources().getDisplayMetrics().density);
params.setMargins(marginPx, marginPx, marginPx, marginPx);
webView.setLayoutParams(params);
QLog.d("QZ", "WebView margin set to: " + marginDp + "dp (" + marginPx + "px)");
}
}
// JavaScript interface class
public class WebAppInterface {
@JavascriptInterface
public void expandFloatingWindow(int additionalHeight) {
handler.post(new Runnable() {
@Override
public void run() {
expandWindow(additionalHeight);
}
});
}
@JavascriptInterface
public void restoreFloatingWindow() {
handler.post(new Runnable() {
@Override
public void run() {
restoreWindow();
}
});
}
@JavascriptInterface
public void enableDraggingMargins() {
handler.post(new Runnable() {
@Override
public void run() {
enableDraggingTemporarily();
}
});
}
}
}

View File

@@ -10,7 +10,7 @@ import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import android.content.pm.ServiceInfo;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class ForegroundService extends Service {
public static final String CHANNEL_ID = "ForegroundServiceChannel";
@@ -43,7 +43,7 @@ public class ForegroundService extends Service {
startForeground(1, notification);
}
} catch (Exception e) {
QLog.e("ForegroundService", "Failed to start foreground service", e);
Log.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
//do heavy work on a background thread

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;
@@ -53,22 +53,22 @@ public class Garmin {
private static Integer Power = 0;
public static int getHR() {
QLog.d(TAG, "getHR " + HR);
Log.d(TAG, "getHR " + HR);
return HR;
}
public static int getPower() {
QLog.d(TAG, "getPower " + Power);
Log.d(TAG, "getPower " + Power);
return Power;
}
public static double getSpeed() {
QLog.d(TAG, "getSpeed " + Speed);
Log.d(TAG, "getSpeed " + Speed);
return Speed;
}
public static int getFootCad() {
QLog.d(TAG, "getFootCad " + FootCad);
Log.d(TAG, "getFootCad " + FootCad);
return FootCad;
}
@@ -83,7 +83,7 @@ public class Garmin {
@Override
public void onInitializeError(ConnectIQ.IQSdkErrorStatus errStatus) {
QLog.e(TAG, errStatus.toString());
Log.e(TAG, errStatus.toString());
connectIqReady = false;
}
@@ -91,7 +91,7 @@ public class Garmin {
public void onSdkReady() {
connectIqInitializing = false;
connectIqReady = true;
QLog.i(TAG, " onSdkReady");
Log.i(TAG, " onSdkReady");
registerWatchMessagesReceiver();
registerDeviceStatusReceiver();
@@ -118,16 +118,16 @@ public class Garmin {
try {
List<IQDevice> devices = connectIQ.getConnectedDevices();
if (devices != null && devices.size() > 0) {
QLog.v(TAG, "getDevice connected: " + devices.get(0).toString() );
Log.v(TAG, "getDevice connected: " + devices.get(0).toString() );
deviceCache = devices.get(0);
return deviceCache;
} else {
return deviceCache;
}
} catch (InvalidStateException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
} catch (ServiceUnavailableException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
return null;
}
@@ -193,33 +193,33 @@ public class Garmin {
@Override
public void onApplicationInfoReceived(IQApp app) {
QLog.d(TAG, "App installed.");
Log.d(TAG, "App installed.");
}
@Override
public void onApplicationNotInstalled(String applicationId) {
if (getDevice() != null) {
Toast.makeText(context, "App not installed on your Garmin watch", Toast.LENGTH_LONG).show();
QLog.d(TAG, "watch app not installed.");
Log.d(TAG, "watch app not installed.");
}
}
});
} catch (InvalidStateException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
} catch (ServiceUnavailableException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
}
private static void registerDeviceStatusReceiver() {
QLog.d(TAG, "registerDeviceStatusReceiver");
Log.d(TAG, "registerDeviceStatusReceiver");
IQDevice device = getDevice();
try {
if (device != null) {
connectIQ.registerForDeviceEvents(device, new ConnectIQ.IQDeviceEventListener() {
@Override
public void onDeviceStatusChanged(IQDevice device, IQDevice.IQDeviceStatus newStatus) {
QLog.d(TAG, "Device status changed, now " + newStatus);
Log.d(TAG, "Device status changed, now " + newStatus);
}
});
}
@@ -229,7 +229,7 @@ public class Garmin {
}
private static void registerWatchMessagesReceiver(){
QLog.d(TAG, "registerWatchMessageReceiver");
Log.d(TAG, "registerWatchMessageReceiver");
IQDevice device = getDevice();
try {
if (device != null) {
@@ -238,7 +238,7 @@ public class Garmin {
public void onMessageReceived(IQDevice device, IQApp app, List<Object> message, ConnectIQ.IQMessageStatus status) {
if (status == ConnectIQ.IQMessageStatus.SUCCESS) {
//MessageHandler.getInstance().handleMessageFromWatchUsingCIQ(message, status, context);
QLog.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
Log.d(TAG, "onMessageReceived, status: " + status.toString() + message.get(0));
try {
String var[] = message.toArray()[0].toString().split(",");
HR = Integer.parseInt(var[0].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
@@ -249,21 +249,21 @@ public class Garmin {
Speed = Double.parseDouble(var[1].replaceAll("\\[", "").replaceAll("\\]", "").replaceAll("\\{", "").replaceAll("\\}", "").replaceAll(" ", "").split("=")[1]);
}
}
QLog.d(TAG, "HR " + HR);
QLog.d(TAG, "FootCad " + FootCad);
Log.d(TAG, "HR " + HR);
Log.d(TAG, "FootCad " + FootCad);
} catch (Exception e) {
QLog.e(TAG, "Processing error", e);
Log.e(TAG, "Processing error", e);
}
} else {
QLog.d(TAG, "onMessageReceived error, status: " + status.toString());
Log.d(TAG, "onMessageReceived error, status: " + status.toString());
}
}
});
} else {
QLog.d(TAG, "registerWatchMessagesReceiver: No device found.");
Log.d(TAG, "registerWatchMessagesReceiver: No device found.");
}
} catch (InvalidStateException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
}
@@ -273,19 +273,19 @@ public class Garmin {
try {
if (context != null) {
QLog.d(TAG, "Shutting down with wrapped context");
Log.d(TAG, "Shutting down with wrapped context");
connectIQ.shutdown(context);
} else {
QLog.d(TAG, "Shutting down without wrapped context");
Log.d(TAG, "Shutting down without wrapped context");
connectIQ.shutdown(applicationContext);
}
} catch (InvalidStateException e) {
// This is usually because the SDK was already shut down so no worries.
QLog.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
Log.e(TAG, "This is usually because the SDK was already shut down so no worries.", e);
} catch (IllegalArgumentException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
} catch (RuntimeException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
}
@@ -299,11 +299,11 @@ public class Garmin {
}
}
} catch (InvalidStateException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
} catch (IllegalArgumentException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
} catch (RuntimeException e) {
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
}
}

View File

@@ -1,7 +1,7 @@
package org.cagnulen.qdomyoszwift;
import android.content.Context;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.app.Activity;
// ANT+ Plugin imports
@@ -42,14 +42,14 @@ public class HeartChannelController {
private boolean isConnected = false;
public int heart = 0; // Public to be accessible from ChannelService
public HeartChannelController(int antHeartDeviceNumber) {
public HeartChannelController() {
this.context = Ant.activity;
openChannel(antHeartDeviceNumber);
openChannel();
}
public boolean openChannel(int deviceNumber) {
// Request access to heart rate device (deviceNumber = 0 means first available)
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, deviceNumber, 0,
public boolean openChannel() {
// Request access to first available heart rate device
releaseHandle = AntPlusHeartRatePcc.requestAccess((Activity)context, 0, 0, // 0 means first available device
new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
@Override
public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
@@ -57,26 +57,26 @@ public class HeartChannelController {
case SUCCESS:
hrPcc = result;
isConnected = true;
QLog.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName() + " (Device #" + deviceNumber + ")");
Log.d(TAG, "Connected to heart rate monitor: " + result.getDeviceName());
subscribeToHrEvents();
break;
case CHANNEL_NOT_AVAILABLE:
QLog.e(TAG, "Channel Not Available");
Log.e(TAG, "Channel Not Available");
break;
case ADAPTER_NOT_DETECTED:
QLog.e(TAG, "ANT Adapter Not Available");
Log.e(TAG, "ANT Adapter Not Available");
break;
case BAD_PARAMS:
QLog.e(TAG, "Bad request parameters");
Log.e(TAG, "Bad request parameters");
break;
case OTHER_FAILURE:
QLog.e(TAG, "RequestAccess failed");
Log.e(TAG, "RequestAccess failed");
break;
case DEPENDENCY_NOT_INSTALLED:
QLog.e(TAG, "Dependency not installed");
Log.e(TAG, "Dependency not installed");
break;
default:
QLog.e(TAG, "Unrecognized result: " + resultCode);
Log.e(TAG, "Unrecognized result: " + resultCode);
break;
}
}
@@ -84,7 +84,7 @@ public class HeartChannelController {
new IDeviceStateChangeReceiver() {
@Override
public void onDeviceStateChange(DeviceState newDeviceState) {
QLog.d(TAG, "Device State Changed to: " + newDeviceState);
Log.d(TAG, "Device State Changed to: " + newDeviceState);
if (newDeviceState == DeviceState.DEAD) {
isConnected = false;
}
@@ -104,7 +104,7 @@ public class HeartChannelController {
BigDecimal heartBeatEventTime, DataState dataState) {
heart = computedHeartRate;
QLog.d(TAG, "Heart Rate: " + heart);
Log.d(TAG, "Heart Rate: " + heart);
}
});
}
@@ -117,7 +117,7 @@ public class HeartChannelController {
}
hrPcc = null;
isConnected = false;
QLog.d(TAG, "Channel Closed");
Log.d(TAG, "Channel Closed");
}
public int getHeartRate() {

View File

@@ -16,7 +16,7 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -169,7 +169,7 @@ public class HidBridge {
} catch(NullPointerException e)
{
Log("Error happened while writing. Could not connect to the device or interface is busy?");
QLog.e("HidBridge", QLog.getStackTraceString(e));
Log.e("HidBridge", Log.getStackTraceString(e));
return false;
}
return true;
@@ -289,7 +289,7 @@ public class HidBridge {
catch (NullPointerException e) {
Log("Error happened while reading. No device or the connection is busy");
QLog.e("HidBridge", QLog.getStackTraceString(e));
Log.e("HidBridge", Log.getStackTraceString(e));
}
catch (ThreadDeath e) {
if (readConnection != null) {
@@ -332,7 +332,7 @@ public class HidBridge {
}
}
else {
QLog.d("TAG", "permission denied for the device " + device);
Log.d("TAG", "permission denied for the device " + device);
}
}
}
@@ -344,7 +344,7 @@ public class HidBridge {
* @param message to log.
*/
private void Log(String message) {
QLog.d("HidBridge", message);
Log.d("HidBridge", message);
}
/**

View File

@@ -8,7 +8,7 @@ import com.garmin.android.connectiq.IQDevice;
import java.nio.BufferUnderflowException;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class IQMessageReceiverWrapper extends BroadcastReceiver {
private final BroadcastReceiver receiver;
@@ -20,7 +20,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
QLog.d(TAG, "onReceive intent " + intent.getAction());
Log.d(TAG, "onReceive intent " + intent.getAction());
if ("com.garmin.android.connectiq.SEND_MESSAGE_STATUS".equals(intent.getAction())) {
replaceIQDeviceById(intent, "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE");
} else if ("com.garmin.android.connectiq.OPEN_APPLICATION".equals(intent.getAction())) {
@@ -32,7 +32,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
try {
receiver.onReceive(context, intent);
} catch (IllegalArgumentException | BufferUnderflowException e) {
QLog.d(TAG, e.toString());
Log.d(TAG, e.toString());
}
}
@@ -44,7 +44,7 @@ public class IQMessageReceiverWrapper extends BroadcastReceiver {
intent.putExtra(extraName, device.getDeviceIdentifier());
}
} catch (ClassCastException e) {
QLog.d(TAG, e.toString());
Log.d(TAG, e.toString());
// It's already a long, i.e. on the simulator.
}
}

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.location.LocationManager;
import android.provider.Settings;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class LocationHelper {
private static final String TAG = "LocationHelper";
@@ -13,7 +13,7 @@ public class LocationHelper {
private static boolean isBluetoothEnabled = false;
public static boolean start(Context context) {
QLog.d(TAG, "Starting LocationHelper check...");
Log.d(TAG, "Starting LocationHelper check...");
isLocationEnabled = isLocationEnabled(context);
isBluetoothEnabled = isBluetoothEnabled();
@@ -23,7 +23,7 @@ public class LocationHelper {
public static void requestPermissions(Context context) {
if (!isLocationEnabled || !isBluetoothEnabled) {
QLog.d(TAG, "Some services are disabled. Prompting user...");
Log.d(TAG, "Some services are disabled. Prompting user...");
if (!isLocationEnabled) {
promptEnableLocation(context);
}
@@ -31,7 +31,7 @@ public class LocationHelper {
promptEnableBluetooth(context);
}
} else {
QLog.d(TAG, "All services are already enabled.");
Log.d(TAG, "All services are already enabled.");
}
}
@@ -50,14 +50,14 @@ public class LocationHelper {
}
private static void promptEnableLocation(Context context) {
QLog.d(TAG, "Prompting to enable Location...");
Log.d(TAG, "Prompting to enable Location...");
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private static void promptEnableBluetooth(Context context) {
QLog.d(TAG, "Prompting to enable Bluetooth...");
Log.d(TAG, "Prompting to enable Bluetooth...");
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.os.Build;
public class MediaButtonReceiver extends BroadcastReceiver {
@@ -13,7 +13,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
QLog.d("MediaButtonReceiver", "Received intent: " + intent.toString());
Log.d("MediaButtonReceiver", "Received intent: " + intent.toString());
String intentAction = intent.getAction();
if ("android.media.VOLUME_CHANGED_ACTION".equals(intentAction)) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -21,7 +21,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
int currentVolume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1);
int previousVolume = intent.getIntExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", -1);
QLog.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
Log.d("MediaButtonReceiver", "Volume changed. Current: " + currentVolume + ", Max: " + maxVolume);
nativeOnMediaButtonEvent(previousVolume, currentVolume, maxVolume);
}
}
@@ -29,39 +29,18 @@ public class MediaButtonReceiver extends BroadcastReceiver {
private native void nativeOnMediaButtonEvent(int prev, int current, int max);
public static void registerReceiver(Context context) {
try {
if (instance == null) {
instance = new MediaButtonReceiver();
}
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
if (context == null) {
QLog.e("MediaButtonReceiver", "Context is null, cannot register receiver");
return;
}
if (Build.VERSION.SDK_INT >= 34) {
try {
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
} catch (SecurityException se) {
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
} else {
try {
context.registerReceiver(instance, filter);
} catch (SecurityException se) {
QLog.e("MediaButtonReceiver", "Security exception while registering receiver: " + se.getMessage());
}
}
QLog.d("MediaButtonReceiver", "Receiver registered successfully");
} catch (IllegalArgumentException e) {
QLog.e("MediaButtonReceiver", "Invalid arguments for receiver registration: " + e.getMessage());
} catch (Exception e) {
QLog.e("MediaButtonReceiver", "Unexpected error while registering receiver: " + e.getMessage());
if (instance == null) {
instance = new MediaButtonReceiver();
}
IntentFilter filter = new IntentFilter("android.media.VOLUME_CHANGED_ACTION");
if (Build.VERSION.SDK_INT >= 34) {
context.registerReceiver(instance, filter, Context.RECEIVER_EXPORTED);
} else {
context.registerReceiver(instance, filter);
}
Log.d("MediaButtonReceiver", "registerReceiver");
}
public static void unregisterReceiver(Context context) {
if (instance != null) {
context.unregisterReceiver(instance);

View File

@@ -12,7 +12,7 @@ import android.util.DisplayMetrics;
import android.os.Build;
import android.provider.Settings;
import android.app.AppOpsManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.annotation.TargetApi;
import com.rvalerio.fgchecker.AppChecker;
@@ -83,11 +83,11 @@ public class MediaProjection {
@Override
public void onForeground(String packageName) {
_packageName = packageName;
/*QLog.e("MediaProjection", packageName);
/*Log.e("MediaProjection", packageName);
if(isLandscape())
QLog.e("MediaProjection", "Landscape");
Log.e("MediaProjection", "Landscape");
else
QLog.e("MediaProjection", "Portrait");*/
Log.e("MediaProjection", "Portrait");*/
}
})
.timeout(1000)

View File

@@ -1,5 +1,5 @@
package org.cagnulen.qdomyoszwift;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
@@ -12,6 +12,6 @@ public class MyActivity extends org.qtproject.qt5.android.bindings.QtActivity {
super.onCreate(savedInstanceState);
this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
activity_ = this;
QLog.v(TAG, "onCreate");
Log.v(TAG, "onCreate");
}
}

View File

@@ -2,7 +2,7 @@ package org.cagnulen.qdomyoszwift;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import java.util.List;
@@ -12,7 +12,7 @@ public class NativeScanCallback extends ScanCallback {
public native void scanError(int code);
@Override
public void onScanResult(int callbackType, ScanResult result) {
QLog.i(TAG, "Res " + result);
Log.i(TAG, "Res " + result);
newScanResult(new ScanRecordResult(result));
}
@@ -24,7 +24,7 @@ public class NativeScanCallback extends ScanCallback {
@Override
public void onScanFailed(int errorCode) {
QLog.i(TAG, "onScanFailed "+errorCode);
Log.i(TAG, "onScanFailed "+errorCode);
scanError(errorCode);
}
}

View File

@@ -17,7 +17,7 @@
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -61,7 +61,7 @@ public class PowerChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
Log.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -92,7 +92,7 @@ public class PowerChannelController {
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
@@ -102,7 +102,7 @@ public class PowerChannelController {
}
}
} else {
QLog.w(TAG, "No channel available");
Log.w(TAG, "No channel available");
}
return mIsOpen;
@@ -112,7 +112,7 @@ public class PowerChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
Log.e(TAG, logString);
}
@@ -142,7 +142,7 @@ public class PowerChannelController {
.append(failureReason);
}
QLog.e(TAG, logString.toString());
Log.e(TAG, logString.toString());
mAntChannel.release();
}
@@ -158,7 +158,7 @@ public class PowerChannelController {
mAntChannel = null;
}
QLog.e(TAG, "Channel Closed");
Log.e(TAG, "Channel Closed");
}
/**
@@ -175,13 +175,13 @@ public class PowerChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Channel Death");
Log.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
byte[] payload = new byte[8];
if(carousalTimer == null) {
@@ -189,7 +189,7 @@ public class PowerChannelController {
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsollicited");
Log.d(TAG, "Tx Unsollicited");
byte[] payload = new byte[8];
eventCount = (eventCount + 1) & 0xFF;
cumulativePower = (cumulativePower + power) & 0xFFFF;
@@ -225,7 +225,7 @@ public class PowerChannelController {
// Rx Data
//updateData(new AcknowledgedDataMessage(antParcel).getPayload());
payload = new AcknowledgedDataMessage(antParcel).getPayload();
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
Log.d(TAG, "AcknowledgedDataMessage: " + payload);
if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) {
payload[0] = (byte) 0x01;
@@ -268,7 +268,7 @@ public class PowerChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
Log.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -320,7 +320,7 @@ public class PowerChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
QLog.e(TAG, "No Device Found");
Log.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

View File

@@ -1,181 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.util.Log;
/**
* QLog - Wrapper for Android's Log class that redirects logs to Qt's logging system
* Usage: import org.cagnulen.qdomyoszwift.Log;
*/
public class QLog {
public static native void sendToQt(int level, String tag, String message);
static {
try {
// Try to load the native library if needed
System.loadLibrary("qtlogging_native");
} catch (UnsatisfiedLinkError e) {
// Library might be loaded elsewhere, or will be loaded later
Log.w("QLog", "Native library not loaded yet: " + e.getMessage());
}
}
// Debug level methods
public static int d(String tag, String msg) {
try {
sendToQt(3, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.d(tag, msg);
}
public static int d(String tag, String msg, Throwable tr) {
try {
sendToQt(3, tag, msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.d(tag, msg, tr);
}
// Error level methods
public static int e(String tag, String msg) {
try {
sendToQt(6, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.e(tag, msg);
}
public static int e(String tag, String msg, Throwable tr) {
try {
sendToQt(6, tag, msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.e(tag, msg, tr);
}
// Info level methods
public static int i(String tag, String msg) {
try {
sendToQt(4, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.i(tag, msg);
}
public static int i(String tag, String msg, Throwable tr) {
try {
sendToQt(4, tag, msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.i(tag, msg, tr);
}
// Verbose level methods
public static int v(String tag, String msg) {
try {
sendToQt(2, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.v(tag, msg);
}
public static int v(String tag, String msg, Throwable tr) {
try {
sendToQt(2, tag, msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.v(tag, msg, tr);
}
// Warning level methods
public static int w(String tag, String msg) {
try {
sendToQt(5, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.w(tag, msg);
}
public static int w(String tag, String msg, Throwable tr) {
try {
sendToQt(5, tag, msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.w(tag, msg, tr);
}
public static int w(String tag, Throwable tr) {
try {
sendToQt(5, tag, Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.w(tag, tr);
}
// What a Terrible Failure: Report an exception that should never happen
public static int wtf(String tag, String msg) {
try {
sendToQt(7, tag, "WTF: " + msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.wtf(tag, msg);
}
public static int wtf(String tag, Throwable tr) {
try {
sendToQt(7, tag, "WTF: " + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.wtf(tag, tr);
}
public static int wtf(String tag, String msg, Throwable tr) {
try {
sendToQt(7, tag, "WTF: " + msg + '\n' + Log.getStackTraceString(tr));
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.wtf(tag, msg, tr);
}
// Utility methods
public static String getStackTraceString(Throwable tr) {
return Log.getStackTraceString(tr);
}
public static boolean isLoggable(String tag, int level) {
return Log.isLoggable(tag, level);
}
// Additional utility methods
public static int println(int priority, String tag, String msg) {
try {
sendToQt(priority, tag, msg);
} catch (UnsatisfiedLinkError e) {
// Native library not available, continue with Android logging only
}
return Log.println(priority, tag, msg);
}
// API Level 28+ (Android 9+) methods
public static RuntimeException getStackTraceElement() {
try {
return (RuntimeException) Log.class.getMethod("getStackTraceElement").invoke(null);
} catch (Exception e) {
return new RuntimeException("QLog: Failed to get stack trace element");
}
}
}

View File

@@ -17,7 +17,7 @@ import android.os.Environment;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.Settings;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.RadioButton;
@@ -43,8 +43,6 @@ public class QZAdbRemote implements DeviceConnectionListener {
private static final String LOG_TAG = "QZ:AdbRemote";
private static String lastCommand = "";
private static boolean ADBConnected = false;
private static boolean cryptoReady = false;
private static final Object cryptoLock = new Object();
private static String _address = "127.0.0.1";
private static Context _context;
@@ -64,46 +62,31 @@ public class QZAdbRemote implements DeviceConnectionListener {
@Override
public void notifyConnectionEstablished(DeviceConnection devConn) {
QLog.d(LOG_TAG, "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = true;
QLog.i(LOG_TAG, "notifyConnectionEstablished - CONNECTED=true, lastCommand=" + lastCommand);
QLog.d(LOG_TAG, "notifyConnectionEstablished - END: ADBConnected=" + ADBConnected);
Log.i(LOG_TAG, "notifyConnectionEstablished" + lastCommand);
}
@Override
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
QLog.d(LOG_TAG, "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
if (e != null) {
QLog.e(LOG_TAG, "notifyConnectionFailed - STACK_TRACE: ", e);
}
Log.e(LOG_TAG, e.getMessage());
}
@Override
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
QLog.d(LOG_TAG, "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception") + ", ADBConnected=" + ADBConnected);
if (e != null) {
QLog.e(LOG_TAG, "notifyStreamFailed - STACK_TRACE: ", e);
}
Log.e(LOG_TAG, e.getMessage());
}
@Override
public void notifyStreamClosed(DeviceConnection devConn) {
QLog.d(LOG_TAG, "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
ADBConnected = false;
QLog.e(LOG_TAG, "notifyStreamClosed - ADBConnected=" + ADBConnected);
Log.e(LOG_TAG, "notifyStreamClosed");
}
@Override
public AdbCrypto loadAdbCrypto(DeviceConnection devConn) {
QLog.d(LOG_TAG, "loadAdbCrypto - START: devConn=" + devConn + ", context=" + _context);
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
QLog.d(LOG_TAG, "loadAdbCrypto - RESULT: crypto=" + (crypto != null ? "valid" : "null"));
return crypto;
return AdbUtils.readCryptoConfig(_context.getFilesDir());
}
@Override
@@ -113,7 +96,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
@Override
public void receivedData(DeviceConnection devConn, byte[] data, int offset, int length) {
QLog.i(LOG_TAG, data.toString());
Log.i(LOG_TAG, data.toString());
}
@Override
@@ -128,132 +111,96 @@ public class QZAdbRemote implements DeviceConnectionListener {
private DeviceConnection startConnection(String host, int port) {
QLog.d(LOG_TAG, "startConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
/* Create the connection object */
DeviceConnection conn = binder.createConnection(host, port);
QLog.d(LOG_TAG, "startConnection - CONNECTION_CREATED: conn=" + conn);
/* Add this activity as a connection listener */
binder.addListener(conn, this);
QLog.d(LOG_TAG, "startConnection - LISTENER_ADDED: this=" + this);
/* Begin the async connection process */
QLog.d(LOG_TAG, "startConnection - STARTING_CONNECT: about to call conn.startConnect()");
conn.startConnect();
QLog.d(LOG_TAG, "startConnection - END: startConnect() called, returning conn=" + conn);
return conn;
}
private DeviceConnection connectOrLookupConnection(String host, int port) {
QLog.d(LOG_TAG, "connectOrLookupConnection - START: host=" + host + ", port=" + port + ", binder=" + binder);
DeviceConnection conn = binder.findConnection(host, port);
QLog.d(LOG_TAG, "connectOrLookupConnection - EXISTING_CONN: conn=" + (conn != null ? "found" : "null"));
if (conn == null) {
/* No existing connection, so start the connection process */
QLog.d(LOG_TAG, "connectOrLookupConnection - NEW_CONNECTION: starting new connection");
conn = startConnection(host, port);
}
else {
/* Add ourselves as a new listener of this connection */
QLog.d(LOG_TAG, "connectOrLookupConnection - REUSE_CONNECTION: adding listener to existing connection");
binder.addListener(conn, this);
}
QLog.d(LOG_TAG, "connectOrLookupConnection - END: returning conn=" + conn);
return conn;
}
public ServiceConnection serviceConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
QLog.d(LOG_TAG, "onServiceConnected - START: componentName=" + arg0 + ", binder=" + arg1 + ", _address=" + _address);
binder = (ShellService.ShellServiceBinder)arg1;
QLog.d(LOG_TAG, "onServiceConnected - BINDER_SET: binder=" + binder + ", existing_connection=" + connection);
if (connection != null) {
QLog.d(LOG_TAG, "onServiceConnected - REMOVING_OLD_LISTENER: connection=" + connection);
binder.removeListener(connection, QZAdbRemote.getInstance());
}
QLog.d(LOG_TAG, "onServiceConnected - CONNECTING: about to call connectOrLookupConnection");
connection = connectOrLookupConnection(_address, 5555);
QLog.d(LOG_TAG, "onServiceConnected - END: connection=" + connection);
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
QLog.d(LOG_TAG, "onServiceDisconnected - START: componentName=" + arg0 + ", old_binder=" + binder);
binder = null;
QLog.d(LOG_TAG, "onServiceDisconnected - END: binder set to null");
}
};
static public void createConnection(String ip, Context context) {
QLog.d(LOG_TAG, "createConnection - START: ip=" + ip + ", context=" + context + ", existing_binder=" + binder);
_address = ip;
_context = context;
QLog.d(LOG_TAG, "createConnection - PARAMS_SET: _address=" + _address + ", _context=" + _context);
/* If we have old RSA keys, just use them */
QLog.d(LOG_TAG, "createConnection - CHECKING_CRYPTO: reading existing crypto config");
AdbCrypto crypto = AdbUtils.readCryptoConfig(_context.getFilesDir());
QLog.d(LOG_TAG, "createConnection - CRYPTO_CHECK: crypto=" + (crypto != null ? "exists" : "null"));
if (crypto == null)
{
/* We need to make a new pair */
QLog.i(LOG_TAG,
Log.i(LOG_TAG,
"This will only be done once.");
QLog.d(LOG_TAG, "createConnection - GENERATING_CRYPTO: synchronously generating crypto keys");
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
if (crypto == null) {
QLog.e(LOG_TAG, "Unable to generate and save RSA key pair");
cryptoReady = false;
return;
}
QLog.d(LOG_TAG, "createConnection - CRYPTO_GENERATED: crypto keys generated successfully");
synchronized (cryptoLock) {
cryptoReady = true;
}
} else {
QLog.d(LOG_TAG, "createConnection - CRYPTO_EXISTS: marking crypto as ready");
synchronized (cryptoLock) {
cryptoReady = true;
}
new Thread(new Runnable() {
@Override
public void run() {
AdbCrypto crypto;
crypto = AdbUtils.writeNewCryptoConfig(_context.getFilesDir());
if (crypto == null)
{
Log.e(LOG_TAG,
"Unable to generate and save RSA key pair");
return;
}
}
}).start();
}
QLog.d(LOG_TAG, "createConnection - SERVICE_CHECK: binder=" + (binder != null ? "exists" : "null"));
if (binder == null) {
QLog.i(LOG_TAG, "createConnection - STARTING_SERVICE: Starting ShellService.class");
service = new Intent(_context, ShellService.class);
service.putExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
QLog.d(LOG_TAG, "createConnection - SERVICE_INTENT: service=" + service);
/* Bind the service if we're not bound already. After binding, the callback will
* perform the initial connection. */
QLog.d(LOG_TAG, "createConnection - BINDING_SERVICE: about to bind service");
_context.bindService(service, QZAdbRemote.getInstance().serviceConn, Service.BIND_AUTO_CREATE);
QLog.d(LOG_TAG, "createConnection - SERVICE_BOUND: bindService called");
QLog.d(LOG_TAG, "createConnection - STARTING_SERVICE: SDK_INT=" + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
QLog.d(LOG_TAG, "createConnection - FOREGROUND_SERVICE: starting foreground service");
_context.startForegroundService(service);
}
else {
QLog.d(LOG_TAG, "createConnection - REGULAR_SERVICE: starting regular service");
_context.startService(service);
}
QLog.d(LOG_TAG, "createConnection - SERVICE_STARTED: service start completed");
} else {
QLog.d(LOG_TAG, "createConnection - SKIP_SERVICE: binder already exists, skipping service creation");
}
QLog.d(LOG_TAG, "createConnection - END: method completed");
}
static public void sendCommand(String command) {
QLog.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
Log.d(LOG_TAG, "sendCommand " + ADBConnected + " " + command);
if(ADBConnected) {
StringBuilder commandBuffer = new StringBuilder();
@@ -265,7 +212,7 @@ public class QZAdbRemote implements DeviceConnectionListener {
/* Send it to the device */
connection.queueCommand(commandBuffer.toString());
} else {
QLog.e(LOG_TAG, "sendCommand ADB is not connected!");
Log.e(LOG_TAG, "sendCommand ADB is not connected!");
}
}

View File

@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.os.SystemClock;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -68,7 +68,7 @@ public class SDMChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
Log.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -99,7 +99,7 @@ public class SDMChannelController {
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
@@ -108,7 +108,7 @@ public class SDMChannelController {
}
}
} else {
QLog.w(TAG, "No channel available");
Log.w(TAG, "No channel available");
}
return mIsOpen;
@@ -117,7 +117,7 @@ public class SDMChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
Log.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
@@ -146,11 +146,11 @@ public class SDMChannelController {
.append(failureReason);
}
QLog.e(TAG, logString.toString());
Log.e(TAG, logString.toString());
mAntChannel.release();
QLog.e(TAG, "ANT Command Failed");
Log.e(TAG, "ANT Command Failed");
}
public void close() {
@@ -164,7 +164,7 @@ public class SDMChannelController {
mAntChannel = null;
}
QLog.e(TAG, "Channel Closed");
Log.e(TAG, "Channel Closed");
}
/**
@@ -186,20 +186,20 @@ public class SDMChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Channel Death");
Log.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsollicited");
Log.d(TAG, "Tx Unsollicited");
long realtimeMillis = SystemClock.elapsedRealtime();
double speedM_s = speed / 3.6;
long deltaTime = (realtimeMillis - lastTime);
@@ -243,7 +243,7 @@ public class SDMChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
Log.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -278,7 +278,7 @@ public class SDMChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
QLog.e(TAG, "No Device Found");
Log.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

View File

@@ -18,7 +18,7 @@ import android.media.projection.MediaProjectionManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.WindowManager;
@@ -43,7 +43,7 @@ import android.graphics.Rect;
import android.graphics.Point;
import androidx.core.util.Pair;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.os.Build;
public class ScreenCaptureService extends Service {
@@ -137,7 +137,7 @@ public class ScreenCaptureService extends Service {
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mWidth;
//QLog.e(TAG, "Image reviewing");
//Log.e(TAG, "Image reviewing");
isRunning = true;
@@ -152,7 +152,7 @@ public class ScreenCaptureService extends Service {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
IMAGES_PRODUCED++;
QLog.e(TAG, "captured image: " + IMAGES_PRODUCED);
Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
*/
InputImage inputImage = InputImage.fromBitmap(bitmap, 0);
@@ -169,7 +169,7 @@ public class ScreenCaptureService extends Service {
public void onSuccess(Text result) {
// Task completed successfully
//QLog.e(TAG, "Image done!");
//Log.e(TAG, "Image done!");
String resultText = result.getText();
lastText = resultText;
@@ -204,12 +204,12 @@ public class ScreenCaptureService extends Service {
@Override
public void onFailure(Exception e) {
// Task failed with an exception
//QLog.e(TAG, "Image fail");
//Log.e(TAG, "Image fail");
isRunning = false;
}
});
} else {
//QLog.e(TAG, "Image ignored");
//Log.e(TAG, "Image ignored");
}
}
} catch (Exception e) {
@@ -246,7 +246,7 @@ public class ScreenCaptureService extends Service {
private class MediaProjectionStopCallback extends MediaProjection.Callback {
@Override
public void onStop() {
QLog.e(TAG, "stopping projection.");
Log.e(TAG, "stopping projection.");
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -276,12 +276,12 @@ public class ScreenCaptureService extends Service {
if (!storeDirectory.exists()) {
boolean success = storeDirectory.mkdirs();
if (!success) {
QLog.e(TAG, "failed to create file storage directory.");
Log.e(TAG, "failed to create file storage directory.");
stopSelf();
}
}
} else {
QLog.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
Log.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
stopSelf();
}
@@ -310,7 +310,7 @@ public class ScreenCaptureService extends Service {
startForeground(notification.first, notification.second);
}
} catch (Exception e) {
QLog.e("ForegroundService", "Failed to start foreground service", e);
Log.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
// start projection

View File

@@ -6,7 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.content.IntentFilter;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.app.Service;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -35,7 +35,7 @@ public class Shortcuts {
List<ShortcutInfo> shortcuts = new ArrayList<>();
QLog.d("Shortcuts", folder);
Log.d("Shortcuts", folder);
File[] files = new File(folder, "profiles").listFiles();
if (files != null) {
for (int i = 0; i < files.length && i < 5; i++) { // Limit to 5 shortcuts
@@ -45,7 +45,7 @@ public class Shortcuts {
if (dotIndex > 0) { // Check if there is a dot, indicating an extension exists
fileNameWithoutExtension = fileNameWithoutExtension.substring(0, dotIndex);
}
QLog.d("Shortcuts", file.getAbsolutePath());
Log.d("Shortcuts", file.getAbsolutePath());
Intent intent = new Intent(context, context.getClass());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra("profile_path", file.getAbsolutePath());
@@ -74,7 +74,7 @@ public class Shortcuts {
for (String key : extras.keySet()) {
Object value = extras.get(key);
if("profile_path".equals(key)) {
QLog.d("Shortcuts", "profile_path: " + value.toString());
Log.d("Shortcuts", "profile_path: " + value.toString());
return value.toString();
}
}
@@ -88,7 +88,7 @@ public class Shortcuts {
if (extras != null) {
for (String key : extras.keySet()) {
Object value = extras.get(key);
QLog.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
Log.d("Shortcuts", "Key: " + key + ", Value: " + value.toString());
}
}
}

View File

@@ -17,7 +17,7 @@ package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import android.os.SystemClock;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
@@ -67,7 +67,7 @@ public class SpeedChannelController {
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
Log.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type. In
// order for master (TX) channels and slave (RX) channels to connect, they
@@ -98,7 +98,7 @@ public class SpeedChannelController {
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
@@ -107,7 +107,7 @@ public class SpeedChannelController {
}
}
} else {
QLog.w(TAG, "No channel available");
Log.w(TAG, "No channel available");
}
return mIsOpen;
@@ -116,7 +116,7 @@ public class SpeedChannelController {
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
Log.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
@@ -145,11 +145,11 @@ public class SpeedChannelController {
.append(failureReason);
}
QLog.e(TAG, logString.toString());
Log.e(TAG, logString.toString());
mAntChannel.release();
QLog.e(TAG, "ANT Command Failed");
Log.e(TAG, "ANT Command Failed");
}
public void close() {
@@ -163,7 +163,7 @@ public class SpeedChannelController {
mAntChannel = null;
}
QLog.e(TAG, "Channel Closed");
Log.e(TAG, "Channel Closed");
}
/**
@@ -185,20 +185,20 @@ public class SpeedChannelController {
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Channel Death");
Log.e(TAG, "Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
Log.d(TAG, "Rx: " + antParcel);
Log.d(TAG, "Message Type: " + messageType);
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsollicited");
Log.d(TAG, "Tx Unsollicited");
long realtimeMillis = SystemClock.elapsedRealtime();
if (lastTime != 0) {
@@ -252,7 +252,7 @@ public class SpeedChannelController {
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
Log.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
@@ -296,7 +296,7 @@ public class SpeedChannelController {
break;
case RX_SEARCH_TIMEOUT:
// TODO May want to keep searching
QLog.e(TAG, "No Device Found");
Log.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:

View File

@@ -8,7 +8,7 @@ import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.app.Service;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -43,12 +43,12 @@ public class Usbserial {
static int lastReadLen = 0;
public static void open(Context context) {
QLog.d("QZ","UsbSerial open");
Log.d("QZ","UsbSerial open");
// Find all available drivers from attached devices.
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
if (availableDrivers.isEmpty()) {
QLog.d("QZ","UsbSerial no available drivers");
Log.d("QZ","UsbSerial no available drivers");
return;
}
@@ -58,7 +58,7 @@ public class Usbserial {
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
RingtoneManager.getRingtone(context, notification).play();
QLog.d("QZ","USB permission ...");
Log.d("QZ","USB permission ...");
final Boolean[] granted = {null};
BroadcastReceiver usbReceiver = new BroadcastReceiver() {
@Override
@@ -85,12 +85,12 @@ public class Usbserial {
// Do something here
}
}
QLog.d("QZ","USB permission "+granted[0]);
Log.d("QZ","USB permission "+granted[0]);
}
UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
if (connection == null) {
QLog.d("QZ","UsbSerial no permissions");
Log.d("QZ","UsbSerial no permissions");
// add UsbManager.requestPermission(driver.getDevice(), ..) handling here
return;
}
@@ -104,14 +104,14 @@ public class Usbserial {
// Do something here
}
QLog.d("QZ","UsbSerial port opened");
Log.d("QZ","UsbSerial port opened");
}
public static void write (byte[] bytes) {
if(port == null)
return;
QLog.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
Log.d("QZ","UsbSerial writing " + new String(bytes, StandardCharsets.UTF_8));
try {
port.write(bytes, 2000);
}
@@ -132,7 +132,7 @@ public class Usbserial {
try {
lastReadLen = port.read(receiveData, 2000);
QLog.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
Log.d("QZ","UsbSerial reading " + lastReadLen + new String(receiveData, StandardCharsets.UTF_8));
}
catch (IOException e) {
// Do something here

View File

@@ -1,145 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import org.cagnulen.qdomyoszwift.QLog;
public class VirtualGearingBridge {
private static final String TAG = "VirtualGearingBridge";
public static boolean isAccessibilityServiceEnabled(Context context) {
String settingValue = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
QLog.d(TAG, "Enabled accessibility services: " + settingValue);
if (settingValue != null) {
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(':');
splitter.setString(settingValue);
while (splitter.hasNext()) {
String service = splitter.next();
QLog.d(TAG, "Checking service: " + service);
if (service.contains("org.cagnulen.qdomyoszwift/.VirtualGearingService") ||
service.contains("VirtualGearingService")) {
QLog.d(TAG, "VirtualGearingService is enabled");
return true;
}
}
}
QLog.d(TAG, "VirtualGearingService is not enabled");
return false;
}
public static void openAccessibilitySettings(Context context) {
try {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
QLog.d(TAG, "Opened accessibility settings");
} catch (Exception e) {
QLog.e(TAG, "Failed to open accessibility settings", e);
}
}
public static void simulateShiftUp() {
QLog.d(TAG, "Simulating shift up with app-specific coordinates");
VirtualGearingService.shiftUpSmart();
}
public static void simulateShiftDown() {
QLog.d(TAG, "Simulating shift down with app-specific coordinates");
VirtualGearingService.shiftDownSmart();
}
public static String getCurrentAppPackageName(Context context) {
try {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
ActivityManager.RunningAppProcessInfo myProcess = new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(myProcess);
// For Android 5.0+ we should use UsageStatsManager, but for simplicity
// we use a more direct approach via current foreground process
// In a complete implementation we should use UsageStatsManager
// For now return null and let the service detect the app
return null;
}
} catch (Exception e) {
QLog.e(TAG, "Error getting current app package name", e);
}
return null;
}
public static int[] getScreenSize(Context context) {
try {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return new int[]{displayMetrics.widthPixels, displayMetrics.heightPixels};
} catch (Exception e) {
QLog.e(TAG, "Error getting screen size", e);
return new int[]{1080, 1920}; // Default fallback
}
}
public static void simulateTouch(int x, int y) {
QLog.d(TAG, "Simulating touch at (" + x + ", " + y + ")");
VirtualGearingService.simulateKeypress(x, y);
}
public static boolean isServiceRunning() {
boolean running = VirtualGearingService.isServiceEnabled();
QLog.d(TAG, "Service running: " + running);
return running;
}
// Native methods to get settings from C++ side
public static native double getVirtualGearingShiftUpX();
public static native double getVirtualGearingShiftUpY();
public static native double getVirtualGearingShiftDownX();
public static native double getVirtualGearingShiftDownY();
public static native int getVirtualGearingApp();
// Methods to get coordinates that will be/were sent
public static String getShiftUpCoordinates() {
try {
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
// Use VirtualGearingService to get screen size (it has access to service context)
int[] screenSize = VirtualGearingService.getScreenSize();
int x = config.shiftUp.getX(screenSize[0]);
int y = config.shiftUp.getY(screenSize[1]);
return x + "," + y;
} catch (Exception e) {
QLog.e(TAG, "Error getting shift up coordinates", e);
return "0,0";
}
}
public static String getShiftDownCoordinates() {
try {
AppConfiguration.AppConfig config = AppConfiguration.getCurrentConfig();
// Use VirtualGearingService to get screen size (it has access to service context)
int[] screenSize = VirtualGearingService.getScreenSize();
int x = config.shiftDown.getX(screenSize[0]);
int y = config.shiftDown.getY(screenSize[1]);
return x + "," + y;
} catch (Exception e) {
QLog.e(TAG, "Error getting shift down coordinates", e);
return "0,0";
}
}
public static String getLastTouchCoordinates() {
// For now, return the last coordinates that would be sent for shift up
// This could be enhanced to track actual last touch
return getShiftUpCoordinates();
}
}

View File

@@ -1,152 +0,0 @@
package org.cagnulen.qdomyoszwift;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import org.cagnulen.qdomyoszwift.QLog;
public class VirtualGearingService extends AccessibilityService {
private static final String TAG = "VirtualGearingService";
private static VirtualGearingService instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
QLog.d(TAG, "VirtualGearingService created");
}
@Override
public void onDestroy() {
super.onDestroy();
instance = null;
QLog.d(TAG, "VirtualGearingService destroyed");
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// Capture foreground app package name for smart coordinates
if (event != null && event.getPackageName() != null) {
String packageName = event.getPackageName().toString();
if (!packageName.equals(currentPackageName)) {
currentPackageName = packageName;
QLog.d(TAG, "App changed to: " + packageName);
}
}
}
@Override
public void onInterrupt() {
QLog.d(TAG, "VirtualGearingService interrupted");
}
public static boolean isServiceEnabled() {
return instance != null;
}
public static void simulateKeypress(int x, int y) {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate keypress");
return;
}
try {
GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(x, y);
path.lineTo(x + 1, y);
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(
path, 0, ViewConfiguration.getTapTimeout(), false);
gestureBuilder.addStroke(stroke);
instance.dispatchGesture(gestureBuilder.build(), null, null);
QLog.d(TAG, "Simulated keypress at (" + x + ", " + y + ")");
} catch (Exception e) {
QLog.e(TAG, "Error simulating keypress", e);
}
}
// Legacy methods for backward compatibility
public static void shiftUp() {
QLog.d(TAG, "Using legacy shiftUp - consider using shiftUpSmart()");
simulateKeypress(100, 200);
}
public static void shiftDown() {
QLog.d(TAG, "Using legacy shiftDown - consider using shiftDownSmart()");
simulateKeypress(100, 300);
}
// New smart methods with app-specific coordinates
public static void shiftUpSmart() {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate smart shift up");
return;
}
try {
// Try to detect app from package name of last AccessibilityEvent
String currentPackage = getCurrentPackageName();
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
// Calculate coordinates based on screen dimensions
int[] screenSize = getScreenSize();
int x = config.shiftUp.getX(screenSize[0]);
int y = config.shiftUp.getY(screenSize[1]);
QLog.d(TAG, "Smart shift up for " + config.appName + " at (" + x + ", " + y + ")");
simulateKeypress(x, y);
} catch (Exception e) {
QLog.e(TAG, "Error in shiftUpSmart, falling back to legacy", e);
shiftUp();
}
}
public static void shiftDownSmart() {
if (instance == null) {
QLog.w(TAG, "Service not enabled, cannot simulate smart shift down");
return;
}
try {
String currentPackage = getCurrentPackageName();
AppConfiguration.AppConfig config = AppConfiguration.getConfigForPackage(currentPackage);
int[] screenSize = getScreenSize();
int x = config.shiftDown.getX(screenSize[0]);
int y = config.shiftDown.getY(screenSize[1]);
QLog.d(TAG, "Smart shift down for " + config.appName + " at (" + x + ", " + y + ")");
simulateKeypress(x, y);
} catch (Exception e) {
QLog.e(TAG, "Error in shiftDownSmart, falling back to legacy", e);
shiftDown();
}
}
private static String currentPackageName = null;
private static String getCurrentPackageName() {
return currentPackageName != null ? currentPackageName : "unknown";
}
public static int[] getScreenSize() {
if (instance != null) {
try {
android.content.res.Resources resources = instance.getResources();
android.util.DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
QLog.d(TAG, "Screen size: " + width + "x" + height + " (density=" + displayMetrics.density + ")");
return new int[]{width, height};
} catch (Exception e) {
QLog.e(TAG, "Error getting screen size from service", e);
}
}
QLog.w(TAG, "Using fallback screen size");
return new int[]{1080, 1920}; // Default fallback
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@@ -33,7 +33,7 @@ public class WearableController {
_intent = new Intent(context, WearableMessageListenerService.class);
// FloatingWindowGFG service is started
context.startService(_intent);
QLog.v("WearableController", "started");
Log.v("WearableController", "started");
}
public static int getHeart() {

View File

@@ -15,7 +15,7 @@ import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.DataMap;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.os.Bundle;
import com.google.android.gms.common.api.Status;
import java.io.InputStream;
@@ -31,7 +31,7 @@ public class WearableMessageListenerService extends Service implements
@Override
public void onCreate() {
super.onCreate();
QLog.v("WearableMessageListenerService","onCreate");
Log.v("WearableMessageListenerService","onCreate");
}
public static int getHeart() {
@@ -55,7 +55,7 @@ public class WearableMessageListenerService extends Service implements
mWearableClient.addListener(this);
Wearable.getDataClient(this).addListener(this);
QLog.v("WearableMessageListenerService","onStartCommand");
Log.v("WearableMessageListenerService","onStartCommand");
// Return START_STICKY to restart the service if it's killed by the system
return START_STICKY;
@@ -65,9 +65,9 @@ public class WearableMessageListenerService extends Service implements
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_DELETED) {
QLog.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
Log.d(TAG, "DataItem deleted: " + event.getDataItem().getUri());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
QLog.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
Log.d(TAG, "DataItem changed: " + event.getDataItem().getUri() + " " + event.getDataItem().getUri().getPath());
if(event.getDataItem().getUri().getPath().equals("/qz")) {
new Thread(new Runnable() {
@Override
@@ -78,14 +78,14 @@ public class WearableMessageListenerService extends Service implements
heart_rate = DataMap.fromByteArray(result.get(0).getData())
.getInt("heart_rate", 0);
} else {
QLog.e(TAG, "Unexpected number of DataItems found.\n"
Log.e(TAG, "Unexpected number of DataItems found.\n"
+ "\tExpected: 1\n"
+ "\tActual: " + result.getCount());
}
} else {
QLog.d(TAG, "onHandleIntent: failed to get current alarm state");
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onHandleIntent: failed to get current alarm state");
}
QLog.d(TAG, "Heart: " + heart_rate);
Log.d(TAG, "Heart: " + heart_rate);
}
}).start();
}
@@ -96,17 +96,17 @@ public class WearableMessageListenerService extends Service implements
@Override
public void onConnected(Bundle bundle) {
QLog.v("WearableMessageListenerService","onConnected");
Log.v("WearableMessageListenerService","onConnected");
}
@Override
public void onConnectionSuspended(int i) {
QLog.v("WearableMessageListenerService","onConnectionSuspended");
Log.v("WearableMessageListenerService","onConnectionSuspended");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
QLog.v("WearableMessageListenerService","onConnectionFailed");
Log.v("WearableMessageListenerService","onConnectionFailed");
}
@Override
@@ -117,8 +117,8 @@ public class WearableMessageListenerService extends Service implements
// Handle the received message data here
String messageData = new String(data); // Assuming it's a simple string message
QLog.v("Wearable", path);
QLog.v("Wearable", messageData);
Log.v("Wearable", path);
Log.v("Wearable", messageData);
// You can then perform actions or update data in your service based on the received message
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.ContextWrapper;
import android.content.IntentFilter;
@@ -44,12 +44,12 @@ public class ZapClickLayer {
}
public static int processCharacteristic(byte[] value) {
QLog.d(TAG, "processCharacteristic");
Log.d(TAG, "processCharacteristic");
return device.processCharacteristic("QZ", value);
}
public static byte[] buildHandshakeStart() {
QLog.d(TAG, "buildHandshakeStart");
Log.d(TAG, "buildHandshakeStart");
return device.buildHandshakeStart();
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;
@@ -49,17 +49,17 @@ public class ZwiftAPI {
// Ora puoi usare 'message' come un oggetto normale
} catch (InvalidProtocolBufferException e) {
// Gestisci l'eccezione se il messaggio non può essere parsato
QLog.e(TAG, e.toString());
Log.e(TAG, e.toString());
}
}
public static float getAltitude() {
QLog.d(TAG, "getAltitude " + playerState.getAltitude());
Log.d(TAG, "getAltitude " + playerState.getAltitude());
return playerState.getAltitude();
}
public static float getDistance() {
QLog.d(TAG, "getDistance " + playerState.getDistance());
Log.d(TAG, "getDistance " + playerState.getDistance());
return playerState.getDistance();
}
}

View File

@@ -17,7 +17,7 @@ import android.widget.EditText;
import android.widget.Toast;
import android.os.Looper;
import android.os.Handler;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.garmin.android.connectiq.ConnectIQ;
import com.garmin.android.connectiq.ConnectIQAdbStrategy;
import com.garmin.android.connectiq.IQApp;

View File

@@ -9,7 +9,6 @@ import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.Socket;
import java.util.HashMap;
import org.cagnulen.qdomyoszwift.QLog;
/**
* This class represents an ADB connection.
@@ -125,13 +124,10 @@ public class AdbConnection implements Closeable {
try {
/* Read and parse a message off the socket's input stream */
AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream);
QLog.d("AdbConnection", "connectionThread - Received packet: command=0x" + Integer.toHexString(msg.command) + ", arg0=" + msg.arg0 + ", arg1=" + msg.arg1);
/* Verify magic and checksum */
if (!AdbProtocol.validateMessage(msg)) {
QLog.w("AdbConnection", "connectionThread - Invalid message, dropping packet");
if (!AdbProtocol.validateMessage(msg))
continue;
}
switch (msg.command)
{
@@ -179,25 +175,21 @@ public class AdbConnection implements Closeable {
break;
case AdbProtocol.CMD_AUTH:
QLog.d("AdbConnection", "connectionThread - Received AUTH packet, type=" + msg.arg0);
byte[] packet;
if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN)
{
/* This is an authentication challenge */
QLog.d("AdbConnection", "connectionThread - AUTH_TYPE_TOKEN challenge, sentSignature=" + conn.sentSignature);
if (conn.sentSignature)
{
/* We've already tried our signature, so send our public key */
QLog.d("AdbConnection", "connectionThread - Sending RSA public key");
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC,
conn.crypto.getAdbPublicKeyPayload());
}
else
{
/* We'll sign the token */
QLog.d("AdbConnection", "connectionThread - Signing token with private key");
packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE,
conn.crypto.signAdbTokenPayload(msg.payload));
conn.sentSignature = true;
@@ -206,22 +198,16 @@ public class AdbConnection implements Closeable {
/* Write the AUTH reply */
conn.outputStream.write(packet);
conn.outputStream.flush();
QLog.d("AdbConnection", "connectionThread - AUTH response sent");
}
else {
QLog.w("AdbConnection", "connectionThread - Unhandled AUTH type: " + msg.arg0);
}
break;
case AdbProtocol.CMD_CNXN:
QLog.d("AdbConnection", "connectionThread - Received CNXN packet! maxData=" + msg.arg1);
synchronized (conn) {
/* We need to store the max data size */
conn.maxData = msg.arg1;
/* Mark us as connected and unwait anyone waiting on the connection */
conn.connected = true;
QLog.d("AdbConnection", "connectionThread - Connection established! Notifying waiting threads");
conn.notifyAll();
}
break;
@@ -233,7 +219,6 @@ public class AdbConnection implements Closeable {
} catch (Exception e) {
/* The cleanup is taken care of by a combination of this thread
* and close() */
QLog.e("AdbConnection", "connectionThread - Exception in connection thread: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
break;
}
}
@@ -285,32 +270,23 @@ public class AdbConnection implements Closeable {
if (connected)
throw new IllegalStateException("Already connected");
QLog.d("AdbConnection", "connect() - Starting ADB connection");
/* Write the CONNECT packet */
outputStream.write(AdbProtocol.generateConnect());
outputStream.flush();
QLog.d("AdbConnection", "connect() - CONNECT packet sent, starting connection thread");
/* Start the connection thread to respond to the peer */
connectAttempted = true;
connectionThread.start();
QLog.d("AdbConnection", "connect() - Connection thread started, waiting for connection...");
/* Wait for the connection to go live */
synchronized (this) {
if (!connected) {
QLog.d("AdbConnection", "connect() - Waiting for connection to complete...");
if (!connected)
wait();
QLog.d("AdbConnection", "connect() - Wait completed, connected=" + connected);
}
if (!connected) {
QLog.e("AdbConnection", "connect() - Connection failed after wait");
throw new IOException("Connection failed");
}
}
QLog.d("AdbConnection", "connect() - Successfully connected!");
}
/**

View File

@@ -11,7 +11,6 @@ import com.cgutman.adblib.AdbConnection;
import com.cgutman.adblib.AdbCrypto;
import com.cgutman.adblib.AdbStream;
import com.cgutman.androidremotedebugger.AdbUtils;
import org.cagnulen.qdomyoszwift.QLog;
public class DeviceConnection implements Closeable {
private static final int CONN_TIMEOUT = 5000;
@@ -60,58 +59,42 @@ public class DeviceConnection implements Closeable {
}
public void startConnect() {
QLog.d("DeviceConnection", "startConnect - START: host=" + host + ", port=" + port + ", listener=" + listener);
new Thread(new Runnable() {
@Override
public void run() {
QLog.d("DeviceConnection", "startConnect.run - THREAD_START: host=" + host + ", port=" + port);
boolean connected = false;
Socket socket = new Socket();
AdbCrypto crypto;
/* Load the crypto config */
QLog.d("DeviceConnection", "startConnect.run - LOADING_CRYPTO: calling loadAdbCrypto");
crypto = listener.loadAdbCrypto(DeviceConnection.this);
if (crypto == null) {
QLog.e("DeviceConnection", "startConnect.run - CRYPTO_FAILED: crypto is null, returning");
return;
}
QLog.d("DeviceConnection", "startConnect.run - CRYPTO_LOADED: crypto=" + crypto);
try {
/* Establish a connect to the remote host */
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECT: connecting to " + host + ":" + port + " with timeout=" + CONN_TIMEOUT);
socket.connect(new InetSocketAddress(host, port), CONN_TIMEOUT);
QLog.d("DeviceConnection", "startConnect.run - SOCKET_CONNECTED: socket connected successfully");
} catch (IOException e) {
QLog.e("DeviceConnection", "startConnect.run - SOCKET_FAILED: connection failed", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
return;
}
try {
/* Establish the application layer connection */
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTION: creating AdbConnection");
connection = AdbConnection.create(socket, crypto);
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECT: calling connection.connect()");
connection.connect();
QLog.d("DeviceConnection", "startConnect.run - ADB_CONNECTED: ADB connection established");
/* Open the shell stream */
QLog.d("DeviceConnection", "startConnect.run - SHELL_STREAM: opening shell stream");
shellStream = connection.open("shell:");
QLog.d("DeviceConnection", "startConnect.run - SHELL_OPENED: shell stream opened successfully");
connected = true;
} catch (IOException e) {
QLog.e("DeviceConnection", "startConnect.run - ADB_IO_ERROR: IOException during ADB connection", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
} catch (InterruptedException e) {
QLog.e("DeviceConnection", "startConnect.run - ADB_INTERRUPTED: InterruptedException during ADB connection", e);
listener.notifyConnectionFailed(DeviceConnection.this, e);
} finally {
/* Cleanup if the connection failed */
if (!connected) {
QLog.d("DeviceConnection", "startConnect.run - CLEANUP: connection failed, cleaning up");
AdbUtils.safeClose(shellStream);
/* The AdbConnection object will close the underlying socket
@@ -129,16 +112,12 @@ public class DeviceConnection implements Closeable {
}
/* Notify the listener that the connection is complete */
QLog.d("DeviceConnection", "startConnect.run - NOTIFY_SUCCESS: calling listener.notifyConnectionEstablished");
listener.notifyConnectionEstablished(DeviceConnection.this);
QLog.d("DeviceConnection", "startConnect.run - NOTIFIED: notifyConnectionEstablished called");
/* Start the receive thread */
QLog.d("DeviceConnection", "startConnect.run - START_RECEIVE: starting receive thread");
startReceiveThread();
/* Enter the blocking send loop */
QLog.d("DeviceConnection", "startConnect.run - SEND_LOOP: entering send loop");
sendLoop();
}
}).start();
@@ -169,32 +148,23 @@ public class DeviceConnection implements Closeable {
}
private void startReceiveThread() {
QLog.d("DeviceConnection", "startReceiveThread - START: creating receive thread");
new Thread(new Runnable() {
@Override
public void run() {
QLog.d("DeviceConnection", "startReceiveThread.run - THREAD_START: receive thread started");
try {
while (!shellStream.isClosed()) {
QLog.d("DeviceConnection", "startReceiveThread.run - READING: waiting for data from shellStream");
byte[] data = shellStream.read();
QLog.d("DeviceConnection", "startReceiveThread.run - DATA_RECEIVED: " + data.length + " bytes received");
listener.receivedData(DeviceConnection.this, data, 0, data.length);
}
QLog.d("DeviceConnection", "startReceiveThread.run - STREAM_CLOSED: shellStream is closed");
listener.notifyStreamClosed(DeviceConnection.this);
} catch (IOException e) {
QLog.e("DeviceConnection", "startReceiveThread.run - IO_ERROR: IOException in receive thread", e);
listener.notifyStreamFailed(DeviceConnection.this, e);
} catch (InterruptedException e) {
QLog.d("DeviceConnection", "startReceiveThread.run - INTERRUPTED: receive thread interrupted");
} finally {
QLog.d("DeviceConnection", "startReceiveThread.run - CLEANUP: cleaning up receive thread");
AdbUtils.safeClose(DeviceConnection.this);
}
}
}).start();
QLog.d("DeviceConnection", "startReceiveThread - END: receive thread started");
}
public boolean isClosed() {

View File

@@ -23,7 +23,7 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import androidx.core.app.NotificationCompat;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
public class ShellService extends Service implements DeviceConnectionListener {
@@ -48,20 +48,14 @@ public class ShellService extends Service implements DeviceConnectionListener {
public class ShellServiceBinder extends Binder {
public DeviceConnection createConnection(String host, int port) {
QLog.d("ShellService", "createConnection - START: host=" + host + ", port=" + port + ", listener=" + listener);
DeviceConnection conn = new DeviceConnection(listener, host, port);
QLog.d("ShellService", "createConnection - CONNECTION_CREATED: conn=" + conn);
listener.addListener(conn, ShellService.this);
QLog.d("ShellService", "createConnection - LISTENER_ADDED: returning conn=" + conn);
return conn;
}
public DeviceConnection findConnection(String host, int port) {
String connStr = host+":"+port;
QLog.d("ShellService", "findConnection - SEARCH: connStr=" + connStr + ", mapSize=" + currentConnectionMap.size());
DeviceConnection found = currentConnectionMap.get(connStr);
QLog.d("ShellService", "findConnection - RESULT: found=" + (found != null ? "exists" : "null"));
return found;
return currentConnectionMap.get(connStr);
}
public void notifyPausingActivity(DeviceConnection devConn) {
@@ -82,95 +76,68 @@ public class ShellService extends Service implements DeviceConnectionListener {
}
public void addListener(DeviceConnection conn, DeviceConnectionListener listener) {
QLog.d("ShellService", "addListener - START: conn=" + conn + ", listener=" + listener);
ShellService.this.listener.addListener(conn, listener);
QLog.d("ShellService", "addListener - END: listener added");
}
public void removeListener(DeviceConnection conn, DeviceConnectionListener listener) {
QLog.d("ShellService", "removeListener - START: conn=" + conn + ", listener=" + listener);
ShellService.this.listener.removeListener(conn, listener);
QLog.d("ShellService", "removeListener - END: listener removed");
}
}
@Override
public IBinder onBind(Intent arg0) {
QLog.d("ShellService", "onBind - START: intent=" + arg0 + ", binder=" + binder);
QLog.d("ShellService", "onBind - END: returning binder");
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
QLog.d("ShellService", "onUnbind - START: intent=" + intent + ", connections=" + currentConnectionMap.size());
/* Stop the service if no connections remain */
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "onUnbind - STOPPING_SERVICE: no connections remain");
stopSelf();
} else {
QLog.d("ShellService", "onUnbind - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
}
QLog.d("ShellService", "onUnbind - END: returning false");
return false;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
QLog.d("ShellService", "onStartCommand - START: intent=" + intent + ", flags=" + flags + ", startId=" + startId + ", foregroundId=" + foregroundId);
if (foregroundId == 0) {
try {
int serviceType = intent.getIntExtra(EXTRA_FOREGROUND_SERVICE_TYPE, FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
// If we're not already running in the foreground, use a placeholder
// notification until a real connection is established. After connection
// establishment, the real notification will replace this one.
QLog.d("ShellService", "onStartCommand - FOREGROUND_START: serviceType=" + serviceType + ", SDK_INT=" + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
QLog.d("ShellService", "onStartCommand - FOREGROUND_Q+: starting with service type");
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification(), serviceType);
} else {
QLog.d("ShellService", "onStartCommand - FOREGROUND_LEGACY: starting without service type");
startForeground(FOREGROUND_PLACEHOLDER_ID, createForegroundPlaceholderNotification());
}
QLog.d("ShellService", "onStartCommand - FOREGROUND_SUCCESS: foreground service started");
} catch (Exception e) {
QLog.e("ForegroundService", "Failed to start foreground service", e);
Log.e("ForegroundService", "Failed to start foreground service", e);
return START_NOT_STICKY;
}
} else {
QLog.d("ShellService", "onStartCommand - SKIP_FOREGROUND: already running in foreground with id=" + foregroundId);
}
// Don't restart if we've been killed. We will have already lost our connections
// when we died, so we'll just be running doing nothing if the OS restarted us.
QLog.d("ShellService", "onStartCommand - END: returning START_NOT_STICKY");
return Service.START_NOT_STICKY;
}
@Override
public void onCreate() {
QLog.d("ShellService", "onCreate - START: initializing service");
super.onCreate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: creating notification channel");
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Connection Info", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
QLog.d("ShellService", "onCreate - NOTIFICATION_CHANNEL: channel created");
}
QLog.d("ShellService", "onCreate - WIFI_LOCK: creating wifi lock");
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wlanLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, "RemoteADBShell:ShellService");
QLog.d("ShellService", "onCreate - WIFI_LOCK: wlanLock=" + wlanLock);
QLog.d("ShellService", "onCreate - WAKE_LOCK: creating wake lock");
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "RemoteADBShell:ShellService");
QLog.d("ShellService", "onCreate - WAKE_LOCK: wakeLock=" + wakeLock);
QLog.d("ShellService", "onCreate - END: service initialization complete");
}
@Override
@@ -281,76 +248,44 @@ public class ShellService extends Service implements DeviceConnectionListener {
}
private synchronized void addNewConnection(DeviceConnection devConn) {
QLog.d("ShellService", "addNewConnection - START: devConn=" + devConn + ", currentSize=" + currentConnectionMap.size());
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "addNewConnection - ACQUIRING_LOCKS: first connection, acquiring locks");
wakeLock.acquire();
wlanLock.acquire();
QLog.d("ShellService", "addNewConnection - LOCKS_ACQUIRED: wakeLock and wlanLock acquired");
}
String connString = getConnectionString(devConn);
QLog.d("ShellService", "addNewConnection - ADDING: connString=" + connString);
currentConnectionMap.put(connString, devConn);
QLog.d("ShellService", "addNewConnection - END: connection added, newSize=" + currentConnectionMap.size());
currentConnectionMap.put(getConnectionString(devConn), devConn);
}
private synchronized void removeConnection(DeviceConnection devConn) {
String connString = getConnectionString(devConn);
QLog.d("ShellService", "removeConnection - START: devConn=" + devConn + ", connString=" + connString + ", currentSize=" + currentConnectionMap.size());
currentConnectionMap.remove(connString);
QLog.d("ShellService", "removeConnection - REMOVED: newSize=" + currentConnectionMap.size());
currentConnectionMap.remove(getConnectionString(devConn));
/* Stop the service if no connections remain */
if (currentConnectionMap.isEmpty()) {
QLog.d("ShellService", "removeConnection - STOPPING_SERVICE: no connections remain");
stopSelf();
} else {
QLog.d("ShellService", "removeConnection - KEEPING_SERVICE: " + currentConnectionMap.size() + " connections remain");
}
QLog.d("ShellService", "removeConnection - END");
}
@Override
public void notifyConnectionEstablished(DeviceConnection devConn) {
QLog.d("ShellService", "notifyConnectionEstablished - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
addNewConnection(devConn);
QLog.d("ShellService", "notifyConnectionEstablished - CONNECTION_ADDED: updating notification");
updateNotification(devConn, true);
QLog.d("ShellService", "notifyConnectionEstablished - END: connection established successfully");
}
@Override
public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
QLog.d("ShellService", "notifyConnectionFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
QLog.e("ShellService", "notifyConnectionFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
if (e != null) {
QLog.e("ShellService", "notifyConnectionFailed - STACK_TRACE: ", e);
}
/* No notification is displaying here */
QLog.d("ShellService", "notifyConnectionFailed - END");
}
@Override
public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
QLog.d("ShellService", "notifyStreamFailed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
QLog.e("ShellService", "notifyStreamFailed - ERROR: " + (e != null ? e.getMessage() : "null exception"));
if (e != null) {
QLog.e("ShellService", "notifyStreamFailed - STACK_TRACE: ", e);
}
updateNotification(devConn, false);
QLog.d("ShellService", "notifyStreamFailed - NOTIFICATION_UPDATED: removing connection");
removeConnection(devConn);
QLog.d("ShellService", "notifyStreamFailed - END");
}
@Override
public void notifyStreamClosed(DeviceConnection devConn) {
QLog.d("ShellService", "notifyStreamClosed - START: devConn=" + devConn + ", host=" + (devConn != null ? devConn.getHost() : "null") + ", port=" + (devConn != null ? devConn.getPort() : "null"));
updateNotification(devConn, false);
QLog.d("ShellService", "notifyStreamClosed - NOTIFICATION_UPDATED: removing connection");
removeConnection(devConn);
QLog.d("ShellService", "notifyStreamClosed - END");
}
@Override

View File

@@ -55,7 +55,7 @@ import java.util.List;
import android.app.Activity;
import android.content.Context;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
@@ -65,16 +65,13 @@ import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.PendingPurchasesParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchaseState;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.android.billingclient.api.QueryProductDetailsResult;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
/***********************************************************************
@@ -82,7 +79,7 @@ import com.android.billingclient.api.QueryProductDetailsResult;
** Add Dependencies below to build.gradle file:
dependencies {
def billing_version = "8.0.0"
def billing_version = "4.0.0"
implementation "com.android.billingclient:billing:$billing_version"
}
@@ -100,8 +97,8 @@ public class InAppPurchase implements PurchasesUpdatedListener
public static final int RESULT_OK = BillingClient.BillingResponseCode.OK;
public static final int RESULT_USER_CANCELED = BillingClient.BillingResponseCode.USER_CANCELED;
public static final String TYPE_INAPP = BillingClient.ProductType.INAPP;
public static final String TYPE_SUBS = BillingClient.ProductType.SUBS;
public static final String TYPE_INAPP = BillingClient.SkuType.INAPP;
public static final String TYPE_SUBS = BillingClient.SkuType.SUBS;
public static final String TAG = "InAppPurchase";
// Should be in sync with InAppTransaction::FailureReason
@@ -122,28 +119,26 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
public void initializeConnection(){
QLog.w(TAG, "initializeConnection start");
PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build();
Log.w(TAG, "initializeConnection start");
billingClient = BillingClient.newBuilder(m_context)
.enablePendingPurchases(pendingPurchasesParams)
.enablePendingPurchases()
.setListener(this)
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
QLog.w(TAG, "onBillingSetupFinished");
if (billingResult.getResponseCode() == RESULT_OK) {
Log.w(TAG, "onBillingSetupFinished");
return;
/* if (billingResult.getResponseCode() == RESULT_OK) {
purchasedProductsQueried(m_nativePointer);
} else {
QLog.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
}
Log.w(TAG, "onBillingSetupFinished error!" + billingResult.getResponseCode());
}*/
}
@Override
public void onBillingServiceDisconnected() {
QLog.w(TAG, "Billing service disconnected");
Log.w(TAG, "Billing service disconnected");
}
});
}
@@ -152,23 +147,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
QLog.d(TAG, "onPurchasesUpdated called. Response code: " + responseCode + ", Debug message: " + billingResult.getDebugMessage());
if (purchases == null) {
QLog.e(TAG, "Purchase failed: Data missing from result (purchases is null)");
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
return;
}
if (billingResult.getResponseCode() == RESULT_OK) {
QLog.d(TAG, "Purchase successful, handling " + purchases.size() + " purchases");
handlePurchase(purchases);
} else if (responseCode == RESULT_USER_CANCELED) {
QLog.d(TAG, "Purchase cancelled by user");
purchaseFailed(purchaseRequestCode, FAILUREREASON_USERCANCELED, "");
} else {
String errorString = getErrorString(responseCode);
QLog.e(TAG, "Purchase failed with error: " + errorString + " (code: " + responseCode + ")");
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, errorString);
}
}
@@ -202,7 +192,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult)
{
QLog.d(TAG, "Purchase acknowledged ");
Log.d(TAG, "Purchase acknowledged ");
}
}
);
@@ -210,9 +200,9 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
public void queryDetails(final String[] productIds) {
QLog.d(TAG, "queryDetails: start");
Log.d(TAG, "queryDetails: start");
int index = 0;
QLog.d(TAG, "queryDetails: productIds.length " + productIds.length);
Log.d(TAG, "queryDetails: productIds.length " + productIds.length);
while (index < productIds.length) {
List<String> productIdList = new ArrayList<>();
for (int i = index; i < Math.min(index + 20, productIds.length); ++i) {
@@ -220,44 +210,31 @@ public class InAppPurchase implements PurchasesUpdatedListener
}
index += productIdList.size();
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
for (String productId : productIdList) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(TYPE_SUBS)
.build());
}
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params,
(billingResult, productDetailsResult) -> {
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(productIdList).setType(TYPE_SUBS);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
int responseCode = billingResult.getResponseCode();
QLog.d(TAG, "onProductDetailsResponse: responseCode " + responseCode);
Log.d(TAG, "onSkuDetailsResponse: responseCode " + responseCode);
if (responseCode != RESULT_OK) {
QLog.e(TAG, "queryDetails: Couldn't retrieve product details.");
Log.e(TAG, "queryDetails: Couldn't retrieve sku details.");
return;
}
if (productDetailsList == null || productDetailsList.isEmpty()) {
QLog.e(TAG, "queryDetails: No details list in response.");
if (skuDetailsList == null) {
Log.e(TAG, "queryDetails: No details list in response.");
return;
}
QLog.d(TAG, "onProductDetailsResponse: productDetailsList " + productDetailsList);
for (ProductDetails productDetails : productDetailsList) {
Log.d(TAG, "onSkuDetailsResponse: skuDetailsList " + skuDetailsList);
for (SkuDetails skuDetails : skuDetailsList) {
try {
String queriedProductId = productDetails.getProductId();
String queriedPrice = "";
String queriedTitle = productDetails.getTitle();
String queriedDescription = productDetails.getDescription();
// Get price from subscription offer details
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
queriedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
}
String queriedProductId = skuDetails.getSku();
String queriedPrice = skuDetails.getPrice();
String queriedTitle = skuDetails.getTitle();
String queriedDescription = skuDetails.getDescription();
registerProduct(m_nativePointer,
queriedProductId,
queriedPrice,
@@ -267,6 +244,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
e.printStackTrace();
}
}
}
});
@@ -278,51 +256,33 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void launchBillingFlow(String identifier, final int requestCode){
purchaseRequestCode = requestCode;
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(identifier)
.setProductType(TYPE_SUBS)
.build());
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params,
(billingResult, productDetailsResult) -> {
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
List<String> skuList = new ArrayList<>();
skuList.add(identifier);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(TYPE_SUBS);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() != RESULT_OK) {
QLog.e(TAG, "Unable to launch Google Play purchase screen. Response code: " + billingResult.getResponseCode() + ", Debug message: " + billingResult.getDebugMessage());
Log.e(TAG, "Unable to launch Google Play purchase screen");
String errorString = getErrorString(requestCode);
purchaseFailed(requestCode, FAILUREREASON_ERROR, errorString);
return;
}
else if (productDetailsList == null || productDetailsList.isEmpty()){
else if (skuDetailsList == null){
purchaseFailed(purchaseRequestCode, FAILUREREASON_ERROR, "Data missing from result");
return;
}
ProductDetails productDetails = productDetailsList.get(0);
BillingFlowParams.ProductDetailsParams.Builder productDetailsParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails);
// For subscriptions, we need to set the offer token
if (productDetails.getSubscriptionOfferDetails() != null && !productDetails.getSubscriptionOfferDetails().isEmpty()) {
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
QLog.d(TAG, "Setting offer token for subscription: " + offerToken);
productDetailsParamsBuilder.setOfferToken(offerToken);
} else {
QLog.w(TAG, "No subscription offer details found for product: " + identifier);
}
BillingFlowParams.ProductDetailsParams productDetailsParams = productDetailsParamsBuilder.build();
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(java.util.Arrays.asList(productDetailsParams))
.setSkuDetails(skuDetailsList.get(0))
.build();
//Results will be delivered to onPurchasesUpdated
billingClient.launchBillingFlow((Activity) m_context, purchaseParams);
}
});
}
@@ -332,7 +292,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() != RESULT_OK) {
QLog.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
Log.e(TAG, "Unable to consume purchase. Response code: " + billingResult.getResponseCode());
}
}
};
@@ -353,7 +313,7 @@ public class InAppPurchase implements PurchasesUpdatedListener
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if (billingResult.getResponseCode() != RESULT_OK){
QLog.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
Log.e(TAG, "Unable to acknowledge purchase. Response code: " + billingResult.getResponseCode());
}
}
};
@@ -362,21 +322,18 @@ public class InAppPurchase implements PurchasesUpdatedListener
public void queryPurchasedProducts(final List<String> productIdList) {
QueryPurchasesParams queryPurchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(TYPE_SUBS)
.build();
billingClient.queryPurchasesAsync(queryPurchasesParams, new PurchasesResponseListener() {
billingClient.queryPurchasesAsync(TYPE_INAPP, new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> list) {
for (Purchase purchase : list) {
if (productIdList.contains(purchase.getProducts().get(0))) {
if (productIdList.contains(purchase.getSkus().get(0))) {
registerPurchased(m_nativePointer,
purchase.getProducts().get(0),
purchase.getSkus().get(0),
purchase.getSignature(),
purchase.getOriginalJson(),
purchase.getPurchaseToken(),
"", // getDeveloperPayload() is deprecated
purchase.getDeveloperPayload(),
purchase.getPurchaseTime());
}
}

View File

@@ -17,7 +17,7 @@
package org.qtproject.qt.android.purchasing;
import android.text.TextUtils;
import org.cagnulen.qdomyoszwift.QLog;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
@@ -58,7 +58,7 @@ public class Security {
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (signedData == null) {
QLog.e(TAG, "data is null");
Log.e(TAG, "data is null");
return false;
}
@@ -67,7 +67,7 @@ public class Security {
PublicKey key = Security.generatePublicKey(base64PublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
QLog.w(TAG, "signature does not match data.");
Log.w(TAG, "signature does not match data.");
return false;
}
}
@@ -89,10 +89,10 @@ public class Security {
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
QLog.e(TAG, "Invalid key specification.");
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
QLog.e(TAG, "Base64 decoding failed.");
Log.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
@@ -113,18 +113,18 @@ public class Security {
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
QLog.e(TAG, "Signature verification failed.");
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
QLog.e(TAG, "NoSuchAlgorithmException.");
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
QLog.e(TAG, "Invalid key specification.");
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
QLog.e(TAG, "Signature exception.");
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
QLog.e(TAG, "Base64 decoding failed.");
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}

View File

@@ -1,42 +0,0 @@
#include <QDebug>
#ifdef Q_OS_ANDROID
#include <QAndroidJniObject>
#include <jni.h>
extern "C" JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_QLog_sendToQt(JNIEnv *env, jclass clazz,
jint level, jstring tag, jstring message) {
const char *tagChars = env->GetStringUTFChars(tag, nullptr);
const char *msgChars = env->GetStringUTFChars(message, nullptr);
QString tagStr = QString::fromUtf8(tagChars);
QString msgStr = QString::fromUtf8(msgChars);
// Converti i livelli di log Android in livelli Qt
switch (level) {
case 2: // VERBOSE
qDebug() << "[VERBOSE:" << tagStr << "]" << msgStr;
break;
case 3: // DEBUG
qDebug() << "[DEBUG:" << tagStr << "]" << msgStr;
break;
case 4: // INFO
qInfo() << "[INFO:" << tagStr << "]" << msgStr;
break;
case 5: // WARN
qWarning() << "[WARN:" << tagStr << "]" << msgStr;
break;
case 6: // ERROR
qCritical() << "[ERROR:" << tagStr << "]" << msgStr;
break;
case 7: // ASSERT/WTF
qCritical() << "[ASSERT:" << tagStr << "]" << msgStr;
break;
default:
qDebug() << "[LOG:" << tagStr << "(" << level << ")]" << msgStr;
}
env->ReleaseStringUTFChars(tag, tagChars);
env->ReleaseStringUTFChars(message, msgChars);
}
#endif

View File

@@ -1,64 +0,0 @@
#include "androidstatusbar.h"
#include <QQmlEngine>
#include <QDebug>
#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include <QAndroidJniEnvironment>
#endif
AndroidStatusBar* AndroidStatusBar::m_instance = nullptr;
AndroidStatusBar::AndroidStatusBar(QObject *parent) : QObject(parent)
{
m_instance = this;
}
AndroidStatusBar* AndroidStatusBar::instance()
{
return m_instance;
}
void AndroidStatusBar::registerQmlType()
{
qmlRegisterSingletonType<AndroidStatusBar>("AndroidStatusBar", 1, 0, "AndroidStatusBar",
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject* {
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return new AndroidStatusBar();
});
}
int AndroidStatusBar::apiLevel() const
{
#ifdef Q_OS_ANDROID
return QAndroidJniObject::callStaticMethod<jint>("org/cagnulen/qdomyoszwift/CustomQtActivity", "getApiLevel", "()I");
#else
return 0;
#endif
}
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
{
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
m_top = top;
m_bottom = bottom;
m_left = left;
m_right = right;
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
emit insetsChanged();
}
}
#ifdef Q_OS_ANDROID
// JNI method with standard naming convention
extern "C" JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
if (AndroidStatusBar::instance()) {
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
}
}
#endif

View File

@@ -1,43 +0,0 @@
#ifndef ANDROIDSTATUSBAR_H
#define ANDROIDSTATUSBAR_H
#include <QObject>
#include <QQmlEngine>
class AndroidStatusBar : public QObject
{
Q_OBJECT
Q_PROPERTY(int height READ height NOTIFY insetsChanged)
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
public:
explicit AndroidStatusBar(QObject *parent = nullptr);
static void registerQmlType();
static AndroidStatusBar* instance();
int height() const { return m_top; }
int navigationBarHeight() const { return m_bottom; }
int leftInset() const { return m_left; }
int rightInset() const { return m_right; }
int apiLevel() const;
public slots:
void onInsetsChanged(int top, int bottom, int left, int right);
signals:
void insetsChanged();
private:
int m_top = 0;
int m_bottom = 0;
int m_left = 0;
int m_right = 0;
static AndroidStatusBar* m_instance;
};
#endif // ANDROIDSTATUSBAR_H

View File

@@ -1 +0,0 @@
/Users/cagnulein/Qt/5.15.2/ios/bin/rcc qml.qrc -o ../build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qrc_qml.cpp

View File

@@ -1,23 +0,0 @@
#include "characteristicnotifier0002.h"
#include "bike.h"
#include <QDebug>
#include <QList>
CharacteristicNotifier0002::CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent)
: CharacteristicNotifier(0x0002, parent) {
Bike = bike;
answerList = QList<QByteArray>(); // Initialize empty list
}
void CharacteristicNotifier0002::addAnswer(const QByteArray &newAnswer) {
answerList.append(newAnswer);
}
int CharacteristicNotifier0002::notify(QByteArray &value) {
if(!answerList.isEmpty()) {
value.append(answerList.first()); // Get first item
answerList.removeFirst(); // Remove it from list
return CN_OK;
}
return CN_INVALID;
}

View File

@@ -1,19 +0,0 @@
#ifndef CHARACTERISTICNOTIFIER0002_H
#define CHARACTERISTICNOTIFIER0002_H
#include "bluetoothdevice.h"
#include "characteristicnotifier.h"
#include <QList>
class CharacteristicNotifier0002 : public CharacteristicNotifier {
Q_OBJECT
bluetoothdevice* Bike = nullptr;
QList<QByteArray> answerList;
public:
explicit CharacteristicNotifier0002(bluetoothdevice *bike, QObject *parent = nullptr);
int notify(QByteArray &value) override;
void addAnswer(const QByteArray &newAnswer);
};
#endif // CHARACTERISTICNOTIFIER0002_H

View File

@@ -1,23 +0,0 @@
#include "characteristicnotifier0004.h"
#include "bike.h"
#include <QDebug>
#include <QList>
CharacteristicNotifier0004::CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent)
: CharacteristicNotifier(0x0004, parent) {
Bike = bike;
answerList = QList<QByteArray>();
}
void CharacteristicNotifier0004::addAnswer(const QByteArray &newAnswer) {
answerList.append(newAnswer);
}
int CharacteristicNotifier0004::notify(QByteArray &value) {
if(!answerList.isEmpty()) {
value.append(answerList.first());
answerList.removeFirst();
return CN_OK;
}
return CN_INVALID;
}

View File

@@ -1,19 +0,0 @@
#ifndef CHARACTERISTICNOTIFIER0004_H
#define CHARACTERISTICNOTIFIER0004_H
#include "bluetoothdevice.h"
#include "characteristicnotifier.h"
#include <QList>
class CharacteristicNotifier0004 : public CharacteristicNotifier {
Q_OBJECT
bluetoothdevice* Bike = nullptr;
QList<QByteArray> answerList;
public:
explicit CharacteristicNotifier0004(bluetoothdevice *bike, QObject *parent = nullptr);
int notify(QByteArray &value) override;
void addAnswer(const QByteArray &newAnswer);
};
#endif // CHARACTERISTICNOTIFIER0004_H

View File

@@ -1,7 +1,6 @@
#include "characteristicnotifier2acd.h"
#include "devices/treadmill.h"
#include <qmath.h>
#include <QTime> // Include QTime for Bike->elapsedTime()
CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QObject *parent)
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
@@ -10,8 +9,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL) {
value.append(0x0C); // Inclination available and distance for peloton
//value.append((char)0x01); // heart rate available
value.append((char)0x05); // HeartRate(8) | ElapsedTime(10)
value.append((char)0x01); // heart rate available
uint16_t normalizeSpeed = (uint16_t)qRound(Bike->currentSpeed().value() * 100);
char a = (normalizeSpeed >> 8) & 0XFF;
@@ -33,21 +31,8 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
uint16_t normalizeIncline = 0;
QSettings settings;
bool real_inclination_to_virtual_treamill_bridge = settings.value(QZSettings::real_inclination_to_virtual_treamill_bridge, QZSettings::default_real_inclination_to_virtual_treamill_bridge).toBool();
double inclination = ((treadmill *)Bike)->currentInclination().value();
if(real_inclination_to_virtual_treamill_bridge) {
double offset = settings.value(QZSettings::zwift_inclination_offset,
QZSettings::default_zwift_inclination_offset).toDouble();
double gain = settings.value(QZSettings::zwift_inclination_gain,
QZSettings::default_zwift_inclination_gain).toDouble();
inclination -= offset;
inclination /= gain;
}
if (dt == bluetoothdevice::TREADMILL)
normalizeIncline = (uint32_t)qRound(inclination * 10);
normalizeIncline = (uint32_t)qRound(((treadmill *)Bike)->currentInclination().value() * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
QByteArray inclineBytes;
@@ -55,7 +40,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclineBytes.append(a);
double ramp = 0;
if (dt == bluetoothdevice::TREADMILL)
ramp = qRadiansToDegrees(qAtan(inclination / 100));
ramp = qRadiansToDegrees(qAtan(((treadmill *)Bike)->currentInclination().value() / 100));
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;
@@ -63,18 +48,6 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
rampBytes.append(b);
rampBytes.append(a);
// Get session elapsed time - makes Runna calculations work
QTime sessionElapsedTime = Bike->elapsedTime();
double elapsed_time_seconds =
(double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
QByteArray elapsedBytes;
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
value.append(speedBytes); // Actual value.
value.append(distanceBytes); // Actual value.
@@ -84,9 +57,6 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
value.append(rampBytes); // ramp angle
value.append(Bike->currentHeart().value()); // current heart rate
value.append(elapsedBytes); // Elapsed Time
return CN_OK;
} else
return CN_INVALID;

View File

@@ -14,7 +14,7 @@ class CharacteristicWriteProcessor : public QObject {
public:
int8_t bikeResistanceOffset = 4;
double bikeResistanceGain = 1.0;
bluetoothdevice *Bike = nullptr;
bluetoothdevice *Bike;
explicit CharacteristicWriteProcessor(double bikeResistanceGain, int8_t bikeResistanceOffset,
bluetoothdevice *bike, QObject *parent = nullptr);

View File

@@ -1,316 +0,0 @@
#include "characteristicwriteprocessor0003.h"
#include <QDebug>
#include "bike.h"
CharacteristicWriteProcessor0003::CharacteristicWriteProcessor0003(double bikeResistanceGain,
int8_t bikeResistanceOffset,
bluetoothdevice *bike,
CharacteristicNotifier0002 *notifier0002,
CharacteristicNotifier0004 *notifier0004,
QObject *parent)
: CharacteristicWriteProcessor(bikeResistanceGain, bikeResistanceOffset, bike, parent), notifier0002(notifier0002), notifier0004(notifier0004) {
}
CharacteristicWriteProcessor0003::VarintResult CharacteristicWriteProcessor0003::decodeVarint(const QByteArray& bytes, int startIndex) {
qint64 result = 0;
int shift = 0;
int bytesRead = 0;
for (int i = startIndex; i < bytes.size(); i++) {
quint8 byte = static_cast<quint8>(bytes.at(i));
result |= static_cast<qint64>(byte & 0x7F) << shift;
bytesRead++;
if ((byte & 0x80) == 0) {
break;
}
shift += 7;
}
return {result, bytesRead};
}
double CharacteristicWriteProcessor0003::currentGear() {
if(zwiftGearReceived || !((bike*)Bike))
return currentZwiftGear;
else
return ((bike*)Bike)->gears();
}
qint32 CharacteristicWriteProcessor0003::decodeSInt(const QByteArray& bytes) {
if (static_cast<quint8>(bytes.at(0)) != 0x22) {
qFatal("Invalid field header");
}
int length = static_cast<quint8>(bytes.at(1));
if (static_cast<quint8>(bytes.at(2)) != 0x10) {
qFatal("Invalid inner header");
}
VarintResult varint = decodeVarint(bytes, 3);
qint32 decoded = (varint.value >> 1) ^ -(varint.value & 1);
return decoded;
}
void CharacteristicWriteProcessor0003::handleZwiftGear(const QByteArray &array) {
uint8_t g = 0;
if (array.size() >= 2) {
if ((uint8_t)array[0] == (uint8_t)0xCC && (uint8_t)array[1] == (uint8_t)0x3A) g = 1;
else if ((uint8_t)array[0] == (uint8_t)0xFC && (uint8_t)array[1] == (uint8_t)0x43) g = 2;
else if ((uint8_t)array[0] == (uint8_t)0xAC && (uint8_t)array[1] == (uint8_t)0x4D) g = 3;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0x56) g = 4;
else if ((uint8_t)array[0] == (uint8_t)0x8C && (uint8_t)array[1] == (uint8_t)0x60) g = 5;
else if ((uint8_t)array[0] == (uint8_t)0xE8 && (uint8_t)array[1] == (uint8_t)0x6B) g = 6;
else if ((uint8_t)array[0] == (uint8_t)0xC4 && (uint8_t)array[1] == (uint8_t)0x77) g = 7;
else if (array.size() >= 3) {
if ((uint8_t)array[0] == (uint8_t)0xA0 && (uint8_t)array[1] == (uint8_t)0x83 && (uint8_t)array[2] == (uint8_t)0x01) g = 8;
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x01) g = 9;
else if ((uint8_t)array[0] == (uint8_t)0xB0 && (uint8_t)array[1] == (uint8_t)0x9F && (uint8_t)array[2] == (uint8_t)0x01) g = 10;
else if ((uint8_t)array[0] == (uint8_t)0xB8 && (uint8_t)array[1] == (uint8_t)0xAD && (uint8_t)array[2] == (uint8_t)0x01) g = 11;
else if ((uint8_t)array[0] == (uint8_t)0xC0 && (uint8_t)array[1] == (uint8_t)0xBB && (uint8_t)array[2] == (uint8_t)0x01) g = 12;
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x01) g = 13;
else if ((uint8_t)array[0] == (uint8_t)0xA8 && (uint8_t)array[1] == (uint8_t)0xDC && (uint8_t)array[2] == (uint8_t)0x01) g = 14;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xEC && (uint8_t)array[2] == (uint8_t)0x01) g = 15;
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFD && (uint8_t)array[2] == (uint8_t)0x01) g = 16;
else if ((uint8_t)array[0] == (uint8_t)0xD4 && (uint8_t)array[1] == (uint8_t)0x90 && (uint8_t)array[2] == (uint8_t)0x02) g = 17;
else if ((uint8_t)array[0] == (uint8_t)0x98 && (uint8_t)array[1] == (uint8_t)0xA4 && (uint8_t)array[2] == (uint8_t)0x02) g = 18;
else if ((uint8_t)array[0] == (uint8_t)0xDC && (uint8_t)array[1] == (uint8_t)0xB7 && (uint8_t)array[2] == (uint8_t)0x02) g = 19;
else if ((uint8_t)array[0] == (uint8_t)0x9F && (uint8_t)array[1] == (uint8_t)0xCB && (uint8_t)array[2] == (uint8_t)0x02) g = 20;
else if ((uint8_t)array[0] == (uint8_t)0xD8 && (uint8_t)array[1] == (uint8_t)0xE2 && (uint8_t)array[2] == (uint8_t)0x02) g = 21;
else if ((uint8_t)array[0] == (uint8_t)0x90 && (uint8_t)array[1] == (uint8_t)0xFA && (uint8_t)array[2] == (uint8_t)0x02) g = 22;
else if ((uint8_t)array[0] == (uint8_t)0xC8 && (uint8_t)array[1] == (uint8_t)0x91 && (uint8_t)array[2] == (uint8_t)0x03) g = 23;
else if ((uint8_t)array[0] == (uint8_t)0xF3 && (uint8_t)array[1] == (uint8_t)0xAC && (uint8_t)array[2] == (uint8_t)0x03) g = 24;
else { return; }
}
else { return; }
}
QSettings settings;
if(settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool()) {
int actGear = ((bike*)Bike)->gears();
if (g < actGear) {
for (int i = 0; i < actGear - g; i++) {
((bike*)Bike)->gearDown();
}
} else if (g > actGear) {
for (int i = 0; i < g - actGear; i++) {
((bike*)Bike)->gearUp();
}
}
} else {
if (g < currentZwiftGear) {
for (int i = 0; i < currentZwiftGear - g; ++i) {
((bike*)Bike)->gearDown();
}
} else if (g > currentZwiftGear) {
for (int i = 0; i < g - currentZwiftGear; ++i) {
((bike*)Bike)->gearUp();
}
}
}
currentZwiftGear = g;
zwiftGearReceived = true;
}
QByteArray CharacteristicWriteProcessor0003::encodeHubRidingData(
uint32_t power,
uint32_t cadence,
uint32_t speedX100,
uint32_t hr,
uint32_t unknown1,
uint32_t unknown2
) {
QByteArray buffer;
buffer.append(char(0x03));
auto encodeVarInt32 = [](QByteArray& buf, uint32_t value) {
do {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value) byte |= 0x80;
buf.append(char(byte));
} while (value);
};
encodeVarInt32(buffer, (1 << 3) | 0);
encodeVarInt32(buffer, power);
encodeVarInt32(buffer, (2 << 3) | 0);
encodeVarInt32(buffer, cadence);
encodeVarInt32(buffer, (3 << 3) | 0);
encodeVarInt32(buffer, speedX100);
encodeVarInt32(buffer, (4 << 3) | 0);
encodeVarInt32(buffer, hr);
encodeVarInt32(buffer, (5 << 3) | 0);
encodeVarInt32(buffer, unknown1);
encodeVarInt32(buffer, (6 << 3) | 0);
encodeVarInt32(buffer, unknown2);
return buffer;
}
static uint32_t lastUnknown1 = 836; // Starting value from logs
static uint32_t baseValue = 19000; // Base value from original code
uint32_t CharacteristicWriteProcessor0003::calculateUnknown1(uint16_t power) {
// Increment by a value between 400-800 based on current power
uint32_t increment = 400 + (power * 2);
if (increment > 800) increment = 800;
// Adjust based on power changes
if (power > 0) {
lastUnknown1 += increment;
} else {
// For zero power, larger increments
lastUnknown1 += 600;
}
// Keep within observed range (800-24000)
if (lastUnknown1 > 24000) lastUnknown1 = baseValue;
return lastUnknown1;
}
int CharacteristicWriteProcessor0003::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
static const QByteArray expectedHexArray = QByteArray::fromHex("52696465 4F6E02");
static const QByteArray expectedHexArray2 = QByteArray::fromHex("410805");
static const QByteArray expectedHexArray3 = QByteArray::fromHex("00088804");
static const QByteArray expectedHexArray4 = QByteArray::fromHex("042A0A10 C0BB0120");
static const QByteArray expectedHexArray4b = QByteArray::fromHex("042A0A10 A0830120");
static const QByteArray expectedHexArray5 = QByteArray::fromHex("0422");
static const QByteArray expectedHexArray6 = QByteArray::fromHex("042A0410");
static const QByteArray expectedHexArray7 = QByteArray::fromHex("042A0310");
static const QByteArray expectedHexArray8 = QByteArray::fromHex("0418");
static const QByteArray expectedHexArray9 = QByteArray::fromHex("042a0810");
static const QByteArray expectedHexArray10 = QByteArray::fromHex("000800");
QByteArray receivedData = data;
if (receivedData.startsWith(expectedHexArray)) {
qDebug() << "Zwift Play Processor: Initial connection request";
reply = QByteArray::fromHex("2a08031211220f4154582030342c2053545820303400");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("2a0803120d220b524944455f4f4e28322900");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("526964654f6e0200");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray2)) {
qDebug() << "Zwift Play Processor: Device info request";
reply = QByteArray::fromHex("3c080012320a3008800412040500050"
"11a0b4b49434b5220434f524500320f"
"3430323431383030393834000000003a01314204080110140");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray3)) {
qDebug() << "Zwift Play Processor: Status request";
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray4) || receivedData.startsWith(expectedHexArray4b)) {
qDebug() << "Zwift Play Ask 4";
reply = QByteArray::fromHex("0308001000185920002800309bed01");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("2a08031227222567"
"61705f706172616d735f6368616e6765"
"2832293a2037322c2037322c20302c20"
"36303000");
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray5)) {
qDebug() << "Zwift Play Processor: Slope change request";
double slopefloat = decodeSInt(receivedData.mid(1));
QByteArray slope(2, 0);
slope[0] = quint8(qint16(slopefloat) & 0xFF);
slope[1] = quint8((qint16(slopefloat) >> 8) & 0x00FF);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
QByteArray::fromHex("116901") + slope + QByteArray::fromHex("3228"));
changeSlope(slopefloat, 0 /* TODO */, 0 /* TODO */);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
0
);
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray6)) {
qDebug() << "Zwift Play Ask 6";
reply = QByteArray::fromHex("3c0888041206 0a0440c0bb01");
reply[9] = receivedData[4];
reply[10] = receivedData[5];
reply[11] = receivedData[6];
handleZwiftGear(receivedData.mid(4));
notifier0004->addAnswer(reply);
reply = QByteArray::fromHex("03080010001827e7 20002896143093ed01");
notifier0002->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray7)) {
qDebug() << "Zwift Play Ask 7";
reply = QByteArray::fromHex("03080010001827e7 2000 28 00 3093ed01");
notifier0002->addAnswer(reply);
reply = QByteArray::fromHex("3c088804120503408c60");
reply[8] = receivedData[4];
reply[9] = receivedData[5];
handleZwiftGear(receivedData.mid(4));
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray8)) {
qDebug() << "Zwift Play Processor: Power request";
VarintResult Power = decodeVarint(receivedData, 2);
QByteArray power(2, 0);
power[0] = quint8(qint16(Power.value) & 0xFF);
power[1] = quint8((qint16(Power.value) >> 8) & 0x00FF);
emit ftmsCharacteristicChanged(QLowEnergyCharacteristic(),
QByteArray::fromHex("05") + power);
reply = encodeHubRidingData(
Bike->wattsMetric().value(),
Bike->currentCadence().value(),
0,
Bike->wattsMetric().value(),
calculateUnknown1(Bike->wattsMetric().value()),
0
);
notifier0002->addAnswer(reply);
changePower(Power.value);
}
else if (receivedData.startsWith(expectedHexArray9)) {
qDebug() << "Zwift Play Ask 9";
reply = QByteArray::fromHex("050a08400058b60560fc26");
notifier0004->addAnswer(reply);
}
else if (receivedData.startsWith(expectedHexArray10)) {
qDebug() << "Zwift Play Ask 10";
reply = QByteArray::fromHex("3c0800122408800412040004000c1a00320f42412d4534333732443932374244453a00420408011053");
notifier0004->addAnswer(reply);
}
else {
qDebug() << "Zwift Play Processor: Unhandled request:" << receivedData.toHex();
return -1;
}
return 0;
}

View File

@@ -1,45 +0,0 @@
#ifndef CHARACTERISTICWRITEPROCESSOR0003_H
#define CHARACTERISTICWRITEPROCESSOR0003_H
#include "characteristicnotifier0002.h"
#include "characteristicnotifier0004.h"
#include "characteristicwriteprocessor.h"
class CharacteristicWriteProcessor0003 : public CharacteristicWriteProcessor {
Q_OBJECT
CharacteristicNotifier0002 *notifier0002 = nullptr;
CharacteristicNotifier0004 *notifier0004 = nullptr;
public:
explicit CharacteristicWriteProcessor0003(double bikeResistanceGain, int8_t bikeResistanceOffset,
bluetoothdevice *bike, CharacteristicNotifier0002 *notifier0002,
CharacteristicNotifier0004 *notifier0004,
QObject *parent = nullptr);
int writeProcess(quint16 uuid, const QByteArray &data, QByteArray &out) override;
static QByteArray encodeHubRidingData(uint32_t power,
uint32_t cadence,
uint32_t speedX100,
uint32_t hr,
uint32_t unknown1,
uint32_t unknown2);
static uint32_t calculateUnknown1(uint16_t power);
void handleZwiftGear(const QByteArray &array);
double currentGear();
private:
struct VarintResult {
qint64 value;
int bytesRead;
};
VarintResult decodeVarint(const QByteArray& bytes, int startIndex);
qint32 decodeSInt(const QByteArray& bytes);
int currentZwiftGear = 8;
bool zwiftGearReceived = false;
signals:
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
};
#endif // CHARACTERISTICWRITEPROCESSOR0003_H

View File

@@ -14,7 +14,7 @@ CharacteristicWriteProcessor2AD9::CharacteristicWriteProcessor2AD9(double bikeRe
int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArray &data, QByteArray &reply) {
if (data.size()) {
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == bluetoothdevice::BIKE || dt == bluetoothdevice::ROWING) {
if (dt == bluetoothdevice::BIKE) {
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
@@ -63,12 +63,6 @@ int CharacteristicWriteProcessor2AD9::writeProcess(quint16 uuid, const QByteArra
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_START_RESUME);
reply.append((quint8)FTMS_SUCCESS);
} else if (cmd == FTMS_STOP_PAUSE) {
qDebug() << QStringLiteral("stop/pause simulation! ignoring it");
reply.append((quint8)FTMS_RESPONSE_CODE);
reply.append((quint8)FTMS_STOP_PAUSE);
reply.append((quint8)FTMS_SUCCESS);
} else if (cmd == FTMS_REQUEST_CONTROL) {
qDebug() << QStringLiteral("control requested");

View File

@@ -1,166 +0,0 @@
#include "android_antbike.h"
#include "virtualdevices/virtualbike.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <QThread>
#include <math.h>
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#include <QLowEnergyConnectionParameters>
#endif
#include <chrono>
using namespace std::chrono_literals;
android_antbike::android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
this->noVirtualDevice = noVirtualDevice;
initDone = false;
connect(refresh, &QTimer::timeout, this, &android_antbike::update);
refresh->start(200ms);
}
void android_antbike::update() {
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
#ifdef Q_OS_ANDROID
Heart = KeepAwakeHelper::antObject(true)->callMethod<int>("getHeart", "()I");
Cadence = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikeCadence", "()I");
m_watt = KeepAwakeHelper::antObject(true)->callMethod<int>("getBikePower", "()I");
if (settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = metric::calculateSpeedFromPower(
m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0),
speedLimit());
} else {
Speed = KeepAwakeHelper::antObject(true)->callMethod<double>("getBikeSpeed", "()D");
}
bool bikeConnected = KeepAwakeHelper::antObject(true)->callMethod<jboolean>("isBikeConnected", "()Z");
qDebug() << QStringLiteral("Current ANT Cadence: ") << QString::number(Cadence.value());
qDebug() << QStringLiteral("Current ANT Speed: ") << QString::number(Speed.value());
qDebug() << QStringLiteral("Current ANT Power: ") << QString::number(m_watt.value());
qDebug() << QStringLiteral("Current ANT Heart: ") << QString::number(Heart.value());
qDebug() << QStringLiteral("ANT Bike Connected: ") << bikeConnected;
#endif
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
if (requestInclination != -100) {
Inclination = requestInclination;
emit debug(QStringLiteral("writing incline ") + QString::number(requestInclination));
requestInclination = -100;
}
update_metrics(false, watts());
Distance += ((Speed.value() / (double)3600.0) /
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
) {
bool virtual_device_enabled =
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
connect(virtualBike, &virtualbike::changeInclination, this, &android_antbike::changeInclinationRequested);
connect(virtualBike, &virtualbike::ftmsCharacteristicChanged, this, &android_antbike::ftmsCharacteristicChanged);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
if (!firstStateChanged)
emit connectedAndDiscovered();
firstStateChanged = 1;
// ********************************************************************************************************
if (!noVirtualDevice) {
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool()) {
Heart = (uint8_t)KeepAwakeHelper::heart();
debug("Current Heart: " + QString::number(Heart.value()));
}
#endif
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
}
if (Heart.value()) {
static double lastKcal = 0;
if (KCal.value() < 0) // if the user pressed stop, the KCAL resets the accumulator
lastKcal = abs(KCal.value());
KCal = metric::calculateKCalfromHR(Heart.average(), elapsed.value()) + lastKcal;
}
if (requestResistance != -1 && requestResistance != currentResistance().value()) {
Resistance = requestResistance;
m_pelotonResistance = requestResistance;
}
}
void android_antbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
QByteArray b = newValue;
qDebug() << "routing FTMS packet to the bike from virtualbike" << characteristic.uuid() << newValue.toHex(' ');
}
void android_antbike::changeInclinationRequested(double grade, double percentage) {
if (percentage < 0)
percentage = 0;
changeInclination(grade, percentage);
}
uint16_t android_antbike::wattsFromResistance(double resistance) {
return _ergTable.estimateWattage(Cadence.value(), resistance);
}
resistance_t android_antbike::resistanceFromPowerRequest(uint16_t power) {
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
}
uint16_t android_antbike::watts() { return m_watt.value(); }
bool android_antbike::connected() { return true; }

View File

@@ -1,81 +0,0 @@
#ifndef ANDROID_ANTBIKE_H
#define ANDROID_ANTBIKE_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QObject>
#include <QString>
#include "devices/bike.h"
#include "ergtable.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class android_antbike : public bike {
Q_OBJECT
public:
android_antbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice);
bool connected() override;
uint16_t watts() override;
resistance_t maxResistance() override { return 100; }
resistance_t resistanceFromPowerRequest(uint16_t power) override;
private:
QTimer *refresh;
uint8_t sec1Update = 0;
QByteArray lastPacket;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
QDateTime lastGoodCadence = QDateTime::currentDateTime();
uint8_t firstStateChanged = 0;
bool initDone = false;
bool initRequest = false;
bool noWriteResistance = false;
bool noHeartService = false;
bool noVirtualDevice = false;
uint16_t oldLastCrankEventTime = 0;
uint16_t oldCrankRevs = 0;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
uint16_t wattsFromResistance(double resistance);
signals:
void disconnected();
void debug(QString string);
private slots:
void changeInclinationRequested(double grade, double percentage);
void update();
void ftmsCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
};
#endif // ANDROID_ANTBIKE_H

View File

@@ -132,7 +132,7 @@ void antbike::update() {
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
@@ -168,7 +168,28 @@ uint16_t antbike::wattsFromResistance(double resistance) {
}
resistance_t antbike::resistanceFromPowerRequest(uint16_t power) {
return _ergTable.resistanceFromPowerRequest(power, Cadence.value(), maxResistance());
//QSettings settings;
//bool toorx_srx_3500 = settings.value(QZSettings::toorx_srx_3500, QZSettings::default_toorx_srx_3500).toBool();
/*if(toorx_srx_3500)*/ {
qDebug() << QStringLiteral("resistanceFromPowerRequest") << Cadence.value();
if (Cadence.value() == 0)
return 1;
for (resistance_t i = 1; i < maxResistance(); i++) {
if (wattsFromResistance(i) <= power && wattsFromResistance(i + 1) >= power) {
qDebug() << QStringLiteral("resistanceFromPowerRequest") << wattsFromResistance(i)
<< wattsFromResistance(i + 1) << power;
return i;
}
}
if (power < wattsFromResistance(1))
return 1;
else
return maxResistance();
} /*else {
return power / 10;
}*/
}

View File

@@ -57,12 +57,7 @@ void apexbike::writeCharacteristic(uint8_t *data, uint8_t data_len, const QStrin
}
writeBuffer = new QByteArray((const char *)data, data_len);
if (gattWriteCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer,
QLowEnergyService::WriteWithoutResponse);
} else {
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
}
gattCommunicationChannelService->writeCharacteristic(gattWriteCharacteristic, *writeBuffer);
if (!disable_log) {
qDebug() << QStringLiteral(" >> ") + writeBuffer->toHex(' ') +
@@ -146,37 +141,42 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
lastPacket = newValue;
if (newValue.length() == 10 && newValue.at(2) == 0x31) {
// Invert resistance: bike resistance 1-32 maps to app display 32-1
uint8_t rawResistance = newValue.at(5);
Resistance = 33 - rawResistance; // Invert: 1->32, 32->1
Resistance = newValue.at(5);
emit resistanceRead(Resistance.value());
m_pelotonResistance = Resistance.value();
// Parse cadence from 5th byte (index 4) and multiply by 2
uint8_t rawCadence = newValue.at(4);
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = rawCadence * 2;
}
qDebug() << QStringLiteral("Raw resistance: ") + QString::number(rawResistance) + QStringLiteral(", Inverted resistance: ") + QString::number(Resistance.value()) + QStringLiteral(", Raw cadence: ") + QString::number(rawCadence) + QStringLiteral(", Final cadence: ") + QString::number(Cadence.value());
qDebug() << QStringLiteral("Current resistance: ") + QString::number(Resistance.value());
}
if (newValue.length() != 10 || newValue.at(2) != 0x31) {
if (newValue.length() != 10 || newValue.at(2) != 0x30) {
return;
}
uint16_t distanceData = (newValue.at(7) << 8) | ((uint8_t)newValue.at(8));
uint16_t distanceData = (newValue.at(3) << 8) | ((uint8_t)newValue.at(4));
double distance = ((double)distanceData);
// Calculate speed using the same method as echelon bike
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = 0.37497622 * ((double)Cadence.value());
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
if(distance != lastDistance) {
if(lastDistance != 0) {
double deltaDistance = distance - lastDistance;
double deltaTime = fabs(now.msecsTo(lastTS));
double timeHours = deltaTime / (1000.0 * 60.0 * 60.0);
double k = 0.005333;
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = (deltaDistance *k) / timeHours; // Speed in distance units per hour
} else {
Speed = metric::calculateSpeedFromPower(
watts(), Inclination.value(), Speed.value(),
fabs(now.msecsTo(Speed.lastChanged()) / 1000.0), this->speedLimit());
}
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = Speed.value() / 0.37497622;
}
}
lastDistance = distance;
lastTS = now;
qDebug() << "lastDistance" << lastDistance << "lastTS" << lastTS;
}
if (watts())
@@ -214,7 +214,7 @@ void apexbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
@@ -411,98 +411,7 @@ bool apexbike::connected() {
return m_control->state() == QLowEnergyController::DiscoveredState;
}
uint16_t apexbike::watts() {
double resistance = Resistance.value();
double cadence = Cadence.value();
if (cadence <= 0 || resistance <= 0) {
return 0;
}
// Power table based on user-provided data
// Format: resistance level (1-19), RPM (10-150 in steps of 10), power (watts)
static const int powerTable[19][15] = {
// Resistance 1: RPM 10,20,30,40,50,60,70,80,90,100,110,120,130,140,150
{12, 24, 36, 48, 61, 73, 85, 97, 109, 121, 133, 145, 157, 170, 182},
// Resistance 2
{13, 27, 40, 53, 67, 80, 93, 107, 120, 133, 147, 160, 173, 187, 200},
// Resistance 3
{15, 29, 44, 58, 73, 87, 102, 117, 131, 146, 160, 175, 189, 204, 219},
// Resistance 4
{16, 32, 48, 64, 80, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239},
// Resistance 5
{17, 34, 51, 68, 85, 102, 118, 135, 152, 169, 186, 203, 220, 237, 254},
// Resistance 6
{18, 37, 55, 74, 92, 110, 129, 147, 165, 184, 202, 221, 239, 257, 276},
// Resistance 7
{19, 39, 58, 77, 97, 116, 136, 155, 174, 194, 213, 232, 252, 271, 291},
// Resistance 8
{21, 42, 62, 83, 104, 125, 146, 166, 187, 208, 229, 250, 271, 291, 312},
// Resistance 9
{22, 44, 66, 88, 110, 132, 154, 176, 198, 220, 242, 264, 286, 308, 330},
// Resistance 10
{23, 46, 69, 92, 116, 139, 162, 185, 208, 231, 254, 277, 300, 324, 347},
// Resistance 11
{24, 49, 73, 98, 122, 146, 171, 195, 219, 244, 268, 293, 317, 341, 366},
// Resistance 12
{26, 51, 77, 102, 128, 153, 179, 204, 230, 255, 281, 307, 332, 358, 383},
// Resistance 13
{27, 54, 80, 107, 134, 161, 188, 214, 241, 268, 295, 322, 348, 375, 402},
// Resistance 14
{28, 56, 83, 111, 139, 167, 195, 222, 250, 278, 306, 334, 362, 389, 417},
// Resistance 15
{29, 58, 87, 117, 146, 175, 204, 233, 262, 292, 321, 350, 379, 408, 437},
// Resistance 16
{30, 61, 91, 121, 152, 182, 212, 242, 273, 303, 333, 364, 394, 424, 455},
// Resistance 17
{32, 63, 95, 126, 158, 189, 221, 253, 284, 316, 347, 379, 410, 442, 473},
// Resistance 18
{33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495},
// Resistance 19
{34, 68, 102, 136, 171, 205, 239, 273, 307, 341, 375, 409, 443, 478, 512}
};
// Clamp resistance to valid range (1-19)
int res = qMax(1, qMin(19, (int)qRound(resistance)));
// Convert to array index (0-18)
int resIndex = res - 1;
// RPM ranges from 10 to 150 in steps of 10
// Find the two closest RPM values for interpolation
double rpm = qMax(1.0, cadence); // Ensure RPM is at least 1
if (rpm <= 10.0) {
// Below minimum RPM, extrapolate from first data point
double factor = rpm / 10.0;
return (uint16_t)qMax(0.0, powerTable[resIndex][0] * factor);
}
if (rpm >= 150.0) {
// Above maximum RPM, extrapolate from last data point
double factor = rpm / 150.0;
return (uint16_t)qMax(0.0, powerTable[resIndex][14] * factor);
}
// Find the two RPM values to interpolate between
// RPM values are: 10, 20, 30, ..., 150 (indices 0-14)
int lowerRpmIndex = ((int)rpm - 1) / 10; // Convert RPM to array index
if (lowerRpmIndex > 13) lowerRpmIndex = 13; // Ensure we don't go out of bounds
int upperRpmIndex = lowerRpmIndex + 1;
double lowerRpm = (lowerRpmIndex + 1) * 10.0; // Convert index back to RPM
double upperRpm = (upperRpmIndex + 1) * 10.0;
int lowerPower = powerTable[resIndex][lowerRpmIndex];
int upperPower = powerTable[resIndex][upperRpmIndex];
// Linear interpolation between the two power values
double ratio = (rpm - lowerRpm) / (upperRpm - lowerRpm);
double interpolatedPower = lowerPower + ratio * (upperPower - lowerPower);
return (uint16_t)qMax(0.0, interpolatedPower);
}
uint16_t apexbike::watts() { return wattFromHR(true); }
void apexbike::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;

View File

@@ -2,7 +2,6 @@
#include "devices/bike.h"
#include "qdebugfixup.h"
#include "homeform.h"
#include "virtualgearingdevice.h"
#include <QSettings>
bike::bike() { elapsed.setType(metric::METRIC_ELAPSED); }
@@ -63,33 +62,17 @@ void bike::changePower(int32_t power) {
return;
}
QSettings settings;
bool power_sensor = !settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"));
double erg_filter_upper =
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
double erg_filter_lower =
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
// Apply bike power offset
int bike_power_offset = settings.value(QZSettings::bike_power_offset, QZSettings::default_bike_power_offset).toInt();
power += bike_power_offset;
qDebug() << QStringLiteral("changePower: original power with offset applied: ") + QString::number(power) + QStringLiteral(" (offset: ") + QString::number(bike_power_offset) + QStringLiteral(")");
requestPower = power; // used by some bikes that have ERG mode builtin
if(power_sensor && ergModeSupported && m_rawWatt.value() > 0 && m_watt.value() > 0 && fabs(requestPower - m_watt.average5s()) < qMax(erg_filter_upper, erg_filter_lower)) {
qDebug() << "applying delta watt to power request m_rawWatt" << m_rawWatt.average5s() << "watt" << m_watt.average5s() << "req" << requestPower;
// the concept here is to trying to add or decrease the delta from the power sensor
requestPower += (requestPower - m_watt.average5s());
}
QSettings settings;
bool force_resistance =
settings.value(QZSettings::virtualbike_forceresistance, QZSettings::default_virtualbike_forceresistance)
.toBool();
// bool erg_mode = settings.value(QZSettings::zwift_erg, QZSettings::default_zwift_erg).toBool(); //Not used
// anywhere in code
double erg_filter_upper =
settings.value(QZSettings::zwift_erg_filter, QZSettings::default_zwift_erg_filter).toDouble();
double erg_filter_lower =
settings.value(QZSettings::zwift_erg_filter_down, QZSettings::default_zwift_erg_filter_down).toDouble();
double deltaDown = wattsMetric().value() - ((double)power);
double deltaUp = ((double)power) - wattsMetric().value();
qDebug() << QStringLiteral("filter ") + QString::number(deltaUp) + " " + QString::number(deltaDown) + " " +
@@ -113,84 +96,30 @@ double bike::gears() {
}
return m_gears + gears_offset;
}
void bike::setGears(double gears) {
QSettings settings;
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
double gears_offset = settings.value(QZSettings::gears_offset, QZSettings::default_gears_offset).toDouble();
gears -= gears_offset;
qDebug() << "setGears" << gears;
// Gear boundary handling with smart clamping logic:
// - If we're trying to set a gear outside valid range AND we're already at a valid gear,
// reject the change (normal case: user at gear 1 tries to go to 0.5, should fail)
// - If we're trying to set a gear outside valid range BUT we're currently below minimum,
// clamp to valid range (startup case: system starts at 0, first gearUp with 0.5 gain
// goes to 0.5, should be clamped to 1 to allow the system to reach valid state)
// This prevents the system from getting stuck below minGears due to fractional gains
// while preserving normal boundary rejection behavior for users at valid gear positions
if(gears_zwift_ratio && (gears > 24 || gears < 1)) {
if(gears > 24) {
if(m_gears >= 24) {
qDebug() << "new gear value ignored - already at zwift ratio maximum: 24";
emit gearFailedUp();
return;
} else {
qDebug() << "gear value clamped to zwift ratio maximum: 24";
gears = 24;
emit gearFailedUp();
}
} else {
if(m_gears >= 1) {
qDebug() << "new gear value ignored - already at zwift ratio minimum: 1";
emit gearFailedDown();
return;
} else {
qDebug() << "gear value clamped to zwift ratio minimum: 1";
gears = 1;
emit gearFailedDown();
}
}
qDebug() << "new gear value ignored because of gears_zwift_ratio setting!";
return;
}
if(gears > maxGears()) {
if(m_gears >= maxGears()) {
qDebug() << "new gear value ignored - already at maxGears" << maxGears();
emit gearFailedUp();
return;
} else {
qDebug() << "gear value clamped to maxGears" << maxGears();
gears = maxGears();
emit gearFailedUp();
}
qDebug() << "new gear value ignored because of maxGears" << maxGears();
return;
}
if(gears < minGears()) {
if(m_gears >= minGears()) {
qDebug() << "new gear value ignored - already at or above minGears" << minGears();
emit gearFailedDown();
return;
} else {
qDebug() << "gear value clamped to minGears" << minGears();
gears = minGears();
emit gearFailedDown();
}
qDebug() << "new gear value ignored because of minGears" << minGears();
return;
}
if(m_gears > gears) {
emit gearOkDown();
} else {
emit gearOkUp();
}
m_gears = gears;
if(homeform::singleton()) {
homeform::singleton()->updateGearsValue();
}
if (settings.value(QZSettings::gears_restore_value, QZSettings::default_gears_restore_value).toBool())
settings.setValue(QZSettings::gears_current_value, m_gears);
if (lastRawRequestedResistanceValue != -1) {
changeResistance(lastRawRequestedResistanceValue);
}
@@ -226,7 +155,6 @@ void bike::clearStats() {
m_jouls.clear(true);
elevationAcc = 0;
m_watt.clear(false);
m_rawWatt.clear(false);
WeightLoss.clear(false);
RequestedPelotonResistance.clear(false);
@@ -254,7 +182,6 @@ void bike::setPaused(bool p) {
Heart.setPaused(p);
m_jouls.setPaused(p);
m_watt.setPaused(p);
m_rawWatt.setPaused(p);
WeightLoss.setPaused(p);
m_pelotonResistance.setPaused(p);
Cadence.setPaused(p);
@@ -280,7 +207,6 @@ void bike::setLap() {
Heart.setLap(false);
m_jouls.setLap(true);
m_watt.setLap(false);
m_rawWatt.setLap(false);
WeightLoss.setLap(false);
WattKg.setLap(false);
@@ -467,81 +393,7 @@ double bike::gearsZwiftRatio() {
case 23:
return 5.14;
case 24:
return 5.49;
return 5.49;
}
return 1;
}
void bike::gearUp() {
QSettings settings;
// Check if virtual gearing device is enabled
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
#ifdef Q_OS_ANDROID
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
if (vgd) {
// Check if accessibility service is enabled
if (!vgd->isAccessibilityServiceEnabled()) {
static bool warned = false;
if (!warned) {
qDebug() << "bike::gearUp() - VirtualGearingService not enabled in accessibility settings";
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
warned = true;
}
} else if (vgd->isServiceRunning()) {
qDebug() << "bike::gearUp() - Using virtual gearing device";
QString coordinates = vgd->getShiftUpCoordinates();
vgd->simulateShiftUp();
// Show toast with coordinates
homeform::singleton()->setToastRequested("Virtual Gear Up → " + coordinates);
return;
} else {
qDebug() << "bike::gearUp() - Virtual gearing service not running, falling back to normal gearing";
}
}
#endif
}
// Normal gearing logic
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() + (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}
void bike::gearDown() {
QSettings settings;
// Check if virtual gearing device is enabled
if (settings.value(QZSettings::virtual_gearing_device, QZSettings::default_virtual_gearing_device).toBool()) {
#ifdef Q_OS_ANDROID
VirtualGearingDevice* vgd = VirtualGearingDevice::instance();
if (vgd) {
// Check if accessibility service is enabled
if (!vgd->isAccessibilityServiceEnabled()) {
static bool warned = false;
if (!warned) {
qDebug() << "bike::gearDown() - VirtualGearingService not enabled in accessibility settings";
qDebug() << "Please enable the Virtual Gearing Service in Android Accessibility Settings";
warned = true;
}
} else if (vgd->isServiceRunning()) {
qDebug() << "bike::gearDown() - Using virtual gearing device";
QString coordinates = vgd->getShiftDownCoordinates();
vgd->simulateShiftDown();
// Show toast with coordinates
homeform::singleton()->setToastRequested("Virtual Gear Down → " + coordinates);
return;
} else {
qDebug() << "bike::gearDown() - Virtual gearing service not running, falling back to normal gearing";
}
}
#endif
}
// Normal gearing logic
bool gears_zwift_ratio = settings.value(QZSettings::gears_zwift_ratio, QZSettings::default_gears_zwift_ratio).toBool();
setGears(gears() - (gears_zwift_ratio ? 1 :
settings.value(QZSettings::gears_gain, QZSettings::default_gears_gain).toDouble()));
}

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