Initial implementation of BLE-to-TCP serial bridge for Android

Android app that connects to a BLE UART device (Nordic UART Service by
default, matching RNode BLE) and exposes it as a TCP byte pipe, so socat
can present it to Reticulum as a serial port. Foreground service hosts a
GATT client with MTU negotiation, chunked serialized writes, and
auto-reconnect, plus a single-client last-wins TCP server. No root
required on either end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 16:44:53 -07:00
commit 26146ec8c3
18 changed files with 1235 additions and 0 deletions

117
README.md Normal file
View File

@@ -0,0 +1,117 @@
# BLE Serial Bridge
An Android app that connects to a BLE UART device (Nordic UART Service by
default — the same service RNode firmware uses for BLE serial) and exposes it
as a TCP byte pipe. With `socat` on the consuming side, that pipe becomes a
real serial device node that [Reticulum](https://reticulum.network/) can use
via its serial-based interfaces.
```
┌─────────────┐ BLE/GATT ┌──────────────────┐ TCP ┌──────────────────────┐
│ BLE device │◄────────────►│ BLE Serial Bridge │◄───────►│ socat → pty │
│ (NUS UART, │ │ (this app, │ :7777 │ → rnsd / picocom / │
│ RNode, …) │ │ foreground svc) │ │ anything serial │
└─────────────┘ └──────────────────┘ └──────────────────────┘
```
No root required anywhere. The consumer can be Termux on the same phone
(loopback) or a PC on the same network (enable "Allow LAN").
## Usage
1. Install and open the app, grant Bluetooth (and notification) permissions.
2. If the device requires bonding (RNode BLE does — it displays a pairing
PIN), pair it once in Android's Bluetooth settings first.
3. Set the TCP port (default `7777`). Leave "Allow LAN" off for
Termux-on-the-same-phone use; turn it on to serve a PC over WiFi.
4. The UUID fields are pre-filled with Nordic UART Service. Change them only
for devices with a custom UART-style service.
5. Tap **Scan**, then tap your device. The bridge starts as a foreground
service and auto-reconnects if the BLE link drops.
The TCP server accepts one client at a time; a new connection displaces the
old one, so a restarted `socat` reconnects cleanly.
## Reticulum on the same phone (Termux)
```sh
pkg install socat
socat -d pty,link=$HOME/ttyBLE0,raw,echo=0 tcp:127.0.0.1:7777
```
Then point an RNS interface at the pty. For an RNode over BLE:
```ini
[[RNode BLE]]
type = RNodeInterface
enabled = true
port = /data/data/com.termux/files/home/ttyBLE0
frequency = 867200000
bandwidth = 125000
txpower = 7
spreadingfactor = 8
codingrate = 5
```
For a generic KISS TNC behind a BLE UART:
```ini
[[BLE KISS]]
type = KISSInterface
enabled = true
port = /data/data/com.termux/files/home/ttyBLE0
speed = 115200
```
(`speed` is required by pyserial but meaningless here — the BLE link ignores
it.)
Run `socat` and `rnsd` together, e.g. in two Termux sessions or under a
process supervisor. Disable Android battery optimization for both this app
and Termux, or the OS will kill the bridge.
## Reticulum on a PC (phone as BLE radio bridge)
Enable "Allow LAN" in the app, then on the PC:
```sh
socat -d pty,link=/tmp/ttyBLE0,raw,echo=0 tcp:<phone-ip>:7777
```
and use `/tmp/ttyBLE0` as the `port` in the RNS interface config as above.
To get a system-style name, run socat as root with
`pty,link=/dev/ttyBLE0,raw,echo=0`.
Note: "Allow LAN" binds to all interfaces with no authentication or
encryption — anyone on the network can talk to your radio. Use it only on
networks you trust.
## Sanity check without Reticulum
```sh
socat -d pty,link=/tmp/ttyBLE0,raw,echo=0 tcp:<phone-ip>:7777 &
picocom /tmp/ttyBLE0
```
## Building
Requires the Android SDK (API 36) and JDK 17+.
```sh
gradle assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
```
## Design notes
- **BLE side**: GATT central. Negotiates the largest MTU it can (up to 517),
chunks writes to MTU3, serializes them (one write in flight), and uses
write-without-response when the characteristic supports it. Notifications
on the RX characteristic feed the TCP side. Reconnects every 3 s if the
link drops, for as long as the bridge is running.
- **TCP side**: plain byte stream, no framing, no RFC 2217 — the BLE link has
no baud rate to negotiate. `TCP_NODELAY` is set to keep KISS frame latency
down.
- **Defaults**: Nordic UART Service `6e400001-…`, write `6e400002-…`,
notify `6e400003-…` — matches RNode BLE and most BLE-UART firmware
(Adafruit, Espruino, many ESP32 sketches).