Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

### Changed

### Hardware


## [25.12.25]

### Added

### Changed
- Removed >0 watts requirement to compute ERG.

Expand Down
2 changes: 1 addition & 1 deletion dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@ direct_dependencies:
- espressif/network_provisioning
- idf
- joltwallet/littlefs
manifest_hash: e7ce7a886dfdb3cc5cd59acbdb4ff333c0a91c6e4de55b9fd6323d866c7cc691
manifest_hash: df9baa925ebc2a028ee0e349be53f2169bb7ac38b2cb9c63b3ebf69123c43ef8
target: esp32
version: 2.0.0
2 changes: 1 addition & 1 deletion include/BLE_Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class SpinBLEServer {
public:
int spinDownFlag = 0;
NimBLEServer* pServer = nullptr;
void notifyShift();
void notifyBleAndDircon(NimBLECharacteristic* pCharacteristic,const uint8_t* pData, int length);
double calculateSpeed();
void update();
int connectedClientCount();
Expand Down
106 changes: 106 additions & 0 deletions include/BLE_KickrBikeService.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2020 Anthony Doud & Joel Baranick
* All rights reserved
*
* SPDX-License-Identifier: GPL-2.0-only
*/

#pragma once

#include <NimBLEDevice.h>
#include "BLE_Common.h"

// Forward declaration for custom callback
class KickrBikeCharacteristicCallbacks;

// Gear system configuration for virtual shifting
#define KICKR_BIKE_NUM_GEARS 24
#define KICKR_BIKE_DEFAULT_GEAR 11 // Middle gear (0-indexed, so gear 12 in 1-indexed)

class BLE_KickrBikeService {
public:
BLE_KickrBikeService();
void setupService(NimBLEServer *pServer, MyCharacteristicCallbacks *chrCallbacks);
void update();

// Gear management
void shiftUp();
void shiftDown();
double getCurrentGearRatio() const;

// Function to check shifter position and modify incline accordingly
void updateGearFromShifterPosition();

// RideOn handshake handling
void processWrite(const std::string& value);
void sendRideOnResponse();
void sendKeepAlive();
void sendRideData();
void sendButtonPress(uint8_t buttonId);

// Wahoo gearing service notifications
void sendGearingNotification();

// Opcode message handlers
void handleGetRequest(const uint8_t* data, size_t length);
void handleSetRequest(const uint8_t* data, size_t length);
void handleInfoRequest(const uint8_t* data, size_t length);
void handleReset();
void handleSetLogLevel(const uint8_t* data, size_t length);
void handleVendorMessage(const uint8_t* data, size_t length);
void sendGetResponse(uint16_t objectId, const uint8_t* data, size_t length);
void sendStatusResponse(uint8_t status);

// Gradient/resistance control (independent of FTMS)
void applyGradientToTrainer(float gradient);
void applyGearChange(bool fromZwift = false);

// Power control for ERG mode
void setTargetPower(int watts);
int getTargetPower() const { return targetPower; }

// Enable/disable the service
void enable() { isEnabled = true; }
void disable() { isEnabled = false; }
bool isServiceEnabled() const { return isEnabled; }

private:
BLEService *pKickrBikeService;
BLECharacteristic *syncRxCharacteristic; // Write characteristic for commands
BLECharacteristic *asyncTxCharacteristic; // Notify characteristic for events
BLECharacteristic *syncTxCharacteristic; // Notify characteristic for responses
BLECharacteristic *debugCharacteristic; // Optional debug characteristic
BLECharacteristic *unknown6Characteristic; // Optional unknown characteristic

// Wahoo gearing service (separate from KICKR BIKE protocol)
BLEService *pGearingService;
BLECharacteristic *gearingCharacteristic; // Notify characteristic for gear display

// Gear system state
int lastShifterPosition;

// Gradient and resistance state (independent of FTMS)
int targetPower; // Target power for ERG mode (watts)

// Service state
bool isHandshakeComplete;
bool isEnabled; // Whether this service should control the trainer
unsigned long lastKeepAliveTime;
unsigned long lastGradientUpdateTime;
unsigned long lastRideDataTime;
unsigned long lastGearingUpdateTime;

// Gear ratio table (24 gears)
static const double gearRatios[KICKR_BIKE_NUM_GEARS];

// Helper methods
double calculateEffectiveGrade(double baseGrade, double gearRatio);
bool isRideOnMessage(const std::string& data);
};

// Custom callback class for KickrBike Sync RX characteristic
class KickrBikeCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override;
};

