From 39828b0304f004e4ea0adeec624ac8cbb51562ca Mon Sep 17 00:00:00 2001 From: eMad Date: Thu, 25 Jun 2026 08:35:04 -0400 Subject: [PATCH 1/3] initial changes Custom BLE handling for Spinning CHrono Power --- .gitignore | 1 + .vscode/settings.json | 4 +++ dependencies.lock | 2 +- include/BLE_Common.h | 1 + lib/SS2K/include/Constants.h | 5 +++ lib/SS2K/include/sensors/ChronoData.h | 32 +++++++++++++++++ lib/SS2K/src/sensors/ChronoData.cpp | 41 ++++++++++++++++++++++ lib/SS2K/src/sensors/SensorDataFactory.cpp | 3 ++ 8 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 lib/SS2K/include/sensors/ChronoData.h create mode 100644 lib/SS2K/src/sensors/ChronoData.cpp diff --git a/.gitignore b/.gitignore index 34376472..aee06ddc 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ C/Users/ dependencies.lock .vscode/settings.json *.map +graphify-out/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 475682b2..b42585b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -122,4 +122,8 @@ "idf.pythonInstallPath": "C:\\Users\\anthony\\.espressif\\tools\\idf-python\\3.11.2\\python.exe", "idf.flashType": "UART", "liveServer.settings.port": 5501, + "clangd.arguments": [ + "--compile-commands-dir=d:/git/SmartSpin2k/.cache/clangd", + "--query-driver=C:/Users/emadm/.platformio/packages/toolchain-*/bin/*,C:/Users/emadm/.platformio/packages/tool-*/bin/*" + ], } \ No newline at end of file diff --git a/dependencies.lock b/dependencies.lock index f84838b8..6f1aafb5 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -96,6 +96,6 @@ direct_dependencies: - espressif/network_provisioning - idf - joltwallet/littlefs -manifest_hash: df9baa925ebc2a028ee0e349be53f2169bb7ac38b2cb9c63b3ebf69123c43ef8 +manifest_hash: e7ce7a886dfdb3cc5cd59acbdb4ff333c0a91c6e4de55b9fd6323d866c7cc691 target: esp32 version: 2.0.0 diff --git a/include/BLE_Common.h b/include/BLE_Common.h index 0a881942..7edfed89 100644 --- a/include/BLE_Common.h +++ b/include/BLE_Common.h @@ -44,6 +44,7 @@ const std::vector SUPPORTED_SERVICES = {{CYCLINGPOWERSERVICE_UUI {HEARTSERVICE_UUID, HEARTCHARACTERISTIC_UUID, "Heart Rate Service"}, {ECHELON_DEVICE_UUID, ECHELON_SERVICE_UUID, "Echelon Device"}, // Two lines for Echelon {ECHELON_SERVICE_UUID, ECHELON_DATA_UUID, "Echelon Service"}, // Because one is for search, the other for data + {CHRONO_SERVICE_UUID, CHRONO_DATA_UUID, "Spinner Chrono"}, {FITNESSMACHINESERVICE_UUID, FITNESSMACHINEINDOORBIKEDATA_UUID, "Fitness Machine Service"}, {HID_SERVICE_UUID, HID_REPORT_DATA_UUID, "HID Service"}, {FLYWHEEL_UART_SERVICE_UUID, FLYWHEEL_UART_TX_UUID, "Flywheel UART Service"}}; diff --git a/lib/SS2K/include/Constants.h b/lib/SS2K/include/Constants.h index 7a308ff9..314b7c43 100644 --- a/lib/SS2K/include/Constants.h +++ b/lib/SS2K/include/Constants.h @@ -76,6 +76,11 @@ #define ECHELON_WRITE_UUID NimBLEUUID("0bf669f2-45f2-11e7-9598-0800200c9a66") #define ECHELON_DATA_UUID NimBLEUUID("0bf669f4-45f2-11e7-9598-0800200c9a66") +// Spinning / Precor Spinner Chrono Power (proprietary, no standard CPS/CSC) +#define CHRONO_SERVICE_UUID NimBLEUUID("a026ee07-0a7d-4ab3-97fa-f1500f9feb8b") +#define CHRONO_DATA_UUID NimBLEUUID("a026e01d-0a7d-4ab3-97fa-f1500f9feb8b") +#define CHRONO_BLE_NAME "CHRONO" // device advertises "CHRONO " + // Dummy UUID for Peloton Serial Data Interface #define PELOTON_DATA_UUID NimBLEUUID("00000000-0000-0000-0000-000000000321") #define PELOTON_ADDRESS NimBLEAddress("00:00:00:00:00:00", 0) diff --git a/lib/SS2K/include/sensors/ChronoData.h b/lib/SS2K/include/sensors/ChronoData.h new file mode 100644 index 00000000..933ba53e --- /dev/null +++ b/lib/SS2K/include/sensors/ChronoData.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include "SensorData.h" + +class ChronoData : public SensorData { + public: + ChronoData() : SensorData("CHRO") {} + + bool hasHeartRate(); + bool hasCadence(); + bool hasPower(); + bool hasSpeed(); + bool hasResistance(); + int getHeartRate(); + float getCadence(); + int getPower(); + float getSpeed(); + int getResistance(); + void decode(uint8_t *data, size_t length); + + private: + bool hasData = false; + float cadence = nanf(""); + int power = INT_MIN; +}; diff --git a/lib/SS2K/src/sensors/ChronoData.cpp b/lib/SS2K/src/sensors/ChronoData.cpp new file mode 100644 index 00000000..2914fd2c --- /dev/null +++ b/lib/SS2K/src/sensors/ChronoData.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "sensors/ChronoData.h" + +bool ChronoData::hasHeartRate() { return false; } + +bool ChronoData::hasCadence() { return this->hasData; } + +bool ChronoData::hasPower() { return this->hasData; } + +bool ChronoData::hasSpeed() { return false; } + +bool ChronoData::hasResistance() { return false; } + +int ChronoData::getHeartRate() { return INT_MIN; } + +float ChronoData::getCadence() { return this->cadence; } + +int ChronoData::getPower() { return this->power; } + +float ChronoData::getSpeed() { return nanf(""); } + +int ChronoData::getResistance() { return INT_MIN; } + +void ChronoData::decode(uint8_t *data, size_t length) { + // Validate the fixed 19-byte telemetry packet with its magic header 0x53 0x59 0x16. + if (length < 19 || data[0] != 0x53 || data[1] != 0x59 || data[2] != 0x16) { + hasData = false; + cadence = nanf(""); + power = INT_MIN; + return; + } + cadence = (data[8] | (data[9] << 8)) / 10.0f; // bytes 8-9, uint16 little-endian, ÷10 RPM + power = data[17] | (data[18] << 8); // bytes 17-18, uint16 little-endian, watts + hasData = true; +} diff --git a/lib/SS2K/src/sensors/SensorDataFactory.cpp b/lib/SS2K/src/sensors/SensorDataFactory.cpp index 36b47fe9..947d1b54 100644 --- a/lib/SS2K/src/sensors/SensorDataFactory.cpp +++ b/lib/SS2K/src/sensors/SensorDataFactory.cpp @@ -15,6 +15,7 @@ #include "sensors/EchelonData.h" #include "sensors/PelotonData.h" #include "sensors/CscSensorData.h" +#include "sensors/ChronoData.h" std::shared_ptr SensorDataFactory::getSensorData(const NimBLEUUID characteristicUUID, std::string& uniqueName, uint8_t *data, size_t length) { for (auto &it : SensorDataFactory::knownDevices) { @@ -34,6 +35,8 @@ std::shared_ptr SensorDataFactory::getSensorData(const NimBLEUUID ch sensorData = std::shared_ptr(new FlywheelData()); } else if (characteristicUUID == ECHELON_DATA_UUID) { sensorData = std::shared_ptr(new EchelonData()); + } else if (characteristicUUID == CHRONO_DATA_UUID) { + sensorData = std::shared_ptr(new ChronoData()); } else if (characteristicUUID == PELOTON_DATA_UUID) { sensorData = std::shared_ptr(new PelotonData()); } else if (characteristicUUID == CSCMEASUREMENT_UUID) { From 29b22963018d61595cf14e0e3926de9381d2594a Mon Sep 17 00:00:00 2001 From: eMad Date: Thu, 25 Jun 2026 09:03:48 -0400 Subject: [PATCH 2/3] feat: add ChronoData sensor support and implement BLE client task management --- lib/SS2K/src/sensors/ChronoData.cpp | 5 +++-- src/BLE_Client.cpp | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/SS2K/src/sensors/ChronoData.cpp b/lib/SS2K/src/sensors/ChronoData.cpp index 2914fd2c..0ccbf10f 100644 --- a/lib/SS2K/src/sensors/ChronoData.cpp +++ b/lib/SS2K/src/sensors/ChronoData.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: GPL-2.0-only */ +#include "endian.h" #include "sensors/ChronoData.h" bool ChronoData::hasHeartRate() { return false; } @@ -35,7 +36,7 @@ void ChronoData::decode(uint8_t *data, size_t length) { power = INT_MIN; return; } - cadence = (data[8] | (data[9] << 8)) / 10.0f; // bytes 8-9, uint16 little-endian, ÷10 RPM - power = data[17] | (data[18] << 8); // bytes 17-18, uint16 little-endian, watts + cadence = get_le16(&data[8]) / 10.0f; // bytes 8-9, uint16 little-endian, ÷10 RPM + power = get_le16(&data[17]); // bytes 17-18, uint16 little-endian, watts hasData = true; } diff --git a/src/BLE_Client.cpp b/src/BLE_Client.cpp index cd9b8f14..042c8e73 100644 --- a/src/BLE_Client.cpp +++ b/src/BLE_Client.cpp @@ -344,7 +344,8 @@ void MyClientCallback::onDisconnect(NimBLEClient* pClient, int reason) { SS2K_LOG(BLE_CLIENT_LOG_TAG, "Detected %s Disconnect", spinBLEClient.myBLEDevices[i].serviceUUID.toString().c_str()); if ((spinBLEClient.myBLEDevices[i].charUUID == CYCLINGPOWERMEASUREMENT_UUID) || (spinBLEClient.myBLEDevices[i].charUUID == FITNESSMACHINEINDOORBIKEDATA_UUID) || (spinBLEClient.myBLEDevices[i].charUUID == FLYWHEEL_UART_RX_UUID) || (spinBLEClient.myBLEDevices[i].charUUID == ECHELON_SERVICE_UUID) || - (spinBLEClient.myBLEDevices[i].charUUID == CYCLINGPOWERSERVICE_UUID) || (spinBLEClient.myBLEDevices[i].charUUID == CSCSERVICE_UUID)) { + (spinBLEClient.myBLEDevices[i].charUUID == CHRONO_DATA_UUID) || (spinBLEClient.myBLEDevices[i].charUUID == CYCLINGPOWERSERVICE_UUID) || + (spinBLEClient.myBLEDevices[i].charUUID == CSCSERVICE_UUID)) { SS2K_LOG(BLE_CLIENT_LOG_TAG, "Deregistered PM on Disconnect"); } if ((spinBLEClient.myBLEDevices[i].charUUID == HEARTCHARACTERISTIC_UUID)) { @@ -1094,7 +1095,7 @@ void SpinBLEAdvertisedDevice::set(const NimBLEAdvertisedDevice* device, int id, SS2K_LOG(BLE_CLIENT_LOG_TAG, "Registered CSC on Connect"); } else if (serviceUUID == CYCLINGPOWERSERVICE_UUID || serviceUUID == FITNESSMACHINESERVICE_UUID || (serviceUUID == FLYWHEEL_UART_SERVICE_UUID && this->uniqueName.find("Flywheel") != std::string::npos) || serviceUUID == ECHELON_DEVICE_UUID || - serviceUUID == PELOTON_DATA_UUID) { + serviceUUID == CHRONO_SERVICE_UUID || serviceUUID == PELOTON_DATA_UUID) { this->isPM = true; spinBLEClient.connectedPM = true; SS2K_LOG(BLE_CLIENT_LOG_TAG, "Registered PM on Connect"); From 48f88c1c75b94df38ffa89fd26e6533f24a4fdb6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Jun 2026 13:15:16 +0000 Subject: [PATCH 3/3] Update changelog for version 26.5.27 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 327aa862..9ef2aefe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Hardware +## [26.5.27] + +### Added + +### Changed + +### Hardware + + ## [26.5.4] ### Added