Compare commits

...

9 Commits

Author SHA1 Message Date
Roberto Viola
648f798697 Update project.pbxproj 2025-04-01 10:35:12 +02:00
Roberto Viola
d69685224c Merge branch 'master' into Powermeter-pédale-and-ERG-#2818 2025-04-01 10:14:55 +02:00
Roberto Viola
7be7920fd4 handling TITAN_7000 case 2025-03-31 16:44:58 +02:00
Roberto Viola
a07d3d3e9f Merge branch 'master' into Powermeter-pédale-and-ERG-#2818 2025-03-31 11:08:36 +02:00
Roberto Viola
8f9270dca3 Delete src/ergtable_test.h 2025-03-31 09:27:09 +02:00
Roberto Viola
5654b2f950 Update main.cpp 2025-03-31 09:26:56 +02:00
Roberto Viola
b40839d466 Update qdomyos-zwift.pro 2025-03-31 09:26:32 +02:00
Roberto Viola
1d61778db1 Update ergtable_test.h 2024-12-05 16:41:34 +01:00
Roberto Viola
57172ec5dc Powermeter pédale and ERG #2818 2024-12-05 14:38:52 +01:00
4 changed files with 218 additions and 114 deletions

View File

@@ -532,6 +532,12 @@
87DAE16926E9FF5000B0527E /* moc_shuaa5treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */; };
87DAE16A26E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */; };
87DAE16B26E9FF5000B0527E /* moc_solef80treadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */; };
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */; };
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */; };
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */; };
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */; };
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */; };
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */; };
87DED80627D1273900BE4FBB /* filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80427D1273800BE4FBB /* filedownloader.cpp */; };
87DED80827D1274600BE4FBB /* moc_filedownloader.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */; };
87DF68B825E2673B00FCDA46 /* eslinkertreadmill.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 87DF68B625E2673600FCDA46 /* eslinkertreadmill.cpp */; };
@@ -1559,6 +1565,15 @@
87DAE16626E9FF5000B0527E /* moc_shuaa5treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_shuaa5treadmill.cpp; sourceTree = "<group>"; };
87DAE16726E9FF5000B0527E /* moc_kingsmithr2treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_kingsmithr2treadmill.cpp; sourceTree = "<group>"; };
87DAE16826E9FF5000B0527E /* moc_solef80treadmill.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_solef80treadmill.cpp; sourceTree = "<group>"; };
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = echelonstairclimber.h; path = ../src/devices/echelonstairclimber/echelonstairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = echelonstairclimber.cpp; path = ../src/devices/echelonstairclimber/echelonstairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27E82D9BDB53007A1B9D /* stairclimber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = stairclimber.h; path = ../src/devices/stairclimber.h; sourceTree = SOURCE_ROOT; };
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = stairclimber.cpp; path = ../src/devices/stairclimber.cpp; sourceTree = SOURCE_ROOT; };
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_echelonstairclimber.cpp; sourceTree = "<group>"; };
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_stairclimber.cpp; sourceTree = "<group>"; };
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = moc_moxy5sensor.cpp; sourceTree = "<group>"; };
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = moxy5sensor.h; path = ../src/devices/moxy5sensor/moxy5sensor.h; sourceTree = SOURCE_ROOT; };
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = moxy5sensor.cpp; path = ../src/devices/moxy5sensor/moxy5sensor.cpp; sourceTree = SOURCE_ROOT; };
87DED80427D1273800BE4FBB /* filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = filedownloader.cpp; path = ../src/filedownloader.cpp; sourceTree = "<group>"; };
87DED80527D1273900BE4FBB /* filedownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = filedownloader.h; path = ../src/filedownloader.h; sourceTree = "<group>"; };
87DED80727D1274500BE4FBB /* moc_filedownloader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = moc_filedownloader.cpp; sourceTree = "<group>"; };
@@ -2179,6 +2194,11 @@
2EB56BE3C2D93CDAB0C52E67 /* Sources */ = {
isa = PBXGroup;
children = (
87DC27F02D9BDC43007A1B9D /* moc_moxy5sensor.cpp */,
87DC27F12D9BDC43007A1B9D /* moxy5sensor.h */,
87DC27F22D9BDC43007A1B9D /* moxy5sensor.cpp */,
87DC27EC2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp */,
87DC27ED2D9BDB8F007A1B9D /* moc_stairclimber.cpp */,
8798FDC02D66075B00CF8EE8 /* osxbluetooth_p.h */,
8798FDC12D66075B00CF8EE8 /* osxbtcentralmanager_p.h */,
8798FDC22D66075B00CF8EE8 /* osxbtgcdtimer_p.h */,
@@ -2674,6 +2694,10 @@
87FE06822D170D5600CDAAF6 /* trxappgateusbrower.h */,
87FE06832D170D5600CDAAF6 /* trxappgateusbrower.cpp */,
87FE06802D170D3C00CDAAF6 /* moc_trxappgateusbrower.cpp */,
87DC27E62D9BDB53007A1B9D /* echelonstairclimber.h */,
87DC27E72D9BDB53007A1B9D /* echelonstairclimber.cpp */,
87DC27E82D9BDB53007A1B9D /* stairclimber.h */,
87DC27E92D9BDB53007A1B9D /* stairclimber.cpp */,
);
name = Sources;
sourceTree = "<group>";
@@ -3441,6 +3465,8 @@
873824C027E64707004F1B46 /* moc_dirconmanager.cpp in Compile Sources */,
2F0E70F826316F3600E11F3A /* virtualbike_zwift.swift in Compile Sources */,
8772A0E825E43AE70080718C /* moc_trxappgateusbbike.cpp in Compile Sources */,
87DC27EE2D9BDB8F007A1B9D /* moc_stairclimber.cpp in Compile Sources */,
87DC27EF2D9BDB8F007A1B9D /* moc_echelonstairclimber.cpp in Compile Sources */,
87062646259480B200D06586 /* ViewController.swift in Compile Sources */,
87D269A425F535340076AA48 /* moc_m3ibike.cpp in Compile Sources */,
87BAFE482B8CA7AA00065FCD /* moc_focustreadmill.cpp in Compile Sources */,
@@ -3576,6 +3602,8 @@
8772B7F42CB55E80004AB8E9 /* moc_deerruntreadmill.cpp in Compile Sources */,
87CC3BA425A0885F001EC5A8 /* elliptical.cpp in Compile Sources */,
4AD2C93A2B8FD5855E521630 /* fit_encode.cpp in Compile Sources */,
87DC27F32D9BDC43007A1B9D /* moc_moxy5sensor.cpp in Compile Sources */,
87DC27F42D9BDC43007A1B9D /* moxy5sensor.cpp in Compile Sources */,
87EB918C27EE5FE7002535E1 /* moc_inappproduct.cpp in Compile Sources */,
87E34C2D2886F99A00CEDE4B /* moc_octanetreadmill.cpp in Compile Sources */,
87D91F9A2800B9970026D43C /* proformwifibike.cpp in Compile Sources */,
@@ -3805,6 +3833,8 @@
876BFCA027BE35D8001D7645 /* moc_proformelliptical.cpp in Compile Sources */,
873824BD27E64707004F1B46 /* moc_resolver_p.cpp in Compile Sources */,
876ED21A25C3E9010065F3DC /* moc_material.cpp in Compile Sources */,
87DC27EA2D9BDB53007A1B9D /* echelonstairclimber.cpp in Compile Sources */,
87DC27EB2D9BDB53007A1B9D /* stairclimber.cpp in Compile Sources */,
87A4B76125AF27CB0027EF3C /* metric.cpp in Compile Sources */,
87D10552290996EA00B3935B /* mepanelbike.cpp in Compile Sources */,
9D9484EED654597C394345DE /* moc_echelonconnectsport.cpp in Compile Sources */,
@@ -4255,7 +4285,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4449,7 +4479,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4679,7 +4709,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4775,7 +4805,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4867,7 +4897,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -4983,7 +5013,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1037;
CURRENT_PROJECT_VERSION = 1057;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -193,7 +193,7 @@ void ftmsbike::zwiftPlayInit() {
}
void ftmsbike::forcePower(int16_t requestPower) {
if(resistance_lvl_mode) {
if(resistance_lvl_mode || TITAN_7000) {
forceResistance(resistanceFromPowerRequest(requestPower));
} else {
uint8_t write[] = {FTMS_SET_TARGET_POWER, 0x00, 0x00};
@@ -239,11 +239,15 @@ void ftmsbike::forceResistance(resistance_t requestResistance) {
QSettings settings;
if (!settings.value(QZSettings::ss2k_peloton, QZSettings::default_ss2k_peloton).toBool() &&
resistance_lvl_mode == false && _3G_Cardio_RB == false && JFBK5_0 == false) {
uint8_t write[] = {FTMS_SET_INDOOR_BIKE_SIMULATION_PARAMS, 0x00, 0x00, 0x00, 0x00, 0x28, 0x19};
double fr = (((double)requestResistance) * bikeResistanceGain) + ((double)bikeResistanceOffset);
requestResistance = fr;
if(TITAN_7000)
Resistance = requestResistance;
write[3] = ((uint16_t)requestResistance * 10) & 0xFF;
write[4] = ((uint16_t)requestResistance * 10) >> 8;
@@ -585,7 +589,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)
if(!SL010 && !TITAN_7000)
Resistance = d;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
emit resistanceRead(Resistance.value());
@@ -792,9 +796,11 @@ settings.value(QZSettings::peloton_offset, QZSettings::default_peloton_offset).t
}
if (Flags.resistanceLvl) {
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
emit resistanceRead(Resistance.value());
if(!TITAN_7000) {
Resistance = ((double)(((uint16_t)((uint8_t)newValue.at(index + 1)) << 8) |
(uint16_t)((uint8_t)newValue.at(index))));
emit resistanceRead(Resistance.value());
}
index += 2;
emit debug(QStringLiteral("Current Resistance: ") + QString::number(Resistance.value()));
} else if(!DU30_bike) {
@@ -1327,6 +1333,9 @@ void ftmsbike::deviceDiscovered(const QBluetoothDeviceInfo &device) {
REEBOK = true;
max_resistance = 32;
resistance_lvl_mode = true;
} else if ((bluetoothDevice.name().toUpper().startsWith("Titan 7000"))) {
qDebug() << QStringLiteral("Titan 7000 found");
TITAN_7000 = true;
}
if(settings.value(QZSettings::force_resistance_instead_inclination, QZSettings::default_force_resistance_instead_inclination).toBool()) {

View File

@@ -139,6 +139,7 @@ class ftmsbike : public bike {
bool LYDSTO = false;
bool SL010 = false;
bool REEBOK = false;
bool TITAN_7000 = false;
uint8_t battery_level = 0;

View File

@@ -6,24 +6,82 @@
#include <QObject>
#include <QDebug>
#include <QDateTime>
#include <QMap>
#include <algorithm>
#include "qzsettings.h"
struct ergDataPoint {
uint16_t cadence = 0; // RPM
uint16_t wattage = 0; // Watts
uint16_t resistance = 0; // Some unit
uint16_t cadence = 0;
uint16_t wattage = 0;
uint16_t resistance = 0;
ergDataPoint() = default;
ergDataPoint(uint16_t c, uint16_t w, uint16_t r) : cadence(c), wattage(w), resistance(r) {}
};
Q_DECLARE_METATYPE(ergDataPoint)
struct CadenceResistancePair {
uint16_t cadence;
uint16_t resistance;
bool operator<(const CadenceResistancePair& other) const {
if (resistance != other.resistance) return resistance < other.resistance;
return cadence < other.cadence;
}
};
class WattageStats {
public:
static const int MAX_SAMPLES = 100;
static const int MIN_SAMPLES_REQUIRED = 10;
void addSample(uint16_t wattage) {
samples.append(wattage);
if (samples.size() > MAX_SAMPLES) {
samples.removeFirst();
}
medianNeedsUpdate = true;
}
uint16_t getMedian() {
if (!medianNeedsUpdate) return cachedMedian;
if (samples.isEmpty()) return 0;
QList<uint16_t> sortedSamples = samples;
std::sort(sortedSamples.begin(), sortedSamples.end());
int middle = sortedSamples.size() / 2;
if (sortedSamples.size() % 2 == 0) {
cachedMedian = (sortedSamples[middle-1] + sortedSamples[middle]) / 2;
} else {
cachedMedian = sortedSamples[middle];
}
medianNeedsUpdate = false;
return cachedMedian;
}
int sampleCount() const {
return samples.size();
}
void clear() {
samples.clear();
cachedMedian = 0;
medianNeedsUpdate = true;
}
private:
QList<uint16_t> samples;
uint16_t cachedMedian = 0;
bool medianNeedsUpdate = true;
};
class ergTable : public QObject {
Q_OBJECT
public:
public:
ergTable(QObject *parent = nullptr) : QObject(parent) {
loadSettings();
}
@@ -32,126 +90,139 @@ public:
saveSettings();
}
void reset() {
wattageData.clear();
consolidatedData.clear();
lastResistanceValue = 0xFFFF;
lastResistanceTime = QDateTime::currentDateTime();
// Clear the settings completely
QSettings settings;
settings.remove(QZSettings::ergDataPoints);
settings.sync();
}
void collectData(uint16_t cadence, uint16_t wattage, uint16_t resistance, bool ignoreResistanceTiming = false) {
if(resistance != lastResistanceValue) {
if (resistance != lastResistanceValue) {
qDebug() << "resistance changed";
lastResistanceTime = QDateTime::currentDateTime();
lastResistanceValue = resistance;
}
if(lastResistanceTime.msecsTo(QDateTime::currentDateTime()) < 1000 && ignoreResistanceTiming == false) {
if (lastResistanceTime.msecsTo(QDateTime::currentDateTime()) < 1000 && !ignoreResistanceTiming) {
qDebug() << "skipping collecting data due to resistance changing too fast";
return;
}
if (wattage > 0 && cadence > 0 && !ergDataPointExists(cadence, wattage, resistance)) {
qDebug() << "newPointAdded" << "C" << cadence << "W" << wattage << "R" << resistance;
ergDataPoint point(cadence, wattage, resistance);
dataTable.append(point);
saveergDataPoint(point); // Save each new point to QSettings
} else {
qDebug() << "discarded" << "C" << cadence << "W" << wattage << "R" << resistance;
if (wattage > 0 && cadence > 0) {
CadenceResistancePair pair{cadence, resistance};
wattageData[pair].addSample(wattage);
if (wattageData[pair].sampleCount() >= WattageStats::MIN_SAMPLES_REQUIRED) {
updateDataTable(pair);
}
}
}
double estimateWattage(uint16_t givenCadence, uint16_t givenResistance) {
QList<ergDataPoint> filteredByResistance;
double minResDiff = std::numeric_limits<double>::max();
if (consolidatedData.isEmpty()) return 0;
// Initial filtering by resistance
for (const ergDataPoint& point : dataTable) {
double resDiff = std::abs(point.resistance - givenResistance);
if (resDiff < minResDiff) {
filteredByResistance.clear();
filteredByResistance.append(point);
minResDiff = resDiff;
} else if (resDiff == minResDiff) {
filteredByResistance.append(point);
// Get all points with matching resistance
QList<ergDataPoint> sameResPoints;
for (const auto& point : consolidatedData) {
if (point.resistance == givenResistance) {
sameResPoints.append(point);
}
}
// Fallback search if no close resistance match is found
if (filteredByResistance.isEmpty()) {
double minSimilarity = std::numeric_limits<double>::max();
ergDataPoint closestPoint;
// If no exact resistance match, find closest resistance
if (sameResPoints.isEmpty()) {
uint16_t minResDiff = UINT16_MAX;
uint16_t closestRes = 0;
for (const ergDataPoint& point : dataTable) {
double cadenceDiff = std::abs(point.cadence - givenCadence);
double resDiff = std::abs(point.resistance - givenResistance);
// Weighted similarity measure: Giving more weight to resistance
double similarity = resDiff * 2 + cadenceDiff;
if (similarity < minSimilarity) {
minSimilarity = similarity;
closestPoint = point;
for (const auto& point : consolidatedData) {
uint16_t resDiff = abs(int(point.resistance) - int(givenResistance));
if (resDiff < minResDiff) {
minResDiff = resDiff;
closestRes = point.resistance;
}
}
qDebug() << "case1" << closestPoint.wattage;
// Use the wattage of the closest match based on similarity
return closestPoint.wattage;
}
// Find lower and upper points based on cadence within the filtered list
double lowerDiff = std::numeric_limits<double>::max();
double upperDiff = std::numeric_limits<double>::max();
ergDataPoint lowerPoint, upperPoint;
for (const ergDataPoint& point : filteredByResistance) {
double cadenceDiff = std::abs(point.cadence - givenCadence);
if (point.cadence <= givenCadence && cadenceDiff < lowerDiff) {
lowerDiff = cadenceDiff;
lowerPoint = point;
} else if (point.cadence > givenCadence && cadenceDiff < upperDiff) {
upperDiff = cadenceDiff;
upperPoint = point;
for (const auto& point : consolidatedData) {
if (point.resistance == closestRes) {
sameResPoints.append(point);
}
}
}
double r;
// Find points for interpolation
double lowerWatts = 0, upperWatts = 0;
uint16_t lowerCadence = 0, upperCadence = 0;
// Estimate wattage
if (lowerDiff != std::numeric_limits<double>::max() && upperDiff != std::numeric_limits<double>::max() && lowerDiff !=0 && upperDiff != 0) {
// Interpolation between lower and upper points
double cadenceRatio = 1.0;
if (upperPoint.cadence != lowerPoint.cadence) { // Avoid division by zero
cadenceRatio = (givenCadence - lowerPoint.cadence) / (double)(upperPoint.cadence - lowerPoint.cadence);
for (const auto& point : sameResPoints) {
if (point.cadence <= givenCadence && point.cadence > lowerCadence) {
lowerWatts = point.wattage;
lowerCadence = point.cadence;
}
if (point.cadence >= givenCadence && (upperCadence == 0 || point.cadence < upperCadence)) {
upperWatts = point.wattage;
upperCadence = point.cadence;
}
r = lowerPoint.wattage + (upperPoint.wattage - lowerPoint.wattage) * cadenceRatio;
//qDebug() << "case2" << r << lowerPoint.wattage << upperPoint.wattage << lowerPoint.cadence << upperPoint.cadence << cadenceRatio << lowerDiff << upperDiff;
return r;
} else if (lowerDiff == 0) {
//qDebug() << "case3" << lowerPoint.wattage;
return lowerPoint.wattage;
} else if (upperDiff == 0) {
//qDebug() << "case4" << upperPoint.wattage;
return upperPoint.wattage;
} else {
r = (lowerDiff < upperDiff) ? lowerPoint.wattage : upperPoint.wattage;
//qDebug() << "case5" << r;
// Use the closest point if only one match is found
return r;
}
// Interpolate or use closest value
if (lowerCadence != 0 && upperCadence != 0 && lowerCadence != upperCadence) {
double ratio = (givenCadence - lowerCadence) / double(upperCadence - lowerCadence);
return lowerWatts + ratio * (upperWatts - lowerWatts);
} else if (lowerCadence != 0) {
return lowerWatts;
} else if (upperCadence != 0) {
return upperWatts;
}
// Fallback to closest point
return sameResPoints.first().wattage;
}
QList<ergDataPoint> getConsolidatedData() const {
return consolidatedData;
}
private:
QList<ergDataPoint> dataTable;
QMap<CadenceResistancePair, WattageStats> getWattageData() const {
return wattageData;
}
private:
QMap<CadenceResistancePair, WattageStats> wattageData;
QList<ergDataPoint> consolidatedData;
uint16_t lastResistanceValue = 0xFFFF;
QDateTime lastResistanceTime = QDateTime::currentDateTime();
bool ergDataPointExists(uint16_t cadence, uint16_t wattage, uint16_t resistance) {
for (const ergDataPoint& point : dataTable) {
if (point.cadence == cadence && point.resistance == resistance && cadence != 0 && wattage != 0) {
return true; // Found duplicate
void updateDataTable(const CadenceResistancePair& pair) {
uint16_t medianWattage = wattageData[pair].getMedian();
// Remove existing point if it exists
for (int i = consolidatedData.size() - 1; i >= 0; --i) {
if (consolidatedData[i].cadence == pair.cadence &&
consolidatedData[i].resistance == pair.resistance) {
consolidatedData.removeAt(i);
break;
}
}
return false; // No duplicate
// Add new point
consolidatedData.append(ergDataPoint(pair.cadence, medianWattage, pair.resistance));
qDebug() << "Added/Updated point:"
<< "C:" << pair.cadence
<< "W:" << medianWattage
<< "R:" << pair.resistance;
}
void loadSettings() {
QSettings settings;
QString data = settings.value(QZSettings::ergDataPoints, QZSettings::default_ergDataPoints).toString();
QStringList dataList = data.split(";");
QString data = settings.value(QZSettings::ergDataPoints,
QZSettings::default_ergDataPoints).toString();
QStringList dataList = data.split(";", Qt::SkipEmptyParts);
for (const QString& triple : dataList) {
QStringList fields = triple.split("|");
@@ -159,29 +230,22 @@ private:
uint16_t cadence = fields[0].toUInt();
uint16_t wattage = fields[1].toUInt();
uint16_t resistance = fields[2].toUInt();
//qDebug() << "inputs.append(ergDataPoint(" << cadence << ", " << wattage << ", "<< resistance << "));";
dataTable.append(ergDataPoint(cadence, wattage, resistance));
consolidatedData.append(ergDataPoint(cadence, wattage, resistance));
}
}
}
void saveSettings() {
QSettings settings;
QString data;
for (const ergDataPoint& point : dataTable) {
QString triple = QString::number(point.cadence) + "|" + QString::number(point.wattage) + "|" + QString::number(point.resistance);
data += triple + ";";
}
settings.setValue(QZSettings::ergDataPoints, data);
}
QStringList dataStrings;
void saveergDataPoint(const ergDataPoint& point) {
QSettings settings;
QString data = settings.value(QZSettings::ergDataPoints, QZSettings::default_ergDataPoints).toString();
data += QString::number(point.cadence) + "|" + QString::number(point.wattage) + "|" + QString::number(point.resistance) + ";";
settings.setValue(QZSettings::ergDataPoints, data);
for (const ergDataPoint& point : consolidatedData) {
dataStrings.append(QString("%1|%2|%3").arg(point.cadence)
.arg(point.wattage)
.arg(point.resistance));
}
settings.setValue(QZSettings::ergDataPoints, dataStrings.join(";"));
}
};