extern BLE_KickrBikeService kickrBikeService;
32 changes: 24 additions & 8 deletions lib/SS2K/include/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@

// Device Information Service
#define DEVICE_INFORMATION_SERVICE_UUID NimBLEUUID((uint16_t)0x180A)
#define MANUFACTURER_NAME_UUID NimBLEUUID((uint16_t)0x2A29)
#define MODEL_NUMBER_UUID NimBLEUUID((uint16_t)0x2A24)
#define SERIAL_NUMBER_UUID NimBLEUUID((uint16_t)0x2A25)
#define HARDWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A27)
#define FIRMWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A26)
#define SOFTWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A28)
#define SYSTEM_ID_UUID NimBLEUUID((uint16_t)0x2A23)
#define PNP_ID_UUID NimBLEUUID((uint16_t)0x2A50)
#define MANUFACTURER_NAME_UUID NimBLEUUID((uint16_t)0x2A29)
#define MODEL_NUMBER_UUID NimBLEUUID((uint16_t)0x2A24)
#define SERIAL_NUMBER_UUID NimBLEUUID((uint16_t)0x2A25)
#define HARDWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A27)
#define FIRMWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A26)
#define SOFTWARE_REVISION_UUID NimBLEUUID((uint16_t)0x2A28)
#define SYSTEM_ID_UUID NimBLEUUID((uint16_t)0x2A23)
#define PNP_ID_UUID NimBLEUUID((uint16_t)0x2A50)

// Heart Service
#define HEARTSERVICE_UUID NimBLEUUID((uint16_t)0x180D)
Expand Down Expand Up @@ -91,6 +91,22 @@
#define PELOTON_REQ_POS 1
#define PELOTON_CHECKSUM_POS 2

// Zwift Ride / KICKR BIKE Service
// This service emulates the Wahoo KICKR BIKE protocol for virtual shifting
#define ZWIFT_RIDE_SERVICE_UUID NimBLEUUID("0000FC82-0000-1000-8000-00805F9B34FB")
#define ZWIFT_CUSTOM_SERVICE_UUID NimBLEUUID("00000001-19CA-4651-86E5-FA29DCDD09D1")
#define ZWIFT_SYNC_RX_UUID NimBLEUUID("00000003-19CA-4651-86E5-FA29DCDD09D1") // Write characteristic
#define ZWIFT_ASYNC_TX_UUID NimBLEUUID("00000002-19CA-4651-86E5-FA29DCDD09D1") // Notify characteristic
#define ZWIFT_SYNC_TX_UUID NimBLEUUID("00000004-19CA-4651-86E5-FA29DCDD09D1") // Notify characteristic
#define ZWIFT_DEBUG_CHARACTERISTIC_UUID NimBLEUUID("00000005-19CA-4651-86E5-FA29DCDD09D1")
#define ZWIFT_UNKNOWN_6_CHARACTERISTIC_UUID NimBLEUUID("00000006-19CA-4651-86E5-FA29DCDD09D1")
#define ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT NimBLEUUID((uint16_t)0xFC82)

// Wahoo Gearing Service (KICKR BIKE gear display - separate from Zwift protocol)
// This service provides gear information to Wahoo ELEMNT displays and other compatible devices
#define WAHOO_GEARING_SERVICE_UUID NimBLEUUID("a026ee0d-0a7d-4ab3-97fa-f1500f9feb8b")
#define WAHOO_GEARING_CHARACTERISTIC_UUID NimBLEUUID("a026e03a-0a7d-4ab3-97fa-f1500f9feb8b")

