Skip to content

Commit 0268b63

Browse files
authored
Merge pull request #698 from doudar/Resistance_level_fix
Resistance level fix
2 parents 6260ba9 + 5cfa349 commit 0268b63

File tree

8 files changed

+146
-53
lines changed

8 files changed

+146
-53
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Hardware
1515

1616

17+
## [25.11.4]
18+
19+
### Added
20+
21+
### Changed
22+
- Added support for reading resistance range from connected FTMS devices
23+
- Improved resistance mode control logic for bikes with and without native resistance reporting
24+
- Fixed resistance value parsing to correctly handle 16-bit values
25+
- Reduced default max brake watts from 1400w to 1000w.
26+
27+
### Hardware
28+
29+
1730
## [25.10.19]
1831

1932
### Added

include/SmartSpin_parameters.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class Measurement {
2222
bool simulate;
2323
int value;
2424
int target;
25+
int min;
26+
int max;
2527
unsigned long timestamp;
2628

2729
public:
@@ -41,6 +43,13 @@ class Measurement {
4143
target = tar;
4244
this->timestamp = millis();
4345
}
46+
47+
void setMin(int min) { this->min = min; }
48+
int getMin() { return min; }
49+
50+
void setMax(int max) { this->max = max; }
51+
int getMax() { return max; }
52+
4453
int getTarget() { return target; }
4554

4655
long getTimestamp() { return timestamp; }
@@ -49,6 +58,8 @@ class Measurement {
4958
this->simulate = false;
5059
this->value = 0;
5160
this->target = 0;
61+
this->min = 0;
62+
this->max = 0;
5263
this->timestamp = millis();
5364
}
5465
};

include/settings.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const char* const DEFAULT_PASSWORD = "password";
9595

9696
// Default Max Watts that the brake on the spin bike can absorb from the user.
9797
// This is used to set the upper travel limit for the motor.
98-
#define DEFAULT_MAX_WATTS 1400
98+
#define DEFAULT_MAX_WATTS 1000
9999

100100
// Minimum resistance on a Peloton Bike.
101101
// This is used to set the lower travel limit for the motor.

