gabamnml/omarchy-sony-headphones

Control Sony WH-1000XM headphones from the Omarchy bar: noise cancelling, ambient sound, equalizer, DSEE, Speak-to-Chat and battery. Speaks Sony's protocol directly, no vendor app.

0

stars

4

commits

Python

primary language

Sep 5, 2026

updated

bluetooth
hyprland
omarchy
omarchy-plugin
quickshell
sony
wh-1000xm4

README

Sony Headphones for Omarchy

A bar widget for the Omarchy shell that controls Sony WH-1000XM headphones the way the phone app does — noise cancelling, ambient sound, the equalizer, DSEE Extreme, Speak-to-Chat and battery — without a phone, a vendor app, or a desktop GUI.

It talks Sony's own Bluetooth protocol directly over RFCOMM, in about seven hundred lines of dependency-free Python.

The panel, on a WH-1000XM4

The widget sits in the bar with the battery reading beside it, and the icon follows the mode — headphones while noise cancelling, an ear in ambient sound, struck through when the headphones are off or away.

The widget in the bar

Left click    open the panel
Right click   cycle noise cancelling → ambient sound → off
Scroll        ambient sound level
Middle click  refresh

Requirements

  • Omarchy 4.0 or newer
  • python3 and bluez-utils (both already on a stock Omarchy install)
  • Headphones paired and connected

Install

omarchy plugin add https://github.com/gabamnml/omarchy-sony-headphones --enable

To place it somewhere specific in the bar:

omarchy bar move gabamnml.sony-headphones --section right --index 0

Uninstall

omarchy plugin remove gabamnml.sony-headphones

That takes the widget off the bar and deletes the plugin directory. Two things live outside it and can go too, though nothing depends on them:

rm -rf ~/.cache/omarchy-sony-headphones     # the remembered RFCOMM channel

The helper process stops with the shell; it holds no state of its own and never changes a setting on the headphones unless you ask it to.

Supported models

The plugin speaks Sony's v1 protocol, used by the over-ear 1000X line up to the XM4:

ModelStatus
WH-1000XM4Verified on hardware, firmware 3.0.1 — every setting below round-tripped
WH-1000XM3, WH-1000XM2Same protocol, untested — reports welcome
WF-1000XM4/XM5, WH-1000XM5/XM6, LinkBuds, CH720NNot supported. These speak the v2 protocol; the plugin detects them and says so instead of sending bytes they will ignore

Each model honours a different subset, so the panel only draws the rows that model actually accepts — a control that silently does nothing is worse than one that is not offered. On a WH-1000XM4 that means no touch-panel switch and no auto-power-off timers: the headphones answer "still on" and "when taken off" to every such request, whatever you send them. A model this plugin does not recognise is offered everything.

What you can change

SettingNotes
Noise cancelling / Ambient sound / Wind noise reduction / OffWind noise reduction appears only on models that report it
Ambient level0–20, with Focus on Voice
EqualizerThe nine presets plus Manual; custom bands via the CLI
DSEE ExtremeUpscaling of compressed audio
Speak-to-ChatOn/off, sensitivity, resume timeout, voice focus
Pause when taken off, voice guidance
Touch controlsOn models that allow it — not the WH-1000XM4
Automatic power offNever or when taken off; timers on models that honour them
Battery, firmware, codecRead-only

Keyboard

Inside the panel: j/k or the arrow keys move, h/l adjust the row under the cursor, Enter toggles, Esc closes. Shortcuts: n cycles the mode, d toggles DSEE, s toggles Speak-to-Chat, r refreshes.

A keybinding

The widget exposes IPC, so noise cancelling can hang off a key. In ~/.config/hypr/bindings.lua:

o.bind("SUPER", "N", "Cycle noise cancelling",
  "omarchy-shell gabamnml.sony-headphones cycle")

Also available: open, close, toggle, mode <name>, ambient <0-20>, status.

Settings

Configured from the shell's widget settings, or directly on the entry in ~/.config/omarchy/shell.json:

KeyDefaultMeaning
showBatteryOnShow the battery percentage next to the icon
addressPin a MAC address when several Sony devices are connected

The command line

The helper works on its own, with or without the widget:

bin/sony-headphones status              # everything the headphones report
bin/sony-headphones probe               # diagnose the connection
bin/sony-headphones cycle               # noise cancelling → ambient → off
bin/sony-headphones set nc ambient-sound
bin/sony-headphones set ambient-level 12
bin/sony-headphones set eq bass-boost
bin/sony-headphones set eq-bands "2,0,1,0,-1,3"   # clear bass + 5 bands, -10..10
bin/sony-headphones set dsee toggle
bin/sony-headphones set speak-to-chat on
bin/sony-headphones watch               # stream state changes as JSON lines

