Привет, Хабр! Меня зовут Михаил, я backend-разработчик в SimbirSoft. Хочу поделиться с вами опытом получения различной информации в ОС Linux для использования в своих целях.
Представьте, что нам нужно написать приложение «Погода», которое берёт из сети температуру, влажность и прочие параметры и отображает для пользователя. Было бы неплохо, чтобы оно само определяло, где мы находимся. Но как это сделать? Легко!
На одном из проектов нам предстояло разработать десктопное приложение для портативного устройства и графический интерфейс к нему. При этом, кроме основной функциональности, предстояло выводить дополнительную информацию для пользователя:
- навигационные данные (долготу, широту, высоту);
- сведения о сетевом соединении (название, тип, уровень сигнала Wi-Fi);
- заряд батареи;
- информацию о хранилище (сколько занято/сколько всего).
Стек используемых технологий – C++ в связке с библиотекой Qt (5.12). Задача казалась довольно простой. Но первое впечатление очень часто обманчиво. Особенно в тех случаях, когда вам не приходилось решать подобные задачи. Но обо всём по порядку. Рассмотрим вывод разных видов информации.
Получаем навигационные данные
Почитав документацию к Qt (раз, два) или просто погуглив, вы наверняка найдете решение наподобие тому, что представлено ниже. Небольшой класс, получающий данные о навигации:
class Navigation : public QObject
{
Q_OBJECT
public:
explicit Navigation(QObject *parent = Q_NULLPTR);
};
Navigation::Navigation(QObject *parent):
QObject(parent)
{
auto source = QGeoPositionInfoSource::createDefaultSource(this);
source->setUpdateInterval(1000);
qDebug() << source->sourceName();
connect(source, &QGeoPositionInfoSource::positionUpdated,this,[]( const QGeoPositionInfo & posInfo)
{
auto coords = posInfo.coordinate();
qDebug() << QString("Latitude %1").arg(coords.latitude())
+ QString(" Longitude %1 ").arg(coords.longitude())
+ QString("Altitude %1").arg(coords.altitude());
});
source->startUpdates();
}
Не забываем про необходимый заголовочник:
#include <QtPositioning>
И про модуль в .pro файле:
QT += positioning
Если вдруг он не определяется – ставим нужные либы:
sudo apt install -y libqt5location5 libqt5positioning5 qtlocation5-dev qtpositioning5-dev
Вроде всё собралось, запустилось. Даже что-то в консоль напечаталось. Ура! Задача решена?
А давайте посмотрим на результат. У меня на виртуальной Ubunte 20 вывод был таким:
Видим точность «плюс-минус километр», высоты вообще нет (параметр altitude). Как-то не очень… А если нужно дальше с высотой работать? Придумывать самому? Мне лень – да и неправильно это. Кажется, мы столкнулись с первой проблемой.
От Geoclue к gpsd
Настало время слегка поразмыслить. Что нам нужно от приложения? Получить навигационные данные – широту, долготу и высоту. Но простите – я написал код, собрал его в приложение и запустил на своём компьютере, получив долготу и широту (кстати, это примерное расположение нашего офиса в Самаре ?). Как это получилось? Мой компьютер не имеет GPS-приемника на борту. Так и знал – вездесущий Google следит за мной…
Всё гораздо проще. Настало время пояснить первую строку из вывода, а именно Geoclue. Это геоинформационный сервис, используемый в Linux для определения географического положения компьютера. Желающие ознакомиться с исходниками могут пройти по ссылке. Сервис может использоваться различными приложениями и службами, которые требуют информацию о местоположении для корректной работы. Например, запустил сервис погоды – он сразу показывает её в нужном месте. По крайней мере, если смог его точно определить.
Geoclue использует различные источники для определения местоположения, включая GPS, сети Wi-Fi и IP-адреса. В Ubuntu 20 он предустановлен по умолчанию и автоматически запускается при старте системы.
Всё в нем хорошо – только нам данное решение не подходит. Нам нужно то, что непосредственно работает с любым GPS-приёмником и с любой системой — GPS, ГЛОНАСС, Galileo, «Бэйдоу» или ещё какой-нибудь – не принципиально. Всё, что нужно – взять рандомный приемник, подключить к нашему проекту, и получить вывод корректных навигационных данных.
Что бы нам подошло? Есть демон gpsd! В отличие от Geoclue, он заточен на работу исключительно с навигационными приёмниками, получая и выдавая результаты в формате JSON. Но по умолчанию gpsd в системе не установлен – мы легко исправим это недоразумение командой:
sudo apt install gpsd
Теперь если заново запустить код, представленный выше – скорее всего, ничего не поменяется. Почему так? Вот тут объяснение:
QGeoPositionInfoSource *source = QGeoPositionInfoSource::createDefaultSource(this);
Мы создаем источник данных для получения информации о геопозиции, который в свою очередь считывает данные о местоположении из источников по умолчанию в системе или из плагина с наивысшим приоритетом. Скорее всего, таким будет как раз Geoclue. А нам нужен gpsd. Тогда воспользуемся другим методом и сами укажем источник данных:
QGeoPositionInfoSource *source = QGeoPositionInfoSource::createSource("gpsd", this);
Компилируем, запускаем – результат отрицательный. Данных нет. Источник пустой.
Увы, но для gpsd решения из коробки в Qt нет. Придётся или писать свою реализацию, или воспользоваться уже готовой, написанной кем-то. Например, вот этой.
#include <QtNetwork>
#include "qgeopositioninfosource_gpsd.h"
class Navigation : public QObject
{
Q_OBJECT
public:
explicit Navigation(QObject *parent = Q_NULLPTR);
signals:
void coordinates(QString, QString);
private slots:
void gpsPositionUpdated(const QGeoPositionInfo &info);
private:
QGeoPositionInfoSourceGpsd *sourceGpsd;
QTcpSocket *tcpSocket;
};
#include "navigation.h"
Navigation::Navigation(QObject *parent) : QObject(parent)
{
tcpSocket = new QTcpSocket(this);
tcpSocket->connectToHost(QHostAddress::LocalHost, 2947);
tcpSocket->write("?WATCH={\"enable\":true,\"nmea\":true}\n");
sourceGpsd = new QGeoPositionInfoSourceGpsd(this);
sourceGpsd->setUpdateInterval(1000);
sourceGpsd->setDevice(tcpSocket);
connect(sourceGpsd, &QGeoPositionInfoSourceGpsd::positionUpdated, this, &Navigation::gpsPositionUpdated);
sourceGpsd->startUpdates();
}
void Navigation::gpsPositionUpdated(const QGeoPositionInfo &info)
{
auto coords = info.coordinate();
QString temp = QString(" Latitude %1\n").arg(coords.latitude()) + QString(" Longitude %1\n").arg(coords.longitude());
emit coordinates(temp, QString::number(coords.altitude()) + " m");
}
#include <QCoreApplication>
#include "navigation.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Navigation navigation;
QObject::connect(&navigation, &Navigation::coordinates, [](QString pos, QString height)
{
qDebug() << pos << " " << height;
});
return a.exec();
}
Обратите внимание на строки:
tcpSocket->connectToHost(QHostAddress::LocalHost, 2947);
gpsd представляет собой TCP/IP-сервис, по умолчанию привязанный к порту 2947. Он взаимодействует через этот сокет, принимая команды и возвращая результаты.
А вот тут:
tcpSocket->write("?WATCH={\"enable\":true,\"nmea\":true}\n");
мы посылаем этому сервису команду наблюдать за данными навигации и отправлять их клиентам в формате “nmea”.
Итак, код есть и должен работать. Как бы всё это дело проверить? Можно поискать какой-нибудь навигационный приёмник. Подключить его к компьютеру и проверить работоспособность программы. Можно раздать навигацию с телефона. Но есть и другой способ – gpsfake. Устанавливается командой:
sudo apt install gpsd-clients
Перед запуском фейковых данных неплохо бы их где-нибудь взять. Можно вот тут:
https://nmeagen.org/. Данный ресурс генерирует файлы в формате NMEA – как раз то, что нужно.
Теперь останавливаем gpsd:
sudo systemctl stop gpsd.service
sudo systemctl stop gpsd.socket
Запускаем gpsfake командой из консоли:
sudo gpsfake -P 2947 -t -c 1 -l -D4 /home/user/output.nmea
Здесь 2947 – номер порта, который мы слушаем, /home/user/output.nmea – файл с данными, который мы сгенерировали. И различные параметры, прочитать про которые можно тут.
Вывод будет примерно таким (запускалось в Ubuntu 22):
Теперь можно и нашу программу запустить:
Данные есть – с навигацией справились!
Получаем сведения о сети
Переходим к сведениям о сети. Нужно получить название сетевого соединения, его тип, и уровень сигнала для Wi-Fi соединения.
Начнём с самого сложного – с уровня сигнала Wi-Fi. В Qt 5.12 есть класс QNetworkConfiguration. Из него можно вытащить много всякого интересного про сетевое соединение. Но про уровень сигнала – разработчики Qt похоже «забыли». Ну ничего, сами справимся.
Можно поступить следующим способом – например, использовать данную консольную команду:
watch -d -t -n 1 "awk 'NR==3 {printf(\"WiFi Signal Strength = %.0f%%\\n\",\$3*10/7)}' /proc/net/wireless"
С помощью неё раз в секунду считывается содержимое третьей строки файла /proc/net/wireless
. Там как раз и есть нужный нам параметр.
Вот тут содержимое файла:
А вот вывод команды:
Теперь тот же результат нужно получить, используя команду выше и средства Qt. Думаете, это сложно? Вовсе нет, в Qt есть класс QProcess – вот им и воспользуемся.
#include <QCoreApplication>
#include <QProcess>
#include <QDebug>
#include <QTimer>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QByteArray data;
QProcess mProcess;
QTimer mWiFiTimer;
mWiFiTimer.start(1000);
QObject::connect(&mWiFiTimer, &QTimer::timeout, [&]()
{
mProcess.start("awk NR==3 /proc/net/wireless", QIODevice::ReadOnly);
});
QObject::connect(&mProcess,&QProcess::readyReadStandardOutput,[&]()
{
data = mProcess.readAllStandardOutput();
QString stringData = "";
if(!data.isEmpty())
{
stringData = QString::fromStdString(data.toStdString());
stringData = stringData.section(" ", 4, 4);
stringData.chop(1);
auto val = stringData.toInt();
val = val * 10 / 7;
qDebug() << "Wi-Fi signal level:" << val;
}
});
return a.exec();
}
Вывод будет примерно таким:
Самое сложное, связанное с сетью, сделано. Осталось получить тип и название соединения. Но это легко.
#include <QCoreApplication>
#include <QNetworkConfigurationManager>
#include <QNetworkAccessManager>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QNetworkAccessManager accessMan;
QNetworkConfigurationManager configMan;
QNetworkConfiguration configuration;
QObject::connect(&configMan, &QNetworkConfigurationManager::updateCompleted, [&]()
{
configuration = accessMan.configuration();
qDebug() << configuration.name();
qDebug() << configuration.bearerTypeName();
});
return a.exec();
}
И вывод:
Получаем сведения о заряде батареи (даже если её у вас нет)
Переходим к индикатору заряда батареи. Самая большая проблема с ним вовсе не получение данных (хотя и это тоже). Не знаю, как у вас, но у меня рабочий компьютер – это стационарный системник, а не ноутбук. И электричество он кушает исправно от сети, а не от батареи. А раз нет батареи – нет и заряда батареи.
С Wi-Fi может возникнуть такая же проблема – не у каждого в системнике есть адаптер Wi-Fi. Но эту проблему легко решить – покупкой Wi-Fi «свистка» для компьютера. Вставляем его в свободный USB-порт, раздаем Wi-Fi со своего смартфона – проблема решена.
А вот с батареей такой фокус уже не пройдёт. Хотя я ни разу не удивлюсь, если найдутся умельцы, способные приделать ноутбучную батарею к стационарному системнику, и при этом не произойдёт «аннигиляционный коллапс» окружающего пространства. Но хочется решение проще и безопаснее. И оно есть. Например, такое.
Вернёмся к нему чуть позже, а пока поговорим про заряд батареи. Узнать всё о системе питания в Linux можно в директории /sys/class/power_supply
(в старых версиях Linux путь другой — /proc/acpi/battery
).
Здесь содержатся различные поддиректории для каждого источника питания. Например, AC (переменный ток), BAT (BAT0, BAT1 и так далее) для батарей. В каждой из этих поддиректорий есть файлы, которые содержат различные сведения об источнике питания: status (текущее состояние источника питания — заряжается, разряжается, полностью заряжен и так далее), capacity (текущий уровень заряда батареи в процентах) и прочие. Для того чтобы что-то узнать, достаточно прочитать значения из этих файлов.
Хватит с теорией – пришло время воспользоваться Fake Battery. Напоминаю – у меня стационарный системный блок с питанием от сети. Заходим в /sys/class/power_supply
и видим следующее:
Никаких батарей нет! Настройки питания о них тоже ничего не знают:
Сейчас исправим сей недостаток. Скачиваем/клонируем к себе репозиторий. Выполняем из корня этого репозитория следующую команду:
sudo insmod ./fake_battery.ko
И получаем батареи в системе:
А вот так теперь выглядит окно настройки электропитания:
И индикатор появился!
В общем – полный набор!
Теперь можно и уровень заряда батареи узнать:
#include <QCoreApplication>
#include <QFile>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QFile file("/sys/class/power_supply/BAT0/capacity");
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Battery charge level: " << file.readAll().trimmed();
file.close();
} else {
qDebug() << "Failed to open file";
}
return 0;
return a.exec();
}
Для ленивых — можно найти готовое решение, которое не только заряд, но и много чего ещё вывести может. Вот одно из них.
Получаем информацию о свободном/занятом месте на диске
С зарядом разобрались, осталось последнее. Нужно определить информацию о хранилище — сколько занято/сколько всего есть. Сделать это просто:
#include <QCoreApplication>
#include <QFile>
#include <QDebug>
#include <QStorageInfo>
#include <algorithm>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QString fileSystem;
auto storageList = QStorageInfo::mountedVolumes();
auto val = std::find_if(storageList.begin(), storageList.end(), [&](const auto &storage)
{
fileSystem = storage.fileSystemType().constData();
return fileSystem.contains("ext4");
});
if(val != storageList.end())
{
qDebug() << "used " << QString::number((*val).bytesAvailable()/1000000000.)
<< " from " << QString::number((*val).bytesTotal()/1000000000.);
}
return a.exec();
}
На что тут стоит обратить внимание? Файловые системы бывают разные – не забудьте об этом. В коде выше мы получаем данные для «ext4»:
fileSystem.contains("ext4")
Но есть и другие. А вот здесь:
storage.bytesAvailable()/1000000000.
storage.bytesTotal()/1000000000.
происходит преобразование для вывода данных в Гб.
Заключение
Ну что – вроде со всем справились? Навигацию получили, сведения о сети и о заряде батареи – тоже. И про хранилище не забыли. Можно выдохнуть – всё работает – задача выполнена. А ведь на первый взгляд всё казалось простым и лёгким. В общем-то так и есть. Просто нужно учесть различные мелкие детали. Именно они обычно потом и сказываются на сроках и качестве разработки.
Спасибо за внимание!
Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.
Комментарии (6)
JordanCpp
29.08.2024 07:59К примеру dto'ошка пригодилась бы для тестирования, как оно внешне выглядит в qt интерфейсе, просто ее создали и набили тестовыми данными.
Разделить задачу, на две подзадачи бэк и фронт части.
SSul Автор
29.08.2024 07:59Спасибо за дополнение! Цель статьи — показать, как получить соответствующие данные. Как ими воспользоваться, каждый волен решать самостоятельно. Можно отображать для пользователя, сразу после получения, можно, как вы предложили, разбить по слоям и только потом использовать.
qrKot
29.08.2024 07:59+2Можно и я покритикую?
В заголовке - про извлечение данных из Linux, а в содержимом - gps, уровень wifi, уровень батареи и свободное место на диске.
Половина этого к Linux имеет весьма опосредованное отношение, вторая - вообще кроссплатформенными кутишными апи решается. И все это вместе достаточно сложно назвать прям уж "данными".
Признаться, ожидал какого-то сурового парсинга терабайтных файлов или анализа гигабайтных чанков логов ядра...
SSul Автор
29.08.2024 07:59+1Статья про Linux, который используется для десктопных приложений или автономных устройств. Всякие высоконагруженные системы, обрабатывающие миллионы запросов в секунду, управляющие сотней другой девайсов и прочее — это все же отдельная тема. Поддержка GPS, Wi-Fi и управление батареей тоже ведь могут быть частью ядра Linux.
А кроссплатформенными кутишными апи решается, но не всегда. Обращу внимание, что мы ограничили стек используемых технологий С++ Qt 5.12. Это то, что мы использовали на реальном проекте, и поверьте, возможности данной версии Qt весьма ограничены.
lumag
29.08.2024 07:59+1Вот только
capacity
может быть, а может и не быть. Правильнее вычитывать парыcharge_now
/charge_full
и/илиenergy_now
/energy_full
и по ним уже смотреть.
JordanCpp
Не претензия, а просто мнение. Я бы постарался разделить. На класс достающий данные, обновляющий и вывод этого всего. Достаем в dto коллекцию. Потом это передаём, на вывод уже средствами qt. Все как то смешано.