diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 83c355a9..86417ded 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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:
diff --git a/.gitignore b/.gitignore
index aee06ddc..67e5264f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
@@ -21,6 +20,10 @@ C/Users/
/build
/managed_components
dependencies.lock
+dependencies.lock.*
+sdkconfig.S3release
+sdkconfig.S3debug
.vscode/settings.json
*.map
graphify-out/
+
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..b3974015
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ "recommendations": [
+ "Jason2866.esp-decoder",
+ "pioarduino.pioarduino-ide"
+ ],
+ "unwantedRecommendations": [
+ "ms-vscode.cpptools-extension-pack"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b42585b9..d5e3d756 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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/*"
- ],
}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 210867f4..7ecbebbd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,7 +22,8 @@ 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
@@ -30,11 +31,21 @@ Primary software directories:
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.
@@ -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`.
@@ -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.
@@ -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
@@ -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.
@@ -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:
@@ -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.
@@ -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.
@@ -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
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ef2aefe..e6bcda57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9a5f975e..0fc0d379 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
-)
\ No newline at end of file
+)
diff --git a/CustomCharacteristic.md b/CustomCharacteristic.md
index f6493477..20ee7342 100644
--- a/CustomCharacteristic.md
+++ b/CustomCharacteristic.md
@@ -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)
@@ -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
\ No newline at end of file
+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.
diff --git a/data/bluetoothscanner.html.gz b/data/bluetoothscanner.html.gz
new file mode 100644
index 00000000..e61de0e8
Binary files /dev/null and b/data/bluetoothscanner.html.gz differ
diff --git a/data/btsimulator.html.gz b/data/btsimulator.html.gz
new file mode 100644
index 00000000..c7cec439
Binary files /dev/null and b/data/btsimulator.html.gz differ
diff --git a/data/develop.html.gz b/data/develop.html.gz
new file mode 100644
index 00000000..178f7af8
Binary files /dev/null and b/data/develop.html.gz differ
diff --git a/data/index.html.gz b/data/index.html.gz
new file mode 100644
index 00000000..63766bfc
Binary files /dev/null and b/data/index.html.gz differ
diff --git a/data/list.json b/data/list.json
index fcb36ff5..d5643c83 100644
--- a/data/list.json
+++ b/data/list.json
@@ -1 +1 @@
-["bluetoothscanner.html", "btsimulator.html", "favicon.ico", "index.html", "settings.html", "shift.html", "status.html", "style.css", "streamfit.html", "develop.html"]
\ No newline at end of file
+["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"]
\ No newline at end of file
diff --git a/data/settings.html b/data/settings.html
index 836446cd..fea8c153 100644
--- a/data/settings.html
+++ b/data/settings.html
@@ -115,7 +115,7 @@
Reset to Defaults?
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'
diff --git a/data/settings.html.gz b/data/settings.html.gz
new file mode 100644
index 00000000..627bf40f
Binary files /dev/null and b/data/settings.html.gz differ
diff --git a/data/shift.html.gz b/data/shift.html.gz
new file mode 100644
index 00000000..0a6f70e8
Binary files /dev/null and b/data/shift.html.gz differ
diff --git a/data/status.html.gz b/data/status.html.gz
new file mode 100644
index 00000000..64541f5f
Binary files /dev/null and b/data/status.html.gz differ
diff --git a/data/streamfit.html.gz b/data/streamfit.html.gz
new file mode 100644
index 00000000..35035468
Binary files /dev/null and b/data/streamfit.html.gz differ
diff --git a/data/style.css.gz b/data/style.css.gz
new file mode 100644
index 00000000..d6e10276
Binary files /dev/null and b/data/style.css.gz differ
diff --git a/data_s3/bluetoothscanner.html b/data_s3/bluetoothscanner.html
new file mode 100644
index 00000000..4ea0e487
--- /dev/null
+++ b/data_s3/bluetoothscanner.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+ SmartSpin2k Bluetooth Scanner
+
+
+
+
+
+
+
+
+
+
Loading
+
Bluetooth Devices
+
+
+
+
Connected Devices
+
+
+ Power Meter
+ loading
+
+
+ Heart Monitor
+ loading
+
+
+ Remote
+ loading
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data_s3/bluetoothscanner.html.gz b/data_s3/bluetoothscanner.html.gz
new file mode 100644
index 00000000..e61de0e8
Binary files /dev/null and b/data_s3/bluetoothscanner.html.gz differ
diff --git a/data_s3/btsimulator.html b/data_s3/btsimulator.html
new file mode 100644
index 00000000..01c342d7
--- /dev/null
+++ b/data_s3/btsimulator.html
@@ -0,0 +1,293 @@
+
+
+
+
+
+ SmartSpin2k BLE Simulator
+
+
+
+
+