Compare commits

..

6 Commits

Author SHA1 Message Date
Roberto Viola
9b8c45b337 popup not needed 2026-01-31 20:18:35 +01:00
Roberto Viola
5ab7cd80f4 Merge branch 'master' into Garmin-Chinese-Server 2026-01-31 19:11:15 +00:00
Roberto Viola
d58f2536f2 Update garminconnect.h 2026-01-31 20:07:37 +01:00
Roberto Viola
c4852f3393 Detect MFA via page title and handle CSRF
Instead of scanning the entire response body for "MFA", detect MFA by parsing the HTML <title> (matching the Python garth approach) to avoid false positives from bodies that contain "MFA" text. Extract the page title early, check for "MFA" case-insensitively, and if detected update m_lastError, refresh cookies, extract a new CSRF token using two regex patterns, emit mfaRequired (unless suppressed), and abort the login flow. Also adjust the success check to rely on the title == "Success" and remove the legacy body-based MFA detection block. Added debugging logs for the title, CSRF token, and MFA signal paths.
2026-01-31 08:16:12 +01:00
Roberto Viola
28ae8d8a57 Add verbose debug logging for GarminConnect responses
Introduces a DEBUG_GARMIN_VERBOSE flag to enable detailed logging of HTTP responses and ticket extraction attempts in the GarminConnect authentication flow. This aids in troubleshooting login and MFA issues by providing more insight into response contents and extraction logic.
2026-01-30 08:44:32 +01:00
Roberto Viola
753548c97f Add Garmin server selection and debug logging
Introduces a ComboBox in settings to select between global and China Garmin servers, prompting for app restart when changed. Adds debug logging in garminconnect.cpp to trace domain and API URLs, and logs the loaded domain from settings.
2026-01-29 10:09:54 +01:00
9 changed files with 112 additions and 490 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git log:*)"
]
}
}

View File

