Skip to content
Merged

S3 #747

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a846384
initial commit
doudar Aug 4, 2026
42d1415
Memory management fixes
doudar Aug 4, 2026
868efd1
Update changelog for version 26.7.15
actions-user Aug 4, 2026
9e7a13a
Refactor board configuration and initialization logic; consolidate pi…
doudar Aug 5, 2026
944f48c
Add new HTML, CSS, and Python files for filesystem preparation and co…
doudar Aug 5, 2026
dfda0df
Update resistance handling to blacklist non-Grupetto devices
doudar Aug 5, 2026
d5df58b
Rename S3 firmware artifacts and update build script to remove unpref…
doudar Aug 5, 2026
02aab8b
Add return link to main page in OTA server index and update styles
doudar Aug 5, 2026
d18c627
Update build process to include partitions and bootloader binaries; r…
doudar Aug 5, 2026
db7243a
Enhance OTA filesystem handling to remount LittleFS after upload and …
doudar Aug 5, 2026
039a926
Merge branch 'develop' into S3
doudar Aug 5, 2026
abd2684
Add BLE_allSettings characteristic for chunked JSON snapshot of user …
doudar Aug 5, 2026
53373d6
Merge branch 'S3' of https://github.com/doudar/SmartSpin2k into S3
doudar Aug 5, 2026
59f59c1
Library updates
doudar Aug 7, 2026
77bf2fb
Only use resistance values from Grupetto
doudar Aug 7, 2026
b288f00
Always go to 8 after homing.
doudar Aug 7, 2026
8aa6955
Refactor ERG gain calculations and update library dependencies
doudar Aug 8, 2026
30d263c
removed launch.json
doudar Aug 8, 2026
94092f3
disabled wifi power save
doudar Aug 9, 2026
3a1794f
Enhance inactivity detection logic to reboot after 30 minutes without…
doudar Aug 10, 2026
d659bf7
Add firmware image validation for OTA updates and enhance error handling
doudar Aug 10, 2026
4f7aa5b
Increase BLE reconnect scan interval and default scan duration
doudar Aug 10, 2026
f3bf52b
Implement case-insensitive BLE device identifier matching and add cor…
doudar Aug 11, 2026
f9924a4
Enhance BLE advertisement to include current WiFi IPv4 address and up…
doudar Aug 11, 2026
246f8da
Refine tooltip text for minimum brake watts and update related commen…
doudar Aug 13, 2026
312ab07
Add getIsQuadratic method to ResistanceModel and adjust ERG_LOG_INTER…
doudar Aug 13, 2026
7010cd2
Add utility functions for DIRCON message handling and improve logging…
doudar Aug 13, 2026
75305b3
Add TMC2209 OTP hold current programming and verification
doudar Aug 13, 2026
49a14ea
Refactor stepper serial initialization and enhance TMC2209 connection…
doudar Aug 14, 2026
9a40800
Enhance TMC2209 connection recovery logic with interface counter vali…
doudar Aug 14, 2026
c248a2c
Update BLE connection parameters and enhance DirCon message parsing e…
doudar Aug 14, 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
18 changes: 16 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,30 @@ jobs:
- name: Install PlatformIO
run: python -m pip install platformio
- name: Pre-install PlatformIO packages (release env)
run: platformio pkg install -e release
run: |
platformio pkg install -e release
platformio pkg install -e S3release
- name: Check pre-commit hooks
uses: pre-commit/action@v3.0.1
- name: Build firmware
run: platformio run -e release
- name: Build filesystem
run: platformio run -e release --target buildfs
- name: Build S3 firmware
run: platformio run -e S3release
- name: Build S3 filesystem
run: platformio run -e S3release --target buildfs
- name: Create artifacts
run: |
7z a SmartSpin2kFirmware-${{ steps.date.outputs.date }}.bin.zip ./.pio/build/release/*.bin
cp ./.pio/build/release/firmware.bin firmware.bin
cp ./.pio/build/release/littlefs.bin littlefs.bin
cp ./.pio/build/release/partitions.bin partitions.bin
cp ./.pio/build/release/bootloader.bin bootloader.bin
cp ./.pio/build/S3release/S3firmware.bin S3firmware.bin
cp ./.pio/build/S3release/S3littlefs.bin S3littlefs.bin
cp ./.pio/build/S3release/S3partitions.bin S3partitions.bin
cp ./.pio/build/S3release/S3bootloader.bin S3bootloader.bin
7z a SmartSpin2kFirmware-${{ steps.date.outputs.date }}.bin.zip firmware.bin littlefs.bin partitions.bin bootloader.bin S3firmware.bin S3littlefs.bin S3partitions.bin S3bootloader.bin
- name: Archive
uses: actions/upload-artifact@v7
with:
Expand Down
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
.pio/
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
.vscode/extensions.json
.vscode/launch.json
.idea/
test/tmp_pio_test_transport.cpp
test/output/
Expand All @@ -21,6 +20,10 @@ C/Users/
/build
/managed_components
dependencies.lock
dependencies.lock.*
sdkconfig.S3release
sdkconfig.S3debug
.vscode/settings.json
*.map
graphify-out/

9 changes: 9 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"recommendations": [
"Jason2866.esp-decoder",
"pioarduino.pioarduino-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
6 changes: 0 additions & 6 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,5 @@
"VERSIONFILE",
"WEBSERVER"
],
"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/*"
],
}
31 changes: 26 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,30 @@ Primary software directories:
- `lib/SS2K/`: core sensor parsing library used by firmware and native tests.
- `lib/ArduinoCompat/`: native-test compatibility shims.
- `test/`: Unity tests for native PlatformIO environment.
- `data/`: web interface assets served by the firmware.
- `data/`: web interface assets for classic ESP32 filesystem images.
- `data_s3/`: ESP32-S3 filesystem assets; initially mirrors `data/` but may grow independently.
- `.github/copilot-instructions.md`: older agent/build notes that may still be useful.

## Build And Test

PlatformIO is the expected entry point.

- Build firmware: `pio run --environment release`
- Build ESP32-S3 firmware: `pio run --environment S3release`
- Build filesystem: `pio run --target buildfs`
- Run native tests: `pio test --environment native`
- Static analysis: `pio check -e debug`
- Pre-commit checks: `pre-commit run --all-files`

S3 firmware and filesystem builds use `S3firmware.bin` and `S3littlefs.bin` as their native PlatformIO output/upload names. They also create `S3partitions.bin` and `S3bootloader.bin` copies for releases; the generic partition and bootloader intermediates remain because PlatformIO's flash uploader depends on those names.
The GitHub release archive includes firmware, LittleFS, partition-table, and bootloader binaries for both classic ESP32 and ESP32-S3 targets.

Filesystem builds stage deterministic gzip copies of every HTML/CSS source file under the environment build directory. They also refresh the checked-in `.gz` companions and `list.json` in `data/` or `data_s3/`, which are consumed by repository-based automatic OTA updates.

Codex environment constraint:

- Do not run PlatformIO firmware or filesystem builds from the Codex environment. The Windows Xtensa toolchain can hang and leave orphaned compiler processes here. Make the requested changes, run non-build checks where useful, and clearly leave PlatformIO build validation for the user to run manually.

Important timing/network notes:

- First PlatformIO builds/tests may download ESP32 platforms and toolchains. They can take 15-45 minutes for firmware builds and 5-15 minutes for native tests.
Expand Down Expand Up @@ -75,7 +86,7 @@ Boot entry is `app_main()` in `src/Main.cpp`.
Boot sequence:

1. Initialize Arduino/Serial.
2. Detect hardware revision using `REV_PIN` and `boards.rev1/rev2`.
2. Detect hardware revision using the revision pin and ADC values in `include/boards.h`.
3. Start stepper serial and optional aux serial for Peloton.
4. Mount LittleFS.
5. Load and re-save `userConfig`.
Expand Down Expand Up @@ -227,7 +238,7 @@ Key functions:

- `SpinBLEClient::start()`: creates BLE client task and configures scanning.
- `ScanCallbacks::onResult()`: filters supported devices, updates `foundDevices`, sets slots to connect when user config matches.
- `SpinBLEClient::connectToServer()`: creates fresh NimBLE client, connects, sets slot state, removes duplicates.
- `SpinBLEClient::connectToServer()`: creates fresh BLE client, connects, sets slot state, removes duplicates.
- `subscribeToAllNotifications()`: subscribes to notify/indicate characteristics for supported services.
- `SpinBLEClient::postConnect()`: completes service subscriptions, reads FTMS resistance range, starts FTMS training where needed, drains notification queues.
- `SpinBLEClient::checkBLEReconnect()`: sets `doScan` when configured devices are missing.
Expand Down Expand Up @@ -264,7 +275,7 @@ Stateful parser caution:

Primary files: `src/BLE_Server.cpp`, `src/BLE_Fitness_Machine_Service.cpp`, service-specific `src/BLE_*_Service.cpp`.

`startBLEServer()` creates the NimBLE server and starts services:
`startBLEServer()` creates the BLE server and starts services:

- Cycling Speed/Cadence
- Cycling Power
Expand All @@ -274,6 +285,9 @@ Primary files: `src/BLE_Server.cpp`, `src/BLE_Fitness_Machine_Service.cpp`, serv
- Device Information
- BLE firmware update

The primary BLE advertisement carries the current WiFi IPv4 address in versioned SmartSpin2k manufacturer data.
The device name and 128-bit SmartSpin2k service UUID are kept in the scan response to stay within the legacy advertisement size limit.

Zwift/OpenBikeControl services exist but are currently commented out in regular BLE advertising/setup; DirCon and the source files still matter.

`SpinBLEServer::update()` refreshes wheel/crank revolution counters, then calls service `update()` methods. The FTMS service also processes pending writes.
Expand Down Expand Up @@ -302,6 +316,7 @@ Protocol:
- `cc_read` reads a variable.
- `cc_write` writes a variable.
- Responses generally start with `cc_success` or `cc_error`, followed by the variable id and bytes/string.
- Reading `BLE_allSettings` returns a versioned, MTU-sized sequence of indications whose payloads concatenate into the JSON from `userConfig->returnJSON()`.

The giant switch in `BLE_ss2kCustomCharacteristic::process()` maps variable IDs to `userConfig`, `rtConfig`, and `ss2k` fields. Examples:

Expand Down Expand Up @@ -469,6 +484,7 @@ DirCon exposes BLE-like services over TCP:
- Handles discover-services, discover-characteristics, read, write, enable-notifications, and unsolicited notification messages.
- Services register with `DirConManager::registerService()`.
- FTMS registers a write handler in `BLE_Fitness_Machine_Service::setupService()` so DirCon writes to the FTMS control point run the same control logic as BLE writes.
- The SmartSpin2k custom service registers a write handler so its request/response protocol also works over DirCon; changed-value notifications and chunked all-settings snapshots are mirrored over TCP.
- BLE server updates call `DirConManager::notifyCharacteristic()` so TCP clients receive corresponding updates.

DirCon uses static buffers and fixed client/subscription arrays. Be cautious with dynamic allocation and payload sizes.
Expand All @@ -482,6 +498,7 @@ Responsibilities:
- Start/stop WiFi (`startWifi()`, `stopWifi()`).
- Serve LittleFS web assets and built-in OTA pages.
- Firmware update flow through `HTTP_Server::FirmwareUpdate()`.
- Automatic filesystem updates treat remote `list.json` as an allowlist, preserve config/power-table/recovery metadata, and store the installed filesystem release version in NVS.
- Settings JSON/API behavior through `settingsProcessor()`.
- Periodic web client update through `webClientUpdate()`.
- BLE scanner page support.
Expand Down Expand Up @@ -510,11 +527,12 @@ Use `SS2K_LOG*` macros rather than raw `Serial.printf` unless matching nearby co
- Stepper power, speed, acceleration, travel, and driver tuning.
- ERG constants and compile-time feature flags.
- Power-table sizes, increments, and quality constants.
- Hardware pin assignments for board revisions.
- BLE timing, reconnect, stack, and buffer sizes.
- Peloton aux serial constants.
- Homing thresholds and sensitivity defaults.

Board-specific pin mappings, driver sense resistance, current scaling, revision detection values, homing capability, and homing-sensitivity scaling live in `include/boards.h`.

When changing behavior, prefer adjusting named constants instead of scattering magic numbers.

## Common Change Patterns
Expand Down Expand Up @@ -563,11 +581,14 @@ Changing BLE server characteristics:
- `Measurement` timestamps matter for ERG deduplication.
- `PowerTable` stores positions divided by `TABLE_DIVISOR`; lookup returns full-scale positions.
- `PowerTable` persistence requires homing.
- Saved BLE device identifiers are matched case-insensitively because NimBLE address formatting has changed between lowercase and uppercase across library versions.
- `SpinBLEAdvertisedDevice::reset()` updates global connected flags before clearing local flags.
- BLE address randomization is handled specially in `adevName2UniqueName()`; saved names depend on this behavior.
- `spinBLEServer.writeCache` is shared by BLE writes and DirCon writes.
- `spinDownFlag` is a state machine trigger, not just a bool: `1` means home/startup-ish, `2+` means full spindown/homing.
- `externalControl` bypasses normal target calculation but final state can still be affected by sync/clamping code.
- Firmware OTA paths validate the incoming `esp_image_header_t` chip ID before starting flash writes; filesystem images are intentionally exempt from application-image validation.
- Stepper UART initialization drives TX high for 20 ms before starting hardware UART. TMC connection checks track `IFCNT` across calls to confirm intervening writes were accepted; a failed UART test or unchanged counter restarts UART with one idle-high recovery pulse and aborts the requested setup/power update if that retry fails. Initial setup also checks `OTP_IHOLD`; if its two-bit field is unprogrammed, firmware irreversibly programs byte 2/bit 5 for the 9% standalone hold-current default, while incompatible existing OTP values are never modified.
- Many BLE and motor changes cannot be fully validated without hardware.

## Search Tips
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

### Changed
- Start the maintenance task directly from `app_main()` and finish initialization on its configurable stack, preventing HTTPS firmware checks from overflowing the ESP-IDF main task.
- Consolidated board pin mappings, revision detection, current scaling, homing capability, and driver sense-resistor values in `boards.h`.
- Added a board-specific homing-sensitivity scaler, making the ESP32-S3 default equivalent to 80 while revisions one and two remain at 50.
- Moved stepper homing tuning constants into `settings.h`.
- Restyled the compact built-in recovery, OTA, and login pages and added visible feedback for image validation, upload failures, and reboot completion.
- Added S3-prefixed PlatformIO build artifacts and updated the release workflow to package them directly.
- Gzip HTML and CSS assets during LittleFS builds, serve them with the correct encoding, and make automatic filesystem updates binary-safe.
- Automatic filesystem updates now remove files outside the server manifest while preserving settings and power-table data, track the installed filesystem release version, and never replace it with an older release.
- Successful LittleFS uploads from the recovery page now reboot after acknowledging the upload so the replacement filesystem is mounted cleanly.
- Fixed gzip web responses being labeled with duplicate content-encoding headers, which Firefox rejected.

### Hardware
- Corrected the ESP32-S3 shift-up and shift-down pin assignments.


## [26.7.15]

### Added
- Support for Spinning Bike Chrono

### Changed

### Hardware
Expand Down
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
add_compile_definitions(ARDUINO_ARCH_ESP32=1)
if(IDF_TARGET STREQUAL "esp32s3")
set(SDKCONFIG_DEFAULTS "sdkconfig.defaults;sdkconfig.s3.defaults")
endif()
# Keep component-manager locks target-specific so ESP32 and ESP32-S3 builds can
# coexist without rewriting one another's dependency target.
idf_build_set_property(DEPENDENCIES_LOCK dependencies.lock.${IDF_TARGET})
project(SmartSpin2k)
include_directories(
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/src
)
)
44 changes: 43 additions & 1 deletion CustomCharacteristic.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Custom Characteristic for userConfig Variable manipulation via BLE
SMARTSPIN2K_SERVICE_UUID "77776277-7877-7774-4466-896665500000"
SMARTSPIN2K_CHARACTERISTIC_UUID "77776277-7877-7774-4466-896665500001"

The same service and characteristic are published in the DirCon mDNS service.
A DirCon client sends the protocol bytes in a characteristic-write request and receives the custom-characteristic response bytes in that write response.
Subscribed DirCon clients also receive changed-value notifications.

The primary BLE advertisement includes the current Wi-Fi IPv4 address in manufacturer-specific data.
The device name and SmartSpin2k service UUID remain in the scan response. The payload is:

| Offset | Size | Meaning |
|--------|------|---------|
| 0 | 2 | Reserved development company identifier `0xFFFF`, little-endian |
| 2 | 2 | ASCII payload marker `SS` |
| 4 | 1 | Payload format version (`0x01`) |
| 5 | 4 | IPv4 address octets in network/display order |

An example follows to read/write 26.3kph to simulatedSpeed:

simulatedSpeed is a float and first needs to be converted to int by *10 for transmission, so convert 26.3kph to 263 (multiply by 10)
Expand Down Expand Up @@ -77,11 +91,39 @@ From BLE_common.h
|BLE_externalControl |0x1A |bool |01 disables internal calculation of targetPosition.|
|BLE_syncMode |0x1B |bool |01 stops motor movement for external calibration |
|BLE_UDPLogging |0x2E |bool |Enable/disable UDP log streaming |
|BLE_hardwareVersion |0x2F |str |Read-only detected hardware revision |
|BLE_BLELogging |0x30 |bool/str|Write: enable/disable BLE log streaming. Read: returns last log message|
|BLE_allSettings |0x31 |JSON |Read-only chunked snapshot of all user settings |

*syncMode will disable the movement of the stepper motor by forcing stepperPosition = targetPosition prior to the motor control. While this mode is enabled, it allows the client to set parameters like incline and shifterPosition without moving the motor from it's current position. Once the parameters are set, this mode should be turned back off and SS2K will resume normal operation.


This characteristic also notifies when a shift is preformed or the button is pressed.

See code for more references/info in BLE_Server.cpp starting on line 534
See code for more references/info in BLE_Server.cpp starting on line 534

Hardware-version example:

- Client writes: `0x01, 0x2F`
- An ESP32-S3 board indicates: `0x80, 0x2F`, followed by the ASCII bytes for `Revision Three (ESP32-S3)`.
- Writes to `0x2F` return `cc_error` because the detected hardware revision is read-only.

All-settings snapshot (BLE or DirCon):

- Client writes `0x01, 0x31`. BLE clients subscribe to indications on the custom characteristic.
A DirCon client receives the first chunk in the characteristic-write response and is automatically subscribed for the remaining chunks.
- The server serializes `userConfig->returnJSON()` once.
Over BLE, it sends MTU-sized indications sequentially and waits for each acknowledgement before sending the next.
- Over DirCon, chunks use the same framing and arrive as characteristic notifications after the first write-response chunk.
- Every snapshot chunk begins with this seven-byte header:

| Offset | Size | Meaning |
|--------|------|---------|
| 0 | 1 | `cc_success` (`0x80`) |
| 1 | 1 | `BLE_allSettings` (`0x31`) |
| 2 | 1 | Snapshot framing version (`0x01`) |
| 3 | 2 | Zero-based chunk number, little-endian |
| 5 | 2 | Total chunk count, little-endian |
| 7 | remainder | UTF-8 JSON bytes |

The client validates that it received chunks `0` through `chunk count - 1`, concatenates the bytes after each header, and parses the result as JSON. If the connection closes or a chunk is missing, discard the partial snapshot and issue the read command again. Unknown JSON properties should be ignored so newly added settings remain backward compatible. The snapshot includes sensitive settings such as the Wi-Fi password, consistent with the existing individual password read command.
Binary file added data/bluetoothscanner.html.gz
Binary file not shown.
Binary file added data/btsimulator.html.gz
Binary file not shown.
Binary file added data/develop.html.gz
Binary file not shown.
Binary file added data/index.html.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion data/list.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["bluetoothscanner.html", "btsimulator.html", "favicon.ico", "index.html", "settings.html", "shift.html", "status.html", "style.css", "streamfit.html", "develop.html"]
["bluetoothscanner.html.gz","btsimulator.html.gz","develop.html.gz","favicon.ico","index.html.gz","settings.html.gz","shift.html.gz","status.html.gz","streamfit.html.gz","style.css.gz"]
2 changes: 1 addition & 1 deletion data/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ <h2>Reset to Defaults?</h2>
type: 'slider',
title: 'Min Brake Watts',
tooltip: 'Bike power floor',
tooltipText: 'Set the minimum watts until stepper stops. 0 disables check.',
tooltipText: 'Minimum ERG target while unhomed. Homed bikes use the detected travel limit. 0 disables the check.',
min: 0, max: 200, step: 5,
defaultValue: 0,
unit: 'W'
Expand Down
Binary file added data/settings.html.gz
Binary file not shown.
Binary file added data/shift.html.gz
Binary file not shown.
Binary file added data/status.html.gz
Binary file not shown.
Binary file added data/streamfit.html.gz
Binary file not shown.
Binary file added data/style.css.gz
Binary file not shown.
Loading
Loading