- TypeScript 79.2%
- JavaScript 12.3%
- CSS 4.6%
- HTML 1.9%
- Python 1.9%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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]> |
||
| .claude/skills/debrief | ||
| assets | ||
| docs | ||
| electron | ||
| relay | ||
| scripts | ||
| server | ||
| src | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| spike.html | ||
| tsconfig.json | ||
| vite.config.ts | ||
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.