morse-code / plan.md
RemiFabre
feat: initial Reachy Mini Morse Code app
843a4b2
|
Raw
History Blame Contribute Delete
5.68 kB
# 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).