Moon-Companion

Android app that pairs once with your Flipper and gives every FAP access to GPS, time, and HTTP — all over BLE GATT.

Why have a phone at all?

The Flipper has no Wi-Fi, no cellular, and no real-time clock that survives a battery pull. Every FAP that wants live data has historically needed an ESP32 dev-board plugged into the GPIO header. Moon-Companion takes the phone you already carry and turns it into that dev-board, over BLE — one pairing flow, one battery-aware service, usable by any FAP that links against moon_companion.

The on-device services the phone provides:

Encrypted pairing, once

First time you connect, Android pops its standard pair-accept dialog. Behind the scenes we run SMP with encryption (LE Secure Connections where the chip supports it) and exchange long-term keys. Moon-Companion persists the bond via EncryptedSharedPreferences, and the firmware side stores LTKs in BlueNRG's NVM. From that point on, reconnects happen silently: no prompt, no re-pair, no ritual.

Every RPC characteristic (RPC_TX, RPC_RX) is marked PERMISSION_READ_ENCRYPTED / PERMISSION_WRITE_ENCRYPTED at the GATT server, so an attacker who sniffs the advertisement can't just connect and start issuing HTTP calls — the link has to be encrypted with the bonded keys before the firmware will serve those characteristics.

CCCD nuance: the Client Characteristic Configuration Descriptor on RPC_TX is deliberately unencrypted. Android's GATT stack tries to subscribe before the pairing handshake fully settles on some devices; keeping the CCCD loose avoids a race where subscribe times out while encryption is still being negotiated. The payload characteristic itself stays encrypted, so the subscription permission is harmless.

HTTP proxy with chunked framing

BLE's per-notification payload is tiny — 244 bytes in the best case, often less after headers. A real HTTP response body doesn't fit. The proxy wraps every RPC message in a chunk frame so the firmware can reassemble up to 8 KB payloads without re-negotiating MTU every call.

Chunk frame

magic0xEC — tells the reassembler this is Moon chunk framing, not legacy RPC1 byte
flagsbit 0 = last chunk; rest reserved1 byte
seqchunk index within the current message1 byte
lenpayload bytes in this chunk1 byte
payloadnanopb-encoded MoonPhoneMessage fragment≤ 240 B

Messages are nanopb protobufs — MoonPhoneMessage on the wire, which wraps either an HttpRequest or HttpResponse. We widened the PB_BIND width to 4 bytes on those types so the body field can carry up to 4 KB without length-prefix overflow.

Request flow

Where to find it

Source on GitHub: KaraZajac/Moon-Companion. Minimum Android 9 (API 28). Play Store listing is pending; for alpha users the APK is attached to each GitHub release.