Skip to content

docs: the phone app, before the first line of it - #107

Draft
pierre-rouanet wants to merge 2 commits into
mainfrom
docs-mobile-app-approach
Draft

pierre-rouanet wants to merge 2 commits into
mainfrom
docs-mobile-app-approach

Conversation

@pierre-rouanet

@pierre-rouanet pierre-rouanet commented Aug 18, 2026

Copy link
Copy Markdown
Member

app-path-design.md covers the robot half of the app path and says almost nothing about the client. This adds docs/design/mobile-app.md for the other half, and links the two.

Rebased onto main and rewritten against what landed since August, then extended with what a short iPhone test and a read of the Reachy Mini source turned up.

The app stopped being four screens. route.rs gained the account device flow, the Hub policy catalogue with an install, the skill table, the pad bindings, robot.do, system.logs, and update.rollback / update.select. The arguments on those arms rhyme — ten metres of radio range means whoever tapped it is looking at the robot, and the bond is PIN-checked — so BLE turned out to be the transport that best answers who is watching. The app is the robot's interface now, not a settings utility.

BLE is now a choice, and still the right one. mediad ships, WebRTC is the default transport, the robot serves its own console. The original "there is no alternative" argument is dead; the §2.2 one is not — an app that needs the LAN cannot fix the setting keeping the robot off the LAN. The console needs an address, which means it needs a network, which means it needs whatever the app is for.

The protocol belongs in Rust. tauri-plugin-blec exposes the same handler to Rust as to JS, so the app can depend on duck-ipc-proto and btd::framing and drive the radio from the Rust side — which makes it unable to drift from the daemon. duckctl is now the proof: its own crate, depending on btd for framing, running on three desktop platforms because bluer sits behind cfg(target_os = "linux"). And the caveat — that dependency also pulls tokio, clap and tracing-subscriber in for one module. Where framing lives is a new open item, cheaper to settle before the second consumer than after.

Bluetooth is the permanent channel, not a setup step. Reachy Mini's BLE code runs once and hands over to the LAN; ours is where the robot's interface lives. So the app needs a session layer that reconnects and re-authenticates silently, and that layer has no counterpart to copy.

What the iPhone run and the reference source changed

iOS is not macOS. An iPhone against olducky with --require-pairing on puts up its pairing prompt, where CoreBluetooth on a laptop begins no SMP at all — so §5.5's hang is not general to the stack. The read was not confirmed before the run was stopped, and version read is debug! against a unit set to RUST_LOG=info, so its absence proved nothing either. §5.5 now records how far it got and what to set before resuming, rather than implying more than was seen.

Reachy Mini does not encrypt the link at all. Its characteristics are ["write"] and ["read", "notify"] — no encryption flag anywhere. It registers a NoInputNoOutput agent and never requires a bond, which is why it works on iOS. It reached the same dead end pairing.rs documents — a headless robot cannot do MITM-protected pairing — and answered it by sealing the passphrase above the link layer: ephemeral x25519, HKDF salted with the PIN, AES-GCM with the SSID as AAD.

So §8.1 has three candidate fixes where it assumed one: fix the link layer, seal the call, or seal the session. Two of them need no bond, which is what takes encryption off the app's critical path — it still gates shipping, it no longer gates building. The link stays open and --require-pairing stays off, deliberately and recorded.

One caveat is ours alone: the property that matters comes from the ephemeral ECDH, not from the PIN, so the PIN-as-salt buys nothing at 000000. And their per-robot PIN — the serial's last five characters, printed on the robot — works because their advertisement is anonymous. Ours carries a serial-derived name, so that trick is closed to us by a choice made for a better reason.

Two smaller things worth having written down: the vendored btleplug patch is a crash rather than a nicety (deviceplug/btleplug#397, macOS and iOS both, so duckctl is exposed too), and their public no-PIN PLAY_SOUND from the scan list is exactly the shape §8.2's identify needs.

Three rows moved off the "what the robot owes the app" table — telling robots apart (built, with IPv4 in the advertisement), version skew (decided), and going back from a bad release (routed).

#286 fixes the route.rs comment this turned up, which claimed an authenticated bond the just-works agent cannot produce.

Nothing is built.

🤖 Generated with Claude Code

`app-path-design.md` covers the robot half of the app path and says almost
nothing about the client. This adds `docs/design/mobile-app.md` for the other
half, and links the two.

The protocol belongs in Rust: `tauri-plugin-blec` exposes the same handler to
Rust as to JS, so the app can depend on `duck-ipc-proto` and `btd::framing` and
drive the radio from the Rust side, which makes it unable to drift from the
daemon. `duckctl` is the proof, and the caveat — depending on `btd` for one
module pulls tokio, clap and tracing-subscriber with it.

Bluetooth is the permanent channel, not a setup step. Reachy Mini's BLE code
runs once and hands over to the LAN; ours is where the robot's interface lives,
so the app needs a session layer that reconnects and re-authenticates silently.
That layer has no counterpart to copy.

A phone spike comes before any screen. §5.5 is the blocker and it is still a
fact about CoreBluetooth on a laptop.

Assisted-by: Claude:claude-opus-5
@pierre-rouanet
pierre-rouanet force-pushed the docs-mobile-app-approach branch from 19f5065 to 9aec85a Compare September 14, 2026 12:10
… not have

A short A/B on olducky, plus reading the client and the daemon Reachy Mini
actually ships.

iOS is not macOS. With pairing required, an iPhone puts up its pairing prompt,
where CoreBluetooth on a laptop begins no SMP at all — so the hang §5.5
measured is not general to the stack. The read was not confirmed before the run
was stopped, and `version read` is `debug!` against a unit set to `info`, so its
absence proved nothing either. §5.5 records how far it got and what to set
before resuming rather than implying more than was seen.

And the reference implementation does not encrypt the link at all. Reachy
Mini's characteristics carry no encryption flag; it registers a NoInputNoOutput
agent and never requires a bond, which is why it works on iOS. It reached the
same dead end `pairing.rs` documents — a headless robot cannot do MITM-protected
pairing — and answered it by sealing the passphrase above the link layer
instead.

So §8.1 has three candidate fixes where it assumed one, two of which need no
bond. The link stays open and `--require-pairing` stays off; the gate is
unchanged. The caveat that is ours alone: the property comes from the ephemeral
ECDH, not from the PIN, and their per-robot PIN trick is closed to us because
our advertisement is not anonymous.

Also in mobile-app.md: the vendored btleplug patch is a crash rather than a
nicety and duckctl is exposed to it too, and their public no-PIN `PLAY_SOUND` is
the shape §8.2's `identify` wants.

Assisted-by: Claude:claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant