Architecture
fernweh is a Rust workspace with a strict separation: all networking lives in one protocol crate, and everything above it is UI or platform shell.
Crate layout
| Crate | Responsibility |
|---|---|
|
Pure protocol library — no Tauri, no UI. Samsung/Roku/LG/PS backends behind one |
|
In-process mock devices (self-signed TLS WebSocket, HTTP, UDP DDP, ADB, Android TV) for integration tests. |
|
Tauri v2 shell (package |
|
Leptos CSR frontend (Trunk → wasm); dark amber hardware-remote UI. Excluded from the cargo workspace (it targets |
The backend abstraction
Every network device speaks through one trait in crates/tv-core/src/backend.rs:
TvBackend, driven by a KeyCommand enum. Each backend maps KeyCommand → a
vendor keycode; an unknown mapping returns TvError::Unsupported — it never panics.
error.rs defines the thiserror-based TvError. There are no unwrap() /
expect() outside tests.
Discovery
A single discover() runs SSDP M-SEARCH (Samsung DIAL, roku:ecp, LG
webos-second-screen), DDP for PlayStation, and mDNS
(_androidtvremote2._tcp.local.) concurrently, fetches the device-description XML
for friendly name / model / MAC, and dedups by IP over a short window. Vendor is
confirmed from the description <manufacturer> field, because DIAL is not
Samsung-exclusive. See ADR-0006 for how unknown
DIAL responders are surfaced rather than dropped.
Backends at a glance
-
Samsung —
wss://{ip}:8002, self-signed cert accepted (LAN-only), pair → token,ms.remote.controlclicks, WoL power-on. -
Roku — plain HTTP ECP, no auth,
POST /keypress/{key}. -
LG webOS —
wss://{ip}:3001(fallbackws://:3000), register → client-key;ssap://commands plus a pointer-input socket for the d-pad — see ADR-0003. -
PlayStation — DDP wake / standby (buttons need Remote Play, out of scope).
-
Fire TV — the ADB wire protocol over TCP 5555 — see ADR-0006.
-
Android TV — length-delimited protobuf over TLS 6466/6467, mutual-TLS pairing — see ADR-0007.
-
Universal IR — a code database rendered to carrier timings in
tv-core::ir, transmitted by an Android plugin — see ADR-0005.
The rationale for each non-obvious choice is recorded in the Architecture Decisions.