From 981122a905dbd99c5fc72475cb506d47d0d78e4f Mon Sep 17 00:00:00 2001 From: Pato05 Date: Sun, 18 Dec 2022 20:42:05 +0100 Subject: [PATCH] initial release --- .gitignore | 2 + CMakeLists.txt | 12 +++ LICENSE | 7 ++ README.md | 91 +++++++++++++++++++ cppbtbl.cpp | 241 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cppbtbl.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5acb669 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3d7a66d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.24) +project(cppbtbl) + +find_package(sdbus-c++ REQUIRED) + +add_executable(cppbtbl cppbtbl.cpp) +target_link_libraries(cppbtbl PRIVATE SDBusCpp::sdbus-c++) + +install( + TARGETS cppbtbl + DESTINATION ${CMAKE_INSTALL_PREFIX}/bin +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18164fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 Pato05 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..df5368a --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# cppbtbl (**C++** **b**lue**t**ooth **b**attery **l**evel) + +A wrapper around UPower's DBus API that returns connected bluetooth devices' battery (and that can also be used as a waybar module!) + +## Installing + +First of all, you're gonna need to install sdbus-c++, see [sdbus-cpp: Building and installing the library](/~https://github.com/Kistler-Group/sdbus-cpp#building-and-installing-the-library) + +Clone the repo + +```bash +git clone /~https://github.com/pato05/cppbtbl +cd cppbtbl +``` + +Build using CMake + +```bash +cmake . +make +sudo make install +``` + +### Note + +cppbtbl depends on sdbus-c++ which itself depends on libsystemd. Regardless of this dependency, though, you don't necessarily need systemd, as you can get libsystemd without having it: see [sdbus-cpp: Solving libsystemd dependency](/~https://github.com/Kistler-Group/sdbus-cpp/blob/master/docs/using-sdbus-c++.md#solving-libsystemd-dependency). + +Note that if you are using systemd, you won't have any issues using cppbtbl. + +## Waybar + +```json +... + "custom/bt-battery": { + "exec": "cppbtbl -f waybar", + "restart-interval": 5, + "return-type": "json", + "format": "{icon}", + "format-icons": ["󰁺", "󰁻", "󰁼", "󰁼", "󰁽", "󰁿", "󰂀", "󰂀", "󰂂", "󰁹"] + }, +... +``` + +Change format and icons accordingly, and the module should work! + +## Other status bars + +Two extra formats are offered to make cppbtbl usable in this case too. Those formats are `icononly` and `icon+devicename`. Please note that the default icons provided are part of the FontAwesome font, and (as of right now) the only way of changing them is by editing the source code. + +A better way to handle this, would be to make a script that reads `cppbtbl`'s output and re-writes it in whatever way is best for you. For example: + +```bash +#!/usr/bin/bash +DEVICES=() +PERCENTAGES=() +timeout= +while true; do + eval "read $timeout line" + if [ "$?" -ne "0" ]; then + [ -z "$timeout" ] && break + timeout= + # timeout, flush devices info + ... + continue + fi + if [ -z "$line" ]; then + timeout= + # clear ARRAYs, all devices have been disconnected + DEVICES=(); PERCENTAGES=() + echo '' + continue + fi + + IFS=":"; read -a split <<< "${line//: /:}"; unset IFS + DEVICES[${#DEVICES}]="${split[0]}" + PERCENTAGES[${#PERCENTAGES}]="${split[1]}" + # time out read comamnd after 1 second (we supposed that if no other data is available within one second, we need to flush) + timeout="-t 1" +done < <(cppbtbl -f raw) +``` + +Please note that this is just an example, and it could likely be executed better. + + + +Or a better way would be to just modify the source code to your likings. + + + + +### A thank-you goes to [@Justasic](/~https://github.com/Justasic) for helping me a lot in making this. diff --git a/cppbtbl.cpp b/cppbtbl.cpp new file mode 100644 index 0000000..b3a5108 --- /dev/null +++ b/cppbtbl.cpp @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// battery check interval (if a device is connected) +#define CHECK_INTERVAL 15 * 1000 + +#define UPOWER_IFACE "org.freedesktop.UPower" +#define UPOWER_PATH "/org/freedesktop/UPower" +#define UPOWER_DEVICE_IFACE "org.freedesktop.UPower.Device" + +#define PROPERTIES_IFACE "org.freedesktop.DBus.Properties" + +const int icon_length = 5; +const char *icons[5] = { "", "", "", "", "" }; + +std::set watch_list; + +enum OutputFormat { + format_waybar, + format_icon_only, + format_icon_device_name, + format_raw, + + format_invalid +}; +OutputFormat out_format = format_raw; + +const char *get_icon(int percentage) { + int icon_idx = (percentage * icon_length / 100) + 0.5; + return icons[icon_idx]; +} + +void replace_all(std::string &s, const std::string &search, const std::string &replace) { + for( size_t pos = 0; ; pos += replace.length() ) { + // Locate the substring to replace + pos = s.find( search, pos ); + if( pos == std::string::npos ) break; + // Replace by erasing and inserting + s.erase( pos, search.length() ); + s.insert( pos, replace ); + } +} + +void _get_battery_infos() { + if (watch_list.empty()) return; + + int least_percentage = 100; + std::string least_device_name; + std::stringstream tooltip; + for (auto &device : watch_list) { + auto device_obj = sdbus::createProxy(UPOWER_IFACE, device); + std::string device_name; + try { + device_name = device_obj->getProperty("Model") + .onInterface(UPOWER_DEVICE_IFACE) + .get(); + } catch (const sdbus::Error& e) { + device_name = device_obj->getProperty("Serial") + .onInterface(UPOWER_DEVICE_IFACE) + .get(); + } + + double percentage = device_obj->getProperty("Percentage") + .onInterface(UPOWER_DEVICE_IFACE) + .get(); + tooltip << device_name << ": " << percentage << "%\n"; + + if (percentage < least_percentage) { + least_percentage = percentage; + least_device_name = std::move(device_name); + } + } + + std::string tooltip_str = tooltip.str(); + tooltip_str.erase(tooltip_str.length() - 1); + + switch (out_format) { + case format_waybar: + replace_all(tooltip_str, "\n", "\\n"); + replace_all(tooltip_str, "\"", "\\\""); + // lazy af solution, but should work + std::cout << "{\"percentage\":" << least_percentage << ",\"tooltip\":\"" << tooltip_str << "\"}" << std::endl; + break; + case format_icon_device_name: + std::cout << get_icon(least_percentage) << ": " << least_device_name << std::endl; + break; + case format_icon_only: + std::cout << get_icon(least_percentage) << std::endl; + break; + case format_raw: + std::cout << tooltip_str << std::endl; + break; + } +} + +void _device_added(sdbus::ObjectPath &path) { + auto device_object = sdbus::createProxy(UPOWER_IFACE, path); + + std::string native_path = device_object + ->getProperty("NativePath") + .onInterface(UPOWER_DEVICE_IFACE) + .get(); + if (native_path.rfind("/org/bluez") != 0) { + // non-bluetooth device, we can ignore + return; + } + + // std::cerr << "[DEBUG] Added to watchlist: " << path << std::endl; + + watch_list.insert(std::move(path)); +} + +void _device_removed(sdbus::ObjectPath &path) { + watch_list.erase(path); + + if (watch_list.empty()) { + std::cout << std::endl; + } +} + +void _device_added_signal(sdbus::Signal &signal) { + sdbus::ObjectPath device_path; + signal >> device_path; + + // std::cerr << "[DEBUG] Received device_added signal!" << std::endl; + _device_added(device_path); +} + +void _device_removed_signal(sdbus::Signal &signal) { + sdbus::ObjectPath device_path; + signal >> device_path; + + // std::cerr << "[DEBUG] Received device_removed signal!" << std::endl; + _device_removed(device_path); +} + +void help(char *name) { + std::cout + << "Usage: " << name << " -f [format] [-e]\n" + << "-f/--format [format] valid options: waybar, icononly, icon+devicename, raw (default: raw)\n" + << "-h/--help show this help screen\n" + << "-e/--dont-follow output info and exit" << std::endl; +} + +OutputFormat optarg_to_format() { + if (strcmp(optarg, "waybar") == 0) return format_waybar; + if (strcmp(optarg, "icononly") == 0) return format_icon_only; + if (strcmp(optarg, "icon+devicename") == 0) return format_icon_device_name; + if (strcmp(optarg, "raw") == 0) return format_raw; + + return format_invalid; +} + +int main(int argc, char *argv[]) { + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"format", required_argument, 0, 'f'}, + {"dont-follow", no_argument, 0, 'e'} + }; + bool dont_follow = false; + int opt; + while ((opt = getopt_long(argc, argv, "hef:", long_options, nullptr)) != -1) { + switch (opt) { + case 'h': + help(argv[0]); + return 0; + case 'f': + out_format = optarg_to_format(); + if (out_format == format_invalid) { + std::cerr << "Invalid format!\n" + << "Valid values are: waybar, icononly, icon+devicename, raw." << std::endl; + + return 1; + } + break; + case 'e': + dont_follow = true; + break; + default: + return 1; + } + } + + // enumerate currently connected devices, and if applicable, add them to the watch_list + auto proxy = sdbus::createProxy(UPOWER_IFACE, UPOWER_PATH); + auto method = proxy->createMethodCall(UPOWER_IFACE, "EnumerateDevices"); + auto reply = proxy->callMethod(method); + + std::vector res; + reply >> res; + + + for (auto &obj : res) { + _device_added(obj); + } + + if (dont_follow) { + _get_battery_infos(); + return 0; + } + + // signal handlers + proxy->registerSignalHandler(UPOWER_IFACE, "DeviceAdded", &_device_added_signal); + proxy->registerSignalHandler(UPOWER_IFACE, "DeviceRemoved", &_device_removed_signal); + proxy->finishRegistration(); + + auto connection = &proxy->getConnection(); + + // `poll` event loop + timer for polling connected devices' battery + while (true) + { + auto processed = connection->processPendingRequest(); + if (processed) + continue; // Process next one + + int timeout = -1; + if (!watch_list.empty()) { + timeout = CHECK_INTERVAL; + _get_battery_infos(); + } + + auto pollData = connection->getEventLoopPollData(); + struct pollfd fds[] = { + {pollData.fd, pollData.events, 0} + }; + + auto r = poll(fds, 1, timeout); + + if (r < 0 && errno == EINTR) + continue; + + } +} \ No newline at end of file