diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 00000000..44ffa113 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,63 @@ +name: Update Changelog + +on: + pull_request: + branches: + - develop + types: [opened, synchronize, reopened] + +jobs: + update-changelog: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Update Changelog + run: | + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") + echo "Latest tag: $LATEST_TAG" + + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "No [Unreleased] section found" + exit 0 + fi + + FIRST_VERSION=$(grep -oP "## \[\K[0-9]+\.[0-9]+\.[0-9]+" CHANGELOG.md | head -1 || echo "0.0.0") + + if [ "$LATEST_TAG" = "$FIRST_VERSION" ]; then + echo "Latest tag matches first version in changelog" + exit 0 + fi + + # Create temporary files + echo -e "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n### Changed\n\n### Hardware\n" > header.tmp + + # Update the old unreleased section + sed "0,/## \[Unreleased\]/s/## \[Unreleased\]/## [$LATEST_TAG]/" CHANGELOG.md | tail -n +7 > content.tmp + + # Combine files + cat header.tmp content.tmp > CHANGELOG.md + rm header.tmp content.tmp + + if git diff --quiet CHANGELOG.md; then + echo "No changes to commit" + exit 0 + fi + + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add CHANGELOG.md + git commit -m "Update changelog for version $LATEST_TAG" + git push diff --git a/CHANGELOG.md b/CHANGELOG.md index 255313dd..7631e3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Knob homing if calibrate trainer is selected in an app. + +### Changed + +### Hardware + +## [24.10.30] + ### Added - Added pass through shifting in both ERG and SIM mode. @@ -14,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added CSC Service to BLE server. - Added Yosuda-007C. - Updated wiki banner. +- Added automatic update of Changelog sections on pull request to develop. +- Added support for the Zwift gear display. ### Changed @@ -55,7 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a final test to check if ERG mode has commanded a move in the proper direction. - Aligned the values between the config app and web interface. - Added ability to send target watts through the custom characteristic. -- Added a final test to check if ERG mode has commanded a move in the proper direction. +- Added a final test to check if ERG mode has commanded a move in the proper direction. +- Cleaned up targetPosition to make it easier to understand. ### Hardware diff --git a/README.md b/README.md index 79dead68..57db2bb6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # You can now visit us at [Facebook](https://www.facebook.com/groups/716297469953492/) # There's now a companion App! -A brand new shiny Companion app for SmartSpin2k is availiable! [SS2kConfigApp](https://github.com/doudar/SS2kConfigApp/tree/develop) (Google Play Store Coming Soon). +A brand new shiny Companion app for SmartSpin2k is availiable! [SS2kConfigApp](https://github.com/doudar/SS2kConfigApp/tree/develop) You can get it from the Apple App Store here: @@ -21,8 +21,6 @@ You can get it from the Apple App Store here: -If you have Android, it will be on the Play Store soon, but in the meantime, you can side load it using the .apk (located in the .zip) in the releases section of the repository: [SS2kConfigApp](https://github.com/doudar/SS2kConfigApp/releases) - # About SmartSpin2k is a DIY project that allows you to turn any spin bike into a smart trainer. With SmartSpin2k, you can connect your spin bike to Zwift, TrainerRoad, or other popular training apps. This allows you to control your bike's resistance automatically, track your performance, and compete with other riders online. diff --git a/include/BLE_Common.h b/include/BLE_Common.h index a9b450bd..0fe36269 100644 --- a/include/BLE_Common.h +++ b/include/BLE_Common.h @@ -7,15 +7,14 @@ #pragma once -// #define CONFIG_SW_COEXIST_ENABLE 1 - -#include #include +#include #include #include #include #include "Main.h" #include "BLE_Definitions.h" +#include "BLE_Wattbike_Service.h" #define BLE_CLIENT_LOG_TAG "BLE_Client" #define BLE_COMMON_LOG_TAG "BLE_Common" @@ -44,12 +43,6 @@ class MyServerCallbacks : public NimBLEServerCallbacks { bool onConnParamsUpdateRequest(NimBLEClient *pClient, const ble_gap_upd_params *params); }; -class MyCallbacks : public NimBLECharacteristicCallbacks { - public: - void onWrite(BLECharacteristic *); - void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue); -}; - // TODO add the rest of the server to this class class SpinBLEServer { private: @@ -62,6 +55,7 @@ class SpinBLEServer { bool IndoorBikeData : 1; bool CyclingSpeedCadence : 1; } clientSubscribed; + int spinDownFlag = 0; NimBLEServer *pServer = nullptr; void setClientSubscribed(NimBLEUUID pUUID, bool subscribe); void notifyShift(); @@ -72,7 +66,14 @@ class SpinBLEServer { SpinBLEServer() { memset(&clientSubscribed, 0, sizeof(clientSubscribed)); } }; +class MyCallbacks : public NimBLECharacteristicCallbacks { + public: + void onWrite(BLECharacteristic *); + void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue); +}; + extern SpinBLEServer spinBLEServer; +extern BLE_Wattbike_Service wattbikeService; void startBLEServer(); void logCharacteristic(char *buffer, const size_t bufferCapacity, const byte *data, const size_t dataLength, const NimBLEUUID serviceUUID, const NimBLEUUID charUUID, @@ -199,4 +200,4 @@ class MyClientCallback : public NimBLEClientCallbacks { void onAuthenticationComplete(ble_gap_conn_desc); }; -extern SpinBLEClient spinBLEClient; \ No newline at end of file +extern SpinBLEClient spinBLEClient; diff --git a/include/BLE_Custom_Characteristic.h b/include/BLE_Custom_Characteristic.h index 105a872a..ce91a0be 100644 --- a/include/BLE_Custom_Characteristic.h +++ b/include/BLE_Custom_Characteristic.h @@ -58,6 +58,8 @@ const uint8_t BLE_resetPowerTable = 0x26; // Delete all power table infor const uint8_t BLE_powerTableData = 0x27; // sets or requests power table data const uint8_t BLE_simulatedTargetWatts = 0x28; // current target watts const uint8_t BLE_simulateTargetWatts = 0x29; // are we sending target watts +const uint8_t BLE_hMin = 0x2A; // Minimum homing value +const uint8_t BLE_hMax = 0x2B; // Maximum homing value class BLE_ss2kCustomCharacteristic { public: @@ -79,4 +81,4 @@ class BLE_ss2kCustomCharacteristic { class ss2kCustomCharacteristicCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *); void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue); -}; \ No newline at end of file +}; diff --git a/include/BLE_Fitness_Machine_Service.h b/include/BLE_Fitness_Machine_Service.h index 004e55c0..4c39e271 100644 --- a/include/BLE_Fitness_Machine_Service.h +++ b/include/BLE_Fitness_Machine_Service.h @@ -15,6 +15,7 @@ class BLE_Fitness_Machine_Service { BLE_Fitness_Machine_Service(); void setupService(NimBLEServer *pServer, MyCallbacks *chrCallbacks); void update(); + bool spinDown(); private: BLEService *pFitnessMachineService; @@ -27,6 +28,7 @@ class BLE_Fitness_Machine_Service { BLECharacteristic *fitnessMachineInclinationRange; BLECharacteristic *fitnessMachineTrainingStatus; uint8_t ftmsIndoorBikeData[11] = {0}; - bool spinDown(); void processFTMSWrite(); -}; \ No newline at end of file +}; + +extern BLE_Fitness_Machine_Service fitnessMachineService; \ No newline at end of file diff --git a/include/BLE_Wattbike_Service.h b/include/BLE_Wattbike_Service.h new file mode 100644 index 00000000..fe72306b --- /dev/null +++ b/include/BLE_Wattbike_Service.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include + +class BLE_Wattbike_Service { + public: + BLE_Wattbike_Service(); + void setupService(NimBLEServer *pServer); + void update(); + void parseNemit(); + + private: + NimBLEService *pWattbikeService; + NimBLECharacteristic *wattbikeReadCharacteristic; + NimBLECharacteristic *wattbikeWriteCharacteristic; +}; diff --git a/include/Main.h b/include/Main.h index 2703f60b..54805573 100644 --- a/include/Main.h +++ b/include/Main.h @@ -27,10 +27,10 @@ class SS2K { int shiftersHoldForScan; uint64_t scanDelayTime; uint64_t scanDelayStart; - - public: int32_t targetPosition; int32_t currentPosition; + + public: bool stepperIsRunning; bool externalControl; bool syncMode; @@ -47,12 +47,21 @@ class SS2K { static void IRAM_ATTR shiftUp(); static void IRAM_ATTR shiftDown(); static void moveStepper(); + + // the position the stepper motor will move to + int32_t getTargetPosition() { return targetPosition; } + void setTargetPosition(int32_t tp) { targetPosition = tp; } + + // the position the stepper motor is currently at + int32_t getCurrentPosition() { return currentPosition; } + void setCurrentPosition(int32_t cp) { currentPosition = cp; } + void resetIfShiftersHeld(); void startTasks(); void stopTasks(); void restartWifi(); - void setupTMCStepperDriver(); - void updateStepperPower(); + void setupTMCStepperDriver(bool reset = false); + void updateStepperPower(int pwr = 0); void updateStealthChop(); void updateStepperSpeed(int speed = 0); void checkDriverTemperature(); @@ -61,6 +70,7 @@ class SS2K { static void rxSerial(void); void txSerial(); void pelotonConnected(); + void goHome(bool bothDirections = false); SS2K() { targetPosition = 0; diff --git a/include/SmartSpin_parameters.h b/include/SmartSpin_parameters.h index 9abb0e2a..7174ede9 100644 --- a/include/SmartSpin_parameters.h +++ b/include/SmartSpin_parameters.h @@ -55,13 +55,13 @@ class Measurement { class RuntimeParameters { private: - double targetIncline = 0.0; - double currentIncline = 0.0; + double targetIncline = 0.0; float simulatedSpeed = 0.0; uint8_t FTMSMode = 0x00; int shifterPosition = 0; - int32_t minStep = -DEFAULT_STEPPER_TRAVEL; - int32_t maxStep = DEFAULT_STEPPER_TRAVEL; + bool homed = false; + int32_t minStep = -DEFAULT_STEPPER_TRAVEL; + int32_t maxStep = DEFAULT_STEPPER_TRAVEL; int minResistance = -DEFAULT_RESISTANCE_RANGE; int maxResistance = DEFAULT_RESISTANCE_RANGE; bool simTargetWatts = false; @@ -77,9 +77,6 @@ class RuntimeParameters { void setTargetIncline(float inc) { targetIncline = inc; } float getTargetIncline() { return targetIncline; } - void setCurrentIncline(float inc) { currentIncline = inc; } - float getCurrentIncline() { return currentIncline; } - void setSimulatedSpeed(float spd) { simulatedSpeed = spd; } float getSimulatedSpeed() { return simulatedSpeed; } @@ -89,6 +86,9 @@ class RuntimeParameters { void setShifterPosition(int sp) { shifterPosition = sp; } int getShifterPosition() { return shifterPosition; } + void setHomed(bool hmd) { homed = hmd; } + int getHomed() { return homed; } + void setMinStep(int ms) { minStep = ms; } int getMinStep() { return minStep; } @@ -124,7 +124,9 @@ class userParameters { bool stepperDir; bool shifterDir; bool udpLogEnabled = false; - + int32_t hMin = INT32_MIN; + int32_t hMax = INT32_MIN; + bool FTMSControlPointWrite = false; String ssid; String password; @@ -200,6 +202,12 @@ class userParameters { void setFoundDevices(String fdv) { foundDevices = fdv; } const char* getFoundDevices() { return foundDevices.c_str(); } + void setHMin(int32_t min) { hMin = min; } + int32_t getHMin() { return hMin; } + + void setHMax(int32_t max) { hMax = max; } + int32_t getHMax() { return hMax; } + void setDefaults(); String returnJSON(); void saveToLittleFS(); diff --git a/include/settings.h b/include/settings.h index 102a9ddc..19b7da46 100644 --- a/include/settings.h +++ b/include/settings.h @@ -254,7 +254,7 @@ const char* const DEFAULT_PASSWORD = "password"; #define RUNTIMECONFIG_JSON_SIZE 512 + DEBUG_LOG_BUFFER_SIZE // PowerTable Version -#define TABLE_VERSION 4 +#define TABLE_VERSION 5 /* Number of entries in the ERG Power Lookup Table This is currently maintained as to keep memory usage lower and reduce the print output of the table. diff --git a/lib/SS2K/include/Constants.h b/lib/SS2K/include/Constants.h index 8c9dae4f..c30fdee6 100644 --- a/lib/SS2K/include/Constants.h +++ b/lib/SS2K/include/Constants.h @@ -59,6 +59,11 @@ #define FITNESSMACHINEPOWERRANGE_UUID NimBLEUUID((uint16_t)0x2AD8) #define FITNESSMACHINEINCLINATIONRANGE_UUID NimBLEUUID((uint16_t)0x2AD5) +// Wattbike Service +#define WATTBIKE_SERVICE_UUID NimBLEUUID("b4cc1223-bc02-4cae-adb9-1217ad2860d1") +#define WATTBIKE_READ_UUID NimBLEUUID("b4cc1224-bc02-4cae-adb9-1217ad2860d1") +#define WATTBIKE_WRITE_UUID NimBLEUUID("b4cc1225-bc02-4cae-adb9-1217ad2860d1") + // GATT service/characteristic UUIDs for Flywheel Bike from ptx2/gymnasticon/ #define FLYWHEEL_UART_SERVICE_UUID NimBLEUUID("6e400001-b5a3-f393-e0a9-e50e24dcca9e") #define FLYWHEEL_UART_RX_UUID NimBLEUUID("6e400002-b5a3-f393-e0a9-e50e24dcca9e") diff --git a/src/BLE_Client.cpp b/src/BLE_Client.cpp index 9ab26d0d..36fc7226 100644 --- a/src/BLE_Client.cpp +++ b/src/BLE_Client.cpp @@ -13,6 +13,7 @@ appearance: 1156, manufacturer data: 640302018743, serviceUUID: #include "Main.h" #include "BLE_Common.h" +#include "BLE_Fitness_Machine_Service.h" #include "SS2KLog.h" #include @@ -105,6 +106,15 @@ void bleClientTask(void *pvParameters) { } } } + // Spin Down process for the Server. It's here because it needs to be non-blocking for the maintenance loop. + if (spinBLEServer.spinDownFlag) { + if (spinBLEServer.spinDownFlag >= 2) { // Home Both Directions + fitnessMachineService.spinDown(); + } else { // Startup Homing + ss2k->goHome(false); + } + spinBLEServer.spinDownFlag = 0; + } } } @@ -120,9 +130,8 @@ bool SpinBLEClient::connectToServer() { for (int i = 0; i < NUM_BLE_DEVICES; i++) { if (spinBLEClient.myBLEDevices[i].doConnect == true) { // Client wants to be connected if (spinBLEClient.myBLEDevices[i].advertisedDevice) { // Client is assigned - // If this device is advertising HR service AND not advertising FTMS service AND there is no connected PM AND the next slot is set to connect, connect that one first and - // connect the HRM last. - // if (spinBLEClient.myBLEDevices[i].advertisedDevice->isAdvertisingService(HEARTSERVICE_UUID) && + // If this device is advertising HR service AND not advertising FTMS service AND there is no connected PM AND the next slot is set to connect, connect that one first + // and connect the HRM last. if (spinBLEClient.myBLEDevices[i].advertisedDevice->isAdvertisingService(HEARTSERVICE_UUID) && // (!spinBLEClient.myBLEDevices[i].advertisedDevice->isAdvertisingService(FITNESSMACHINESERVICE_UUID)) && (!connectedPM) && // (spinBLEClient.myBLEDevices[i + 1].doConnect == true)) { // myDevice = spinBLEClient.myBLEDevices[i + 1].advertisedDevice; @@ -595,7 +604,7 @@ void SpinBLEClient::FTMSControlPointWrite(const uint8_t *pData, int length) { const int kLogBufCapacity = length + 40; char logBuf[kLogBufCapacity]; if (modData[0] == FitnessMachineControlPointProcedure::SetIndoorBikeSimulationParameters) { // use virtual Shifting - int incline = ss2k->targetPosition / userConfig->getInclineMultiplier(); + int incline = ss2k->getTargetPosition() / userConfig->getInclineMultiplier(); modData[3] = (uint8_t)(incline & 0xff); modData[4] = (uint8_t)(incline >> 8); writeCharacteristic->writeValue(modData, length); @@ -642,7 +651,8 @@ void SpinBLEClient::postConnect() { return; } - // 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 appliance. + // 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 + // appliance. if (userConfig->getFTMSControlPointWrite()) { writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::RequestControl, 1); vTaskDelay(BLE_NOTIFY_DELAY / portTICK_PERIOD_MS); @@ -890,7 +900,7 @@ void SpinBLEAdvertisedDevice::reset() { if (this->isPM) spinBLEClient.connectedPM = false; if (this->isCSC) spinBLEClient.connectedCD = false; spinBLEClient.connectedSpeed = false; - advertisedDevice = nullptr; + advertisedDevice = nullptr; // NimBLEAddress peerAddress; this->connectedClientID = BLE_HS_CONN_HANDLE_NONE; this->serviceUUID = (uint16_t)0x0000; diff --git a/src/BLE_Custom_Characteristic.cpp b/src/BLE_Custom_Characteristic.cpp index 5baeea93..cce568ea 100644 --- a/src/BLE_Custom_Characteristic.cpp +++ b/src/BLE_Custom_Characteristic.cpp @@ -491,16 +491,16 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "<-targetPosition"); if (rxValue[0] == cc_read) { returnValue[0] = cc_success; - returnValue[2] = (uint8_t)(ss2k->targetPosition & 0xff); - returnValue[3] = (uint8_t)(ss2k->targetPosition >> 8); - returnValue[4] = (uint8_t)(ss2k->targetPosition >> 16); - returnValue[5] = (uint8_t)(ss2k->targetPosition >> 24); + returnValue[2] = (uint8_t)(ss2k->getTargetPosition() & 0xff); + returnValue[3] = (uint8_t)(ss2k->getTargetPosition() >> 8); + returnValue[4] = (uint8_t)(ss2k->getTargetPosition() >> 16); + returnValue[5] = (uint8_t)(ss2k->getTargetPosition() >> 24); returnLength += 4; } if (rxValue[0] == cc_write) { - returnValue[0] = cc_success; - ss2k->targetPosition = (int32_t((uint8_t)(rxValue[2]) << 0 | (uint8_t)(rxValue[3]) << 8 | (uint8_t)(rxValue[4]) << 16 | (uint8_t)(rxValue[5]) << 24)); - logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, " (%f)", ss2k->targetPosition); + returnValue[0] = cc_success; + ss2k->setTargetPosition(int32_t((uint8_t)(rxValue[2]) << 0 | (uint8_t)(rxValue[3]) << 8 | (uint8_t)(rxValue[4]) << 16 | (uint8_t)(rxValue[5]) << 24)); + logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, " (%f)", ss2k->getTargetPosition()); } break; @@ -679,7 +679,7 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { } } break; - case BLE_simulatedTargetWatts: //0x28 + case BLE_simulatedTargetWatts: // 0x28 logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "<-targetWatts"); if (rxValue[0] == cc_read) { returnValue[0] = cc_success; @@ -693,7 +693,7 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "(%d)", rtConfig->watts.getTarget()); } break; - case BLE_simulateTargetWatts: //0x29 + case BLE_simulateTargetWatts: // 0x29 logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "<-simulatetargetwatts"); if (rxValue[0] == cc_read) { returnValue[0] = cc_success; @@ -706,7 +706,39 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "(%s)", rtConfig->getSimTargetWatts() ? "true" : "false"); } break; + case BLE_hMin: // 0x2A + logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "<-hMin"); + if (rxValue[0] == cc_read) { + returnValue[0] = cc_success; + returnValue[2] = (uint8_t)(userConfig->getHMin() & 0xff); + returnValue[3] = (uint8_t)(userConfig->getHMin() >> 8); + returnValue[4] = (uint8_t)(userConfig->getHMin() >> 16); + returnValue[5] = (uint8_t)(userConfig->getHMin() >> 24); + returnLength += 4; + } + if (rxValue[0] == cc_write) { + returnValue[0] = cc_success; + ss2k->setTargetPosition(int32_t((uint8_t)(rxValue[2]) << 0 | (uint8_t)(rxValue[3]) << 8 | (uint8_t)(rxValue[4]) << 16 | (uint8_t)(rxValue[5]) << 24)); + logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, " (%f)", userConfig->getHMin()); + } + break; + case BLE_hMax: // 0x2B + logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "<-hMax"); + if (rxValue[0] == cc_read) { + returnValue[0] = cc_success; + returnValue[2] = (uint8_t)(userConfig->getHMax() & 0xff); + returnValue[3] = (uint8_t)(userConfig->getHMax() >> 8); + returnValue[4] = (uint8_t)(userConfig->getHMax() >> 16); + returnValue[5] = (uint8_t)(userConfig->getHMax() >> 24); + returnLength += 4; + } + if (rxValue[0] == cc_write) { + returnValue[0] = cc_success; + ss2k->setTargetPosition(int32_t((uint8_t)(rxValue[2]) << 0 | (uint8_t)(rxValue[3]) << 8 | (uint8_t)(rxValue[4]) << 16 | (uint8_t)(rxValue[5]) << 24)); + logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, " (%f)", userConfig->getHMax()); + } + break; } SS2K_LOG(CUSTOM_CHAR_LOG_TAG, "%s", logBuf); @@ -728,7 +760,7 @@ void BLE_ss2kCustomCharacteristic::process(std::string rxValue) { // iterate through all smartspin user parameters and notify the specific one if changed void BLE_ss2kCustomCharacteristic::parseNemit() { static userParameters _oldParams; - static RuntimeParameters _oldRTParams; + static RuntimeParameters _oldRTParams; if (userConfig->getAutoUpdate() != _oldParams.getAutoUpdate()) { _oldParams.setAutoUpdate(userConfig->getAutoUpdate()); @@ -842,19 +874,30 @@ void BLE_ss2kCustomCharacteristic::parseNemit() { BLE_ss2kCustomCharacteristic::notify(BLE_shiftDir); return; } - if(rtConfig->getFTMSMode() != _oldRTParams.getFTMSMode()){ + if (rtConfig->getFTMSMode() != _oldRTParams.getFTMSMode()) { _oldRTParams.setFTMSMode(rtConfig->getFTMSMode()); BLE_ss2kCustomCharacteristic::notify(BLE_FTMSMode); return; } - if(rtConfig->watts.getTarget() != _oldRTParams.watts.getTarget()){ - _oldRTParams.watts.setTarget(rtConfig->watts.getTarget()); - BLE_ss2kCustomCharacteristic::notify(BLE_simulatedTargetWatts); - return; + if (rtConfig->watts.getTarget() != _oldRTParams.watts.getTarget()) { + _oldRTParams.watts.setTarget(rtConfig->watts.getTarget()); + BLE_ss2kCustomCharacteristic::notify(BLE_simulatedTargetWatts); + return; } - if(rtConfig->getSimTargetWatts() != _oldRTParams.getSimTargetWatts()){ + if (rtConfig->getSimTargetWatts() != _oldRTParams.getSimTargetWatts()) { _oldRTParams.setSimTargetWatts(rtConfig->getSimTargetWatts()); BLE_ss2kCustomCharacteristic::notify(BLE_simulateTargetWatts); - return; + return; + } + if (userConfig->getHMin() != _oldParams.getHMin()) { + _oldParams.setHMin(userConfig->getHMin()); + BLE_ss2kCustomCharacteristic::notify(BLE_hMin); + return; + } + + if (userConfig->getHMax() != _oldParams.getHMax()) { + _oldParams.setHMax(userConfig->getHMax()); + BLE_ss2kCustomCharacteristic::notify(BLE_hMax); + return; } } \ No newline at end of file diff --git a/src/BLE_Fitness_Machine_Service.cpp b/src/BLE_Fitness_Machine_Service.cpp index a202f73b..a4568efb 100644 --- a/src/BLE_Fitness_Machine_Service.cpp +++ b/src/BLE_Fitness_Machine_Service.cpp @@ -130,29 +130,28 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { switch ((uint8_t)rxValue[0]) { case FitnessMachineControlPointProcedure::RequestControl: returnValue[2] = FitnessMachineControlPointResultCode::Success; // 0x01; - pCharacteristic->setValue(returnValue, 3); - rtConfig->watts.setTarget(0); - rtConfig->setSimTargetWatts(false); + rtConfig->watts.setTarget(0); + rtConfig->setSimTargetWatts(false); logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "-> Control Request"); ftmsTrainingStatus[1] = FitnessMachineTrainingStatus::Idle; // 0x01; fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus, 2); ftmsStatus = {FitnessMachineStatus::StartedOrResumedByUser}; + pCharacteristic->setValue(returnValue, 3); break; case FitnessMachineControlPointProcedure::Reset: { returnValue[2] = FitnessMachineControlPointResultCode::Success; // 0x01; - pCharacteristic->setValue(returnValue, 3); logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "-> Reset"); ftmsTrainingStatus[1] = FitnessMachineTrainingStatus::Idle; // 0x01; ftmsStatus = {FitnessMachineStatus::Reset}; fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus, 2); + pCharacteristic->setValue(returnValue, 3); } break; case FitnessMachineControlPointProcedure::SetTargetInclination: { rtConfig->setFTMSMode((uint8_t)rxValue[0]); returnValue[2] = FitnessMachineControlPointResultCode::Success; // 0x01; - pCharacteristic->setValue(returnValue, 3); port = (rxValue[2] << 8) + rxValue[1]; port *= 10; @@ -164,6 +163,7 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { ftmsStatus = {FitnessMachineStatus::TargetInclineChanged, (uint8_t)rxValue[1], (uint8_t)rxValue[2]}; ftmsTrainingStatus[1] = FitnessMachineTrainingStatus::Other; // 0x00; fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus, 2); + pCharacteristic->setValue(returnValue, 3); } break; case FitnessMachineControlPointProcedure::SetTargetResistanceLevel: { @@ -266,13 +266,16 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { case FitnessMachineControlPointProcedure::SpinDownControl: { rtConfig->setFTMSMode((uint8_t)rxValue[0]); uint8_t controlPoint[6] = {FitnessMachineControlPointProcedure::ResponseCode, 0x01, 0x24, 0x03, 0x96, 0x0e}; // send low and high speed targets + returnValue[2] = FitnessMachineControlPointResultCode::Success; pCharacteristic->setValue(controlPoint, 6); logBufLength += snprintf(logBuf + logBufLength, kLogBufCapacity - logBufLength, "-> Spin Down Requested"); ftmsStatus = {FitnessMachineStatus::SpinDownStatus, 0x01}; // send low and high speed targets ftmsTrainingStatus[1] = FitnessMachineTrainingStatus::Other; // 0x00; - fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus, 2); + pCharacteristic->setValue(returnValue, 3); + pCharacteristic->indicate(); + spinBLEServer.spinDownFlag = 2; } break; case FitnessMachineControlPointProcedure::SetTargetedCadence: { @@ -307,8 +310,6 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() { fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus, 2); fitnessMachineTrainingStatus->notify(false); } - for (int i = 0; i < ftmsStatus.size(); i++) { - } fitnessMachineStatusCharacteristic->setValue(ftmsStatus.data(), ftmsStatus.size()); pCharacteristic->indicate(); fitnessMachineTrainingStatus->notify(false); @@ -322,30 +323,42 @@ bool BLE_Fitness_Machine_Service::spinDown() { return false; } uint8_t spinStatus[2] = {0x14, 0x01}; - + SS2K_LOG(FMTS_SERVER_LOG_TAG, "Spin Status: %d", rxValue[1]); + Serial.printf("Spin Status: %d", rxValue[1]); if (rxValue[1] == 0x01) { - // debugDirector("Spin Down Initiated", true); + SS2K_LOG(FMTS_SERVER_LOG_TAG, "Spin Down Initiated"); + Serial.printf("Spin Down Initiated"); vTaskDelay(1000 / portTICK_RATE_MS); spinStatus[1] = 0x04; // send Stop Pedaling fitnessMachineStatusCharacteristic->setValue(spinStatus, 2); + fitnessMachineStatusCharacteristic->notify(); + vTaskDelay(1000 / portTICK_RATE_MS); + spinStatus[1] = 0x02; // Success + fitnessMachineStatusCharacteristic->setValue(spinStatus, 2); + fitnessMachineStatusCharacteristic->notify(); + ss2k->goHome(true); } + if (rxValue[1] == 0x04) { - // debugDirector("Stop Pedaling", true); + SS2K_LOG(FMTS_SERVER_LOG_TAG, "Stop Pedaling"); + Serial.printf("Stop Pedaling"); vTaskDelay(1000 / portTICK_RATE_MS); spinStatus[1] = 0x02; // Success fitnessMachineStatusCharacteristic->setValue(spinStatus, 2); + fitnessMachineStatusCharacteristic->notify(); } if (rxValue[1] == 0x02) { - // debugDirector("Success", true); + SS2K_LOG(FMTS_SERVER_LOG_TAG, "Success"); + Serial.printf("Success"); spinStatus[0] = 0x00; spinStatus[1] = 0x00; // Success fitnessMachineStatusCharacteristic->setValue(spinStatus, 2); uint8_t returnValue[3] = {0x00, 0x00, 0x00}; fitnessMachineControlPoint->setValue(returnValue, 3); fitnessMachineControlPoint->indicate(); + fitnessMachineStatusCharacteristic->notify(); + ss2k->goHome(true); } - fitnessMachineStatusCharacteristic->notify(); - return true; } \ No newline at end of file diff --git a/src/BLE_Server.cpp b/src/BLE_Server.cpp index 8c1d983c..a2610c35 100644 --- a/src/BLE_Server.cpp +++ b/src/BLE_Server.cpp @@ -14,6 +14,7 @@ #include "BLE_Fitness_Machine_Service.h" #include "BLE_Custom_Characteristic.h" #include "BLE_Device_Information_Service.h" +#include "BLE_Wattbike_Service.h" #include #include @@ -32,6 +33,7 @@ BLE_Heart_Service heartService; BLE_Fitness_Machine_Service fitnessMachineService; BLE_ss2kCustomCharacteristic ss2kCustomCharacteristic; BLE_Device_Information_Service deviceInformationService; +BLE_Wattbike_Service wattbikeService; void startBLEServer() { // Server Setup @@ -46,7 +48,8 @@ void startBLEServer() { fitnessMachineService.setupService(spinBLEServer.pServer, &chrCallbacks); ss2kCustomCharacteristic.setupService(spinBLEServer.pServer); deviceInformationService.setupService(spinBLEServer.pServer); - + wattbikeService.setupService(spinBLEServer.pServer); // No callback needed + BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); // const std::string fitnessData = {0b00000001, 0b00100000, 0b00000000}; // pAdvertising->setServiceData(FITNESSMACHINESERVICE_UUID, fitnessData); @@ -56,6 +59,7 @@ void startBLEServer() { pAdvertising->addServiceUUID(CSCSERVICE_UUID); pAdvertising->addServiceUUID(HEARTSERVICE_UUID); pAdvertising->addServiceUUID(SMARTSPIN2K_SERVICE_UUID); + pAdvertising->addServiceUUID(WATTBIKE_SERVICE_UUID); pAdvertising->setMaxInterval(250); pAdvertising->setMinInterval(160); pAdvertising->setScanResponse(true); @@ -74,6 +78,7 @@ void SpinBLEServer::update() { cyclingPowerService.update(); cyclingSpeedCadenceService.update(); fitnessMachineService.update(); + wattbikeService.parseNemit(); // Changed from update() to parseNemit() } double SpinBLEServer::calculateSpeed() { @@ -182,7 +187,7 @@ void MyCallbacks::onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_con SS2K_LOG(BLE_SERVER_LOG_TAG, "%s", str.c_str()); } -//This might be worth depreciating. With multiple clients connected (SS2k App, + Training App), it at least needs to be an int, not a bool. +// This might be worth depreciating. With multiple clients connected (SS2k App, + Training App), it at least needs to be an int, not a bool. void SpinBLEServer::setClientSubscribed(NimBLEUUID pUUID, bool subscribe) { if (pUUID == HEARTCHARACTERISTIC_UUID) { spinBLEServer.clientSubscribed.Heartrate = subscribe; diff --git a/src/BLE_Wattbike_Service.cpp b/src/BLE_Wattbike_Service.cpp new file mode 100644 index 00000000..b4b479dc --- /dev/null +++ b/src/BLE_Wattbike_Service.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "BLE_Wattbike_Service.h" +#include "BLE_Common.h" +#include + +BLE_Wattbike_Service::BLE_Wattbike_Service() : pWattbikeService(nullptr), wattbikeReadCharacteristic(nullptr), wattbikeWriteCharacteristic(nullptr) {} + +void BLE_Wattbike_Service::setupService(NimBLEServer *pServer) { + // Create Wattbike service + pWattbikeService = spinBLEServer.pServer->createService(WATTBIKE_SERVICE_UUID); + + // Create characteristic for gear notifications + wattbikeReadCharacteristic = pWattbikeService->createCharacteristic(WATTBIKE_READ_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + + // Create characteristic for receiving commands + wattbikeWriteCharacteristic = pWattbikeService->createCharacteristic(WATTBIKE_WRITE_UUID, NIMBLE_PROPERTY::WRITE); + + // Start the service + pWattbikeService->start(); +} + +void BLE_Wattbike_Service::parseNemit() { + static int lastGear = -1; // Track last gear position + static unsigned long lastNotifyTime = 0; // Track last notification time + const unsigned long NOTIFY_INTERVAL = 30000; // 30 seconds in milliseconds + + // Get current shifter position + int currentGear = rtConfig->getShifterPosition(); + if (currentGear < 1) { // Ensure gear is at least 1 + currentGear = 1; + } + + unsigned long currentTime = millis(); + + // Only call update if gear changed or 30 seconds elapsed + if (currentGear != lastGear || (currentTime - lastNotifyTime) >= NOTIFY_INTERVAL) { + update(); // Call existing update function to handle notification + + // Update tracking variables + lastGear = currentGear; + lastNotifyTime = currentTime; + } +} + +void BLE_Wattbike_Service::update() { + // Get current shifter position + int currentGear = rtConfig->getShifterPosition(); + if (currentGear < 1) { // Ensure gear is at least 1 + currentGear = 1; + } + + // Create gear data packet with sequence number and fixed value + static uint8_t seq = 0; + ++seq; + + uint8_t gearData[4]; + gearData[0] = seq; // Sequence number + gearData[1] = 0x03; // Fixed value + gearData[2] = 0xB6; // Fixed value + gearData[3] = (byte)currentGear; // Gear value + + // Update the characteristic + wattbikeReadCharacteristic->setValue(gearData, sizeof(gearData)); + wattbikeReadCharacteristic->notify(); + + // Log the update + const int kLogBufCapacity = 100; + char logBuf[kLogBufCapacity]; + logCharacteristic(logBuf, kLogBufCapacity, gearData, sizeof(gearData), WATTBIKE_SERVICE_UUID, wattbikeReadCharacteristic->getUUID(), "Wattbike Gear[ %d ]", currentGear); +} diff --git a/src/ERG_Mode.cpp b/src/ERG_Mode.cpp index a7b1b1a4..37a135b3 100644 --- a/src/ERG_Mode.cpp +++ b/src/ERG_Mode.cpp @@ -37,6 +37,9 @@ void PowerTable::runERG() { if (ss2k->isUpdating) { return; } + if (spinBLEServer.spinDownFlag) { + return; + } if (rtConfig->cad.getValue() > 0 && rtConfig->watts.getValue() > 0) { hasConnectedPowerMeter = spinBLEClient.connectedPM; @@ -67,6 +70,10 @@ void PowerTable::runERG() { if (ss2k->resetPowerTableFlag) { powerTable->reset(); + userConfig->setHMin(INT32_MIN); + userConfig->setHMax(INT32_MIN); + rtConfig->setHomed(false); + userConfig->saveToLittleFS(); } loopCounter++; } @@ -76,7 +83,7 @@ void PowerBuffer::set(int i) { this->powerEntry[i].readings++; this->powerEntry[i].watts = rtConfig->watts.getValue(); this->powerEntry[i].cad = rtConfig->cad.getValue(); - this->powerEntry[i].targetPosition = rtConfig->getCurrentIncline() / 100; // dividing by 100 to save memory. + this->powerEntry[i].targetPosition = ss2k->getCurrentPosition() / 100; // dividing by 100 to save memory. } void PowerBuffer::reset() { @@ -131,6 +138,12 @@ void PowerTable::processPowerValue(PowerBuffer& powerBuffer, int cadence, Measur void PowerTable::setStepperMinMax() { int32_t _return = RETURN_ERROR; + // if Homing was preformed, skip estimating min_max + if (rtConfig->getHomed()) { + SS2K_LOG(ERG_MODE_LOG_TAG, "Using detected travel limits during homing"); + return; + } + // if the FTMS device reports resistance feedback, skip estimating min_max if (rtConfig->resistance.getValue() > 0) { rtConfig->setMinStep(-DEFAULT_STEPPER_TRAVEL); @@ -144,13 +157,13 @@ void PowerTable::setStepperMinMax() { _return = this->lookup(minBreakWatts, NORMAL_CAD); if (_return != RETURN_ERROR) { // never set less than one shift below current incline. - if ((_return >= rtConfig->getCurrentIncline()) && (rtConfig->watts.getValue() > userConfig->getMinWatts())) { - _return = rtConfig->getCurrentIncline() - userConfig->getShiftStep(); + if ((_return >= ss2k->getCurrentPosition()) && (rtConfig->watts.getValue() > userConfig->getMinWatts())) { + _return = ss2k->getCurrentPosition() - userConfig->getShiftStep(); SS2K_LOG(ERG_MODE_LOG_TAG, "Min Position too close to current incline: %d", _return); } // never set above max step. if (_return >= rtConfig->getMaxStep()) { - _return = rtConfig->getCurrentIncline() - userConfig->getShiftStep() * 2; + _return = ss2k->getCurrentPosition() - userConfig->getShiftStep() * 2; SS2K_LOG(ERG_MODE_LOG_TAG, "Min Position above max!: %d", _return); } rtConfig->setMinStep(_return); @@ -163,13 +176,13 @@ void PowerTable::setStepperMinMax() { _return = this->lookup(maxBreakWatts, NORMAL_CAD); if (_return != RETURN_ERROR) { // never set less than one shift above current incline. - if ((_return <= rtConfig->getCurrentIncline()) && (rtConfig->watts.getValue() < userConfig->getMaxWatts())) { - _return = rtConfig->getCurrentIncline() + userConfig->getShiftStep(); + if ((_return <= ss2k->getCurrentPosition()) && (rtConfig->watts.getValue() < userConfig->getMaxWatts())) { + _return = ss2k->getCurrentPosition() + userConfig->getShiftStep(); SS2K_LOG(ERG_MODE_LOG_TAG, "Max Position too close to current incline: %d", _return); } // never set below min step. if (_return <= rtConfig->getMinStep()) { - _return = rtConfig->getCurrentIncline() + userConfig->getShiftStep() * 2; + _return = ss2k->getCurrentPosition() + userConfig->getShiftStep() * 2; SS2K_LOG(ERG_MODE_LOG_TAG, "Max Position below min!: %d", _return); } rtConfig->setMaxStep(_return); @@ -838,6 +851,9 @@ bool PowerTable::_manageSaveState() { file.read((uint8_t*)&version, sizeof(version)); int savedQuality; file.read((uint8_t*)&savedQuality, sizeof(savedQuality)); + bool savedHomed; + file.read((uint8_t*)&savedHomed, sizeof(savedHomed)); + if (version != TABLE_VERSION) { SS2K_LOG(POWERTABLE_LOG_TAG, "Expected power table version %d, found version %d", TABLE_VERSION, version); file.close(); @@ -853,32 +869,35 @@ bool PowerTable::_manageSaveState() { this->_save(); } - SS2K_LOG(POWERTABLE_LOG_TAG, "Loading power table version %d, Size %d", version, savedQuality); - - // Initialize a counter for reliable positions - int reliablePositions = 0; - - // Check if we have at least 3 reliable positions in the active table in order to determine a reliable offset to load the saved table - for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { - for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { - int16_t savedTargetPosition = INT16_MIN; - int8_t savedReadings = 0; - file.read((uint8_t*)&savedTargetPosition, sizeof(savedTargetPosition)); - file.read((uint8_t*)&savedReadings, sizeof(savedReadings)); - // Does the saved file have a position that the active session has also recorded? - // We start comparing at watt position 3 (j>2) because low resistance positions are notoriously unreliable. - if ((j > 2) && (this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) && (this->tableRow[i].tableEntry[j].readings > MINIMUM_RELIABLE_POSITIONS) && - (savedReadings > 0)) { - reliablePositions++; + SS2K_LOG(POWERTABLE_LOG_TAG, "Loading power table version %d, Size %d, Homed %d", version, savedQuality, savedHomed); + + // If both current and saved tables were created with homing, we can skip position reliability checks + bool canSkipReliabilityChecks = savedHomed && rtConfig->getHomed(); + + if (!canSkipReliabilityChecks) { + // Initialize a counter for reliable positions + int reliablePositions = 0; + + // Check if we have at least 3 reliable positions in the active table in order to determine a reliable offset to load the saved table + for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { + for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { + int16_t savedTargetPosition = INT16_MIN; + int8_t savedReadings = 0; + file.read((uint8_t*)&savedTargetPosition, sizeof(savedTargetPosition)); + file.read((uint8_t*)&savedReadings, sizeof(savedReadings)); + // Does the saved file have a position that the active session has also recorded? + // We start comparing at watt position 3 (j>2) because low resistance positions are notoriously unreliable. + if ((j > 2) && (this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) && (this->tableRow[i].tableEntry[j].readings > MINIMUM_RELIABLE_POSITIONS) && + (savedReadings > 0)) { + reliablePositions++; + } } } - } - if (reliablePositions < MINIMUM_RELIABLE_POSITIONS) { // Do we have enough active data in order to calculate a (good) offset when we load the new table? - SS2K_LOG(POWERTABLE_LOG_TAG, "Not enough matching positions to load the Power Table. %d of %d needed.", reliablePositions, MINIMUM_RELIABLE_POSITIONS); - file.close(); - return false; - } else { - // continue loading + if (reliablePositions < MINIMUM_RELIABLE_POSITIONS) { // Do we have enough active data in order to calculate a (good) offset when we load the new table? + SS2K_LOG(POWERTABLE_LOG_TAG, "Not enough matching positions to load the Power Table. %d of %d needed.", reliablePositions, MINIMUM_RELIABLE_POSITIONS); + file.close(); + return false; + } } file.close(); @@ -894,44 +913,62 @@ bool PowerTable::_manageSaveState() { // get these reads done, so that we're in the right position to read the data from the file. file.read((uint8_t*)&version, sizeof(version)); file.read((uint8_t*)&savedQuality, sizeof(savedQuality)); - std::vector offsetDifferences; - - reliablePositions = 0; - // Read table entries and calculate offsets - for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { - for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { - int16_t savedTargetPosition = INT16_MIN; - int8_t savedReadings = 0; - file.read((uint8_t*)&savedTargetPosition, sizeof(savedTargetPosition)); - file.read((uint8_t*)&savedReadings, sizeof(savedReadings)); - if ((this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) && (savedTargetPosition != INT16_MIN) && (savedReadings > 0) && - (this->tableRow[i].tableEntry[j].readings > MINIMUM_RELIABLE_POSITIONS)) { - int offset = this->tableRow[i].tableEntry[j].targetPosition - savedTargetPosition; - offsetDifferences.push_back(offset); - SS2K_LOG(POWERTABLE_LOG_TAG, "offset %d", offset); - reliablePositions++; + file.read((uint8_t*)&savedHomed, sizeof(savedHomed)); + + float averageOffset = 0; + if (!canSkipReliabilityChecks) { + std::vector offsetDifferences; + int reliablePositions = 0; + // Read table entries and calculate offsets + for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { + for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { + int16_t savedTargetPosition = INT16_MIN; + int8_t savedReadings = 0; + file.read((uint8_t*)&savedTargetPosition, sizeof(savedTargetPosition)); + file.read((uint8_t*)&savedReadings, sizeof(savedReadings)); + if ((this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) && (savedTargetPosition != INT16_MIN) && (savedReadings > 0) && + (this->tableRow[i].tableEntry[j].readings > MINIMUM_RELIABLE_POSITIONS)) { + int offset = this->tableRow[i].tableEntry[j].targetPosition - savedTargetPosition; + offsetDifferences.push_back(offset); + SS2K_LOG(POWERTABLE_LOG_TAG, "offset %d", offset); + reliablePositions++; + } + this->tableRow[i].tableEntry[j].targetPosition = savedTargetPosition; + this->tableRow[i].tableEntry[j].readings = savedReadings; } - this->tableRow[i].tableEntry[j].targetPosition = savedTargetPosition; - this->tableRow[i].tableEntry[j].readings = savedReadings; - // SS2K_LOG(POWERTABLE_LOG_TAG, "Position %d, %d, Target %d, Readings %d, loaded", i, j, this->tableRow[i].tableEntry[j].targetPosition, - // this->tableRow[i].tableEntry[j].readings); } + averageOffset = std::accumulate(offsetDifferences.begin(), offsetDifferences.end(), 0.0) / offsetDifferences.size(); + } else { + // If both tables were created with homing, just load the values directly + for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { + for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { + int16_t savedTargetPosition = INT16_MIN; + int8_t savedReadings = 0; + file.read((uint8_t*)&savedTargetPosition, sizeof(savedTargetPosition)); + file.read((uint8_t*)&savedReadings, sizeof(savedReadings)); + this->tableRow[i].tableEntry[j].targetPosition = savedTargetPosition; + this->tableRow[i].tableEntry[j].readings = savedReadings; + } + } + SS2K_LOG(POWERTABLE_LOG_TAG, "Both tables were created with homing, loaded values directly"); } file.close(); - float averageOffset = std::accumulate(offsetDifferences.begin(), offsetDifferences.end(), 0.0) / offsetDifferences.size(); - // Apply the offset to all loaded positions except for INT16_MIN values - for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { - for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { - if (this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) { - this->tableRow[i].tableEntry[j].targetPosition += averageOffset; + // Apply the offset if needed + if (!canSkipReliabilityChecks) { + for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { + for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { + if (this->tableRow[i].tableEntry[j].targetPosition != INT16_MIN) { + this->tableRow[i].tableEntry[j].targetPosition += averageOffset; + } } } + SS2K_LOG(POWERTABLE_LOG_TAG, "Power Table loaded with an offset of %d.", averageOffset); } + // set the flag so it isn't loaded again this session. this->_hasBeenLoadedThisSession = true; - SS2K_LOG(POWERTABLE_LOG_TAG, "Power Table loaded with an offset of %d.", averageOffset); } // Implement saving on a timer @@ -961,6 +998,10 @@ bool PowerTable::_save() { int size = getNumReadings(); file.write((uint8_t*)&size, sizeof(size)); + // Write homing state + bool isHomed = rtConfig->getHomed(); + file.write((uint8_t*)&isHomed, sizeof(isHomed)); + // Write table entries for (int i = 0; i < POWERTABLE_CAD_SIZE; i++) { for (int j = 0; j < POWERTABLE_WATT_SIZE; j++) { @@ -1063,7 +1104,7 @@ void ErgMode::computeResistance() { if (actualDelta != 0) { rtConfig->setTargetIncline(rtConfig->getTargetIncline() + (100 * actualDelta)); } else { - rtConfig->setTargetIncline(rtConfig->getCurrentIncline()); + rtConfig->setTargetIncline(ss2k->getCurrentPosition()); } oldResistance = rtConfig->resistance; } @@ -1085,7 +1126,7 @@ void ErgMode::computeErg() { newWatts.setTarget(userConfig->getMinWatts()); } - bool isUserSpinning = this->_userIsSpinning(newCadence, rtConfig->getCurrentIncline()); + bool isUserSpinning = this->_userIsSpinning(newCadence, ss2k->getCurrentPosition()); if (!isUserSpinning) { SS2K_LOG(ERG_MODE_LOG_TAG, "ERG Mode but no User Spin"); return; @@ -1106,11 +1147,11 @@ void ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) { // Sanity check for targets if (tableResult != RETURN_ERROR) { - if (rtConfig->watts.getValue() > newWatts.getTarget() && tableResult > rtConfig->getCurrentIncline()) { + if (rtConfig->watts.getValue() > newWatts.getTarget() && tableResult > ss2k->getCurrentPosition()) { SS2K_LOG(ERG_MODE_LOG_TAG, "Table Result Failed High Test: %d", tableResult); tableResult = RETURN_ERROR; } - if (rtConfig->watts.getValue() < newWatts.getTarget() && tableResult < rtConfig->getCurrentIncline()) { + if (rtConfig->watts.getValue() < newWatts.getTarget() && tableResult < ss2k->getCurrentPosition()) { SS2K_LOG(ERG_MODE_LOG_TAG, "Table Result Failed Low Test: %d", tableResult); tableResult = RETURN_ERROR; } @@ -1121,14 +1162,14 @@ void ErgMode::_setPointChangeState(int newCadence, Measurement& newWatts) { int wattChange = newWatts.getTarget() - newWatts.getValue(); float deviation = ((float)wattChange * 100.0) / ((float)newWatts.getTarget()); float factor = abs(deviation) > 10 ? userConfig->getERGSensitivity() * 2 : userConfig->getERGSensitivity() / 2; - tableResult = rtConfig->getCurrentIncline() + (wattChange * factor); + tableResult = ss2k->getCurrentPosition() + (wattChange * factor); } SS2K_LOG(ERG_MODE_LOG_TAG, "SetPoint changed:%dw PowerTable Result: %d", newWatts.getTarget(), tableResult); _updateValues(newCadence, newWatts, tableResult); - if (rtConfig->getTargetIncline() != rtConfig->getCurrentIncline()) { // add some time to wait while the knob moves to target position. - int timeToAdd = abs(rtConfig->getCurrentIncline() - rtConfig->getTargetIncline()); + if (rtConfig->getTargetIncline() != ss2k->getCurrentPosition()) { // add some time to wait while the knob moves to target position. + int timeToAdd = abs(ss2k->getCurrentPosition() - rtConfig->getTargetIncline()); if (timeToAdd > 5000) { // 5 seconds SS2K_LOG(ERG_MODE_LOG_TAG, "Capping ERG seek time to 5 seconds"); timeToAdd = 5000; @@ -1145,14 +1186,14 @@ void ErgMode::_inSetpointState(int newCadence, Measurement& newWatts) { float deviation = ((float)wattChange * 100.0) / ((float)newWatts.getTarget()); float factor = abs(deviation) > 10 ? userConfig->getERGSensitivity() : userConfig->getERGSensitivity() / 2; - float newIncline = rtConfig->getCurrentIncline() + (wattChange * factor); + float newIncline = ss2k->getCurrentPosition() + (wattChange * factor); _updateValues(newCadence, newWatts, newIncline); } void ErgMode::_updateValues(int newCadence, Measurement& newWatts, float newIncline) { rtConfig->setTargetIncline(newIncline); - _writeLog(rtConfig->getCurrentIncline(), newIncline, this->setPoint, newWatts.getTarget(), this->watts.getValue(), newWatts.getValue(), this->cadence, newCadence); + _writeLog(ss2k->getCurrentPosition(), newIncline, this->setPoint, newWatts.getTarget(), this->watts.getValue(), newWatts.getValue(), this->cadence, newCadence); this->watts = newWatts; this->setPoint = newWatts.getTarget(); diff --git a/src/Main.cpp b/src/Main.cpp index 6a93275a..2e7680f7 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -18,10 +18,11 @@ #include "BLE_Custom_Characteristic.h" #include #include "settings.h" +#include "BLE_Wattbike_Service.h" // Stepper Motor Serial HardwareSerial stepperSerial(2); -TMC2208Stepper driver(&SERIAL_PORT, R_SENSE); // Hardware Serial +TMC2209Stepper driver(&SERIAL_PORT, R_SENSE, 0b00); // Hardware Serial // Peloton Serial HardwareSerial auxSerial(1); @@ -159,6 +160,11 @@ void setup() { 20, /* priority of the task */ &maintenanceLoopTask, /* Task handle to keep track of created task */ 1); /* pin task to core */ + + // if we have homing data, use that instead. + if (userConfig->getHMax() != INT32_MIN && userConfig->getHMin() != INT32_MIN) { + spinBLEServer.spinDownFlag = 1; + } } void loop() { // Delete this task so we can make one that's more memory efficient. @@ -178,6 +184,8 @@ void SS2K::maintenanceLoop(void *pvParameters) { BLECommunications(); // send BLE notification for any userConfig values that changed. BLE_ss2kCustomCharacteristic::parseNemit(); + // Update Zwift Gear UI if shift happened + wattbikeService.parseNemit(); // Run What used to be in the Stepper Task. ss2k->moveStepper(); // Run what used to be in the ERG Mode Task. @@ -187,7 +195,7 @@ void SS2K::maintenanceLoop(void *pvParameters) { // If we're in ERG mode, modify shift commands to inc/dec the target watts instead. ss2k->FTMSModeShiftModifier(); // If we have a resistance bike attached, slow down when we're close to the limits. - if (ss2k->pelotonIsConnected) { + if (ss2k->pelotonIsConnected && !rtConfig->getHomed()) { int speed = userConfig->getStepperSpeed(); float resistance = rtConfig->resistance.getValue(); float maxResistance = rtConfig->getMaxResistance(); @@ -241,6 +249,7 @@ void SS2K::maintenanceLoop(void *pvParameters) { if (ss2k->resetDefaultsFlag) { LittleFS.format(); userConfig->setDefaults(); + powerTable->reset(); userConfig->saveToLittleFS(); ss2k->resetDefaultsFlag = false; ss2k->rebootFlag = true; @@ -301,6 +310,9 @@ void SS2K::maintenanceLoop(void *pvParameters) { #endif // UNIT_TEST void SS2K::FTMSModeShiftModifier() { + if (spinBLEServer.spinDownFlag) { + return; + } int shiftDelta = rtConfig->getShifterPosition() - ss2k->lastShifterPosition; if (shiftDelta) { // Shift detected switch (rtConfig->getFTMSMode()) { @@ -351,6 +363,8 @@ void SS2K::FTMSModeShiftModifier() { ((ss2k->targetPosition + shiftDelta * userConfig->getShiftStep()) > rtConfig->getMaxStep())) { SS2K_LOG(MAIN_LOG_TAG, "Shift Blocked by stepper limits."); rtConfig->setShifterPosition(ss2k->lastShifterPosition); + } else if (rtConfig->getHomed()) { + // was homed. Allow because previous test would have failed if out of bounds. } else if ((rtConfig->resistance.getValue() <= rtConfig->getMinResistance()) && (shiftDelta > 0)) { // User Shifted in the proper direction - allow } else if ((rtConfig->resistance.getValue() >= rtConfig->getMaxResistance()) && (shiftDelta < 0)) { @@ -381,17 +395,20 @@ void SS2K::restartWifi() { } void SS2K::moveStepper() { + if (spinBLEServer.spinDownFlag) { + return; + } bool _stepperDir = userConfig->getStepperDir(); if (stepper) { ss2k->stepperIsRunning = stepper->isRunning(); ss2k->currentPosition = stepper->getCurrentPosition(); if (!ss2k->externalControl) { if ((rtConfig->getFTMSMode() == FitnessMachineControlPointProcedure::SetTargetPower)) { - // don't drive lower out of bounds. This is a final test that should never happen. + // don't drive lower out of bounds. This is a final test that should never happen. if ((stepper->getCurrentPosition() > rtConfig->getTargetIncline()) && (rtConfig->watts.getValue() < rtConfig->watts.getTarget())) { rtConfig->setTargetIncline(stepper->getCurrentPosition() + 1); } - // don't drive higher out of bounds. This is a final test that should never happen. + // don't drive higher out of bounds. This is a final test that should never happen. if ((stepper->getCurrentPosition() < rtConfig->getTargetIncline()) && (rtConfig->watts.getValue() > rtConfig->watts.getTarget())) { rtConfig->setTargetIncline(stepper->getCurrentPosition() - 1); } @@ -412,7 +429,7 @@ void SS2K::moveStepper() { vTaskDelay(100 / portTICK_PERIOD_MS); } - if (ss2k->pelotonIsConnected) { + if (ss2k->pelotonIsConnected && !rtConfig->getHomed()) { if ((rtConfig->resistance.getValue() > rtConfig->getMinResistance()) && (rtConfig->resistance.getValue() < rtConfig->getMaxResistance())) { stepper->moveTo(ss2k->targetPosition); } else if (rtConfig->resistance.getValue() <= rtConfig->getMinResistance()) { // Limit Stepper to Min Resistance @@ -441,8 +458,6 @@ void SS2K::moveStepper() { stepper->moveTo(rtConfig->getMaxStep() - 1); } } - //Sync external object - rtConfig->setCurrentIncline((float)stepper->getCurrentPosition()); if (connectedClientCount() > 0) { stepper->setAutoEnable(false); // Keep the stepper from rolling back due to head tube slack. Motor Driver still lowers power between moves @@ -511,39 +526,107 @@ void SS2K::resetIfShiftersHeld() { } } -void SS2K::setupTMCStepperDriver() { +void SS2K::setupTMCStepperDriver(bool reset) { // FastAccel setup - engine.init(); - stepper = engine.stepperConnectToPin(currentBoard.stepPin); - stepper->setDirectionPin(currentBoard.dirPin, userConfig->getStepperDir()); - stepper->setEnablePin(currentBoard.enablePin); - stepper->setAutoEnable(true); - stepper->setSpeedInHz(DEFAULT_STEPPER_SPEED); - stepper->setAcceleration(STEPPER_ACCELERATION); - stepper->setDelayToDisable(1000); - - // TMC Driver Setup - driver.begin(); + if (!reset) { + engine.init(); + stepper = engine.stepperConnectToPin(currentBoard.stepPin); + stepper->setDirectionPin(currentBoard.dirPin, userConfig->getStepperDir()); + stepper->setEnablePin(currentBoard.enablePin); + stepper->setAutoEnable(true); + stepper->setSpeedInHz(DEFAULT_STEPPER_SPEED); + stepper->setAcceleration(STEPPER_ACCELERATION); + stepper->setDelayToDisable(1000); + // TMC Driver Setup + driver.begin(); + } + driver.pdn_disable(true); driver.mstep_reg_select(true); - ss2k->updateStepperSpeed(); - ss2k->updateStepperPower(); - driver.microsteps(4); // Set microsteps to 1/8th - driver.irun(currentBoard.pwrScaler); - driver.ihold((uint8_t)(currentBoard.pwrScaler * .5)); // hold current % 0-DRIVER_MAX_PWR_SCALER - driver.iholddelay(10); // Controls the number of clock cycles for motor + driver.microsteps(4); // Set microsteps to 1/8th + driver.iholddelay(10); // Controls the number of clock cycles for motor // power down after standstill is detected driver.TPOWERDOWN(128); driver.toff(5); - ss2k->updateStealthChop(); + this->updateStealthChop(); + driver.irun(currentBoard.pwrScaler); + driver.ihold((uint8_t)(currentBoard.pwrScaler * .5)); // hold current % 0-DRIVER_MAX_PWR_SCALER + this->updateStepperSpeed(); + this->updateStepperPower(); + this->setCurrentPosition(stepper->getCurrentPosition()); +} + +void SS2K::goHome(bool bothDirections) { + if (currentBoard.name != r2_NAME) { + SS2K_LOG(MAIN_LOG_TAG, "Board Doesn't support homing"); + return; + } + SS2K_LOG(MAIN_LOG_TAG, "Homing..."); + int _IFCNT = driver.IFCNT(); // Number of UART commands rx by driver + SS2K_LOG(MAIN_LOG_TAG, "Updating driver..."); + updateStepperPower(100); + vTaskDelay(50 / portTICK_PERIOD_MS); + driver.irun(2); // low power + vTaskDelay(50 / portTICK_PERIOD_MS); + driver.ihold((uint8_t)(1)); + vTaskDelay(50 / portTICK_PERIOD_MS); + this->updateStepperSpeed(1500); + bool stalled = false; + int threshold = 0; + vTaskDelay(1000 / portTICK_PERIOD_MS); + if (bothDirections) { + stepper->runForward(); + vTaskDelay(250 / portTICK_PERIOD_MS); // wait until stable + threshold = driver.SG_RESULT(); // take reading + Serial.printf("%d ", driver.SG_RESULT()); + vTaskDelay(250 / portTICK_PERIOD_MS); + while (!stalled) { + // stalled = (threshold < 200); // Were we already at the stop? + stalled = (driver.SG_RESULT() < threshold - 100); + } + stalled = false; + stepper->forceStop(); + stepper->disableOutputs(); + vTaskDelay(2000 / portTICK_PERIOD_MS); + rtConfig->setMaxStep(stepper->getCurrentPosition() - 200); + SS2K_LOG(MAIN_LOG_TAG, "Max Position found: %d.", rtConfig->getMaxStep()); + stepper->enableOutputs(); + } + stepper->runBackward(); + vTaskDelay(250 / portTICK_PERIOD_MS); + threshold = driver.SG_RESULT(); + Serial.printf("%d ", driver.SG_RESULT()); + vTaskDelay(250 / portTICK_PERIOD_MS); + while (!stalled) { + stalled = (driver.SG_RESULT() < threshold - 75); + } + stepper->forceStop(); + stepper->disableOutputs(); + vTaskDelay(100 / portTICK_PERIOD_MS); + stepper->setCurrentPosition((int32_t)0); + ss2k->setTargetPosition(0); + rtConfig->setMinStep(stepper->getCurrentPosition() + userConfig->getShiftStep()); + SS2K_LOG(MAIN_LOG_TAG, "Min Position found: %d.", rtConfig->getMinStep()); + stepper->enableOutputs(); + + // Start Saving Settings + if (bothDirections) { + userConfig->setHMin(rtConfig->getMinStep()); + userConfig->setHMax(rtConfig->getMaxStep()); + } + // In case this was only one direction homing. + rtConfig->setMaxStep(userConfig->getHMax()); + userConfig->saveToLittleFS(); + rtConfig->setHomed(true); + this->setupTMCStepperDriver(true); } // Applies current power to driver -void SS2K::updateStepperPower() { - uint16_t rmsPwr = (userConfig->getStepperPower()); +void SS2K::updateStepperPower(int pwr) { + uint16_t rmsPwr = (pwr == 0) ? userConfig->getStepperPower() : pwr; driver.rms_current(rmsPwr); uint16_t current = driver.cs_actual(); - SS2K_LOG(MAIN_LOG_TAG, "Stepper power is now %d. read:cs=%U", userConfig->getStepperPower(), current); + SS2K_LOG(MAIN_LOG_TAG, "Stepper power is now %d. read:cs=%U", (pwr == 0) ? userConfig->getStepperPower() : pwr, current); } // Applies current StealthChop to driver diff --git a/src/SensorCollector.cpp b/src/SensorCollector.cpp index dae792d2..2452dcf3 100644 --- a/src/SensorCollector.cpp +++ b/src/SensorCollector.cpp @@ -72,7 +72,7 @@ void collectAndSet(NimBLEUUID charUUID, NimBLEUUID serviceUUID, NimBLEAddress ad } //////adding incline so that i can plot it - logBufLength += snprintf(logBuf + logBufLength, kLogBufMaxLength - logBufLength, " POS(%d)", ss2k->currentPosition); + logBufLength += snprintf(logBuf + logBufLength, kLogBufMaxLength - logBufLength, " POS(%d)", ss2k->getCurrentPosition()); strncat(logBuf + logBufLength, " ]", kLogBufMaxLength - logBufLength); SS2K_LOG(BLE_COMMON_LOG_TAG, "%s", logBuf); diff --git a/src/SmartSpin_parameters.cpp b/src/SmartSpin_parameters.cpp index e113a0aa..913e0563 100644 --- a/src/SmartSpin_parameters.cpp +++ b/src/SmartSpin_parameters.cpp @@ -28,16 +28,16 @@ String RuntimeParameters::returnJSON() { doc["simCad"] = this->cad.getSimulate(); doc["resistance"] = this->resistance.getValue(); doc["targetResistance"] = this->resistance.getTarget(); - doc["targetIncline"] = targetIncline; - doc["currentIncline"] = currentIncline; - doc["speed"] = simulatedSpeed; - doc["simTargetWatts"] = simTargetWatts; - doc["FTMSMode"] = FTMSMode; - doc["shifterPosition"] = shifterPosition; - doc["minStep"] = minStep; - doc["maxStep"] = maxStep; - doc["minResistance"] = minResistance; - doc["maxResistance"] = maxResistance; + doc["homed"] = this->homed; + doc["targetIncline"] = this->targetIncline; + doc["speed"] = this->simulatedSpeed; + doc["simTargetWatts"] = this->simTargetWatts; + doc["FTMSMode"] = this->FTMSMode; + doc["shifterPosition"] = this->shifterPosition; + doc["minStep"] = this->minStep; + doc["maxStep"] = this->maxStep; + doc["minResistance"] = this->minResistance; + doc["maxResistance"] = this->maxResistance; String output; serializeJson(doc, output); @@ -51,7 +51,7 @@ void userParameters::setDefaults() { shiftStep = DEFAULT_SHIFT_STEP; stealthChop = STEALTHCHOP; stepperPower = DEFAULT_STEPPER_POWER; - stepperSpeed = DEFAULT_STEPPER_SPEED; + stepperSpeed = DEFAULT_STEPPER_SPEED; inclineMultiplier = INCLINE_MULTIPLIER; powerCorrectionFactor = 1.0; ERGSensitivity = ERG_SENSITIVITY; @@ -67,6 +67,8 @@ void userParameters::setDefaults() { stepperDir = true; shifterDir = true; udpLogEnabled = false; + hMin = INT32_MIN; + hMax = INT32_MIN; } //--------------------------------------------------------------------------------- @@ -100,6 +102,8 @@ String userParameters::returnJSON() { doc["shifterDir"] = shifterDir; doc["stepperDir"] = stepperDir; doc["udpLogEnabled"] = udpLogEnabled; + doc["hMin"] = hMin; + doc["hMax"] = hMax; String output; serializeJson(doc, output); @@ -142,12 +146,14 @@ void userParameters::saveToLittleFS() { doc["connectedPowerMeter"] = connectedPowerMeter; doc["connectedHeartMonitor"] = connectedHeartMonitor; doc["connectedRemote"] = connectedRemote; - //doc["foundDevices"] = foundDevices; - doc["maxWatts"] = maxWatts; - doc["minWatts"] = minWatts; - doc["shifterDir"] = shifterDir; - doc["stepperDir"] = stepperDir; - doc["udpLogEnabled"] = udpLogEnabled; + // doc["foundDevices"] = foundDevices; + doc["maxWatts"] = maxWatts; + doc["minWatts"] = minWatts; + doc["shifterDir"] = shifterDir; + doc["stepperDir"] = stepperDir; + doc["udpLogEnabled"] = udpLogEnabled; + doc["hMin"] = hMin; + doc["hMax"] = hMax; // Serialize JSON to file if (serializeJson(doc, file) == 0) { @@ -193,16 +199,16 @@ void userParameters::loadFromLittleFS() { setPassword(doc["password"]); setConnectedPowerMeter(doc["connectedPowerMeter"]); setConnectedHeartMonitor(doc["connectedHeartMonitor"]); - //setFoundDevices(doc["foundDevices"]); - + // setFoundDevices(doc["foundDevices"]); + // If statements to upgrade old versions of config.txt that didn't include these - if (doc["ERGSensitivity"]) { + if (doc["ERGSensitivity"]) { setERGSensitivity(doc["ERGSensitivity"]); } if (doc["maxWatts"]) { setMaxWatts(doc["maxWatts"]); } - if (doc["stepperSpeed"]){ + if (doc["stepperSpeed"]) { setStepperSpeed(doc["stepperSpeed"]); } if (doc["minWatts"]) { @@ -226,6 +232,12 @@ void userParameters::loadFromLittleFS() { if (doc["connectedRemote"]) { setConnectedRemote(doc["connectedRemote"]); } + if (!doc["hMin"].isNull()) { + setHMin(doc["hMin"]); + } + if (!doc["hMax"].isNull()) { + setHMax(doc["hMax"]); + } SS2K_LOG(CONFIG_LOG_TAG, "Config File Loaded: %s", configFILENAME); file.close();