@@ -2094,15 +2094,6 @@ void bluetooth::deviceDiscovered(const QBluetoothDeviceInfo &device) {
connect(lifespanTreadmill, &lifespantreadmill::inclinationChanged, this, &bluetooth::inclinationChanged);
lifespanTreadmill->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(lifespanTreadmill);
} else if (b.name().startsWith(QStringLiteral("AT-R")) && !mobiRower && filter) {
this->setLastBluetoothDevice(b);
this->stopDiscovery();
mobiRower = new mobirower(noWriteResistance, noHeartService);
emit deviceConnected(b);
connect(mobiRower, &bluetoothdevice::connectedAndDiscovered, this,
&bluetooth::connectedAndDiscovered);
mobiRower->deviceDiscovered(b);
this->signalBluetoothDeviceConnected(mobiRower);
} else if ((b.name().toUpper().startsWith(QStringLiteral("ECH-ROW")) ||
b.name().toUpper().startsWith(QStringLiteral("ROWSPORT")) ||
b.name().toUpper().startsWith(QStringLiteral("ROW-S"))) &&
@@ -3614,11 +3605,6 @@ void bluetooth::restart() {
delete echelonRower;
echelonRower = nullptr;
}
if (mobiRower) {
delete mobiRower;
mobiRower = nullptr;
}
if (echelonStride) {
delete echelonStride;
@@ -4054,8 +4040,6 @@ bluetoothdevice *bluetooth::device() {
return echelonConnectSport;
} else if (echelonRower) {
return echelonRower;
} else if (mobiRower) {
return mobiRower;
} else if (echelonStride) {
return echelonStride;
} else if (echelonStairclimber) {

View File

@@ -154,7 +154,6 @@
#include "zwift_play/zwiftPlayDevice.h"
#include "zwift_play/zwiftclickremote.h"
#include "devices/mobirower/mobirower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
@@ -270,7 +269,6 @@ class bluetooth : public QObject, public SignalHandler {
echelonrower *echelonRower = nullptr;
ftmsrower *ftmsRower = nullptr;
smartrowrower *smartrowRower = nullptr;
mobirower *mobiRower = nullptr;
echelonstride *echelonStride = nullptr;
echelonstairclimber *echelonStairclimber = nullptr;
lifefitnesstreadmill *lifefitnessTreadmill = nullptr;

View File

@@ -1,353 +0,0 @@
#include "mobirower.h"
#ifdef Q_OS_ANDROID
#include "keepawakehelper.h"
#endif
#include "virtualdevices/virtualbike.h"
#include "virtualdevices/virtualrower.h"
#include <QBluetoothLocalDevice>
#include <QDateTime>
#include <QFile>
#include <QMetaEnum>
#include <QSettings>
#include <chrono>
#include <math.h>
using namespace std::chrono_literals;
#ifdef Q_OS_IOS
extern quint8 QZ_EnableDiscoveryCharsAndDescripttors;
#endif
mobirower::mobirower(bool noWriteResistance, bool noHeartService) {
#ifdef Q_OS_IOS
QZ_EnableDiscoveryCharsAndDescripttors = true;
#endif
m_watt.setType(metric::METRIC_WATT, deviceType());
Speed.setType(metric::METRIC_SPEED);
refresh = new QTimer(this);
this->noWriteResistance = noWriteResistance;
this->noHeartService = noHeartService;
initDone = false;
connect(refresh, &QTimer::timeout, this, &mobirower::update);
refresh->start(200ms);
}
void mobirower::update() {
if (m_control == nullptr)
return;
if (m_control->state() == QLowEnergyController::UnconnectedState) {
emit disconnected();
return;
}
if (bluetoothDevice.isValid() && m_control->state() == QLowEnergyController::DiscoveredState &&
gattCommunicationChannelService && gattNotifyCharacteristic.isValid() && initDone) {
update_metrics(true, watts());
if (requestStart != -1) {
qDebug() << QStringLiteral("starting...");
requestStart = -1;
emit bikeStarted();
}
if (requestStop != -1) {
qDebug() << QStringLiteral("stopping...");
requestStop = -1;
}
}
}
void mobirower::serviceDiscovered(const QBluetoothUuid &gatt) {
qDebug() << QStringLiteral("serviceDiscovered ") + gatt.toString();
}
void mobirower::characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
Q_UNUSED(characteristic);
QSettings settings;
QString heartRateBeltName =
settings.value(QZSettings::heart_rate_belt_name, QZSettings::default_heart_rate_belt_name).toString();
qDebug() << QStringLiteral(" << ") + newValue.toHex(' ');
// Validate packet: 13 bytes, starts with 0xab 0x04
if (newValue.length() < 13 ||
(uint8_t)newValue.at(0) != 0xab ||
(uint8_t)newValue.at(1) != 0x04) {
qDebug() << QStringLiteral("Invalid packet format");
return;
}
// Parse power from bytes 9-10 (big-endian uint16)
uint16_t power = ((uint8_t)newValue.at(9) << 8) | (uint8_t)newValue.at(10);
// Parse stroke count from bytes 11-12 (big-endian uint16)
uint16_t strokeCount = ((uint8_t)newValue.at(11) << 8) | (uint8_t)newValue.at(12);
// Calculate cadence from stroke delta
double timeDelta = lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime());
if (timeDelta > 0 && strokeCount >= lastStrokeCount) {
uint16_t strokeDelta = strokeCount - lastStrokeCount;
// Convert to strokes per minute (SPM)
double cadence = (strokeDelta / (timeDelta / 60000.0));
if (cadence < 200) { // sanity check
Cadence = cadence;
}
}
lastStrokeCount = strokeCount;
m_watt = power;
StrokesCount = strokeCount;
// Calculate speed from strokes (standard rower formula)
// Using a simplified formula: speed in km/h derived from cadence
if (Cadence.value() > 0) {
// Typical rower: ~10m per stroke at normal pace
// Speed = (cadence * meters_per_stroke * 60) / 1000 for km/h
double metersPerStroke = 8.0; // approximate
Speed = (Cadence.value() * metersPerStroke * 60.0) / 1000.0;
} else {
Speed = 0;
}
StrokesLength =
((Speed.value() / 60.0) * 1000.0) /
Cadence.value(); // this is just to fill the tile
if (watts())
KCal +=
((((0.048 * ((double)watts()) + 1.19) *
settings.value(QZSettings::weight, QZSettings::default_weight).toFloat() * 3.5) /
200.0) /
(60000.0 / ((double)lastRefreshCharacteristicChanged.msecsTo(
QDateTime::currentDateTime()))));
Distance += ((Speed.value() / 3600000.0) *
((double)lastRefreshCharacteristicChanged.msecsTo(QDateTime::currentDateTime())));
if (Cadence.value() > 0) {
CrankRevs++;
LastCrankEventTime += (uint16_t)(1024.0 / (((double)(Cadence.value())) / 60.0));
}
lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
#ifdef Q_OS_ANDROID
if (settings.value(QZSettings::ant_heart, QZSettings::default_ant_heart).toBool())
Heart = (uint8_t)KeepAwakeHelper::heart();
else
#endif
{
if (heartRateBeltName.startsWith(QStringLiteral("Disabled"))) {
update_hr_from_external();
}
}
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence = settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
bool virtual_device_rower =
settings.value(QZSettings::virtual_device_rower, QZSettings::default_virtual_device_rower).toBool();
if (ios_peloton_workaround && cadence && !virtual_device_rower && h && firstStateChanged) {
h->virtualbike_setCadence(currentCrankRevolutions(), lastCrankEventTime());
h->virtualbike_setHeartRate((uint8_t)metrics_override_heartrate());
}
#endif
#endif
qDebug() << QStringLiteral("Current Power: ") + QString::number(m_watt.value());
qDebug() << QStringLiteral("Current Stroke Count: ") + QString::number(StrokesCount.value());
qDebug() << QStringLiteral("Current Speed: ") + QString::number(Speed.value());
qDebug() << QStringLiteral("Current Cadence: ") + QString::number(Cadence.value());
qDebug() << QStringLiteral("Current Distance: ") + QString::number(Distance.value());
qDebug() << QStringLiteral("Current Watt: ") + QString::number(watts());
if (m_control->error() != QLowEnergyController::NoError) {
qDebug() << QStringLiteral("QLowEnergyController ERROR!!") << m_control->errorString();
}
}
void mobirower::stateChanged(QLowEnergyService::ServiceState state) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceState>();
qDebug() << QStringLiteral("BTLE stateChanged ") + QString::fromLocal8Bit(metaEnum.valueToKey(state));
if (state == QLowEnergyService::ServiceDiscovered) {
// Find the notify characteristic (0xffe4)
QBluetoothUuid notifyCharUuid((quint16)0xffe4);
gattNotifyCharacteristic = gattCommunicationChannelService->characteristic(notifyCharUuid);
if (!gattNotifyCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattNotifyCharacteristic not valid, trying to find by properties");
auto characteristics_list = gattCommunicationChannelService->characteristics();
for (const QLowEnergyCharacteristic &c : qAsConst(characteristics_list)) {
qDebug() << QStringLiteral("c -> ") << c.uuid() << c.properties();
if ((c.properties() & QLowEnergyCharacteristic::Notify) == QLowEnergyCharacteristic::Notify) {
gattNotifyCharacteristic = c;
break;
}
}
}
if (!gattNotifyCharacteristic.isValid()) {
qDebug() << QStringLiteral("gattNotifyCharacteristic still not valid");
return;
}
// establish hook into notifications
connect(gattCommunicationChannelService, &QLowEnergyService::characteristicChanged, this,
&mobirower::characteristicChanged);
connect(gattCommunicationChannelService,
static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error),
this, &mobirower::errorService);
connect(gattCommunicationChannelService, &QLowEnergyService::descriptorWritten, this,
&mobirower::descriptorWritten);
// ******************************************* virtual bike/rower init *************************************
if (!firstStateChanged && !this->hasVirtualDevice()
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
&& !h
#endif
#endif
) {
QSettings settings;
bool virtual_device_enabled =
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();
#ifdef Q_OS_IOS
#ifndef IO_UNDER_QT
bool cadence =
settings.value(QZSettings::bike_cadence_sensor, QZSettings::default_bike_cadence_sensor).toBool();
bool ios_peloton_workaround =
settings.value(QZSettings::ios_peloton_workaround, QZSettings::default_ios_peloton_workaround).toBool();
if (ios_peloton_workaround && cadence && !virtual_device_rower) {
qDebug() << "ios_peloton_workaround activated!";
h = new lockscreen();
h->virtualbike_ios();
} else
#endif
#endif
if (virtual_device_enabled) {
if (!virtual_device_rower) {
qDebug() << QStringLiteral("creating virtual bike interface...");
auto virtualBike = new virtualbike(this, noWriteResistance, noHeartService);
this->setVirtualDevice(virtualBike, VIRTUAL_DEVICE_MODE::PRIMARY);
} else {
qDebug() << QStringLiteral("creating virtual rower interface...");
auto virtualRower = new virtualrower(this, noWriteResistance, noHeartService);
this->setVirtualDevice(virtualRower, VIRTUAL_DEVICE_MODE::PRIMARY);
}
}
}
firstStateChanged = 1;
// ********************************************************************************************************
QByteArray descriptor;
descriptor.append((char)0x01);
descriptor.append((char)0x00);
gattCommunicationChannelService->writeDescriptor(
gattNotifyCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration), descriptor);
}
}
void mobirower::descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue) {
qDebug() << QStringLiteral("descriptorWritten ") + descriptor.name() + " " + newValue.toHex(' ');
initDone = true;
emit connectedAndDiscovered();
}
void mobirower::serviceScanDone(void) {
qDebug() << QStringLiteral("serviceScanDone");
// Service UUID 0xffe0
QBluetoothUuid serviceUuid((quint16)0xffe0);
gattCommunicationChannelService = m_control->createServiceObject(serviceUuid);
if (!gattCommunicationChannelService) {
qDebug() << "service 0xffe0 not found, trying to find any service";
auto services = m_control->services();
for (const QBluetoothUuid &s : qAsConst(services)) {
qDebug() << QStringLiteral("service ") << s.toString();
}
if (!services.isEmpty()) {
gattCommunicationChannelService = m_control->createServiceObject(services.first());
}
}
if (!gattCommunicationChannelService) {
qDebug() << "no service found";
return;
}
connect(gattCommunicationChannelService, &QLowEnergyService::stateChanged, this, &mobirower::stateChanged);
gattCommunicationChannelService->discoverDetails();
}
void mobirower::errorService(QLowEnergyService::ServiceError err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyService::ServiceError>();
qDebug() << QStringLiteral("mobirower::errorService") + QString::fromLocal8Bit(metaEnum.valueToKey(err)) +
m_control->errorString();
}
void mobirower::error(QLowEnergyController::Error err) {
QMetaEnum metaEnum = QMetaEnum::fromType<QLowEnergyController::Error>();
qDebug() << "mobirower::error" + QString::fromLocal8Bit(metaEnum.valueToKey(err)) + m_control->errorString();
}
void mobirower::deviceDiscovered(const QBluetoothDeviceInfo &device) {
qDebug() << "Found new device: " + device.name() + " (" + device.address().toString() + ')';
bluetoothDevice = device;
m_control = QLowEnergyController::createCentral(bluetoothDevice, this);
connect(m_control, &QLowEnergyController::serviceDiscovered, this, &mobirower::serviceDiscovered);
connect(m_control, &QLowEnergyController::discoveryFinished, this, &mobirower::serviceScanDone);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, &mobirower::error);
connect(m_control, &QLowEnergyController::stateChanged, this, &mobirower::controllerStateChanged);
connect(m_control,
static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
this, [this](QLowEnergyController::Error error) {
Q_UNUSED(error);
Q_UNUSED(this);
qDebug() << QStringLiteral("Cannot connect to remote device.");
emit disconnected();
});
connect(m_control, &QLowEnergyController::connected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("Controller connected. Search services...");
m_control->discoverServices();
});
connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
Q_UNUSED(this);
qDebug() << QStringLiteral("LowEnergy controller disconnected");
emit disconnected();
});
// Connect
m_control->connectToDevice();
return;
}
bool mobirower::connected() {
if (!m_control) {
return false;
}
return m_control->state() == QLowEnergyController::DiscoveredState;
}
uint16_t mobirower::watts() {
return m_watt.value();
}
void mobirower::controllerStateChanged(QLowEnergyController::ControllerState state) {
qDebug() << QStringLiteral("controllerStateChanged") << state;
if (state == QLowEnergyController::UnconnectedState && m_control) {
qDebug() << QStringLiteral("trying to connect back again...");
initDone = false;
m_control->connectToDevice();
}
}

