From 8d709635efcd46d572d4bfe4d84b4b65041558be Mon Sep 17 00:00:00 2001 From: Piotr K Date: Fri, 3 Jul 2026 20:10:07 +0200 Subject: [PATCH] Add Bluetooth Audio Input Unlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlocks the hidden "Bluetooth audio input" (DAC/Sink mode) on the HiBy R1. Two patches to /usr/bin/hiby_player: 1. Bypass the view blocklist (FUN_004f7220) by shifting the addiu immediate +1, so it checks "g_bt_input_hiby" instead of "vg_bt_input_hiby". Original string stays intact, so event handler binding (back button, disconnect dialog) keeps working. 2. Inject the missing menu item by overwriting the volume_sync addition with a jump to a 40-byte code cave in .rodata. The cave re-executes volume_sync, then adds bt_input, then jumps back. Volume Sync stays functional. Includes patch_bt_input.py which locates strings, blocklist, menu builder, and a code cave dynamically — works across community variants without hardcoded offsets. Handles MIPS addiu sign-extension. Tested on sorting-patch hiby_player (fw 1.6): option appears in Bluetooth settings, Sink mode activates, back button and disconnect dialog work, no bootloop, USB working mode unaffected. --- README.md | 40 ++++++++++++++ patch_bt_input.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 patch_bt_input.py diff --git a/README.md b/README.md index 3b0e4d6..f3b692a 100644 --- a/README.md +++ b/README.md @@ -561,6 +561,46 @@ On device, set the slider to minimum: cat /sys/class/backlight/backlight_pwm0/brightness # should read 1, not 5 ``` --- +## HiBy R1 — Bluetooth Audio Input Unlock +**Target binary:** `/usr/bin/hiby_player` (MIPS32 LE ELF) +**Change:** Unlocks the natively supported but hidden "Bluetooth audio input" (DAC/Sink mode) in the Bluetooth settings list. Works flawlessly with UI navigation. +--- +### Background +The firmware fully supports Bluetooth Sink mode (used in R3 Pro II), but in R1 the option is removed from the settings list, and a blocklist routine prevents its view (`vg_bt_input_hiby`) from opening. + +**Change 1 (Bypass Blocklist):** The blocklist routine (`FUN_004f7220`) explicitly checks against `vg_bt_input_hiby`. We shift the string pointer by 1 byte, making it check for `g_bt_input_hiby` instead, effectively unblocking the view. +```asm +addiu s1, s1, LOW(vg_addr) -> addiu s1, s1, LOW(vg_addr) + 1 +``` + +**Change 2 (Inject Menu Item):** The UI builds the Bluetooth menu item by item. We overwrite one item addition (`volume_sync`) with a jump to a code cave (40 bytes of `0x00` in `.rodata`). In the cave, we execute the original `volume_sync` addition, followed by our new `bt_input` addition, and jump back seamlessly. +```asm +lui a1, HIGH(bt_in_addr) +move a0, s2 +jal add_item_func +addiu a1, a1, LOW(bt_in_addr) +``` +--- +### Patch Locations +Offset is build-dependent. Values for the Sorting-patch player (fw 1.6): + +| Description | Offset (hex) | Offset (dec) | Before | After | Change | +|---|---|---|---|---|---| +| Blocklist bypass | `0x000F723C` | 1,012,284 | `58 3C 31 26` | `59 3C 31 26` | `addiu s1,s1,15448` → `15449` | +| Cave Jump | `0x000AD034` | 708,660 | `79 00 05 3C` | `FE BD 1D 08` | `lui a1,0x79` → `j 0x76f7f8` | +| Code Cave | `0x0036F7F8` | 3,602,424 | `00 00 00 ...` | *(Cave payload)* | 40 bytes of payload | +--- +### How to Apply +> **Back up your original binary before patching.** + +**Using `patch_bt_input.py` (any build).** Locates the UI menu builder, blocklist offsets, and an empty code cave automatically by instruction signature. It works regardless of binary layout (handy for other community variants where the offset differs): +```bash +python3 patch_bt_input.py hiby_player # patches hiby_player in place +``` +--- +### Verification +On device, open Bluetooth settings. "Bluetooth audio input" should appear in the list. Tapping it opens the DAC/Sink mode. Tapping the back arrow returns to the menu seamlessly. +--- ## After Flashing — Manual Setup diff --git a/patch_bt_input.py b/patch_bt_input.py new file mode 100644 index 0000000..bbd0658 --- /dev/null +++ b/patch_bt_input.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +import struct +import sys + +def main(): + if len(sys.argv) < 2: + print("Usage: patch_bt_input.py ") + sys.exit(1) + + target_file = sys.argv[1] + with open(target_file, 'rb') as f: + data = bytearray(f.read()) + + def find_string(s): + idx = data.find(s + b'\x00') + if idx != -1: + return idx + 0x400000 + return None + + vg_addr = find_string(b'vg_bt_input_hiby') + vol_addr = find_string(b'volume_sync') + bt_in_addr = find_string(b'bt_input') + + if not all([vg_addr, vol_addr, bt_in_addr]): + print("Error: Could not find required strings in the binary. Are you using an unmodified R1 hiby_player?") + sys.exit(1) + + print(f"[+] Found vg_bt_input_hiby at {hex(vg_addr)}") + print(f"[+] Found volume_sync at {hex(vol_addr)}") + print(f"[+] Found bt_input at {hex(bt_in_addr)}") + + # 1. Patch the blocklist (FUN_004f7220) + # Search for addiu s1, s1, LOW(vg_addr) -> 0x2631xxxx + target_addiu_s1 = 0x26310000 | (vg_addr & 0xffff) + target_bytes_s1 = struct.pack(' 0x24a5xxxx + target_addiu_a1 = 0x24a50000 | (vol_addr & 0xffff) + target_bytes_a1 = struct.pack('> 16) != 0x3c05 or move_instr != 0x02402025 or (jal_instr >> 26) != 0x0c: + print("Error: Menu builder sequence mismatch. Has it already been patched with a code cave?") + sys.exit(1) + + add_item_func = (jal_instr & 0x03ffffff) << 2 + print(f"[+] Found menu builder sequence at {hex(menu_start_idx)}. UI add_item func: {hex(add_item_func)}") + + # 3. Find code cave (40 bytes of \x00) + cave_size = 40 + zeros = b'\x00' * cave_size + # Search in executable regions (.text or .rodata, starting around 0x300000) + cave_idx = data.find(zeros, 0x300000) + if cave_idx == -1: + print("Error: Could not find code cave of 40 zero bytes.") + sys.exit(1) + + cave_vaddr = cave_idx + 0x400000 + print(f"[+] Found code cave at file offset {hex(cave_idx)} (vaddr {hex(cave_vaddr)})") + + # 4. Generate payload (Code Cave) + # We will execute the original volume_sync addition, then add our new bt_input, then jump back. + def get_high_low(addr): + low = addr & 0xffff + high = addr >> 16 + if low >= 0x8000: + high += 1 + return high, low + + vol_high, vol_low = get_high_low(vol_addr) + bt_high, bt_low = get_high_low(bt_in_addr) + + payload = struct.pack('> 2)) # j return_addr + payload += struct.pack('> 2) + overwrite = struct.pack('