feat: extend bluetooth module

pull/1540/head
Daan Goossens 2022-05-02 18:11:21 +02:00
parent caee2e611f
commit 638b4e6573
6 changed files with 497 additions and 49 deletions

View File

@ -56,15 +56,11 @@
#endif
#ifdef HAVE_GIO_UNIX
#include "modules/inhibitor.hpp"
#include "modules/bluetooth.hpp"
#endif
#include "bar.hpp"
#include "modules/custom.hpp"
#include "modules/temperature.hpp"
#if defined(__linux__)
#ifdef WANT_RFKILL
#include "modules/bluetooth.hpp"
#endif
#endif
namespace waybar {

View File

@ -1,18 +1,77 @@
#pragma once
#include "ALabel.hpp"
#ifdef WANT_RFKILL
#include "util/rfkill.hpp"
#endif
#include <gio/gio.h>
#include <vector>
#include <string>
#include <optional>
namespace waybar::modules {
class Bluetooth : public ALabel {
struct AdapterInfo
{
std::string path;
std::string address;
std::string address_type;
// std::string name; // just use alias instead
std::string alias;
bool powered;
bool discoverable;
bool pairable;
bool discovering;
};
// NOTE: there are some properties that not all devices provide
struct DeviceInfo
{
std::string path;
std::string paired_adapter;
std::string address;
std::string address_type;
// std::optional<std::string> name; // just use alias instead
std::string alias;
std::optional<std::string> icon;
bool paired;
bool trusted;
bool blocked;
bool connected;
bool services_resolved;
// TODO: make experimental in waybar as it is also a experimental feature in bluez
std::optional<unsigned char> battery_percentage;
};
public:
Bluetooth(const std::string&, const Json::Value&);
~Bluetooth() = default;
auto update() -> void;
private:
static auto onInterfaceAddedOrRemoved(GDBusObjectManager*, GDBusObject*, GDBusInterface*, gpointer) -> void;
static auto onInterfaceProxyPropertiesChanged(GDBusObjectManagerClient*, GDBusObjectProxy*, GDBusProxy*, GVariant*, const gchar* const*, gpointer) -> void;
auto getDeviceBatteryPercentage(GDBusObject*) -> std::optional<unsigned char>;
auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool;
auto getAdapterProperties(GDBusObject*, AdapterInfo&) -> bool;
auto findCurAdapter(AdapterInfo&) -> bool;
auto findConnectedDevices(const std::string&, std::vector<DeviceInfo>&) -> void;
#ifdef WANT_RFKILL
util::Rfkill rfkill_;
#endif
const std::unique_ptr<GDBusObjectManager, void (*)(GDBusObjectManager*)> manager_;
std::string state_;
AdapterInfo cur_adapter_;
std::vector<DeviceInfo> connected_devices_;
DeviceInfo cur_focussed_device_;
std::vector<std::string> device_preference_;
};
} // namespace waybar::modules

View File

@ -6,21 +6,44 @@ waybar - bluetooth module
# DESCRIPTION
The *bluetooth* module displays information about the status of the device's bluetooth device.
The *bluetooth* module displays information about the bluetooth adapter and its connections.
# CONFIGURATION
Addressed by *bluetooth*
*adapter-alias*: ++
typeof: string ++
Use the adapter with the defined alias (name). Otherwise select a random adapter to display. Recommended to define when there is more than 1 adapter available to the system (use ```bluetoothctl show``` to show all available adapters).
*format-device-preference*: ++
typeof: array ++
A ranking of bluetooth devices, addressed by their alias (name). The order is *first displayed* to *last displayed*. If this config option is not defined or none of the devices in the list are connected, it will fall back to showing the last connected device. A devices alias can be manually changed using the ```bluetoothctl set-alias``` command.
*format*: ++
typeof: string ++
default: *{icon}* ++
default: * {status}* ++
The format, how information should be displayed. This format is used when other formats aren't specified.
*format-icons*: ++
typeof: array/object ++
Based on the device status, the corresponding icon gets selected. ++
The order is *low* to *high*. Or by the state if it is an object.
*format-disabled*: ++
typeof: string ++
This format is used when the displayed adapter is disabled.
*format-off*: ++
typeof: string ++
This format is used when the displayed adapter is turned off.
*format-on*: ++
typeof: string ++
This format is used when the displayed adapter is turned on with no devices connected.
*format-connected*: ++
typeof: string ++
This format is used when the displayed adapter is connected to at least 1 device.
*format-connected-battery*: ++
typeof: string ++
This format is used when the selected connected device, defined by the config option *format-device-preference*, provides is battery percentage. This needs the experimental features of bluez to be enabled to work.
*rotate*: ++
typeof: integer ++
@ -71,23 +94,65 @@ Addressed by *bluetooth*
typeof: string ++
The format, how information should be displayed in the tooltip. This format is used when other formats aren't specified.
*tooltip-format-disabled*: ++
typeof: string ++
This format is used when the displayed adapter is disabled.
*tooltip-format-off*: ++
typeof: string ++
This format is used when the displayed adapter is turned off.
*tooltip-format-on*: ++
typeof: string ++
This format is used when the displayed adapter is turned on with no devices connected.
*tooltip-format-connected*: ++
typeof: string ++
This format is used when the displayed adapter is connected to at least 1 device.
*tooltip-format-connected-battery*: ++
typeof: string ++
This format is used when the selected connected device, defined by the config option *format-device-preference*, provides is battery percentage. This needs the experimental features of bluez to be enabled to work.
# FORMAT REPLACEMENTS
*{status}*: Status of the bluetooth device.
*{icon}*: Icon, as defined in *format-icons*.
*{num_connections}*: Number of connections the selected adapter has.
*{adapter_address}*: Address of the selected adapter.
*{adapter_address_type}*: Address type of the selected adapter.
*{adapter_alias}*: Alias of the selected adapter. By default equal to the *adapter_name* but can be changed by the user when there are conflicts.
*{device_address}*: Address of the current selected selected connected device.
*{device_address_type}*: Address type of the current selected selected connected device.
*{device_alias}*: Alias of the current selected connected device. By default equal to the *device_name* but can be changed by the user when there are conflicts.
*{device_battery_percentage}*: Battery percentage of the current selected device if available. Only use in the *format-connected-battery* and *tooltip-format-connected-battery*.
# EXAMPLES
```
"bluetooth": {
"format": "{icon}",
"format-alt": "bluetooth: {status}",
"format-icons": {
"enabled": "",
"disabled": ""
},
"tooltip-format": "{}"
"format": " {status}",
"format-connected": " {device_alias}",
"format-connected-battery": " {device_battery_percentage}%",
"tooltip-format": "{adapter_alias} {adapter_address}"
}
```
```
"bluetooth": {
// "adapter-alias": "adapter1", // specify the adapter alias (name) if there are more than 1 on the system
"format": " {status}",
"format-connected": " {num_connections} connected",
// TODO: make it so that it shows all connected devices in the tooltip
// "tooltip-format-connected": "{device_alias}",
// "tooltip-format-connected-battery": "{device_alias} {device_battery_percentage}%",
}
```
@ -95,3 +160,9 @@ Addressed by *bluetooth*
- *#bluetooth*
- *#bluetooth.disabled*
- *#bluetooth.off*
- *#bluetooth.on*
- *#bluetooth.connected*
- *#bluetooth.discoverable*
- *#bluetooth.discovering*
- *#bluetooth.pairable*

View File

@ -253,17 +253,15 @@ endif
if (giounix.found() and not get_option('logind').disabled())
add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp')
src_files += 'src/modules/inhibitor.cpp'
src_files += 'src/modules/bluetooth.cpp'
endif
if get_option('rfkill').enabled()
if is_linux
if get_option('rfkill').enabled() and is_linux
add_project_arguments('-DWANT_RFKILL', language: 'cpp')
src_files += files(
'src/modules/bluetooth.cpp',
'src/util/rfkill.cpp'
)
endif
endif
if tz_dep.found()
add_project_arguments('-DHAVE_LIBDATE', language: 'cpp')

View File

@ -104,17 +104,13 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
if (ref == "inhibitor") {
return new waybar::modules::Inhibitor(id, bar_, config_[name]);
}
#endif
if (ref == "temperature") {
return new waybar::modules::Temperature(id, config_[name]);
}
#if defined(__linux__)
#ifdef WANT_RFKILL
if (ref == "bluetooth") {
return new waybar::modules::Bluetooth(id, config_[name]);
}
#endif
#endif
if (ref == "temperature") {
return new waybar::modules::Temperature(id, config_[name]);
}
if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) {
return new waybar::modules::Custom(ref.substr(7), id, config_[name]);
}

