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).