How it works

Sony's app protocol runs over an RFCOMM serial channel. Messages are framed as 0x3e <type> <seq> <length:4> <payload> <checksum> 0x3c, escaped so the marker bytes never appear inside a message, and every message — in both directions — is answered with an acknowledgement carrying the flipped sequence number.

bin/sony-headphones watch keeps that link open and serves a unix socket, so the widget gets push updates when you press the button on the earcup, and short CLI calls apply instantly instead of paying for a fresh connection each time.

The RFCOMM channel comes from the device's own SDP record, read over L2CAP by a small SDP client in the helper. BlueZ exposes no API for a remote service record and bluez-utils no longer ships sdptool, and guessing is not an option: every other channel refuses the connection, a blocking connect to a closed one takes seconds, and a WH-1000XM4 that has been walked channel by channel starts refusing the right one too. The answer is cached under ~/.cache/omarchy-sony-headphones/.

The headphones drop the control session on their own after a while and nothing announces it, so the daemon treats silence in answer to its periodic battery poll as a dead link, and a command that arrives on one reconnects and runs rather than failing in your hands.

Nothing here touches the network. The only things it talks to are the headphones and bluetoothctl.

Development

python3 tests/test_protocol.py     # 65 tests, no headphones required
omarchy plugin validate .

There is a stand-in device for working without hardware. It answers requests with real reply payloads through the real framing, so the widget can be driven end to end and every setting is round-tripped in the test suite:

SONY_HEADPHONES_DEMO=1 bin/sony-headphones status
SONY_HEADPHONES_DEMO=1 bin/sony-headphones watch   # the widget attaches to this

Editing the QML reloads the widget on save. Editing Model.js does not — the QML engine caches .pragma library imports, so run omarchy restart shell after changing it.

Credits

The v1 protocol — payload opcodes, byte layouts, and what each field means — was reverse engineered by Gadgetbridge, whose SonyProtocolImplV1 is the reference this implementation follows. The message framing was cross-checked against SonyHeadphonesClient and SonyBridge, and the RFCOMM layer against ohm-app's protocol notes.

This is an independent project. Sony has nothing to do with it.

License

AGPL-3.0-or-later, matching Gadgetbridge, from which the protocol knowledge comes. See LICENSE.

Contributors

gabamnml

4 commits

gabamnml/omarchy-sony-headphones

Control Sony WH-1000XM headphones from the Omarchy bar: noise cancelling, ambient sound, equalizer, DSEE, Speak-to-Chat and battery. Speaks Sony's protocol directly, no vendor app.

0

stars

4

commits

Python

primary language

Sep 5, 2026

updated

bluetooth
hyprland
omarchy
omarchy-plugin
quickshell
sony
wh-1000xm4

README

Sony Headphones for Omarchy

A bar widget for the Omarchy shell that controls Sony WH-1000XM headphones the way the phone app does — noise cancelling, ambient sound, the equalizer, DSEE Extreme, Speak-to-Chat and battery — without a phone, a vendor app, or a desktop GUI.

It talks Sony's own Bluetooth protocol directly over RFCOMM, in about seven hundred lines of dependency-free Python.

The panel, on a WH-1000XM4

The widget sits in the bar with the battery reading beside it, and the icon follows the mode — headphones while noise cancelling, an ear in ambient sound, struck through when the headphones are off or away.

The widget in the bar

Left click    open the panel
Right click   cycle noise cancelling → ambient sound → off
Scroll        ambient sound level
Middle click  refresh

Requirements

  • Omarchy 4.0 or newer
  • python3 and bluez-utils (both already on a stock Omarchy install)
  • Headphones paired and connected

Install

omarchy plugin add https://github.com/gabamnml/omarchy-sony-headphones --enable

To place it somewhere specific in the bar:

omarchy bar move gabamnml.sony-headphones --section right --index 0

Uninstall

omarchy plugin remove gabamnml.sony-headphones

That takes the widget off the bar and deletes the plugin directory. Two things live outside it and can go too, though nothing depends on them:

rm -rf ~/.cache/omarchy-sony-headphones     # the remembered RFCOMM channel

The helper process stops with the shell; it holds no state of its own and never changes a setting on the headphones unless you ask it to.

Supported models

The plugin speaks Sony's v1 protocol, used by the over-ear 1000X line up to the XM4:

ModelStatus
WH-1000XM4Verified on hardware, firmware 3.0.1 — every setting below round-tripped
WH-1000XM3, WH-1000XM2Same protocol, untested — reports welcome
WF-1000XM4/XM5, WH-1000XM5/XM6, LinkBuds, CH720NNot supported. These speak the v2 protocol; the plugin detects them and says so instead of sending bytes they will ignore

