Skip to content

Commit 8d70963

Browse files
committed
Add Bluetooth Audio Input Unlock
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.
1 parent aee59b9 commit 8d70963

2 files changed

Lines changed: 173 additions & 0 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,46 @@ On device, set the slider to minimum:
561561
cat /sys/class/backlight/backlight_pwm0/brightness # should read 1, not 5
562562
```
563563
---
564+
## HiBy R1 — Bluetooth Audio Input Unlock
565+
**Target binary:** `/usr/bin/hiby_player` (MIPS32 LE ELF)
566+
**Change:** Unlocks the natively supported but hidden "Bluetooth audio input" (DAC/Sink mode) in the Bluetooth settings list. Works flawlessly with UI navigation.
567+
---
568+
### Background
569+
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.
570+
571+
**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.
572+
```asm
573+
addiu s1, s1, LOW(vg_addr) -> addiu s1, s1, LOW(vg_addr) + 1
574+
```
575+
576+
**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.
577+
```asm
578+
lui a1, HIGH(bt_in_addr)
579+
move a0, s2
580+
jal add_item_func
581+
addiu a1, a1, LOW(bt_in_addr)
582+
```
583+
---
584+
### Patch Locations
585+
Offset is build-dependent. Values for the Sorting-patch player (fw 1.6):
586+
587+
| Description | Offset (hex) | Offset (dec) | Before | After | Change |
588+
|---|---|---|---|---|---|
589+
| Blocklist bypass | `0x000F723C` | 1,012,284 | `58 3C 31 26` | `59 3C 31 26` | `addiu s1,s1,15448``15449` |
590+
| Cave Jump | `0x000AD034` | 708,660 | `79 00 05 3C` | `FE BD 1D 08` | `lui a1,0x79``j 0x76f7f8` |
591+
| Code Cave | `0x0036F7F8` | 3,602,424 | `00 00 00 ...` | *(Cave payload)* | 40 bytes of payload |
592+
---
593+
### How to Apply
594+
> **Back up your original binary before patching.**
595+
596+
**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):
597+
```bash
598+
python3 patch_bt_input.py hiby_player # patches hiby_player in place
599+
```
600+
---
601+
### Verification
602+
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.
603+
---
564604

565605
## After Flashing — Manual Setup
566606

patch_bt_input.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
import struct
3+
import sys
4+
5+
def main():
6+
if len(sys.argv) < 2:
7+
print("Usage: patch_bt_input.py <hiby_player_binary>")
8+
sys.exit(1)
9+
10+
target_file = sys.argv[1]
11+
with open(target_file, 'rb') as f:
12+
data = bytearray(f.read())
13+
14+
def find_string(s):
15+
idx = data.find(s + b'\x00')
16+
if idx != -1:
17+
return idx + 0x400000
18+
return None
19+
20+
vg_addr = find_string(b'vg_bt_input_hiby')
21+
vol_addr = find_string(b'volume_sync')
22+
bt_in_addr = find_string(b'bt_input')
23+
24+
if not all([vg_addr, vol_addr, bt_in_addr]):
25+
print("Error: Could not find required strings in the binary. Are you using an unmodified R1 hiby_player?")
26+
sys.exit(1)
27+
28+
print(f"[+] Found vg_bt_input_hiby at {hex(vg_addr)}")
29+
print(f"[+] Found volume_sync at {hex(vol_addr)}")
30+
print(f"[+] Found bt_input at {hex(bt_in_addr)}")
31+
32+
# 1. Patch the blocklist (FUN_004f7220)
33+
# Search for addiu s1, s1, LOW(vg_addr) -> 0x2631xxxx
34+
target_addiu_s1 = 0x26310000 | (vg_addr & 0xffff)
35+
target_bytes_s1 = struct.pack('<I', target_addiu_s1)
36+
37+
blocklist_idx = data.find(target_bytes_s1)
38+
if blocklist_idx == -1:
39+
# Check if already patched to +1
40+
target_addiu_s1_patched = 0x26310000 | ((vg_addr + 1) & 0xffff)
41+
if data.find(struct.pack('<I', target_addiu_s1_patched)) != -1:
42+
print("[*] Blocklist already bypassed.")
43+
else:
44+
print("Error: Could not find blocklist instruction. Is the binary already heavily modified?")
45+
sys.exit(1)
46+
else:
47+
print(f"[+] Found blocklist instruction at file offset {hex(blocklist_idx)}. Bypassing blocklist...")
48+
new_addiu_s1 = 0x26310000 | ((vg_addr + 1) & 0xffff)
49+
data[blocklist_idx:blocklist_idx+4] = struct.pack('<I', new_addiu_s1)
50+
51+
# 2. Patch the menu builder
52+
# Search for addiu a1, a1, LOW(vol_addr) -> 0x24a5xxxx
53+
target_addiu_a1 = 0x24a50000 | (vol_addr & 0xffff)
54+
target_bytes_a1 = struct.pack('<I', target_addiu_a1)
55+
56+
# Only pick the first occurrence (stock code)
57+
menu_idx = data.find(target_bytes_a1)
58+
if menu_idx == -1:
59+
print("Error: Could not find menu builder instruction.")
60+
sys.exit(1)
61+
62+
menu_start_idx = menu_idx - 12
63+
# Verify the sequence:
64+
# lui a1, HIGH(vol_addr)
65+
# move a0, s2
66+
# jal add_item
67+
# addiu a1, a1, LOW(vol_addr)
68+
69+
lui_instr, = struct.unpack('<I', data[menu_start_idx:menu_start_idx+4])
70+
move_instr, = struct.unpack('<I', data[menu_start_idx+4:menu_start_idx+8])
71+
jal_instr, = struct.unpack('<I', data[menu_start_idx+8:menu_start_idx+12])
72+
73+
if (lui_instr >> 16) != 0x3c05 or move_instr != 0x02402025 or (jal_instr >> 26) != 0x0c:
74+
print("Error: Menu builder sequence mismatch. Has it already been patched with a code cave?")
75+
sys.exit(1)
76+
77+
add_item_func = (jal_instr & 0x03ffffff) << 2
78+
print(f"[+] Found menu builder sequence at {hex(menu_start_idx)}. UI add_item func: {hex(add_item_func)}")
79+
80+
# 3. Find code cave (40 bytes of \x00)
81+
cave_size = 40
82+
zeros = b'\x00' * cave_size
83+
# Search in executable regions (.text or .rodata, starting around 0x300000)
84+
cave_idx = data.find(zeros, 0x300000)
85+
if cave_idx == -1:
86+
print("Error: Could not find code cave of 40 zero bytes.")
87+
sys.exit(1)
88+
89+
cave_vaddr = cave_idx + 0x400000
90+
print(f"[+] Found code cave at file offset {hex(cave_idx)} (vaddr {hex(cave_vaddr)})")
91+
92+
# 4. Generate payload (Code Cave)
93+
# We will execute the original volume_sync addition, then add our new bt_input, then jump back.
94+
def get_high_low(addr):
95+
low = addr & 0xffff
96+
high = addr >> 16
97+
if low >= 0x8000:
98+
high += 1
99+
return high, low
100+
101+
vol_high, vol_low = get_high_low(vol_addr)
102+
bt_high, bt_low = get_high_low(bt_in_addr)
103+
104+
payload = struct.pack('<I', 0x3c050000 | vol_high) # lui a1, vol_high
105+
payload += struct.pack('<I', 0x02402025) # move a0, s2
106+
payload += struct.pack('<I', jal_instr) # jal add_item_func
107+
payload += struct.pack('<I', 0x24a50000 | vol_low) # addiu a1, a1, vol_low
108+
109+
payload += struct.pack('<I', 0x3c050000 | bt_high) # lui a1, bt_high
110+
payload += struct.pack('<I', 0x02402025) # move a0, s2
111+
payload += struct.pack('<I', jal_instr) # jal add_item_func
112+
payload += struct.pack('<I', 0x24a50000 | bt_low) # addiu a1, a1, bt_low
113+
114+
ret_vaddr = menu_start_idx + 0x400000 + 16
115+
payload += struct.pack('<I', 0x08000000 | (ret_vaddr >> 2)) # j return_addr
116+
payload += struct.pack('<I', 0x00000000) # nop
117+
118+
assert len(payload) == 40
119+
data[cave_idx:cave_idx+40] = payload
120+
121+
# 5. Overwrite original menu sequence with jump to code cave
122+
j_cave = 0x08000000 | (cave_vaddr >> 2)
123+
overwrite = struct.pack('<I', j_cave) + b'\x00\x00\x00\x00' * 3
124+
data[menu_start_idx:menu_start_idx+16] = overwrite
125+
print(f"[+] Injected code cave jump at file offset {hex(menu_start_idx)}.")
126+
127+
with open(target_file, 'wb') as f:
128+
f.write(data)
129+
130+
print("[+] Patch applied successfully!")
131+
132+
if __name__ == '__main__':
133+
main()

0 commit comments

Comments
 (0)