Skip to content
Open
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 @@ -14,6 +14,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.
121 changes: 121 additions & 0 deletions include/BLE_Zwift_Service.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2020 Anthony Doud & Joel Baranick
* All rights reserved
*
* SPDX-License-Identifier: GPL-2.0-only
*/

#pragma once

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

// Zwift manufacturer ID
inline constexpr uint16_t kZwiftManufacturerId = 0x094A;

// Zwift device type identifiers (from manufacturer data)
enum class ZwiftDeviceType : uint8_t {
BC1 = 0x09,
ClickV2Right = 0x0A,
ClickV2Left = 0x0B,
};

// Keepalive / riding data interval in milliseconds
inline constexpr unsigned long kZwiftKeepaliveIntervalMs = 5000;
inline constexpr unsigned long kZwiftSessionTimeoutMs = kZwiftKeepaliveIntervalMs * 2;
inline constexpr unsigned long kZwiftRidingDataIntervalMs = 250;
inline constexpr char kZwiftBleLogTag[] = "BLE_Zwift";
inline constexpr char kZwiftDirConLogTag[] = "DRC_Zwift";

class BLE_Zwift_Service {
public:
BLE_Zwift_Service();
void setupService(NimBLEServer *pServer);
void update();

// Returns true if a Zwift client has been used recently.
bool isConnected();

// Returns the current virtual gear ratio x10000 (0 = no virtual shifting active)
uint32_t getGearRatioX10000();

// Send a shift up notification to Zwift (key down + key up)
void sendShiftUp();

// Send a shift down notification to Zwift (key down + key up)
void sendShiftDown();

// Handle incoming write to sync_rx (handshake and protocol commands)
// isDirCon=true uses trainer protocol, isDirCon=false uses Click v2 controller protocol
void handleSyncRxWrite(const std::string &value, bool isDirCon = false);

private:
NimBLEService *pZwiftService;
NimBLECharacteristic *asyncCharacteristic;
NimBLECharacteristic *syncRxCharacteristic;
NimBLECharacteristic *syncTxCharacteristic;
NimBLECharacteristic *unknownCharacteristic5;
NimBLECharacteristic *unknownCharacteristic6;

bool isDirCon;
unsigned long _lastActivityTime;
unsigned long _lastKeepaliveTime;
unsigned long _lastRidingDataTime;
uint32_t _gearRatioX10000;

const char *getLogTag() const;
bool keepAlive(unsigned long now);
void resetSession();

// Encode a button mask into a protobuf varint message and send as notification
void sendButtonNotification(ZwiftProtocol::RideButtonMask buttonMask);

// Encode uint32 as protobuf varint, returns number of bytes written
static size_t encodeVarint32(uint32_t value, uint8_t *buffer);

// Encode uint64 as ULEB128 varint, returns number of bytes written
static size_t encodeUleb128(uint64_t value, uint8_t *buffer);
// Returns the number of bytes a ULEB128-encoded value would occupy
static size_t encodeUleb128Len(uint64_t value);

// Decode ULEB128 varint from buffer, returns number of bytes consumed
static size_t decodeUleb128(const uint8_t *buf, size_t bufLen, uint64_t *result);

// Send the "all buttons released" state
void sendAllButtonsReleased();

// Send a fully-built payload on sync_tx and mirror it through DirCon.
void sendSyncTxPayload(const uint8_t *payload, size_t length);

// Send a fully-built payload on async and mirror it through DirCon.
void sendAsyncPayload(const uint8_t *payload, size_t length);

// Send current GearRatio device information on sync_tx.
void sendGearRatioSyncTx();

// Send current GeneralInfo device information on sync_tx.
void sendGeneralInfoSyncTx();

// Send TRAINER_CONFIG_STATUS category 2 with reported real/virtual gear ratios.
void sendTrainerConfigSimulationStatus(uint32_t realGearRatioX10000, uint32_t virtualGearRatioX10000);

// Send TRAINER_CONFIG_STATUS category 3 with virtual_shifting_mode=1 on async.
void sendTrainerConfigVirtualShiftStatus(uint8_t virtualShiftingMode = 1);

// Handle Zwift trainer protocol command (non-RideOn messages)
void handleZwiftCommand(const uint8_t *data, size_t length);

// Apply received gear ratio to shifter position
void applyGearRatio();

friend class ZwiftSyncRxCallbacks;
};

// Callback class for the Zwift sync_rx characteristic writes
class ZwiftSyncRxCallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override;
void onSubscribe(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo, uint16_t subValue) override;
};

extern BLE_Zwift_Service zwiftService;
43 changes: 35 additions & 8 deletions include/DirConManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,49 @@
#define DIRCON_MAX_CLIENTS 1
#define DIRCON_RECEIVE_BUFFER_SIZE 256
#define DIRCON_SEND_BUFFER_SIZE 256
#define DIRCON_MAX_CHARACTERISTICS 10 // maximum number of characteristics to track for subscriptions
#define DIRCON_MAX_CHARACTERISTICS 20 // maximum number of characteristics to track for subscriptions
#define DIRCON_MAX_SERVICES 10 // maximum number of services that can register with DirCon

