No description
  • TypeScript 79.2%
  • JavaScript 12.3%
  • CSS 4.6%
  • HTML 1.9%
  • Python 1.9%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Viscid e8a9e0a4ba Room light: progress bar and heart-zone gauge modes
Two new modes for the cove light, both leaving the white lamp as it is
and using the 120 colour pixels as a bar:

- Ride progress: the bar fills with the workout clock (reported once a
  second with the ping). Shapes fill / glow / countdown; painted in the
  lamp colour, ember, ocean or a spectrum revealed along the strip
  (default ember). A dark lamp draws the bar in a dim ember.
- Heart zones: the strip is the five zones as a spectrum, Z1 blue to Z5
  red, a fifth each, and the bar reaches to the live heart rate, so the
  colour it settles at is the zone. Gauges: rainbow bar, zone bar,
  needle. Runs whenever a heart rate is live.

A shared "white lamp during the bar" slider dims the lamp for either
mode. The Settings card shows only the selected mode's rows.

Runner: a bar value eases toward each report (500 ms, with a linear
floor so the head pixel saturates), holds through the fade-out, and
every fade-out now ends with one frame at exactly the lamp. Demos ramp
ahead of the easing so the preview reaches the far end.

Verified by unit tests (bar renderers, gauge mapping, runner easing)
and scripts/verify-lights.mjs against the fake WLED, which now also
records the far pixel and the lit count.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 20:30:26 -04:00
.claude/skills/debrief Debrief skill: the send-off drops the stairs/hills imagery 2026-09-18 00:11:40 -04:00
assets Start-menu app: npm run install-shortcut; npm start works on every platform 2026-08-25 20:51:06 -04:00
docs Results: compare heart rate at watts, as time contours 2026-09-18 00:11:40 -04:00
electron Start-menu app: npm run install-shortcut; npm start works on every platform 2026-08-25 20:51:06 -04:00
relay Room meter: SwitchBot readings via the relay, an Environment tab, room line in the debrief 2026-09-02 00:57:59 -04:00
scripts Room light: progress bar and heart-zone gauge modes 2026-09-19 20:30:26 -04:00
server Room light: progress bar and heart-zone gauge modes 2026-09-19 20:30:26 -04:00
src Room light: progress bar and heart-zone gauge modes 2026-09-19 20:30:26 -04:00
.gitignore Weekly riding time is a setting (riding h/week, 2-6, default 4) 2026-08-19 12:13:14 -04:00
index.html Room light: progress bar and heart-zone gauge modes 2026-09-19 20:30:26 -04:00
package-lock.json Room light: the cove light pulses with the ride over WLED 2026-09-18 00:11:40 -04:00
package.json Room light: the cove light pulses with the ride over WLED 2026-09-18 00:11:40 -04:00
README.md Room light: progress bar and heart-zone gauge modes 2026-09-19 20:30:26 -04:00
spike.html BikeBeater: cadence-coaching app for the Rogue Echo Bike 2026-08-16 19:21:33 -04:00
tsconfig.json BikeBeater: cadence-coaching app for the Rogue Echo Bike 2026-08-16 19:21:33 -04:00
vite.config.ts Room light: the cove light pulses with the ride over WLED 2026-09-18 00:11:40 -04:00

BikeBeater

Ride your Rogue Echo Bike (V3 connected console) to a program by ear: the app plays a metronome — one beep per pedal revolution — at the tempo that produces the target console RPM, and distinct audio cues when your actual cadence drifts outside tolerance. The console broadcasts live cadence/power over Bluetooth FTMS; the app compares what you're doing to what the program asks and nudges you back.

A one-time rhythm calibration (Sound card → Calibrate rhythm) maps beep tempo to console RPM: the console reports flywheel RPM, which is belt-geared faster than the cranks by an unpublished ratio, so the mapping must be measured, not assumed. You pedal one revolution per beep through three tempos (~90 s); the median transmitted cadence per tempo over the beat rate gives the gear ratio, cross-checked for consistency and snapped to a simple mapping when it is one (src/engine/calibrate.ts). Until calibrated, the app assumes 1:1 and the metronome will feel wrong — calibrate first.

Run

npm install
npm start          # server + desktop overlay, one terminal; close the overlay to stop both

npm start starts the Vite dev server (with the SQLite API) if nothing is on :5173, waits for it, then launches the overlay; a server already running is reused. For the pieces separately: npm run dev (server) and npm run app (overlay).

npm run install-shortcut puts BikeBeater in the Start menu (Windows / WSL — pin it to the taskbar from there) or in the app launcher (Linux); one click runs npm start. The repo is the app: a code change is live on the next launch, nothing to rebuild or reinstall.

Platforms: on macOS, Linux and native Windows the overlay is Electron from node_modules. Under WSL the overlay must run on the Windows side (a transparent window doesn't survive WSLg), so install the Windows Electron build into WSL — npm_config_platform=win32 npm install — and npm start launches it through scripts/app.sh while the server stays in WSL.

Or open http://localhost:5173 in Chrome or Edge on the Windows side (Web Bluetooth doesn't exist in WSL, and localhost forwarding makes the WSL dev server reachable from Windows). Press the console's Bluetooth/connect state if it needs one, click Connect Echo bike, pick a program, Start ride.

  • ?demo=1 — desk demo: fake bike + short program, straight to the ride screen (click the sound chip once; browsers block audio until a gesture).

Hardware spike (do this first on the real bike)

http://localhost:5173/spike.html — connect, pedal a few minutes including sprints and a full stop, Export JSON. The export answers the open hardware questions (real notify rate/jitter, flag layout, 0-RPM behavior, reconnect quirks) and becomes test fixtures in src/fixtures/.

Facts the design leans on

  • The Echo's cadence is genuinely measured (reed switch on the flywheel) but the number is flywheel RPM, not crank RPM — fixed belt gearing links them by a constant, unpublished ratio (confirmed on-bike: a 1-beep-per- transmitted-RPM metronome is audibly out of sync with the pedals). Watts are derived from RPM via a fan curve — console-relative, not calibrated power. Program targets and drift detection stay in console units; only the metronome tempo is divided by the calibrated ratio.
  • Because watts are a function of RPM, programs target cadence; expected watts are shown as an estimate once the app has passively learned enough (rpm, watts) pairs (src/engine/powermodel.ts, localStorage).
  • FTMS Indoor Bike Data arrives ~1 Hz as a scalar — beeps set the tempo but cannot phase-lock to crank position.
  • Drift alerting is a state machine (src/engine/drift.ts): EMA smoothing, outer/inner hysteresis bands, dwell times, re-alert cooldown, grace period after interval changes, and separate "stopped" and "stale" states.

Room light

The bedroom cove light (WLED, ~/Projects/roomlight) can pulse with the ride: a red heartbeat swell on every strap beat, or a soft breath at the pedalling cadence. Or it can be a progress bar: the white lamp stays as it is and the colour strip fills with the workout clock (fill from dark, glow from a quarter, or a countdown that empties), painted in the lamp colour, ember, ocean or a spectrum. Or a heart-zone gauge: the strip is the five zones as a spectrum (Z1 blue to Z5 red, a fifth each) and the bar reaches to the live heart rate, so the colour it settles at is the zone (rainbow bar, zone bar, or a needle). Settings tab → Room light: mode (off / auto / heartbeat / cadence / ride progress / heart zones), the WLED address as an IP (lights.local does not resolve inside WSL), and a shape picker that previews each shape on the card and on the strip for eight seconds.

How it works: the browser decides the mode from what is live and sends tiny messages (beat, pedal period, bar value, mode) to the app server over /lights; server/lights.ts draws the strip at 40 fps and sends each frame to WLED as DDP over UDP. The app never writes the lamp's own settings — it reads the current level and overlays frames around it; when frames stop for any reason (mode off, window closed, server dead) WLED restores its preset within 2.5 s. The pulse maths and the state machine are pure and tested (src/engine/lights.ts, src/engine/lightrunner.ts); node scripts/verify-lights.mjs drives the whole path against a fake WLED.

Tests

npm test

Covers FTMS frame parsing, the drift state machine (alert/clear/grace/ stopped/stale/overshoot timing), and program stepping. Recorded rides (Download recording / spike export) can be replayed through the detector to tune thresholds without re-riding.