Skip to content

Zwift shifting#727

Open
doudar wants to merge 21 commits intodevelopfrom
Zwift_shifting
Open

Zwift shifting#727
doudar wants to merge 21 commits intodevelopfrom
Zwift_shifting

Conversation

@doudar
Copy link
Copy Markdown
Owner

@doudar doudar commented Mar 8, 2026

Implementing ZP

doudar and others added 21 commits February 28, 2026 22:50
Move FTMS control point write logic from DirConManager.cpp into
BLE_Fitness_Machine_Service::handleDirConWrite().

Move Zwift sync_rx write logic (including auto-subscription UUIDs)
from DirConManager.cpp into BLE_Zwift_Service::handleDirConWrite().

DirConManager now calls each service's handleDirConWrite() generically
without referencing any service-specific UUIDs or protocol details.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ncy; adjust spacing and update service UUID references
Copilot AI review requested due to automatic review settings April 15, 2026 14:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces Zwift virtual shifting support (ZP) by adding a Zwift custom BLE service and an OpenBikeControl BLE service, and extends the DirCon bridge to discover services dynamically and dispatch writes to service-specific handlers.

Changes:

  • Add Zwift custom BLE service + protocol handling for handshake, riding data, and virtual shift events.
  • Add OpenBikeControl BLE service (button state + haptic/app-info writes) and advertise it over mDNS.
  • Refactor DirCon to use a service registration model for discovery, MDNS TXT updates, and write handling callbacks.

Reviewed changes

Copilot reviewed 26 out of 29 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/SS2KLog.cpp Hardens log formatting/sending and adds hex helpers (now templated).
include/SS2KLog.h Exposes new templated hex helpers.
src/Main.cpp Forwards physical shifter deltas to Zwift/OpenBikeControl services.
include/Main.h Adds accessors for last shifter position to prevent shift echo loops.
src/DirConMessage.cpp Improves unknown-message logging by dumping full message bytes.
src/DirConManager.cpp Adds service registration + write dispatch, improved subscription tracking, OpenBikeControl mDNS advertising, and notification locking.
include/DirConManager.h Adds DirCon service registry/write-handler API and subscription entry struct.
src/BLE_Zwift_Service.cpp New Zwift Ride custom service implementation (handshake, keepalive, gear ratio, shifting).
include/BLE_Zwift_Service.h Public interface for Zwift service + callback class.
src/BLE_OpenBikeControl_Service.cpp New OpenBikeControl BLE service implementation (button state notify, haptic/app-info writes).
include/BLE_OpenBikeControl_Service.h Public interface for OpenBikeControl service + callback classes.
src/BLE_Server.cpp Wires Zwift/OpenBikeControl services into server startup and update loop; adjusts advertising/scan response.
src/BLE_Heart_Service.cpp Switches DirCon integration to registerService().
src/BLE_Fitness_Machine_Service.cpp Switches DirCon integration to registerService() with a write handler for FTMS CP.
src/BLE_Device_Information_Service.cpp Improves DIS fields (manufacturer/model/hw rev/serial/system id) for interoperability.
src/BLE_Cycling_Speed_Cadence.cpp Switches DirCon integration to registerService().
src/BLE_Cycling_Power_Service.cpp Switches DirCon integration to registerService().
src/BLE_Custom_Characteristic.cpp Minor formatting-only change.
lib/SS2K/include/Constants.h Adds UUID constants for Zwift and OpenBikeControl services/characteristics.
include/Zwift_Protocol_Messages.h Adds Zwift protobuf/tag/enum definitions used by the Zwift service.
platformio.ini Adds a pre-build cleanup script; updates NimBLE-Arduino comment link.
scripts/pre_build_cleanup.py New pre-build script to remove stale x509 bundle artifacts and force single-job builds.
scripts/pycache/pre_build_cleanup.cpython-313.pyc Added compiled Python artifact (should not be committed).
sdkconfig.release.old Updates ESP-IDF config snapshot (tooling/build settings).
sdkconfig.release Updates ESP-IDF release config (NimBLE/Arduino selective build settings).
include/cert.h Updates root CA certificate bundle used for GitHub raw fetches.
CHANGELOG.md Adds a new (currently empty) version section.
Hardware/V3 - Integrated PCB/PCB/SS2K_KiCad_Files/SmartSpin2k-backups/SmartSpin2k-2023-01-21_082918.zip Hardware backup ZIP (binary).
Hardware/V3 - Integrated PCB/PCB/SS2K_KiCad_Files/SmartSpin2k-backups/SmartSpin2k-2022-10-31_214835.zip Hardware backup ZIP (binary).