View File

@@ -1,82 +0,0 @@
#ifndef MOBIROWER_H
#define MOBIROWER_H
#include <QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/qlowenergyadvertisingdata.h>
#include <QtBluetooth/qlowenergyadvertisingparameters.h>
#include <QtBluetooth/qlowenergycharacteristic.h>
#include <QtBluetooth/qlowenergycharacteristicdata.h>
#include <QtBluetooth/qlowenergycontroller.h>
#include <QtBluetooth/qlowenergydescriptordata.h>
#include <QtBluetooth/qlowenergyservice.h>
#include <QtBluetooth/qlowenergyservicedata.h>
#include <QtCore/qbytearray.h>
#ifndef Q_OS_ANDROID
#include <QtCore/qcoreapplication.h>
#else
#include <QtGui/qguiapplication.h>
#endif
#include <QtCore/qlist.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qtimer.h>
#include <QDateTime>
#include <QObject>
#include <QString>
#include "rower.h"
#ifdef Q_OS_IOS
#include "ios/lockscreen.h"
#endif
class mobirower : public rower {
Q_OBJECT
public:
mobirower(bool noWriteResistance, bool noHeartService);
bool connected() override;
private:
void startDiscover();
uint16_t watts() override;
QTimer *refresh;
QLowEnergyService *gattCommunicationChannelService = nullptr;
QLowEnergyCharacteristic gattNotifyCharacteristic;
uint8_t firstStateChanged = 0;
uint16_t lastStrokeCount = 0;
QDateTime lastRefreshCharacteristicChanged = QDateTime::currentDateTime();
bool initDone = false;
bool noWriteResistance = false;
bool noHeartService = false;
#ifdef Q_OS_IOS
lockscreen *h = 0;
#endif
Q_SIGNALS:
void disconnected();
public slots:
void deviceDiscovered(const QBluetoothDeviceInfo &device);
private slots:
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue);
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue);
void stateChanged(QLowEnergyService::ServiceState state);
void controllerStateChanged(QLowEnergyController::ControllerState state);
void serviceDiscovered(const QBluetoothUuid &gatt);
void serviceScanDone(void);
void update();
void error(QLowEnergyController::Error err);
void errorService(QLowEnergyService::ServiceError);
};
#endif // MOBIROWER_H