View File

@ -1,30 +1,358 @@
#include "modules/bluetooth.hpp"
#include <algorithm>
#include <spdlog/spdlog.h>
#include <fmt/format.h>
namespace {
using GDBusManager = std::unique_ptr<GDBusObjectManager, void (*)(GDBusObjectManager*)>;
auto generateManager() -> GDBusManager {
GError* error = nullptr;
GDBusObjectManager* manager = g_dbus_object_manager_client_new_for_bus_sync(
G_BUS_TYPE_SYSTEM,
GDBusObjectManagerClientFlags::G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
"org.bluez",
"/",
NULL,
NULL,
NULL,
NULL,
&error
);
if (error) {
spdlog::error("g_dbus_object_manager_client_new_for_bus_sync() failed: {}", error->message);
g_error_free(error);
}
auto destructor = [](GDBusObjectManager* manager) {
if (manager) {
g_object_unref(manager);
}
};
return GDBusManager{manager, destructor};
}
auto getBoolProperty(GDBusProxy* proxy, const char* property_name) -> bool {
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
bool property_value = g_variant_get_boolean(gvar);
g_variant_unref(gvar);
return property_value;
}
spdlog::error("getBoolProperty() failed: doesn't have property {}", property_name);
return false;
}
auto getOptionalStringProperty(GDBusProxy* proxy, const char* property_name) -> std::optional<std::string> {
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
std::string property_value = g_variant_get_string(gvar, NULL);
g_variant_unref(gvar);
return property_value;
}
return std::nullopt;
}
auto getStringProperty(GDBusProxy* proxy, const char* property_name) -> std::string {
auto property_value = getOptionalStringProperty(proxy, property_name);
if (!property_value.has_value()) {
spdlog::error("getStringProperty() failed: doesn't have property {}", property_name);
}
return property_value.value_or("");
}
auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned char {
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
unsigned char property_value;
g_variant_get(gvar, "y", &property_value);
g_variant_unref(gvar);
return property_value;
}
spdlog::error("getUcharProperty() failed: doesn't have property {}", property_name);
return 0;
}
} // namespace
waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config)
: ALabel(config, "bluetooth", id, "{icon}", 10), rfkill_{RFKILL_TYPE_BLUETOOTH} {
: ALabel(config, "bluetooth", id, " {status}", 10),
#ifdef WANT_RFKILL
rfkill_{RFKILL_TYPE_BLUETOOTH},
#endif
manager_(generateManager()) {
if (config_["format-device-preference"].isArray()) {
std::transform(config_["format-device-preference"].begin(),
config_["format-device-preference"].end(),
std::back_inserter(device_preference_),
[](auto x){ return x.asString(); });
}
// NOTE: assumption made that the adapter that is selcected stays unchanged
// for duration of the module
if (!findCurAdapter(cur_adapter_)) {
if (config_["adapter-alias"].isString()) {
spdlog::error("find_cur_adapter() failed: no bluetooth adapter found with alias '{}'", config_["adapter-alias"].asString());
} else {
spdlog::error("find_cur_adapter() failed: no bluetooth adapter found");
}
return;
}
findConnectedDevices(cur_adapter_.path, connected_devices_);
g_signal_connect(manager_.get(), "interface-proxy-properties-changed", G_CALLBACK(onInterfaceProxyPropertiesChanged), this);
g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this);
g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), this);
#ifdef WANT_RFKILL
rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update)));
#endif
dp.emit();
}
auto waybar::modules::Bluetooth::update() -> void {
std::string status = rfkill_.getState() ? "disabled" : "enabled";
label_.set_markup(
fmt::format(format_, fmt::arg("status", status), fmt::arg("icon", getIcon(0, status))));
if (status == "disabled") {
label_.get_style_context()->add_class("disabled");
} else {
label_.get_style_context()->remove_class("disabled");
// focussed device is either:
// - the first device in the device_preference_ list that is connected to the
// current adapter (if none fallback to last connected device)
// - it is the last device that connected to the current adapter
if (!connected_devices_.empty()) {
bool preferred_device_connected = false;
if (!device_preference_.empty()) {
for (const std::string& device_alias : device_preference_) {
auto it = std::find_if(connected_devices_.begin(), connected_devices_.end(), [device_alias](auto device){ return device_alias == device.alias; });
if (it != connected_devices_.end()) {
preferred_device_connected = true;
cur_focussed_device_ = *it;
break;
}
}
}
if (!preferred_device_connected) {
cur_focussed_device_ = connected_devices_.back();
}
}
std::string state;
std::string tooltip_format;
if (!cur_adapter_.powered)
state = "off";
else if (!connected_devices_.empty())
state = "connected";
else
state = "on";
#ifdef WANT_RFKILL
if (rfkill_.getState())
state = "disabled";
#endif
if (!alt_) {
if (state == "connected" && cur_focussed_device_.battery_percentage.has_value() && config_["format-connected-battery"].isString()) {
format_ = config_["format-connected-battery"].asString();
} else if (config_["format-" + state].isString()) {
format_ = config_["format-" + state].asString();
} else if (config_["format"].isString()) {
format_ = config_["format"].asString();
} else {
format_ = default_format_;
}
}
if (config_["tooltip-format-" + state].isString()) {
tooltip_format = config_["tooltip-format-" + state].asString();
} else if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
auto update_style_context = [this](const std::string& style_class, bool in_next_state) {
if (in_next_state && !label_.get_style_context()->has_class(style_class)) {
label_.get_style_context()->add_class(style_class);
} else if (!in_next_state && label_.get_style_context()->has_class(style_class)) {
label_.get_style_context()->remove_class(style_class);
}
};
update_style_context("discoverable", cur_adapter_.discoverable);
update_style_context("discovering", cur_adapter_.discovering);
update_style_context("pairable", cur_adapter_.pairable);
if (!state_.empty()) {
update_style_context(state_, false);
}
update_style_context(state, true);
state_ = state;
label_.set_markup(fmt::format(format_,
fmt::arg("status", state_),
fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("adapter_address", cur_adapter_.address),
fmt::arg("adapter_address_type", cur_adapter_.address_type),
fmt::arg("adapter_alias", cur_adapter_.alias),
fmt::arg("device_address", cur_focussed_device_.address),
fmt::arg("device_address_type", cur_focussed_device_.address_type),
fmt::arg("device_alias", cur_focussed_device_.alias),
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0))
));
// TODO: make possible to show information about all connected devices in the tooltip
if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) {
auto tooltip_format = config_["tooltip-format"].asString();
auto tooltip_text = fmt::format(tooltip_format, status, fmt::arg("status", status));
label_.set_tooltip_text(tooltip_text);
label_.set_tooltip_text(fmt::format(tooltip_format,
fmt::arg("status", state_),
fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("adapter_address", cur_adapter_.address),
fmt::arg("adapter_address_type", cur_adapter_.address_type),
fmt::arg("adapter_alias", cur_adapter_.alias),
fmt::arg("device_address", cur_focussed_device_.address),
fmt::arg("device_address_type", cur_focussed_device_.address_type),
fmt::arg("device_alias", cur_focussed_device_.alias),
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0))
));
}
// Call parent update
ALabel::update();
}
// NOTE: only for when the org.bluez.Battery1 interface is added/removed after/before a device is connected/disconnected
auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager *manager, GDBusObject *object,
GDBusInterface *interface,
gpointer user_data) -> void
{
std::string interface_name = g_dbus_proxy_get_interface_name(G_DBUS_PROXY(interface));
std::string object_path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(interface));
if (interface_name == "org.bluez.Battery1") {
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), [object_path](auto d){ return d.path == object_path; });
if (device != bt->connected_devices_.end()) {
device->battery_percentage = bt->getDeviceBatteryPercentage(object);
bt->dp.emit();
}
}
}
auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged(GDBusObjectManagerClient *manager,
GDBusObjectProxy *object_proxy,
GDBusProxy *interface_proxy,
GVariant *changed_properties,
const gchar *const *invalidated_properties,
gpointer user_data) -> void
{
std::string interface_name = g_dbus_proxy_get_interface_name(interface_proxy);
std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy));
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
if (interface_name == "org.bluez.Adapter1") {
if (object_path == bt->cur_adapter_.path) {
bt->getAdapterProperties(G_DBUS_OBJECT(object_proxy), bt->cur_adapter_);
bt->dp.emit();
}
} else if (interface_name == "org.bluez.Device1" ||
interface_name == "org.bluez.Battery1") {
DeviceInfo device;
bt->getDeviceProperties(G_DBUS_OBJECT(object_proxy), device);
auto cur_device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), [device](auto d){ return d.path == device.path; });
if (cur_device == bt->connected_devices_.end()) {
if (device.connected) {
bt->connected_devices_.push_back(device);
bt->dp.emit();
}
} else {
label_.set_tooltip_text(status);
if (!device.connected) {
bt->connected_devices_.erase(cur_device);
} else {
*cur_device = device;
}
bt->dp.emit();
}
}
}
auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) -> std::optional<unsigned char> {
GDBusProxy* proxy_device_bat = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Battery1"));
if (proxy_device_bat != NULL) {
unsigned char battery_percentage = getUcharProperty(proxy_device_bat, "Percentage");
g_object_unref(proxy_device_bat);
return battery_percentage;
}
return std::nullopt;
}
auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info) -> bool {
GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1"));
if (proxy_device != NULL) {
device_info.path = g_dbus_object_get_object_path(object);
device_info.paired_adapter = getStringProperty(proxy_device, "Adapter");
device_info.address = getStringProperty(proxy_device, "Address");
device_info.address_type = getStringProperty(proxy_device, "AddressType");
device_info.alias = getStringProperty(proxy_device, "Alias");
device_info.icon = getOptionalStringProperty(proxy_device, "Icon");
device_info.paired = getBoolProperty(proxy_device, "Paired");
device_info.trusted = getBoolProperty(proxy_device, "Trusted");
device_info.blocked = getBoolProperty(proxy_device, "Blocked");
device_info.connected = getBoolProperty(proxy_device, "Connected");
device_info.services_resolved = getBoolProperty(proxy_device, "ServicesResolved");
g_object_unref(proxy_device);
device_info.battery_percentage = getDeviceBatteryPercentage(object);
return true;
}
return false;
}
auto waybar::modules::Bluetooth::getAdapterProperties(GDBusObject* object, AdapterInfo& adapter_info) -> bool {
GDBusProxy* proxy_adapter = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1"));
if (proxy_adapter != NULL) {
adapter_info.path = g_dbus_object_get_object_path(object);
adapter_info.address = getStringProperty(proxy_adapter, "Address");
adapter_info.address_type = getStringProperty(proxy_adapter, "AddressType");
adapter_info.alias = getStringProperty(proxy_adapter, "Alias");
adapter_info.powered = getBoolProperty(proxy_adapter, "Powered");
adapter_info.discoverable = getBoolProperty(proxy_adapter, "Discoverable");
adapter_info.pairable = getBoolProperty(proxy_adapter, "Pairable");
adapter_info.discovering = getBoolProperty(proxy_adapter, "Discovering");
g_object_unref(proxy_adapter);
return true;
}
return false;
}
auto waybar::modules::Bluetooth::findCurAdapter(AdapterInfo& adapter_info) -> bool {
bool found_adapter = false;
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
for (GList* l = objects; l != NULL; l = l->next) {
GDBusObject* object = G_DBUS_OBJECT(l->data);
if (getAdapterProperties(object, adapter_info) && (!config_["adapter-alias"].isString() || config_["adapter-alias"].asString() == adapter_info.alias)) {
found_adapter = true;
break;
}
}
g_list_free_full(objects, g_object_unref);
return found_adapter;
}
auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_adapter_path, std::vector<DeviceInfo>& connected_devices) -> void {
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
for (GList* l = objects; l != NULL; l = l->next)
{
GDBusObject* object = G_DBUS_OBJECT(l->data);
DeviceInfo device;
if (getDeviceProperties(object, device) && device.connected && device.paired_adapter == cur_adapter_.path) {
connected_devices.push_back(device);
}
}
g_list_free_full(objects, g_object_unref);
}