src/BLE_Client.cpp

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ void ScanCallbacks::onResult(const NimBLEAdvertisedDevice* advertisedDevice) {
401401
if (serviceInfo) {
402402
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Supported Device: %s with service %s", aDevName.c_str(), serviceInfo->name.c_str());
403403
const NimBLEUUID& primaryServiceUUID = serviceInfo->serviceUUID;
404-
//check to see if we're already connected to this device
404+
// check to see if we're already connected to this device
405405
for (size_t i = 0; i < NUM_BLE_DEVICES; i++) {
406406
if (spinBLEClient.myBLEDevices[i].advertisedDevice != nullptr) {
407407
if (aDevName == String(spinBLEClient.myBLEDevices[i].uniqueName.c_str())) {
@@ -671,50 +671,80 @@ void SpinBLEClient::postConnect() {
671671
// Enable device notifications
672672
byte message[] = {0xF0, 0xB0, 0x01, 0x01, 0xA2};
673673
writeCharacteristic->writeValue(message, 5);
674-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Activated Echelon callbacks.");
674+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Activated Echelon callbacks on device: %s", _BLEd.uniqueName.c_str());
675675
rtConfig->setMinResistance(MIN_ECHELON_RESISTANCE);
676676
rtConfig->setMaxResistance(MAX_ECHELON_RESISTANCE);
677677
}
678678

679-
if ((_BLEd.charUUID == FITNESSMACHINEINDOORBIKEDATA_UUID)) {
680-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Updating Connection Params for: %s", _BLEd.peerAddress.toString().c_str());
681-
spinBLEClient.handleBattInfo(pClient, true);
679+
if (pClient->getService(FITNESSMACHINESERVICE_UUID)) {
680+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Initializing FTMS on device: %s", _BLEd.uniqueName.c_str());
682681

683682
auto featuresCharacteristic = pClient->getService(FITNESSMACHINESERVICE_UUID)->getCharacteristic(FITNESSMACHINEFEATURE_UUID);
684683
if (featuresCharacteristic == nullptr) {
685684
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Failed to find FTMS features characteristic UUID: %s", FITNESSMACHINEFEATURE_UUID.toString().c_str());
686-
return;
687-
}
688-
689-
if (featuresCharacteristic->canRead()) {
690-
auto value = featuresCharacteristic->readValue();
691-
if (value.size() < sizeof(uint64_t)) {
692-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Failed to read FTMS features characteristic");
693-
return;
694-
}
695-
696-
// We're only interested in the machine fitness features, not the target setting features.
697-
auto features = *reinterpret_cast<const uint32_t*>(value.data());
698-
if (!(features & FitnessMachineFeatureFlags::Types::ElapsedTimeSupported) || !(features & FitnessMachineFeatureFlags::Types::RemainingTimeSupported)) {
699-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "FTMS Control Point StartOrResume not supported");
700-
return;
685+
} else {
686+
if (featuresCharacteristic->canRead()) {
687+
auto value = featuresCharacteristic->readValue();
688+
if (value.size() < sizeof(uint64_t)) {
689+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Failed to read FTMS features characteristic for: %s", _BLEd.uniqueName.c_str());
690+
} else {
691+
// We're only interested in the machine fitness features, not the target setting features.
692+
auto features = *reinterpret_cast<const uint32_t*>(value.data());
693+
if (!(features & FitnessMachineFeatureFlags::Types::ElapsedTimeSupported) || !(features & FitnessMachineFeatureFlags::Types::RemainingTimeSupported)) {
694+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "FTMS Control Point StartOrResume not supported on: %s", _BLEd.uniqueName.c_str());
695+
}
696+
697+
NimBLERemoteCharacteristic* writeCharacteristic = pClient->getService(FITNESSMACHINESERVICE_UUID)->getCharacteristic(FITNESSMACHINECONTROLPOINT_UUID);
698+
if (writeCharacteristic == nullptr) {
699+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Failed to find FTMS control characteristic UUID: %s, on %s", FITNESSMACHINECONTROLPOINT_UUID.toString().c_str(),
700+
_BLEd.uniqueName.c_str());
701+
} else {
702+
// If we would like to control an external FTMS trainer. With most spin bikes we would want this off, but it's useful if you want to use the SmartSpin2k as an
703+
// appliance.
704+
if (userConfig->getFTMSControlPointWrite()) {
705+
writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::RequestControl, 1);
706+
delay(BLE_NOTIFY_DELAY);
707+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Activated FTMS Training on device: %s", _BLEd.uniqueName.c_str());
708+
}
709+
writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::StartOrResume, 1);
710+
}
711+
}
701712
}
702713
}
714+
// update resistance range if supported:
715+
auto resistanceRangeCharacteristic = pClient->getService(FITNESSMACHINESERVICE_UUID)->getCharacteristic(FITNESSMACHINERESISTANCELEVELRANGE_UUID);
716+
if (resistanceRangeCharacteristic && resistanceRangeCharacteristic->canRead()) {
717+
auto rr = resistanceRangeCharacteristic->readValue();
718+
if (rr.size() >= 6) {
719+
const uint8_t* b = reinterpret_cast<const uint8_t*>(rr.data());
720+
int16_t minRaw = static_cast<int16_t>(b[0] | (static_cast<uint16_t>(b[1]) << 8));
721+
int16_t maxRaw = static_cast<int16_t>(b[2] | (static_cast<uint16_t>(b[3]) << 8));
722+
uint16_t incRaw = static_cast<uint16_t>(b[4] | (static_cast<uint16_t>(b[5]) << 8));
723+
724+
float incF = static_cast<float>(incRaw) / 10.0f; // FTMS resolution 0.1, convert to actual increment value
725+
float minF = (static_cast<float>(minRaw) / 10.0f)/incF; // Convert FTMS 0.1 units to normalized scale
726+
float maxF = (static_cast<float>(maxRaw) / 10.0f)/incF; // Convert FTMS 0.1 units to normalized scale
727+
728+
// Internal resistance is integer-based; round to nearest
729+
int minRes = static_cast<int>(minF >= 0.0f ? (minF + 0.5f) : (minF - 0.5f));
730+
int maxRes = static_cast<int>(maxF >= 0.0f ? (maxF + 0.5f) : (maxF - 0.5f));
731+
732+
if (minRes > maxRes) {
733+
// Defensive: swap if device reports reversed
734+
int tmp = minRes;
735+
minRes = maxRes;
736+
maxRes = tmp;
737+
}
703738

704-
NimBLERemoteCharacteristic* writeCharacteristic = pClient->getService(FITNESSMACHINESERVICE_UUID)->getCharacteristic(FITNESSMACHINECONTROLPOINT_UUID);
705-
if (writeCharacteristic == nullptr) {
706-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Failed to find FTMS control characteristic UUID: %s", FITNESSMACHINECONTROLPOINT_UUID.toString().c_str());
707-
return;
708-
}
709-
710-
// If we would like to control an external FTMS trainer. With most spin bikes we would want this off, but it's useful if you want to use the SmartSpin2k as an
711-
// appliance.
712-
if (userConfig->getFTMSControlPointWrite()) {
713-
writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::RequestControl, 1);
714-
delay(BLE_NOTIFY_DELAY);
715-
SS2K_LOG(BLE_CLIENT_LOG_TAG, "Activated FTMS Training.");
739+
rtConfig->resistance.setMin(minRes);
740+
rtConfig->resistance.setMax(maxRes);
741+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "FTMS Resistance Range: raw min=%.1f raw max=%.1f inc=%.1f -> set %d->%d", minF, maxF, incF, minRes, maxRes);
742+
} else {
743+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "FTMS Resistance Range characteristic too short (%d bytes)", rr.size());
744+
}
745+
} else {
746+
SS2K_LOG(BLE_CLIENT_LOG_TAG, "FTMS Resistance Range characteristic unavailable or unreadable");
716747
}
717-
writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::StartOrResume, 1);
718748
}
719749
}
720750
}