// BLE HID
#define APPEARANCE_HID_GENERIC_UUID NimBLEUUID((uint16_t)0x3C0)
#define APPEARANCE_HID_KEYBOARD_UUID NimBLEUUID((uint16_t)0x3C1)
Expand Down
7 changes: 1 addition & 6 deletions src/BLE_Cycling_Power_Service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,7 @@ void BLE_Cycling_Power_Service::update() {
auto byteArray = cpm.toByteArray();

// Notify the cycling power measurement characteristic
// Need to set the value before notifying so that read works correctly.
cyclingPowerMeasurementCharacteristic->setValue(&byteArray[0], byteArray.size());
cyclingPowerMeasurementCharacteristic->notify();

// Also notify DirCon TCP clients
DirConManager::notifyCharacteristic(NimBLEUUID(CYCLINGPOWERSERVICE_UUID), cyclingPowerMeasurementCharacteristic->getUUID(), &byteArray[0], byteArray.size());
spinBLEServer.notifyBleAndDircon(cyclingPowerMeasurementCharacteristic, &byteArray[0], byteArray.size());

const int kLogBufCapacity = 150;
char logBuf[kLogBufCapacity];
Expand Down
8 changes: 3 additions & 5 deletions src/BLE_Cycling_Speed_Cadence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

BLE_Cycling_Speed_Cadence::BLE_Cycling_Speed_Cadence() : pCyclingSpeedCadenceService(nullptr), cscMeasurement(nullptr), cscFeature(nullptr) {}

void BLE_Cycling_Speed_Cadence::setupService(NimBLEServer *pServer, MyCharacteristicCallbacks *chrCallbacks) {
void BLE_Cycling_Speed_Cadence::setupService(NimBLEServer* pServer, MyCharacteristicCallbacks* chrCallbacks) {
pCyclingSpeedCadenceService = pServer->createService(CSCSERVICE_UUID);
cscMeasurement = pCyclingSpeedCadenceService->createCharacteristic(CSCMEASUREMENT_UUID, NIMBLE_PROPERTY::NOTIFY);
cscFeature = pCyclingSpeedCadenceService->createCharacteristic(CSCFEATURE_UUID, NIMBLE_PROPERTY::READ);
Expand All @@ -37,7 +37,7 @@ void BLE_Cycling_Speed_Cadence::update() {
CscMeasurement csc;

// Clear all flags initially
*(reinterpret_cast<uint8_t *>(&(csc.flags))) = 0;
*(reinterpret_cast<uint8_t*>(&(csc.flags))) = 0;

// Set flags based on data presence
csc.flags.wheelRevolutionDataPresent = 1; // Wheel Revolution Data Present
Expand All @@ -52,9 +52,7 @@ void BLE_Cycling_Speed_Cadence::update() {
auto byteArray = csc.toByteArray();

// Notify the cycling power measurement characteristic
// Need to set the value before notifying so that read works correctly.
cscMeasurement->setValue(&byteArray[0], byteArray.size());
cscMeasurement->notify();
spinBLEServer.notifyBleAndDircon(cscMeasurement, &byteArray[0], byteArray.size());

const int kLogBufCapacity = 150;
char logBuf[kLogBufCapacity];
Expand Down
37 changes: 8 additions & 29 deletions src/BLE_Fitness_Machine_Service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void BLE_Fitness_Machine_Service::setupService(NimBLEServer *pServer, MyCharacte
pFitnessMachineService->start();

// Add service UUID to DirCon MDNS
DirConManager::addBleServiceUuid(pFitnessMachineService->getUUID());
// DirConManager::addBleServiceUuid(pFitnessMachineService->getUUID());
}

void BLE_Fitness_Machine_Service::update() {
Expand Down Expand Up @@ -125,12 +125,7 @@ void BLE_Fitness_Machine_Service::update() {
}

// Notify the cycling power measurement characteristic
// Need to set the value before notifying so that read works correctly.
fitnessMachineIndoorBikeData->setValue(ftmsIndoorBikeData.data(), ftmsIndoorBikeData.size());
fitnessMachineIndoorBikeData->notify();

// Also notify DirCon TCP clients about Indoor Bike Data
DirConManager::notifyCharacteristic(NimBLEUUID(FITNESSMACHINESERVICE_UUID), fitnessMachineIndoorBikeData->getUUID(), ftmsIndoorBikeData.data(), ftmsIndoorBikeData.size());
spinBLEServer.notifyBleAndDircon(fitnessMachineIndoorBikeData, ftmsIndoorBikeData.data(), ftmsIndoorBikeData.size());

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
Expand Down Expand Up @@ -343,36 +338,20 @@ void BLE_Fitness_Machine_Service::processFTMSWrite() {
ftmsTrainingStatus[1] = FitnessMachineTrainingStatus::Other; // 0x00;
}
// not checking for subscription because a write request would have triggered this
fitnessMachineControlPoint->setValue(returnValue.data(), returnValue.size());
fitnessMachineControlPoint->notify();
spinBLEServer.notifyBleAndDircon(fitnessMachineControlPoint, returnValue.data(), returnValue.size());
if (fitnessMachineTrainingStatus->getValue() != ftmsTrainingStatus) {
fitnessMachineTrainingStatus->setValue(ftmsTrainingStatus);
fitnessMachineTrainingStatus->notify();
// Also notify DirCon TCP clients
DirConManager::notifyCharacteristic(NimBLEUUID(FITNESSMACHINESERVICE_UUID), fitnessMachineTrainingStatus->getUUID(), ftmsTrainingStatus.data(), ftmsTrainingStatus.size());
}
spinBLEServer.notifyBleAndDircon(fitnessMachineTrainingStatus, ftmsTrainingStatus.data(), ftmsTrainingStatus.size());
}
if (fitnessMachineStatusCharacteristic->getValue() != ftmsStatus) {
fitnessMachineStatusCharacteristic->setValue(ftmsStatus);
fitnessMachineStatusCharacteristic->notify();
// Also notify DirCon TCP clients
DirConManager::notifyCharacteristic(NimBLEUUID(FITNESSMACHINESERVICE_UUID), fitnessMachineStatusCharacteristic->getUUID(), ftmsStatus.data(), ftmsStatus.size());
}

// Also notify DirCon TCP clients
DirConManager::notifyCharacteristic(NimBLEUUID(FITNESSMACHINESERVICE_UUID), fitnessMachineControlPoint->getUUID(), returnValue.data(), returnValue.size());
spinBLEServer.notifyBleAndDircon(fitnessMachineStatusCharacteristic, ftmsStatus.data(), ftmsStatus.size());
}
}
}

bool BLE_Fitness_Machine_Service::spinDown(uint8_t response) {
uint8_t spinStatus[2] = {FitnessMachineStatus::SpinDownStatus, response};
// Set the value of the characteristic
fitnessMachineStatusCharacteristic->setValue(spinStatus, sizeof(spinStatus));
// Notify the connected client
fitnessMachineStatusCharacteristic->notify();
spinBLEServer.notifyBleAndDircon(fitnessMachineStatusCharacteristic, spinStatus, sizeof(spinStatus));
SS2K_LOG(FMTS_SERVER_LOG_TAG, "Sent SpinDown Status: 0x%02X", response);
// Also notify DirCon TCP clients about the status change
DirConManager::notifyCharacteristic(NimBLEUUID(FITNESSMACHINESERVICE_UUID), fitnessMachineStatusCharacteristic->getUUID(), spinStatus, sizeof(spinStatus));

return true;
}

Expand Down
6 changes: 1 addition & 5 deletions src/BLE_Heart_Service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ void BLE_Heart_Service::update() {

byte heartRateMeasurement[2] = {0x00, (byte)rtConfig->hr.getValue()};
// Notify the cycling power measurement characteristic
// Need to set the value before notifying so that read works correctly.
heartRateMeasurementCharacteristic->setValue(heartRateMeasurement, 2);
heartRateMeasurementCharacteristic->notify();
DirConManager::notifyCharacteristic(NimBLEUUID(HEARTSERVICE_UUID), heartRateMeasurementCharacteristic->getUUID(), heartRateMeasurement, 2);

spinBLEServer.notifyBleAndDircon(heartRateMeasurementCharacteristic, heartRateMeasurement, 2);
const int kLogBufCapacity = 125; // Data(10), Sep(data/2), Arrow(3), CharId(37), Sep(3), CharId(37), Sep(3), Name(8), Prefix(2), HR(7), Suffix(2), Nul(1), rounded up
char logBuf[kLogBufCapacity];
const size_t heartRateMeasurementLength = sizeof(heartRateMeasurement) / sizeof(heartRateMeasurement[0]);
Expand Down
Loading