Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5457dbf
Add Zwift custom BLE service and integrate virtual shifting functiona…
doudar Mar 1, 2026
cbf1549
Controls almost working.
doudar Mar 3, 2026
9ed9e20
Zwift rxing commands
doudar Mar 3, 2026
81f0856
eliminated shifting feedback loop
doudar Mar 3, 2026
60239b2
in progress dev
doudar Mar 4, 2026
16e473d
in progress
doudar Mar 8, 2026
0c52753
Refactor service-specific DirCon write handling into service files
doudar Mar 8, 2026
7c3f1ff
Enhance DirCon service registration and write handling; remove deprec…
doudar Mar 8, 2026
619115d
Update changelog for version 26.1.31
actions-user Mar 8, 2026
a67c1a8
Refactor BLE_Zwift_Service to improve session management and logging;…
doudar Mar 8, 2026
445304d
Merge branch 'Zwift_shifting' of https://github.com/doudar/SmartSpin2…
doudar Mar 8, 2026
89081f0
back to Arduino BLE library
doudar Mar 9, 2026
dafecdc
Refactor BLE_Zwift_Service for improved code readability and consiste…
doudar Mar 9, 2026
bcec78c
Thread safety and advertisement cleanup
doudar Mar 9, 2026
a1d7e04
Enhance BLE_Zwift_Service with new payload handling methods and updat…
doudar Mar 12, 2026
1607b8b
WIP
doudar Mar 12, 2026
a65ef8c
Fixed some ZP errors
doudar Mar 12, 2026
fdc0c3b
Update BLE_Server.cpp
doudar Mar 16, 2026
0e98dc2
wip
doudar Mar 18, 2026
5cbfcfe
templated a couple functions
doudar Mar 19, 2026
063541e
added openBikeControl
doudar Apr 15, 2026
4db14df
Garmin BLE changes
eMadman May 1, 2026
8f9576b
fix(ble): restructure advertising and ensure spec compliance for Garm…
eMadman May 2, 2026
da0958b
Garmin BLE changes
eMadman May 1, 2026
ff1af3f
fix(ble): restructure advertising and ensure spec compliance for Garm…
eMadman May 2, 2026
8fc6b5f
Merge branch 'garmin-pairing' of https://github.com/doudar/SmartSpin2…
doudar May 2, 2026
354e924
Update changelog for version 26.5.2
actions-user May 2, 2026
78f948c
updated changelog
doudar May 2, 2026
4adf903
Merge branch 'garmin-pairing' into Zwift_shifting
doudar May 2, 2026
501a8a4
fixed advertising
doudar May 2, 2026
c58a2d7
Disabled OpenBikeControl and ZP (for now)
doudar May 3, 2026
a2b027d
Advertisement work
doudar May 4, 2026
859416b
removed vector from cps
doudar May 4, 2026
f3af749
removed hrm from advertisement
doudar May 4, 2026
9cc428f
Add mDNS service support for OpenBikeControl and update DirConManager…
doudar May 4, 2026
7680a2d
code review fixes
doudar May 4, 2026
0ccfeda
Merge pull request #733 from doudar/Zwift_shifting
doudar May 4, 2026
b792922
Potential fix for pull request finding
doudar May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

### Changed
- Adjusted BLE advertising to present SmartSpin2k as a cycling power sensor for improved Garmin discovery and pairing.
- Updated BLE setup to use a public device address for Garmin compatibility.

### Hardware


## [26.5.2]

### Added
- Added CoolStep support for StealthChop stepper operation.

Expand All @@ -26,6 +37,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Hardware


## [26.1.31]

### Added

### Changed

### Hardware


## [26.1.22]

### Added
Expand Down
Binary file not shown.
Binary file not shown.
12 changes: 6 additions & 6 deletions dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,25 @@ dependencies:
type: service
version: 1.6.3
espressif/esp_modem:
component_hash: b541c3f5765d3ba062c0d004b9ca6da6d0af908293421c2dbe046c537fb2a011
component_hash: 40af7f39c6c8c0c85c98014721fca8d9d2186f18f70645c66b94548192cf1e03
dependencies:
- name: idf
require: private
version: '>=4.1'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.4.0
version: 1.4.2
espressif/mdns:
component_hash: 3ec0af5f6bce310512e90f482388d21cc7c0e99668172d2f895356165fc6f7c5
component_hash: 8bcf12e37c58c1d584aef32a02b92548124c7a3a9fcf548d3235c844a035e0f0
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.8.2
version: 1.11.1
espressif/network_provisioning:
component_hash: ef2e10182fd1861e68b821491916327c25416ca7ae70e5a6e43313dbc71fe993
dependencies:
Expand All @@ -77,15 +77,15 @@ dependencies:
type: idf
version: 5.4.2
joltwallet/littlefs:
component_hash: fe3d04a59a4c370408b0e0b69d9096c06371b9ee12ad8e06b9d52ac63ab1570c
component_hash: 29eff7921c4e8bce021aeb7fff2723bfcafed661756de700646ea0f6223c2a32
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.20.0
version: 1.21.1
direct_dependencies:
- chmorgan/esp-libhelix-mp3
- espressif/cbor
Expand Down
78 changes: 51 additions & 27 deletions include/BLE_Definitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <array>

// The .xml file is wrong, make sure to reference the actual FTMS .pdf
struct FitnessMachineIndoorBikeDataFlags {
enum Types : uint16_t {
Expand Down Expand Up @@ -232,6 +234,9 @@ inline CyclingPowerFeatureFlags::Types operator|(CyclingPowerFeatureFlags::Types

class CyclingPowerMeasurement {
public:
static constexpr size_t MaxPayloadLength = 14;
using Buffer = std::array<uint8_t, MaxPayloadLength>;

// Flags definition as per specification
struct Flags {
uint16_t pedalPowerBalancePresent : 1;
Expand Down Expand Up @@ -260,38 +265,52 @@ class CyclingPowerMeasurement {
uint16_t cumulativeCrankRevolutions;
uint16_t lastCrankEventTime;

std::vector<uint8_t> toByteArray() {
std::vector<uint8_t> data;
// Add flags to data vector
data.push_back(static_cast<uint8_t>(*(reinterpret_cast<uint16_t*>(&flags)) & 0xFF));
data.push_back(static_cast<uint8_t>((*(reinterpret_cast<uint16_t*>(&flags)) >> 8) & 0xFF));
size_t toByteArray(Buffer& data) const {
size_t offset = 0;
uint16_t flagBits = 0;
flagBits |= flags.pedalPowerBalancePresent ? (1U << 0) : 0;
flagBits |= flags.pedalPowerBalanceReference ? (1U << 1) : 0;
flagBits |= flags.accumulatedTorquePresent ? (1U << 2) : 0;
flagBits |= flags.accumulatedTorqueSource ? (1U << 3) : 0;
flagBits |= flags.wheelRevolutionDataPresent ? (1U << 4) : 0;
flagBits |= flags.crankRevolutionDataPresent ? (1U << 5) : 0;
flagBits |= flags.extremeForceMagnitudesPresent ? (1U << 6) : 0;
flagBits |= flags.extremeTorqueMagnitudesPresent ? (1U << 7) : 0;
flagBits |= flags.extremeAnglesPresent ? (1U << 8) : 0;
flagBits |= flags.topDeadSpotAnglePresent ? (1U << 9) : 0;
flagBits |= flags.bottomDeadSpotAnglePresent ? (1U << 10) : 0;
flagBits |= flags.accumulatedEnergyPresent ? (1U << 11) : 0;
flagBits |= flags.offsetCompensationIndicator ? (1U << 12) : 0;

data[offset++] = static_cast<uint8_t>(flagBits & 0xFF);
data[offset++] = static_cast<uint8_t>((flagBits >> 8) & 0xFF);

// Add Instantaneous Power
data.push_back(static_cast<uint8_t>(instantaneousPower & 0xFF));
data.push_back(static_cast<uint8_t>((instantaneousPower >> 8) & 0xFF));
data[offset++] = static_cast<uint8_t>(instantaneousPower & 0xFF);
data[offset++] = static_cast<uint8_t>((instantaneousPower >> 8) & 0xFF);

// Conditional fields based on flags
if (flags.wheelRevolutionDataPresent) {
// Add wheel revolution data if present
data.push_back(static_cast<uint8_t>(cumulativeWheelRevolutions & 0xFF));
data.push_back(static_cast<uint8_t>((cumulativeWheelRevolutions >> 8) & 0xFF));
data.push_back(static_cast<uint8_t>((cumulativeWheelRevolutions >> 16) & 0xFF));
data.push_back(static_cast<uint8_t>((cumulativeWheelRevolutions >> 24) & 0xFF));
data[offset++] = static_cast<uint8_t>(cumulativeWheelRevolutions & 0xFF);
data[offset++] = static_cast<uint8_t>((cumulativeWheelRevolutions >> 8) & 0xFF);
data[offset++] = static_cast<uint8_t>((cumulativeWheelRevolutions >> 16) & 0xFF);
data[offset++] = static_cast<uint8_t>((cumulativeWheelRevolutions >> 24) & 0xFF);

data.push_back(static_cast<uint8_t>(lastWheelEventTime & 0xFF));
data.push_back(static_cast<uint8_t>((lastWheelEventTime >> 8) & 0xFF));
data[offset++] = static_cast<uint8_t>(lastWheelEventTime & 0xFF);
data[offset++] = static_cast<uint8_t>((lastWheelEventTime >> 8) & 0xFF);
}
// Conditional fields based on flags
if (flags.crankRevolutionDataPresent) {
// Add crank revolution data if present
data.push_back(static_cast<uint8_t>(cumulativeCrankRevolutions & 0xFF));
data.push_back(static_cast<uint8_t>((cumulativeCrankRevolutions >> 8) & 0xFF));
data[offset++] = static_cast<uint8_t>(cumulativeCrankRevolutions & 0xFF);
data[offset++] = static_cast<uint8_t>((cumulativeCrankRevolutions >> 8) & 0xFF);

data.push_back(static_cast<uint8_t>(lastCrankEventTime & 0xFF));
data.push_back(static_cast<uint8_t>((lastCrankEventTime >> 8) & 0xFF));
data[offset++] = static_cast<uint8_t>(lastCrankEventTime & 0xFF);
data[offset++] = static_cast<uint8_t>((lastCrankEventTime >> 8) & 0xFF);
}

return data;
return offset;
}
};

Expand All @@ -312,6 +331,9 @@ inline CyclingSpeedCadenceFeatureFlags::Types operator|(CyclingSpeedCadenceFeatu

class CscMeasurement {
public:
static constexpr size_t MaxPayloadLength = 11;
using Buffer = std::array<uint8_t, MaxPayloadLength>;

// Flags definition as per specification
struct Flags {
uint8_t wheelRevolutionDataPresent : 1;
Expand All @@ -330,33 +352,35 @@ class CscMeasurement {
*(reinterpret_cast<uint8_t*>(&flags)) = 0;
}

std::vector<uint8_t> toByteArray() {
std::vector<uint8_t> data;
size_t toByteArray(Buffer& data) const {
size_t offset = 0;
uint8_t flagBits = 0;
flagBits |= flags.wheelRevolutionDataPresent ? (1U << 0) : 0;
flagBits |= flags.crankRevolutionDataPresent ? (1U << 1) : 0;

// Add flags to data vector
data.push_back(*(reinterpret_cast<uint8_t*>(&flags)));
data[offset++] = flagBits;

// 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);
data[offset++] = static_cast<uint8_t>((cumulativeWheelRevolutions >> (i * 8)) & 0xFF);
}
for (int i = 0; i < 2; ++i) {
data.push_back((lastWheelEventTime >> (i * 8)) & 0xFF);
data[offset++] = static_cast<uint8_t>((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);
data[offset++] = static_cast<uint8_t>((cumulativeCrankRevolutions >> (i * 8)) & 0xFF);
}
for (int i = 0; i < 2; ++i) {
data.push_back((lastCrankEventTime >> (i * 8)) & 0xFF);
data[offset++] = static_cast<uint8_t>((lastCrankEventTime >> (i * 8)) & 0xFF);
}
}

return data;
return offset;
}
};
54 changes: 54 additions & 0 deletions include/BLE_OpenBikeControl_Service.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 Anthony Doud & Joel Baranick
* All rights reserved
*
* SPDX-License-Identifier: GPL-2.0-only
*/

#pragma once

#include <NimBLEDevice.h>

class BLE_OpenBikeControl_Service {
public:
BLE_OpenBikeControl_Service();

void setupService(NimBLEServer *pServer);
bool isConnected();
void sendShiftUp();
void sendShiftDown();

void handleHapticWrite(const uint8_t *data, size_t length, bool isDirCon = false);
void handleAppInfoWrite(const uint8_t *data, size_t length, bool isDirCon = false);
void handleButtonStateSubscription(uint16_t subValue);

private:
NimBLEService *pOpenBikeControlService;
NimBLECharacteristic *buttonStateCharacteristic;
NimBLECharacteristic *hapticFeedbackCharacteristic;
NimBLECharacteristic *appInformationCharacteristic;
unsigned long _lastClientActivityMs;

static void setupMDNS();
static void addServiceUuidToMDNS(const NimBLEUUID& serviceUuid);

void sendButtonState(uint8_t buttonId, uint8_t state);
void markClientActivity();
};

class OpenBikeControlHapticCallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
};

class OpenBikeControlAppInfoCallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
};

class OpenBikeControlButtonStateCallbacks : public NimBLECharacteristicCallbacks {
public:
void onSubscribe(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo, uint16_t subValue) override;
};

extern BLE_OpenBikeControl_Service openBikeControlService;
Loading
Loading