Compare commits

..

9 Commits

Author SHA1 Message Date
Roberto Viola
3a250fe3a3 Enhance Virtual Gearing: Android-only visibility and coordinate feedback
- Make Virtual Gearing Device visible only on Android platform
- Add customizable coordinate settings for different cycling apps (MyWhoosh, IndieVelo, etc.)
- Implement app selection ComboBox with auto-populated default coordinates
- Add real-time coordinate customization UI with percentage-based values
- Show toast messages with exact touch coordinates when gear changes occur
- Integrate settings bridge between Java and C++ for dynamic coordinate access
- Add debug logging for screen dimensions and coordinate calculations
- Replace hardcoded coordinates with user-configurable system

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 08:10:00 +02:00
Roberto Viola
094d2c88cb adding new files 2025-09-24 11:58:43 +02:00
Roberto Viola
7f3dda70fd Update settings.qml 2025-09-22 13:30:17 +02:00
Roberto Viola
79d96ba182 fix 2025-09-22 08:29:07 +02:00
Roberto Viola
5ec861dab3 Merge branch 'virtualgear_device_android' of https://github.com/cagnulein/qdomyos-zwift into virtualgear_device_android 2025-09-22 04:57:15 +02:00
Roberto Viola
f90edbd632 log 2025-09-22 04:57:00 +02:00
Roberto Viola
621ed69627 Merge branch 'master' into virtualgear_device_android 2025-09-21 14:33:30 +02:00
Roberto Viola
b754b7f773 other files 2025-09-21 14:04:07 +02:00
Roberto Viola
efb9dfbdb1 virtual gear device on android 2025-09-21 14:01:01 +02:00
278 changed files with 4959 additions and 18266 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -371,4 +371,5 @@ The ProForm 995i implementation serves as the reference example:
## Additional Memories
- When adding a new setting in QML (setting-tiles.qml), you must:
* Add the property at the END of the properties list
* Add the property at the END of the properties list
- #usa le qdebug invece che le emit debug

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 70;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -130,7 +130,6 @@
87097D31275EA9AF0020EE6F /* moc_sportsplusbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */; };
870A5DB32CEF8FB100839641 /* moc_technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */; };
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870A5DB42CEF8FD200839641 /* technogymbike.cpp */; };
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870C72632E91565E00DC8A84 /* ios_liveactivity.mm */; };
8710706C29C48AEA0094D0F3 /* handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706B29C48AEA0094D0F3 /* handleurl.cpp */; };
8710706E29C48AF30094D0F3 /* moc_handleurl.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */; };
8710707329C4A5E70094D0F3 /* GarminConnect.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 8710707229C4A5E70094D0F3 /* GarminConnect.swift */; };
@@ -523,8 +522,6 @@
87C5F0D926285E7E0067A1B5 /* moc_mimeattachment.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C5F0CE26285E7E0067A1B5 /* moc_mimeattachment.cpp */; };
87C7074227E4CF5300E79C46 /* moc_keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */; };
87C7074327E4CF5900E79C46 /* keepbike.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87C7073F27E4CF4500E79C46 /* keepbike.cpp */; };
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */; };
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */; };
87CC3B9D25A08812001EC5A8 /* moc_domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */; };
87CC3B9E25A08812001EC5A8 /* moc_elliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */; };
87CC3BA325A0885F001EC5A8 /* domyoselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */; };
@@ -600,12 +597,6 @@
87EBB2AB2D39214E00348B15 /* moc_workoutmodel.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EBB2A12D39214E00348B15 /* moc_workoutmodel.cpp */; };
87EFB56E25BD703D0039DD5A /* proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */; };
87EFB57025BD704A0039DD5A /* moc_proformtreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */; };
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87EFC5652E918D35005BB573 /* WidgetKit.framework */; };
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 87FA94662B6B89FD00B6AB9A /* SwiftUI.framework */; };
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */; };
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */; };
87EFE45927A518F5006EA1C3 /* nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */; };
87EFE45B27A51901006EA1C3 /* moc_nautiluselliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */; };
87F02E4029178524000DB52C /* octaneelliptical.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87F02E3E29178523000DB52C /* octaneelliptical.cpp */; };
@@ -713,13 +704,6 @@
remoteGlobalIDString = 876E4E102594747F00BD5714;
remoteInfo = watchkit;
};
87EFC5742E918D38005BB573 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 6DB9C3763D02B1415CD9D565 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 87EFC5632E918D35005BB573;
remoteInfo = QZWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -745,17 +729,6 @@
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
87EFC5762E918D38005BB573 /* QZWidgetExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
99542592E9780B9225F24AA8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -1001,8 +974,6 @@
87097D30275EA9AE0020EE6F /* moc_sportsplusbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_sportsplusbike.cpp; sourceTree = "<group>"; };
870A5DB22CEF8FB100839641 /* moc_technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_technogymbike.cpp; sourceTree = "<group>"; };
870A5DB42CEF8FD200839641 /* technogymbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = technogymbike.cpp; path = ../src/devices/technogymbike/technogymbike.cpp; sourceTree = SOURCE_ROOT; };
870C72622E91565E00DC8A84 /* ios_liveactivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_liveactivity.h; path = ../src/ios/ios_liveactivity.h; sourceTree = SOURCE_ROOT; };
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_liveactivity.mm; path = ../src/ios/ios_liveactivity.mm; sourceTree = SOURCE_ROOT; };
8710706A29C48AE90094D0F3 /* handleurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = handleurl.h; path = ../src/handleurl.h; sourceTree = "<group>"; };
8710706B29C48AEA0094D0F3 /* handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = handleurl.cpp; path = ../src/handleurl.cpp; sourceTree = "<group>"; };
8710706D29C48AF30094D0F3 /* moc_handleurl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_handleurl.cpp; sourceTree = "<group>"; };
@@ -1615,9 +1586,6 @@
87C7073F27E4CF4500E79C46 /* keepbike.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = keepbike.cpp; path = ../src/devices/keepbike/keepbike.cpp; sourceTree = "<group>"; };
87C7074027E4CF4500E79C46 /* keepbike.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = keepbike.h; path = ../src/devices/keepbike/keepbike.h; sourceTree = "<group>"; };
87C7074127E4CF5300E79C46 /* moc_keepbike.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_keepbike.cpp; sourceTree = "<group>"; };
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = garminconnect.h; path = ../src/garminconnect.h; sourceTree = SOURCE_ROOT; };
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = garminconnect.cpp; path = ../src/garminconnect.cpp; sourceTree = SOURCE_ROOT; };
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_garminconnect.cpp; sourceTree = "<group>"; };
87CC3B9B25A08812001EC5A8 /* moc_domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_domyoselliptical.cpp; sourceTree = "<group>"; };
87CC3B9C25A08812001EC5A8 /* moc_elliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_elliptical.cpp; sourceTree = "<group>"; };
87CC3B9F25A0885D001EC5A8 /* domyoselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = domyoselliptical.cpp; path = ../src/devices/domyoselliptical/domyoselliptical.cpp; sourceTree = "<group>"; };
@@ -1725,11 +1693,6 @@
87EFB56C25BD703C0039DD5A /* proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = proformtreadmill.cpp; path = ../src/devices/proformtreadmill/proformtreadmill.cpp; sourceTree = "<group>"; };
87EFB56D25BD703C0039DD5A /* proformtreadmill.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = proformtreadmill.h; path = ../src/devices/proformtreadmill/proformtreadmill.h; sourceTree = "<group>"; };
87EFB56F25BD704A0039DD5A /* moc_proformtreadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_proformtreadmill.cpp; sourceTree = "<group>"; };
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QZWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
87EFC5652E918D35005BB573 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveActivityBridge.swift; path = ../src/ios/LiveActivityBridge.swift; sourceTree = SOURCE_ROOT; };
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QZWidgetExtension.entitlements; sourceTree = "<group>"; };
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QZWorkoutAttributes.swift; path = QZWidget/QZWorkoutAttributes.swift; sourceTree = "<group>"; };
87EFE45727A518F5006EA1C3 /* nautiluselliptical.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = nautiluselliptical.h; path = ../src/devices/nautiluselliptical/nautiluselliptical.h; sourceTree = "<group>"; };
87EFE45827A518F5006EA1C3 /* nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = nautiluselliptical.cpp; path = ../src/devices/nautiluselliptical/nautiluselliptical.cpp; sourceTree = "<group>"; };
87EFE45A27A51900006EA1C3 /* moc_nautiluselliptical.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_nautiluselliptical.cpp; sourceTree = "<group>"; };
@@ -1966,20 +1929,6 @@
FF5BDAB0076F3391B219EA52 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = "<absolute>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
87EFC5682E918D35005BB573 /* QZWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (87EFC5772E918D38005BB573 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = QZWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
876E4E172594748000BD5714 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -1988,15 +1937,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5612E918D35005BB573 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5672E918D35005BB573 /* SwiftUI.framework in Frameworks */,
87EFC5662E918D35005BB573 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D1C883685E82D5676953459A /* Link Binary With Libraries */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -2338,13 +2278,6 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
87CBCF0F2EFAA2F8004F5ECE /* garminconnect.h */,
87CBCF102EFAA2F8004F5ECE /* garminconnect.cpp */,
87CBCF112EFAA2F8004F5ECE /* moc_garminconnect.cpp */,
87EFC58E2E919DB7005BB573 /* QZWorkoutAttributes.swift */,
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
@@ -3198,7 +3131,6 @@
4D765E1B1EA6C757220C63E7 /* CoreFoundation.framework */,
FCC237CA5AD60B9BA4447615 /* Foundation.framework */,
344F66310C19536DB4886D8F /* qtpcre2 */,
87EFC5652E918D35005BB573 /* WidgetKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -3461,7 +3393,6 @@
E8C543AB96796ECAA2E65C57 /* qdomyoszwift */ = {
isa = PBXGroup;
children = (
87EFC57E2E919C98005BB573 /* QZWidgetExtension.entitlements */,
8768C8CE2BBC12170099DBE1 /* adb */,
87BAC3BE2BA497160003E925 /* PrivacyInfo.xcprivacy */,
8745B2752AFCB4A300991A39 /* android */,
@@ -3472,7 +3403,6 @@
74B182DB50CB5611B5C1C297 /* Supporting Files */,
876E4E122594747F00BD5714 /* watchkit */,
876E4E1E2594748000BD5714 /* watchkit Extension */,
87EFC5682E918D35005BB573 /* QZWidget */,
AF39DD055C3EF8226FBE929D /* Frameworks */,
858FCAB0EB1F29CF8B07677C /* Bundle Data */,
FE0A091FDBFB3E9C31B7A1BD /* Products */,
@@ -3487,7 +3417,6 @@
040B10E2EF2CEF79F2205FE2 /* qdomyoszwift.app */,
876E4E112594747F00BD5714 /* watchkit.app */,
876E4E1A2594748000BD5714 /* watchkit Extension.appex */,
87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -3504,13 +3433,11 @@
30414803F31797EB689AE508 /* Copy Bundle Resources */,
99542592E9780B9225F24AA8 /* Embed Frameworks */,
876E4E332594748100BD5714 /* Embed Watch Content */,
87EFC57B2E918D38005BB573 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
876E4E312594748100BD5714 /* PBXTargetDependency */,
87EFC5752E918D38005BB573 /* PBXTargetDependency */,
);
name = qdomyoszwift;
packageProductDependencies = (
@@ -3556,28 +3483,6 @@
productReference = 876E4E1A2594748000BD5714 /* watchkit Extension.appex */;
productType = "com.apple.product-type.watchkit2-extension";
};
87EFC5632E918D35005BB573 /* QZWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */;
buildPhases = (
87EFC5602E918D35005BB573 /* Sources */,
87EFC5612E918D35005BB573 /* Frameworks */,
87EFC5622E918D35005BB573 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
87EFC5682E918D35005BB573 /* QZWidget */,
);
name = QZWidgetExtension;
packageProductDependencies = (
);
productName = QZWidgetExtension;
productReference = 87EFC5642E918D35005BB573 /* QZWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -3585,7 +3490,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
LastSwiftUpdateCheck = 2600;
LastSwiftUpdateCheck = 1220;
TargetAttributes = {
799833E5566DEFFC37E4BF1E = {
DevelopmentTeam = 6335M7T29D;
@@ -3601,9 +3506,6 @@
DevelopmentTeam = 6335M7T29D;
ProvisioningStyle = Automatic;
};
87EFC5632E918D35005BB573 = {
CreatedOnToolsVersion = 26.0.1;
};
};
};
buildConfigurationList = DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */;
@@ -3625,7 +3527,6 @@
799833E5566DEFFC37E4BF1E /* qdomyoszwift */,
876E4E102594747F00BD5714 /* watchkit */,
876E4E192594748000BD5714 /* watchkit Extension */,
87EFC5632E918D35005BB573 /* QZWidgetExtension */,
);
};
/* End PBXProject section */
@@ -3672,13 +3573,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5622E918D35005BB573 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -3696,19 +3590,10 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
87EFC5602E918D35005BB573 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
87EFC5902E919DB7005BB573 /* QZWorkoutAttributes.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F7E50F631C51CD5B5DC0BC43 /* Compile Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
@@ -3957,7 +3842,6 @@
87943AB429E0215D007575F2 /* localipaddress.cpp in Compile Sources */,
87EB917627EE5FB3002535E1 /* nautilusbike.cpp in Compile Sources */,
ACB47DC464A2BC9D39C544AD /* gpx.cpp in Compile Sources */,
87EFC57D2E918DAA005BB573 /* LiveActivityBridge.swift in Compile Sources */,
6361329E515248BB41640C07 /* homeform.cpp in Compile Sources */,
87A18F072660D5C1002D7C96 /* ftmsrower.cpp in Compile Sources */,
87C5F0D026285E7E0067A1B5 /* moc_smtpclient.cpp in Compile Sources */,
@@ -4040,8 +3924,6 @@
872088EB2CE6543C008C2C17 /* moc_mqttpublisher.cpp in Compile Sources */,
872088EC2CE6543C008C2C17 /* moc_qmqttclient.cpp in Compile Sources */,
875CA94C2D130F8100667EE6 /* moc_osc.cpp in Compile Sources */,
87CBCF122EFAA2F8004F5ECE /* garminconnect.cpp in Compile Sources */,
87CBCF132EFAA2F8004F5ECE /* moc_garminconnect.cpp in Compile Sources */,
872088ED2CE6543C008C2C17 /* moc_qmqttmessage.cpp in Compile Sources */,
872088EE2CE6543C008C2C17 /* moc_qmqttsubscription.cpp in Compile Sources */,
872088EF2CE6543C008C2C17 /* moc_qmqttconnection_p.cpp in Compile Sources */,
@@ -4071,7 +3953,6 @@
873824AE27E64706004F1B46 /* moc_browser.cpp in Compile Sources */,
8738249727E646E3004F1B46 /* characteristicnotifier2a53.cpp in Compile Sources */,
876C64712E74139F00F1BEC0 /* moc_fitbackupwriter.cpp in Compile Sources */,
87EFC58F2E919DB7005BB573 /* QZWorkoutAttributes.swift in Compile Sources */,
876C64722E74139F00F1BEC0 /* fitbackupwriter.cpp in Compile Sources */,
DF373364C5474D877506CB26 /* FitMesg.mm in Compile Sources */,
87FE06812D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp in Compile Sources */,
@@ -4251,11 +4132,6 @@
target = 876E4E102594747F00BD5714 /* watchkit */;
targetProxy = 876E4E302594748100BD5714 /* PBXContainerItemProxy */;
};
87EFC5752E918D38005BB573 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 87EFC5632E918D35005BB573 /* QZWidgetExtension */;
targetProxy = 87EFC5742E918D38005BB573 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -4579,7 +4455,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1165;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4757,7 +4633,6 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = "";
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
@@ -4780,7 +4655,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1165;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -4961,7 +4836,6 @@
QMAKE_PKGINFO_TYPEINFO = "????";
QMAKE_SHORT_VERSION = 1.7;
QT_LIBRARY_SUFFIX = _debug;
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
@@ -5017,7 +4891,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1165;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -5113,7 +4987,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1165;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5205,7 +5079,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1166;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5321,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
CURRENT_PROJECT_VERSION = 1166;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5393,184 +5267,6 @@
};
name = Release;
};
87EFC5792E918D38005BB573 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
87EFC57A2E918D38005BB573 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1237;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QZWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QZWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
SwiftUI,
"-framework",
WidgetKit,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cagnulein.qdomyoszwift.QZWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -5601,15 +5297,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
87EFC5782E918D38005BB573 /* Build configuration list for PBXNativeTarget "QZWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
87EFC5792E918D38005BB573 /* Debug */,
87EFC57A2E918D38005BB573 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
DAC4C1AA5EDEA1C85E9CA5E6 /* Build configuration list for PBXProject "qdomyoszwift" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,58 +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 {
anchors.fill: parent
height: parent.height
width: parent.width
visible: true
WebView {
anchors.fill: parent
height: parent.height
width: parent.width
visible: !rootItem.generalPopupVisible
url: rootItem.getIntervalsICUAuthUrl
}
Popup {
id: popupIntervalsICUConnectedWeb
parent: Overlay.overlay
enabled: rootItem.generalPopupVisible
onEnabledChanged: { if(rootItem.generalPopupVisible) popupIntervalsICUConnectedWeb.open() }
onClosed: { rootItem.generalPopupVisible = false; }
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
width: 380
height: 120
modal: true
focus: true
palette.text: "white"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition
{
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
}
exit: Transition
{
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: 370
height: 120
text: qsTr("Your Intervals.icu account is now connected!<br><br>When you will press STOP on QZ a file<br>will be automatically uploaded to Intervals.icu!")
}
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.19" android:versionCode="1234" android:installLocation="auto">
<manifest package="org.cagnulen.qdomyoszwift" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionName="2.20.11" android:versionCode="1155" 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 -->
@@ -106,6 +106,16 @@
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" />

View File

@@ -0,0 +1,4 @@
<?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

@@ -0,0 +1,10 @@
<?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

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

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

View File

@@ -211,9 +211,8 @@ public class SDMChannelController {
payload[1] = (byte) (((lastTime % 256000) / 5) & 0xFF);
payload[2] = (byte) ((lastTime % 256000) / 1000);
payload[3] = (byte) 0x00;
int speedFixed = (int) Math.round(speedM_s * 256.0);
payload[4] = (byte) (speedFixed & 0xFF); // low byte
payload[5] = (byte) ((speedFixed >> 8) & 0xFF); // high byte
payload[4] = (byte) speedM_s;
payload[5] = (byte) ((speedM_s - (double)((int)speedM_s)) / (1.0/256.0));
payload[6] = (byte) stride_count++; // bad but it works on zwift
payload[7] = (byte) ((double)deltaTime * 0.03125);

View File

@@ -43,11 +43,7 @@ public class Usbserial {
static int lastReadLen = 0;
public static void open(Context context) {
open(context, 2400); // Default baud rate for Computrainer
}
public static void open(Context context, int baudRate) {
QLog.d("QZ","UsbSerial open with baud rate: " + baudRate);
QLog.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);
@@ -102,12 +98,13 @@ public class Usbserial {
port = driver.getPorts().get(0); // Most devices have just one port (port 0)
try {
port.open(connection);
port.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
QLog.d("QZ","UsbSerial port opened successfully at " + baudRate + " baud");
port.setParameters(2400, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
}
catch (IOException e) {
QLog.d("QZ","UsbSerial port open failed: " + e.getMessage());
// Do something here
}
QLog.d("QZ","UsbSerial port opened");
}
public static void write (byte[] bytes) {

View File

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

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

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

View File

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

View File

@@ -8,7 +8,7 @@ int CharacteristicNotifier2A63::notify(QByteArray &value) {
if (normalizeWattage < 0)
normalizeWattage = 0;
if (Bike->deviceType() == BIKE) {
if (Bike->deviceType() == bluetoothdevice::BIKE) {
/*
// set measurement
measurement[2] = power & 0xFF;

View File

@@ -7,9 +7,9 @@ CharacteristicNotifier2ACD::CharacteristicNotifier2ACD(bluetoothdevice *Bike, QO
: CharacteristicNotifier(0x2acd, parent), Bike(Bike) {}
int CharacteristicNotifier2ACD::notify(QByteArray &value) {
BLUETOOTH_TYPE dt = Bike->deviceType();
if (dt == TREADMILL || dt == ELLIPTICAL) {
value.append(0x0E); // Inclination, distance and average speed available
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)
@@ -19,26 +19,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
QByteArray speedBytes;
speedBytes.append(b);
speedBytes.append(a);
// average speed in 0.01 km/h (distance from startup / elapsed time)
double elapsed_time_seconds = 0.0;
uint16_t averageSpeed = 0;
{
QTime sessionElapsedTime = Bike->elapsedTime();
elapsed_time_seconds = (double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
if (elapsed_time_seconds > 0) {
double distance_m = Bike->odometerFromStartup() * 1000.0;
double avg_kmh = (distance_m * 3.6) / elapsed_time_seconds;
averageSpeed = (uint16_t)qRound(avg_kmh * 100.0);
}
}
QByteArray averageSpeedBytes;
averageSpeedBytes.append(static_cast<char>(averageSpeed & 0xFF));
averageSpeedBytes.append(static_cast<char>((averageSpeed >> 8) & 0xFF));
// peloton wants the distance from the qz startup to handle stacked classes
// https://github.com/cagnulein/qdomyos-zwift/issues/2018
uint32_t normalizeDistance = (uint32_t)qRound(Bike->odometerFromStartup() * 1000);
@@ -65,7 +46,7 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclination /= gain;
}
if (dt == TREADMILL)
if (dt == bluetoothdevice::TREADMILL)
normalizeIncline = (uint32_t)qRound(inclination * 10);
a = (normalizeIncline >> 8) & 0XFF;
b = normalizeIncline & 0XFF;
@@ -73,22 +54,28 @@ int CharacteristicNotifier2ACD::notify(QByteArray &value) {
inclineBytes.append(b);
inclineBytes.append(a);
double ramp = 0;
if (dt == TREADMILL)
if (dt == bluetoothdevice::TREADMILL)
ramp = qRadiansToDegrees(qAtan(inclination / 100));
int16_t normalizeRamp = (int16_t)qRound(ramp * 10);
int16_t normalizeRamp = (int32_t)qRound(ramp * 10);
a = (normalizeRamp >> 8) & 0XFF;
b = normalizeRamp & 0XFF;
QByteArray rampBytes;
rampBytes.append(b);
rampBytes.append(a);
// Get session elapsed time - makes Runna calculations work
QTime sessionElapsedTime = Bike->elapsedTime();
double elapsed_time_seconds =
(double)sessionElapsedTime.hour() * 3600.0 +
(double)sessionElapsedTime.minute() * 60.0 +
(double)sessionElapsedTime.second() +
(double)sessionElapsedTime.msec() / 1000.0;
uint16_t ftms_elapsed_time_field = (uint16_t)qRound(elapsed_time_seconds);
QByteArray elapsedBytes;
elapsedBytes.append(static_cast<char>(ftms_elapsed_time_field & 0xFF));
elapsedBytes.append(static_cast<char>((ftms_elapsed_time_field >> 8) & 0xFF));
value.append(speedBytes); // Actual value.
value.append(averageSpeedBytes); // Average speed value.
value.append(distanceBytes); // Actual value.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@
#include "devices/bluetoothdevice.h"
#include "virtualdevices/virtualbike.h"
#include <QObject>
#include <QElapsedTimer>
class bike : public bluetoothdevice {
@@ -32,7 +31,7 @@ class bike : public bluetoothdevice {
virtual resistance_t resistanceFromPowerRequest(uint16_t power);
virtual uint16_t powerFromResistanceRequest(resistance_t requestResistance);
virtual bool ergManagedBySS2K() { return false; }
BLUETOOTH_TYPE deviceType() override;
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
metric pelotonResistance();
void clearStats() override;
void setLap() override;
@@ -52,7 +51,6 @@ class bike : public bluetoothdevice {
*/
metric currentSteeringAngle() { return m_steeringAngle; }
virtual bool inclinationAvailableByHardware();
virtual bool inclinationAvailableBySoftware();
bool ergModeSupportedAvailableByHardware() { return ergModeSupported; }
virtual bool ergModeSupportedAvailableBySoftware() { return ergModeSupported; }
@@ -66,18 +64,8 @@ class bike : public bluetoothdevice {
void changeInclination(double grade, double percentage) override;
virtual void changeSteeringAngle(double angle) { m_steeringAngle = angle; }
virtual void resistanceFromFTMSAccessory(resistance_t res) { Q_UNUSED(res); }
void gearUp() {
QSettings settings;
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 gearDown() {
QSettings settings;
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 gearUp();
void gearDown();
Q_SIGNALS:
void bikeStarted();
@@ -112,25 +100,6 @@ class bike : public bluetoothdevice {
double m_speedLimit = 0;
// Sim mode support: convert inclination to power for devices without native inclination
bool m_slopeControlEnabled = false;
double m_currentSlopePercent = 0.0;
int m_lastSlopeTargetPower = -1;
bool m_slopePowerChangeInProgress = false;
QElapsedTimer m_slopePowerTimer;
// Physics-based power calculation from slope
virtual double computeSlopeTargetPower(double gradePercent, double speedKmh);
// Update power based on current slope and speed (called periodically)
virtual void updateSlopeTargetPower(bool force = false);
// Check if device supports native inclination control
virtual bool supportsNativeInclination() const { return true; }
// Helper: get current speed for slope calculations
double getCurrentSpeedForSlope();
uint16_t wattFromHR(bool useSpeedAndCadence);
};

View File

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

View File

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

View File

@@ -501,8 +501,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
QString proform_rower_ip = settings.value(QZSettings::proform_rower_ip, QZSettings::default_proform_rower_ip).toString();
QString computrainerSerialPort =
settings.value(QZSettings::computrainer_serialport, QZSettings::default_computrainer_serialport).toString();
QString kettlerUsbSerialPort =
settings.value(QZSettings::kettler_usb_serialport, QZSettings::default_kettler_usb_serialport).toString();
QString csaferowerSerialPort = settings.value(QZSettings::csafe_rower, QZSettings::default_csafe_rower).toString();
QString csafeellipticalSerialPort =
settings.value(QZSettings::csafe_elliptical_port, QZSettings::default_csafe_elliptical_port).toString();
@@ -687,19 +685,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
filter = (b.name().compare(filterDevice, Qt::CaseInsensitive) == 0);
}
const QString deviceName = b.name();
const QString upperDeviceName = deviceName.toUpper();
bool isTrxAppGateUsbBikeTC = false;
if (upperDeviceName.startsWith(QStringLiteral("TC")) && deviceName.length() == 5) {
isTrxAppGateUsbBikeTC = true;
for (int idx = 2; idx < deviceName.length(); ++idx) {
if (!deviceName.at(idx).isDigit()) {
isTrxAppGateUsbBikeTC = false;
break;
}
}
}
if (deviceName.startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
if (b.name().startsWith(QStringLiteral("M3")) && !m3iBike && filter) {
if (m3ibike::isCorrectUnit(b)) {
this->setLastBluetoothDevice(b);
@@ -821,19 +807,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(computrainerBike);
} else if (!kettlerUsbSerialPort.isEmpty() && !kettlerUsbBike) {
this->stopDiscovery();
kettlerUsbBike =
new kettlerusbbike(noWriteResistance, noHeartService, bikeResistanceOffset, bikeResistanceGain);
emit deviceConnected(b);
connect(kettlerUsbBike, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
connect(kettlerUsbBike, &kettlerusbbike::debug, this, &bluetooth::debug);
kettlerUsbBike->deviceDiscovered(b);
if (this->discoveryAgent && !this->discoveryAgent->isActive()) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(kettlerUsbBike);
} else if (!csaferowerSerialPort.isEmpty() && !csafeRower) {
this->stopDiscovery();
csafeRower = new csaferower(noWriteResistance, noHeartService, false);
@@ -1028,7 +1001,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
}
this->signalBluetoothDeviceConnected(domyosRower);
} else if ((b.name().startsWith(QStringLiteral("Domyos-Bike")) && (!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool())) &&
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
!b.name().startsWith(QStringLiteral("DomyosBridge")) && !domyosBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
domyosBike = new domyosbike(noWriteResistance, noHeartService, testResistance, bikeResistanceOffset,
@@ -1043,8 +1016,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
emit searchingStop();
}
this->signalBluetoothDeviceConnected(domyosBike);
} else if ((b.name().toUpper().startsWith(QStringLiteral("MRK-R11S-")) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower)) &&
} else if (b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) && iconsole_rower &&
!trxappgateusbRower && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -1098,7 +1070,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(domyosElliptical);
} else if ((b.name().toUpper().startsWith(QStringLiteral("YPOO-U3-")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH_590E")) ||
b.name().toUpper().startsWith(QStringLiteral("SCH411/510E")) ||
b.name().toUpper().startsWith(QStringLiteral("KETTLER ")) ||
b.name().toUpper().startsWith(QStringLiteral("FEIER-EM-")) ||
b.name().toUpper().startsWith(QStringLiteral("MX-AS ")) ||
@@ -1108,7 +1079,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("CARDIOPOWER EEGO")) ||
(b.name().toUpper().startsWith(QStringLiteral("E35")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().startsWith(QStringLiteral("FS-")) && iconsole_elliptical) ||
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && !horizonTreadmill && ftms_bike.contains(QZSettings::default_ftms_bike) && filter) {
!b.name().compare(ftms_elliptical, Qt::CaseInsensitive)) && !ypooElliptical && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
ypooElliptical =
@@ -1259,7 +1230,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
this->signalBluetoothDeviceConnected(soleElliptical);
} else if (b.name().startsWith(QStringLiteral("Domyos")) &&
!b.name().startsWith(QStringLiteral("DomyosBr")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKE-")) &&
!b.name().toUpper().startsWith(QStringLiteral("DOMYOS-BIKING-")) && !domyos && !domyosElliptical && b.name().compare(ftms_treadmill, Qt::CaseInsensitive) &&
!domyosBike && !domyosRower && !ftmsBike && !horizonTreadmill &&
(!deviceHasService(b, QBluetoothUuid((quint16)0x1826)) || settings.value(QZSettings::domyostreadmill_notfmts, QZSettings::default_domyostreadmill_notfmts).toBool()) &&
@@ -1375,8 +1345,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
if (this->discoveryAgent && !this->discoveryAgent->isActive())
emit searchingStop();
this->signalBluetoothDeviceConnected(shuaA5Treadmill);
} else if (((b.name().toUpper().startsWith(QStringLiteral("TRUE")) &&
!(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) && b.name().length() == 19)) ||
} else if ((b.name().toUpper().startsWith(QStringLiteral("TRUE")) ||
b.name().toUpper().startsWith(QStringLiteral("ASSAULT TREADMILL ")) ||
(b.name().toUpper().startsWith(QStringLiteral("WDWAY")) && b.name().length() == 8) || // WdWay179
(b.name().toUpper().startsWith(QStringLiteral("TREADMILL")) && !gem_module_inclination && !deviceHasService(b, QBluetoothUuid((quint16)0x1814)) && !deviceHasService(b, QBluetoothUuid((quint16)0x1826)))) &&
@@ -1509,7 +1478,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("XTERRA TR")) ||
b.name().toUpper().startsWith(QStringLiteral("T118_")) ||
b.name().toUpper().startsWith(QStringLiteral("TM4500")) ||
b.name().toUpper().startsWith(QStringLiteral("TM6500")) ||
b.name().toUpper().startsWith(QStringLiteral("RUNN ")) ||
b.name().toUpper().startsWith(QStringLiteral("YS_T1MPLUST")) ||
b.name().toUpper().startsWith(QStringLiteral("YPOO-MINI PRO-")) ||
@@ -1518,21 +1486,16 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("3G ELITE ")) ||
b.name().toUpper().startsWith(QStringLiteral("AB300S-")) ||
b.name().toUpper().startsWith(QStringLiteral("TF04-")) || // Sport Synology Z5 Treadmill #2415
(b.name().toUpper().startsWith(QStringLiteral("TRUE TREADMILL ")) &&
b.name().length() == 19) || // TRUE TREADMILL followed by 4 digits (e.g. TRUE TREADMILL 0000)
(b.name().toUpper().startsWith(QStringLiteral("FIT-")) && !b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) || // FIT-1596 and sports tech f37s treadmill #2412
b.name().toUpper().startsWith(QStringLiteral("FIT-TM-")) || // FIT-TM- treadmill with real inclination
b.name().toUpper().startsWith(QStringLiteral("LJJ-")) || // LJJ-02351A
b.name().toUpper().startsWith(QStringLiteral("WLT-EP-")) || // Flow elliptical
(b.name().toUpper().startsWith("SCHWINN 810")) ||
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
(b.name().toUpper().startsWith("SF-T")) || // Sunny Fitness Treadmill
(b.name().toUpper().startsWith("MRK-T")) || // MERACH W50 TREADMILL
b.name().toUpper().startsWith(QStringLiteral("KS-MC")) ||
b.name().toUpper().startsWith(QStringLiteral("FOCUS M3")) ||
b.name().toUpper().startsWith(QStringLiteral("ANPIUS-")) ||
b.name().toUpper().startsWith(QStringLiteral("KICKR RUN")) ||
b.name().toUpper().startsWith(QStringLiteral("SPERAX_RM-01")) ||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) ||
(b.name().toUpper().startsWith(QStringLiteral("KS-HD-Z1D"))) || // Kingsmith WalkingPad Z1
(b.name().toUpper().startsWith(QStringLiteral("KS-AP-"))) || // Kingsmith WalkingPad R3 Hybrid+
(b.name().toUpper().startsWith(QStringLiteral("NOBLEPRO CONNECT")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // FTMS
@@ -1541,7 +1504,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith(QStringLiteral("XT485")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
b.name().toUpper().startsWith(QStringLiteral("MOBVOI TM")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("MOBVOI WMTP")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TM4800-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("LB600")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T60-")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TUNTURI T90-")) || // FTMS
@@ -1549,14 +1511,12 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("ASSAULTRUNNER")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("CITYSPORTS-LINKER")) ||
(b.name().toUpper().startsWith(QStringLiteral("TP1")) && b.name().length() == 3) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15 && ftms_bike.contains(QZSettings::default_ftms_bike)) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("CTM")) && b.name().length() >= 15) || // FTMS
(b.name().toUpper().startsWith(QStringLiteral("F85")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("S77")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F89")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("F80")) && !sole_inclination) || // FMTS
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("TM XP_")) || // FTMS
b.name().toUpper().startsWith(QStringLiteral("THERUN T15")) // FTMS
(b.name().toUpper().startsWith(QStringLiteral("ANPLUS-"))) // FTMS
) &&
!horizonTreadmill && filter) {
this->setLastBluetoothDevice(b);
@@ -1716,7 +1676,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("YS_G1_")) || // Yesoul S3
(b.name().toUpper().startsWith("YS_G1MPLUS")) || // Yesoul G1M Plus
(b.name().toUpper().startsWith("YS_G1MMAX")) || // Yesoul G1M Max
(b.name().toUpper().startsWith("YS_A6_")) || // Yesoul A6
(b.name().toUpper().startsWith("DS25-")) || // Bodytone DS25
(b.name().toUpper().startsWith("SCHWINN 510T")) ||
(b.name().toUpper().startsWith("3G CARDIO ")) ||
@@ -1751,7 +1710,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("KETTLERBLE")) ||
(b.name().toUpper().startsWith("JAS_C3")) ||
(b.name().toUpper().startsWith("SCH_190U")) ||
(b.name().toUpper().startsWith("SCH_290R")) ||
(b.name().toUpper().startsWith("RAVE WHITE")) ||
(b.name().toUpper().startsWith("DOMYOS-BIKING-")) ||
(b.name().startsWith(QStringLiteral("Domyos-Bike")) && deviceHasService(b, QBluetoothUuid((quint16)0x1826)) && !settings.value(QZSettings::domyosbike_notfmts, QZSettings::default_domyosbike_notfmts).toBool()) ||
@@ -1769,7 +1727,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("JUSTO")) ||
(b.name().toUpper().startsWith("MYCYCLE ")) ||
(b.name().toUpper().startsWith("T2 ")) ||
(b.name().toUpper().startsWith("S18")) ||
(b.name().toUpper().startsWith("RC-MAX-")) ||
(b.name().toUpper().startsWith("TPS-SPBIKE-2.0")) ||
(b.name().toUpper().startsWith("NEO BIKE SMART")) ||
@@ -1797,28 +1754,21 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith("MRK-S26S-")) ||
(b.name().toUpper().startsWith("MRK-S26C-")) ||
(b.name().toUpper().startsWith("ROBX")) ||
(b.name().toUpper().startsWith("ORLAUF_ARES")) ||
(b.name().toUpper().startsWith("SPEEDMAGPRO")) ||
(b.name().toUpper().startsWith("XCX-")) ||
(b.name().toUpper().startsWith("SMARTBIKE-")) ||
(b.name().toUpper().startsWith("D500V2")) ||
(b.name().toUpper().startsWith("NEO BIKE PLUS ")) ||
(b.name().toUpper().startsWith(QStringLiteral("PM5")) && !b.name().toUpper().endsWith(QStringLiteral("SKI")) && !b.name().toUpper().endsWith(QStringLiteral("ROW"))) ||
(b.name().toUpper().startsWith("L-") && b.name().length() == 11) ||
(b.name().toUpper().startsWith("DMASUN-") && b.name().toUpper().endsWith("-BIKE")) ||
(b.name().toUpper().startsWith(QStringLiteral("FIT-BK-"))) ||
(b.name().toUpper().startsWith("VFSPINBIKE")) ||
(b.name().toUpper().startsWith("GLT") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) ||
(b.name().toUpper().startsWith("SPORT01-") && deviceHasService(b, QBluetoothUuid((quint16)0x1826))) || // Labgrey Magnetic Exercise Bike https://www.amazon.co.uk/dp/B0CXMF1NPY?_encoding=UTF8&psc=1&ref=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&ref_=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&social_share=cm_sw_r_cp_ud_dp_PE420HA7RD7WJBZPN075&skipTwisterOG=1
(b.name().toUpper().startsWith("FS-YK-")) ||
(b.name().toUpper().startsWith(QStringLiteral("HT")) && (b.name().length() == 10)) ||
(b.name().toUpper().startsWith("ZUMO")) || (b.name().toUpper().startsWith("XS08-")) ||
(b.name().toUpper().startsWith("B94")) || (b.name().toUpper().startsWith("STAGES BIKE")) ||
(b.name().toUpper().startsWith("SUITO")) || (b.name().toUpper().startsWith("D2RIDE")) ||
(b.name().toUpper().startsWith("DIRETO X")) || (b.name().toUpper().startsWith("MERACH-667-")) ||
!b.name().compare(ftms_bike, Qt::CaseInsensitive) || (b.name().toUpper().startsWith("SMB1")) ||
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE")) ||
(b.name().toUpper().startsWith("YPBM") && b.name().length() == 10)) &&
(b.name().toUpper().startsWith("UBIKE FTMS")) || (b.name().toUpper().startsWith("INRIDE"))) &&
ftms_rower.contains(QZSettings::default_ftms_rower) &&
!ftmsBike && !ftmsRower && !snodeBike && !fitPlusBike && !stagesBike && filter) {
this->setLastBluetoothDevice(b);
@@ -1902,7 +1852,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
(b.name().toUpper().startsWith(QStringLiteral("RM")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DR")) && b.name().length() == 2) ||
(b.name().toUpper().startsWith(QStringLiteral("DFC")) && b.name().length() == 3) ||
(b.name().toUpper().startsWith(QStringLiteral("THINK A")) && b.name().length() == 18) ||
(b.name().toUpper().startsWith(QStringLiteral("ASSIOMA")) &&
powerSensorName.startsWith(QStringLiteral("Disabled")))) &&
!stagesBike && !ftmsBike && filter) {
@@ -1918,7 +1867,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
// connect(stagesBike, SIGNAL(inclinationChanged(double)), this, SLOT(inclinationChanged(double)));
stagesBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(stagesBike);
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) && ftms_rower.contains(QZSettings::default_ftms_rower) && !smartrowRower && filter) { // Issue #4033
} else if (b.name().toUpper().startsWith(QStringLiteral("SMARTROW")) && !smartrowRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
smartrowRower =
@@ -1956,10 +1905,8 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
b.name().toUpper().startsWith(QStringLiteral("S4 COMMS")) ||
b.name().toUpper().startsWith(QStringLiteral("KS-WLT")) || // KS-WLT-W1
b.name().toUpper().startsWith(QStringLiteral("I-ROWER")) ||
b.name().toUpper().startsWith(QStringLiteral("MRK-CRYDN-")) ||
b.name().toUpper().startsWith(QStringLiteral("YOROTO-RW-")) ||
b.name().toUpper().startsWith(QStringLiteral("SF-RW")) ||
b.name().toUpper().startsWith(QStringLiteral("SMARTROWER")) || // Chaoke 107a magnetic rowing machine (Discussion #4029)
b.name().toUpper().startsWith(QStringLiteral("NORDLYS")) ||
b.name().toUpper().startsWith(QStringLiteral("ROWER ")) ||
b.name().toUpper().startsWith(QStringLiteral("ROGUE CONSOLE ")) ||
@@ -2059,7 +2006,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(ziproTreadmill, &ziprotreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
ziproTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(ziproTreadmill);
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN"))) && !lifespanTreadmill && filter) {
} else if ((b.name().toUpper().startsWith(QLatin1String("LIFESPAN-TM"))) && !lifespanTreadmill && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
lifespanTreadmill = new lifespantreadmill(this->pollDeviceTime, noConsole, noHeartService);
@@ -2117,9 +2064,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(apexBike, &bluetoothdevice::connectedAndDiscovered, this, &bluetooth::connectedAndDiscovered);
apexBike->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(apexBike);
} else if ((b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) ||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFBIKE")) ||
b.name().toUpper().startsWith(QStringLiteral("BKOOLFITNESSBIKE"))) && !bkoolBike && filter) {
} else if (b.name().toUpper().startsWith(QStringLiteral("BKOOLSMARTPRO")) && !bkoolBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
bkoolBike = new bkoolbike(noWriteResistance, noHeartService);
@@ -2447,7 +2392,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
toorx->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(toorx);
} else if (((b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT")) && !b.name().toUpper().startsWith(QStringLiteral("BH DUALKIT TREAD"))) ||
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike && !toorx &&
b.name().toUpper().startsWith(QStringLiteral("BH-"))) && !iConceptBike &&
!iconcept_elliptical && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
@@ -2499,15 +2444,15 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(activioTreadmill, &activiotreadmill::debug, this, &bluetooth::debug);
activioTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(activioTreadmill);
} else if (((deviceName.startsWith(QStringLiteral("TOORX"))) ||
(deviceName.startsWith(QStringLiteral("V-RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("K80_"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("ICONSOLE+"))) ||
(upperDeviceName.startsWith(QStringLiteral("I-RUNNING"))) ||
(upperDeviceName.startsWith(QStringLiteral("DKN RUN"))) ||
(upperDeviceName.startsWith(QStringLiteral("ADIDAS "))) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK")))) &&
} else if (((b.name().startsWith(QStringLiteral("TOORX"))) ||
(b.name().startsWith(QStringLiteral("V-RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("K80_"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) ||
(b.name().toUpper().startsWith(QStringLiteral("I-RUNNING"))) ||
(b.name().toUpper().startsWith(QStringLiteral("DKN RUN"))) ||
(b.name().toUpper().startsWith(QStringLiteral("ADIDAS "))) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK")))) &&
!trxappgateusb && !trxappgateusbBike && !toorx_bike && !toorx_ftms && !toorx_ftms_treadmill && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) &&
ftms_bike.contains(QZSettings::default_ftms_bike) &&
filter) {
@@ -2521,25 +2466,24 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(trxappgateusb, &trxappgateusbtreadmill::debug, this, &bluetooth::debug);
trxappgateusb->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(trxappgateusb);
} else if (isTrxAppGateUsbBikeTC ||
(upperDeviceName.startsWith(QStringLiteral("TUN ")) ||
upperDeviceName.startsWith(QStringLiteral("FITHIWAY")) ||
upperDeviceName.startsWith(QStringLiteral("FIT HI WAY")) ||
upperDeviceName.startsWith(QStringLiteral("BIKZU_")) ||
upperDeviceName.startsWith(QStringLiteral("PASYOU-")) ||
upperDeviceName.startsWith(QStringLiteral("VIRTUFIT")) ||
upperDeviceName.startsWith(QStringLiteral("IBIKING+")) ||
((deviceName.startsWith(QStringLiteral("TOORX")) ||
upperDeviceName.startsWith(QStringLiteral("I-CONSOIE+")) ||
upperDeviceName.startsWith(QStringLiteral("I-CONSOLE+")) ||
upperDeviceName.startsWith(QStringLiteral("ICONSOLE+")) ||
upperDeviceName.startsWith(QStringLiteral("VIFHTR2.1")) ||
(upperDeviceName.startsWith(QStringLiteral("REEBOK"))) ||
upperDeviceName.contains(QStringLiteral("CR011R")) ||
(upperDeviceName.startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
upperDeviceName.startsWith(QStringLiteral("DKN MOTION"))) &&
} else if ((b.name().toUpper().startsWith(QStringLiteral("TUN ")) ||
b.name().toUpper().startsWith(QStringLiteral("FITHIWAY")) ||
b.name().toUpper().startsWith(QStringLiteral("FIT HI WAY")) ||
b.name().toUpper().startsWith(QStringLiteral("BIKZU_")) ||
b.name().toUpper().startsWith(QStringLiteral("PASYOU-")) ||
b.name().toUpper().startsWith(QStringLiteral("VIRTUFIT")) ||
b.name().toUpper().startsWith(QStringLiteral("IBIKING+")) ||
((b.name().startsWith(QStringLiteral("TOORX")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOIE+")) ||
b.name().toUpper().startsWith(QStringLiteral("I-CONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("ICONSOLE+")) ||
b.name().toUpper().startsWith(QStringLiteral("VIFHTR2.1")) ||
(b.name().toUpper().startsWith(QStringLiteral("REEBOK"))) ||
b.name().toUpper().contains(QStringLiteral("CR011R")) ||
(b.name().toUpper().startsWith(QStringLiteral("FAL-SPORTS")) && toorx_bike) ||
b.name().toUpper().startsWith(QStringLiteral("DKN MOTION"))) &&
(toorx_bike))) &&
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical) && !csc_as_bike) {
!trxappgateusb && !toorx_ftms && !toorx_ftms_treadmill && !trxappgateusbBike && filter && !iconsole_elliptical && !iconsole_rower && ftms_elliptical.contains(QZSettings::default_ftms_elliptical)) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
trxappgateusbBike =
@@ -2643,7 +2587,7 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if (((b.name().startsWith(QStringLiteral("FS-")) && fitplus_bike) ||
(b.name().toUpper().startsWith("H9110 OSAKA")) ||
b.name().startsWith(QStringLiteral("MRK-"))) &&
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && !horizonTreadmill && filter) {
!fitPlusBike && !ftmsBike && !ftmsRower && !snodeBike && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
fitPlusBike =
@@ -2790,11 +2734,11 @@ void bluetooth::connectedAndDiscovered() {
settings.value(QZSettings::fitmetria_fanfit_enable, QZSettings::default_fitmetria_fanfit_enable).toBool();
// only at the first very connection, setting the user default resistance
if (device() && firstConnected && device()->deviceType() == BIKE &&
if (device() && firstConnected && device()->deviceType() == bluetoothdevice::BIKE &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() != 1) {
qobject_cast<bike *>(device())->changeResistance(
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt());
} else if (device() && firstConnected && device()->deviceType() == ELLIPTICAL &&
} else if (device() && firstConnected && device()->deviceType() == bluetoothdevice::ELLIPTICAL &&
settings.value(QZSettings::bike_resistance_start, QZSettings::default_bike_resistance_start).toUInt() !=
1) {
qobject_cast<elliptical *>(device())->changeResistance(
@@ -2962,7 +2906,7 @@ void bluetooth::connectedAndDiscovered() {
#else
settings.setValue(QZSettings::power_sensor_address, b.deviceUuid().toString());
#endif
if (device() && device()->deviceType() == BIKE) {
if (device() && device()->deviceType() == bluetoothdevice::BIKE) {
powerSensor = new stagesbike(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -2971,7 +2915,7 @@ void bluetooth::connectedAndDiscovered() {
connect(powerSensor, &bluetoothdevice::cadenceChanged, this->device(),
&bluetoothdevice::cadenceSensor);
powerSensor->deviceDiscovered(b);
} else if (device() && device()->deviceType() == TREADMILL) {
} else if (device() && device()->deviceType() == bluetoothdevice::TREADMILL) {
powerSensorRun = new strydrunpowersensor(false, false, true);
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -3028,7 +2972,7 @@ void bluetooth::connectedAndDiscovered() {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().startsWith(eliteSterzoSmartName))) && !eliteSterzoSmart &&
!eliteSterzoSmartName.startsWith(QStringLiteral("Disabled")) && this->device() &&
this->device()->deviceType() == BIKE) {
this->device()->deviceType() == bluetoothdevice::BIKE) {
settings.setValue(QZSettings::elite_sterzo_smart_lastdevice_name, b.name());
#ifndef Q_OS_IOS
@@ -3050,7 +2994,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::sram_axs_controller, QZSettings::default_sram_axs_controller).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SRAM "))) && !sramAXSController && this->device() &&
this->device()->deviceType() == BIKE) {
this->device()->deviceType() == bluetoothdevice::BIKE) {
sramAXSController = new sramaxscontroller();
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -3069,7 +3013,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::zwift_click, QZSettings::default_zwift_click).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("ZWIFT CLICK"))) && !zwiftClickRemote && this->device() &&
this->device()->deviceType() == BIKE) {
this->device()->deviceType() == bluetoothdevice::BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 9. is it? " << int(b.manufacturerData(2378).at(0));
@@ -3094,7 +3038,7 @@ void bluetooth::connectedAndDiscovered() {
if(settings.value(QZSettings::zwift_play, QZSettings::default_zwift_play).toBool()) {
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if (((b.name().toUpper().startsWith("SQUARE"))) && !eliteSquareController && this->device() &&
this->device()->deviceType() == BIKE) {
this->device()->deviceType() == bluetoothdevice::BIKE) {
eliteSquareController = new elitesquarecontroller(this->device());
// connect(heartRateBelt, SIGNAL(disconnected()), this, SLOT(restart()));
@@ -3114,7 +3058,7 @@ void bluetooth::connectedAndDiscovered() {
bool zwiftplay_swap = settings.value(QZSettings::zwiftplay_swap, QZSettings::default_zwiftplay_swap).toBool();
for (const QBluetoothDeviceInfo &b : qAsConst(devices)) {
if ((((b.name().toUpper().startsWith("ZWIFT PLAY"))) || b.name().toUpper().startsWith("ZWIFT RIDE") || b.name().toUpper().startsWith("ZWIFT SF2")) && zwiftPlayDevice.size() < 2 && this->device() &&
this->device()->deviceType() == BIKE) {
this->device()->deviceType() == bluetoothdevice::BIKE) {
if(b.manufacturerData(2378).size() > 0) {
qDebug() << "this should be 3 or 2. is it? " << int(b.manufacturerData(2378).at(0));
@@ -3157,8 +3101,8 @@ void bluetooth::connectedAndDiscovered() {
settings.value(QZSettings::ant_cadence, QZSettings::default_ant_cadence).toBool(),
settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool(),
settings.value(QZSettings::ant_garmin, QZSettings::default_ant_garmin).toBool(),
device()->deviceType() == TREADMILL ||
device()->deviceType() == ELLIPTICAL,
device()->deviceType() == bluetoothdevice::TREADMILL ||
device()->deviceType() == bluetoothdevice::ELLIPTICAL,
settings.value(QZSettings::android_antbike, QZSettings::default_android_antbike).toBool(),
settings.value(QZSettings::technogym_group_cycle, QZSettings::default_technogym_group_cycle).toBool(),
settings.value(QZSettings::ant_bike_device_number, QZSettings::default_ant_bike_device_number).toInt(),
@@ -3751,11 +3695,6 @@ void bluetooth::restart() {
delete computrainerBike;
computrainerBike = nullptr;
}
if (kettlerUsbBike) {
delete kettlerUsbBike;
kettlerUsbBike = nullptr;
}
if (csafeRower) {
delete csafeRower;
@@ -4111,8 +4050,6 @@ bluetoothdevice *bluetooth::device() {
#ifndef Q_OS_IOS
} else if (computrainerBike) {
return computrainerBike;
} else if (kettlerUsbBike) {
return kettlerUsbBike;
} else if (csafeRower) {
return csafeRower;
} else if (csafeElliptical) {
@@ -4178,7 +4115,7 @@ void bluetooth::stateFileUpdate() {
if (!device()) {
return;
}
if (device()->deviceType() != TREADMILL) {
if (device()->deviceType() != bluetoothdevice::TREADMILL) {
return;
}

View File

@@ -34,7 +34,6 @@
#include "devices/coresensor/coresensor.h"
#ifndef Q_OS_IOS
#include "devices/computrainerbike/computrainerbike.h"
#include "devices/kettlerusbbike/kettlerusbbike.h"
#include "devices/csaferower/csaferower.h"
#include "devices/csafeelliptical/csafeelliptical.h"
#endif
@@ -193,7 +192,6 @@ class bluetooth : public QObject, public SignalHandler {
focustreadmill *focusTreadmill = nullptr;
#ifndef Q_OS_IOS
computrainerbike *computrainerBike = nullptr;
kettlerusbbike *kettlerUsbBike = nullptr;
csaferower *csafeRower = nullptr;
csafeelliptical *csafeElliptical = nullptr;
#endif

View File

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

View File

@@ -1,7 +1,6 @@
#ifndef BLUETOOTHDEVICE_H
#define BLUETOOTHDEVICE_H
#include "bluetoothdevicetype.h"
#include "definitions.h"
#include "metric.h"
#include "qzsettings.h"
@@ -253,11 +252,6 @@ class bluetoothdevice : public QObject {
*/
virtual metric elevationGain();
/**
* @brief negativeElevationGain Gets a metric object to get and set the negative elevation gain (descents). Units: ?
*/
virtual metric negativeElevationGain();
/**
* @brief clearStats Clear the statistics.
*/
@@ -457,6 +451,7 @@ class bluetoothdevice : public QObject {
*/
void setTargetPowerZone(double pz) { TargetPowerZone = pz; }
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
enum WORKOUT_EVENT_STATE { STARTED = 0, PAUSED = 1, RESUMED = 2, STOPPED = 3 };
/**
@@ -633,11 +628,6 @@ class bluetoothdevice : public QObject {
*/
metric elevationAcc;
/**
* @brief negativeElevationAcc The negative elevation gain (descents). Units: meters
*/
metric negativeElevationAcc;
/**
* @brief m_watt Metric to get and set the power read from the trainer or from the power sensor Unit: watts
*/
@@ -797,11 +787,6 @@ class bluetoothdevice : public QObject {
*/
void update_hr_from_external();
/**
* @brief update_ios_live_activity Updates iOS Live Activity with throttling (max 1 update per second)
*/
void update_ios_live_activity();
/**
* @brief calculateMETS Calculate the METS (Metabolic Equivalent of Tasks)
* Units: METs (1 MET is approximately 3.5mL of Oxygen consumed per kg of body weight per minute)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
using namespace std::chrono_literals;
cscbike::cscbike(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -207,22 +207,6 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
emit debug(QStringLiteral("Current Crank Event Time: ") + QString::number(_LastCrankEventTime));
}
// CSC Combo Sensor Fallback Logic
//
// Some combo sensors (e.g., Giant Combo) advertise both speed and cadence capabilities
// by setting CrankPresent=true in the CSC flags byte, but only transmit valid wheel data
// while sending crank data as zeros. This happens when:
// 1. The sensor supports dual mode (speed+cadence) but only speed sensor is mounted
// 2. The cadence sensor is not activated/calibrated
// 3. Firmware always sets CrankPresent flag regardless of actual crank sensor status
//
// In these cases, we use wheel revolutions as a fallback to calculate cadence.
// This works when the wheel circumference is set to a small value (e.g., 20cm for
// indoor trainers), which effectively converts wheel RPM to a cadence-like metric.
//
// Note: When using wheel revs as cadence, the calculated RPM can exceed 256 (the
// typical limit for real crank cadence). For example, with 20cm wheel circumference
// at 12.5 km/h, wheel RPM ≈ 1000. The validation logic below accounts for this.
if ((!CrankPresent || _CrankRevs == 0) && WheelPresent) {
CrankRevs = _WheelRevs;
LastCrankEventTime = _LastWheelEventTime;
@@ -238,23 +222,7 @@ void cscbike::characteristicChanged(const QLowEnergyCharacteristic &characterist
if (CrankRevs != oldCrankRevs && deltaT) {
double cadence = ((CrankRevs - oldCrankRevs) / deltaT) * 1024 * 60;
// Cadence Validation Logic
//
// Normal cadence validation applies a 256 RPM limit for real crank sensors (no human
// can pedal faster than 256 RPM). However, when using wheel revs as fallback
// (_CrankRevs == 0), we bypass this limit because:
// - Wheel RPM with small circumferences (e.g., 20cm) can legitimately exceed 256
// - Example: 12.5 km/h with 20cm circumference = ~1042 wheel RPM
// - This high RPM represents wheel rotation rate, not actual pedaling cadence
//
// The condition breakdown:
// Part 1: (cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent)
// - For real crank data: applies 256 RPM limit
// - For wheel fallback: no limit when _CrankRevs == 0
// Part 2: (!CrankPresent && WheelPresent)
// - Pure speed sensors with no crank capability
if ((cadence >= 0 && (cadence < 256 || _CrankRevs == 0) && CrankPresent) || (!CrankPresent && WheelPresent))
if ((cadence >= 0 && cadence < 256 && CrankPresent) || (!CrankPresent && WheelPresent))
Cadence = cadence;
lastGoodCadence = now;
} else if (lastGoodCadence.msecsTo(now) > 2000) {

View File

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

View File

@@ -16,7 +16,7 @@ using namespace std::chrono_literals;
deerruntreadmill::deerruntreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService, double forceInitSpeed,
double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -75,14 +75,6 @@ void deerruntreadmill::writeCharacteristic(const QLowEnergyCharacteristic charac
}
}
void deerruntreadmill::waitForAPacket() {
QEventLoop loop;
QTimer timeout;
connect(this, &deerruntreadmill::packetReceived, &loop, &QEventLoop::quit);
timeout.singleShot(3000, &loop, SLOT(quit()));
loop.exec();
}
void deerruntreadmill::writeUnlockCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log) {
QEventLoop loop;
QTimer timeout;
@@ -130,70 +122,18 @@ uint8_t deerruntreadmill::calculateXOR(uint8_t arr[], size_t size) {
return result;
}
uint8_t deerruntreadmill::calculatePitPatChecksum(uint8_t arr[], size_t size) {
uint8_t result = 0;
if (size < 5) {
qDebug() << QStringLiteral("array too small for PitPat checksum");
return 0;
}
// For PitPat protocol:
// 1. XOR from byte 5 to byte (size - 3) for long messages (>= 7 bytes)
// or from byte 2 to byte (size - 3) for short messages (< 7 bytes)
// 2. XOR the result with byte 1
size_t startIdx = (size < 7) ? 2 : 5;
for (size_t i = startIdx; i <= size - 3; i++) {
result ^= arr[i];
}
// XOR with byte 1 (command byte)
result ^= arr[1];
return result;
}
void deerruntreadmill::forceSpeed(double requestSpeed) {
QSettings settings;
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
if (pitpat) {
// PitPat speed template
// Pattern: 6a 17 00 00 00 00 [speed_high] [speed_low] 01 00 8a 00 04 00 00 00 00 00 12 2e 0c [checksum] 43
// Speed encoding: speed value * 1000 (e.g., 2.0 km/h = 2000 = 0x07d0)
uint8_t writeSpeed[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x07, 0x6c, 0x01, 0x00, 0x8a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x2e, 0x0c, 0xc3, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
uint16_t speed = (uint16_t)(requestSpeed * 1000.0);
writeSpeed[6] = (speed >> 8) & 0xFF; // High byte
writeSpeed[7] = speed & 0xFF; // Low byte
writeSpeed[21] = calculatePitPatChecksum(writeSpeed, sizeof(writeSpeed)); // Checksum at byte 21
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed PitPat speed=") + QString::number(requestSpeed), false, true);
} else if (superun_ba04) {
// Superun BA04 speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0x14, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0xdb, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed BA04 speed=") + QString::number(requestSpeed), false, false);
} else {
// Default speed template
uint8_t writeSpeed[] = {0x4d, 0x00, 0xc9, 0x17, 0x6a, 0x17, 0x02, 0x00, 0x06, 0x40, 0x04, 0x4c, 0x01, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd8, 0x43};
writeSpeed[2] = pollCounter;
writeSpeed[10] = ((int)((requestSpeed * 100)) >> 8) & 0xFF;
writeSpeed[11] = ((int)((requestSpeed * 100))) & 0xFF;
writeSpeed[25] = calculateXOR(writeSpeed, sizeof(writeSpeed));
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
}
writeCharacteristic(gattWriteCharacteristic, writeSpeed, sizeof(writeSpeed),
QStringLiteral("forceSpeed speed=") + QString::number(requestSpeed), false, false);
}
void deerruntreadmill::forceIncline(double requestIncline) {
@@ -276,7 +216,8 @@ void deerruntreadmill::update() {
}
if (pitpat) {
forceSpeed(1.0);
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
} else {
// should be:
// 0x49 = inited
@@ -299,16 +240,13 @@ void deerruntreadmill::update() {
emit tapeStarted();
} else if (requestStop != -1) {
emit debug(QStringLiteral("stopping... ") + paused);
/*if (lastState == PAUSED) {
uint8_t pause[] = {0x05, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x07};
if (pitpat) {
uint8_t stop[] = {
0x6a, 0x17, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00,
0x8a, 0x00, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x12, 0x2e,
0x0c, 0xaa, 0x43};
writeCharacteristic(gattWriteCharacteristic, stop, sizeof(stop), QStringLiteral("stop"), false, true);
} else {
writeCharacteristic(gattWriteCharacteristic, pause, sizeof(pause), QStringLiteral("pause"), false,
true);
} else*/ {
uint8_t stop[] = {0x4d, 0x00, 0x48, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x50, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x85, 0x11, 0xd6, 0x43};
stop[2] = pollCounter;
@@ -454,37 +392,15 @@ void deerruntreadmill::btinit(bool startTape) {
// PitPat treadmill initialization sequence
uint8_t initData1[] = {0x6a, 0x05, 0xfd, 0xf8, 0x43};
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("pitpat init 1"), false, true);
uint8_t unlockData[] = {0x6b, 0x05, 0x9d, 0x98, 0x43};
writeUnlockCharacteristic(unlockData, sizeof(unlockData), QStringLiteral("pitpat unlock"), false);
uint8_t initData2[] = {0x6a, 0x05, 0xd7, 0xd2, 0x43};
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("pitpat init 2"), false, true);
uint8_t startData[] = {0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x43};
writeCharacteristic(gattWriteCharacteristic, startData, sizeof(startData), QStringLiteral("pitpat start"), false, true);
} else if (superun_ba04) {
// Superun BA04 treadmill initialization sequence
// Wait for initial packet from treadmill before sending init
emit debug(QStringLiteral("BA04: waiting for initial packet..."));
waitForAPacket();
// Init 1: pollCounter = 0
uint8_t initData1[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 0; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData1, sizeof(initData1), QStringLiteral("BA04 init 1"), false, true);
uint8_t initData2[] = {0x4d, 0x00, 0x00, 0x05, 0x6a, 0x05, 0xfd, 0xf8, 0x43};
initData1[2] = 1; // pollCounter = 0
writeCharacteristic(gattWriteCharacteristic, initData2, sizeof(initData2), QStringLiteral("BA04 init 2"), false, true);
// Init 2: pollCounter = 1
uint8_t initData3[] = {0x4d, 0x00, 0x01, 0x17, 0x6a, 0x17, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x05, 0x00, 0x50, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb5, 0x7c, 0x7c, 0x43};
initData3[2] = 2; // pollCounter = 1
writeCharacteristic(gattWriteCharacteristic, initData3, sizeof(initData3), QStringLiteral("BA04 init 3"), false, true);
// Start pollCounter from 2 after init
pollCounter = 3;
}
initDone = true;
}
@@ -497,8 +413,6 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
QBluetoothUuid _gattNotifyCharacteristicId((quint16)0xfff2);
QBluetoothUuid _pitpatWriteCharacteristicId((quint16)0xfba1);
QBluetoothUuid _pitpatNotifyCharacteristicId((quint16)0xfba2);
QBluetoothUuid _superunWriteCharacteristicId((quint16)0xff01);
QBluetoothUuid _superunNotifyCharacteristicId((quint16)0xff02);
QBluetoothUuid _unlockCharacteristicId((quint16)0x2b2a);
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
@@ -513,7 +427,7 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
qDebug() << QStringLiteral("unlock char uuid") << c.uuid() << QStringLiteral("handle") << c.handle()
<< c.properties();
}
unlock_characteristic = unlock_service->characteristic(_unlockCharacteristicId);
if (unlock_characteristic.isValid()) {
emit debug(QStringLiteral("unlock characteristic found"));
@@ -531,9 +445,6 @@ void deerruntreadmill::stateChanged(QLowEnergyService::ServiceState state) {
if (pitpat) {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_pitpatWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_pitpatNotifyCharacteristicId);
} else if (superun_ba04) {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_superunWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_superunNotifyCharacteristicId);
} else {
gattWriteCharacteristic = gattCommunicationChannelService->characteristic(_gattWriteCharacteristicId);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(_gattNotifyCharacteristicId);
@@ -577,7 +488,6 @@ void deerruntreadmill::characteristicWritten(const QLowEnergyCharacteristic &cha
void deerruntreadmill::serviceScanDone(void) {
QBluetoothUuid _gattCommunicationChannelServiceId((quint16)0xfff0);
QBluetoothUuid _pitpatServiceId((quint16)0xfba0);
QBluetoothUuid _superunServiceId((quint16)0xffff);
QBluetoothUuid _unlockServiceId((quint16)0x1801);
emit debug(QStringLiteral("serviceScanDone"));
@@ -593,11 +503,6 @@ void deerruntreadmill::serviceScanDone(void) {
emit debug(QStringLiteral("Detected pitpat treadmill variant"));
gattCommunicationChannelService = m_control->createServiceObject(_pitpatServiceId);
unlock_service = m_control->createServiceObject(_unlockServiceId);
} else if (services_list.contains(_superunServiceId)) {
superun_ba04 = true;
pitpat = false;
emit debug(QStringLiteral("Detected Superun BA04 treadmill variant"));
gattCommunicationChannelService = m_control->createServiceObject(_superunServiceId);
} else {
pitpat = false;
gattCommunicationChannelService = m_control->createServiceObject(_gattCommunicationChannelServiceId);

View File

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

View File

@@ -27,12 +27,6 @@ using namespace std::chrono_literals;
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
#define DM_MACHINE_OP_ROUVY(OP, P1, P2, P3) \
OP(WAHOO_KICKR, "ELITE AVANTI $uuid_hex$ W", DM_MACHINE_TYPE_TREADMILL | DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
OP(WAHOO_BLUEHR, "Wahoo HRM", DM_MACHINE_TYPE_BIKE | DM_MACHINE_TYPE_TREADMILL, P1, P2, P3) \
OP(WAHOO_RPM_SPEED, "Wahoo SPEED $uuid_hex$", DM_MACHINE_TYPE_BIKE, P1, P2, P3) \
OP(WAHOO_TREADMILL, "Wahoo TREAD $uuid_hex$", DM_MACHINE_TYPE_TREADMILL, P1, P2, P3)
#define DP_PROCESS_WRITE_0003() (zwift_play_emulator ? writeP0003 : 0)
#define DP_PROCESS_WRITE_2AD9() writeP2AD9
#define DP_PROCESS_WRITE_2AD9T() writeP2AD9
@@ -49,7 +43,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0xE005, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_E005, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD2, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_CYCLE, 0x2AD3, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x00\x01"), DP_PROCESS_WRITE_NULL, P1, P2, P3) \
@@ -57,7 +51,7 @@ using namespace std::chrono_literals;
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD6, DPKT_CHAR_PROP_FLAG_READ, DM_BT("\x0A\x00\x96\x00\x0A\x00"), \
DP_PROCESS_WRITE_NULL, P1, P2, P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE | DPKT_CHAR_PROP_FLAG_INDICATE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
OP(FITNESS_MACHINE_TREADMILL, 0x2AD9, DPKT_CHAR_PROP_FLAG_WRITE, DM_BT("\x00"), DP_PROCESS_WRITE_2AD9T, P1, P2, \
P3) \
OP(FITNESS_MACHINE_TREADMILL, 0x2ACD, DPKT_CHAR_PROP_FLAG_NOTIFY, DM_BT("\x00"), DP_PROCESS_WRITE_NULL, P1, P2, \
P3) \
@@ -134,12 +128,12 @@ enum {
} \
if (P2.size()) { \
QString dircon_id = QString("%1").arg(settings.value(QZSettings::dircon_id, \
QZSettings::default_dircon_id).toInt(), rouvy_compatibility ? 5 : 4, 10, QChar('0')); \
QZSettings::default_dircon_id).toInt(), 4, 10, QChar('0')); \
DirconProcessor *processor = new DirconProcessor( \
P2, \
QString(QStringLiteral(NAME)) \
.replace(QStringLiteral("$uuid_hex$"), dircon_id), \
server_base_port + DM_MACHINE_##DESC, rouvy_compatibility ? dircon_id : QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
server_base_port + DM_MACHINE_##DESC, QString(QStringLiteral("%1")).arg(DM_MACHINE_##DESC), mac, \
this); \
QString servdesc; \
foreach (DirconProcessorService *s, P2) { servdesc += *s + QStringLiteral(","); } \
@@ -177,16 +171,15 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QSettings settings;
DirconProcessorService *service;
QList<DirconProcessorService *> services, proc_services;
BLUETOOTH_TYPE dt = Bike->deviceType();
bluetoothdevice::BLUETOOTH_TYPE dt = Bike->deviceType();
bt = Bike;
uint8_t type = dt == TREADMILL || dt == ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
uint8_t type = dt == bluetoothdevice::TREADMILL || dt == bluetoothdevice::ELLIPTICAL ? DM_MACHINE_TYPE_TREADMILL
: DM_MACHINE_TYPE_BIKE;
qDebug() << "Building Dircom Manager";
uint16_t server_base_port =
settings.value(QZSettings::dircon_server_base_port, QZSettings::default_dircon_server_base_port).toUInt();
bool bike_wheel_revs = settings.value(QZSettings::bike_wheel_revs, QZSettings::default_bike_wheel_revs).toBool();
bool zwift_play_emulator = settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool();
bool rouvy_compatibility = settings.value(QZSettings::rouvy_compatibility, QZSettings::default_rouvy_compatibility).toBool();
DM_CHAR_NOTIF_OP(DM_CHAR_NOTIF_BUILD_OP, Bike, 0, 0)
@@ -216,11 +209,7 @@ DirconManager::DirconManager(bluetoothdevice *Bike, int8_t bikeResistanceOffset,
QObject::connect(&bikeTimer, &QTimer::timeout, this, &DirconManager::bikeProvider);
QString mac = getMacAddress();
if (rouvy_compatibility) {
DM_MACHINE_OP_ROUVY(DM_MACHINE_INIT_OP, services, proc_services, type)
} else {
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
}
DM_MACHINE_OP(DM_MACHINE_INIT_OP, services, proc_services, type)
if (zwift_play_emulator || settings.value(QZSettings::race_mode, QZSettings::default_race_mode).toBool())
bikeTimer.start(50ms);
@@ -247,7 +236,7 @@ double DirconManager::currentGear() {
QSettings settings;
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool() && writeP0003)
return writeP0003->currentGear();
else if(bt && bt->deviceType() == BIKE)
else if(bt && bt->deviceType() == bluetoothdevice::BIKE)
return ((bike*)bt)->gears();
return 0;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ domyostreadmill::domyostreadmill(uint32_t pollDeviceTime, bool noConsole, bool n
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;
@@ -761,14 +761,13 @@ void domyostreadmill::btinit(bool startTape) {
writeCharacteristic(initDataStart4, sizeof(initDataStart4), QStringLiteral("init"), false, true);
writeCharacteristic(initDataStart5, sizeof(initDataStart5), QStringLiteral("init"), false, true);
// writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
// writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
forceSpeedOrIncline(lastSpeed, lastInclination);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
if (startTape) {
// writeCharacteristic(initDataStart6, sizeof(initDataStart6), "init", false, false);
// writeCharacteristic(initDataStart7, sizeof(initDataStart7), "init", false, true);
forceSpeedOrIncline(lastSpeed, lastInclination);
writeCharacteristic(initDataStart8, sizeof(initDataStart8), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart9, sizeof(initDataStart9), QStringLiteral("init"), false, true);
writeCharacteristic(initDataStart10, sizeof(initDataStart10), QStringLiteral("init"), false, false);
writeCharacteristic(initDataStart11, sizeof(initDataStart11), QStringLiteral("init"), false, true);
writeCharacteristic(initDataStart12, sizeof(initDataStart12), QStringLiteral("init"), false, false);

View File

@@ -23,7 +23,7 @@ echelonconnectsport::echelonconnectsport(bool noWriteResistance, bool noHeartSer
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -23,7 +23,7 @@ echelonrower::echelonrower(bool noWriteResistance, bool noHeartService, int8_t b
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
speedRaw.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,15 +22,11 @@ void elliptical::update_metrics(bool watt_calc, const double watts) {
WeightLoss = metric::calculateWeightLoss(KCal.value());
WattKg = m_watt.value() / settings.value(QZSettings::weight, QZSettings::default_weight).toFloat();
} else if (m_watt.value() > 0) {
if (watt_calc) {
m_watt = 0;
}
m_watt = 0;
WattKg = 0;
}
} else if (m_watt.value() > 0) {
if (watt_calc) {
m_watt = 0;
}
m_watt = 0;
WattKg = 0;
}
@@ -40,9 +36,6 @@ void elliptical::update_metrics(bool watt_calc, const double watts) {
_lastTimeUpdate = current;
_firstUpdate = false;
// Update iOS Live Activity with throttling
update_ios_live_activity();
}
resistance_t elliptical::resistanceFromPowerRequest(uint16_t power) { return power / 10; } // in order to have something
@@ -144,7 +137,7 @@ metric elliptical::currentInclination() { return Inclination; }
uint8_t elliptical::fanSpeed() { return FanSpeed; }
bool elliptical::connected() { return false; }
BLUETOOTH_TYPE elliptical::deviceType() { return ELLIPTICAL; }
bluetoothdevice::BLUETOOTH_TYPE elliptical::deviceType() { return bluetoothdevice::ELLIPTICAL; }
void elliptical::clearStats() {
moving.clear(true);

View File

@@ -24,7 +24,7 @@ class elliptical : public bluetoothdevice {
virtual int pelotonToEllipticalResistance(int pelotonResistance);
virtual bool inclinationAvailableByHardware();
virtual bool inclinationSeparatedFromResistance();
BLUETOOTH_TYPE deviceType() override;
bluetoothdevice::BLUETOOTH_TYPE deviceType() override;
void clearStats() override;
void setPaused(bool p) override;
void setLap() override;

View File

@@ -71,7 +71,7 @@ class CRC8
eslinkertreadmill::eslinkertreadmill(uint32_t pollDeviceTime, bool noConsole, bool noHeartService,
double forceInitSpeed, double forceInitInclination) {
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
this->noConsole = noConsole;
this->noHeartService = noHeartService;

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
using namespace std::chrono_literals;
fakerower::fakerower(bool noWriteResistance, bool noHeartService, bool noVirtualDevice) {
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -31,36 +31,14 @@ fakerower::fakerower(bool noWriteResistance, bool noHeartService, bool noVirtual
void fakerower::update() {
QSettings settings;
QDateTime now = QDateTime::currentDateTime();
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
update_metrics(false, watts());
if (RequestedPower.value() != -1) {
m_watt = (double)RequestedPower.value() * (1.0 + (((double)rand() / RAND_MAX) * 0.4 - 0.2));
if(RequestedPower.value())
Cadence = 50 + (static_cast<double>(rand()) / RAND_MAX) * 50;
else
Cadence = 0;
StrokesCount += (Cadence.value()) *
((double)lastRefreshCharacteristicChanged.msecsTo(now)) / 60000;
emit debug(QStringLiteral("writing power ") + QString::number(RequestedPower.value()));
//requestPower = -1;
// bepo70: Disregard the current inclination for calculating speed. When the video
// has a high inclination you have to give many power to get the desired playback speed,
// if inclination is very low little more power gives a quite high speed jump.
// Speed = metric::calculateSpeedFromPower(m_watt.value(), Inclination.value(),
// Speed.value(),fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), speedLimit());
Speed = metric::calculateSpeedFromPower(
m_watt.value(), 0, Speed.value(), fabs(QDateTime::currentDateTime().msecsTo(Speed.lastChanged()) / 1000.0), 0);
}
Distance += ((Speed.value() / (double)3600.0) /
((double)1000.0 / (double)(lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime()))));
lastRefreshCharacteristicChanged = now;
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
// ******************************************* virtual bike init *************************************
if (!firstStateChanged && !this->hasVirtualDevice() && !noVirtualDevice

View File

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

View File

@@ -30,7 +30,7 @@ fitplusbike::fitplusbike(bool noWriteResistance, bool noHeartService, int8_t bik
QZ_EnableDiscoveryCharsAndDescripttors = true;
}
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;

View File

@@ -19,7 +19,7 @@ fitshowtreadmill::fitshowtreadmill(uint32_t pollDeviceTime, bool noConsole, bool
double forceInitInclination) {
Q_UNUSED(noConsole)
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
this->noHeartService = noHeartService;

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ using namespace std::chrono_literals;
ftmsbike::ftmsbike(bool noWriteResistance, bool noHeartService, int8_t bikeResistanceOffset,
double bikeResistanceGain) {
QSettings settings;
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -194,7 +194,7 @@ void ftmsbike::zwiftPlayInit() {
}
void ftmsbike::forcePower(int16_t requestPower) {
if((resistance_lvl_mode || TITAN_7000) && !MAGNUS && !SS2K) {
if((resistance_lvl_mode || TITAN_7000) && !MAGNUS) {
forceResistance(resistanceFromPowerRequest(requestPower));
} else {
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
@@ -251,16 +251,10 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
writeCharacteristic(write, sizeof(write),
QStringLiteral("forceResistance ") + QString::number(requestResistance));
} else {
if(requestResistance < 0) {
qDebug() << "Negative resistance detected:" << requestResistance << "using fallback value 1";
requestResistance = 1;
}
if(SL010 || SPORT01)
if(SL010)
Resistance = requestResistance;
if(JFBK5_0 || DIRETO_XR || YPBM || FIT_BK) {
if(JFBK5_0 || DIRETO_XR) {
uint8_t write[] = {FTMS_SET_TARGET_RESISTANCE_LEVEL, 0x00, 0x00};
write[1] = ((uint16_t)requestResistance * 10) & 0xFF;
write[2] = ((uint16_t)requestResistance * 10) >> 8;
@@ -277,26 +271,6 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
}
}
void ftmsbike::forceInclination(double requestInclination) {
// FTMS SET_INDOOR_BIKE_SIMULATION_PARAMS command
// Byte 0: OpCode
// Byte 1-2: Wind Speed (sint16, 0.001 m/s)
// Byte 3-4: Grade/Inclination (sint16, 0.01%)
// Byte 5-6: Coefficient of Rolling Resistance (uint8, 0.0001)
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x28, 0x19};
// Convert inclination to FTMS format (multiply by 100 for 0.01% units)
int16_t inclination = (int16_t)(requestInclination * 100.0);
// Pack Grade in bytes 3-4 as little-endian sint16
write[3] = ((uint16_t)inclination) & 0xFF;
write[4] = ((uint16_t)inclination) >> 8;
writeCharacteristic(write, sizeof(write),
QStringLiteral("forceInclination ") + QString::number(requestInclination));
}
void ftmsbike::update() {
QSettings settings;
@@ -308,6 +282,8 @@ void ftmsbike::update() {
if (initRequest) {
zwiftPlayInit();
if(ICSE)
requestResistance = 1; // to force the engine to send every second a target inclination
// when we are emulating the zwift protocol, zwift doesn't senf the start simulation frames, so we have to send them
if(settings.value(QZSettings::zwift_play_emulator, QZSettings::default_zwift_play_emulator).toBool())
@@ -364,23 +340,10 @@ void ftmsbike::update() {
forceResistance(rR);
}
}
requestResistance = -1;
if(!ICSE)
requestResistance = -1;
}
// gpx scenario for example
if(!virtualBike || !virtualBike->ftmsDeviceConnected()) {
if ((requestInclination != -100 || (lastGearValue != gears() && requestInclination != -100))) {
emit debug(QStringLiteral("writing inclination ") + QString::number(requestInclination));
forceInclination(requestInclination + gears()); // since this bike doesn't have the concept of resistance,
// i'm using the gears in the inclination
requestInclination = -100;
} else if(lastGearValue != gears() && lastRawRequestedInclinationValue != -100) {
// in order to send the new gear value ASAP
forceInclination(lastRawRequestedInclinationValue + gears()); // since this bike doesn't have the concept of resistance,
// i'm using the gears in the inclination
}
}
if((virtualBike && virtualBike->ftmsDeviceConnected()) && lastGearValue != gears() && lastRawRequestedInclinationValue != -100 && lastPacketFromFTMS.length() >= 7) {
qDebug() << "injecting fake ftms frame in order to send the new gear value ASAP" << lastPacketFromFTMS.toHex(' ');
ftmsCharacteristicChanged(QLowEnergyCharacteristic(), lastPacketFromFTMS);
@@ -580,7 +543,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
};
// clean time in case for a long period we don't receive values
if(lastRefreshCharacteristicChanged2AD2.secsTo(now) > secondsToResetTimer) {
if(lastRefreshCharacteristicChanged2AD2.secsTo(now) > 5) {
qDebug() << "clearing lastRefreshCharacteristicChanged2AD2" << lastRefreshCharacteristicChanged2AD2 << now;
lastRefreshCharacteristicChanged2AD2 = now;
}
@@ -611,13 +574,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
100.0;
index += 2;
emit debug(QStringLiteral("Current Average Speed: ") + QString::number(avgSpeed));
// Use average speed if instant speed is not available (moreData flag set)
if (Flags.moreData) {
if (!settings.value(QZSettings::speed_power_based, QZSettings::default_speed_power_based).toBool()) {
Speed = avgSpeed;
emit debug(QStringLiteral("Current Speed (from average): ") + QString::number(Speed.value()));
}
}
}
if (Flags.instantCadence) {
@@ -639,15 +595,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
2.0;
index += 2;
emit debug(QStringLiteral("Current Average Cadence: ") + QString::number(avgCadence));
// Use average cadence if instant cadence is not available
if (!Flags.instantCadence) {
if (settings.value(QZSettings::cadence_sensor_name, QZSettings::default_cadence_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
Cadence = avgCadence;
emit debug(QStringLiteral("Current Cadence (from average): ") + QString::number(Cadence.value()));
}
}
}
if (Flags.totDistance) {
@@ -675,7 +622,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
if(BIKE_)
d = d / 10.0;
// for this bike, i will use the resistance that I set directly because the bike sends a different ratio.
if(!SL010 && !TITAN_7000 && !SPORT01)
if(!SL010 && !TITAN_7000)
Resistance = d;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit resistanceRead(Resistance.value());
@@ -691,25 +638,18 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
double cr = 97.62165482;
if (Cadence.value() && m_watt.value()) {
double res =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
br) /
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
if (isnan(res)) {
if (Cadence.value() > 0) {
// let's keep the last good value
} else {
m_pelotonResistance = 0;
}
if(YS_G1MPLUS) {
m_pelotonResistance = Resistance.value(); // 1:1 ratio
} else {
m_pelotonResistance = res;
m_pelotonResistance =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
br) /
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
}
if (!resistance_received && !DU30_bike && !SL010) {
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
@@ -723,31 +663,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
if(DU30_bike) {
m_watt = wattsFromResistance(Resistance.value());
emit debug(QStringLiteral("Current Watt: ") + QString::number(m_watt.value()));
} else if (SPORT01) {
// Custom power calculation for SPORT01
// Resistance multipliers for levels 1-10
const double k[10] = {0.60, 0.75, 0.85, 0.95, 1.00, 1.18, 1.40, 1.70, 2.00, 2.40};
// Baseline power curve coefficients (MyWhoosh cadence-power at resistance 5)
double ac = 0.01243107769;
double bc = 1.145964912;
double cc = -23.50977444;
// Calculate baseline power from cadence (resistance level 5 baseline)
double baseline_watt = ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc;
// Get current resistance level (1-10) and apply multiplier
int resistance_level = (int)Resistance.value();
if(resistance_level < 1) resistance_level = 1;
if(resistance_level > 10) resistance_level = 10;
// Apply resistance multiplier
m_watt = baseline_watt * k[resistance_level - 1];
if(m_watt.value() < 0) m_watt = 0;
emit debug(QStringLiteral("Current Watt (SPORT01 formula - R%1 x%2): %3")
.arg(resistance_level).arg(k[resistance_level - 1]).arg(m_watt.value()));
} else if (MRK_S26C) {
m_watt = Cadence.value() * (Resistance.value() * 1.16);
emit debug(QStringLiteral("Current Watt (MRK-S26C formula): ") + QString::number(m_watt.value()));
@@ -778,15 +693,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
(uint16_t)((uint8_t)newValue.at(index))));
index += 2;
emit debug(QStringLiteral("Current Average Watt: ") + QString::number(avgPower));
// Use average power if instant power is zero or not available
if ((!Flags.instantPower || m_watt.value() == 0) && avgPower > 0) {
if (settings.value(QZSettings::power_sensor_name, QZSettings::default_power_sensor_name)
.toString()
.startsWith(QStringLiteral("Disabled"))) {
m_watt = avgPower;
emit debug(QStringLiteral("Current Watt (from average): ") + QString::number(m_watt.value()));
}
}
}
if (Flags.expEnergy && newValue.length() > index + 1) {
@@ -1132,7 +1038,7 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
double cr = 97.62165482;
if (Cadence.value() && m_watt.value()) {
double res =
m_pelotonResistance =
(((sqrt(pow(br, 2.0) - 4.0 * ar *
(cr - (m_watt.value() * 132.0 /
(ac * pow(Cadence.value(), 2.0) + bc * Cadence.value() + cc)))) -
@@ -1140,17 +1046,6 @@ void ftmsbike::characteristicChanged(const QLowEnergyCharacteristic &characteris
(2.0 * ar)) *
settings.value(QZSettings::peloton_gain, QZSettings::default_peloton_gain).toDouble()) +
settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).toDouble();
if (isnan(res)) {
if (Cadence.value() > 0) {
// let's keep the last good value
} else {
m_pelotonResistance = 0;
}
} else {
m_pelotonResistance = res;
}
Resistance = m_pelotonResistance;
emit resistanceRead(Resistance.value());
}
@@ -1317,7 +1212,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
}
}
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SCH_190U || SCH_290R || DOMYOS || SMB1 || FIT_BK) {
if (settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SCH_190U || DOMYOS || SMB1 || FIT_BK) {
QBluetoothUuid ftmsService((quint16)0x1826);
if (s->serviceUuid() != ftmsService) {
qDebug() << QStringLiteral("hammer racer bike wants to be subscribed only to FTMS service in order "
@@ -1370,7 +1265,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
}
QBluetoothUuid _gattWriteCharControlPointId((quint16)0x2AD9);
if ((c.properties() & QLowEnergyCharacteristic::Write || c.properties() & QLowEnergyCharacteristic::WriteNoResponse) && c.uuid() == _gattWriteCharControlPointId) {
if (c.properties() & QLowEnergyCharacteristic::Write && c.uuid() == _gattWriteCharControlPointId) {
qDebug() << QStringLiteral("FTMS service and Control Point found");
gattWriteCharControlPointId = c;
gattFTMSService = s;
@@ -1397,7 +1292,7 @@ void ftmsbike::stateChanged(QLowEnergyService::ServiceState state) {
}
if (gattFTMSService && gattWriteCharControlPointId.isValid() &&
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SCH_290R || SMB1 || FIT_BK)) {
(settings.value(QZSettings::hammer_racer_s, QZSettings::default_hammer_racer_s).toBool() || SMB1 || FIT_BK)) {
init();
}
@@ -1534,23 +1429,9 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
#endif
writeCharacteristicZwiftPlay((uint8_t*)message.data(), message.length(), "gearInclination", false, false);
return;
} else if(b.at(0) == FTMS_SET_TARGET_POWER && !ergModeSupported) {
} else if(b.at(0) == FTMS_SET_TARGET_POWER && ((zwiftPlayService != nullptr && gears_zwift_ratio) || !ergModeSupported)) {
qDebug() << "discarding";
return;
} else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
// handling watt gain and offset for erg mode
double watt_gain = settings.value(QZSettings::watt_gain, QZSettings::default_watt_gain).toDouble();
double watt_offset = settings.value(QZSettings::watt_offset, QZSettings::default_watt_offset).toDouble();
if (watt_gain != 1.0 || watt_offset != 0) {
uint16_t powerRequested = (((uint8_t)b.at(1)) + (b.at(2) << 8));
qDebug() << "applying watt_gain/watt_offset from" << powerRequested;
powerRequested = ((powerRequested / watt_gain) - watt_offset);
qDebug() << "to" << powerRequested;
b[1] = powerRequested & 0xFF;
b[2] = powerRequested >> 8;
}
}
// gears on erg mode is quite useless and it's confusing
/* else if(b.at(0) == FTMS_SET_TARGET_POWER && b.length() > 2) {
@@ -1572,14 +1453,10 @@ void ftmsbike::ftmsCharacteristicChanged(const QLowEnergyCharacteristic &charact
}
void ftmsbike::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
static bool connectedAndDiscoveredOk = false;
emit debug(QStringLiteral("descriptorWritten ") + descriptor.name() + QStringLiteral(" ") + newValue.toHex(' '));
initRequest = true;
if(!connectedAndDiscoveredOk) {
connectedAndDiscoveredOk = true;
emit connectedAndDiscovered();
}
emit connectedAndDiscovered();
}
void ftmsbike::descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
@@ -1664,14 +1541,10 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if ((bluetoothDevice.name().toUpper().startsWith("ICSE") && bluetoothDevice.name().length() == 4)) {
qDebug() << QStringLiteral("ICSE found");
ICSE = true;
secondsToResetTimer = 15;
autoResistanceEnable = false; // Disable auto resistance for ICSE bikes
qDebug() << QStringLiteral("ICSE: autoResistance disabled by default");
} else if ((bluetoothDevice.name().toUpper().startsWith("DOMYOS"))) {
qDebug() << QStringLiteral("DOMYOS found");
resistance_lvl_mode = true;
ergModeSupported = false;
max_resistance = 32;
DOMYOS = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("3G Cardio RB"))) {
qDebug() << QStringLiteral("_3G_Cardio_RB found");
@@ -1680,10 +1553,6 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << QStringLiteral("SCH_190U found");
SCH_190U = true;
max_resistance = 100;
} else if((bluetoothDevice.name().toUpper().startsWith("SCH_290R"))) {
qDebug() << QStringLiteral("SCH_290R found");
SCH_290R = true;
max_resistance = 100;
} else if(bluetoothDevice.name().toUpper().startsWith("D2RIDE")) {
qDebug() << QStringLiteral("D2RIDE found");
D2RIDE = true;
@@ -1699,7 +1568,6 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if(bluetoothDevice.name().toUpper().startsWith("JFBK5.0") || bluetoothDevice.name().toUpper().startsWith("JFBK7.0")) {
qDebug() << QStringLiteral("JFBK5.0 found");
resistance_lvl_mode = true;
ergModeSupported = false;
JFBK5_0 = true;
} else if((bluetoothDevice.name().toUpper().startsWith("BIKE-"))) {
qDebug() << QStringLiteral("BIKE- found");
@@ -1765,30 +1633,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if(device.name().toUpper().startsWith("HAMMER")) {
qDebug() << QStringLiteral("HAMMER found");
HAMMER = true;
} else if(device.name().toUpper().startsWith("YPBM") && device.name().length() == 10) {
qDebug() << QStringLiteral("YPBM found");
YPBM = true;
resistance_lvl_mode = true;
ergModeSupported = false;
max_resistance = 32;
} else if(device.name().toUpper().startsWith("SPORT01")) {
qDebug() << QStringLiteral("SPORT01 found");
SPORT01 = true;
resistance_lvl_mode = true;
ergModeSupported = false;
max_resistance = 10;
Resistance = 1; // Initialize resistance to 1 for SPORT01
} else if(device.name().toUpper().startsWith("FS-YK-")) {
qDebug() << QStringLiteral("FS-YK- found");
FS_YK = true;
ergModeSupported = false; // this bike doesn't have ERG mode natively
} else if(device.name().toUpper().startsWith("S18")) {
qDebug() << QStringLiteral("S18 found");
S18 = true;
max_resistance = 24;
}
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {
resistance_lvl_mode = true;
}

View File

@@ -81,7 +81,6 @@ class ftmsbike : public bike {
// true because or the bike supports it by hardware or because QZ is emulating this in this module
bool ergModeSupportedAvailableBySoftware() override { return true; }
bool inclinationAvailableBySoftware() override { return !resistance_lvl_mode; }
private:
bool writeCharacteristic(uint8_t *data, uint8_t data_len, const QString &info, bool disable_log = false,
@@ -95,7 +94,6 @@ class ftmsbike : public bike {
void init();
void forceResistance(resistance_t requestResistance);
void forcePower(int16_t requestPower);
void forceInclination(double requestInclination);
uint16_t wattsFromResistance(double resistance);
QTimer *refresh;
@@ -140,7 +138,6 @@ class ftmsbike : public bike {
bool DOMYOS = false;
bool _3G_Cardio_RB = false;
bool SCH_190U = false;
bool SCH_290R = false;
bool D2RIDE = false;
bool WATTBIKE = false;
bool VFSPINBIKE = false;
@@ -164,12 +161,6 @@ class ftmsbike : public bike {
bool MAGNUS = false;
bool MRK_S26C = false;
bool HAMMER = false;
bool YPBM = false;
bool SPORT01 = false;
bool FS_YK = false;
bool S18 = false;
uint8_t secondsToResetTimer = 5;
int16_t T2_lastGear = 0;

View File

@@ -1,7 +1,6 @@
#include "devices/ftmsrower/ftmsrower.h"
#include "devices/ftmsbike/ftmsbike.h"
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualtreadmill.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
@@ -20,7 +19,7 @@
using namespace std::chrono_literals;
ftmsrower::ftmsrower(bool noWriteResistance, bool noHeartService) {
m_watt.setType(metric::METRIC_WATT, deviceType());
m_watt.setType(metric::METRIC_WATT);
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
@@ -154,9 +153,8 @@ void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic
if (newValue.length() >= 10) {
// Extract RowState from byte 9 - this indicates if user is actively rowing
pm5RowState = (uint8_t)newValue.at(9);
pm5RowStateReceived = true; // Mark that we've received RowState at least once
emit debug(QStringLiteral("PM5 CE060031 RAW: ") + newValue.toHex(' ') +
emit debug(QStringLiteral("PM5 CE060031 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
@@ -166,33 +164,19 @@ void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic
// Extract cadence (SPM) from byte 5
uint8_t spm = (uint8_t)newValue.at(5);
if (spm > 0) {
// Only check RowState if we've received it at least once
if (!pm5RowStateReceived || pm5RowState != 0) {
Cadence = spm;
lastStroke = now;
}
Cadence = spm;
lastStroke = now;
}
// Zero cadence if RowState indicates not rowing (and we've received RowState)
if (pm5RowStateReceived && pm5RowState == 0) {
Cadence = 0;
}
// Extract speed from bytes 3-4 (little endian) in 0.001m/s
// Extract speed from bytes 3-4 (little endian) in 0.001m/s
uint16_t speedRaw = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
if (speedRaw > 0) {
// Only check RowState if we've received it at least once
if (!pm5RowStateReceived || pm5RowState != 0) {
Speed = (speedRaw * 0.001) * 3.6; // Convert m/s to km/h
}
Speed = (speedRaw * 0.001) * 3.6; // Convert m/s to km/h
}
// Zero speed if RowState indicates not rowing (and we've received RowState)
if (pm5RowStateReceived && pm5RowState == 0) {
Speed = 0;
}
emit debug(QStringLiteral("PM5 CE060032 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Cadence: ") + QString::number(Cadence.value()) +
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
emit debug(QStringLiteral("PM5 CE060032 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Cadence: ") + QString::number(Cadence.value()) +
QStringLiteral(" Speed: ") + QString::number(Speed.value()) +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
@@ -208,29 +192,19 @@ void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic
// Extract stroke count from bytes 7-8 (little endian)
uint16_t strokeCount = ((uint8_t)newValue.at(8) << 8) | (uint8_t)newValue.at(7);
if (strokeCount != StrokesCount.value()) {
// Only check RowState if we've received it at least once
if (!pm5RowStateReceived || pm5RowState != 0) {
StrokesCount = strokeCount;
lastStroke = now;
}
StrokesCount = strokeCount;
lastStroke = now;
}
// Extract power from bytes 3-4 (little endian)
uint16_t power = ((uint8_t)newValue.at(4) << 8) | (uint8_t)newValue.at(3);
if (power > 0) {
// Only check RowState if we've received it at least once
if (!pm5RowStateReceived || pm5RowState != 0) {
m_watt = power;
}
m_watt = power;
}
// Zero power if RowState indicates not rowing (and we've received RowState)
if (pm5RowStateReceived && pm5RowState == 0) {
m_watt = 0;
}
emit debug(QStringLiteral("PM5 CE060036 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
QStringLiteral(" Stroke Count: ") + QString::number(StrokesCount.value()) +
emit debug(QStringLiteral("PM5 CE060036 RAW: ") + newValue.toHex(' ') +
QStringLiteral(" Power: ") + QString::number(m_watt.value()) +
QStringLiteral(" Stroke Count: ") + QString::number(StrokesCount.value()) +
QStringLiteral(" RowState: ") + QString::number(pm5RowState));
}
}
@@ -266,10 +240,9 @@ void ftmsrower::parseConcept2Data(const QLowEnergyCharacteristic &characteristic
}
lastRefreshCharacteristicChanged = now;
// Apply RowState logic after all characteristics processing (fallback safety check)
// Only apply if we've received RowState at least once to avoid zeroing values at startup
if (PM5 && pm5RowStateReceived && pm5RowState == 0) {
// Apply RowState logic after all characteristics processing
if (PM5 && pm5RowState == 0) {
m_watt = 0;
Cadence = 0;
Speed = 0;
@@ -314,16 +287,6 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
return;
}
if(PM5 && pm5RowStateReceived && pm5RowState == 0) {
// If using PM5 and RowState indicates not rowing, ignore data to avoid bogus values
qDebug() << "PM5 RowState indicates not rowing, ignoring FTMS data.";
Cadence = 0;
m_watt = 0;
Speed = 0;
lastRefreshCharacteristicChanged = now;
return;
}
lastPacket = newValue;
union flags {
@@ -396,17 +359,10 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
}
if (Flags.totDistance) {
if (ICONSOLE_PLUS || FITSHOW) {
// For ICONSOLE+, always calculate distance from speed instead of using characteristic data
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(now)));
} else {
// For other devices, use the distance from characteristic data
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint32_t)((uint8_t)newValue.at(index)))) /
1000.0;
}
Distance = ((double)((((uint32_t)((uint8_t)newValue.at(index + 2)) << 16) |
(uint32_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint32_t)((uint8_t)newValue.at(index)))) /
1000.0;
index += 3;
} else {
Distance += ((Speed.value() / 3600000.0) *
@@ -424,10 +380,8 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
emit debug(QStringLiteral("Current Pace: ") + QString::number(instantPace));
if((DFIT_L_R && Cadence.value() > 0) || !DFIT_L_R) {
if(instantPace == 0 || instantPace == 65535)
Speed = 0;
else
Speed = (60.0 / instantPace) * 30.0; // translating pace (min/500m) to km/h in order to match the pace function in the rower.cpp
Speed = (60.0 / instantPace) *
30.0; // translating pace (min/500m) to km/h in order to match the pace function in the rower.cpp
}
emit debug(QStringLiteral("Current Speed: ") + QString::number(Speed.value()));
}
@@ -504,8 +458,6 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
emit debug(QStringLiteral("Current Heart: ") + QString::number(Heart.value()));
} else
emit debug(QStringLiteral("Error on parsing heart"));
} else if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
}
@@ -532,6 +484,10 @@ void ftmsrower::characteristicChanged(const QLowEnergyCharacteristic &characteri
lastRefreshCharacteristicChanged = now;
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();
@@ -664,8 +620,6 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
settings.value(QZSettings::virtual_device_enabled, QZSettings::default_virtual_device_enabled).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
bool virtual_device_force_treadmill =
settings.value(QZSettings::virtual_device_force_treadmill, QZSettings::default_virtual_device_force_treadmill).toBool();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
@@ -683,13 +637,7 @@ void ftmsrower::stateChanged(QLowEnergyService::ServiceState state) {
#endif
{
if (virtual_device_enabled) {
if (virtual_device_force_treadmill) {
emit debug(QStringLiteral("creating virtual treadmill interface..."));
auto virtualTreadmill = new virtualtreadmill(this, noHeartService);
connect(virtualTreadmill, &virtualtreadmill::debug, this, &ftmsrower::debug);
this->setVirtualDevice(virtualTreadmill, VIRTUAL_DEVICE_MODE::PRIMARY);
} else if (!virtual_device_rower) {
if (!virtual_device_rower) {
emit debug(QStringLiteral("creating virtual bike interface..."));
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
@@ -831,12 +779,6 @@ void ftmsrower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
} else if (device.name().toUpper().startsWith(QStringLiteral("NORDLYS"))) {
NORDLYS = true;
qDebug() << "NORDLYS found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("ICONSOLE+"))) {
ICONSOLE_PLUS = true;
qDebug() << "ICONSOLE+ found!";
} else if (device.name().toUpper().startsWith(QStringLiteral("FS-"))) {
FITSHOW = true;
qDebug() << "FITSHOW found!";
}
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);

View File

@@ -71,8 +71,6 @@ class ftmsrower : public rower {
bool KINGSMITH = false;
bool PM5 = false;
bool NORDLYS = false;
bool ICONSOLE_PLUS = false;
bool FITSHOW = false;
bool WATER_ROWER = false;
bool DFIT_L_R = false;
@@ -83,7 +81,6 @@ class ftmsrower : public rower {
// PM5 specific variables
uint8_t pm5RowState = 0;
bool pm5RowStateReceived = false;
#ifdef Q_OS_IOS
lockscreen *h = 0;

View File

@@ -64,25 +64,11 @@ void heartratebelt::disconnectBluetooth() {
void heartratebelt::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
// qDebug() << "characteristicChanged" << characteristic.uuid() << newValue << newValue.length();
Q_UNUSED(characteristic);
emit packetReceived();
emit debug(QStringLiteral(" << ") + newValue.toHex(' '));
// Handle Battery Service
if (characteristic.uuid() == QBluetoothUuid((quint16)0x2A19)) {
if(newValue.length() > 0) {
uint8_t battery = (uint8_t)newValue.at(0);
if(battery != battery_level) {
if(homeform::singleton())
homeform::singleton()->setToastRequested(bluetoothDevice.name() + QStringLiteral(" Battery Level ") + QString::number(battery) + " %");
}
battery_level = battery;
qDebug() << QStringLiteral("battery: ") << battery;
}
return;
}
// Handle Heart Rate Measurement
if (newValue.length() > 1) {
Heart = (uint8_t)newValue[1];
emit heartRate((uint8_t)Heart.value());
@@ -96,35 +82,6 @@ void heartratebelt::stateChanged(QLowEnergyService::ServiceState state) {
emit debug(QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state)));
if (state == QLowEnergyService::ServiceDiscovered) {
// Check if this is the Battery Service
QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());
if (service && service == gattBatteryService) {
// Handle Battery Service
auto characteristics_list = gattBatteryService->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
emit debug(QStringLiteral("battery characteristic ") + c.uuid().toString());
}
connect(gattBatteryService, &QLowEnergyService::characteristicChanged, this,
&heartratebelt::characteristicChanged);
connect(gattBatteryService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &heartratebelt::errorService);
// Enable notifications for battery level
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattBatteryService->writeDescriptor(
c.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
return;
}
// Original code for Heart Rate Service
auto characteristics_list = gattCommunicationChannelService->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
emit debug(QStringLiteral("characteristic ") + c.uuid().toString());
@@ -177,13 +134,7 @@ void heartratebelt::serviceScanDone(void) {
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this,
&heartratebelt::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
else if (s == QBluetoothUuid::BatteryService) {
QBluetoothUuid _gattBatteryServiceId(QBluetoothUuid::BatteryService);
gattBatteryService = m_control->createServiceObject(_gattBatteryServiceId);
connect(gattBatteryService, &QLowEnergyService::stateChanged, this,
&heartratebelt::stateChanged);
gattBatteryService->discoverDetails();
return;
}
}
}

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