# 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/` (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).