Skip to content

draft: mobile app daemon integration (dev/mobile-app-integration)#1070

Draft
tfrere wants to merge 38 commits into
mainfrom
dev/mobile-app-integration
Draft

draft: mobile app daemon integration (dev/mobile-app-integration)#1070
tfrere wants to merge 38 commits into
mainfrom
dev/mobile-app-integration

Conversation

@tfrere
Copy link
Copy Markdown
Contributor

@tfrere tfrere commented May 4, 2026

Summary

Draft integration branch for the Reachy Mini mobile app daemon work. This is the line we use day-to-day locally and on robots; it supersedes the older umbrella branch integration/mobile-app-daemon in breadth (extra signaling, health, BLE TLV, central URL default, tray-on-central, etc.).

Not intended for merge as-is without review split — this PR exists so Pollen can see the full diff, run CI, and decide how to land it (cherry-picks vs smaller PRs).

Scope (high level)

  • WebRTC DataChannel http_proxy / ws_proxy for unified transport
  • HF auth: token bridging, refresh-relay, tray + lite registering on central
  • BLE: Wi-Fi provisioning commands, WIFI_PROBE, TLV advert (install_id, central_peer_id, network_mode), pairing disabled on adapter
  • Central signaling: heartbeat from welcome, producer meta / peer health, default REACHY_CENTRAL_URL alignment
  • Stable install_id, /api/daemon/identity, macOS GStreamer DeviceMonitor guard
  • Docs: docs/known-issues/libnice-session-reuse-crash.md

Related

Notes

  • Branch: dev/mobile-app-integration (pushed on origin).
  • Rebase onto main will be needed before any non-draft merge (branch is behind main by some commits).

Made with Cursor

tfrere and others added 30 commits April 15, 2026 14:58
…rerData

Allow BLE scanners to discover the robot's IP address(es) without
establishing a GATT connection. The advertisement payload encodes each
non-loopback IPv4 as 5 bytes (1 flag byte + 4 IP bytes), refreshed
every 10 seconds alongside the existing GATT network characteristic.

This enables passive robot discovery via BLE scan, which is useful as
a fallback when mDNS/WiFi discovery is unreliable.

Made-with: Cursor
Expose four new BLE commands so a mobile client can provision WiFi
over Bluetooth, even before the robot is on the network:

  WIFI_STATUS                 -> {"mode","connected","known","error"}
  WIFI_SCAN                   -> ["SSID1", "SSID2", ...]
  WIFI_CONNECT {"ssid","psk"} -> "OK: Connecting to <ssid>"
  WIFI_FORGET <ssid>          -> "OK: Forgotten <ssid>"

