Привет, Хабр! Меня зовут Михаил, я 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)


  1. JordanCpp
    29.08.2024 07:59

    Не претензия, а просто мнение. Я бы постарался разделить. На класс достающий данные, обновляющий и вывод этого всего. Достаем в dto коллекцию. Потом это передаём, на вывод уже средствами qt. Все как то смешано.


  1. JordanCpp
    29.08.2024 07:59

    К примеру dto'ошка пригодилась бы для тестирования, как оно внешне выглядит в qt интерфейсе, просто ее создали и набили тестовыми данными.

    Разделить задачу, на две подзадачи бэк и фронт части.


    1. SSul Автор
      29.08.2024 07:59

      Спасибо за дополнение! Цель статьи — показать, как получить соответствующие данные. Как ими воспользоваться, каждый волен решать самостоятельно. Можно отображать для пользователя, сразу после получения, можно, как вы предложили, разбить по слоям и только потом использовать.


  1. qrKot
    29.08.2024 07:59
    +2

    Можно и я покритикую?

    В заголовке - про извлечение данных из Linux, а в содержимом - gps, уровень wifi, уровень батареи и свободное место на диске.

    Половина этого к Linux имеет весьма опосредованное отношение, вторая - вообще кроссплатформенными кутишными апи решается. И все это вместе достаточно сложно назвать прям уж "данными".

    Признаться, ожидал какого-то сурового парсинга терабайтных файлов или анализа гигабайтных чанков логов ядра...


    1. SSul Автор
      29.08.2024 07:59
      +1

      Статья про Linux, который используется для десктопных приложений или автономных устройств. Всякие высоконагруженные системы, обрабатывающие миллионы запросов в секунду, управляющие сотней другой девайсов и прочее — это все же отдельная тема. Поддержка GPS, Wi-Fi и управление батареей тоже ведь могут быть частью ядра Linux.
      А кроссплатформенными кутишными апи решается, но не всегда. Обращу внимание, что мы ограничили стек используемых технологий С++ Qt 5.12. Это то, что мы использовали на реальном проекте, и поверьте, возможности данной версии Qt весьма ограничены.


  1. lumag
    29.08.2024 07:59
    +1

    Вот только capacity может быть, а может и не быть. Правильнее вычитывать пары charge_now / charge_full и/или energy_now / energy_full и по ним уже смотреть.