These notes are for coding agents working in this repository. They are intentionally focused on firmware/software behavior and ignore the physical design files in Hardware/ or hardware/.
Keep this file current while doing code work when it would help future agents: prune notes that become wrong, and add important discoveries. Many tasks will not need an AGENTS.md edit. Keep it concise and project-essential rather than comprehensive.
SmartSpin2k is ESP32 firmware for converting a spin bike into a BLE smart trainer. It:
- Reads local shifter buttons and optional Peloton aux serial data.
- Acts as a BLE client for power meters, cadence sensors, heart-rate monitors, FTMS trainers, HID remotes, Echelon, Flywheel, and Peloton-like data.
- Acts as a BLE server exposing cycling power, cycling speed/cadence, heart rate, FTMS, device info, firmware update, and SmartSpin2k custom characteristics.
- Serves a web UI from
data/over WiFi/LittleFS. - Runs a DirCon TCP bridge for BLE-like service discovery, reads, writes, and notifications over WiFi.
- Drives a TMC2209/FastAccelStepper motor to change resistance.
Primary software directories:
src/: main firmware modules.include/: headers, settings, UUIDs, BLE data structures, board pins.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..github/copilot-instructions.md: older agent/build notes that may still be useful.
PlatformIO is the expected entry point.
- Build firmware:
pio run --environment release - 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
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.
- In restricted environments, PlatformIO can fail on network downloads. If that happens, report it rather than trying to fake validation.
- The firmware itself cannot be fully run without ESP32 hardware, BLE devices, and a stepper driver.
Native tests cover sensor parsing, BLE device-name stability logic, power-buffer behavior, and power-table lookup/fill/save flows. When changing:
src/Power_Table.cpporsrc/PowerTable_Helpers.cpp, runpio test -e native.lib/SS2K/src/sensors/*, run the native tests for sensor parsing.- BLE/FTMS code, build firmware and reason carefully about characteristic payload formats; many paths need hardware/manual validation.
Formatting:
.clang-formatis based on Google style, keeps include order, uses 180 column limit, and aligns consecutive macros/assignments.- Existing C++ is mixed Arduino/ESP-IDF style. Prefer local patterns over broad refactors.
Most runtime state is global and initialized in src/Main.cpp:
SS2K* ss2k: top-level controller state and stepper/shifter behavior.userParameters* userConfig: persisted user settings loaded from LittleFS config.RuntimeParameters* rtConfig: live measurements, targets, mode, limits, and transient state.ErgMode* ergMode: ERG controller.PowerTable* powerTable: learned watts/cadence/position table.SpinBLEClient spinBLEClient: BLE client manager and connected sensor slots.SpinBLEServer spinBLEServer: BLE server manager and server write queue.Board currentBoard: selected at boot by analog hardware revision voltage.UdpAppender,WebSocketAppender,BleAppender: log sinks registered withLogHandler.
Because the firmware uses FreeRTOS tasks and callbacks, treat these globals as shared state. Many setters have side effects through timestamps, saved config, BLE notifications, or stepper behavior.
Boot entry is app_main() in src/Main.cpp.
Boot sequence:
- Initialize Arduino/Serial.
- Detect hardware revision using
REV_PINandboards.rev1/rev2. - Start stepper serial and optional aux serial for Peloton.
- Mount LittleFS.
- Load and re-save
userConfig. - Start WiFi and run firmware update check.
- Configure GPIO pins.
- Initialize LED state; commanded-reboot quiet mode uses RTC memory so true power cycles still show startup blink behavior.
- Configure TMC/FastAccelStepper via
SS2K::setupTMCStepperDriver(). - Register log appenders.
- Start BLE via
setupBLE(). - Start web server and DirCon.
- Create
SS2K::maintenanceLooptask.
SS2K::maintenanceLoop() is the main cooperative loop. It roughly does:
- Every
BLE_NOTIFY_DELAY:BLECommunications(), flush logs, websocket loop. - If not updating and not in spindown:
ss2k->moveStepper(),ss2k->FTMSModeShiftModifier(),ergMode->runERG(). - Periodically poll Peloton aux serial via
txSerial(). - Always handle local shifter button state.
- Notify changed custom-characteristic values via
BLE_ss2kCustomCharacteristic::parseNemit(). - Update HTTP clients and DirCon.
- Update LED status/diagnostics.
- Slow stepper near Peloton resistance limits when unhomed.
- Handle reboot/default-reset/save flags.
- Every roughly 6 seconds, log status and reboot after 30 minutes of inactivity.
Do not introduce long blocking work into maintenanceLoop() unless the existing code already does so for a hardware procedure like homing.
Defined in include/SmartSpin_parameters.h.
Fields:
simulate: whether this measurement is simulated rather than real sensor data.value: current measured/simulated value.target: requested target value.min,max: bounds used mostly for resistance ranges.timestamp: updated bysetSimulate(),setValue(), andsetTarget().
Used for rtConfig->watts, hr, cad, batt, and resistance.
Be careful: timestamp equality is used by ERG code to skip already processed watt samples. If adding setters or bypassing setters, update behavior can silently break.
Live state. Key fields:
targetIncline: internal target used by ERG, resistance mode, and simulation mode before becomingss2k->targetPosition.simulatedSpeed: optional speed from sensors or custom characteristic.FTMSMode: current mode/opcode from FTMS control point.shifterPosition: logical gear/shift count.homed: whether reliable min/max stepper positions are known.minStep,maxStep: movement limits for stepper target clamping.minResistance,maxResistance: external/real resistance bounds.simTargetWatts: custom-characteristic target-watts simulation flag.bleLogEnabled: allows BLE log access through custom characteristic.
rtConfig is the live truth for sensor measurements and control modes. BLE server notifications read from it. Stepper and ERG code write to it.
Persisted user config. Key fields:
- Firmware/device/WiFi: update URL, device name, auto update, SSID/password.
- Stepper tuning:
shiftStep,stepperPower,stepperSpeed,stepperDir,stealthChop,homingSensitivity. - Control tuning:
inclineMultiplier,powerCorrectionFactor,ERGSensitivity. - ERG/power table bounds:
minWatts,maxWatts,pTab4Pwr,hMin,hMax. - BLE preferences: connected power meter, heart monitor, remote, found devices.
- Logging: UDP logging enabled.
Persistence lives in src/SmartSpin_parameters.cpp with JSON serialization to configFILENAME in LittleFS. For new config fields, update defaults, JSON output, save, load, and custom characteristic handling if the app needs it.
Top-level controller in include/Main.h.
Private state:
- Button debounce and current button states.
lastShifterPosition: previous logical shift position for delta detection.- Scan delay state.
targetPositionandcurrentPosition: actual stepper positions.
Public flags:
stepperIsRunningexternalControl: bypasses normal target computation inmoveStepper().syncMode: forces current stepper position to match target.pelotonIsConnectedrebootFlag,saveFlag,resetDefaultsFlag,resetPowerTableFlag,isUpdating
Important functions:
maintenanceLoop(): main task.moveStepper(): computes/clamps/sends stepper target._resistanceMove(): converts resistance target into stepper target or fallback ERG target.FTMSModeShiftModifier(): remaps shifter changes depending on FTMS mode.handleShiftButtons(): debounced local button input.goHome(),_findEndStop(),_findFTMSHome(): homing procedures.setupTMCStepperDriver(),updateStepperPower(),updateStealthChop(),updateStepperSpeed(): motor driver configuration.txSerial(),rxSerial(),pelotonConnected(): Peloton aux serial integration.setLEDEnabled(),updateLED(): main LED state and diagnostic blink/pulse behavior.
The common path is:
- BLE client receives notification in
notifyCB()(src/BLE_Client.cpp). - Notification is enqueued into the matching
SpinBLEAdvertisedDevice. SpinBLEClient::postConnect()/client task drains queued data and callscollectAndSet().collectAndSet()(src/SensorCollector.cpp) usesSensorDataFactoryto decode bytes.- Decoded values update
rtConfigmeasurements unless simulation or config rules say to ignore them. ergMode->runERG()may updatertConfig->targetIncline.SS2K::moveStepper()turns mode/state intoss2k->targetPositionand callsstepper->moveTo().- BLE server services read
rtConfigand notify connected apps.
Important gates in collectAndSet():
- Heart rate zeros are ignored until 10 consecutive zero-ish readings.
- Cadence accepts 1-249 RPM, otherwise sets cadence to 0.
- Power is multiplied by
userConfig->getPowerCorrectionFactor()and accepted from 1-2999 W. - Peloton cadence/power can be ignored when an external BLE power meter is configured.
- If
userConfig->getPTab4Pwr()is true, real sensor power does not overwrite watts because watts are derived from the power table. - IC Bike resistance is blacklisted due to non-standard behavior.
- Real resistance clears
rtConfig->resistance.simulate.
Primary files: include/BLE_Common.h, src/BLE_Client.cpp, src/BLE_Common.cpp.
SpinBLEClient owns:
- Connection flags:
connectedPM,connectedHRM,connectedCD,connectedCT,connectedSpeed,connectedRemote. - Scan flag:
doScan. - CSC cumulative values reused by server-side CSC/Cycling Power notifications.
myBLEDevices[NUM_BLE_DEVICES]: slots for connected/discovered devices.
SpinBLEAdvertisedDevice stores:
advertisedDevice,peerAddress,uniqueName.connectedClientID, service/characteristic UUIDs.- Type flags: HRM, PM, CSC, CT, remote.
doConnect,isPostConnected,lastDataUpdateTime.- FreeRTOS queue for notification payloads.
Key functions:
SpinBLEClient::start(): creates BLE client task and configures scanning.ScanCallbacks::onResult(): filters supported devices, updatesfoundDevices, sets slots to connect when user config matches.SpinBLEClient::connectToServer(): creates fresh NimBLE 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(): setsdoScanwhen configured devices are missing.SpinBLEClient::adevName2UniqueName(): stable names for saved device preferences. Public/static random addresses get address suffix; private random addresses prefer manufacturer-data suffix or base name.
BLEServices::SUPPORTED_SERVICES maps service UUIDs to the characteristic UUIDs this firmware expects. If adding sensor support, update this list, SensorDataFactory, and tests if parsing is involved.
Primary files: lib/SS2K/include/sensors/*, lib/SS2K/src/sensors/*.
SensorData is the abstract interface:
- Capability flags:
hasHeartRate(),hasCadence(),hasPower(),hasSpeed(),hasResistance(). - Getters return real values or sentinel values (
INT_MIN,nanf("")) when absent. decode(uint8_t* data, size_t length)stores parsed fields.
SensorDataFactory caches parser instances by (characteristicUUID, uniqueName) so stateful parsers keep previous cumulative counters. It returns:
CyclePowerDatafor Cycling Power Measurement.HeartRateDatafor Heart Rate.FitnessMachineIndoorBikeDatafor FTMS Indoor Bike Data.FlywheelDatafor Flywheel UART.EchelonDatafor Echelon data.PelotonDatafor Peloton aux serial payloads.CscSensorDatafor Cycling Speed/Cadence.NullDatafor unknown characteristic UUIDs.
Stateful parser caution:
- Cycling power and CSC cadence/speed require previous cumulative revolution/event-time state. Do not replace cached objects with one-shot parsing unless you account for that.
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:
- Cycling Speed/Cadence
- Cycling Power
- Heart
- Fitness Machine
- SmartSpin2k custom characteristic
- Device Information
- BLE firmware update
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.
MyCharacteristicCallbacks::onWrite() queues FTMS control-point writes in spinBLEServer.writeCache. BLE_Fitness_Machine_Service::processFTMSWrite() consumes that queue.
FTMS control point behavior:
RequestControl: success, clears watt target and simulated target watts.Reset: success, status reset/idle.SetTargetInclination: sets FTMS mode andrtConfig->targetIncline.SetTargetResistanceLevel: sets FTMS mode andrtConfig->resistance.target, clamping to min/max.SetTargetPower: sets FTMS mode, setsrtConfig->watts.target, optionally forwards corrected target to a connected FTMS trainer.SetIndoorBikeSimulationParameters: sets simulation mode incline and forwards to connected FTMS device.SpinDownControl: setsspinBLEServer.spinDownFlag = 2; BLE client task later callsss2k->goHome(true)once cadence is present.StartOrResume,StopOrPause,SetTargetedCadence: update FTMS status/training status.
BLE_Fitness_Machine_Service::update() sends Indoor Bike Data from rtConfig and notifies DirCon. It computes speed from rtConfig->simulatedSpeed if available, otherwise from server power-based speed estimate. If resistance is not recently real, it simulates resistance from stepper position.
Primary files: include/BLE_Custom_Characteristic.h, src/BLE_Custom_Characteristic.cpp, CustomCharacteristic.md.
Protocol:
cc_readreads a variable.cc_writewrites a variable.- Responses generally start with
cc_successorcc_error, followed by the variable id and bytes/string.
The giant switch in BLE_ss2kCustomCharacteristic::process() maps variable IDs to userConfig, rtConfig, and ss2k fields. Examples:
- Firmware URL, device name, WiFi SSID/password.
- Simulated watts/cadence/heart rate/speed.
- Simulate flags.
- FTMS mode and target watts.
- Shift step, shifter position, min/max brake watts.
- Stepper power/speed/direction/stealthChop.
- External control and sync mode.
- Save/reboot/reset flags.
- Power table row transfer.
- Homing min/max/sensitivity.
pTab4Pwr, UDP logging, BLE logging.
parseNemit() compares current config/runtime values against static old copies and sends one notification per call for changed values. Some changes, like hMin/hMax, trigger userConfig->saveToLittleFS(). Turning pTab4Pwr on sets spinBLEServer.spinDownFlag = 1 to trigger homing.
When adding a custom characteristic variable:
- Add/confirm the ID in
include/BLE_Custom_Characteristic.h. - Update
process()read/write behavior. - Update
parseNemit()if clients need change notifications. - Update
CustomCharacteristic.md. - Preserve byte order conventions used nearby.
Primary files: include/Stepper.h, src/Stepper.cpp.
Globals:
HardwareSerial stepperSerial(2)TMC2209Stepper driverFastAccelStepperEngine engineFastAccelStepper* stepper
SS2K::moveStepper() is the key function:
- Updates
ss2k->stepperIsRunningandss2k->currentPosition. - If
externalControlis false:- ERG mode (
SetTargetPower):targetPosition = rtConfig->targetIncline, with optional guardrails to avoid moving opposite the watt error. - Resistance mode (
SetTargetResistanceLevel): calls_resistanceMove(). - Simulation mode: target is
shifterPosition * shiftStep + targetIncline * inclineMultiplier.
- ERG mode (
- If
syncMode, stops movement and sets current stepper position to target. - Applies Peloton/resistance safety nudges and min/max step clamps.
- Calls
stepper->moveTo(targetPosition). - Enables outputs only when cadence is present; otherwise auto-enable is used.
- Detects runtime stepper direction changes and updates the direction pin after current motion stops.
_resistanceMove() has two modes:
- Simulated resistance: maps 0-100 percent target resistance into known min/max step range. If no reliable range exists, it falls back by setting
watts.targetand switching to ERG mode. - Real resistance: compares
resistance.targetandresistance.value, then incrementstargetInclineusing larger moves for big errors and smaller moves for near-target corrections.
Homing:
goHome(false)finds minimum/home only. Startup can use this.goHome(true)performs a full spindown/homing and saveshMin/hMax.- If a real FTMS resistance-reporting device is connected,
_findFTMSHome()homes by driving to reported min/max resistance. - Otherwise
_findEndStop()uses TMC StallGuard, with repeated taps and drift detection. - Homing aborts when shifter position changes.
- Homing changes driver current/speed/StealthChop and must restore normal driver setup.
Stepper safety:
- Homed devices clamp to
rtConfig->minStep/maxStep. - Unhomed devices use provisional defaults unless power-table/resistance updates refine limits.
- Do not bypass
moveStepper()target clamping for ordinary control paths.
Primary files: include/ERG_Mode.h, src/ERG_Mode.cpp.
ErgMode::runERG() is called from the main maintenance loop. It:
- Waits for delayed stepper movement after large power-table seeks.
- Saves power table after delayed
saveFlag. - Loads power table once per session.
- Adds live power/cadence/position samples to the power table when cadence exists and
pTab4Pwris false. - Calls
computeErg()when FTMS mode is target power and a power meter or simulation is active. - Periodically updates stepper min/max from power table.
- Handles power-table reset flag.
- If
pTab4Pwris true, estimates watts from cadence and current stepper position usingPowerTable::lookupWatts().
computeErg():
- Stops ERG and switches back to simulation mode if cadence is below
MIN_ERG_CADENCE. - Raises target to
userConfig->minWattswhen apps request too little. - Skips if the same watt timestamp/target was already processed or current watts are negative.
- For large setpoint changes, tries
_setPointChangeState()using the power table when homed. - Falls back to
_inSetpointState()proportional control. - Writes the new target to
rtConfig->targetIncline.
_setPointChangeState():
- Chooses increasing/decreasing mode.
- Looks up a position near the target watts/cadence with a PID window offset.
- Rejects table results that move the wrong way or become negative while homed.
- Adds delay based on step distance and configured stepper speed.
_inSetpointState():
- Uses proportional-only control with
ERGSensitivity. - Scales gain by watt error size.
- Caps movement by stepper speed and
ERG_MODE_DELAY.
Primary files: include/Power_Table.h, include/PowerTable_Helpers.h, src/Power_Table.cpp, src/PowerTable_Helpers.cpp.
Purpose:
- Learn mapping between watts, cadence, and stepper target position.
- Use that mapping for ERG setpoint jumps and optional power estimation (
pTab4Pwr). - Infer min/max stepper limits when not using homing or real resistance feedback.
Data structures:
PowerEntry: raw sample with watts, cadence, target position, resistance, reading count.PowerBuffer: fixedPOWER_SAMPLESsample buffer used before committing a table entry.TableEntry: compact stored table cell:int16_t targetPosition,int8_t readings.PTData:POWERTABLE_CAD_SIZExPOWERTABLE_WATT_SIZEtable.ResistanceModel: regression model predicting position from watts/cadence and inverse watts from position/cadence.PTHelpers: indexing, lookup, cleaning, interpolation, monotonic enforcement, and model fitting.
Table dimensions/constants are in include/settings.h:
- Watts increment:
POWERTABLE_WATT_INCREMENT. - Cadence starts at
MINIMUM_TABLE_CAD, incrementPOWERTABLE_CAD_INCREMENT. - Positions are divided by
TABLE_DIVISORwhen stored to save memory. INT16_MINmarks empty table positions.readings == 1means inferred/low-confidence; human/real readings are generally2+.
Flow:
PowerTable::processPowerValue()accepts sane cadence/watts and stable position samples.- A full
PowerBufferis averaged inPowerTable::newEntry(). PTHelpers::calculateIndex()maps watts/cadence to table indexes.PTHelpers::enterData()averages the cell, rejects monotonic violations, fills gaps, runs PAVA-style monotonic correction, fitsResistanceModel, and fills inferred cells.lookup()uses the model to predict a target position and multiplies byTABLE_DIVISOR.lookupWatts()inverses the model forpTab4Pwr.
Persistence:
_manageSaveState()loads/savesPOWER_TABLE_FILENAME.- Saving/loading requires
rtConfig->homed. - File format starts with
TABLE_VERSION, saved reading quality, and saved homed state, then table entries. _save()refuses to save with no valid readings.reset()clears table, resets homing settings, and rewrites/attempts save.
Caution:
ResistanceModel::fit()uses only entries withreadings >= 2.- Inferred cells can improve lookup smoothness but should not be treated as real measurements.
- Power-table and homing logic are tightly linked now. Loading/saving without homing is intentionally blocked.
Primary files: include/DirConManager.h, src/DirConManager.cpp, include/DirConMessage.h, src/DirConMessage.cpp.
DirCon exposes BLE-like services over TCP:
- Starts a WiFi server on
DIRCON_TCP_PORT. - Publishes MDNS service and BLE service UUID TXT records.
- 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. - 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.
Primary files: src/HTTP_Server_Basic.cpp, include/HTTP_Server_Basic.h, data/*.
Responsibilities:
- Start/stop WiFi (
startWifi(),stopWifi()). - Serve LittleFS web assets and built-in OTA pages.
- Firmware update flow through
HTTP_Server::FirmwareUpdate(). - Settings JSON/API behavior through
settingsProcessor(). - Periodic web client update through
webClientUpdate(). - BLE scanner page support.
Web UI changes usually need matching firmware handlers when adding settings. Config fields are not automatically exposed unless settingsProcessor() and/or the custom BLE characteristic know about them.
Primary files: src/SS2KLog.cpp, include/SS2KLog.h, src/*Appender.cpp.
Use SS2K_LOG* macros rather than raw Serial.printf unless matching nearby code or during very low-level diagnostics. Log sinks:
- Serial/log handler.
- UDP when enabled.
- WebSocket.
- BLE custom logging through
BleAppender.
DEBUG_BLE_TX_RX, CUSTOM_CHAR_DEBUG, DEBUG_POWERTABLE, DEBUG_DIRCON_MESSAGES, and DEBUG_STACK in settings.h unlock extra diagnostics. Be aware that verbose BLE/Peloton logging can be noisy or timing-sensitive.
include/settings.h is central. It contains:
- Firmware update URLs and LittleFS filenames.
- Device/WiFi defaults.
- 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.
When changing behavior, prefer adjusting named constants instead of scattering magic numbers.
Adding a new sensor parser:
- Add
SensorDatasubclass inlib/SS2K/include/sensorsandlib/SS2K/src/sensors. - Add service/characteristic UUIDs to
include/Constants.horinclude/BLE_Definitions.has appropriate. - Add the service/characteristic to
BLEServices::SUPPORTED_SERVICES. - Add factory branch in
SensorDataFactory::getSensorData(). - Add native tests for parser behavior.
- Make sure
collectAndSet()accepts or intentionally ignores the new values.
Adding a persisted setting:
- Add field/getter/setter/default in
userParameters. - Update
setDefaults(),returnJSON(),saveToLittleFS(),loadFromLittleFS(). - Add custom characteristic read/write/notify if the app controls it.
- Add HTTP settings support if the web UI controls it.
- Consider whether a live change needs immediate side effects, like updating stepper speed/power.
Changing ERG or resistance behavior:
- Trace the relevant FTMS opcode in
processFTMSWrite(). - Follow how
rtConfigfields are updated. - Check
FTMSModeShiftModifier()for shifter remapping side effects. - Check
ErgMode::runERG()andcomputeErg(). - Check final clamps in
SS2K::moveStepper(). - Run native tests and note any hardware validation needed.
Changing BLE server characteristics:
- Update the service setup.
- Update callback/write handling.
- Update DirCon registration/notification if TCP clients should see it.
- Update
CustomCharacteristic.mdor relevant docs. - Keep BLE packet sizes and little-endian encoding consistent.
Hardware/is physical design material and should be ignored for firmware/software analysis unless explicitly requested.- Some paths and folder names differ by case (
Hardwarevshardware); use both excludes in searches. compile_commands.json,.pio/,managed_components/, map files, and generated logs are large/noisy.- LED behavior is owned by
SS2K/src/Main.cpp, not BLE common code. Its reboot inhibit flag isRTC_DATA_ATTR, which should surviveESP.restart()but clear on power loss. SensorDataFactoryintentionally caches parser objects per unique device/characteristic.Measurementtimestamps matter for ERG deduplication.PowerTablestores positions divided byTABLE_DIVISOR; lookup returns full-scale positions.PowerTablepersistence requires homing.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.writeCacheis shared by BLE writes and DirCon writes.spinDownFlagis a state machine trigger, not just a bool:1means home/startup-ish,2+means full spindown/homing.externalControlbypasses normal target calculation but final state can still be affected by sync/clamping code.- Many BLE and motor changes cannot be fully validated without hardware.
Useful commands:
- List software files:
rg --files -g '!Hardware/**' -g '!hardware/**' - Find symbols:
rg -n "symbolName" src include lib/SS2K test - Find BLE UUID use:
rg -n "UUID|SERVICE|CHARACTERISTIC" include src lib/SS2K - Find config variables:
rg -n "getName|setName|BLE_name|jsonKey" include src data - Find FTMS behavior:
rg -n "FitnessMachineControlPointProcedure|SetTargetPower|SetIndoorBikeSimulationParameters" src include
Prefer rg over slower recursive tools.