All four commands simply proxy to the existing /wifi/* FastAPI routes
over localhost HTTP. This reuses the daemon's `busy_lock`, threading
and automatic hotspot fallback instead of duplicating `nmcli` logic in
the Bluetooth service.

Mutating commands (SCAN, CONNECT, FORGET) require prior PIN auth; like
today for CMD_*. Unlike CMD_* we do NOT reset `self.connected` after
each WIFI_* command so a client can chain scan -> connect -> poll in a
single provisioning session.

WIFI_STATUS is readable without authentication (same rationale as the
public NETWORK_STATUS characteristic) so the UI can surface the
current state during the first-time setup flow.

Implementation notes:
- Uses `urllib` from the standard library: the Bluetooth service runs
  out of the system Python (not the daemon venv).
- Responses are kept compact (short JSON keys, SSID list capped at 12)
  so a single BLE MTU is enough in practice.
- WIFI_CONNECT is fire-and-forget; clients poll WIFI_STATUS to observe
  the outcome (which also reports nmcli errors via the /wifi/error
  relay).
- WIFI_STATUS is excluded from the command/response log filter since
  clients are expected to poll it.

Made-with: Cursor
Adds `GET /api/hf-auth/token` so a remote client (mobile app WebView)
can retrieve the stored HF OAuth token and seed it into a sandboxed
iframe or the ReachyMini JS SDK that cannot go through the HF OAuth
flow itself (blocked by `X-Frame-Options: SAMEORIGIN` on
`huggingface.co/login`).

The daemon's HTTP API is already unauthenticated on the local network,
so any client that can reach this endpoint can also start/stop apps
at will. Exposing the token here does not widen the attack surface.
We deliberately keep the endpoint separate from `/status` so the
desktop frontend, which never needs the raw token, is not tempted to
consume it.

Made with Cursor.

Made-with: Cursor
Adds `POST /api/hf-auth/refresh-relay` to force the central signaling
relay to drop its SSE channel and re-register with the currently
stored HF token, via the existing `notify_token_change` path.

Recovers from a desynchronised state where `/relay-status` claims
`connected` but `/central-robot-status` returns `robots: []`, which
means the relay is still holding an SSE channel open with central but
is no longer registered as a producer for the authenticated user. From
the outside this shows up as "the robot is online but no one can call
it" until someone restarts the daemon.

Before this endpoint, the only recovery was SSH + `systemctl restart`.
The mobile app can now detect the desync on startup and call this
endpoint to self-heal.

Implementation piggy-backs on `notify_token_change`, the same path
the login flow uses, so the relay's reconnect logic is exercised in
a well-trodden way. Works with any token currently stored (raw user
tokens or OAuth access tokens) because we do not re-validate the
token shape here.

Made with Cursor.

Made-with: Cursor
notify_token_change short-circuits with "Token unchanged, no action
needed" whenever old_token == new_token, which is exactly the case
we ship POST /refresh-relay for: the user is still signed in with
the same token, but the relay's SSE has desynced from central.

Adds CentralSignalingRelay.force_reconnect() + notify_force_reconnect()
that always tear down the current connection and re-register with
the stored token. Factored the close + set-token-updated sequence
into _reconnect_now() so update_token and force_reconnect stay in
lockstep.

Before: POST /refresh-relay on a logged-in daemon was a silent
no-op, leaving the mobile app's auto-heal spinner spinning and
"waiting for reachy" stuck forever.

After: the relay drops its SSE and re-registers within ~1s, central
lists the robot again, and the conversation engine starts cleanly.

Made with Cursor.

Made-with: Cursor
When `force_reconnect` (or `update_token`) tears down connections via
`_close_connections()`, aiohttp can propagate a CancelledError up
through an in-flight `session.get(...)` - typically because it was
wedged in DNS resolution (`_resolve_host`) at the time of cancellation.
That CancelledError bubbled up to `_run_loop`'s inner handler, which
unconditionally re-raised it, which killed the entire relay thread.
Every subsequent `POST /api/hf-auth/refresh-relay` then just flipped
the reported state without actually doing anything, because there was
no loop left to service the reconnect.

Now distinguish legitimate shutdown (`self._running == False`, from
`stop()`) from in-flight cancellation triggered by a reconnect
request: the former still exits the thread as before, the latter
transitions into RECONNECTING and lets the loop retry.

Reproduces reliably when the daemon boots before the robot has DNS
(WiFi still negotiating) and the mobile app's auto-heal fires at the
same time.

Made-with: Cursor
When the daemon's relay receives `welcome` from central, it used to:
  1. set state -> CONNECTED ("Remote access enabled as ...")
  2. await _send_to_central(setPeerStatus producer)

If step 2 was cancelled mid-flight (force_reconnect, token rotation,
DNS hiccup, transient HTTP error swallowed silently), observers
already saw `relay-status: {state: connected}` while central had
never registered the relay as a producer for the authenticated user.
From the outside this looked like
  - /relay-status -> {"state":"connected","is_connected":true}
  - /central-robot-status -> {"available":true,"robots":[]}

Reorder so producer registration happens BEFORE the state flip, so
the CONNECTED announcement only fires once central has acknowledged
the producer role.

Also make _send_to_central observable: log INFO on success, WARNING
with the response body on non-200, ERROR with the exception on
network failure, and a WARNING when the call is skipped (no
session / no token). Without these logs we had no way to tell from
the journal whether setPeerStatus had reached central, and the same
zombie-relay condition documented on /refresh-relay was only ever
diagnosable by SSHing in and tcpdumping.

Reproduces by calling /refresh-relay (or rotating the token) while
the previous _send_to_central is still in flight - which is exactly
what the mobile app's auto-heal flow does on every login.

Made-with: Cursor
Adds a daemon-side logging foundation that mirrors the mobile app's
PR-A: every log line carries a 4-char trace-id, and that id travels
end-to-end via the `X-Trace-Id` HTTP header, so a single user action
on the phone can be grep'd across mobile DevTools and the daemon's
systemd journal.

What's added
- `logging_ctx.py`: ContextVar-based trace-id, `TraceIdFilter` so
  `%(trace_id)s` in formatters always has a value, `redact_dict` /
  `redact_token` for secret-safe payloads, and a `kv` / `kv_log`
  helper for structured `event_name key=value` lines.
- `trace_middleware.py`: `TraceIdMiddleware` reads `X-Trace-Id` from
  incoming requests (or mints one when absent), scopes it on the
  ContextVar for the duration of the request, emits one structured
  `http.request` line per non-noisy path, and echoes the id back as
  a response header so clients can confirm correlation.
- `main.py`: installs the filter on the stderr handler and prepends
  `[trace_id]` to the format string. Registers `TraceIdMiddleware`
  AFTER CORSMiddleware so it runs first on the request side (Starlette
  wraps in reverse registration order).

Instrumentation points
- hf_auth: `auth.save_token.{success,failure}`, `auth.delete_token.
  {success,failure}`
- wifi_config: `wifi.connect.{request,busy,success,failure}`,
  `wifi.forget.{success,failure}`
- central_signaling_relay: `central.relay.state` on every transition
  (kept the existing human-readable line for journal compatibility)

Trace-id propagation contract
- Mobile sets `X-Trace-Id: abc1` on every daemon HTTP call (PR-A).
- The middleware copies that into the ContextVar, so any
  `logger.info(...)` call inside the request handler is auto-tagged.
- WebRTC `http_proxy` will propagate the same header through the
  `headers` field of the proxied request (already wired on the
  mobile side; daemon-side proxy handler is on the integration
  branch).

Behaviour change is additive: existing `logger.info(...)` calls keep
working, they just gain a `[trace]` prefix when called from inside a
request.

Made-with: Cursor
Adds a tiny, additive endpoint that returns the daemon's package
version plus an `api_revision` marker. Mobile and remote clients use
it during the handshake to detect feature mismatches (e.g. a phone
shipping a new endpoint that the daemon hasn't grown yet) and warn
the user rather than failing later with a cryptic 404.

Why a separate `api_revision` field instead of relying on the
package version: pip versions track whatever the maintainer wants
(0.x bumps, marketing tags) and aren't a reliable signal for "this
endpoint exists". Bumping `api_revision` whenever the HTTP surface
changes gives clients a stable, monotonic boolean check.

The endpoint requires no auth and no robot, so it's safe to call as
the very first probe.

Made-with: Cursor
Adding ManufacturerData to the advertisement on top of LocalName,
ServiceUUIDs (128-bit, 18B) and Appearance overflowed the 31-byte
legacy cap and the controller rejected the payload (`Failed to add
advertisement: <unknown status> (0xea)`), leaving the robot fully
non-discoverable over BLE.

Two changes to make the payload fit reliably:

1. Drop ServiceUUIDs and Appearance from the primary advertisement.
   * ServiceUUIDs are not used client-side for filtering (the mobile
     app matches on LocalName 'reachymini'); the full GATT service
     tree remains discoverable once the client connects.
   * Appearance was always 0x0000 ('Unknown') and adds no signal.

2. Cap manufacturer data to 2 IPv4 entries (5B each).
   Two entries cover the common dual-homed case (eth0 + wlan0 or
   wlan0 + hotspot). Clients that need every interface can still
   query the GATT NETWORK_STATUS characteristic.

After: typical advert is Flags(3) + LocalName(12) + ManufData(14) =
~29B for two interfaces, safely under the legacy cap.

Made-with: Cursor
…Client can reach them

The mobile app's unified RobotClient prefixes all calls with /api (so it can
swap LAN HTTP for the WebRTC HTTP-proxy data channel transparently). The
wifi_config router was only mounted at /wifi/* (legacy first-boot tooling
contract), which made the mobile app's forgetCurrentNetwork / status calls
404. We now dual-mount it: /wifi/* stays for the BLE provisioning service,
and /api/wifi/* is the canonical path for everything that talks to the daemon
through RobotClient.
Adds two parallel transport channels on top of the existing WebRTC
data-channel so the mobile app can talk to the daemon's FastAPI
without a separate LAN HTTP/WebSocket session:

- http_proxy: tunnels HTTP requests over the DC and forwards them to
  the daemon's loopback FastAPI port (set via set_loopback_http_port,
  wired from the FastAPI lifespan with --fastapi-port).
- ws_proxy: multiplexes WebSocket subscriptions over the DC by
  stream_id (ws_open / ws_send / ws_close / ws_message / ws_closed /
  ws_error frames), so several long-lived subscriptions can run in
  parallel (e.g. /api/move/ws/updates + /api/state/ws/full).

Also closes the cold-boot recovery gap for the central signaling
relay: if the daemon boots without an HF token, the relay never came
up. After save-token / refresh-relay we now ensure the relay is
running via the new ensure_central_signaling_relay() helper, so the
robot becomes visible on the central relay without a daemon restart.

Made-with: Cursor
On macOS 26 ("Tahoe"), calling Gst.DeviceMonitor.stop() can segfault
inside gst_device_provider_stop -> avfdeviceprovider, which kills the
whole Python daemon with SIGSEGV before any Python traceback can be
emitted. The parent process only sees "exit code 1" with no clue.

The monitor is short-lived (one enumeration at boot per source type)
so skipping the explicit stop on darwin is acceptable: the underlying
providers are reclaimed when the Python object is garbage collected
and when the process exits.

Other platforms keep the original behavior, wrapped in a defensive
try/except so an unexpected stop() failure logs instead of propagating.

Tested on macOS 14 (Sonoma) and macOS 26 (Tahoe) with the Reachy Mini
daemon in USB mode.

Made-with: Cursor
…ating

Add a first-class concept of "this robot's name" that is:
- persisted to ~/.config/reachy_mini/daemon.json (XDG-compliant) so a
  user-chosen name survives restarts;
- exposed through GET/POST /api/daemon/robot-name with server-side
  validation (1-32 ASCII printable chars, auto-trim) and an
  asyncio.Lock so concurrent renames cannot race;
- live-updatable: rename propagates to the persisted config, the mDNS
  TXT record, and the HF central signaling relay (via setPeerStatus)
  without dropping any active WebRTC session.

Also gate the central signaling relay on a non-default name. Until the
user explicitly names their Reachy through the mobile-app onboarding
(or POST /api/daemon/robot-name), the relay stays off so we don't
clutter the HF fleet listing with anonymous "reachy_mini" duplicates
that are impossible to tell apart. The relay starts the moment the
default-to-custom rename happens, via Daemon.set_robot_name calling
ensure_central_signaling_relay.

Bump api_revision to "2" so the mobile app can detect this surface.

Made-with: Cursor
Generate a UUID4 hex once per install, persist alongside robot_name in
~/.config/reachy_mini/daemon.json, and expose it on every discovery
channel (mDNS TXT, BLE GATT, HF central setPeerStatus meta, and a new
GET /api/daemon/identity endpoint). This gives the mobile app a stable
reconciliation key to dedupe sightings of the same physical robot
across BLE / loopback / central into a single listing row.

Also drop the "robot still uses default name" gate that previously
deferred central registration: with install_id, two unnamed reachy_mini
instances are uniquely distinguishable on central, so the gate is no
longer load-bearing and just delays the user's robot from showing up
on their HF dashboard. Bumps api_revision to "3".

Made-with: Cursor
The HF central server currently strips ``meta.install_id`` from the
producer status it forwards, which broke client-side dedupe between
a "this Mac" loopback row and the same robot's central listing. Until
central propagates the full meta blob, fall back to the producer
``peerId`` central just handed back to us on the welcome frame:

- ``CentralSignalingRelay`` exposes the assigned id via a new
  ``central_peer_id`` property + module helper ``get_central_peer_id``.
- ``Daemon.status()`` refreshes ``DaemonStatus.central_peer_id`` on
  every read (cheap, the value rotates per relay reconnect).
- ``GET /api/daemon/identity`` now returns ``central_peer_id`` next to
  ``install_id`` so mobile clients can dedupe central rows by either
  key with no extra round-trip.

Made-with: Cursor
Default keeps pointing at the upstream cduss/reachy_mini_central Space,
so unconfigured deployments keep working. Override the env var at
service startup to redirect the SSE relay and the proxied
/api/robot-status calls at a fork (test Space, staging, etc.) without
patching the source.

Single source of truth lives in central_signaling_relay (the relay
already owned the constant); the hf_auth router imports it instead of
duplicating the default URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…peer id

Replace the IPv4-list manufacturer data (under Pollen id 0xFFFF) with
a versioned TLV payload:

  - byte 0: format version 0x02
  - tag 0x01 (8 bytes): first half of install_id, stable per-install
  - tag 0x02 (8 bytes): first half of central_peer_id, present only
    while the relay is connected

Lets BLE-discovering clients (mobile app, desktop tray) dedupe a BLE
row against the same physical robot's loopback / central-listing row
without having to GATT-connect first. The central peer id TLV doubles
as a cheap "hotspot mode?" hint: a modern daemon advertising no peer
id is typically broadcasting its own AP and waiting for Wi-Fi setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tfrere added 8 commits April 29, 2026 11:18
…ive meta updates

Lays down the daemon side of the lifecycle contract documented in
docs/SIGNALING.md: the daemon now owns the verdict on whether
meta.health is ok / degraded / error, and the central signaling relay
forwards that verdict to listeners without reinterpreting it.

New module daemon/peer_health.py: pure compute(DaemonStatus) ->
PeerHealth so the truth table is exhaustively unit-tested (14 cases)
and stays decoupled from the relay. no_backend / backend_not_ready /
motor_comm / media / daemon_fatal form the stable error_code taxonomy
clients branch on.

CentralSignalingRelay grows three knobs all wired through
start_central_relay:

- health_provider callback consulted by _build_producer_meta() on
  every emission. Always-fresh, never cached past one setPeerStatus
  send.
- kind (robot | tray), wireless_version, version, capabilities baked
  into the producer meta so clients can distinguish a tray-without-
  hardware from a real robot without parsing free-text.
- schema_version field set to 1 - additive changes keep this at 1,
  bump only on breaking semantics.

Two new primitives:

- update_producer_meta() rebuilds the meta from the live
  health_provider, diffs against the last published payload, sends
  only on change. Cheap to call on every status tick (1 Hz today).
  Wrapped in synchronous notify_meta_change() for callers on the
  daemon's status thread.
- withdraw() sends setPeerStatus(roles=[]) and keeps the WebSocket
  open. Distinct from stop() which closes everything. Used for
  transient unavailability without forcing a reconnect storm.

Daemon hooks them via _publish_status:

- Every status tick calls notify_meta_change() so health transitions
  reach central within ~1s without manual plumbing.
- A _NO_HARDWARE_GRACE_SECONDS=30 watchdog evicts a desktop-tray
  daemon from the central listing once it has had no backend for
  half a minute; recovery flips it back automatically.
- Graceful shutdown (_stop_central_signaling_relay) sends an explicit
  withdraw() before closing the relay so the mobile picker drops the
  row in real time instead of waiting for the central TTL.

Made-with: Cursor
… field

RobotBackendStatus.ready is initialised once at backend boot and never
refreshed afterwards (it's a pydantic field on the static status model).
The live readiness signal is the threading.Event on Backend.ready, which
flips to set() once the backend has actually finished its async init.

Without the override compute() always read the stale False from the
model and reported error/backend_not_ready on a perfectly healthy
robot, breaking the new health-aware client gating.

We thread an Optional[bool] backend_ready_override through compute()
and have Daemon._compute_peer_health() pass backend.ready.is_set().
The fallback to the model field stays in place so unit tests and
third-party callers without a live Event keep working.

Made-with: Cursor
Two complementary changes that together kill the "ghost robot in the
central listing" failure mode (most visible when forgetting an active
WiFi network: the producer used to linger for several minutes before
TTL eviction kicked in).

1. Heartbeat re-emission (central_signaling_relay.py)

   `update_producer_meta()` now force-resends `setPeerStatus` once
   `HEARTBEAT_INTERVAL_SECONDS` (default 20s, env-tunable) have
   elapsed since the last send, even when the meta payload is byte-
   for-byte identical. This is the only liveness signal central
   accepts now; the matching central commit drops the unsound "touch
   on SSE keepalive yield" path and depends on the daemon's
   heartbeat-shaped POSTs to keep its lease alive.

   - First send still triggers on the initial registration in the
     `welcome` handler, which seeds `_last_published_at`.
   - Meta deltas (health flips, name change) still propagate on the
     next 1Hz tick, NOT gated on the heartbeat interval.
   - `withdraw()` clears both `_last_published_meta` and
     `_last_published_at` so a future re-registration fully resends.

2. Cooperative withdraw on `/wifi/forget*` (wifi_config.py)

   When the user forgets the currently-active WiFi network, the
   daemon will lose internet within a few seconds. We now call
   `notify_withdraw(timeout=1.0)` BEFORE wiping the connection, while
   we still have connectivity to central. Result: the ghost row
   disappears from the mobile app's "Hugging Face central" list
   instantly instead of after ~55s of TTL.

   Best-effort: any failure (slow central, partial network) just
   falls back on the new TTL flow.

   Same logic added to `/wifi/forget_all` when at least one
   non-Hotspot connection is currently active.

Tests:

- 7 new unit tests for the heartbeat behaviour
  (`test_central_signaling_heartbeat.py`).
- `docs/SIGNALING.md` rewritten around the new model: explicit
  heartbeat protocol, why server-pushed keepalives can't be liveness,
  cooperative-withdraw subsection.

Made-with: Cursor
…advert

* WIFI_PROBE: new public BLE command returning a JSON snapshot of
  wlan/gateway/dns/internet/daemon reachability for fast handshake
  diagnostics. Runs the 4 probes in parallel under a strict total
  budget so a stuck DNS resolver can't block the call.

* network_mode TLV (tag 0x03, 1 byte enum OFFLINE/HOTSPOT/CONNECTED)
  is appended to the BLE advertisement on each refresh tick. Mobile
  scanners use it to render an authoritative "Setup pending" badge
  without false positives from transient relay flaps.

Made-with: Cursor
Removes the three `wireless_version` gates that prevented a lite
daemon (e.g. desktop tray piloting a USB Reachy Mini) from joining
the central signaling relay. The relay code path is already
lite-friendly (it tags the producer with `kind="tray"` vs `"robot"`)
- the only blocker was these legacy "Coming soon to Lite version"
checks in the HTTP layer:

- save-token: ensure_central_signaling_relay() now runs after any
  successful token push, not only on wireless.
- relay-status: delegates to the relay state machine instead of
  short-circuiting on lite. Real status (CONNECTED / WAITING_FOR_TOKEN
  / unavailable) is the source of truth.
- refresh-relay: cold-boot recovery path no longer skips lite.

This unlocks remote WebRTC access to a tray-piloted Reachy from a
phone over the internet, matching the wireless robot's discovery
flow.

Made-with: Cursor
The default fork lives at https://tfrere-reachy-mini-central.hf.space.
Daemons that booted with no explicit env override were silently
publishing to the upstream cduss instance while the mobile app polled
the tfrere fork; the two stores never converged so robots stayed
invisible from the app.

Aligning the daemon default and the JS SDK default removes the
need for ad-hoc REACHY_CENTRAL_URL exports on every robot install,
and keeps the SDK docs and runtime in sync. Production overrides
via REACHY_CENTRAL_URL still take precedence.

Made-with: Cursor
The daemon now reads ``recommended_heartbeat_interval_seconds`` from
the ``welcome`` SSE frame and uses that as its actual heartbeat
interval (clamped to [1.0s, 60.0s] as a defence against malformed
welcomes). The module-level ``HEARTBEAT_INTERVAL_SECONDS`` becomes
the fallback used only before the first welcome and against older
central versions that do not advertise the field. Fully backwards
compatible.

Default lowered from 20s to 5s to pair with the central's new 15s
lease (LEASE/3 ratio gives ~2 missed heartbeats of headroom). The
combined effect shrinks the worst-case staleness window observed by
remote clients (mobile/desktop too far for BLE) from ~55s to ~18s.

Operationally: tuning ``LEASE_SECONDS`` server-side is now the only
knob needed; every daemon auto-aligns on its next reconnect with
zero coordinated redeploy.

Observability:
- ``get_relay_status()`` exposes the live ``heartbeat_interval_seconds``
  and ``central_lease_seconds`` so operators can confirm the contract
  the daemon is currently honouring.
- Negotiated values are logged on each welcome.

Tests cover override, fallback, clamping, non-numeric defence, and
end-to-end re-emit window driven by the negotiated interval.

Docs: docs/SIGNALING.md updated with the negotiation flow and
observability cheat sheet for client implementers.

Made-with: Cursor
Every GATT characteristic exposed by the daemon (commands, responses,
install_id, network status) is intentionally unencrypted because the
information they carry is also broadcast in the BLE advertisement
manufacturer data. There is therefore no security benefit in having
the adapter accept SMP pairing requests, and the only user-visible
side-effect of being pairable was an iOS / Android "Pair this
accessory?" prompt the first time the mobile app connected.

Drop the NoInputNoOutput Just Works agent registration and set the
adapter to Pairable=False so the mobile OS no longer surfaces a
pairing dialog. The NoInputAgent class is kept in the module for
reference in case a future encrypted-write characteristic warrants
re-enabling bonding.

Also document the libnice session-reuse abort observed on the
central / Wi-Fi WebRTC path under
docs/known-issues/libnice-session-reuse-crash.md so the diagnostic
work is not lost while we deal with it later.

Made-with: Cursor
@tfrere
Copy link
Copy Markdown
Contributor Author

tfrere commented May 4, 2026

PR branch coverage vs dev/mobile-app-integration

Generated for internal tracking (see draft PR #1070). Re-run after fetching:

git fetch origin
DEV=dev/mobile-app-integration
# Replace BRANCH with e.g. feat/webrtc-http-proxy
git rev-list --count "$DEV..origin/$BRANCH"   # commits on PR not in dev
git rev-list --count "origin/$BRANCH..$DEV"   # commits on dev not in PR
git merge-base --is-ancestor "origin/$BRANCH" "$DEV" && echo "PR contained" || echo "diverged"
git log "$DEV..origin/$BRANCH" --oneline -n 20
git log "origin/$BRANCH..$DEV" --oneline -n 15

Summary table

PR Branch On PR not in dev On dev not in PR PR tip ancestor of dev? Note
#1029 feat/ble-advertise-network-ip 0 79 yes Superseded by TLV advert + network_mode; safe to close.
#1038 fix/macos-gst-device-monitor-segfault 1 81 no Same fix as 35a188a8 on dev (duplicate SHA vs b7ce45df on PR branch). PR branch stale.
#1045 feat/ble-wifi-provisioning 19 35 no PR branch polluted with unrelated merges (main, JS, mobile-app-integration-light). Content is in dev; do not merge PR branch as-is.
#1046 feat/hf-token-endpoint 0 38 yes Fully contained; keep PR only as upstream slice or close as superseded by #1070.
#1047 feat/hf-auth-refresh-relay 0 34 yes Fully contained.
#1048 feat/webrtc-http-proxy 23 24 no PR branch has unrelated merges; http_proxy landed on dev via e5712012. Prefer #1070 or cherry-pick.
#1049 integration/mobile-app-daemon 0 13 yes Umbrella; dev adds 13 commits on top.
#1050 feat/structured-logging 0 36 yes Fully contained.
#1051 feat/wifi-forget-http-endpoint 1 38 no Tip diverges only because 252c7e06 is a merge commit not replayed on dev; symmetric diff shows dev ahead (forget + tests + relay). PR branch is stale; safe to close once #1070 lands.
#1052 feat/daemon-version-endpoint 0 37 yes Fully contained.

Commands per open child PR (copy-paste)

git fetch origin
DEV=dev/mobile-app-integration

for b in feat/ble-advertise-network-ip fix/macos-gst-device-monitor-segfault feat/ble-wifi-provisioning \
         feat/hf-token-endpoint feat/hf-auth-refresh-relay feat/webrtc-http-proxy \
         integration/mobile-app-daemon feat/structured-logging feat/wifi-forget-http-endpoint \
         feat/daemon-version-endpoint; do
  echo "=== $b ==="
  git rev-list --count "$DEV..origin/$b"
  git rev-list --count "origin/$b..$DEV"
  git merge-base --is-ancestor "origin/$b" "$DEV" && echo ancestor || echo diverged
done

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