Open Bodyash opened 7 months ago
Current this extension getting values from Gnome Bluetooth which is reported by bluez it reports only a single battery. So currently it might not be possible.
However I will keep this issue open, right I do not know how get information for L/R/case, but if I find something I will update it here. If you know any other way for getting individual battery info for your sony device using scripts or commandline, let me know.
bluetoothctl reports only one battery level
How it looks on android:
And whats interesting - bluetothctl reports 70%, while android reports 50% for case and 100% for L/R
I also do not know how bluez report 70%. Seem like bluez is reporting average of left and right.
Also i think bluez interacts with pipewire for headphone battery information.
Correct way would be to get the minimum of left and right instead of average.
I think it is better to create an issue with bluez.
https://github.com/bluez/bluez/issues
But somebody needs to create a request for them to consider and start working on it.
As for extension to get this information directly would require directly communucation with RFCOMM, which I do not know if its possible, and even if it is possible would require lot of work. But if bluez manages to report Left Right and or Case information, than I can add the functionality to extension.
Yikes, I was just about to create another extension to display multiple battery levels. There should be multiple "Battery Level" GATT characteristics available in bluetoothctl for a single device, thus the sequence should be:
bluetoothctl gatt.list-attributes | grep -B 2 "Battery Level"
then you copy paste the /org/bluez/* address that you got and:
bluetoothctl
menu gatt
select-attribute <address>
read
I made a script using bash and pydbus to do that automatically, I would love to get a feedback if it works. One issue is that I have no idea if you can somehow get a label for the batteries, i.e. which one is left and which one is right:
import subprocess
from pydbus import SystemBus
bash_script = """
#!/bin/bash
bluetoothctl gatt.list-attributes | grep -B 2 "Battery Level" --no-group-separator | grep "bluez" | while read line; do
echo ${line}
done
"""
process = subprocess.Popen(
bash_script, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
result, error = process.communicate()
if error:
print(error)
exit(1)
bus = SystemBus()
for path in result.decode().strip().split("\n"):
battery_characteristic = bus.get("org.bluez", path)
value = battery_characteristic.ReadValue({})
print(value)
I just found this by the way -> https://bbs.archlinux.org/viewtopic.php?id=236499 It appears that there's a high chance that most of the headsets use a different protocol than BLE to send battery level information
menuback
Yikes, I was just about to create another extension to display multiple battery levels. There should be multiple "Battery Level" GATT characteristics available in bluetoothctl for a single device, thus the sequence should be:
bluetoothctl gatt.list-attributes | grep -B 2 "Battery Level"
then you copy paste the /org/bluez/* address that you got and:
bluetoothctl menu gatt select-attribute <address> read
I made a script using bash and pydbus to do that automatically, I would love to get a feedback if it works. One issue is that I have no idea if you can somehow get a label for the batteries, i.e. which one is left and which one is right:
import subprocess from pydbus import SystemBus bash_script = """ #!/bin/bash bluetoothctl gatt.list-attributes | grep -B 2 "Battery Level" --no-group-separator | grep "bluez" | while read line; do echo ${line} done """ process = subprocess.Popen( bash_script, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) result, error = process.communicate() if error: print(error) exit(1) bus = SystemBus() for path in result.decode().strip().split("\n"): battery_characteristic = bus.get("org.bluez", path) value = battery_characteristic.ReadValue({}) print(value)
I have Jabra Elite 75t and using Jabras android app, I can get earBuds battery level (no left or right) and case battery level.
However forthe above above commands does now work. bluetoothctl gatt.list-attributes | grep -B 2 "Battery Level"
manually getting list attributes shows device
May be this might work for Sony headset that BodyAsh uses, for jabra it doesnot.
So for Jabra it displays two mac address. One public address that I can connect to
Name: Jabra Elite 75t
Alias: Jabra Elite 75t
Class: 0x00240404 (2360324)
Icon: audio-headset
Paired: yes
Bonded: yes
Trusted: yes
Blocked: no
Connected: yes
LegacyPairing: no
UUID: Serial Port (00001101-0000-1000-8000-00805f9b34fb)
UUID: Headset (00001108-0000-1000-8000-00805f9b34fb)
UUID: Audio Sink (0000110b-0000-1000-8000-00805f9b34fb)
UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
UUID: Advanced Audio Distribu.. (0000110d-0000-1000-8000-00805f9b34fb)
UUID: A/V Remote Control (0000110e-0000-1000-8000-00805f9b34fb)
UUID: Handsfree (0000111e-0000-1000-8000-00805f9b34fb)
UUID: PnP Information (00001200-0000-1000-8000-00805f9b34fb)
Modalias: bluetooth:v0067p24A7d0200
ManufacturerData.Key: 0x0067 (103)
ManufacturerData.Value:
03 07 01 60 03 0b a7 24 ...`...$
Battery Percentage: 0x64 (100)
one random address (assuming this is case)
device 6A:BA:3F:18:E3:34 (random)
Name: Jabra Elite 75t
Alias: Jabra Elite 75t
Paired: no
Bonded: no
Trusted: no
Blocked: no
Connected: no
LegacyPairing: no
UUID: Battery Service (0000180f-0000-1000-8000-00805f9b34fb)
UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb)
UUID: GN Netcom (0000feff-0000-1000-8000-00805f9b34fb)
But I have no clue how to get the case battery level.
I just found this by the way -> https://bbs.archlinux.org/viewtopic.php?id=236499 It appears that there's a high chance that most of the headsets use a different protocol than BLE to send battery level information
That just reminds me there is a python that a Gnome extension uses. I read script and there was something about gettting L / R and Case Battey level using RFCOMM. But unfortnately the script only works when my jabra headset is disconnected and it only shows the earbuds battery level, but that might work for other devices.
This is the extension https://github.com/MichalW/gnome-bluetooth-battery-indicator
This is the script that it used https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level
I'd love to see this implemented. I have a pair of ZMK keyboard that reports two battery values.
Multiple battery level reporting is added in ZMK here: https://github.com/zmkfirmware/zmk/pull/2045 There's lots of info and scripts/tools in the comments.
# info XX:XX:XX:XX:XX:XX
Device XX:XX:XX:XX:XX:XX (random)
Name: My keyboard
Alias: My keyboard
Appearance: 0x03c1 (961)
Icon: input-keyboard
Paired: yes
Bonded: yes
Trusted: yes
Blocked: no
Connected: yes
WakeAllowed: yes
LegacyPairing: no
UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb)
UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb)
UUID: Battery Service (0000180f-0000-1000-8000-00805f9b34fb)
UUID: Human Interface Device (00001812-0000-1000-8000-00805f9b34fb)
Modalias: bluetooth:somestringhere
AdvertisingFlags:
06 .
Battery Percentage: 0x4f (79)
# gatt.list-attributes
Primary Service (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b
0000180a-0000-1000-8000-00805f9b34fb
Device Information
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b/char0020
00002a50-0000-1000-8000-00805f9b34fb
PnP ID
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b/char001e
00002a29-0000-1000-8000-00805f9b34fb
Manufacturer Name String
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b/char001c
00002a24-0000-1000-8000-00805f9b34fb
Model Number String
Primary Service (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015
0000180f-0000-1000-8000-00805f9b34fb
Battery Service
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016
00002a19-0000-1000-8000-00805f9b34fb
Battery Level
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016/desc001a
00002901-0000-1000-8000-00805f9b34fb
Characteristic User Description
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016/desc0019
00002904-0000-1000-8000-00805f9b34fb
Characteristic Format
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016/desc0018
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
Primary Service (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010
0000180f-0000-1000-8000-00805f9b34fb
Battery Service
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011
00002a19-0000-1000-8000-00805f9b34fb
Battery Level
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011/desc0014
00002904-0000-1000-8000-00805f9b34fb
Characteristic Format
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011/desc0013
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
Primary Service (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0001
00001801-0000-1000-8000-00805f9b34fb
Generic Attribute Profile
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0001/char0007
00002b2a-0000-1000-8000-00805f9b34fb
Database Hash
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0001/char0005
00002b29-0000-1000-8000-00805f9b34fb
Client Supported Features
Characteristic (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0001/char0002
00002a05-0000-1000-8000-00805f9b34fb
Service Changed
Descriptor (Handle 0x0000)
/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0001/char0002/desc0004
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
# gatt.select-attribute /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011
# gatt.read
4f
# gatt.select-attribute /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016
# gatt.read
1b
I'm also able to read battery levels on my phone using "nRF Connect" (by Nordic Semiconductor ASA on Google Play).
hello @Genteure
Above is the rough code for getting battery level using GATT Extract and run it using
gjs-console /home/$USER/Downloads/test-gatt-battery-service.js
Let me know the results
I changed line 119 to print(`BAT${i} = ${level}%, ${path}`);
Number of battery levels reported: 2
BAT1 = null%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016
BAT2 = null%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011
Edit: it's the real null
not literal string null
, typeof(level)
gives object
.
After running bluetoothctl
gatt.select-attribute; gatt.read
it becomes
Number of battery levels reported: 2
BAT1 = 25%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016
BAT2 = 67%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011
I plugged the cable in, both bluetoothctl info [ADDR]
and this extension is showing 100% charge as expected (a hardware limitation, it's reading the USB charging voltage instead of the raw battery output)
Number of battery levels reported: 2
BAT1 = 25%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0015/char0016
BAT2 = 67%, /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service0010/char0011
Observations:
BAT2
is the "main" battery, this info could be read from another gatt attribute.Ubuntu 24.04 LTS
$ gnome-shell --version
GNOME Shell 46.0
$ gjs --version
gjs 1.80.2
$ bluetoothd -v
5.72
$ bluetoothctl -v
bluetoothctl: 5.72
$ dbus-daemon --version
D-Bus Message Bus Daemon 1.14.10
Hey @Genteure Thank you. For the corrections.
.js
files.Looks like script is reading cached values from bluez?
BAT2 is the "main" battery, this info could be read from another gatt attribute.
I only added ${path}
to the print
call to see the corresponding path and changed the mac address. I'm away from my machine right now, I can upload it later if you want.
Why do you think the readings are cached?
Because the value read by the script only changes after I called gatt.read
in bluetoothctl
. I searched for bluez docs and indeed the value
property is cached.
This is a python script that reads battery values. https://gist.github.com/madushan1000/9744eb6350a5dd9685fb6bfbb25fbb8a
There's a bit of python object mapping magic but I think it's calling the array{byte} ReadValue(dict options)
method. It also reads the characteristic descriptor for battery name.
@Genteure
test2-gatt-battery-service.zip
Can you test this script?
BAT2 is the "main" battery, this info could be read from another gatt attribute.
is there any way to distinguish which is main and which is aux?
I got a script working with my zmk keyboard based on your scripts and various docs. It should also work with any other bluetooth device that exposes multiple battery services.
const { Gio, GLib } = imports.gi;
// import GLib from 'gi://GLib';
// import Gio from 'gi://Gio';
const BLUEZ_BUS_NAME = 'org.bluez';
const BLUEZ_ROOT_PATH = '/';
const BLUEZ_DEVICE = 'org.bluez.Device1';
const BLUEZ_GATT_SERVICE = 'org.bluez.GattService1'
const BLUEZ_GATT_CHARACTERISCITC = 'org.bluez.GattCharacteristic1'
const BLUEZ_GATT_DESCRIPTOR = 'org.bluez.GattDescriptor1'
const UUID_SERVICE_BATTERY = "0000180f-0000-1000-8000-00805f9b34fb"
const UUID_CHAR_BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb"
// Characteristic Presentation Format
const UUID_CHAR_PRESENTATION_FORMAT = "00002904-0000-1000-8000-00805f9b34fb"
// Characteristic User Description
const UUID_CHAR_USER_DESC = "00002901-0000-1000-8000-00805f9b34fb"
const bus = Gio.DBus.system;
function getManagedObjects() {
const objManager = Gio.DBusProxy.new_sync(
bus,
Gio.DBusProxyFlags.NONE,
null,
BLUEZ_BUS_NAME,
BLUEZ_ROOT_PATH,
'org.freedesktop.DBus.ObjectManager',
null,
);
let result = objManager.call_sync(
'GetManagedObjects',
null,
Gio.DBusCallFlags.NONE,
-1,
null
);
return result.get_child_value(0).deepUnpack();
}
/**
* Searches for devices with battery service.
*
* @param {Object} managedObjects - The managed objects containing device information.
* @returns {Array<string>} - An array of object paths for devices with battery service.
*/
function searchDevicesWithBatteryService(managedObjects) {
let devices = new Set();
for (let [objectPath, interfaces] of Object.entries(managedObjects)) {
if (BLUEZ_GATT_SERVICE in interfaces) {
let service = interfaces[BLUEZ_GATT_SERVICE];
let uuid = service['UUID'].deepUnpack();
if (uuid === UUID_SERVICE_BATTERY) {
const devicePath = service['Device'].deepUnpack();
// print(`Found device: ${devicePath}`);
// example: /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX
devices.add(devicePath);
}
}
}
return [...devices];
}
/**
* Calls the ReadValue method on a characteristic or descriptor to read its value.
* @param {string} path - The path of the characteristic or descriptor.
* @param {string} interface - Either org.bluez.GattCharacteristic1 or org.bluez.GattDescriptor1.
* @returns {Uint8Array} - The value.
*/
function readBluetoothValue(path, interface) {
const callResult = bus.call_sync(
BLUEZ_BUS_NAME,
path,
interface,
'ReadValue',
new GLib.Variant('(a{sv})', [{}]),
null,
Gio.DBusCallFlags.NONE,
-1,
null
);
const byteArray = callResult.get_child_value(0).deepUnpack();
return byteArray
}
function listBatteryLevels(managedObjects, devicePath) {
// We are looping through the managed objects multiple times and
// the algorithm can be optimized, but unless we have a large number
// of devices it should be fine. I'm keeping it simple for now.
const gattServicePath = [];
// Find GATT caracteristics for battery level.
for (let [objectPath, interfaces] of Object.entries(managedObjects)) {
if (objectPath.startsWith(devicePath) && BLUEZ_GATT_CHARACTERISCITC in interfaces) {
let characteristic = interfaces[BLUEZ_GATT_CHARACTERISCITC];
let uuid = characteristic['UUID'].deepUnpack();
if (uuid === UUID_CHAR_BATTERY_LEVEL) {
// print(`Characteristic: ${characteristic['UUID'].deepUnpack()}`);
// example: /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/serviceXX/charXX
gattServicePath.push(objectPath);
}
}
}
const batteryLevels = [];
// Read battery level and user description for each battery.
for (let servicePath of gattServicePath) {
// result should be a single byte representing the battery percentage
const batteryByteArray = readBluetoothValue(servicePath, BLUEZ_GATT_CHARACTERISCITC);
const batteryPercent = batteryByteArray[0];
let batteryLevel = {
batteryPercent: batteryPercent,
displayName: "Main",
sortOrder: -1,
userDesc: null,
presentDesc: null
};
// Read Characteristic Presentation Format for battery description.
for (let [objectPath, interfaces] of Object.entries(managedObjects)) {
if (objectPath.startsWith(servicePath) && BLUEZ_GATT_DESCRIPTOR in interfaces) {
let descriptor = interfaces[BLUEZ_GATT_DESCRIPTOR];
let uuid = descriptor['UUID'].deepUnpack();
if (uuid === UUID_CHAR_PRESENTATION_FORMAT) {
const byteArray = readBluetoothValue(objectPath, BLUEZ_GATT_DESCRIPTOR);
// print(`Presentation Format: (length: ${byteArray.length}) ${formatByteArray(byteArray)}`);
// byte 0: format, 0x04 for unsigned 8-bit integer
// byte 1: exponent, 0x00
// byte 2-3: unit, 0x27ad for percentage
// byte 4: namespace, 0x01 for Bluetooth SIG assigned numbers
// byte 5-6: description
if (byteArray.length !== 7) {
print(`Invalid Presentation Format: ${formatByteArray(byteArray)}`);
batteryLevel = null;
break;
}
if (byteArray[0] !== 0x04 || byteArray[1] !== 0x00 || byteArray[2] !== 0xad || byteArray[3] !== 0x27 || byteArray[4] !== 0x01) {
print(`Unsupported Presentation Format: ${formatByteArray(byteArray)}`);
batteryLevel = null;
break;
}
// byte 5-6 is the description
let cpfDesc = byteArray[5] | (byteArray[6] << 8);
if (cpfDesc === 0x0106) { // main
batteryLevel.sortOrder = -1;
} else {
batteryLevel.sortOrder = cpfDesc;
}
batteryLevel.presentDesc = cpfDescToName(cpfDesc);
} else if (uuid === UUID_CHAR_USER_DESC) {
const byteArray = readBluetoothValue(objectPath, BLUEZ_GATT_DESCRIPTOR);
// print(`User Description: (length: ${byteArray.length}) ${decodeByteArray(byteArray)}`);
batteryLevel.userDesc = decodeByteArray(byteArray);
}
}
}
if (batteryLevel) { // if we have a valid battery level
if (batteryLevel.userDesc) { // prefer user description
batteryLevel.displayName = batteryLevel.userDesc;
} else if (batteryLevel.presentDesc) { // fallback to presentation format
batteryLevel.displayName = batteryLevel.presentDesc;
}
batteryLevels.push(batteryLevel);
}
}
batteryLevels.sort((a, b) => a.sortOrder - b.sortOrder); // sort by sortOrder, which is the CPF description
return batteryLevels;
}
// Bluetooth SIG GATT Characteristic Presentation Format Description
// https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.html#bookmark46
function cpfDescToName(cpf) {
if (cpf < 0) return 'Invalid';
if (cpf === 0x0000) return 'Unknown';
if (cpf <= 0x00FF) {
const tenth = cpf % 10, moduloHundred = cpf % 100;
if (tenth === 1 && moduloHundred !== 11) { return cpf + "st"; }
if (tenth === 2 && moduloHundred !== 12) { return cpf + "nd"; }
if (tenth === 3 && moduloHundred !== 13) { return cpf + "rd"; }
return cpf + "th";
}
switch (cpf) {
case 0x0100: return 'Front';
case 0x0101: return 'Back';
case 0x0102: return 'Top';
case 0x0103: return 'Bottom';
case 0x0104: return 'Upper';
case 0x0105: return 'Lower';
case 0x0106: return 'Main';
case 0x0107: return 'Backup';
case 0x0108: return 'Auxiliary';
case 0x0109: return 'Supplementary';
case 0x010A: return 'Flash';
case 0x010B: return 'Inside';
case 0x010C: return 'Outside';
case 0x010D: return 'Left';
case 0x010E: return 'Right';
case 0x010F: return 'Internal';
case 0x0110: return 'External';
default: return 'Invalid';
}
}
/**
* Formats a Uint8Array into a list of byte text for display.
* @param {Uint8Array} byteArray - The Uint8Array to format.
* @returns {string} - The formatted string of byte text.
*/
function formatByteArray(byteArray) {
return Array.from(byteArray).map(byte => byte.toString(16).padStart(2, '0')).join(' ');
}
/**
* Decodes a Uint8Array into a string.
* @param {Uint8Array} byteArray - The Uint8Array to decode.
* @returns {string} - The decoded string.
*/
function decodeByteArray(byteArray) {
return new TextDecoder('utf-8').decode(byteArray);
}
function main() {
let managedObjects = getManagedObjects();
let devicePaths = searchDevicesWithBatteryService(managedObjects);
for (let path of devicePaths) {
try {
let batteryLevels = listBatteryLevels(managedObjects, path);
print(`Device: ${path}`);
for (let batteryLevel of batteryLevels) {
print(` ${batteryLevel.displayName}: ${batteryLevel.batteryPercent}%`);
print(` Description: ${batteryLevel.userDesc}`);
print(` Presentation: ${batteryLevel.presentDesc}`);
}
} catch (error) {
print(`Error reading battery level for ${path}: ${error}`);
}
}
}
main();
Device: /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX
Main: 42%
Description: null
Presentation: Main
Peripheral 0: 22%
Description: Peripheral 0
Presentation: Auxiliary
Nice thanks.
So I am also trying to get left right case battery levels of headphone such as galaxy buds, sony etc through rfcomm but haven't been successfully. I am trying do to this purely in gjs without any python script or other language scripts.
If at all I succeed with getting L,R case level for earphones, I will make a new GUI that supports both GATT and Rfcomm to display multiple battery levels.
Have you been able to get a serial connection to your headphones?
I'm giving a try on my main earbuds "Redmi Buds 5 Pro". I got a few binary logs and a rfcomm UUID from the app com.mi.earphone, and it looks pretty promising.
The app seems to be calling BluetoothDevice.createRfcommSocketToServiceRecord (UUID uuid)
with the UUID 0000fd2d-0000-1000-8000-00805f9b34fb
.
0xfd2d
is an assigned number to Xiaomi Inc. so that looks correct. https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.html#bookmark117
This uuid is also listed in bluetoothctl info
:
Device XXXXXXXXXXXX (public)
Name: Redmi Buds 5 Pro
Alias: Redmi Buds 5 Pro
Class: 0x00244404 (2376708)
Icon: audio-headset
Paired: yes
Bonded: yes
Trusted: no
Blocked: no
Connected: yes
LegacyPairing: no
UUID: Vendor specific (00000000-0000-0000-0099-aabbccddeeff)
UUID: Vendor specific (00000000-deca-fade-deca-deafdecacaff)
UUID: Audio Sink (0000110b-0000-1000-8000-00805f9b34fb)
UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
UUID: A/V Remote Control (0000110e-0000-1000-8000-00805f9b34fb)
UUID: Handsfree (0000111e-0000-1000-8000-00805f9b34fb)
UUID: PnP Information (00001200-0000-1000-8000-00805f9b34fb)
UUID: Audio Input Control (00001843-0000-1000-8000-00805f9b34fb)
UUID: Volume Control (00001844-0000-1000-8000-00805f9b34fb)
UUID: Volume Offset Control (00001845-0000-1000-8000-00805f9b34fb)
UUID: Coordinated Set Identif.. (00001846-0000-1000-8000-00805f9b34fb)
UUID: Microphone Control (0000184d-0000-1000-8000-00805f9b34fb)
UUID: Audio Stream Control (0000184e-0000-1000-8000-00805f9b34fb)
UUID: Broadcast Audio Scan (0000184f-0000-1000-8000-00805f9b34fb)
UUID: Published Audio Capabil.. (00001850-0000-1000-8000-00805f9b34fb)
UUID: Common Audio (00001853-0000-1000-8000-00805f9b34fb)
UUID: Telephony and Media Audio (00001855-0000-1000-8000-00805f9b34fb)
UUID: Unknown (0000fd2d-0000-1000-8000-00805f9b34fb)
UUID: Vendor specific (0000ff01-0000-1000-8000-00805f9b34ff)
UUID: Vendor specific (fa349b5f-8050-0030-0010-00001bbb231d)
Modalias: bluetooth:xxxxxxxxxx
Battery Percentage: 0x41 (65)
Now the problem: There's no RFCOMM port listed for fd2d
under linux.
Running sdptool browse <addr>
shows 4 Service with "RFCOMM", and fd2d
is not listed anywhere in the output. Redacted output showing only service with RFCOMM listed:
Service Name: Hands-Free unit
Service RecHandle: 0x10000
Service Class ID List:
"Handsfree" (0x111e)
"Generic Audio" (0x1203)
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 1
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
Profile Descriptor List:
"Handsfree" (0x111e)
Version: 0x0108
-----
Service Name: BTNOTIFYR
Service RecHandle: 0x20000000
Service Class ID List:
UUID 128: 0000ff01-0000-1000-8000-00805f9b34ff
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 8
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
-----
Service Name: Airoha_APP
Service RecHandle: 0x20000002
Service Class ID List:
UUID 128: 00000000-0000-0000-0099-aabbccddeeff
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 21
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
Profile Descriptor List:
"Serial Port" (0x1101)
Version: 0x0102
-----
Service Name: Airoha_IAP2
Service RecHandle: 0x2000000d
Service Class ID List:
UUID 128: 00000000-deca-fade-deca-deafdecacaff
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 30
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
I rebooted this machine into Windows and paired the earbuds there, that service is showing up with the name "xiaoai", so it looks like is something with linux/bluez.
I tried changing ControllerMode = bredr
in bluez config but that made no difference.
Any suggestions on what to try next?
Does your mi buds report left, right and case charging values in android app? Unfortunately I do not have bluetooth device that reports seperate battery level even in android OEM app ( Jabra earbuds)
Honestly I do not know much about bluetooth, and I think you know much more than I do :) I am still learning.
Can you test this python script. I want to know if earbud do send seperate battery levels test-rfcomm.zip
Change the MAC address in the script, add yours.
First test is with earbuds connected. Most likely I will not get any data. Let the script complete (may take some time) and exit.
Disconnect the earbuds but keep it ON and run the script. The script should be able to connect to one of the channels and get data. Post the data here.
You should have python3 installed. The script is based on https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/blob/master/bluetooth_battery.py
and also I had a look at pipewire https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/plugins/bluez5/backend-native.c#L1112
You can even try TheWeirdDev's script and see what it output. Most likely that also will work only when disconnected. But you will also have to install python-bluez for that.
I just started digging about bluetooth in the last month of two.
Yes the app does have separate display for left, right and case.
test-rfcomm.py
gives really inconsistent result, it behaves like the connection is being rate limited. It feels like I can only try to connect about 5 times in a row, and any more it fails with [Errno 111] Connection refused
even when it should be able to connect. Needs to wait ~1 minute between scans.
Successfully connected on channel 1!
Received data: AT+BRSF=1023
Transmit BRSF
Received data: AT+CIND=?
Transmit CIND=
Received data: AT+CIND?
Transmit CIND?
Received data: AT+CMER=3,0,0,1
Transmit OK
Received data: AT+BIND=1,2
Transmit OK
Received data: AT+BIND=?
Transmit BIND=?
Received data: AT+BIND?
Transmit BIND?
Received data: AT+NREC=0
Transmit OK
Received data: AT+CCWA=1
Transmit OK
Received data: AT+BIA=0,1,1,1,0
Transmit OK
Received data: AT+CLIP=1
Transmit OK
AT+VGS=10data: AT+XAPL=MTK-HB-0400,2
Transmit XAPL
AT+BIEV=2,100: AT+BIEV=2,100
Transmit BIEV
Error with channel 1: invalid literal for int() with base 10: b'100\rAT+BIEV'
Closing socket
00000000-0000-0000-0099-aabbccddeeff
Airoha_APP
port 21, able to connect, to me feels like an example phone app by Airoha (the chip maker) left unused and not deleted by xiaomi.
00000000-deca-fade-deca-deafdecacaff
Airoha_IAP2
port 30, able to connect, is Apple related stuff https://wiomoc.de/misc/posts/mfi_iap.html
0000ff01-0000-1000-8000-00805f9b34ff
BTNOTIFYR
port 8, unable to connect, I have no idea what this is.
The only other port I'm able to connect is 24, which might be the fd2d
service? I tried sending the binary data I got from the app log but got no response.
I went through the app again, there's something called MMA Service
which I have no idea what it stands for. The app checks a few flags from both the system and a device database (unsure about this) and if conditions are met, packed everything into a android.os.Parcel
and passed it to a android.os.IBinder
. It also receives data through a android.os.Binder
implementation, and unpacks byte[]
from android.os.Parcel
. I don't have android dev experience but based on the naming and the behavior, I'm guessing another (perhaps system level) component is handling the real communication. The app itself did open a rfcomm socket but left unused. I'm going to try again on another non xiaomi phone later.
Request: FEDCBAC402000500FFFFFFFFEF
Example incomplete response: FEDCBA04020038000011005265646D69204275647320352050726F0501418841880202C605032717506C0204000205000306
-parseTargetInfo- type :0,value: 5265646D69204275647320352050726F
-parseTargetInfo- type :1,value: 41884188
-parseTargetInfo- >> hBuf : 0001000000000001, lBuf : 0100000001000000
-parseTargetInfo- >> version : 16776, name : 4.1.8.8
-parseTargetInfo- >> hBuf : 0001000000000001, lBuf : 0100000001000000
-parseTargetInfo- >> subversion : 16776, subname : 4.1.8.8
-parseTargetInfo- type :2,value: C6
-parseTargetInfo- type :3,value: 2717506C
-parseTargetInfo- type :4,value: 00
-parseTargetInfo- type :5,value: 00
-parseTargetInfo- type :6,value: 4188
-parseTargetInfo- >> uboot version : 16776, name : 4.1.8.8
-parseTargetInfo- type :8,value: 01
-parseTargetInfo- type :7,value: C6D546
-parseTargetInfo- type :13,value: 01
Not sure about what each byte means yet, but 02
is "get target info", the byte following 02
is a incrementing sequence number used to correspond responds to requests.
In the response there are 10 smaller sections, the format is 1 byte length, 1 byte type, (length-1) byte value. We are interested in type 7.
C6D546
is the battery info, one byte per side in the order of left, right, case. The first bit is charging status.
0xC6 & 127 == 70
, 0xD5 & 127 = 85
, 0x46 = 70
. So it's left charging 70%, right charging 85%, case not charging 70%. These numbers matches what was displayed in the app.
The protocol is there, just need a way to send it and receive the response.
This would be so much easier if I have a pair of Galaxy Buds or airpods and having a known working reference.
Looks like gjs does not support opening rfcomm sockets. I tried passing in raw numbers but that didn't work either.
const { Gio } = imports.gi;
const PF_BLUETOOTH = 31;
const BTPROTO_RFCOMM = 3;
const sock = Gio.Socket.new(PF_BLUETOOTH, Gio.SocketType.SOCK_STREAM, BTPROTO_RFCOMM);
JS ERROR: Error: 31 is not a valid value for enum argument family
I think the only way to keep most of the logic within gjs is creating a rfcomm socket elsewhere and call Gio.Socket.new_from_fd(fd)
.
I found the unfortunate reason why I didn't got a response from my earbuds: the connection needs to be authenticated.
SPP_SEND::data(26 Bytes) [FEDCBAC450001215019B014C7C182E2202D22AE800F3EE93FAEF]
SPP_RCV::data:[FEDCBA0450001300150177B109A3026955718791028FC916C151EF](27 Bytes)
SPP_SEND::data(11 Bytes) [FEDCBAC4510003160100EF]
SPP_RCV::data:[FEDCBA04510003001601EF](11 Bytes)
SPP_RCV::data:[FEDCBAC050001200015F1146ED177BB4C70575B45F5034C373EF](26 Bytes)
SPP_SEND::data(27 Bytes) [FEDCBA0450001300000174395BA52FC7F9DC30938A84E494D3F2EF]
SPP_RCV::data:[FEDCBAC0510003010100EF](11 Bytes)
SPP_SEND::data(11 Bytes) [FEDCBA04510003000101EF]
SPP_SEND::data(13 Bytes) [FEDCBAC402000517FFFFFFFFEF]
SPP_RCV::data:[FEDCBA04020038001711005265646D69204275647320352050726F0501418841880202E405032717506C0204010205000306](64 Bytes)
FEDCBA
looks like the preamble, and EF
at the end is the postamble.
The 4th byte is flags.
The 5th byte is message id.
0x50
80
= auth check0x51
81
= auth send calc result0x02
2
= get target infoThe app first authenticate the device (first 4 messages). If we were to implement this, we can just send some random bytes (or the same bytes all the time) and not check the response, we don't need to check if it's a genuine xiaomi earbuds anyway.
Then the device authenticate the app (the next 4 messages). It generates a byte array and expect the app to do some math on it. In the app the calculation is implemented in a native libxm_bluetooth.so
binary.
import socket
import threading
import time
def receive_data(sock):
while True:
data = sock.recv(1024)
print(f">> {data}")
def main():
host = 'XX:XX:XX:XX:XX:XX'
port = 24
# sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
try:
sock.connect((host, port))
print("Connected")
except socket.error as e:
print(f"Failed to connect: {e}")
return
threading.Thread(target=receive_data, args=(sock,)).start()
# FEDCBAC450001215019B014C7C182E2202D22AE800F3EE93FAEF
sock.send(b"\xFE\xDC\xBA\xC4\x50\x00\x12\x15\x01\x9B\x01\x4C\x7C\x18\x2E\x22\x02\xD2\x2A\xE8\x00\xF3\xEE\x93\xFA\xEF")
time.sleep(1)
# FEDCBAC4510003160100EF
sock.send(b"\xFE\xDC\xBA\xC4\x51\x00\x03\x16\x01\x00\xEF")
time.sleep(6)
if __name__ == "__main__":
main()
Connected
>> b'\xfe\xdc\xba\x04P\x00\x13\x00\x15\x01w\xb1\t\xa3\x02iUq\x87\x91\x02\x8f\xc9\x16\xc1Q\xef'
>> b'\xfe\xdc\xba\x04Q\x00\x03\x00\x16\x01\xef'
>> b'\xfe\xdc\xba\xc0P\x00\x12\x00\x01\xc9\x1c\x03\xc8o\x0e\xa7\xb6\n\xb7\xf8\xdc\xec\x1e\x05\xe4\xef'
>> b'\xfe\xdc\xba\xc0P\x00\x12\x00\x01\xc9\x1c\x03\xc8o\x0e\xa7\xb6\n\xb7\xf8\xdc\xec\x1e\x05\xe4\xef'
>> b'\xfe\xdc\xba\xc0P\x00\x12\x00\x01\xc9\x1c\x03\xc8o\x0e\xa7\xb6\n\xb7\xf8\xdc\xec\x1e\x05\xe4\xef'
I don't think I can make any progress beyond this. I have zero knowledge about native binary reverse engineering.
Looking from implementing this into extension perspective, I am looking for a standard way rather than reverse engineering packets, each and every manufacturer/model earbuds.
I am assuming IPHONEACCEV reports individual battery as well, but I do not have earbuds that reports L,R and Case levels, or even L and R channels individually. First I need to confirm this.
If it does, than there are many other hurdles.
RFCOMM in purely GJS hurdles. You are correct, Gio.Sockets does not support bluetooth RFCOMM/L2CAP, and only way around might be to find a way to call Gio.Socket.new_from_fd(fd).
PythonBluez hurdles Incase I cannot do it using gjs only, gnome-extension-reviewer may make an exemption to use python script. PythonBluez are deprecated, but I think we can workaround by only importing sockets which is a standard with python and the probably we can use gjs to do other bluetooth related stuff such as getting uuid, mac address (just a thought)
RFCOMM hurdles I think since pipewire / pulse audio uses these port, still I am not sure if there is a way around establishing reliable rfcomm connections. I am not sure but I think, this is the reason why these python scripts only work when my earbuds are disconnected but paired and still ON. I do not have OEM specific Vendor UUID for my earbuds but I think it might be a possibility if bluetooth device have vendor uuid.
You could check this using btmon
sudo btmon | grep -A1 -E "IPHONEACCEV|BIEV|XEVENT=BATTERY"
My earbuds output of btmon
sudo btmon | grep -A1 -E "IPHONEACCEV|BIEV|XEVENT=BATTERY"
41 54 2b 42 49 45 56 3d 32 2c 37 31 0d 40 AT+BIEV=2,71.@
< SCO Data TX: Handle 257 flags 0x00 dlen 60 #15404 [hci0] 64.494182
--
41 54 2b 49 50 48 4f 4e 45 41 43 43 45 56 3d 32 AT+IPHONEACCEV=2
2c 31 2c 37 2c 32 2c 30 0d 40 ,1,7,2,0.@
This is what I get on my earbuds.
BIEV report as battery level 71 AT+BIEV=2,71
AT+IPHONEACCEV=2,1,7,2,0 The 7 indicates the level of Battery that should be incremented by 1 and multipled by 10, so level is 80%
I just found some discussion here
https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/issues/16
https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/issues/33
Some examples of custom UUID's used in script https://github.com/timschneeb/GalaxyBuds-BatteryLevel
https://github.com/Toxblh/sony-headphones-control-linux/blob/master/prototype/main.py
With your knowledge of Bluetooth & programming I think you will be able to achieve this..
I made a bluetooth hci capture on my phone.
I don't see any IPHONEACCEV
.
There are lots of AT+XIAOMI=1,1,74,228,228,75,229
with different numbers, although after AT+XIAOMI=FF....FF
exchanges. The earbuds sends the first AT+XIAOMI
message.
I'm 100% sure those numbers are battery statuses. 228 = 0b10000000 | 100
, and 75 is the case battery at the time.
Looking at bluetooth captures on my linux machine, there's no AT+XIAOMI
messages at all.
I tried connecting to HFP (RFCOMM channel 1) using a python script based on https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/blob/68c086298bbc930f6f2e8e3d588a41f5a88aa79b/bluetooth_battery.py#L149-L195 and modified based on the hci pcap I took on my phone, but I also wasn't able to get a AT+XIAOMI
message from the earbuds.
IPHONEACCEV
looks like an Apple specific command, it's not that universal like a bluetooth standard. Xiaomi clearly made their own commands too.
For my specific earbud model, there's a IAP2 bluetooth services. This is my speculation but I wouldn't be surprised if there's a protocol (potentially newer than custom AT commands) in there for communicating battery info to Apple devices.
The technical specification for iAP2 is only available to the members of the MFi licensing program https://apple.stackexchange.com/questions/216219
I think since pipewire / pulse audio uses these port, still I am not sure if there is a way around establishing reliable rfcomm connections. I am not sure but I think, this is the reason why these python scripts only work when my earbuds are disconnected but paired and still ON.
Yes indeed. When I connect to RFCOMM channel 1 (HFP) on my earbuds using a python script, the "bluetooth connected" sound plays. And when the earbuds is connected via "normal" methods, from wireshark captures it's obvious one of the components (I'm not sure which) is connecting to it. Honestly at this point I don't think this path is very viable.
Both of the devices mentioned in this thread are impossible to support via pure GJS because they need RFCOMM.
For the Sony device mentioned above, you can use the service df21fe2c-2515-4fdb-8886-f12c4d67927c
which is Google Fast Pair Service (GFPS). This is a well documented service available in many different brands of headphones, connect via RFCOMM and you can query battery information right away. https://developers.google.com/nearby/fast-pair/specifications/extensions/messagestream
For the Xiaomi device, since data is exchanged via proprietary AT commands in-band on the HFP RFCOMM channel, support for this must be integrated into pipewire or pulseaudio. Pipewire will query for battery status over the HFP RFCOMM channel and register it with BlueZ over DBus, and it's possible to register multiple batteries with BlueZ for one device as far as I can tell. So if you ever figure out how to make the headphones report battery info, please consider submitting a patch to pipewire.
Both of the devices mentioned in this thread are impossible to support via pure GJS because they need RFCOMM.
But we have been trying with Python script using bluez/socket lib and were not a successful to get even a single battery level (forget about multiple ) using RFcomm when device connected and or streaming.
Only when I disconnected the device and then run the script I can communicate with RFcomm and get battery level.
I guess Pipewire/pa take over the RFcomm channel and hence the script cannot established RFcomm connection when device is connected/streaming. I didn't find a way around this. My knowledge regarding Bluetooth protocols is very limited.
That's correct, if the data can only be accessed in-band, they must be implemented in pipewire or pulseaudio instead. The HFP RFCOMM channel will always be used by pipewire or pulseaudio, and it's not possible to multiplex that connection.
For GFPS there is no such restriction since it's a separate service, and there are existing implementations such as this project: https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level. If you can do python scripting in the extension, support for GFPS can be implemented easily.
BIEV report as battery level 71 AT+BIEV=2,71
AT+IPHONEACCEV=2,1,7,2,0 The 7 indicates the level of Battery that should be incremented by 1 and multipled by 10, so level is 80%
By the way, I forgot to point out that this is incorrect. 2,1,7,2,0
decodes as "two kv pairs, battery 80%, undocked". First number the number of kv pairs. Key 1 means battery and the value is 0-9, key 2 means docking status, value is 0 (undocked) or 1 (docked). The docking status doesn't necessarily refer to the earbuds being in the charging case, Apple did not provide any clarifications on that.
Hello
I have sony wf-c700n
On android phone it shows case charge level and also charge level for left and right headphone
Is it possible on linux?