// Result struct populated by service write handler callbacks
struct DirConWriteResult {
bool updateResponseData; // If true, response includes characteristic's current value
NimBLEUUID autoSubscribeUuids[4]; // UUIDs to auto-subscribe the client to
size_t autoSubscribeCount; // Number of valid entries in autoSubscribeUuids

DirConWriteResult() : updateResponseData(false), autoSubscribeUuids(), autoSubscribeCount(0) {}
};

// Subscription entry: stores UUID alongside active flag to avoid hash collisions
struct Subscription {
NimBLEUUID uuid;
bool active = false;
};

// Write handler callback: returns true if the characteristic was handled by this service
typedef bool (*DirConWriteHandler)(NimBLECharacteristic* characteristic, const uint8_t* data, size_t length, DirConWriteResult* result);

class DirConManager {
public:
static bool start();
static void stop();
static void update();

// Add a BLE service UUID to DirCon MDNS service
static void addBleServiceUuid(const NimBLEUUID& serviceUuid);
// Register a BLE service with DirCon for discovery and optional write handling.
// Services call this during setup. Write handler may be nullptr for read-only/notify-only services.
static void registerService(const NimBLEUUID& serviceUuid, DirConWriteHandler writeHandler = nullptr);

// Notify DirCon clients about BLE characteristic changes
static void notifyCharacteristic(const NimBLEUUID& serviceUuid, const NimBLEUUID& characteristicUuid, uint8_t* data, size_t length);
static void notifyCharacteristic(const NimBLEUUID& serviceUuid, const NimBLEUUID& characteristicUuid, uint8_t* data, size_t length, bool onlySubscribers = true);

private:
// Service registration
struct ServiceRegistration {
NimBLEUUID serviceUuid;
DirConWriteHandler writeHandler;
};
static ServiceRegistration registeredServices[DIRCON_MAX_SERVICES];
static size_t registeredServiceCount;

// Core functionality
static bool started;
static String statusMessage;
Expand All @@ -56,17 +84,16 @@ class DirConManager {
static bool processDirConMessage(DirConMessage* message, size_t clientIndex);
static void sendErrorResponse(uint8_t messageId, uint8_t sequenceNumber, uint8_t errorCode, size_t clientIndex);
static void sendResponse(DirConMessage* message, size_t clientIndex);
static void broadcastNotification(const NimBLEUUID& characteristicUuid, uint8_t* data, size_t length);
static void broadcastNotification(const NimBLEUUID& characteristicUuid, uint8_t* data, size_t length, bool onlySubscribers = true);

// Service and characteristic handling
static std::vector<NimBLEUUID> getAvailableServices();
static void addBleServiceUuid(const NimBLEUUID& serviceUuid);
static std::vector<NimBLECharacteristic*> getCharacteristics(const NimBLEUUID& serviceUuid);
static uint8_t getDirConProperties(uint32_t characteristicProperties);
static NimBLECharacteristic* findCharacteristic(const NimBLEUUID& characteristicUuid);

// Subscription tracking
static bool clientSubscriptions[DIRCON_MAX_CLIENTS][DIRCON_MAX_CHARACTERISTICS]; // Simple subscription tracking
static size_t charSubscriptionIndex(const NimBLEUUID& characteristicUuid);
static Subscription clientSubscriptions[DIRCON_MAX_CLIENTS][DIRCON_MAX_CHARACTERISTICS];
static void addSubscription(size_t clientIndex, const NimBLEUUID& characteristicUuid);
static void removeSubscription(size_t clientIndex, const NimBLEUUID& characteristicUuid);
static void removeAllSubscriptions(size_t clientIndex);
Expand Down
3 changes: 3 additions & 0 deletions include/Main.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class SS2K {
int32_t getCurrentPosition() { return currentPosition; }
void setCurrentPosition(int32_t cp) { currentPosition = cp; }

int getLastShifterPosition() { return lastShifterPosition; }
void setLastShifterPosition(int sp) { lastShifterPosition = sp; }

void resetIfShiftersHeld();
void startTasks();
void stopTasks();
Expand Down
8 changes: 6 additions & 2 deletions include/SS2KLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <freertos/semphr.h>
#include <freertos/message_buffer.h>
#include "LogAppender.h"
#include <string>
#include <vector>

#define SS2K_LOG_TAG "SS2K"
Expand Down Expand Up @@ -79,8 +80,11 @@ extern LogHandler logHandler;

void ss2k_remove_newlines(std::string *str);

int ss2k_log_hex_to_buffer(const byte *data, const size_t data_length, char *buffer, const int buffer_offset, const size_t buffer_length);
int ss2k_log_hex_to_buffer(const char *data, const size_t data_length, char *buffer, const int buffer_offset, const size_t buffer_length);
template <typename T>
std::string toHexString(const T *data, size_t dataLength);

template <typename T>
int ss2k_log_hex_to_buffer(const T *data, const size_t data_length, char *buffer, const int buffer_offset, const size_t buffer_length);

void ss2k_log_write(esp_log_level_t level, const char *module, const char *format, ...);

Expand Down
Loading
Loading