A standalone Light Phone III tool that controls Sony headphones and earbuds over Bluetooth — noise-cancelling / ambient mode, ambient level, focus-on-voice, and battery status — directly from the phone's minimal UI.
It is model-agnostic: it auto-detects the protocol dialect (v1 or v2) from the
handshake and discovers each device's capabilities at runtime (single vs L/R +
case battery, ANC variant / wind-noise support), so it adapts to whatever Sony
device is paired rather than hardcoding a model list. Verified end-to-end on
WF-1000XM6 (earbuds) and WH-1000XM5 (over-ear); other Sony models that
speak the same serial protocol (WF-1000XM4/XM5, WH-1000XM3/XM4, LinkBuds /
LinkBuds S, WF-C500/C700N, …) are supported by the same adaptive logic. The
connected device's name is shown as the title, and controls it doesn't support
(e.g. ANC on LinkBuds) are hidden. No web interface: it talks to the earbuds natively
over an RFCOMM (SPP-style) serial link, the same "serial control" mechanism
reverse-engineered by
usering-around/sony-wf1000xm5-controller
and documented by Gadgetbridge.
It is a thin, self-contained repo built against the Light SDK (as a git submodule), laid out to drop straight into Light's tool build / review pipeline.
sony-wf-control-app/
├── light-sdk/ # git submodule → tthayer/light-sdk @ feat/sony-wf-app
├── tool/ # the ONLY dev-owned module
│ ├── lighttool.toml # tool id, label, version, declared permissions
│ ├── build.gradle.kts # deps (project(":sdk:client") + light.sdk plugin)
│ └── src/
│ ├── main/kotlin/com/thelightphone/sonywf/
│ │ ├── SonyWfHomeScreen.kt # @InitialScreen — the UI
│ │ ├── SonyWfViewModel.kt # state + connection orchestration
│ │ └── protocol/ # pure-Kotlin Sony protocol (framing,
│ │ │ # ACK handshake, commands, parsers)
│ │ └── *.kt
│ └── test/kotlin/… # protocol unit tests
├── settings.gradle.kts # grafts the submodule's SDK projects into this build
├── build.gradle.kts # thin root: plugin classpath + ext build knobs
├── gradle.properties
└── gradlew, gradle/ # wrapper (matches the pinned SDK)
The Light SDK sandboxes tool code — app modules cannot touch android.bluetooth
directly (no Context, no getSystemService). So Bluetooth access is brokered
by the SDK, exactly like the YubiKey tool brokers USB/NFC through
LightSecurityKey:
LightBluetoothSerial(in the SDK, obtained aslightContext.bluetoothSerial) owns theBluetoothAdapter, enumerates already-paired devices, opens the RFCOMM socket to the Sony service UUID (956C7B26-D49A-4BA8-B03F-B17D393CB6E2), and exposes a protocol-agnosticLightSerialConnection(suspendwrite, aFlow<ByteArray>of inbound bytes,close).tool/src/main/kotlin/.../protocol/is pure Kotlin: it frames the Sony message protocol (header0x3e/ trailer0x3c, escaping, big-endian length, checksum), runs the strict send-one-wait-for-ACK handshake, and parses ANC and battery notifications. It has no Android dependencies (only theLightSerialConnectioninterface), so it is unit-tested on the JVM.
Pairing: pair the earbuds once in the system Bluetooth settings. This tool does not scan/discover, so it declares only
BLUETOOTH_CONNECT.
Verified end-to-end on a real Light Phone III (model TLP301) against WF-1000XM6:
RFCOMM connect, the Init/ACK handshake, ANC + battery read, and ANC writes all
work. But the shipped LightOS server (com.lightos) currently refuses to
grant BLUETOOTH_CONNECT — its permission screen shows "Not allowed"
(LightSdkPermissionActivity: … BLUETOOTH_CONNECT is not grantable by this server). The SDK-side grant plumbing here is correct (it launches the request
via startActivityForResult so the caller is identified, matching the CAMERA
path), and LightSdkServer.androidPermissionAllowed allows it — but that only
affects the SDK/emulator build, not the prebuilt on-device com.lightos.
So on a production device the permission must be granted one of two ways:
- Light adds
BLUETOOTH_CONNECTto the production server's grantable set (the real fix for end users — requires a LightOS change). - Grant it manually over adb (for development / personal use):
After that the tool connects and works fully.
adb shell pm grant com.thelightphone.sonycontrol android.permission.BLUETOOTH_CONNECT
Upstream Light SDK has no Bluetooth support, so this repo pins its submodule to
tthayer/light-sdk@feat/sony-wf-app, which adds:
LightBluetoothSerial+LightSerialConnection(the RFCOMM broker), exposed onSealedLightContext.bluetoothSerial.BLUETOOTH_CONNECTin the plugin's allowed-permission set (+ the impliedandroid.hardware.bluetoothfeature).- A LightOS-server grant so the dangerous runtime
BLUETOOTH_CONNECTpermission is actually granted on-device (mirrors howCAMERAis handled).
The gear icon opens a settings page listing whatever the connected device
advertises (discovered from its support-function table, so other models show
their own subset). On a WF-1000XM6: Auto Ambient Sound, Equalizer preset,
DSEE, Background Music Effect, Speak-to-Chat, pause when removed, head
gestures, voice assistant, auto power off, Auto Power Save, connection
quality, hearing your own voice on calls, multipoint, and power off (tap
twice). Tap a row to step to its next value. The bottom bar carries volume
down/up, previous/next and play/pause. Wire specs:
spec-mdr-v2-functions.md,
spec-mdr-v2-settings-sound-power.md,
spec-mdr-v2-settings-system-playback.md.
The tool checks Sony's firmware feed (info.update.sony.net, hence the
INTERNET permission) once per connection, using the category/service IDs the
headphones report themselves.
- Tandem-FOTA devices (the firmware travels over the same RFCOMM link): full check → download → transfer → install, driven from the tool. Tap UPDATE, confirm, and keep the headphones on, near the phone, with the app open until it finishes.
- MTK/Airoha devices (verified on the WH-1000XM5, chip MT2822): full check → download → transfer → install too, over a second RFCOMM link running the Airoha RACE protocol. The transfer takes 15 to 40 minutes; DIAG still runs the read-only chip probe.
- Two-bud MTK/Airoha earbuds (e.g. WF-1000XM6, chip MT2833, capability
tws): check only for now. The install is refused before anything is written; the agent/partner relay flow is not implemented yet. - "MC app" devices: check only. The tool reports the available version and tells you to install it with Sony's Sound Connect app.
- Devices that advertise no update support get no update line.
Specs: docs/protocol/fw-update-design.md
(API contract), spec-tandem-fota.md
(Tandem transfer), spec-airoha-mt28xx-single.md
(MTK transfer), spec-airoha-mt2833-tws.md
(MTK two-bud transfer, not implemented yet), spec-firmware-download.md
(feed discovery/decrypt), spec-update-orchestration.md
(gating).
Requires JDK 17 and an Android SDK. Provide a GitHub token with read:packages
so the transitive keyboard dependency resolves — either in local.properties:
sdk.dir=/path/to/android-sdk
gpr.user=<your-github-user>
gpr.key=<token with read:packages>or as env vars GH_PACKAGES_USER / GH_PACKAGES_TOKEN. Then:
git submodule update --init --recursive
./gradlew :tool:assembleDebug # → tool/build/outputs/apk/debug/tool-debug.apk
./gradlew :tool:testDebugUnitTest # run the protocol unit testsThe APK is signed with the shared Light dev keystore (from the submodule) for local sideloading. Light's build service re-signs with its own key.
Bluetooth needs a real device — the emulator has no BT radio, so
LightBluetoothSerialexposes a demo mode (a canned WF-1000XM5) for emulator testing.
git -C light-sdk fetch origin
git -C light-sdk checkout <commit-or-tag>
git add light-sdk && git commit -m "bump light-sdk to <ref>".github/workflows/build.ymlcompiles the debug APK and runs the protocol unit tests on every push (non-main) and PR — the compile gate..github/workflows/release.ymlcuts a GitHub Release on every merge tomain: derives the next version from conventional commits (feat:→ minor,fix:→ patch,feat!:/BREAKING CHANGE→ major, seeded at v0.1.0), syncstool/lighttool.toml, builds a signed release APK, and attaches it to avX.Y.Zrelease.
Light's containerized builder clones this repo and extracts only
tool/build.gradle.kts, tool/lighttool.toml, and tool/src/main/**, then
compiles them against its own pinned, sandboxed copy of the SDK. It generates
AndroidManifest.xml from lighttool.toml, so this repo intentionally contains
no hand-written manifest and never sets applicationId, versionCode,
versionName, or namespace.
Protocol details reverse-engineered by
usering-around/sony-wf1000xm5-controller
and Gadgetbridge. This tool
reimplements the serial protocol in Kotlin for the Light Phone.