View File

@@ -391,6 +391,9 @@ bool GarminConnect::fetchCsrfToken()
bool GarminConnect::performLogin(const QString &email, const QString &password, bool suppressMfaSignal)
{
qDebug() << "GarminConnect: Performing login...";
qDebug() << "GarminConnect: Using domain:" << m_domain;
qDebug() << "GarminConnect: SSO URL:" << ssoUrl();
qDebug() << "GarminConnect: Connect API URL:" << connectApiUrl();
QString ssoEmbedUrl = ssoUrl() + SSO_EMBED_PATH;
@@ -452,15 +455,54 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
qDebug() << "GarminConnect: Login response length:" << response.length();
qDebug() << "GarminConnect: Response snippet:" << response.left(300);
// Check for success title (like Python garth library)
// Check page title (like Python garth library)
// garth checks ONLY the title for MFA detection, not the body
// This is important because some servers (like garmin.cn) may have "MFA" text
// in their Success page HTML body, which would cause false positives
QString pageTitle;
QRegularExpression titleRegex("<title>(.+?)</title>");
QRegularExpressionMatch titleMatch = titleRegex.match(response);
if (titleMatch.hasMatch()) {
QString title = titleMatch.captured(1);
qDebug() << "GarminConnect: Page title:" << title;
if (title == "Success") {
qDebug() << "GarminConnect: Login successful (Success page detected)";
pageTitle = titleMatch.captured(1);
qDebug() << "GarminConnect: Page title:" << pageTitle;
}
// Check if MFA is required by looking at the TITLE (garth approach)
// This is more reliable than checking the body which may contain "MFA" in scripts/URLs
if (pageTitle.contains("MFA", Qt::CaseInsensitive)) {
m_lastError = "MFA Required";
qDebug() << "GarminConnect: MFA detected in page title";
// Extract new CSRF token from MFA page - try multiple patterns
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
QRegularExpressionMatch match = csrfRegex1.match(response);
if (!match.hasMatch()) {
match = csrfRegex2.match(response);
}
if (match.hasMatch()) {
m_csrfToken = match.captured(1);
qDebug() << "GarminConnect: CSRF token from MFA page:" << m_csrfToken.left(20) << "...";
}
// Update cookies
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
if (!suppressMfaSignal) {
qDebug() << "GarminConnect: Emitting mfaRequired signal";
emit mfaRequired();
} else {
qDebug() << "GarminConnect: MFA required but signal suppressed (retrying with MFA code)";
}
reply->deleteLater();
return false;
}
// Check if login was successful (title is "Success")
if (pageTitle == "Success") {
qDebug() << "GarminConnect: Login successful (Success page detected)";
// Continue to extract ticket below
}
// Check for error messages in response
@@ -549,39 +591,17 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
return false;
}
// Check if MFA is required (legacy check for non-redirect MFA)
if (response.contains("MFA", Qt::CaseInsensitive) ||
response.contains("Enter MFA Code", Qt::CaseInsensitive)) {
m_lastError = "MFA Required";
qDebug() << "GarminConnect: MFA content detected in response";
// Extract new CSRF token from MFA page - try multiple patterns
QRegularExpression csrfRegex1("name=\"_csrf\"[^>]*value=\"([^\"]+)\"");
QRegularExpression csrfRegex2("value=\"([^\"]+)\"[^>]*name=\"_csrf\"");
QRegularExpressionMatch match = csrfRegex1.match(response);
if (!match.hasMatch()) {
match = csrfRegex2.match(response);
}
if (match.hasMatch()) {
m_csrfToken = match.captured(1);
}
// Update cookies
m_cookies = m_manager->cookieJar()->cookiesForUrl(url);
if (!suppressMfaSignal) {
emit mfaRequired();
}
reply->deleteLater();
return false;
}
// Extract ticket from response URL (already declared above)
if (responseUrl.isEmpty()) {
responseUrl = reply->url();
}
if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: Response URL:" << responseUrl.toString();
qDebug() << "GarminConnect: Response length:" << response.length();
qDebug() << "GarminConnect: Full response body:" << response;
}
QUrlQuery responseQuery(responseUrl);
QString ticket = responseQuery.queryItemValue("ticket");
@@ -599,6 +619,8 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
if (match.hasMatch()) {
ticket = match.captured(1);
qDebug() << "GarminConnect: Found ticket with fallback pattern:" << ticket.left(20) << "...";
} else if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: No ticket patterns matched in response body";
}
}
}
@@ -608,6 +630,9 @@ bool GarminConnect::performLogin(const QString &email, const QString &password,
if (ticket.isEmpty()) {
m_lastError = "Failed to extract ticket from login response";
qDebug() << "GarminConnect:" << m_lastError;
if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
}
return false;
}
@@ -708,8 +733,12 @@ void GarminConnect::handleMfaReplyFinished()
qDebug() << "GarminConnect: MFA response status code:" << statusCode;
qDebug() << "GarminConnect: MFA response redirect URL:" << responseUrl.toString();
// If no redirect, log response body to understand what happened
if (responseUrl.isEmpty()) {
// Log detailed response information
if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: MFA response length:" << response.length();
qDebug() << "GarminConnect: Full MFA response body:" << response;
} else if (responseUrl.isEmpty()) {
// If no redirect, log response body to understand what happened (non-verbose)
qDebug() << "GarminConnect: MFA response body (first 500 chars):" << response.left(500);
}
@@ -748,6 +777,9 @@ void GarminConnect::handleMfaReplyFinished()
// If not found in redirect URL, try response body
if (ticket.isEmpty() && !response.isEmpty()) {
if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: Attempting to extract ticket from MFA response body";
}
// Try multiple patterns for ticket extraction
QRegularExpression ticketRegex1("embed\\?ticket=([^\"]+)\"");
QRegularExpression ticketRegex2("ticket=([^&\"']+)");
@@ -761,6 +793,16 @@ void GarminConnect::handleMfaReplyFinished()
if (match.hasMatch()) {
ticket = match.captured(1);
qDebug() << "GarminConnect: Found ticket in response body (pattern 2):" << ticket.left(20) << "...";
} else if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: No MFA ticket patterns matched. Checking for other patterns...";
// Check for JSON format
if (response.contains("ticket")) {
qDebug() << "GarminConnect: Response contains 'ticket' keyword, may be JSON or different format";
}
// Check for common response patterns
if (response.contains("\"")) {
qDebug() << "GarminConnect: Response contains quoted strings (may be JSON)";
}
}
}
}
@@ -770,6 +812,9 @@ void GarminConnect::handleMfaReplyFinished()
if (ticket.isEmpty()) {
m_lastError = "Failed to extract ticket after MFA";
qDebug() << "GarminConnect:" << m_lastError;
if (DEBUG_GARMIN_VERBOSE) {
qDebug() << "GarminConnect: Response snippet:" << response.left(1000);
}
emit authenticationFailed(m_lastError);
return;
}
@@ -1401,6 +1446,7 @@ void GarminConnect::loadTokensFromSettings()
m_oauth1Token.oauth_token = settings.value(QZSettings::garmin_oauth1_token, QZSettings::default_garmin_oauth1_token).toString();
m_oauth1Token.oauth_token_secret = settings.value(QZSettings::garmin_oauth1_token_secret, QZSettings::default_garmin_oauth1_token_secret).toString();
m_domain = settings.value(QZSettings::garmin_domain, QZSettings::default_garmin_domain).toString();
qDebug() << "GarminConnect: Loaded Garmin domain from settings:" << m_domain;
if (!m_oauth2Token.access_token.isEmpty()) {
qDebug() << "GarminConnect: Loaded tokens from settings (OAuth1 + OAuth2)";

View File

@@ -176,6 +176,7 @@ private:
static constexpr const char* SSO_URL_PATH = "/sso/signin";
static constexpr const char* SSO_EMBED_PATH = "/sso/embed";
static constexpr const char* OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
static constexpr bool DEBUG_GARMIN_VERBOSE = false; // Set to true for detailed response logging (may contain sensitive data)
// Private methods
QString ssoUrl() const { return QString("https://sso.%1").arg(m_domain); }

View File

@@ -101,7 +101,6 @@ SOURCES += \
$$PWD/devices/pitpatbike/pitpatbike.cpp \
$$PWD/devices/speraxtreadmill/speraxtreadmill.cpp \
$$PWD/devices/sportsplusrower/sportsplusrower.cpp \
$$PWD/devices/mobirower/mobirower.cpp \
$$PWD/devices/sportstechelliptical/sportstechelliptical.cpp \
$$PWD/devices/sramAXSController/sramAXSController.cpp \
$$PWD/devices/stairclimber.cpp \
@@ -379,7 +378,6 @@ HEADERS += \
$$PWD/devices/pitpatbike/pitpatbike.h \
$$PWD/devices/speraxtreadmill/speraxtreadmill.h \
$$PWD/devices/sportsplusrower/sportsplusrower.h \
$$PWD/devices/mobirower/mobirower.h \
$$PWD/devices/sportstechelliptical/sportstechelliptical.h \
$$PWD/devices/sramAXSController/sramAXSController.h \
$$PWD/devices/stairclimber.h \

View File

@@ -6726,6 +6726,29 @@ import Qt.labs.platform 1.1
}
}
RowLayout {
spacing: 10
Label {
text: qsTr("Garmin Server:")
Layout.fillWidth: true
}
ComboBox {
id: garminServerComboBox
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
model: ["Global (garmin.com)", "China (garmin.cn)"]
currentIndex: settings.garmin_domain === "garmin.cn" ? 1 : 0
onCurrentIndexChanged: {
var newDomain = currentIndex === 1 ? "garmin.cn" : "garmin.com";
if (newDomain !== settings.garmin_domain) {
rootItem.garmin_connect_logout();
settings.garmin_domain = newDomain;
window.settings_restart_to_apply = true;
}
}
}
}
Button {
text: "Test Garmin Login"
Layout.alignment: Qt.AlignHCenter