Each model honours a different subset, so the panel only draws the rows that model actually accepts — a control that silently does nothing is worse than one that is not offered. On a WH-1000XM4 that means no touch-panel switch and no auto-power-off timers: the headphones answer "still on" and "when taken off" to every such request, whatever you send them. A model this plugin does not recognise is offered everything.

What you can change

SettingNotes
Noise cancelling / Ambient sound / Wind noise reduction / OffWind noise reduction appears only on models that report it
Ambient level0–20, with Focus on Voice
EqualizerThe nine presets plus Manual; custom bands via the CLI
DSEE ExtremeUpscaling of compressed audio
Speak-to-ChatOn/off, sensitivity, resume timeout, voice focus
Pause when taken off, voice guidance
Touch controlsOn models that allow it — not the WH-1000XM4
Automatic power offNever or when taken off; timers on models that honour them
Battery, firmware, codecRead-only

Keyboard

Inside the panel: j/k or the arrow keys move, h/l adjust the row under the cursor, Enter toggles, Esc closes. Shortcuts: n cycles the mode, d toggles DSEE, s toggles Speak-to-Chat, r refreshes.

A keybinding

The widget exposes IPC, so noise cancelling can hang off a key. In ~/.config/hypr/bindings.lua:

o.bind("SUPER", "N", "Cycle noise cancelling",
  "omarchy-shell gabamnml.sony-headphones cycle")

Also available: open, close, toggle, mode <name>, ambient <0-20>, status.

Settings

Configured from the shell's widget settings, or directly on the entry in ~/.config/omarchy/shell.json:

KeyDefaultMeaning
showBatteryOnShow the battery percentage next to the icon
addressPin a MAC address when several Sony devices are connected

The command line

The helper works on its own, with or without the widget:

bin/sony-headphones status              # everything the headphones report
bin/sony-headphones probe               # diagnose the connection
bin/sony-headphones cycle               # noise cancelling → ambient → off
bin/sony-headphones set nc ambient-sound
bin/sony-headphones set ambient-level 12
bin/sony-headphones set eq bass-boost
bin/sony-headphones set eq-bands "2,0,1,0,-1,3"   # clear bass + 5 bands, -10..10
bin/sony-headphones set dsee toggle
bin/sony-headphones set speak-to-chat on
bin/sony-headphones watch               # stream state changes as JSON lines

How it works

Sony's app protocol runs over an RFCOMM serial channel. Messages are framed as 0x3e <type> <seq> <length:4> <payload> <checksum> 0x3c, escaped so the marker bytes never appear inside a message, and every message — in both directions — is answered with an acknowledgement carrying the flipped sequence number.

bin/sony-headphones watch keeps that link open and serves a unix socket, so the widget gets push updates when you press the button on the earcup, and short CLI calls apply instantly instead of paying for a fresh connection each time.

The RFCOMM channel comes from the device's own SDP record, read over L2CAP by a small SDP client in the helper. BlueZ exposes no API for a remote service record and bluez-utils no longer ships sdptool, and guessing is not an option: every other channel refuses the connection, a blocking connect to a closed one takes seconds, and a WH-1000XM4 that has been walked channel by channel starts refusing the right one too. The answer is cached under ~/.cache/omarchy-sony-headphones/.

The headphones drop the control session on their own after a while and nothing announces it, so the daemon treats silence in answer to its periodic battery poll as a dead link, and a command that arrives on one reconnects and runs rather than failing in your hands.

Nothing here touches the network. The only things it talks to are the headphones and bluetoothctl.

Development

python3 tests/test_protocol.py     # 65 tests, no headphones required
omarchy plugin validate .

There is a stand-in device for working without hardware. It answers requests with real reply payloads through the real framing, so the widget can be driven end to end and every setting is round-tripped in the test suite:

SONY_HEADPHONES_DEMO=1 bin/sony-headphones status
SONY_HEADPHONES_DEMO=1 bin/sony-headphones watch   # the widget attaches to this

Editing the QML reloads the widget on save. Editing Model.js does not — the QML engine caches .pragma library imports, so run omarchy restart shell after changing it.

Credits

The v1 protocol — payload opcodes, byte layouts, and what each field means — was reverse engineered by Gadgetbridge, whose SonyProtocolImplV1 is the reference this implementation follows. The message framing was cross-checked against SonyHeadphonesClient and SonyBridge, and the RFCOMM layer against ohm-app's protocol notes.

This is an independent project. Sony has nothing to do with it.

License

AGPL-3.0-or-later, matching Gadgetbridge, from which the protocol knowledge comes. See LICENSE.

Contributors

gabamnml

4 commits

Languages

Python

72.0%

QML

24.1%

JavaScript

3.9%