diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 08d4fbed..70d567d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,6 @@ blank_issues_enabled : flase contact_links: - - name: SmartSpin2K Wiki + - name: SmartSpin2k Wiki url: https://github.com/doudar/SmartSpin2k/wiki about: Please check here first if you have questions. - name: SmartSpin Discussions diff --git a/CHANGELOG.md b/CHANGELOG.md index 12898e34..36d75f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added pass through shifting in both ERG and SIM mode. - Refined and added BLE custom characteristics for upcoming configuration app. -- +- Added CSC Service to BLE server. + ### Changed - Updated communications overview picture. - Updated kit purchasing links. @@ -25,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - BLE scans blocked during firmware upgrade. - Increased the default incline multiplier to 5. - Added more robust activity monitoring and reboot every 30 minutes if there is no activity. +- Updated all references of SmartSkin2K to SmartSpin2k for consistency. +- Fixed bug where BT scanner "Loading" wouldn't disappear if "NONE" and "NONE" were selected. ### Hardware - added Yesoul S3. @@ -412,7 +415,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 *1.3.21 * SS2K BLE Server now accepts more than one simultaneous connection (you can not connect SS2K to both Zwift and another app simultaneously) * Echelon bike is now supported -* SmartSpin2K.local more accessible with different browsers (fixed certain MDNS dropouts) +* SmartSpin2k.local more accessible with different browsers (fixed certain MDNS dropouts) * Flywheel bike support built in (still untested) * Backend (client) completely revamped to allow more device decoders, better stability, and faster network speeds. * Lots of FTMS server and client polishing diff --git a/Hardware/Common Assets/Inserts/README.md b/Hardware/Common Assets/Inserts/README.md index 52fcc35e..858d67ea 100644 --- a/Hardware/Common Assets/Inserts/README.md +++ b/Hardware/Common Assets/Inserts/README.md @@ -1,4 +1,4 @@ -These are the parts that adapt your knob to the SmartSpin2K. +These are the parts that adapt your knob to the SmartSpin2k. You'll see two of most of these because as we make them we've designed two slightly different sizes to accommodate diff --git a/Hardware/Common Assets/Shifters/README.MD b/Hardware/Common Assets/Shifters/README.MD index 4d8f95e9..7895755d 100644 --- a/Hardware/Common Assets/Shifters/README.MD +++ b/Hardware/Common Assets/Shifters/README.MD @@ -10,7 +10,7 @@ Fusion 360 exports and generic STEP files are both provided - [See Shifter Assembly video linked in Wiki](https://github.com/doudar/SmartSpin2k/wiki/Building-How-To) ## Setup -- In case you need to flip the orientation of the shifter, you can easily do so in the SmartSpin2K User Interface. Navigate to [http://SmartSpin2K.local/](http://SmartSpin2K.local). Here, you can toggle the Shifter Direction button until the device shifts in the right direction and save your settings. You can verify your settings in the [Web Shifter](http://smartspin2k.local/shift.html) +- In case you need to flip the orientation of the shifter, you can easily do so in the SmartSpin2k User Interface. Navigate to [http://SmartSpin2k.local/](http://SmartSpin2k.local). Here, you can toggle the Shifter Direction button until the device shifts in the right direction and save your settings. You can verify your settings in the [Web Shifter](http://smartspin2k.local/shift.html) ## BOM - Momentary microswitches diff --git a/Hardware/README.md b/Hardware/README.md index bc2acb69..68b477dd 100644 --- a/Hardware/README.md +++ b/Hardware/README.md @@ -1,4 +1,4 @@ -# SmartSpin2K Hardware +# SmartSpin2k Hardware V3 is the brand spanking new version where we've gone to a more rigid mount and designed a completly custom PCB with SMT components which can be manufactured using automated techniques. The newest PCB sports a uart->serial driver to allow direct communication with Peloton bikes. V2 prints easier, is smaller and uses less hardware. It has a much better gear ratio than V1 and supports a carrier board PCB onto which the devkits mount for easy wiring/soldering. A rigid mount referred to as Direct Mount is available for stable mounting. @@ -18,7 +18,7 @@ Use with the SMT manufaactured PCB and for compatibility with Peloton bikes. ## [Common Assets](https://github.com/doudar/SmartSpin2k/tree/develop/Hardware/Common%20Assets) ### [Arm](https://github.com/doudar/SmartSpin2k/tree/develop/Hardware/Common%20Assets/Arm) -The Arm is used to connect the SmartSpin2K to the bike mount. See Readme for sizing and fitment information. +The Arm is used to connect the SmartSpin2k to the bike mount. See Readme for sizing and fitment information. Required for V3 and V2 Direct Mount. @@ -28,7 +28,7 @@ Attaches to the head tube of your bike and provides an attachment point for the Required for V3 and V2 Direct Mount ### [Gears](https://github.com/doudar/SmartSpin2k/tree/develop/Hardware/Common%20Assets/Bike%20Mount) -Internal drive gears for the SmartSpin2K. 11:40t gearing. +Internal drive gears for the SmartSpin2k. 11:40t gearing. Required for all V2 and V3 variations @@ -39,7 +39,7 @@ Required for all V2 and V3 variations ### [KnobCup](https://github.com/doudar/SmartSpin2k/tree/develop/Hardware/Common%20Assets/KnobCups) -Knob Cup retains the insert and connects to the internal gearbox of the SmartSpin2K +Knob Cup retains the insert and connects to the internal gearbox of the SmartSpin2k Required for all V2 and V3 variations diff --git a/Hardware/V2 - Through Hole/readme.md b/Hardware/V2 - Through Hole/readme.md index d1600bbf..d6728a47 100644 --- a/Hardware/V2 - Through Hole/readme.md +++ b/Hardware/V2 - Through Hole/readme.md @@ -1,6 +1,6 @@ Hardware 2.0 # [Description] -This version of SmartSpin2K is designed with DIY in mind. The PCB can be hand soldered using readily available components. +This version of SmartSpin2k is designed with DIY in mind. The PCB can be hand soldered using readily available components. The case for V2 is available in multiple versions that are suitable for various needs. The direct mount design is the currently recommended case design. diff --git a/data/bluetoothscanner.html b/data/bluetoothscanner.html index 27d41497..412e7fc0 100644 --- a/data/bluetoothscanner.html +++ b/data/bluetoothscanner.html @@ -7,7 +7,7 @@ background-color: #03245c; } - SmartSpin2K Bluetooth Scanner + SmartSpin2k Bluetooth Scanner diff --git a/data/btsimulator.html b/data/btsimulator.html index 0cabde73..c362cfad 100644 --- a/data/btsimulator.html +++ b/data/btsimulator.html @@ -8,7 +8,7 @@ background-color: #03245c; } - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/data/develop.html b/data/develop.html index 6fe06ccc..da2d3955 100644 --- a/data/develop.html +++ b/data/develop.html @@ -8,7 +8,7 @@ } - SmartSpin2K Web Server + SmartSpin2k Web Server
diff --git a/data/hrtowatts.html b/data/hrtowatts.html index 2e034e1f..bcf94fe3 100644 --- a/data/hrtowatts.html +++ b/data/hrtowatts.html @@ -8,7 +8,7 @@ background-color: #03245c; } - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/data/index.html b/data/index.html index e305f588..516e25c6 100644 --- a/data/index.html +++ b/data/index.html @@ -8,7 +8,7 @@ } - SmartSpin2K Web Server + SmartSpin2k Web Server
http://github.com/doudar/SmartSpin2k diff --git a/data/settings.html b/data/settings.html index e8039a35..df80a14a 100644 --- a/data/settings.html +++ b/data/settings.html @@ -7,7 +7,7 @@ background-color: #03245c; } - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/data/shift.html b/data/shift.html index a44f0307..079fc13a 100644 --- a/data/shift.html +++ b/data/shift.html @@ -8,7 +8,7 @@ background-color: #03245c; } - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/data/status.html b/data/status.html index 48ba3b2c..067dac5b 100644 --- a/data/status.html +++ b/data/status.html @@ -7,7 +7,7 @@ background-color: #03245c; } - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/data/streamfit.html b/data/streamfit.html index 8a8a554e..72138f73 100644 --- a/data/streamfit.html +++ b/data/streamfit.html @@ -4,7 +4,7 @@ - SmartSpin2K Web Server + SmartSpin2k Web Server diff --git a/include/BLE_Common.h b/include/BLE_Common.h index 615289a8..9890884f 100644 --- a/include/BLE_Common.h +++ b/include/BLE_Common.h @@ -52,13 +52,14 @@ class MyCallbacks : public NimBLECharacteristicCallbacks { class ss2kCustomCharacteristicCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *); + void onSubscribe(NimBLECharacteristic *pCharacteristic, ble_gap_conn_desc *desc, uint16_t subValue); }; class ss2kCustomCharacteristic { public: - //Used internally for notify and onWrite Callback. + // Used internally for notify and onWrite Callback. static void process(std::string rxValue); - //Custom Characteristic value that needs to be notified + // Custom Characteristic value that needs to be notified static void notify(char _item); // Notify any changed value in userConfig static void parseNemit(); @@ -73,6 +74,7 @@ class SpinBLEServer { bool Heartrate : 1; bool CyclingPowerMeasurement : 1; bool IndoorBikeData : 1; + bool CyclingSpeedCadence : 1; } clientSubscribed; NimBLEServer *pServer = nullptr; void setClientSubscribed(NimBLEUUID pUUID, bool subscribe); @@ -87,8 +89,10 @@ void startBLEServer(); bool spinDown(); void logCharacteristic(char *buffer, const size_t bufferCapacity, const byte *data, const size_t dataLength, const NimBLEUUID serviceUUID, const NimBLEUUID charUUID, const char *format, ...); +void updateWheelAndCrankRev(); void updateIndoorBikeDataChar(); void updateCyclingPowerMeasurementChar(); +void updateCyclingSpeedCadenceChar(); void calculateInstPwrFromHR(); void updateHeartRateMeasurementChar(); int connectedClientCount(); @@ -158,18 +162,21 @@ class SpinBLEClient { private: public: // Not all of these need to be public. This should be cleaned up // later. - boolean connectedPM = false; - boolean connectedHRM = false; - boolean connectedCD = false; - boolean connectedCT = false; - boolean connectedRemote = false; - boolean doScan = false; - bool dontBlockScan = true; - int intentionalDisconnect = 0; - int noReadingIn = 0; - int cscCumulativeCrankRev = 0; - int cscLastCrankEvtTime = 0; - int reconnectTries = MAX_RECONNECT_TRIES; + boolean connectedPM = false; + boolean connectedHRM = false; + boolean connectedCD = false; + boolean connectedCT = false; + boolean connectedSpeed = false; + boolean connectedRemote = false; + boolean doScan = false; + bool dontBlockScan = true; + int intentionalDisconnect = 0; + int noReadingIn = 0; + long int cscCumulativeCrankRev = 0; + double cscLastCrankEvtTime = 0.0; + long int cscCumulativeWheelRev = 0; + double cscLastWheelEvtTime = 0.0; + int reconnectTries = MAX_RECONNECT_TRIES; BLERemoteCharacteristic *pRemoteCharacteristic = nullptr; diff --git a/include/BLE_Definitions.h b/include/BLE_Definitions.h index 778aae59..f84a3fb5 100644 --- a/include/BLE_Definitions.h +++ b/include/BLE_Definitions.h @@ -7,6 +7,29 @@ #pragma once +struct FitnessMachineIndoorBikeDataFlags { + enum Types : uint16_t { + MoreDataBit = 1U << 0, + AverageSpeedPresent = 1U << 1, + InstantaneousCadencePresent = 1U << 2, + AverageCadencePresent = 1U << 3, + TotalDistancePresent = 1U << 4, + ResistanceLevelPresent = 1U << 5, + InstantaneousPowerPresent = 1U << 6, + AveragePowerPresent = 1U << 7, + ExpendedEnergyPresent = 1U << 8, + HeartRatePresent = 1U << 9, + MetabolicEquivalentPresent = 1U << 10, + ElapsedTimePresent = 1U << 11, + RemainingTimePresent = 1U << 12 + + }; +}; + +inline FitnessMachineIndoorBikeDataFlags::Types operator|(FitnessMachineIndoorBikeDataFlags::Types a, FitnessMachineIndoorBikeDataFlags::Types b) { + return static_cast(static_cast(a) | static_cast(b)); +} + // https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0/ // Table 4.13: Training Status Field Definition struct FitnessMachineTrainingStatus { @@ -166,4 +189,120 @@ struct FitnessMachineFeature { struct FtmsStatus { uint8_t data[8]; int length; +}; + +class CyclingPowerMeasurement { + public: + // Flags definition as per specification + struct Flags { + uint16_t pedalPowerBalancePresent : 1; + uint16_t pedalPowerBalanceReference : 1; + uint16_t accumulatedTorquePresent : 1; + uint16_t accumulatedTorqueSource : 1; + uint16_t wheelRevolutionDataPresent : 1; + uint16_t crankRevolutionDataPresent : 1; + uint16_t extremeForceMagnitudesPresent : 1; + uint16_t extremeTorqueMagnitudesPresent : 1; + uint16_t extremeAnglesPresent : 1; + uint16_t topDeadSpotAnglePresent : 1; + uint16_t bottomDeadSpotAnglePresent : 1; + uint16_t accumulatedEnergyPresent : 1; + uint16_t offsetCompensationIndicator : 1; + uint16_t reserved : 3; + } flags; + + // Assuming these are the possible data fields based on flags + int16_t instantaneousPower; // Mandatory + // Other fields as optional, based on the flags + uint8_t pedalPowerBalance; // Example optional field + uint16_t accumulatedTorque; + uint32_t cumulativeWheelRevolutions; + uint16_t lastWheelEventTime; + uint16_t cumulativeCrankRevolutions; + uint16_t lastCrankEventTime; + + std::vector toByteArray() { + std::vector data; + // Add flags to data vector + data.push_back(static_cast(*(reinterpret_cast(&flags)) & 0xFF)); + data.push_back(static_cast((*(reinterpret_cast(&flags)) >> 8) & 0xFF)); + + // Add Instantaneous Power + data.push_back(static_cast(instantaneousPower & 0xFF)); + data.push_back(static_cast((instantaneousPower >> 8) & 0xFF)); + + // Conditional fields based on flags + if (flags.crankRevolutionDataPresent) { + // Add crank revolution data if present + data.push_back(static_cast(cumulativeCrankRevolutions & 0xFF)); + data.push_back(static_cast((cumulativeCrankRevolutions >> 8) & 0xFF)); + + data.push_back(static_cast(lastCrankEventTime & 0xFF)); + data.push_back(static_cast((lastCrankEventTime >> 8) & 0xFF)); + } + // Conditional fields based on flags + if (flags.wheelRevolutionDataPresent) { + // Add wheel revolution data if present + data.push_back(static_cast(cumulativeWheelRevolutions & 0xFF)); + data.push_back(static_cast((cumulativeWheelRevolutions >> 8) & 0xFF)); + data.push_back(static_cast((cumulativeWheelRevolutions >> 16) & 0xFF)); + data.push_back(static_cast((cumulativeWheelRevolutions >> 24) & 0xFF)); + + data.push_back(static_cast(lastWheelEventTime & 0xFF)); + data.push_back(static_cast((lastWheelEventTime >> 8) & 0xFF)); + } + + return data; + } +}; + +class CscMeasurement { + public: + // Flags definition as per specification + struct Flags { + uint8_t wheelRevolutionDataPresent : 1; + uint8_t crankRevolutionDataPresent : 1; + uint8_t reserved : 6; + } flags; + + // Data fields + uint32_t cumulativeWheelRevolutions; + uint16_t lastWheelEventTime; // Resolution of 1/1024 seconds + uint16_t cumulativeCrankRevolutions; + uint16_t lastCrankEventTime; // Resolution of 1/1024 seconds + + CscMeasurement() : cumulativeWheelRevolutions(0), lastWheelEventTime(0), cumulativeCrankRevolutions(0), lastCrankEventTime(0) { + // Clear all flags initially + *(reinterpret_cast(&flags)) = 0; + } + + std::vector toByteArray() { + std::vector data; + + // Add flags to data vector + data.push_back(*(reinterpret_cast(&flags))); + + // Conditional fields based on flags + if (flags.wheelRevolutionDataPresent) { + // Add wheel revolution data if present + for (int i = 0; i < 4; ++i) { + data.push_back((cumulativeWheelRevolutions >> (i * 8)) & 0xFF); + } + for (int i = 0; i < 2; ++i) { + data.push_back((lastWheelEventTime >> (i * 8)) & 0xFF); + } + } + + if (flags.crankRevolutionDataPresent) { + // Add crank revolution data if present + for (int i = 0; i < 2; ++i) { + data.push_back((cumulativeCrankRevolutions >> (i * 8)) & 0xFF); + } + for (int i = 0; i < 2; ++i) { + data.push_back((lastCrankEventTime >> (i * 8)) & 0xFF); + } + } + + return data; + } }; \ No newline at end of file diff --git a/include/HTTP_Server_Basic.h b/include/HTTP_Server_Basic.h index b38628fd..ee58f32b 100644 --- a/include/HTTP_Server_Basic.h +++ b/include/HTTP_Server_Basic.h @@ -18,6 +18,7 @@ class HTTP_Server { void start(); void stop(); + static void handleBTScanner(); static void handleLittleFSFile(); static void handleIndexFile(); static void settingsProcessor(); diff --git a/include/Main.h b/include/Main.h index f8813d8f..7faeeeb3 100644 --- a/include/Main.h +++ b/include/Main.h @@ -7,13 +7,13 @@ #pragma once -#include "settings.h" #include "HTTP_Server_Basic.h" #include "SmartSpin_parameters.h" #include "BLE_Common.h" #include "LittleFS_Upgrade.h" #include "boards.h" #include "SensorCollector.h" +#include "SS2KLog.h" #define MAIN_LOG_TAG "Main" diff --git a/include/README b/include/README index 5e424e39..53cede99 100644 --- a/include/README +++ b/include/README @@ -1,4 +1,4 @@ -SmartSpin2K - A buildable software & hardware project that can upgrade a stationary spin bike into a smart bike. +SmartSpin2k - A buildable software & hardware project that can upgrade a stationary spin bike into a smart bike. This software registers an ESP32 as a BLE FTMS device which then uses a stepper motor to turn the resistance knob on a regular spin bike. BLE code based on examples from https://github.com/nkolban diff --git a/include/SS2KLog.h b/include/SS2KLog.h index 61f63a70..774543c9 100644 --- a/include/SS2KLog.h +++ b/include/SS2KLog.h @@ -22,10 +22,6 @@ #define SS2K_LOG_TAG "SS2K" #define LOG_HANDLER_TAG "Log_Handler" -#ifndef DEBUG_LOG_BUFFER_SIZE -#define DEBUG_LOG_BUFFER_SIZE 600 -#endif - #ifndef DEBUG_FILE_CHARS_PER_LINE #define DEBUG_FILE_CHARS_PER_LINE 64 #endif diff --git a/include/SmartSpin_parameters.h b/include/SmartSpin_parameters.h index 3814568f..e3c4a563 100644 --- a/include/SmartSpin_parameters.h +++ b/include/SmartSpin_parameters.h @@ -13,6 +13,8 @@ #include #endif +#include "settings.h" + #define CONFIG_LOG_TAG "Config" class Measurement { diff --git a/include/settings.h b/include/settings.h index f2e4ca3d..902da682 100644 --- a/include/settings.h +++ b/include/settings.h @@ -7,16 +7,14 @@ #pragma once -#include "SS2KLog.h" - // Update firmware on boot? #define AUTO_FIRMWARE_UPDATE true // Default Bluetooth WiFi and MDNS Name -#define DEVICE_NAME "SmartSpin2K" +const char * const DEVICE_NAME = "SmartSpin2k"; // Default WiFi Password -#define DEFAULT_PASSWORD "password" +const char * const DEFAULT_PASSWORD = "password"; // default URL To get Updates From. // If changed you'll also need to get a root certificate from the new server @@ -246,6 +244,10 @@ // how long to try STA mode before falling back to AP mode #define WIFI_CONNECT_TIMEOUT 10 +#ifndef DEBUG_LOG_BUFFER_SIZE +#define DEBUG_LOG_BUFFER_SIZE 600 +#endif + // Max size of userconfig #define USERCONFIG_JSON_SIZE 1524 + DEBUG_LOG_BUFFER_SIZE diff --git a/lib/SS2K/include/Constants.h b/lib/SS2K/include/Constants.h index ec095c5e..fca91946 100644 --- a/lib/SS2K/include/Constants.h +++ b/lib/SS2K/include/Constants.h @@ -9,7 +9,7 @@ #include -// SmartSpin2K custom UUID's +// SmartSpin2k custom UUID's #define SMARTSPIN2K_SERVICE_UUID NimBLEUUID("77776277-7877-7774-4466-896665500000") #define SMARTSPIN2K_CHARACTERISTIC_UUID NimBLEUUID("77776277-7877-7774-4466-896665500001") @@ -31,6 +31,7 @@ // Cycling Power Service #define CSCSERVICE_UUID NimBLEUUID((uint16_t)0x1816) #define CSCMEASUREMENT_UUID NimBLEUUID((uint16_t)0x2A5B) +#define CSCFEATURE_UUID NimBLEUUID((uint16_t)0x2A5C) #define CYCLINGPOWERSERVICE_UUID NimBLEUUID((uint16_t)0x1818) #define CYCLINGPOWERMEASUREMENT_UUID NimBLEUUID((uint16_t)0x2A63) #define CYCLINGPOWERFEATURE_UUID NimBLEUUID((uint16_t)0x2A65) diff --git a/lib/SS2K/include/sensors/FitnessMachineIndoorBikeData.h b/lib/SS2K/include/sensors/FitnessMachineIndoorBikeData.h index 80baaf2e..6ed9f3ac 100644 --- a/lib/SS2K/include/sensors/FitnessMachineIndoorBikeData.h +++ b/lib/SS2K/include/sensors/FitnessMachineIndoorBikeData.h @@ -39,11 +39,9 @@ class FitnessMachineIndoorBikeData : public SensorData { InstantaneousPower = 6, AveragePower = 7, TotalEnergy = 8, - EnergyPerHour = 9, + HeartRate = 9, EnergyPerMinute = 10, - HeartRate = 11, - MetabolicEquivalent = 12, - ElapsedTime = 13, + ElapsedTime = 11, RemainingTime = 14 }; diff --git a/platformio.ini b/platformio.ini index 24b4ccab..e1e672b9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = release [esp32doit] lib_ldf_mode = chain lib_compat_mode = strict -platform = espressif32 @ 6.5.0 +platform = espressif32 @ 6.0.1 board = esp32doit-devkit-v1 framework = arduino board_build.partitions = min_spiffs.csv @@ -24,12 +24,12 @@ monitor_filters = esp32_exception_decoder build_flags = !python git_tag_macro.py !python build_date_macro.py - -D CONFIG_BT_NIMBLE_MAX_CONNECTIONS=6 + -D CONFIG_BT_NIMBLE_MAX_CONNECTIONS=7 -D CONFIG_MDNS_STRICT_MODE=1 -D CORE_DEBUG_LEVEL=1 -D ARDUINO_SERIAL_EVENT_TASK_STACK_SIZE=3500 lib_deps = - https://github.com/h2zero/NimBLE-Arduino/archive/refs/tags/1.4.1.zip + https://github.com/h2zero/NimBLE-Arduino/archive/refs/tags/1.4.0.zip https://github.com/teemuatlut/TMCStepper/archive/refs/tags/v0.7.3.zip https://github.com/bblanchon/ArduinoJson/archive/refs/tags/v6.20.0.zip https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot/archive/refs/tags/V1.3.0.zip diff --git a/src/BLE_Client.cpp b/src/BLE_Client.cpp index 65a36a40..b20effc9 100644 --- a/src/BLE_Client.cpp +++ b/src/BLE_Client.cpp @@ -283,21 +283,29 @@ bool SpinBLEClient::connectToServer() { pChr = pSvc->getCharacteristic(charUUID); if (pChr) { /** make sure it's not null */ - + if (pChr->canRead()) { + NimBLEAttValue value = pChr->readValue(); + const int kLogBufMaxLength = 250; + char logBuf[kLogBufMaxLength]; + int logBufLength = ss2k_log_hex_to_buffer(value.data(), value.length(), logBuf, 0, kLogBufMaxLength); + logBufLength += snprintf(logBuf + logBufLength, kLogBufMaxLength - logBufLength, " <-initial Value"); + SS2K_LOG(BLE_COMMON_LOG_TAG, "%s", logBuf); + } if (pChr->canNotify()) { - // if(!pChr->registerForNotify(notifyCB)) { if (!pChr->subscribe(true, onNotify)) { /** Disconnect if subscribe failed */ + SS2K_LOG(BLE_CLIENT_LOG_TAG, "Notifications Failed for %s", pClient->getPeerAddress().toString().c_str()); spinBLEClient.myBLEDevices[device_number].reset(); pClient->deleteServices(); NimBLEDevice::getScan()->erase(pClient->getPeerAddress()); NimBLEDevice::deleteClient(pClient); return false; } + SS2K_LOG(BLE_CLIENT_LOG_TAG, "Notifications Subscribed for %s", pClient->getPeerAddress().toString().c_str()); } else if (pChr->canIndicate()) { /** Send false as first argument to subscribe to indications instead of notifications */ - // if(!pChr->registerForNotify(notifyCB, false)) { if (!pChr->subscribe(false, onNotify)) { + SS2K_LOG(BLE_CLIENT_LOG_TAG, "Indications Failed for %s", pClient->getPeerAddress().toString().c_str()); /** Disconnect if subscribe failed */ spinBLEClient.myBLEDevices[device_number].reset(); pClient->deleteServices(); @@ -305,6 +313,7 @@ bool SpinBLEClient::connectToServer() { NimBLEDevice::deleteClient(pClient); return false; } + SS2K_LOG(BLE_CLIENT_LOG_TAG, "Indications Subscribed for %s", pClient->getPeerAddress().toString().c_str()); } this->reconnectTries = MAX_RECONNECT_TRIES; SS2K_LOG(BLE_CLIENT_LOG_TAG, "Successful %s subscription.", pChr->getUUID().toString().c_str()); @@ -556,7 +565,7 @@ void SpinBLEClient::removeDuplicates(NimBLEClient *pClient) { void SpinBLEClient::resetDevices(NimBLEClient *pClient) { for (auto &_BLEd : spinBLEClient.myBLEDevices) { if (pClient->getPeerAddress() == _BLEd.peerAddress) { - SS2K_LOGW(BLE_CLIENT_LOG_TAG, "Reset Client: %s", _BLEd.peerAddress.toString().c_str()); + SS2K_LOGW(BLE_CLIENT_LOG_TAG, "Reset Client: %s", _BLEd.peerAddress.toString().c_str()); _BLEd.reset(); } } @@ -632,13 +641,13 @@ void SpinBLEClient::postConnect() { return; } - // Start Training + // 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); - writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::StartOrResume, 1); SS2K_LOG(BLE_CLIENT_LOG_TAG, "Activated FTMS Training."); } + writeCharacteristic->writeValue(FitnessMachineControlPointProcedure::StartOrResume, 1); SS2K_LOG(BLE_CLIENT_LOG_TAG, "Updating Connection Params for: %s", _BLEd.peerAddress.toString().c_str()); BLEDevice::getServer()->updateConnParams(pClient->getConnId(), 120, 120, 2, 1000); spinBLEClient.handleBattInfo(pClient, true); @@ -780,15 +789,15 @@ void SpinBLEClient::keepAliveBLE_HID(NimBLEClient *pClient) { void SpinBLEClient::checkBLEReconnect() { if ((String(userConfig->getConnectedHeartMonitor()) != "none") && !(spinBLEClient.connectedHRM)) { this->doScan = true; - SS2K_LOG(BLE_CLIENT_LOG_TAG,"No HRM Connected"); + SS2K_LOG(BLE_CLIENT_LOG_TAG, "No HRM Connected"); } if ((String(userConfig->getConnectedPowerMeter()) != "none") && !(spinBLEClient.connectedPM)) { this->doScan = true; - SS2K_LOG(BLE_CLIENT_LOG_TAG,"No PM Connected"); + SS2K_LOG(BLE_CLIENT_LOG_TAG, "No PM Connected"); } if ((String(userConfig->getConnectedRemote()) != "none") && !(spinBLEClient.connectedRemote)) { this->doScan = true; - SS2K_LOG(BLE_CLIENT_LOG_TAG,"No Rem Connected"); + SS2K_LOG(BLE_CLIENT_LOG_TAG, "No Rem Connected"); } } @@ -879,6 +888,7 @@ void SpinBLEAdvertisedDevice::reset() { if (this->isHRM) spinBLEClient.connectedHRM = false; if (this->isPM) spinBLEClient.connectedPM = false; if (this->isCSC) spinBLEClient.connectedCD = false; + spinBLEClient.connectedSpeed = false; advertisedDevice = nullptr; // NimBLEAddress peerAddress; diff --git a/src/BLE_Common.cpp b/src/BLE_Common.cpp index 291f4b52..e42aca63 100644 --- a/src/BLE_Common.cpp +++ b/src/BLE_Common.cpp @@ -101,10 +101,13 @@ void BLECommunications(void *pvParameters) { spinBLEClient.postConnect(); if (connectedClientCount() > 0 && !ss2k->isUpdating) { + // Setup the information + updateWheelAndCrankRev(); // update the BLE information on the server updateIndoorBikeDataChar(); updateCyclingPowerMeasurementChar(); updateHeartRateMeasurementChar(); + updateCyclingSpeedCadenceChar(); // controlPointIndicate(); if (spinDown()) { // Possibly do something in the future. Right now we just fake the spindown. diff --git a/src/BLE_Server.cpp b/src/BLE_Server.cpp index 8770a56e..cc6f5262 100644 --- a/src/BLE_Server.cpp +++ b/src/BLE_Server.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include // BLE Server Settings SpinBLEServer spinBLEServer; @@ -27,6 +29,10 @@ BLECharacteristic *cyclingPowerMeasurementCharacteristic; BLECharacteristic *cyclingPowerFeatureCharacteristic; BLECharacteristic *sensorLocationCharacteristic; +BLEService *pCyclingSpeedCadenceService; +BLECharacteristic *cscMeasurement; +BLECharacteristic *cscFeature; + BLEService *pFitnessMachineService; BLECharacteristic *fitnessMachineFeature; BLECharacteristic *fitnessMachineIndoorBikeData; @@ -57,27 +63,17 @@ std::string FTMSWrite = ""; // 00000001000000000000 - Offset Compensation Indicator (bit 12) // 98765432109876543210 - bit placement helper :) -byte heartRateMeasurement[2] = {0x00, 0x00}; -byte cyclingPowerMeasurement[9] = {0b0000000100011, 0, 200, 0, 0, 0, 0, 0, 0}; -byte cpsLocation[1] = {0b000}; // sensor location 5 == left crank -byte cpFeature[1] = {0b00100000}; // crank information present // 3rd & 2nd - // byte is reported power - -// byte ftmsService[6] = {0x00, 0x00, 0x00, 0b01, 0b0100000, 0x00}; - -struct FitnessMachineFeature ftmsFeature = { - FitnessMachineFeatureFlags::Types::CadenceSupported | FitnessMachineFeatureFlags::Types::HeartRateMeasurementSupported | - FitnessMachineFeatureFlags::Types::PowerMeasurementSupported | FitnessMachineFeatureFlags::Types::InclinationSupported | - FitnessMachineFeatureFlags::Types::ResistanceLevelSupported, - FitnessMachineTargetFlags::PowerTargetSettingSupported | FitnessMachineTargetFlags::Types::InclinationTargetSettingSupported | - FitnessMachineTargetFlags::Types::ResistanceTargetSettingSupported | FitnessMachineTargetFlags::Types::IndoorBikeSimulationParametersSupported | - FitnessMachineTargetFlags::Types::SpinDownControlSupported | FitnessMachineTargetFlags::Types::TargetedCadenceConfigurationSupported}; - -uint8_t ftmsIndoorBikeData[11] = {0x64, 0x02, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; // 1001100100 ISpeed, ICAD, - // Resistance, IPower, HeartRate -uint8_t ftmsResistanceLevelRange[6] = {0x01, 0x00, 0x64, 0x00, 0x01, 0x00}; // 1:100 increment 1 -uint8_t ftmsPowerRange[6] = {0x01, 0x00, 0xA0, 0x0F, 0x01, 0x00}; // 1:4000 watts increment 1 -uint8_t ftmsInclinationRange[6] = {0x38, 0xff, 0xc8, 0x00, 0x01, 0x00}; // -20.0:20.0 increment .1 +byte heartRateMeasurement[2] = {0x00, 0x00}; +byte cpsLocation[1] = {0b0101}; // sensor location 5 == left crank +byte cpFeature[1] = {0b001100}; // crank information & wheel revolution data present + +// Fitness Machine +uint8_t ftmsIndoorBikeData[11] = {0}; + +// Resistance, IPower, HeartRate +uint8_t ftmsResistanceLevelRange[6] = {0x01, 0x00, 0x64, 0x00, 0x01, 0x00}; // 1:100 increment 1 +uint8_t ftmsPowerRange[6] = {0x01, 0x00, 0xA0, 0x0F, 0x01, 0x00}; // 1:4000 watts increment 1 +uint8_t ftmsInclinationRange[6] = {0x38, 0xff, 0xc8, 0x00, 0x01, 0x00}; // -20.0:20.0 increment .1 uint8_t ftmsTrainingStatus[2] = {0x08, 0x00}; uint8_t ss2kCustomCharacteristicValue[3] = {0x00, 0x00, 0x00}; @@ -101,16 +97,34 @@ void startBLEServer() { SS2K_LOG(BLE_SERVER_LOG_TAG, "Starting BLE Server"); spinBLEServer.pServer = BLEDevice::createServer(); + // Fitness Machine Feature Flags Setup + struct FitnessMachineFeature ftmsFeature = {FitnessMachineFeatureFlags::Types::CadenceSupported | FitnessMachineFeatureFlags::Types::HeartRateMeasurementSupported | + FitnessMachineFeatureFlags::Types::PowerMeasurementSupported | FitnessMachineFeatureFlags::Types::InclinationSupported | + FitnessMachineFeatureFlags::Types::ResistanceLevelSupported, + FitnessMachineTargetFlags::PowerTargetSettingSupported | FitnessMachineTargetFlags::Types::InclinationTargetSettingSupported | + FitnessMachineTargetFlags::Types::ResistanceTargetSettingSupported | + FitnessMachineTargetFlags::Types::IndoorBikeSimulationParametersSupported | + FitnessMachineTargetFlags::Types::SpinDownControlSupported}; + // Fitness Machine Indoor Bike Data Flags Setup + FitnessMachineIndoorBikeDataFlags::Types ftmsIBDFlags = FitnessMachineIndoorBikeDataFlags::InstantaneousCadencePresent | + FitnessMachineIndoorBikeDataFlags::ResistanceLevelPresent | FitnessMachineIndoorBikeDataFlags::InstantaneousPowerPresent | + FitnessMachineIndoorBikeDataFlags::HeartRatePresent; + // HEART RATE MONITOR SERVICE SETUP pHeartService = spinBLEServer.pServer->createService(HEARTSERVICE_UUID); heartRateMeasurementCharacteristic = pHeartService->createCharacteristic(HEARTCHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); // Power Meter MONITOR SERVICE SETUP pPowerMonitor = spinBLEServer.pServer->createService(CYCLINGPOWERSERVICE_UUID); - cyclingPowerMeasurementCharacteristic = pPowerMonitor->createCharacteristic(CYCLINGPOWERMEASUREMENT_UUID, NIMBLE_PROPERTY::NOTIFY); + cyclingPowerMeasurementCharacteristic = pPowerMonitor->createCharacteristic(CYCLINGPOWERMEASUREMENT_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); cyclingPowerFeatureCharacteristic = pPowerMonitor->createCharacteristic(CYCLINGPOWERFEATURE_UUID, NIMBLE_PROPERTY::READ); sensorLocationCharacteristic = pPowerMonitor->createCharacteristic(SENSORLOCATION_UUID, NIMBLE_PROPERTY::READ); + // Cycling Speed and Cadence service setup + pCyclingSpeedCadenceService = spinBLEServer.pServer->createService(CSCSERVICE_UUID); + cscMeasurement = pCyclingSpeedCadenceService->createCharacteristic(CSCMEASUREMENT_UUID, NIMBLE_PROPERTY::NOTIFY); + cscFeature = pCyclingSpeedCadenceService->createCharacteristic(CSCFEATURE_UUID, NIMBLE_PROPERTY::READ); + // Fitness Machine service setup pFitnessMachineService = spinBLEServer.pServer->createService(FITNESSMACHINESERVICE_UUID); fitnessMachineFeature = pFitnessMachineService->createCharacteristic(FITNESSMACHINEFEATURE_UUID, NIMBLE_PROPERTY::READ); @@ -128,28 +142,31 @@ void startBLEServer() { spinBLEServer.pServer->setCallbacks(new MyServerCallbacks()); - // Creating Characteristics - heartRateMeasurementCharacteristic->setValue(heartRateMeasurement, 2); - - cyclingPowerMeasurementCharacteristic->setValue(cyclingPowerMeasurement, 9); - cyclingPowerFeatureCharacteristic->setValue(cpFeature, 1); - sensorLocationCharacteristic->setValue(cpsLocation, 1); - + // Set Initial Values and Flags + heartRateMeasurementCharacteristic->setValue(heartRateMeasurement, sizeof(heartRateMeasurement)); + byte cyclingPowerMeasurement[8] = {0b1000000, 0, 0, 0, 0, 0, 0}; // Crank Revolution data present + cyclingPowerMeasurementCharacteristic->setValue(cyclingPowerMeasurement, sizeof(cyclingPowerMeasurement)); + cyclingPowerFeatureCharacteristic->setValue(cpFeature, sizeof(cpFeature)); + byte cscFeatureFlags[1] = {0b11}; + cscFeature->setValue(cscFeatureFlags, sizeof(cscFeatureFlags)); + sensorLocationCharacteristic->setValue(cpsLocation, sizeof(cpsLocation)); fitnessMachineFeature->setValue(ftmsFeature.bytes, sizeof(ftmsFeature)); - - fitnessMachineIndoorBikeData->setValue(ftmsIndoorBikeData, 14); - fitnessMachineResistanceLevelRange->setValue(ftmsResistanceLevelRange, 6); - fitnessMachinePowerRange->setValue(ftmsPowerRange, 6); - fitnessMachineInclinationRange->setValue(ftmsInclinationRange, 6); - - smartSpin2kCharacteristic->setValue(ss2kCustomCharacteristicValue, 3); - + ftmsIndoorBikeData[0] = static_cast(ftmsIBDFlags & 0xFF); // LSB, mask with 0xFF to get the lower 8 bits + ftmsIndoorBikeData[1] = static_cast((ftmsIBDFlags >> 8) & 0xFF); // MSB, shift right by 8 bits and mask with 0xFF + fitnessMachineIndoorBikeData->setValue(ftmsIndoorBikeData, sizeof(ftmsIndoorBikeData)); + fitnessMachineResistanceLevelRange->setValue(ftmsResistanceLevelRange, sizeof(ftmsResistanceLevelRange)); + fitnessMachinePowerRange->setValue(ftmsPowerRange, sizeof(ftmsPowerRange)); + fitnessMachineInclinationRange->setValue(ftmsInclinationRange, sizeof(ftmsInclinationRange)); + smartSpin2kCharacteristic->setValue(ss2kCustomCharacteristicValue, sizeof(ss2kCustomCharacteristicValue)); + + cscMeasurement->setCallbacks(&chrCallbacks); cyclingPowerMeasurementCharacteristic->setCallbacks(&chrCallbacks); heartRateMeasurementCharacteristic->setCallbacks(&chrCallbacks); fitnessMachineIndoorBikeData->setCallbacks(&chrCallbacks); fitnessMachineControlPoint->setCallbacks(&chrCallbacks); smartSpin2kCharacteristic->setCallbacks(new ss2kCustomCharacteristicCallbacks()); + pCyclingSpeedCadenceService->start(); pHeartService->start(); pPowerMonitor->start(); pFitnessMachineService->start(); @@ -158,8 +175,10 @@ void startBLEServer() { // const std::string fitnessData = {0b00000001, 0b00100000, 0b00000000}; BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); // pAdvertising->setServiceData(FITNESSMACHINESERVICE_UUID, fitnessData); + pAdvertising->addServiceUUID(FITNESSMACHINESERVICE_UUID); pAdvertising->addServiceUUID(CYCLINGPOWERSERVICE_UUID); + pAdvertising->addServiceUUID(CSCSERVICE_UUID); pAdvertising->addServiceUUID(HEARTSERVICE_UUID); pAdvertising->addServiceUUID(SMARTSPIN2K_SERVICE_UUID); pAdvertising->setMaxInterval(250); @@ -206,25 +225,82 @@ bool spinDown() { return true; } +// Returns Current Speed in km/h +double xxxcurrentSpeed() { + // Constants for the formula: C = 0.5 * AirDensity * DragCoefficient * FrontalArea + RollingResistance + const double combinedConstant = 0.5 * 1.225 * 0.63 * 0.5 + 0.004; + // Calculate the speed in m/s using the cubic root formula: (P / C)^(1/3) + double speedInMetersPerSecond = std::cbrt(rtConfig->watts.getValue() / combinedConstant); + // Convert speed from m/s to km/h + return speedInMetersPerSecond * 3.6; + // Scale the speed to fit the resolution of 0.01 km/h + // speedFtmsUnit = speedKmH * 100; +} + +double calculateSpeed() { + // Constants for the formula: adjusted for calibration + const double dragCoefficient = 0.95; + const double frontalArea = 0.5; // m^2 + const double airDensity = 1.225; // kg/m^3 + const double rollingResistance = 0.004; + const double combinedConstant = 0.5 * airDensity * dragCoefficient * frontalArea + rollingResistance; + + double power = rtConfig->watts.getValue(); // Power in watts + double speedInMetersPerSecond = std::cbrt(power / combinedConstant); // Speed in m/s + + // Convert speed from m/s to km/h + double speedKmH = speedInMetersPerSecond * 3.6; + + // Apply a calibration factor based on empirical data to adjust the speed into a realistic range + double calibrationFactor = 1; // This is an example value; adjust based on calibration + speedKmH *= calibrationFactor; + + return speedKmH; +} + +void updateWheelAndCrankRev() { + float wheelSize = 2.127; // 700cX28 circumference, typical in meters + float wheelSpeedMps = 0.0; + if (rtConfig->getSimulatedSpeed() > 5) { + wheelSpeedMps = rtConfig->getSimulatedSpeed() / 3.6; + } else { + wheelSpeedMps = calculateSpeed() / 3.6; // covert km/h to m/s + } + + // Calculate wheel revolutions per minute + float wheelRpm = (wheelSpeedMps / wheelSize) * 60; + double wheelRevPeriod = (60 * 1024) / wheelRpm; + if (wheelRpm > 0) { + spinBLEClient.cscCumulativeWheelRev++; // Increment cumulative wheel revolutions + spinBLEClient.cscLastWheelEvtTime += wheelRevPeriod; // Convert RPM to time, ensuring no division by zero + } + + float cadence = rtConfig->cad.getValue(); + if (cadence > 0) { + float crankRevPeriod = (60 * 1024) / cadence; + spinBLEClient.cscCumulativeCrankRev++; + spinBLEClient.cscLastCrankEvtTime += crankRevPeriod; + } +} + void updateIndoorBikeDataChar() { if (!spinBLEServer.clientSubscribed.IndoorBikeData) { return; } - float cadRaw = rtConfig->cad.getValue(); - int cad = static_cast(cadRaw * 2); - int watts = rtConfig->watts.getValue(); - int hr = rtConfig->hr.getValue(); - int res = rtConfig->resistance.getValue(); - int speed = 0; - float speedRaw = rtConfig->getSimulatedSpeed(); - - if (speedRaw <= 0) { - speed = (((cad * watts) / 100) * 1.5); + float cadRaw = rtConfig->cad.getValue(); + int cad = static_cast(cadRaw * 2); + int watts = rtConfig->watts.getValue(); + int hr = rtConfig->hr.getValue(); + int res = rtConfig->resistance.getValue(); + int speedFtmsUnit = 0; + if (rtConfig->getSimulatedSpeed() > 5) { + speedFtmsUnit = rtConfig->getSimulatedSpeed() * 100; } else { - speed = static_cast(speedRaw); + speedFtmsUnit = calculateSpeed() * 100; } - ftmsIndoorBikeData[2] = (uint8_t)(speed & 0xff); - ftmsIndoorBikeData[3] = (uint8_t)(speed >> 8); + + ftmsIndoorBikeData[2] = (uint8_t)(speedFtmsUnit & 0xff); + ftmsIndoorBikeData[3] = (uint8_t)(speedFtmsUnit >> 8); ftmsIndoorBikeData[4] = (uint8_t)(cad & 0xff); ftmsIndoorBikeData[5] = (uint8_t)(cad >> 8); @@ -240,18 +316,12 @@ void updateIndoorBikeDataChar() { fitnessMachineIndoorBikeData->setValue(ftmsIndoorBikeData, 11); fitnessMachineIndoorBikeData->notify(); - // ftmsResistanceLevelRange[0] = (uint8_t)rtConfig->getMinResistance() & 0xff; - // ftmsResistanceLevelRange[1] = (uint8_t)rtConfig->getMinResistance() >> 8; - // ftmsResistanceLevelRange[2] = (uint8_t)rtConfig->getMaxResistance() & 0xff; - // ftmsResistanceLevelRange[3] = (uint8_t)rtConfig->getMaxResistance() >> 8; - // ftmsResistanceLevelRange.setValue(ftmsResistanceLevelRange, 6); - const int kLogBufCapacity = 200; // Data(30), Sep(data/2), Arrow(3), CharId(37), Sep(3), CharId(37), Sep(3), Name(10), Prefix(2), HR(7), SEP(1), CD(10), SEP(1), PW(8), // SEP(1), SD(7), Suffix(2), Nul(1), rounded up char logBuf[kLogBufCapacity]; const size_t ftmsIndoorBikeDataLength = sizeof(ftmsIndoorBikeData) / sizeof(ftmsIndoorBikeData[0]); logCharacteristic(logBuf, kLogBufCapacity, ftmsIndoorBikeData, ftmsIndoorBikeDataLength, FITNESSMACHINESERVICE_UUID, fitnessMachineIndoorBikeData->getUUID(), - "FTMS(IBD)[ HR(%d) CD(%.2f) PW(%d) SD(%.2f) ]", hr % 1000, fmodf(cadRaw, 1000.0), watts % 10000, fmodf(speed, 1000.0)); + "FTMS(IBD)[ HR(%d) CD(%.2f) PW(%d) SD(%.2f) ]", hr % 1000, fmodf(cadRaw, 1000.0), watts % 10000, fmodf(speedFtmsUnit / 100, 1000.0)); } void updateCyclingPowerMeasurementChar() { @@ -259,39 +329,69 @@ void updateCyclingPowerMeasurementChar() { return; } int power = rtConfig->watts.getValue(); - int remainder, quotient; - quotient = power / 256; - remainder = power % 256; - cyclingPowerMeasurement[2] = remainder; - cyclingPowerMeasurement[3] = quotient; - cyclingPowerMeasurementCharacteristic->setValue(cyclingPowerMeasurement, 9); float cadence = rtConfig->cad.getValue(); - if (cadence > 0) { - float crankRevPeriod = (60 * 1024) / cadence; - spinBLEClient.cscCumulativeCrankRev++; - spinBLEClient.cscLastCrankEvtTime += crankRevPeriod; - int remainder, quotient; - quotient = spinBLEClient.cscCumulativeCrankRev / 256; - remainder = spinBLEClient.cscCumulativeCrankRev % 256; - cyclingPowerMeasurement[5] = remainder; - cyclingPowerMeasurement[6] = quotient; - quotient = spinBLEClient.cscLastCrankEvtTime / 256; - remainder = spinBLEClient.cscLastCrankEvtTime % 256; - cyclingPowerMeasurement[7] = remainder; - cyclingPowerMeasurement[8] = quotient; - } + CyclingPowerMeasurement cpm; + + // Example setting of flags and values + cpm.flags = {0}; // Clear all flags initially + cpm.flags.crankRevolutionDataPresent = 1; // Crank Revolution Data Present + cpm.flags.wheelRevolutionDataPresent = 1; + cpm.instantaneousPower = rtConfig->watts.getValue(); + cpm.cumulativeCrankRevolutions = spinBLEClient.cscCumulativeCrankRev; + cpm.lastCrankEventTime = spinBLEClient.cscLastCrankEvtTime; + cpm.cumulativeWheelRevolutions = spinBLEClient.cscCumulativeWheelRev; + cpm.lastWheelEventTime = spinBLEClient.cscLastWheelEvtTime; + + auto byteArray = cpm.toByteArray(); + + cyclingPowerMeasurementCharacteristic->setValue(&byteArray[0], byteArray.size()); cyclingPowerMeasurementCharacteristic->notify(); const int kLogBufCapacity = 150; // Data(18), Sep(data/2), Arrow(3), CharId(37), Sep(3), CharId(37), Sep(3),Name(8), Prefix(2), CD(10), SEP(1), PW(8), Suffix(2), Nul(1), rounded up char logBuf[kLogBufCapacity]; - const size_t cyclingPowerMeasurementLength = sizeof(cyclingPowerMeasurement) / sizeof(cyclingPowerMeasurement[0]); - logCharacteristic(logBuf, kLogBufCapacity, cyclingPowerMeasurement, cyclingPowerMeasurementLength, FITNESSMACHINESERVICE_UUID, fitnessMachineIndoorBikeData->getUUID(), + const size_t byteArrayLength = byteArray.size(); + + logCharacteristic(logBuf, kLogBufCapacity, &byteArray[0], byteArrayLength, CYCLINGPOWERSERVICE_UUID, cyclingPowerMeasurementCharacteristic->getUUID(), "CPS(CPM)[ CD(%.2f) PW(%d) ]", cadence > 0 ? fmodf(cadence, 1000.0) : 0, power % 10000); } +void updateCyclingSpeedCadenceChar() { + if (!spinBLEServer.clientSubscribed.CyclingSpeedCadence) { + return; + } + + CscMeasurement csc; + + // Clear all flags initially + *(reinterpret_cast(&(csc.flags))) = 0; + + // Set flags based on data presence + csc.flags.wheelRevolutionDataPresent = 1; // Wheel Revolution Data Present + csc.flags.crankRevolutionDataPresent = 1; // Crank Revolution Data Present + + // Set data fields + csc.cumulativeWheelRevolutions = spinBLEClient.cscCumulativeWheelRev; + csc.lastWheelEventTime = spinBLEClient.cscLastWheelEvtTime; + csc.cumulativeCrankRevolutions = spinBLEClient.cscCumulativeCrankRev; + csc.lastCrankEventTime = spinBLEClient.cscLastCrankEvtTime; + + auto byteArray = csc.toByteArray(); + + cscMeasurement->setValue(&byteArray[0], byteArray.size()); + cscMeasurement->notify(); + + const int kLogBufCapacity = 150; + char logBuf[kLogBufCapacity]; + const size_t byteArrayLength = byteArray.size(); + + logCharacteristic(logBuf, kLogBufCapacity, &byteArray[0], byteArrayLength, CSCSERVICE_UUID, cscMeasurement->getUUID(), + "CSC(CSM)[ WheelRev(%lu) WheelTime(%u) CrankRev(%u) CrankTime(%u) ]", spinBLEClient.cscCumulativeWheelRev, spinBLEClient.cscLastWheelEvtTime, + spinBLEClient.cscCumulativeCrankRev, spinBLEClient.cscLastCrankEvtTime); +} + void updateHeartRateMeasurementChar() { if (!spinBLEServer.clientSubscribed.Heartrate) { return; @@ -323,9 +423,11 @@ void MyServerCallbacks::onConnect(BLEServer *pServer, ble_gap_conn_desc *desc) { void MyServerCallbacks::onDisconnect(BLEServer *pServer) { SS2K_LOG(BLE_SERVER_LOG_TAG, "Bluetooth Remote Client Disconnected. Remaining Clients: %d", pServer->getConnectedCount()); BLEDevice::startAdvertising(); - //client disconnected while trying to write fw - reboot to clear the faulty upload. - if (ss2k->isUpdating) {SS2K_LOG(BLE_SERVER_LOG_TAG, "Rebooting because of update interruption.", pServer->getConnectedCount()); - ss2k->rebootFlag = true;} + // client disconnected while trying to write fw - reboot to clear the faulty upload. + if (ss2k->isUpdating) { + SS2K_LOG(BLE_SERVER_LOG_TAG, "Rebooting because of update interruption.", pServer->getConnectedCount()); + ss2k->rebootFlag = true; + } } bool MyServerCallbacks::onConnParamsUpdateRequest(NimBLEClient *pClient, const ble_gap_upd_params *params) { @@ -366,6 +468,8 @@ void SpinBLEServer::setClientSubscribed(NimBLEUUID pUUID, bool subscribe) { spinBLEServer.clientSubscribed.CyclingPowerMeasurement = subscribe; } else if (pUUID == FITNESSMACHINEINDOORBIKEDATA_UUID) { spinBLEServer.clientSubscribed.IndoorBikeData = subscribe; + } else if (pUUID == CSCMEASUREMENT_UUID) { + spinBLEServer.clientSubscribed.CyclingSpeedCadence = subscribe; } } diff --git a/src/BLE_Setup.cpp b/src/BLE_Setup.cpp index b4609145..11589ab8 100644 --- a/src/BLE_Setup.cpp +++ b/src/BLE_Setup.cpp @@ -14,7 +14,7 @@ void setupBLE() { // Common BLE setup for both client and server SS2K_LOG(BLE_SETUP_LOG_TAG, "Starting Arduino BLE Client application..."); BLEDevice::init(userConfig->getDeviceName()); - BLEDevice::setMTU(515); + BLEDevice::setMTU(515); //-- enabling this is very important for BLE firmware updates. FTMSWrite = ""; spinBLEClient.start(); startBLEServer(); diff --git a/src/Custom_Characteristic.cpp b/src/Custom_Characteristic.cpp index d13466a1..9df86321 100644 --- a/src/Custom_Characteristic.cpp +++ b/src/Custom_Characteristic.cpp @@ -45,6 +45,10 @@ void ss2kCustomCharacteristicCallbacks::onWrite(BLECharacteristic *pCharacterist ss2kCustomCharacteristic::process(rxValue); } +void ss2kCustomCharacteristicCallbacks::onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue){ + NimBLEDevice::setMTU(515); +} + void ss2kCustomCharacteristic::notify(char _item) { std::string returnValue = {cc_read, _item}; process(returnValue); diff --git a/src/HTTP_Server_Basic.cpp b/src/HTTP_Server_Basic.cpp index a54140d6..35bf8ff2 100644 --- a/src/HTTP_Server_Basic.cpp +++ b/src/HTTP_Server_Basic.cpp @@ -52,26 +52,42 @@ void _staSetup() { WiFi.setAutoReconnect(true); } +void _APSetup() { + // WiFi.eraseAP(); //Needed if we switch back to espressif32 @6.5.0 + WiFi.mode(WIFI_AP); + WiFi.setHostname("reset"); // Fixes a bug when switching Arduino Core Versions + WiFi.softAPsetHostname("reset"); + WiFi.setHostname(userConfig->getDeviceName()); + WiFi.softAPsetHostname(userConfig->getDeviceName()); + WiFi.enableAP(true); + vTaskDelay(500); // Micro controller requires some time to reset the mode +} + // ********************************WIFI Setup************************* void startWifi() { int i = 0; // Trying Station mode first: - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connecting to: %s", userConfig->getSsid()); - if (WiFi.SSID() != String(userConfig->getSsid())) { + if (strcmp(userConfig->getSsid(), DEVICE_NAME) != 0) { + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Connecting to: %s", userConfig->getSsid()); _staSetup(); - } - - while (WiFi.status() != WL_CONNECTED) { - vTaskDelay(1000 / portTICK_RATE_MS); - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for connection to be established..."); - i++; - if (i > WIFI_CONNECT_TIMEOUT || (String(userConfig->getSsid()) == DEVICE_NAME)) { - i = 0; - SS2K_LOG(HTTP_SERVER_LOG_TAG, "Couldn't Connect. Switching to AP mode"); - break; + while (WiFi.status() != WL_CONNECTED) { + vTaskDelay(1000 / portTICK_RATE_MS); + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Waiting for connection to be established..."); + i++; + if (i > WIFI_CONNECT_TIMEOUT) { + i = 0; + SS2K_LOG(HTTP_SERVER_LOG_TAG, "Couldn't Connect. Switching to AP mode"); + WiFi.disconnect(true, true); + WiFi.setAutoReconnect(false); + WiFi.mode(WIFI_MODE_NULL); + vTaskDelay(1000 / portTICK_RATE_MS); + break; + } } } + + // Did we connect in STA mode? if (WiFi.status() == WL_CONNECTED) { myIP = WiFi.localIP(); httpServer.internetConnection = true; @@ -79,24 +95,10 @@ void startWifi() { // Couldn't connect to existing network, Create SoftAP if (WiFi.status() != WL_CONNECTED) { - // This below is to compensate for a bug in platform = espressif32 @ 6.5.0 . Hopefully it's fixed soon. - // The Symptoms are that we cannot connect in AP mode unless .eraseAp is called. Then Station needs to get initialized and dumped again before it will connect. Dumbest thing - // ever. - WiFi.eraseAP(); - _staSetup(); - WiFi.disconnect(true, true); - WiFi.mode(WIFI_MODE_NULL); - vTaskDelay(1000 / portTICK_RATE_MS); - // *********************** End of platform = espressif32 @ 6.5.0 Bug Workaround ************************* - WiFi.mode(WIFI_AP); - WiFi.softAPsetHostname(userConfig->getDeviceName()); - WiFi.setAutoReconnect(false); - WiFi.enableAP(true); - vTaskDelay(50); // Micro controller requires some time to reset the mode + _APSetup(); if (strcmp(userConfig->getSsid(), DEVICE_NAME) == 0) { - // If default SSID is still in use, let the user - // select a new password. - // Else Fall Back to the default password (probably "password") + // If default SSID is still in use, let the user select a new password. + // Else fall back to the default password. WiFi.softAP(userConfig->getDeviceName(), userConfig->getPassword()); } else { WiFi.softAP(userConfig->getDeviceName(), DEFAULT_PASSWORD); @@ -155,7 +157,7 @@ void HTTP_Server::start() { server.on("/shift.html", handleLittleFSFile); server.on("/settings.html", handleLittleFSFile); server.on("/status.html", handleLittleFSFile); - server.on("/bluetoothscanner.html", handleLittleFSFile); + server.on("/bluetoothscanner.html", handleBTScanner); server.on("/streamfit.html", handleLittleFSFile); server.on("/hrtowatts.html", handleLittleFSFile); server.on("/favicon.ico", handleLittleFSFile); @@ -176,7 +178,7 @@ void HTTP_Server::start() { server.on("/load_defaults.html", []() { SS2K_LOG(HTTP_SERVER_LOG_TAG, "Setting Defaults from Web Request"); - ss2k->resetDefaultsFlag = true; + ss2k->resetDefaultsFlag = true; String response = "

