Cracking OPOv1:
Reverse Engineering the OnePlus Buds Protocol
A story of frustration, failure, and finally cracking the ANC protocol that OnePlus refused to document.
The Frustration
I was working on my MacBook, earbuds in. I decided to change from Transparency to ANC because I needed to focus.
I tried pinching the earbuds to toggle the mode, but it didn't work. The ANC just wouldn't switch over to Transparency (or vice versa) no matter how many times I tried the gesture.
I thought, "No problem, I'll just download the official app on my Mac and change it from there." But when I went to look for it, I realized there is no app. There is absolutely no desktop software to control these things.
That's when I started searching for a solution.
The real problem: I paid for these earbuds. They're connected to my laptop right now via Bluetooth. The hardware link exists — but I have zero software control over it unless I use a phone. That's absurd.
The Search
I figured someone must have solved this already. These earbuds aren't brand new. There are developers out there who reverse-engineer everything. Surely one of them had already written a script.
- →"OnePlus Nord Buds 3 Pro macOS" — just Reddit threads of other people asking the same question, with no answers.
- →"Nord Buds ANC script" — nothing. "HeyMelody API" — nothing. "OnePlus BLE protocol" — absolutely nothing.
- →GitHub search for "nord buds" — a few apps for Nord Buds 2, but nothing for the 3 Pro, and nothing in Swift or Python that actually talked to the hardware.
What surprised me wasn't just that OnePlus didn't make a desktop app. It's that they never published anything. No API docs, no developer SDK, no blog post explaining how their protocol works — not even a GitHub issue where someone had figured it out. For a company that sells hundreds of millions of devices, they put zero effort into letting anyone outside their own team do anything with them.
Big companies treat their protocols like trade secrets. Lock you into their ecosystem, limit your control to their apps, and if you use anything other than what they officially support — you're on your own. It's not about security, it's about control.
The Decision
Fine. I'll do it myself.
I knew the earbuds were connected via Bluetooth Low Energy — BLE — and I knew that BLE devices always expose their features through a standard interface called GATT. That interface is readable by any device with Bluetooth. My Mac could see it. It was just a question of understanding what to send.
I had three things going for me:
- 1A Bluetooth HCI snoop log (btsnoop) from my Android phone showing the raw packet exchange between HeyMelody and the buds
- 2The HeyMelody APK, which I could decompile and read the Java source to understand what it was doing
- 3CoreBluetooth on macOS — Apple's first-class BLE API that lets you read, write, and subscribe to characteristics from a Swift script
First Steps — Understanding BLE
Before diving in: here's how Bluetooth Low Energy actually works under the hood. This context is critical for everything that follows.
The GATT Model
BLE devices advertise their capabilities using a layered structure called GATT (Generic Attribute Profile). Think of it as a file system — with folders (services) containing files (characteristics), each identified by a UUID.
Services
Groups of related functionality. Each has a UUID. The Nord Buds expose 6+ services — one for battery, one for device info, one for the OPO protocol, etc.
Characteristics
Individual data endpoints inside a service. Each has properties: READ, WRITE, NOTIFY, or combinations. ANC control lives inside one of these.
Notifications
Instead of polling, you subscribe to a characteristic and the device pushes updates to you. Responses from the earbuds come back this way.
Write With vs. Without Response
This distinction sounds minor but it bit me for weeks. In CoreBluetooth there are two write types:
- .withResponse — the peripheral must send an ATT acknowledgement before you can send the next packet. Safe, slow, used for critical writes.
- .withoutResponse — fire-and-forget. No ATT-level ack. Faster, and required for characteristics that don't support confirmed writes.
The Nord Buds' OPO command characteristic only supports .withoutResponse. Use the wrong one and the write silently fails — no error, no response, nothing. Hours lost.
HCI Snoop Logs (btsnoop)
Android's developer options let you enable Bluetooth HCI snoop logging. Once enabled, every BLE packet going in or out of your phone is written to a .log file. You copy that to your laptop, open it in Wireshark, filter by your earbud's MAC address, and you can see exactly what HeyMelody sends — byte by byte — every time you tap a button in the app.
This is the single most powerful tool I had. The packets don't lie.
Service Discovery Hell
First thing I did was enumerate every service and characteristic the buds expose. Here's what came back:
| Service UUID | Type | Role |
|---|---|---|
| 0000079A-D102-11E1-9B23-00025B00A5A5 | OPO Protocol | ← THIS is the main command channel |
| FE2C1234-8366-4814-8EB0-01DE32100BEA | OnePlus custom | Telemetry / firmware update |
| 66666666-6666-6666-6666-666666666666 | Unknown custom | Possibly diagnostics |
| 0000079C-... | OPO extended | Additional commands (SCO / A2DP) |
| 180A | Standard BLE | Device Information (manufacturer, model) |
| 180F | Standard BLE | Battery Level (simple, not detailed) |
The 0000079A UUID is the OPO (OnePlus/Oppo) proprietary protocol service — shared across OnePlus, Oppo, and Realme devices. It has two key characteristics inside it:
0100079A— Write-only. This is where you send commands.0200079A— Notify-only. This is where responses come back.
Then there's FE2C — a dense service with many characteristics. Lots of readable values, lots of writable ones. It looked important. My first instinct was to explore it. That was my first mistake.
The False Starts
Weeks. I spent weeks going nowhere. Here's a log of every wrong assumption I made:
Mistake #1 — Wrong Service (FE2C)
FE2C had the most characteristics and the most readable data — so naturally I assumed it was the main control channel. Wrong. FE2C handles low-level diagnostics, firmware update status, and device telemetry. Writing to its characteristics either silently no-ops or triggers some internal logging mode. ANC is not in there.
Mistake #2 — Wrong Write Type
I was using .withResponse on the 0100079A characteristic. The write would "succeed" (CoreBluetooth didn't throw an error) but nothing happened on the earbuds. This is because the characteristic only accepts .withoutResponse writes — and if you use the wrong type, the peripheral silently ignores you. Zero indication anything went wrong.
Mistake #3 — Phantom CRC
Looking at packets in Wireshark I kept seeing a trailing byte that looked like a checksum. I spent days trying to reverse-engineer a CRC8 or CRC16 algorithm. There is no CRC. There is no checksum at all. The length byte accounts for everything. The "extra" byte at the end was just part of the data payload — I was miscounting from the wrong offset.
Mistake #4 — No Authentication
My first tests skipped the Hello + Register sequence and jumped straight to sending an ANC command. The earbuds just... ignored it. There's no error response — the device won't tell you "you're not authenticated." It simply doesn't act on unauthenticated commands. Without the handshake, nothing works.
Decompiling the App
I pulled the HeyMelody APK from my phone and ran it through jadx — a Java decompiler that reconstructs readable source from Android bytecode. The output is messy, obfuscated, class names like C0878a.java, but the logic is there.
Inside the BtOperate class I found the ANC command builder:
The number 1028 is the key. In hex: 0x0404. Split into two bytes:
- High byte 0x04 = Category: ANC subsystem
- Low byte 0x04 = Sub-command: Set (write new value)
And from the UUID constants file, confirming the service:
That confirmed it: 0000079A is the right service. Not FE2C. Two weeks of going in the wrong direction — one line in a decompiled APK set me straight.
Packet Analysis
With the correct service confirmed, I went back to the btsnoop capture and filtered for packets on 0000079A with Category byte = 0x04. Three packets jumped out immediately — one for each ANC mode:
| Mode | Full Packet | Last Byte |
|---|---|---|
| ANC OFF | AA 0A 00 00 04 04 40 03 00 01 01 04 | 0x04 |
| ANC ON | AA 0A 00 00 04 04 44 03 00 01 01 01 | 0x01 |
| Transparency | AA 0A 00 00 04 04 42 03 00 01 01 02 | 0x02 |
Notice: 11 bytes are identical across all three packets. The only thing that changes is the very last byte. That's the mode selector. Everything else — the framing, the length, the category, the sequence number — is boilerplate.
The sequence number (byte 6) also increments: 0x40, 0x42, 0x44. This lets the device match responses to requests. You don't strictly need to increment it correctly for a one-shot command, but the device uses it for ordering.
Then for the authentication packets:
The token B5 50 A0 69 is a device-specific identifier — probably derived from the MAC address or baked into the firmware. It's the same every time for a given pair of buds. Send it after the Hello and the device opens a trusted session.
The Dark Night
I had everything I needed. I wrote the Swift code. I implemented the sequence exactly:
- Connect to the 0000079A service
- Subscribe to the notify characteristic
- Send Hello
- Wait 2 seconds
- Send Register with token
- Wait 1.5 seconds
- Send ANC Query
- Wait 1.5 seconds
- Send ANC Set
I ran the script. And...
Nothing. The earbuds just sat there. No response on the notify characteristic. No mode change sound. No output. The packets went out — I could see them being written — but nothing came back.
I checked the write type — correct. Checked the UUIDs — correct. Checked the packet bytes against the btsnoop — identical. There was no logical reason it shouldn't work.
I was genuinely close to giving up. The official app exists. My phone is right there. Maybe I just accept that this is how it is. Maybe some things aren't worth cracking open.
The Breakthrough
One last thing to try. I'd been subscribing to notifications only on the 0200079A characteristic — the notify char inside the OPO service. What if the device was sending responses back on the other service? What if FE2C was the response channel, even though 0000079A was the command channel?
I modified the code to subscribe to notifications on every characteristic across both services simultaneously. Sent the same command sequence.
That little tri-tone that plays when ANC switches on. The earbuds changed mode. It actually worked.
The device had been processing my commands the entire time. I just wasn't receiving its responses because I was listening on the wrong channel. Subscribing to both made the round-trip complete.
Deep Protocol Analysis
Now that it works, here's the full picture — every byte, every timing constraint, every category code.
OPO Protocol — Packet Structure
Every packet starts with 0xAA and follows this exact layout. The very last byte is the only thing that changes between ANC ON / OFF / Transparency.
Field-by-field Breakdown
| Byte | Field | Size | What it means | Value in example |
|---|---|---|---|---|
| 0 | SOF | 1 B | Start-of-Frame marker — every valid packet begins with 0xAA. If you don't see AA at byte 0, the packet is garbage or you're reading the wrong characteristic. | AA |
| 1 | LEN | 1 B | Length of everything from byte 4 (CAT) to the end. 0x0A = 10 means 10 more bytes follow after byte 3. Use this to know when a packet ends. | 0A (= 10) |
| 2–3 | PAD | 2 B | Always 0x00 0x00. Padding / reserved. Probably for future use or alignment. Always zero — don't touch them. | 00 00 |
| 4 | CAT | 1 B | Category — which subsystem are we talking to? 0x00 = System (hello/register), 0x03 = Device Info, 0x04 = ANC, 0x05 = Equalizer, 0x06 = Battery. | 04 (ANC) |
| 5 | SUB | 1 B | Sub-command within the category. 0x04 = Set (write a new value), 0x82 = Query (ask what the current value is), 0x01 = Basic command, 0x85 = Registration. | 04 (Set) |
| 6 | SEQ | 1 B | Sequence number — a simple counter so both sides can match requests to responses. Increments by 2: 0x40 → 0x42 → 0x44. The device echoes this back in its ACK. | 42 |
| 7 | FLAG | 1 B | Mode flags — always 0x03 for ANC set commands. Likely a bitmask: bit 0 = persist to flash, bit 1 = notify both buds simultaneously. | 03 |
| 8–10 | DATA | 3 B | Fixed payload for ANC commands. Always 0x00 0x01 0x01. Possibly: reserved byte + left-bud flag + right-bud flag (both enabled). | 00 01 01 |
| 11 | MODE | 1 B | THE important byte. 0x01 = ANC on (noise cancellation active), 0x02 = Transparency (lets ambient sound through), 0x04 = ANC off (no processing). | 01 / 02 / 04 |
Known OPO Category Codes (CAT byte)
| CAT | System name | What you can do | Sub-commands |
|---|---|---|---|
| 0x00 | System | Say hello, register, keep-alive | 0x01 Hello · 0x85 Register |
| 0x03 | Device Info | Firmware version, model, serial | 0x01 Query → 0x81 Response |
| 0x04 | ANC | Read or set noise-cancellation mode | 0x82 Query · 0x04 Set · 0x81 Response |
| 0x05 | Equalizer | Read or set EQ preset | 0x01 Query · 0x02 Response |
| 0x06 | Battery | Read left, right, and case charge levels | 0x01 Query → 0x81 Response |
The Complete Command Sequence — Annotated
Here is the exact sequence of packets you send and the device's response to each one:
Complete Working Sequence
Every packet sent between your Mac and the Nord Buds — in order, with timing.
Why the Timing Matters
The earbuds run on a low-power microcontroller. They don't have a fast OS or a large buffer. If you fire packets too quickly, they pile up in the GATT queue and the device processes them out of order — or drops them entirely. If you wait too long between Register and the first command, the session token expires and you're back to unauthenticated.
| Transition | Minimum delay | Recommended | Why |
|---|---|---|---|
| HELLO → REGISTER | ~1.5 s | 2.0 s | Device needs to initialise the session and prep its token validator |
| REGISTER → QUERY | ~1.0 s | 1.5 s | Token validation and state sync between L + R buds via internal RF |
| QUERY → SET | ~1.0 s | 1.5 s | Query must complete and response must be acknowledged before set |
The Mode Byte Values
The Raw Packets — All Four Steps
Full Implementation
The complete Swift script. Drop it anywhere, chmod +x it, run it with swift nordbud.swift on. Requires macOS with Bluetooth powered on and the buds already paired.
What I Learned
setNotifyValue(true) call.Now I can control my earbuds from my laptop.
OnePlus couldn't give me that — I took it myself.
