Compare commits

...

12 Commits

Author SHA1 Message Date
Roberto Viola
97bc2a201c Update project.pbxproj 2025-04-03 09:33:16 +02:00
Roberto Viola
8f38f72cbe Merge branch 'master' into Powermeter-pédale-and-ERG-#2818 2025-04-03 09:32:20 +02:00
Roberto Viola
b3e180775b handling TITAN_7000 case 2025-04-01 10:53:06 +02:00
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 188 additions and 114 deletions

View File

@@ -4285,7 +4285,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = "ADB_HOST=1";
@@ -4479,7 +4479,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4709,7 +4709,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4805,7 +4805,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -4897,7 +4897,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5013,7 +5013,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1057;
CURRENT_PROJECT_VERSION = 1060;
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(";"));
}
};