ADR-0006: Fire TV control over ADB

Status

Accepted

Area

Device backends

Context

Fire TV is not a Google Android-TV device. Fire OS is Amazon’s Android fork; the Android TV Remote v2 protobuf protocol (TCP 6466/6467, PIN pairing) used by Shield/Sony/Chromecast-with-Google-TV does not apply. Fire TV LAN control is over ADB (adb connect <ip>:5555, input keyevent <N>), which requires the user enabling ADB debugging on the device.

Discovery previously dropped unknown DIAL responders, which is why Fire TV used to "vanish". discover() now disambiguates the DIAL search target by <manufacturer> instead of discarding responders it does not recognise.

Decision

Discovery disambiguation

discover() disambiguates the DIAL search target by <manufacturer>:

  • samsungsamsung (supported)

  • amazonfiretv (supported, ADB backend)

  • anything else → vendor unknown, supported = false (kept, not dropped — this is why Fire TV used to "vanish")

Absent manufacturer keeps the prior benefit-of-the-doubt Samsung mapping to avoid regressing verified real sets. Added DiscoveredTv::supported: bool (#[serde(default = true)]); the UI renders supported == false as a greyed "detected . unsupported" card with no Pair button.

FireTvBackend ADB wire protocol

FireTvBackend speaks the ADB wire protocol directly (tokio TCP, no external adb binary): 24-byte little-endian header (command, arg0, arg1, data_length, checksum, magic = cmd ^ 0xFFFFFFFF), CNXN handshake, AUTH, and per-keypress OPEN shell:input keyevent N streams (OKAY/WRTE-ack/CLSE).

ADB auth

ADB auth = RSA-2048, PKCS#1 v1.5 over SHA-1. The device’s 20-byte challenge is a SHA-1 digest, so we sign it directly (rsa + sha1). The keypair is generated once and persisted as adbkey.pem under <app data>/adb/ (a single host identity shared across all Fire TVs — ADB authenticates the host, not the device), so the "Allow USB debugging?" dialog only appears once. pair() returns an empty token (like Roku); the on-disk key is the credential.

The RSAPUBLICKEY path raises the on-device dialog. If the device doesn’t trust our key it re-challenges; we then send AUTH(RSAPUBLICKEY) with the key encoded in Android’s packed little-endian struct (android_pubkey.c):

modulus_size_words, n0inv, modulus[256], rr[256], exponent

base64 + " host@fernweh", computed with rsa::BigUint (rr = 2^4096 mod n; n0inv via a Newton-Hensel inverse mod 2^32).

Keymap

Android KeyEvent codes, exhaustive, no _ arm:

  • Power 26, Home 3, Back 4, Menu 82

  • Up/Down/Left/Right 19/20/21/22, Ok 23 (DPAD_CENTER)

  • Vol+/- 24/25, Mute 164, Ch+/- 166/167

  • Play/Pause 85 (MEDIA_PLAY_PAUSE), Stop 86, Rewind 89, FastForward 90

  • Source 178 (TV_INPUT), Info 165, Exit 4 (reuses BACK)

  • Digit(0..9) 7..16

power(on/off) both send POWER (Fire OS has only a toggle). send_textinput text (best-effort; spaces not handled).

Consequences

UNVERIFIED without a real Fire TV (flagged loudly in-source):

  1. The exact android_pubkey byte layout — unit-tested for shape (524 bytes, words=64, exp=65537) but not accepted by real hardware.

  2. The RSAPUBLICKEY → "Allow USB debugging?" → CNXN acceptance flow — the mock accepts at SIGNATURE, so only TOKEN→SIGNATURE is exercised end-to-end.

Everything else (framing, keymap, OPEN/WRTE/CLSE streaming) is covered by tests/firetv_proto.rs against `tv-mock’s ADB server + lib unit tests.