Spaces:
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:
- High-pass filter (~2 kHz) to isolate sharp clicks from room rumble / voice.
- Short-term energy in 5 ms windows.
- Spectral flux = positive energy differences.
- 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)
- 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.) - 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 toRemiFabre/<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).