Defaults have been " "loaded.



Please reconnect to the device on WiFi " @@ -437,6 +439,11 @@ void HTTP_Server::webClientUpdate(void *pvParameters) { } } +void HTTP_Server::handleBTScanner(){ + spinBLEClient.doScan = true; + handleLittleFSFile(); +} + void HTTP_Server::handleIndexFile() { String filename = "/index.html"; if (LittleFS.exists(filename)) { diff --git a/src/Main.cpp b/src/Main.cpp index 55500ea4..74b10353 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -17,6 +17,7 @@ #include "WebsocketAppender.h" #include "Custom_Characteristic.h" #include +#include "settings.h" // Stepper Motor Serial HardwareSerial stepperSerial(2); diff --git a/src/SensorCollector.cpp b/src/SensorCollector.cpp index da103905..426c4ac9 100644 --- a/src/SensorCollector.cpp +++ b/src/SensorCollector.cpp @@ -54,6 +54,7 @@ void collectAndSet(NimBLEUUID charUUID, NimBLEUUID serviceUUID, NimBLEAddress ad if (sensorData->hasSpeed()) { float speed = sensorData->getSpeed(); rtConfig->setSimulatedSpeed(speed); + spinBLEClient.connectedSpeed = true; logBufLength += snprintf(logBuf + logBufLength, kLogBufMaxLength - logBufLength, " SD(%.2f)", fmodf(speed, 1000.0)); } if (sensorData->hasResistance()) {