-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
multiple battery levels #21
Comments
Current this extension getting values from Gnome Bluetooth which is reported by bluez it reports only a single battery. 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. |
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. |
Yikes, I was just about to create another extension to display multiple battery levels. 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 |
menuback
I have Jabra Elite 75t and using Jabras android app, I can get earBuds battery level (no left or right) and case battery level. 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
one random address (assuming this is case)
But I have no clue how to get the case battery level. |
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 This is the script that it used |
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: zmkfirmware/zmk#2045
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
Let me know the results |
I changed line 119 to
Edit: it's the real After running
I plugged the cable in, both
Observations:
|
Hey @Genteure
|
I only added
Because the value read by the script only changes after I called This is a python script that reads battery values. https://gist.github.com/madushan1000/9744eb6350a5dd9685fb6bfbb25fbb8a |
test2-gatt-battery-service.zip Can you test this script?
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();
|
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 This uuid is also listed in 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 Running
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 Any suggestions on what to try next? |
Does your mi buds report left, right and case charging values in android app? Honestly I do not know much about bluetooth, and I think you know much more than I do :) Can you test this python script. I want to know if earbud do send seperate battery levels 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. and also I had a look at pipewire 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.
The only other port I'm able to connect is 24, which might be the I went through the app again, there's something called Request: Example incomplete response:
Not sure about what each byte means yet, but 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.
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);
I think the only way to keep most of the logic within gjs is creating a rfcomm socket elsewhere and call I found the unfortunate reason why I didn't got a response from my earbuds: the connection needs to be authenticated.
The 4th byte is flags.
The 5th byte is message id.
The 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 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()
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. If it does, than there are many other hurdles. RFCOMM in purely GJS hurdles. PythonBluez hurdles RFCOMM hurdles You could check this using btmon
My earbuds output of btmon
This is what I get on my earbuds. BIEV report as battery level 71 AT+IPHONEACCEV=2,1,7,2,0 I just found some discussion here TheWeirdDev/Bluetooth_Headset_Battery_Level#16 TheWeirdDev/Bluetooth_Headset_Battery_Level#33 Some examples of custom UUID's used in script 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 There are lots of Looking at bluetooth captures on my linux machine, there's no 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
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. |
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 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. |
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. |
By the way, I forgot to point out that this is incorrect. |
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?
The text was updated successfully, but these errors were encountered: