Spaces:
Running
Running
| # Reachy Mini Morse Code — plan | |
| ## Goal | |
| A shareable browser app (HF Space, JS, bare-HTML + CDN host shell, mirroring | |
| `marionette-js`) that lets a Reachy Mini **communicate in Morse code**: | |
| - The **robot** transmits by *hitting its antennas together* — each hit makes | |
| an audible click. | |
| - A **phone / laptop** transmits by *beeping* through its speaker. | |
| - Either device (or a second robot) **listens** through a microphone, detects | |
| the impulses, and decodes them back to text. | |
| - Two Reachy Minis face-to-face can therefore talk to each other. | |
| ## Why an "impulse" wire code (the key design decision) | |
| An antenna hit is **impulsive**: it produces one short click. You cannot make a | |
| click "longer" the way a tone can be held, so we **cannot** use the classic | |
| Morse discriminator (dot = short tone, dash = long tone) on the robot channel. | |
| To keep *every* channel interoperable through *one* detector (robot-clicks, | |
| phone-beeps, robot↔robot), the on-the-wire code is built from **onsets only** | |
| (the instant each sound starts), and dot vs dash is carried by **rhythm**: | |
| - **dot** = 1 impulse | |
| - **dash** = 2 impulses spaced by `dahGapMs` (a quick double-tap) | |
| - elements within a letter separated by `elemGapMs` | |
| - letters separated by `letterGapMs` | |
| - words separated by `wordGapMs` | |
| with `dahGapMs < elemGapMs < letterGapMs < wordGapMs` (well separated so the | |
| decoder can bin each inter-onset interval unambiguously). This is still real | |
| Morse semantically — the learn-chart shows the standard `.-` patterns — it is | |
| just transmitted as an impulse rhythm instead of tone length. The user | |
| explicitly framed Morse as "an equivalence between letters and rhythm", which | |
| is exactly this. | |
| Beeps use the *same* impulse code (short clicks) so a beeping phone and a | |
| tapping robot are mutually intelligible. (An "audible long-tone" cosmetic mode | |
| can be added later; it is not the machine-decodable wire format.) | |
| All timings live in one `timing.js` config with a single `unitMs` speed knob, | |
| so they can be **calibrated on real hardware** (click sharpness, room | |
| reverb, mic latency) without touching logic. | |
| ## Detection algorithm (ported from `marionette/tests/audio_analysis.py`) | |
| `detect_transient_onsets`, proven in the Marionette audio-sync tests: | |
| 1. High-pass filter (~2 kHz) to isolate sharp clicks from room rumble / voice. | |
| 2. Short-term energy in 5 ms windows. | |
| 3. Spectral flux = positive energy differences. | |
| 4. Normalise, peak-pick with a min-separation guard. | |
| Ported to the Web Audio API (`BiquadFilter` highpass + per-frame energy/flux in | |
| an `AudioWorklet`/`ScriptProcessor`) for live mic decoding, and to a pure-JS | |
| offline function reused verbatim in unit tests against synthetic audio. | |
| ## Architecture (bare-HTML + CDN host shell, like marionette-js) | |
| ``` | |
| index.html #root (host shell mount) + #app (in-iframe surface), CDN SDK pin | |
| main.js dispatcher: standalone mountHost() | embed connectToHost() | |
| style.css mobile-first, OKLCH palette, dark/light from handle.theme | |
| lib/ | |
| morse.js text <-> morse string ('.- ...') tables + encode/decode [pure, tested] | |
| timing.js unit/gap config + WPM-ish speed presets [pure, tested] | |
| wire.js morse <-> timed impulse schedule (emit) and onset-times | |
| -> morse decode (gap binning) [pure, tested] | |
| detector.js offline + streaming transient-onset detector (Web Audio) [DSP core tested] | |
| synth.js Web Audio click/beep emitter (plays a wire schedule) | |
| robot-tapper.js drives antennas to clap on schedule (setAntennasDeg) | |
| mic.js getUserMedia -> streaming detector -> onset times | |
| viz.js canvas waveform + live onset markers | |
| animation-helpers.js re-export SDK /animation (safelyReturnToPose) [stubbed in tests] | |
| views/ | |
| composer.js type text -> choose emitter (robot antennas | this device) -> send | |
| listener.js live mic visualization + decoded text | |
| learn.js Morse chart (letter <-> .- <-> rhythm), tap-to-hear | |
| topbar.js / settings.js speed + detector threshold + theme | |
| tests/unit/ morse, wire, detector (synthetic round-trip) — vitest | |
| public/icon.svg | |
| README.md HF Space frontmatter (sdk: static, hf_oauth, tags) | |
| ``` | |
| ## Robot antenna "clap" (from `marionette/tests/test_antenna_collision.py`) | |
| Right antenna held, left antenna slams in to contact and back — low-PID | |
| antennas make this safe and audible. In the JS SDK we drive it in real time | |
| with `setAntennasDeg(right, left)` on a timed schedule from `wire.js`. Exact | |
| contact angles + hold time get **calibrated on hardware**; defaults seeded from | |
| the Marionette recipe (right ≈ −39°, left rest 0° → slam ≈ +40°). | |
| ## What needs hardware (deferred — will ping Rémi) | |
| 1. **Mic + speaker calibration**: emit the impulse schedule from the Mac | |
| speaker, record with the Mac mic, confirm the detector recovers the onsets | |
| through-air and tune `unitMs` / thresholds / highpass. (Noise — needs Rémi.) | |
| 2. **Robot antenna-click test**: confirm the clap is audible + detectable and | |
| tune the contact angles / hold. (Needs the robot connected.) | |
| Everything else (codec, detector DSP, full UI, synth, robot-tapper wiring) is | |
| built and validated in software first (synthetic-audio round-trip, no | |
| mic/speaker/robot). | |
| ## Open questions (defaulted; will confirm with Rémi, non-blocking) | |
| - App/Space name: `morse-code` / "Reachy Mini Morse Code". Deploy to | |
| `RemiFabre/<name>` (user is logged in as RemiFabre, org pollen-robotics). | |
| - Default speed: ~`unitMs = 120 ms` (slow & robust) — final value from calibration. | |
| - Long-tone cosmetic beep mode: out of scope for v1 (impulse code only). | |