Введение
Порой, при решении рабочих задач, возникает необходимость в информации об окружающих сетях Wi-Fi: канал, мощность, тип шифрования и др. И если для популярных мобильных ОС Android и iOS утилит много, то для Sailfish OS — только одна. Сегодня, на примере данной утилиты, будет разобрано получение информации об окружающих сетях Wi-Fi и её отображение двумя способами: списком и графически.
До изучения материала желательно иметь базовое представление о разработке под Sailfish OS и об утилите
wpa_cli.Получение информации о сетях Wi-Fi
Выделяется два основных способа получить информацию об окружающих устройство сетях Wi-Fi: использовать утилиту
wpa_cli или недокументированный элемент TechnologyModel из модуля MeeGo.Connman.Первый способ — решение «в лоб». Использование утилиты
wpa_cli в Sailfish OS ничем не отличается от другого Linux-дистрибутива:wpa_cli scan && wpa_cli scan_results
# wpa_cli scan
Selected interface 'wlan0'
OK
# wpa_cli scan_results
Selected interface 'wlan0'
bssid / frequency / signal level / flags / ssid
10:bf:48:4b:2b:f4 2412 -46 [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS] Asd_496283
d4:21:22:33:ec:46 2417 -57 [WPA2-PSK-CCMP][WPS][ESS] MGTS_243
78:94:b4:99:1c:41 2462 -59 [WPA2-PSK-CCMP][WPS][ESS] MGTS_GPON_8959
78:96:82:64:ea:fd 2427 -62 [WPA2-PSK-CCMP][WPS][ESS] Onlime248
90:f6:52:66:20:92 2412 -41 [WPA2-PSK-CCMP][ESS] Hearthstone
14:cc:20:32:e7:04 2437 -65 [WPA2-PSK-CCMP][WPS][ESS] ViVa239
00:0e:8f:2f:ff:3c 2412 -67 [WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS] Smart_box - 297
d4:6e:0e:b0:17:16 2462 -73 [WPA2-PSK-CCMP+TKIP][WPS][ESS] MGTS_GPON_8959
94:4a:0c:ce:93:05 2462 -71 [WPA2-PSK-CCMP][WPS][ESS] MGTS_GPON_7870
e8:94:f6:fa:43:86 2417 -77 [WPA2-PSK-CCMP][ESS] Home236
be:85:56:e2:9a:fc 2427 -74 [WPA2-PSK-CCMP][WPS][ESS] DIRECT-HR-BRAVIA
c0:a0:bb:1d:4c:58 2412 -74 [WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][WPS][ESS] dlink-4C58
40:3d:ec:31:ca:fb 2432 -86 [WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS] TV kinescope
fc:2d:5e:45:db:35 2437 -82 [WPA2-PSK-CCMP+TKIP][WPS][ESS] Matthew
00:0e:8f:6e:47:ba 2412 -65 [WPA-PSK-TKIP][WPA2-PSK-CCMP][WPS][ESS] Genya
c0:a0:bb:81:c7:4a 2447 -83 [WPA2-PSK-CCMP][ESS] mrnext-245
d8:fe:e3:f9:26:45 2437 -66 [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS] NBNДанный подход имеет две основные проблемы. Во-первых, для его применения требуются права суперпользователя. Во-вторых, необходимо реализовывать чтение результата выполнения консольной команды.
Первая проблема решается запросом пароля у пользователя при запуске программы и дальнейшим использованием модуля QProcess:
Взаимодействие с wpa_cli
/** * Метод вызывает сканирование с помощью утилиты wpa_cli. * @param password - пароль для получения доступа root. */ void WpaCliHelper::callWpaCli(QString password) { // Создание объекта процесса QProcess process; // Запуск сканирования сетей Wi-Fi под пользователем root process.start(QString("/bin/bash -c \"echo %1 | devel-su wpa_cli scan\"").arg((password))); // Ожидание завершения процесса if (!process.waitForFinished()) { // Если произошла ошибка, то выход emit gotScanError(); return; } // Чтение результата работы команды mWifiInfo = process.readAll(); // Если произошла ошибка авторизации, то выход if (mWifiInfo.contains("Auth failed")) { emit gotAuthError(); return; } // Запрос результатов сканирования под пользователем root process.start(QString("/bin/bash -c \"echo %1 | devel-su wpa_cli scan_results\"").arg((password))); // Ожидание завершения процесса if (!process.waitForFinished()) { // Если произошла ошибка, то выход emit gotResultError(); return; } // Чтение результата работы команды mWifiInfo = process.readAll(); // Сигнал об успешном завершении работы emit calledWpaCli(); }
Для чтения результатов сканирования также достаточно одной функции:
Получение результата работы wpa_cli
/** * Метод разбирает результат сканирования сетей утилитой wpa_cli. * @param info - вывод утилиты wpa_cli. */ void WifiInfoParser::parseInfo(QString info) { // Удаление предыдущей информации о сетях wifiInfo.clear(); // Разделение общего вывода по строкам QStringList networks = info.split('\n'); // Обработка отсутствия информации о сетях if (networks.length() == 3) { networkCount = 0; emit parsed(PARSE_COMPLETED_WITH_NO_NETWORKS); } // Сохранение количества окружающих сетей Wi-Fi networkCount = networks.length() - 3; for (int i = 0; i < networkCount; i++) { // Разделение каждой строки на поля QStringList data = networks.at(i+2).split('\t'); // Сохранение информации о сети wifiInfo << QVariant::fromValue( (QStringList() << QString::number(calculateChannel(data[1].toInt())) // Номер канала << data[2] // Уровень сигнала << data[4] // Имя сети << data[0])); // BSSID сети } // Сигнал о завершении разбора вывода emit parsed(PARSE_COMPLETED_CORRECTLY); }
Однако, запрос прав суперпользователя при запуске приложения может смутить обычного пользователя, и, поэтому, необходимо избегать использование ф��нкций, требующих root-доступ. Поэтому, в рамках задачи получения информации об окружающих сетях Wi-Fi рекомендуется использовать модуль
MeeGo.Connman, к сожалению не представленный в стандартной документации.Для его использования необходимо добавить следующую строчку:
import MeeGo.Connman 0.2
Данный модуль предоставляет необходимый для поставленной задачи элемент
TechnologyModel, позволяющий всего в несколько строк в QML-файле получить структурированную информацию об окружающих сетях Wi-Fi:TechnologyModel { id: networksList // Идентификатор для обращения name: "wifi" // Имя интерфейса для сканирования }
Однако, обновлять информацию необходимо вручную. Лучше всего это делать с помощью компонента
Timer:Timer { id: updateTimer // Идентификатор для обращения interval: 2000 // Срабатывание каждые две секунды running: true // Таймер запущен repeat: true // Таймер запускается повторно после срабатывания triggeredOnStart: true // Таймер срабатывает перед первым запуском onTriggered: networksList.requestScan() // Обновление списка окружающих сетей Wi-Fi }
Теперь вся необходимая информация представлена в структурированном виде, а процесс её получения не требует пароля администратора. Соответственно, можно переходить к процессу отображения полученных данных.
Стоит заметить, что такой же интерфейс доступен и для приложений, реализуемых на C++. Пример его использования опубликован в репозитории модуля.
Отображение данных списком
Описанная в предыдущем разделе модель возвращает список элементов типа
NetworkService, который предоставляет всю необходимую информацию об окружающих сетях. Из всего многообразия наибольший интерес для сканера представляют поля, описанные в таблице 1.| Имя поля | Тип данных | Описание |
|---|---|---|
| name | QString | Имя сети |
| frequency | quint16 | Частота сигнала |
| strength | uint | Сила сигнала |
| bssid | QString | BSSID |
| security | QStringList | Ти�� шифрования |
Теперь, зная способ хранения информации о сетях и элемент для отрисовки списков, можно читабельно отобразить полученную информацию (рисунок 1):
Код отображения в виде списка
SilicaListView { // Начало списка сетей id: wifiInfoList // Идентификатор для обращения anchors.fill: parent // Заполнение всего экрана model: networksList // Модель с информацией об окружающих сетях delegate: Item { // Начало элемента списка width: parent.width // Ширина экрана height: Theme.itemSizeHuge // Одна из стандартных рекомендуемых высот элемента списка Column { // Расположение элементов в столбец anchors { fill: parent // Заполнение всего зарезервированного пространства leftMargin: Theme.horizontalPageMargin // Стандартный рекомендуемый отступ rightMargin: Theme.horizontalPageMargin // Стандартный рекомендуемый отступ topMargin: Theme.paddingLarge // Стандартный рекомендуемый отступ } Row { // Расположение элементов в строку // Ширина столбца с учётом отступов anchors.right: parent.right anchors.left: parent.left height: childrenRect.height // Высота зависит от вложенных элементов Label { // Имя сети width: parent.width / 2 // Ширина в половину столбца horizontalAlignment: Text.AlignLeft // Выравнивание текста по левому краю font.bold: true // Полужирное начертание текста text: modelData.name // Имя сети truncationMode: TruncationMode.Fade // Частичное скрытие длинного текста } Label { // Канал, используемый сетью width: parent.width / 4 // Ширина в четверть столбца horizontalAlignment: Text.AlignRight // Выравнивание текста по правому краю text: (calculateChannel(modelData.frequency) + 1) + " ch." // Канал сети } Label { // Мощность сигнала сети width: parent.width / 4 // Ширина в четверть столбца horizontalAlignment: Text.AlignRight // Выравнивание текста по правому краю text: (modelData.strength - 120) + " dBm" // Мощность сигнала сети } } Item { // Дополнительная информация // Ширина столбца с учётом отступов anchors.right: parent.right anchors.left: parent.left height: childrenRect.height // Высота зависит от вложенных элементов Label { // BSSID сети anchors.left: parent.left // Расположение слева text: "bssid: " + modelData.bssid // BSSID сети } Label { // Шифрование сети anchors.right: parent.right // Расположение справа text: modelData.security.join("/") // Шифрование сети } } ProgressBar { // Визуальное отображение мощности сигнала сети width: parent.width // Ширина столбца с учётом отступов minimumValue: 0 // Минимальное возможное значение maximumValue: 100 // Максимальное возможное значение value: modelData.strength // Мощность сигнала сети } } } VerticalScrollDecorator {} // Ползунок вертикальной прокрутки }
Для определения номера канала по частоте сигнала используется стандартная таблица разделения каналов по частоте и простая функция, построенная на её основе:
/** * Функция рассчитывает номер канала сети по её частоте. * @param frequency - частота сети Wi-Fi * @return Номер канала для выбранной сети Wi-Fi */ function calculateChannel(frequency) { var channel = (frequency - 2412) / 5; return channel > 12 ? 13 : channel; }

Рисунок 1. Отображение окружающих сетей Wi-Fi в виде списка.
Также необходимо осуществлять проверку на возможность получения списка сетей и на количество элементов в нём. За это отвечают поля
powered и count, соответственно. В случае, если сетевой интерфейс выключен или вокруг отсутствуют сети, пользователю необходимо отобразить соответствующее сообщение (рисунок 2). Для этого используется элемент ViewPlaceholder:ViewPlaceholder { enabled: !networksList.powered // Отображается если сетевой интерфейс выключен text: qsTr("Please, turn WiFi on") // Текст сообщения пользователю } ViewPlaceholder { enabled: networksList.powered && networksList.count === 0 // Отображается если вокруг нет сетей text: qsTr("There are no WiFi networks") // Текст сообщения пользователю }

Рисунок 2. Сообщение о необходимости включить Wi-Fi.
Графическое отображение данных
Цель графического отображения информации о сетях Wi-Fi — визуально показать перекрытия между ними. Следовательно, потребуются только значения имени сети, силы и частоты её сигнала.
Для начала требуется подготовить экран для рисования. Для этого используется элемент
Canvas, на котором выполняется отрисовка графика при поступлении сигнала onPaint. Также, с помощью элемента Connections, определяется перерисовка графика при изменении информации об окружающих сетях Wi-Fi. Как и в случае со списком, задаётся возможность отображения сообщения пользователю в случае невозможности получить необходимые данные.Код графического отображения
Page { ViewPlaceholder { // Сообщение пользователю о необходимости включить Wi-Fi enabled: !networksList.powered text: qsTr("Please, turn WiFi on") } ViewPlaceholder { // Сообщение пользователю об отсутствии вокруг сетей Wi-Fi enabled: networksList.powered && networksList.count === 0 text: qsTr("There are no WiFi networks") } SilicaFlickable { // Контейнер элементов экрана anchors.fill: parent Canvas { // Поле для рисования id: graph // Идентификатор для обращения anchors { // Зап��лнение всего доступного пространства с учётом стандартных отступов fill: parent leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin topMargin: Theme.paddingLarge bottomMargin: Theme.paddingLarge } onPaint: drawGraph() // Обработка сигнала рисования } } Connections { // Обработка внешних сигналов target: networksList // Отправитель сигналов - модель с информацией об окружающих сетях onScanRequestFinished: graph.requestPaint() // Запуск отрисовки после каждого сканирования } onOrientationChanged: graph.requestPaint() // Запуск отрисовки при повороте экрана }
Функцию отрисовки графика можно разделить на три части. Сначала инициализируется и очищается поле для рисования. Потом вычисляются координаты осей и сетки в соответствии с экраном. И, наконец, выполняется отрисовка.
function drawGraph() { var context = graph.getContext("2d"); // Инициализация двумерного пространства context.clearRect(0, 0, graph.width, graph.height); // Очистка поля для рисования context.lineWidth = 3; // Толщина линии в три пикселя context.strokeStyle = "gray"; // Серый цвет линий context.fillStyle = "gray"; // Серый цвет заливки context.font = "12pt sans-serif"; // Шрифт для текста // Расчёт и отрисовка осей координат и сетки var channels = calculateChannelsPositions(graph.width); var levels = calculateSignalLevelsPositions(graph.height) drawAxes(context, channels, levels); // Расчёт и отрисовка графиков сетей Wi-Fi if (networksList.count === 0) return; drawWifiFigures(context, graph.width, graph.height, channels); }
Координаты осей и сетки рассчитываются равномерным разделением пространства экрана между каналами и возможными уровнями сигнала. Здесь также используется приведённая выше таблица разделения каналов Wi-Fi по частоте.
// Массив со значащими значениями частоты сигнала Wi-Fi property variant channelsInfo: [11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 83] /** * Функция рассчитывает координаты осей каналов в соответствии с шириной экрана. * @:param: width - ширина поля для рисования * @:return: channels - координаты осей каналов в пикселях */ function calculateChannelsPositions(width) { var channels = []; var step = (width - Theme.horizontalPageMargin) / 94; for (var index in channelsInfo) { channels[index] = channelsInfo[index] * step + Theme.horizontalPageMargin; } return channels; } /** * Функция рассчитывает координаты осей мощности сигнала в соответствии с высотой экрана. * @:param: height - высота поля для рисования * @:return: levels - координаты осей мощности сигнала в пикселях */ function calculateSignalLevelsPositions(height) { var levels = []; var step = (height - Theme.paddingLarge) / 10; for (var index = 0; index < 10; ++index) { levels[index] = index * step + Theme.paddingLarge; } return levels; }
После получения значений координат в пикселях, становится возможным отрисовать оси и сетку. На данном этапе основная работа ложится на функции
moveTo и lineTo для перемещения кисти и рисования прямого отрезка, соответственно.Код отрисовки координатной сетки
/** * Функция отображает оси координат. * @:param: context - контекст для рисования */ function drawGraphBounds(context) { context.beginPath(); context.moveTo(2 * Theme.horizontalPageMargin, Theme.paddingLarge); context.lineTo(graph.width - Theme.horizontalPageMargin, Theme.paddingLarge); context.lineTo(graph.width - Theme.horizontalPageMargin, graph.height - Theme.paddingLarge); context.lineTo(2 * Theme.horizontalPageMargin, graph.height - Theme.paddingLarge); context.closePath(); context.stroke(); } /** * Функция отображает ось канала. * @:param: context - контекст для рисования * @:param: channelX - координата для отображения */ function drawChannelAxe(context, channelX) { context.beginPath(); context.moveTo(channelX, Theme.paddingLarge); context.lineTo(channelX, graph.height - Theme.paddingLarge); context.closePath(); context.stroke(); } /** * Функция отображает номер канала. * @:param: context - контекст для рисования * @:param: channelIndex - индекс канала * @:param: channelX - координата для отображения */ function drawChannelNumber(context, channelIndex, channelX) { var text = parseInt(channelIndex) + 1; var textWidth = context.measureText(text).width; context.fillText(text, channelX - (textWidth / 2), graph.height); } /** * Функция запускает рисование осей каналов. * @:param: context - контекст для рисования * @:param: channels - список координат для отображения */ function drawChannelsAxes(context, channels) { context.lineWidth = 1; for (var channelIndex in channels) { drawChannelAxe(context, channels[channelIndex]); drawChannelNumber(context, channelIndex, channels[channelIndex]); } } /** * Функци�� отображает ось мощности сигнала. * @:param: context - контекст для рисования * @:param: signalLevelY - координата для отображения */ function drawSignalLevelAxe(context, signalLevelY) { context.beginPath(); context.moveTo(2 * Theme.horizontalPageMargin, signalLevelY); context.lineTo(graph.width - Theme.horizontalPageMargin, signalLevelY); context.closePath(); context.stroke(); } /** * Функция отображает значение мощности сигнала. * @:param: context - контекст для рисования * @:param: signalLevel - мощность сигнала * @:param: signalLevelY - координата для отображения */ function drawSignalLevel(context, signalLevel, signalLevelY) { if (signalLevel === '0') return; var text = '-' + signalLevel + '0'; var textWidth = context.measureText(text).width; context.fillText(text, Theme.horizontalPageMargin - (textWidth / 2), signalLevelY); } /** * Функция запускает рисование осей мощностей сигнала. * @:param: context - контекст для рисования * @:param: levels - список координат для отображения */ function drawSignalLevelsAxes(context, levels) { for (var levelIndex in levels) { drawSignalLevelAxe(context, levels[levelIndex]); drawSignalLevel(context, levelIndex, levels[levelIndex]); } } /** * Функция запускает рисование осей координат и координатной сетки. * @:param: context - контекст для рисования * @:param: channels - список координат каналов * @:param: levels - список координат уровней мощности сигнала */ function drawAxes(context, channels, levels) { drawGraphBounds(context); drawChannelsAxes(context, channels); drawSignalLevelsAxes(context, levels); }
После отрисовки координатной сетки можно переходить к отображению графиков окружающих сетей. Для этого будут использоваться кривые Безье:
Код отрисовки графиков сетей
// Цвета линий графика property var strokeColors: ["rgb(255, 0, 0)", "rgb(128, 128, 0)", "rgb(255, 255, 0)", "rgb( 0, 128, 0)", "rgb( 0, 255, 0)", "rgb( 0, 128, 128)", "rgb( 0, 255, 255)", "rgb( 0, 0, 128)", "rgb( 0, 0, 255)", "rgb(128, 0, 128)", "rgb(255, 0, 255)", "rgb(128, 0, 0)"] // Цвета заливки графика (с прозрачностью) property var fillColors: ["rgba(255, 0, 0, 0.33)", "rgba(128, 128, 0, 0.33)", "rgba(255, 255, 0, 0.33)", "rgba( 0, 128, 0, 0.33)", "rgba( 0, 255, 0, 0.33)", "rgba( 0, 128, 128, 0.33)", "rgba( 0, 255, 255, 0.33)", "rgba( 0, 0, 128, 0.33)", "rgba( 0, 0, 255, 0.33)", "rgba(128, 0, 128, 0.33)", "rgba(255, 0, 255, 0.33)", "rgba(128, 0, 0, 0.33)"] /** * Функция рассчитывает y-координату по значению мощности сигнала. * @:param: height - высота поля для рисования * @:param: level - мощность сигнала сети * @:return: y-координата для графика сети */ function calculateCurrentSignalLevelPosition(height, level) { return (height - Theme.paddingLarge) / 100 * Math.abs(level) + Theme.paddingLarge; } /** * Функция рассчитывает x-координаты границ для канала сети. * @:param: width - ширина поля для рисования * @:param: channel - номер канала сети * @:return: x-координаты границ для графика сети */ function calculateBoundsPositionForChannel(width, channel) { var step = (width - Theme.horizontalPageMargin) / 94; var left = (channelsInfo[channel] - 11) * step + Theme.horizontalPageMargin; var right = (channelsInfo[channel] + 11) * step + Theme.horizontalPageMargin; return [left, right]; } /** * Функция рассчитывает опорную точку для кривой Безье. * http://codetheory.in/calculate-control-point-to-make-your-canvas-curve-hit-a-specific-point/ * @:param: channelCoord - координата канала сети * @:param: levelPosition - координата уровня мощности сигнала * @:param: bounds - координаты границ покрытия сети * @:return: Координаты опорной ��очки */ function calculateCurrentPoint(channelCoord, levelPosition, bounds) { var cpx = 2 * channelCoord - (bounds[0] + bounds[1]) / 2; var cpy = 2 * levelPosition - (graph.height + graph.height - (2 * Theme.paddingLarge)) / 2; return { x: cpx, y: cpy }; } /** * Функция отрисовывает график сети Wi-Fi с помощью кривой Безье. * @:param: context - контекст для рисования (канвас) * @:param: channelCoord - координата канала * @:param: levelPosition - координата мощности сигнала * @:param: bounds - границы графика сигнала */ function drawWifiFigure(context, channelCoord, levelPosition, bounds) { var cp = calculateCurrentPoint(channelCoord, levelPosition, bounds); context.beginPath(); context.moveTo(bounds[0], graph.height - Theme.paddingLarge); context.quadraticCurveTo(cp.x, cp.y, bounds[1], graph.height - Theme.paddingLarge); context.closePath(); context.stroke(); context.fill(); } /** * Функция отображает имя сети над её графиком. * @:param: context - контекст для рисования (канвас) * @:param: wifiInfo - структура с информацией о сети Wi-Fi * @:param: channels - список каналов * @:param: levelPosition - координата мощности сигнала */ function drawWifiName(context, wifiInfo, channels, levelPosition) { var textWidth = context.measureText(wifiInfo.name).width; context.fillText(wifiInfo.name, channels[calculateChannel(wifiInfo.frequency)] - (textWidth / 2), levelPosition - Theme.paddingSmall); } /** * Функция отрисовывает графики всех окружающих сетей. * @:param: context - контекст для рисования (канвас) * @:param: width - ширина поля для рисования * @:param: height - высота поля для рисования * @:param: channels - список каналов */ function drawWifiFigures(context, width, height, channels) { context.lineWidth = 2; for (var networkIndex = 0; networkIndex < networksList.count; ++networkIndex) { var levelPosition = calculateCurrentSignalLevelPosition(height, (networksList.get(networkIndex).strength - 120)) var bounds = calculateBoundsPositionForChannel(width, calculateChannel(networksList.get(networkIndex).frequency)) context.strokeStyle = strokeColors[networkIndex % strokeColors.length]; context.fillStyle = fillColors[networkIndex % fillColors.length]; drawWifiFigure(context, channels[calculateChannel(networksList.get(networkIndex).frequency)], levelPosition, bounds); context.fillStyle = context.strokeStyle; drawWifiName(context, networksList.get(networkIndex), channels, levelPosition); } }
На этом создание графика сетей заканчивается. Результат представлен на рисунке 3.

Рисунок 3. Графическое отображение сетей Wi-Fi.
Заключение
В данной статье были показаны два способа получения информации об окружающих сетях Wi-Fi и два способа её отображения: списком и графиком. Полный код готового приложения, как всегда, доступен на GitHub.
Возникающие по ходу разработки вопросы и идеи всегда можно обсудить в Telegram-чате и в группе ВКонтакте.
