nikitenok.dev

The LG Magic Remote microphone: two bytes, F9 03

How it turned out that the LG Magic Remote MR25GA microphone is switched on by the host with a command, how this was confirmed on Linux, and why a Mac can't do the same.

Published
Projects
nik-node
Tags
nik-node, ble, reverse-engineering

This is a follow-up to the post about using an LG Magic Remote as a controller for my Mac.

On October 25 at 2:46 a.m. a Linux machine sent the remote two bytes, F9 03, and 70 ms later audio started flowing: 66 packets per second, not a single one lost in a minute and a half. For a month and a half the obstacle wasn’t the hardware. It was that nobody ever told the remote “turn on the microphone”.

Why it didn’t work before

Voice in this remote isn’t a stream you can eavesdrop on. It’s a mode the TV switches on with a command. We had been subscribing to reports and waiting.

On the Mac, bluetoothd doesn’t enable notifications on the remote’s HID reports; the subscriptions are empty. Android as an HID host doesn’t either. But that isn’t even the main thing: nobody ever sent F9 03. Even amity, with a full BlueZ host, never saw voice. Subscribing to the voice report 0xFE isn’t enough: the remote waits for a command from the “TV”.

In hindsight it all adds up. We never saw report 0xFE once, and GET_REPORT on 0xFE and on the 0xF9 command channel returned an error. There are no answers without a command.

Where the protocol came from

The key was found not on the air but in the TV firmware. A third-party developer went through five LG webOS TV firmware images and described the protocol in PR #8 to brainrom/lg-magic. He didn’t carry any LG code into the PR, only the protocol he derived.

The PR has two limitations that matter to me. It was tested only with synthetic data, never on a live remote. And it explicitly doesn’t support the MR25: the driver only matches the MR18–MR20. Nobody had checked the MR25 protocol against 2025 TV firmware. My remote is from the new generation built on a Realtek RTL8762 and has a different vendor ID, so the opcodes could well have changed.

What we found on Linux

Pairing on Linux went through with no dialog, in Just Works mode: no need to catch a Connect button within a second and a half like on the Mac. No driver and no kernel patch were needed; hidraw and a script were enough.

First I checked the channel with a safe command: F9 11 asks for the firmware version. The reply came back within 1 ms, in the same format the PR describes. So commands do reach the remote. Then voice:

  • F9 03 — start. Within 1 ms the regular button report carries the voice-start codes, and 70 ms later the first 0xFE packet arrives.
  • F9 05 — keep the stream going. I sent it every 4 seconds, and the stream ran for 90 seconds without a break. According to the PR, without it the remote stops voice by itself after about 6 seconds.
  • F9 04 — stop.

A 0xFE packet is 124 bytes. The first byte is the report id, the second is a counter that goes up by one with every packet. Then two bytes of event code, which were zeros in all 6330 packets across all recordings. Then two 60-byte blocks: a short header, a 57-byte mSBC frame and a padding byte. mSBC is the same codec Bluetooth headsets use: 16 kHz, mono. One packet is 15 ms of audio.

In 90 seconds 5941 packets arrived, 66 per second. By the counter, zero losses; the biggest gap between packets was 98 ms.

While voice is running, the buttons and the gyroscope go quiet: report 0xFD sends 3–4 packets at the start and that’s it. After F9 04 it comes back right away. That’s the main inconvenience: while the remote is talking, you can’t see whether I’ve released the microphone button. So recording on a button press currently works like this: wait for the press, send F9 03, record a fixed 10 seconds, send F9 04. At 2:57 a.m. I pressed the button and said a phrase: the speech landed between 0.5 and 6 seconds, RMS 1200–3300 over a noise floor of about 17. The cluster’s main session ran this recording through speech recognition, and the Russian text matched word for word: «Раз, два, три. Проверка микрофона пульта. Включи свет на кухне.» (“One, two, three. Testing the remote’s microphone. Turn on the kitchen light.”)

Two things differed from PR #8. The MR25 sends gyroscope and button data on its own; it doesn’t need the F9 01 motion command. And the second byte of 0xFE is a counter, not a status byte as the PR calls it.

Can the Mac send that?

That was my first question. Send: yes. F9 03 and F9 04 go through from the Mac without errors, and the remote turns voice on when the Mac tells it to.

Receive: no. The Mac doesn’t enable notifications, so polling is all that’s left, same as with the buttons. I talked into the remote for about 20 seconds. In 25.1 seconds polling caught 187 packets: 7.5 per second out of the 66 needed, and another 327 requests returned an error. The 374 mSBC frames added up to 2.8 seconds of audio out of 25, about 11 %. The voice is in the file, but in fragments.

Why the Mac can’t do this

My next question was: “Isn’t there a way to connect it to the Mac in the same mode as on Linux?” And once more: “How do I make the Mac work the way Linux does, is there really nothing?”

Through the built-in Bluetooth, using simple standard means: no. Apple doesn’t give third-party apps the HID service. Only bluetoothd itself can enable notifications on HID reports, and for this remote it doesn’t. A helper running as root won’t help. Neither will DriverKit: the driver sits on top of an already assembled HID device and can’t reach GATT, plus it needs entitlements from Apple. You can probably dig deeper somehow, but that’s serious reverse engineering of macOS internals. I don’t want to go there for now.

What works is only a separate radio with its own stack:

  1. A USB dongle with its own BLE stack on the Mac: Bumble or BTstack via libusb.
  2. A Linux VM with the dongle passed through, running plain BlueZ inside. One catch: macOS may grab the external controller for itself. An nvram setting fixes that, but the advice was written for Catalina, and I haven’t checked it on a current macOS.
  3. A bridge on an ESP32 or nRF: the board connects to the remote itself and sends the audio to the Mac over USB.
  4. A mini PC running Linux nearby, sending the audio to the Mac over the network.

What I decided

The remote goes to Linux. If I need it on the Mac, I’ll forward things there in software.

I’m not buying a speaker with a microphone array for now. The remote provides the microphone, my JBL speaker provides the sound, and that’s what I’ll build my own smart speaker from, with no new devices.

Linux now, ESP32-S3 later. I already have the board. The plan: the ESP32-S3 connects to the remote by itself, turns voice on and sends the audio over Wi-Fi, while listening for the wake phrase right on the board. I haven’t tried anything with this remote on an ESP32 yet.

The remote’s microphone is good. I recorded the same phrase through seven microphones: the Mac on its own and through OnePlus and Sony earbuds, the phone on its own and through the same earbuds, and the remote. Only three were recognized without errors: the phone on its own, the phone through the OnePlus, and the remote. Which gives me another idea: buy a second remote like this and turn it into a mini version that reuses the microphone, or move its hardware onto an ESP32.

I’m not going to build the wake phrase into the remote itself for now, though maybe I’ll want to. The remote doesn’t listen on its own: the host switches voice on. That means custom firmware for the Realtek RTL8762 chip: writing it from scratch, risking bricking the remote, and constant listening would drain the batteries; how fast, I haven’t measured.

How to wake the speaker is still open. Three options:

  1. The AI button.
  2. A wake phrase on the host with voice streaming all the time. Then the buttons stay silent the whole time, and it’s unclear how long the batteries would last. A test comes first.
  3. A separate microphone next to the Linux machine.

What’s not solved

  • Releasing the microphone button isn’t visible while voice is running; the recording window is fixed.
  • Not checked whether the MR25 stops the stream by itself without F9 05, or whether it ever sends an end-of-voice code.
  • How to wake the speaker: a button or a wake phrase.
  • How long the batteries last with voice running constantly.

Sources