mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 23:41:50 +01:00
Compare commits
9 Commits
build-1237
...
virtualgea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a250fe3a3 | ||
|
|
094d2c88cb | ||
|
|
7f3dda70fd | ||
|
|
79d96ba182 | ||
|
|
5ec861dab3 | ||
|
|
f90edbd632 | ||
|
|
621ed69627 | ||
|
|
b754b7f773 | ||
|
|
efb9dfbdb1 |
3937
.github/workflows/main.yml
vendored
3937
.github/workflows/main.yml
vendored
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -1,84 +0,0 @@
|
||||
//
|
||||
// QZWidget.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct QZWidgetEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QZWidget: Widget {
|
||||
let kind: String = "QZWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
QZWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
QZWidget()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// QZWidgetBundle.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct QZWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
QZWidget()
|
||||
QZWidgetControl()
|
||||
QZWidgetLiveActivity()
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// QZWidgetControl.swift
|
||||
// QZWidget
|
||||
//
|
||||
// Created by Roberto Viola on 04/10/25.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct QZWidgetControl: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "org.cagnulein.qdomyoszwift.QZWidget",
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value,
|
||||
action: StartTimerIntent()
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension QZWidgetControl {
|
||||
struct Provider: ControlValueProvider {
|
||||
var previewValue: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func currentValue() async throws -> Bool {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start / stop the timer based on `value`.
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
//
|
||||
// QZWidgetLiveActivity.swift
|
||||
// QDomyos-Zwift Live Activity Widget
|
||||
//
|
||||
// Displays workout metrics on Dynamic Island and Lock Screen
|
||||
//
|
||||
|
||||
import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// QZWorkoutAttributes is defined in QZWorkoutAttributes.swift (shared file)
|
||||
|
||||
// MARK: - Live Activity Widget
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: QZWorkoutAttributes.self) { context in
|
||||
// Lock screen/banner UI
|
||||
LockScreenLiveActivityView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded UI
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
Label("\(Int(speed)) \(speedUnit)", systemImage: "speedometer")
|
||||
.font(.caption)
|
||||
Label("\(context.state.heartRate) bpm", systemImage: "heart.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Label("\(Int(context.state.power)) W", systemImage: "bolt.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.yellow)
|
||||
Label("\(Int(context.state.cadence)) rpm", systemImage: "arrow.clockwise")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
// Empty or can add more info
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
HStack {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
Label(String(format: "%.2f \(distanceUnit)", distance), systemImage: "map")
|
||||
Spacer()
|
||||
Label("\(Int(context.state.kcal)) kcal", systemImage: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
} compactLeading: {
|
||||
// Compact leading (left side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("\(context.state.heartRate)")
|
||||
.font(.caption2)
|
||||
}
|
||||
} compactTrailing: {
|
||||
// Compact trailing (right side of Dynamic Island)
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundColor(.yellow)
|
||||
Text("\(Int(context.state.power))")
|
||||
.font(.caption2)
|
||||
}
|
||||
} minimal: {
|
||||
// Minimal view (when multiple activities)
|
||||
Image(systemName: "figure.run")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lock Screen View
|
||||
@available(iOS 16.1, *)
|
||||
struct LockScreenLiveActivityView: View {
|
||||
let context: ActivityViewContext<QZWorkoutAttributes>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "figure.indoor.cycle")
|
||||
.foregroundColor(.blue)
|
||||
Text(context.attributes.deviceName)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let speed = context.attributes.useMiles ? context.state.speed * 0.621371 : context.state.speed
|
||||
let speedUnit = context.attributes.useMiles ? "mph" : "km/h"
|
||||
MetricView(icon: "speedometer", value: String(format: "%.1f", speed), unit: speedUnit)
|
||||
MetricView(icon: "heart.fill", value: "\(context.state.heartRate)", unit: "bpm", color: .red)
|
||||
MetricView(icon: "bolt.fill", value: "\(Int(context.state.power))", unit: "W", color: .yellow)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
let distanceKm = context.state.distance / 1000.0
|
||||
let distance = context.attributes.useMiles ? distanceKm * 0.621371 : distanceKm
|
||||
let distanceUnit = context.attributes.useMiles ? "mi" : "km"
|
||||
MetricView(icon: "arrow.clockwise", value: "\(Int(context.state.cadence))", unit: "rpm")
|
||||
MetricView(icon: "map", value: String(format: "%.2f", distance), unit: distanceUnit)
|
||||
MetricView(icon: "flame.fill", value: "\(Int(context.state.kcal))", unit: "kcal", color: .orange)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metric View Component
|
||||
struct MetricView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let unit: String
|
||||
var color: Color = .primary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
.font(.caption)
|
||||
Text(value)
|
||||
.font(.system(.body, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
Text(unit)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
@available(iOS 16.1, *)
|
||||
struct QZWidgetLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = QZWorkoutAttributes(deviceName: "QZ Bike", useMiles: false)
|
||||
static let contentState = QZWorkoutAttributes.ContentState(
|
||||
speed: 25.5,
|
||||
cadence: 85,
|
||||
power: 200,
|
||||
heartRate: 145,
|
||||
distance: 12500, // meters (will be displayed as 12.50 km or 7.77 mi)
|
||||
kcal: 320,
|
||||
useMiles: false
|
||||
)
|
||||
|
||||
static var previews: some View {
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Island Compact")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Island Expanded")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Minimal")
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .content)
|
||||
.previewDisplayName("Lock Screen")
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// QZWorkoutAttributes.swift
|
||||
// QDomyos-Zwift
|
||||
//
|
||||
// Shared attributes for Live Activities
|
||||
// MUST be included in both qdomyoszwift and QZWidget targets
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
public struct QZWorkoutAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
public var speed: Double
|
||||
public var cadence: Double
|
||||
public var power: Double
|
||||
public var heartRate: Int
|
||||
public var distance: Double
|
||||
public var kcal: Double
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(speed: Double, cadence: Double, power: Double, heartRate: Int, distance: Double, kcal: Double, useMiles: Bool) {
|
||||
self.speed = speed
|
||||
self.cadence = cadence
|
||||
self.power = power
|
||||
self.heartRate = heartRate
|
||||
self.distance = distance
|
||||
self.kcal = kcal
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
|
||||
public var deviceName: String
|
||||
public var useMiles: Bool
|
||||
|
||||
public init(deviceName: String, useMiles: Bool) {
|
||||
self.deviceName = deviceName
|
||||
self.useMiles = useMiles
|
||||
}
|
||||
}
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
68
src/Home.qml
68
src/Home.qml
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) + ");");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4
src/android/res/values/strings.xml
Normal file
4
src/android/res/values/strings.xml
Normal 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>
|
||||
10
src/android/res/xml/virtual_gearing_service_config.xml
Normal file
10
src/android/res/xml/virtual_gearing_service_config.xml
Normal 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" />
|
||||
116
src/android/src/AppConfiguration.java
Normal file
116
src/android/src/AppConfiguration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
145
src/android/src/VirtualGearingBridge.java
Normal file
145
src/android/src/VirtualGearingBridge.java
Normal 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();
|
||||
}
|
||||
}
|
||||
152
src/android/src/VirtualGearingService.java
Normal file
152
src/android/src/VirtualGearingService.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef BLUETOOTHDEVICETYPE_H
|
||||
#define BLUETOOTHDEVICETYPE_H
|
||||
|
||||
enum BLUETOOTH_TYPE { UNKNOWN = 0, TREADMILL, BIKE, ROWING, ELLIPTICAL, JUMPROPE, STAIRCLIMBER };
|
||||
|
||||
#endif // BLUETOOTHDEVICETYPE_H
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user