Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@
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 */; };
87BA10032F0000010000BA03 /* ios_background_keepalive.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BA10022F0000010000BA02 /* ios_background_keepalive.mm */; };
87BA10052F0000010000BA05 /* ios_bluetooth_background.mm in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87BA10042F0000010000BA04 /* ios_bluetooth_background.mm */; };
870F72D62FDEC0FD00181280 /* webtranslation.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 870F72D52FDEC0FD00181280 /* webtranslation.cpp */; };
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 */; };
Expand Down Expand Up @@ -1024,6 +1026,9 @@
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; };
87BA10012F0000010000BA01 /* ios_background_keepalive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ios_background_keepalive.h; path = ../src/ios/ios_background_keepalive.h; sourceTree = SOURCE_ROOT; };
87BA10022F0000010000BA02 /* ios_background_keepalive.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_background_keepalive.mm; path = ../src/ios/ios_background_keepalive.mm; sourceTree = SOURCE_ROOT; };
87BA10042F0000010000BA04 /* ios_bluetooth_background.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_bluetooth_background.mm; path = ../src/ios/ios_bluetooth_background.mm; sourceTree = SOURCE_ROOT; };
870F72D52FDEC0FD00181280 /* webtranslation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = webtranslation.cpp; path = ../src/webtranslation.cpp; 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>"; };
Expand Down Expand Up @@ -2425,6 +2430,9 @@
87EFC57C2E918DAA005BB573 /* LiveActivityBridge.swift */,
870C72622E91565E00DC8A84 /* ios_liveactivity.h */,
870C72632E91565E00DC8A84 /* ios_liveactivity.mm */,
87BA10012F0000010000BA01 /* ios_background_keepalive.h */,
87BA10022F0000010000BA02 /* ios_background_keepalive.mm */,
87BA10042F0000010000BA04 /* ios_bluetooth_background.mm */,
876C646E2E74139F00F1BEC0 /* fitbackupwriter.h */,
876C646F2E74139F00F1BEC0 /* fitbackupwriter.cpp */,
876C64702E74139F00F1BEC0 /* moc_fitbackupwriter.cpp */,
Expand Down Expand Up @@ -3790,6 +3798,8 @@
buildActionMask = 2147483647;
files = (
870C72652E91565E00DC8A84 /* ios_liveactivity.mm in Compile Sources */,
87BA10032F0000010000BA03 /* ios_background_keepalive.mm in Compile Sources */,
87BA10052F0000010000BA05 /* ios_bluetooth_background.mm in Compile Sources */,
8738249627E646E3004F1B46 /* characteristicnotifier2acd.cpp in Compile Sources */,
8738249127E646E3004F1B46 /* dirconpacket.cpp in Compile Sources */,
870A5DB52CEF8FD200839641 /* technogymbike.cpp in Compile Sources */,
Expand Down
57 changes: 25 additions & 32 deletions src/homeform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#include "ios/ios_liveactivity.h"
#include "ios/ios_background_keepalive.h"
#endif
#include "localipaddress.h"
#ifdef Q_OS_ANDROID
Expand Down Expand Up @@ -5760,6 +5761,11 @@ void homeform::Start_inner(bool send_event_to_device) {
} else {
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
// keep the process alive (and so every QTimer and every BLE connection)
// when the user locks the screen during the session
if (settings.value(QZSettings::ios_background_keepalive, QZSettings::default_ios_background_keepalive)
.toBool())
iosBackgroundKeepAlive::start();
if(h && !h->appleWatchAppInstalled() && bluetoothManager->device())
h->startWorkout(bluetoothManager->device()->deviceType());
#endif
Expand Down Expand Up @@ -5879,6 +5885,7 @@ void homeform::Stop() {
h->stopWorkout();
// End iOS Live Activity when workout stops
ios_liveactivity::endLiveActivity();
iosBackgroundKeepAlive::stop();
#endif
#endif

Expand Down Expand Up @@ -6077,12 +6084,10 @@ void homeform::update() {
// Timer jitter detection (same logic as trainprogram::scheduler)
QDateTime now = QDateTime::currentDateTime();
qint64 msecsElapsed = lastUpdateCall.msecsTo(now);

// Reset jitter if it's getting too large
if (qAbs(currentUpdateJitter) > 5000) {
currentUpdateJitter = 0;
if (msecsElapsed < 0) {
msecsElapsed = 0;
}

currentUpdateJitter += msecsElapsed - 1000;
lastUpdateCall = now;

Expand Down Expand Up @@ -8548,50 +8553,38 @@ void homeform::update() {
currentUpdateJitter = 0;
}

// Check for timer jitter gaps and fill missing SessionLine records (same logic as trainprogram)
if (!Session.empty() && qAbs(currentUpdateJitter) > 1000) {
if (currentUpdateJitter > 1000) {
// We are late... fill the missing seconds with SessionLine records
int missedSeconds = currentUpdateJitter / 1000;
if (!Session.empty() && currentUpdateJitter > 1000) {
const uint32_t lastRecordedTime = Session.last().elapsedTime;
const uint32_t elapsedGap = currentElapsedSeconds > lastRecordedTime
? currentElapsedSeconds - lastRecordedTime - 1
: 0;
const int missedSeconds = qMin<int>(currentUpdateJitter / 1000, elapsedGap);

if (missedSeconds > 0) {
qDebug() << "Timer jitter detected: filling" << missedSeconds << "missing SessionLine records";

// Create SessionLine records for each missed second using current device values
uint32_t lastRecordedTime = Session.last().elapsedTime;

for (int i = 1; i <= missedSeconds; i++) {
SessionLine gapFill(
bluetoothManager->device()->currentSpeed().value(), inclination, distance1s,
watts, resistance, peloton_resistance, (uint8_t)bluetoothManager->device()->currentHeart().value(),
pace, cadence, bluetoothManager->device()->calories().value(),
bluetoothManager->device()->elevationGain().value(),
bluetoothManager->device()->negativeElevationGain().value(),
lastRecordedTime + i, // Fill each missing second
lastRecordedTime + i,
lapTrigger, totalStrokes, avgStrokesRate, maxStrokesRate, avgStrokesLength,
bluetoothManager->device()->currentCordinate(), strideLength, groundContact, verticalOscillation, stepCount,
target_cadence->value().toDouble(), target_power->value().toDouble(), target_resistance->value().toDouble(),
target_incline->value().toDouble(), target_speed->value().toDouble(),
bluetoothManager->device()->CoreBodyTemperature.value(), bluetoothManager->device()->SkinTemperature.value(), bluetoothManager->device()->HeatStrainIndex.value(),
0.0, QList<double>());

Session.append(gapFill);
qDebug() << "Added gap-filling SessionLine for elapsed time:" << (lastRecordedTime + i);
}

// Adjust jitter counter (same as trainprogram)
currentUpdateJitter -= (missedSeconds * 1000);
} else if (currentUpdateJitter < -1000) {
// We are early (negative jitter)... remove excess SessionLine records
int excessSeconds = (-currentUpdateJitter) / 1000;
qDebug() << "Negative timer jitter detected: removing" << excessSeconds << "excess SessionLine records";

// Remove excess SessionLine records from the end
for (int i = 0; i < excessSeconds && !Session.empty(); i++) {
Session.removeLast();
qDebug() << "Removed excess SessionLine record";
}

// Adjust jitter counter (same as trainprogram)
currentUpdateJitter += (excessSeconds * 1000);
}

currentUpdateJitter %= 1000;
} else if (currentUpdateJitter < -1000) {
currentUpdateJitter %= 1000;
}

SessionLine s(
Expand Down
1 change: 1 addition & 0 deletions src/ios/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<string>access to step cadence in order to show it on the iphone</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
Expand Down
7 changes: 5 additions & 2 deletions src/ios/LiveActivityBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import UIKit
private var currentActivity: Activity<QZWorkoutAttributes>?
private var inactivityTimer: Timer?

// Timeout in seconds - if no updates received, auto-close the Live Activity
private let inactivityTimeout: TimeInterval = 10.0
// Timeout in seconds - if no updates received, auto-close the Live Activity.
// While QZ is in background the updates are throttled to one every 5 seconds
// (see ios_liveactivity.mm), so this has to leave a comfortable margin or a
// single late BLE notification would close the Live Activity.
private let inactivityTimeout: TimeInterval = 30.0

// MARK: - Private Methods

Expand Down
21 changes: 21 additions & 0 deletions src/ios/ios_app_delegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ @implementation QIOSApplicationDelegate (QZApplicationDelegate)
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
qDebug() << "QZ iOS launch";

// when CoreBluetooth state restoration is in place iOS can relaunch us in
// background to deliver a BLE event: useful to know it while reading a log
NSArray *restoredCentrals = [launchOptions objectForKey:UIApplicationLaunchOptionsBluetoothCentralsKey];
if (restoredCentrals.count > 0) {
qDebug() << "QZ iOS launch: relaunched by CoreBluetooth for" << (int)restoredCentrals.count << "central(s)";
}

UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:UNAuthorizationOptionBadge
completionHandler:^(BOOL granted, NSError *error){
Expand All @@ -94,6 +102,19 @@ - (void)applicationDidBecomeActive:(UIApplication *)application {
qDebug() << "QZ iOS lifecycle: applicationDidBecomeActive";
}

- (void)applicationWillResignActive:(UIApplication *)application {
Q_UNUSED(application)
qDebug() << "QZ iOS lifecycle: applicationWillResignActive";
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
// With the background keep alive running the app stays in the "background"
// state instead of being suspended, so every QTimer (homeform::update, the
// per device refresh timers) and the BLE connections keep working.
qDebug() << "QZ iOS lifecycle: applicationDidEnterBackground, remaining background time"
<< application.backgroundTimeRemaining;
}

- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
bool didHandleShortcut = false;

Expand Down
38 changes: 38 additions & 0 deletions src/ios/ios_background_keepalive.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#ifndef IOS_BACKGROUND_KEEPALIVE_H
#define IOS_BACKGROUND_KEEPALIVE_H

/**
* @brief Keeps the app running while the screen is off / the app is in background.
*
* The `bluetooth-central` background mode alone is not enough: iOS suspends the
* process a few seconds after the screen goes off and only resumes it for short
* windows when a BLE notification arrives. While suspended every QTimer stops,
* so homeform::update() and the per-device refresh timers do not run and the
* session is effectively frozen.
*
* Playing an (inaudible) looping buffer on an active AVAudioSession keeps the
* app in the "running in background" state, exactly like the background
* execution plugins used on Flutter. The session is configured with
* MixWithOthers so the user's music/podcast is never interrupted.
*/
class iosBackgroundKeepAlive {
public:
/**
* @brief Activates the audio session and starts the silent loop.
* Must be called while the app is in foreground (iOS refuses to activate a
* new audio session from background). Calling it twice is a no-op.
*/
static void start();

/**
* @brief Stops the silent loop and deactivates the audio session.
*/
static void stop();

/**
* @brief True when the keep alive loop is currently playing.
*/
static bool isRunning();
};

#endif // IOS_BACKGROUND_KEEPALIVE_H
Loading
Loading