src/BLE_Fitness_Machine_Service.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ BLE_Fitness_Machine_Service::BLE_Fitness_Machine_Service()
2323

2424
void BLE_Fitness_Machine_Service::setupService(NimBLEServer *pServer, MyCharacteristicCallbacks *chrCallbacks) {
2525
// Resistance, IPower, HeartRate
26-
uint8_t ftmsResistanceLevelRange[6] = {0x01, 0x00, 0x64, 0x00, 0x01, 0x00}; // 1:100 increment 1
26+
uint8_t ftmsResistanceLevelRange[6] = {0x01, 0x00, 0x64, 0x00, 0x0A, 0x00}; // .1:10 increment .1
2727
uint8_t ftmsPowerRange[6] = {0x01, 0x00, 0xA0, 0x0F, 0x01, 0x00}; // 1:4000 watts increment 1
2828
uint8_t ftmsInclinationRange[6] = {0x38, 0xff, 0xc8, 0x00, 0x01, 0x00}; // -20.0:20.0 increment .1
2929
// Fitness Machine Feature Flags Setup
@@ -99,7 +99,7 @@ void BLE_Fitness_Machine_Service::update() {
9999
// Add resistance
100100
int resistanceValue;
101101
// Check if bike has resistance reporting capability or resistance simulation enabled
102-
bool hasResistanceReporting = (rtConfig->resistance.getSimulate() ||
102+
bool hasResistanceReporting = (!rtConfig->resistance.getSimulate() &&
103103
(rtConfig->resistance.getTimestamp() > 0 &&
104104
(millis() - rtConfig->resistance.getTimestamp()) < 5000));
105105

@@ -109,6 +109,8 @@ void BLE_Fitness_Machine_Service::update() {
109109
} else {
110110
// Calculate resistance from stepper position for bikes that don't report resistance
111111
resistanceValue = this->calculateResistanceFromPosition();
112+
rtConfig->resistance.setValue(resistanceValue);
113+
rtConfig->resistance.setSimulate(true); // Mark as simulated
112114
}
113115
ftmsIndoorBikeData.push_back(static_cast<uint8_t>(resistanceValue & 0xff));
114116
ftmsIndoorBikeData.push_back(static_cast<uint8_t>(resistanceValue >> 8));
@@ -190,13 +192,13 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() {
190192

191193
case FitnessMachineControlPointProcedure::SetTargetResistanceLevel: {
192194
rtConfig->setFTMSMode((uint8_t)rxValue[0]);
193-
int16_t requestedResistance = (int16_t)rxValue[1];
195+
int16_t requestedResistance = (int16_t)((rxValue[2] << 8) | rxValue[1]);
194196

195197
if (requestedResistance >= rtConfig->getMinResistance() && requestedResistance <= rtConfig->getMaxResistance()) {
196198
rtConfig->resistance.setTarget(requestedResistance);
197199

198200
// For bikes that don't report resistance, calculate stepper position from resistance level (0-100)
199-
bool hasResistanceReporting = (rtConfig->resistance.getSimulate() ||
201+
bool hasResistanceReporting = (!rtConfig->resistance.getSimulate() &&
200202
(rtConfig->resistance.getTimestamp() > 0 &&
201203
(millis() - rtConfig->resistance.getTimestamp()) < 5000));
202204

src/Main.cpp

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@ HardwareSerial auxSerial(1);
3434
AuxSerialBuffer auxSerialBuffer;
3535

3636
FastAccelStepperEngine engine = FastAccelStepperEngine();
37-
FastAccelStepper *stepper = NULL;
37+
FastAccelStepper* stepper = NULL;
3838

3939
TaskHandle_t maintenanceLoopTask;
4040

4141
Boards boards;
4242
Board currentBoard;
4343

4444
///////////// Initialize the Config /////////////
45-
ErgMode *ergMode = new ErgMode;
46-
PowerTable *powerTable = new PowerTable;
47-
SS2K *ss2k = new SS2K;
48-
userParameters *userConfig = new userParameters;
49-
RuntimeParameters *rtConfig = new RuntimeParameters;
45+
ErgMode* ergMode = new ErgMode;
46+
PowerTable* powerTable = new PowerTable;
47+
SS2K* ss2k = new SS2K;
48+
userParameters* userConfig = new userParameters;
49+
RuntimeParameters* rtConfig = new RuntimeParameters;
5050

5151
///////////// Log Appender /////////////
5252
UdpAppender udpAppender;
@@ -178,12 +178,12 @@ void loop() { // Delete this task so we can make one that's more memory efficie
178178
vTaskDelete(NULL);
179179
}
180180

181-
void SS2K::maintenanceLoop(void *pvParameters) {
181+
void SS2K::maintenanceLoop(void* pvParameters) {
182182
static unsigned long intervalTimer2 = millis();
183183
static unsigned long rebootTimer = millis();
184184

185185
while (true) {
186-
delay(5);
186+
delay(10);
187187

188188
// be quiet while updating via BLE
189189
if (!ss2k->isUpdating) {
@@ -321,8 +321,9 @@ void SS2K::maintenanceLoop(void *pvParameters) {
321321
}
322322
#endif // DEBUG_STACK
323323
// Log userParameters
324-
SS2K_LOG(MAIN_LOG_TAG, "PM Con %d, CAD con %d, HRM Con %d, W %d, Cad %d, HR %d, Gear %d, Target Position %d", spinBLEClient.connectedPM, spinBLEClient.connectedCD,
325-
spinBLEClient.connectedHRM, rtConfig->watts.getValue(), rtConfig->cad.getValue(), rtConfig->hr.getValue(), rtConfig->getShifterPosition(), ss2k->targetPosition);
324+
SS2K_LOG(MAIN_LOG_TAG, "PM Con %d, CAD con %d, HRM Con %d, W %d, Cad %d, HR %d, Gear %d, Res %d, Target Position %d", spinBLEClient.connectedPM, spinBLEClient.connectedCD,
325+
spinBLEClient.connectedHRM, rtConfig->watts.getValue(), rtConfig->cad.getValue(), rtConfig->hr.getValue(), rtConfig->getShifterPosition(),
326+
rtConfig->resistance.getValue(), ss2k->targetPosition);
326327

327328
intervalTimer2 = millis();
328329
}
@@ -433,9 +434,44 @@ void SS2K::moveStepper() {
433434
}
434435
#endif
435436
ss2k->targetPosition = rtConfig->getTargetIncline();
436-
} else if (rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetResistanceLevel) {
437-
int actualDelta = rtConfig->resistance.getTarget() - rtConfig->resistance.getValue();
438-
rtConfig->setTargetIncline(ss2k->getCurrentPosition() + ((userConfig->getERGSensitivity() * 3) * actualDelta));
437+
} else if ((rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetResistanceLevel)) {
438+
// Get absolute position for a given resistance percent (0-100)
439+
if (rtConfig->resistance.getSimulate()) {
440+
int32_t minPos, maxPos;
441+
bool usePwr = false;
442+
if (userConfig->getHMin() != INT32_MIN && userConfig->getHMax() != INT32_MIN) {
443+
minPos = userConfig->getHMin();
444+
maxPos = userConfig->getHMax();
445+
} else if(rtConfig->getMinStep() != -DEFAULT_STEPPER_TRAVEL && rtConfig->getMaxStep() != DEFAULT_STEPPER_TRAVEL) {
446+
minPos = rtConfig->getMinStep();
447+
maxPos = rtConfig->getMaxStep();
448+
} else{ //No good position information. Fallback to using ERG
449+
minPos = userConfig->getMinWatts();
450+
maxPos = userConfig->getMaxWatts();
451+
usePwr = true;
452+
}
453+
int resistancePercent = rtConfig->resistance.getTarget();
454+
if (resistancePercent < 0) resistancePercent = 0;
455+
if (resistancePercent > 100) resistancePercent = 100;
456+
int64_t span = (int64_t)maxPos - (int64_t)minPos;
457+
int32_t pos = minPos + (int32_t)((span * resistancePercent) / 100);
458+
if (usePwr) { //fallback to using ERG
459+
rtConfig->watts.setTarget(pos);
460+
rtConfig->setFTMSMode(FitnessMachineControlPointProcedure::SetTargetPower);
461+
return;
462+
}
463+
rtConfig->setTargetIncline(pos);
464+
} else {
465+
int actualDelta = rtConfig->resistance.getTarget() - rtConfig->resistance.getValue();
466+
int direction = (actualDelta > 0) ? 1 : -1;
467+
if (abs(actualDelta) > 20 - userConfig->getERGSensitivity()) {
468+
rtConfig->setTargetIncline(ss2k->getCurrentPosition() + userConfig->getShiftStep() * direction);
469+
} else if (abs(actualDelta) > 3) {
470+
rtConfig->setTargetIncline(ss2k->getCurrentPosition() + actualDelta + (userConfig->getERGSensitivity() * direction));
471+
} else {
472+
rtConfig->setTargetIncline(ss2k->getCurrentPosition() + actualDelta);
473+
}
474+
}
439475
ss2k->targetPosition = rtConfig->getTargetIncline();
440476
} else {
441477
// Simulation Mode
@@ -777,7 +813,7 @@ void SS2K::txSerial() { // Serial.printf(" Before TX ");
777813
}
778814
bool SS2K::pelotonConnected() {
779815
txCheck = TX_CHECK_INTERVAL;
780-
if (rtConfig->resistance.getValue() > 0) {
816+
if (millis() - rtConfig->resistance.getTimestamp() < 5000 && !rtConfig->resistance.getSimulate()) {
781817
rtConfig->setMinResistance(MIN_PELOTON_RESISTANCE);
782818
rtConfig->setMaxResistance(MAX_PELOTON_RESISTANCE);
783819
return true;

src/Power_Table.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ void PowerTable::setStepperMinMax() {
9595
}
9696

9797
// if the FTMS device reports resistance feedback, skip estimating min_max
98-
if (rtConfig->resistance.getValue() > 0) {
98+
if (rtConfig->resistance.getValue() > 0 && !rtConfig->resistance.getSimulate()) {
9999
rtConfig->setMinStep(-DEFAULT_STEPPER_TRAVEL);
100100
rtConfig->setMaxStep(DEFAULT_STEPPER_TRAVEL);
101101
SS2K_LOG(POWERTABLE_LOG_TAG, "Using Resistance Travel Limits");

src/SensorCollector.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ void collectAndSet(NimBLEUUID charUUID, NimBLEUUID serviceUUID, std::string& uni
8484
}
8585

8686
if (sensorData->hasResistance()) {
87+
rtConfig->resistance.setSimulate(false); // Mark as real data
8788
if ((ss2k->pelotonIsConnected) && (charUUID != PELOTON_DATA_UUID)) {
8889
// Peloton connected but using BLE Power Meter. So skip resistance for UUID's that aren't Peloton.
8990
} else {

0 commit comments

Comments
 (0)