TheWeirdDev / Bluetooth_Headset_Battery_Level

A python script to get battery level from Bluetooth headsets
GNU General Public License v3.0
759 stars 80 forks source link

Support Bose's proprietary battery protocol. #60

Closed sesse closed 3 years ago

sesse commented 3 years ago

Bose's headphones (at least my QC 35 II) don't seem to give battery data to any of the regular AT commands; it responds to the commands given just fine, but never gives out battery data. Thus, bluetooth_battery.py just hangs instead of finding the battery percentage.

However, Bose Connect has its own proprietary (non-AT-based) protocol, which based-connect has reverse-engineered. If we detect a Bose MAC address and the user hasn't overridden the port, connect to port 8 and speak that protocol. It supports percentage precision (just sends a number 0-100 raw over the protocol), but in practice, always seems to round off to the nearest 10%.

TheWeirdDev commented 3 years ago

Seems pretty good. Does it interrupt the audio?

sesse commented 3 years ago

No, no audio interruption. There are other commands in the same protocol that do, of course (like the one that changes noise cancellation settings), but just polling the battery level doesn't affect the audio at all.

I do get battery level on my Android phone even without installing the Bose Connect app, so I guess there's some AT command that would work, but I have no idea how to dump the RFCOMM traffic from my phone.

TheWeirdDev commented 3 years ago

It's relatively easy. You can follow these instructions.

If you found something useful in the AT commands, update the code, otherwise I'll merge this one.

sesse commented 3 years ago

Seemingly, this is enough:

index b7226cf..e26b1da 100755
--- a/bluetooth_battery.py
+++ b/bluetooth_battery.py
@@ -44,7 +44,7 @@ def get_at_command(sock, line, device):
         send(sock, b"+BIND: 2,1")
         send(sock, b"OK")
     elif b"XAPL=" in line:
-        send(sock, b"+XAPL: iPhone,7")
+        send(sock, b"+XAPL=iPhone,7")
         send(sock, b"OK")
     elif b"IPHONEACCEV" in line:
         parts = line.strip().split(b',')[1:]

My Android 10 phone reports as “XAPL=iPhone,2”, and it's the equals sign that makes the difference. No idea whether it will break other headphones.

TheWeirdDev commented 3 years ago

It's crazy how there is still no standard around this and everyone do whatever they want 🤦‍♂️.

So you tried this code and it works, right? I have an idea. Try keeping both lines (with colon and with eq sign) and see what happens. Then change the order of them to see if it breaks anything. My guess is that the headphone will pick up the one it understands and ignore the other one, as long as they are both sent before the "OK" message.

sesse commented 3 years ago

Yeah, I've tried it and it works. But no, if I send the one with the colon, it's not ignored; the device goes its merry way with asking about other things. So it's not going to work to send both.

TheWeirdDev commented 3 years ago

According to the 'Accessory Design Guidelines for Apple Devices', the response should contain equals sign. So we'll go with that. The old line wasn't tested properly because none of my devices use that.

And 2 is enough as we don't need any other features.

image

sesse commented 3 years ago

Sounds good. Should I change the pull request, or do you just want to patch it yourself?

TheWeirdDev commented 3 years ago

Change it and I'll merge.

sesse commented 3 years ago

There, update.

kos:..ooth_Headset_Battery_Level> ./bluetooth_battery.py 4c:87:5d:80:10:18
Battery level for 4c:87:5d:80:10:18 is 70%