Comment thread src/SS2KLog.cpp
Comment on lines 54 to 57
if (_messageBufferHandle == NULL) {
ESP_LOGE(LOG_HANDLER_TAG, "Can not send log message. Message Buffer is NULL");
return;
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogHandler::writev() returns early when _messageBufferHandle is NULL without releasing _logBufferMutex, which can permanently deadlock logging for the rest of the runtime. Ensure the semaphore is always given on all exit paths after a successful xSemaphoreTake (e.g., give the mutex before returning on the NULL handle path).

Copilot uses AI. Check for mistakes.
Comment thread src/BLE_Zwift_Service.cpp
DirConManager::notifyCharacteristic(NimBLEUUID(ZWIFT_RIDE_CUSTOM_SERVICE_UUID), asyncCharacteristic->getUUID(), zData, pos);
}

bool BLE_Zwift_Service::isConnected() {
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLE_Zwift_Service::isConnected() treats _lastActivityTime == 0 as a recent activity (millis()-0 < timeout), so it will report connected for the first ~10s after boot even with no Zwift client. This can incorrectly route shifter events and keepalives. Consider requiring _lastActivityTime != 0 (and/or tracking actual NimBLE connection state) before returning true.

Suggested change
bool BLE_Zwift_Service::isConnected() {
bool BLE_Zwift_Service::isConnected() {
if (_lastActivityTime == 0) {
return false;
}

Copilot uses AI. Check for mistakes.
Comment thread src/BLE_Zwift_Service.cpp
// Also update lastShifterPosition so FTMSModeShiftModifier doesn't
// see this Zwift-driven change as a user shift and echo it back.
ss2k->setLastShifterPosition(newShifterPos);
SS2K_LOG(getLogTag(), "Gear %d -> shifter position %d", closestIndex + 1, newShifterPos);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyGearRatio(): closestIndex is already set to i + 1, but the log prints closestIndex + 1, which makes the reported gear off by one. Either keep closestIndex 0-based and add 1 only when presenting, or log closestIndex directly to match the shifter position you apply.

Suggested change
SS2K_LOG(getLogTag(), "Gear %d -> shifter position %d", closestIndex + 1, newShifterPos);
SS2K_LOG(getLogTag(), "Gear %d -> shifter position %d", closestIndex, newShifterPos);

Copilot uses AI. Check for mistakes.
Comment thread src/DirConMessage.cpp
Comment on lines +323 to +328
char hexBuf[len * 3 + 1];
for (size_t i = 0; i < len; i++) {
snprintf(hexBuf + i * 3, 4, "%02X ", data[i]);
}
hexBuf[len * 3] = '\0';
SS2K_LOG(DIRCON_LOG_TAG, "Error parsing DirCon message: Unknown identifier %d. Full message (%d bytes): %s", this->Identifier, len, hexBuf);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses a variable-length array (char hexBuf[len * 3 + 1];). VLAs are not standard C++ and can fail to compile depending on toolchain flags; they also risk large stack usage when len grows. Prefer a fixed-size buffer based on the protocol max (e.g., DIRCON_RECEIVE_BUFFER_SIZE) or build the hex string incrementally into a std::string.

Suggested change
char hexBuf[len * 3 + 1];
for (size_t i = 0; i < len; i++) {
snprintf(hexBuf + i * 3, 4, "%02X ", data[i]);
}
hexBuf[len * 3] = '\0';
SS2K_LOG(DIRCON_LOG_TAG, "Error parsing DirCon message: Unknown identifier %d. Full message (%d bytes): %s", this->Identifier, len, hexBuf);
char hexBuf[(DIRCON_RECEIVE_BUFFER_SIZE * 3) + 1];
size_t maxBytesToLog = DIRCON_RECEIVE_BUFFER_SIZE;
size_t bytesToLog = len < maxBytesToLog ? len : maxBytesToLog;
for (size_t i = 0; i < bytesToLog; i++) {
snprintf(hexBuf + i * 3, 4, "%02X ", data[i]);
}
hexBuf[bytesToLog * 3] = '\0';
if (bytesToLog < len) {
SS2K_LOG(DIRCON_LOG_TAG, "Error parsing DirCon message: Unknown identifier %d. Full message (%d bytes, truncated to %d bytes): %s", this->Identifier, len, bytesToLog, hexBuf);
} else {
SS2K_LOG(DIRCON_LOG_TAG, "Error parsing DirCon message: Unknown identifier %d. Full message (%d bytes): %s", this->Identifier, len, hexBuf);
}

Copilot uses AI. Check for mistakes.
Comment thread src/BLE_Zwift_Service.cpp

void BLE_Zwift_Service::sendShiftDown() {
SS2K_LOG(getLogTag(), "Sending shift down to Zwift");
sendButtonNotification(RideButtonMask::ShiftUpLeft);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendShiftDown() uses RideButtonMask::ShiftUpLeft, which looks like the wrong button bit for a downshift (there is also ShiftDownLeft / ShiftDownRight). This likely sends the incorrect Zwift button event. Use the appropriate ShiftDown* mask for the downshift path.

Suggested change
sendButtonNotification(RideButtonMask::ShiftUpLeft);
sendButtonNotification(RideButtonMask::ShiftDownLeft);

Copilot uses AI. Check for mistakes.
Comment thread src/DirConManager.cpp
Comment on lines +574 to +576
static SemaphoreHandle_t s_notifyMutex = xSemaphoreCreateMutex();

void DirConManager::broadcastNotification(const NimBLEUUID& characteristicUuid, uint8_t* data, size_t length, bool onlySubscribers) {
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s_notifyMutex is created at static initialization time and is used without a null check. If xSemaphoreCreateMutex() fails (or static init ordering is problematic), later xSemaphoreTake() will dereference a null handle and crash. Create the mutex during DirConManager::start() (or lazily on first use) and guard against nullptr before taking/giving it.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
# Keep build deterministic for generated asm artifacts.
env.SetOption("num_jobs", 1)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting env.SetOption("num_jobs", 1) forces single-threaded builds for all environments, which can significantly increase build times. If the intent is only to avoid stale x509 bundle artifacts, consider limiting the workaround to deleting the stale files (or gating the single-job setting behind an env flag / only enabling it for affected environments).

Suggested change
# Keep build deterministic for generated asm artifacts.
env.SetOption("num_jobs", 1)
# Keep the workaround targeted to the affected generated files so
# normal parallel build performance is preserved.

Copilot uses AI. Check for mistakes.
Comment thread src/BLE_Zwift_Service.cpp
Comment on lines +193 to +197
void BLE_Zwift_Service::update() {
unsigned long now = millis();
static unsigned long lastadvTime = 0;
static bool advertiseManufacturerData = true;
static bool swapAdv = false;
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLE_Zwift_Service::update() doesn’t currently gate riding-data notifications behind a real Zwift session/connection, and it doesn’t throttle sends to kZwiftRidingDataIntervalMs (the _lastRidingDataTime member is never used). Add a connection/session check and periodic send logic to avoid spamming BLE/DirCon and wasting CPU.

Copilot uses AI. Check for mistakes.
Comment thread src/Main.cpp
Comment on lines +350 to +354
int absDelta = abs(shiftDelta);
if (zwiftService.isConnected()) {
for (int i = 0; i < absDelta; i++) {
if (shiftDelta > 0) {
zwiftService.sendShiftUp();
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTMSModeShiftModifier() forwards shifts by calling sendShiftUp/Down in a loop. Those helpers include a vTaskDelay(50ms) per step, so a multi-gear change can stall this task for hundreds of milliseconds (and longer if multiple services are connected). Consider making shift notifications non-blocking (e.g., queue events to a dedicated task/timer) so control-loop timing isn’t impacted.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants