File size: 5,675 Bytes
843a4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 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).