📜 ⬆️ ⬇️

Development of Wi-Fi network scanner for Sailfish OS

Introduction


Sometimes, when solving work tasks, there is a need for information about the surrounding Wi-Fi networks: channel, power, encryption type, etc. And if there is a lot of utilities for popular Android and iOS mobile apps, then only one for Sailfish OS. Today, using this utility as an example, obtaining information about the surrounding Wi-Fi networks and displaying it in two ways will be disassembled: list and graphically.

Before studying the material, it is desirable to have a basic understanding of the development under Sailfish OS and the utility wpa_cli .

Getting information about Wi-Fi networks


There are two main ways to get information about the Wi-Fi networks surrounding the device: use the wpa_cli utility or the undocumented TechnologyModel element from the MeeGo.Connman module.

The first method is a head-on solution. Using the wpa_cli utility in Sailfish OS is no different from another Linux distribution:
')
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


This approach has two main problems. First, it requires superuser rights. Secondly, it is necessary to implement the reading of the result of the console command execution.

The first problem is solved by requesting a password from the user when starting the program and further using the QProcess module:

Interacting with 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(); } 


One function is also enough to read the scan results:

Getting the wpa_cli result
 /** *       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); } 


However, requesting superuser privileges when launching an application can confuse a regular user, and, therefore, it is necessary to avoid using functions that require root access. Therefore, as part of the task of obtaining information about the surrounding Wi-Fi networks, it is recommended to use the MeeGo.Connman module, unfortunately not presented in the standard documentation.

To use it, you need to add the following line:

 import MeeGo.Connman 0.2 

This module provides the necessary element for the task TechnologyModel , allowing just a few lines in the QML file to get structured information about the surrounding Wi-Fi networks:

 TechnologyModel { id: networksList //    name: "wifi" //     } 

However, it is necessary to update the information manually. This is best done using the Timer component:

 Timer { id: updateTimer //    interval: 2000 //     running: true //   repeat: true //      triggeredOnStart: true //      onTriggered: networksList.requestScan() //     Wi-Fi } 

Now all the necessary information is presented in a structured form, and the process of obtaining it does not require an administrator password. Accordingly, you can proceed to the process of displaying the received data.

It is worth noting that the same interface is also available for applications implemented in C ++. An example of its use is published in the repository of the module.

Display data list


The model described in the previous section returns a list of elements of the NetworkService type, which provides all the necessary information about the surrounding networks. Of all the variety of the greatest interest for the scanner are the fields described in table 1.

Table 1. Fields used in the Wi-Fi network scanner.
Field nameData typeDescription
nameQStringNetwork name
frequencyquint16Signal frequency
strengthuintSignal strength
bssidQStringBssid
securityQStringListEncryption type

Now, knowing the way of storing information about networks and the element for drawing lists, you can readily display the obtained information (Figure 1):

List Display Code
 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 {} //    } 


To determine the channel number by signal frequency, the standard table of channel separation by frequency and a simple function based on it are used:

 /** *        . * @param frequency -   Wi-Fi * @return      Wi-Fi */ function calculateChannel(frequency) { var channel = (frequency - 2412) / 5; return channel > 12 ? 13 : channel; } 

Figure 1. Displaying surrounding Wi-Fi networks in a list.
Figure 1. Displaying surrounding Wi-Fi networks in a list.

It is also necessary to check for the possibility of obtaining a list of networks and the number of elements in it. Responsible for this are the fields powered and count , respectively. If the network interface is turned off or there are no networks around, the user needs to display a corresponding message (Figure 2). To do this, use the ViewPlaceholder element:

 ViewPlaceholder { enabled: !networksList.powered //      text: qsTr("Please, turn WiFi on") //    } ViewPlaceholder { enabled: networksList.powered && networksList.count === 0 //      text: qsTr("There are no WiFi networks") //    } 

Figure 2. The message about the need to turn on Wi-Fi.
Figure 2. The message about the need to turn on Wi-Fi.

Graphic display of data


The purpose of the graphical display of information about Wi-Fi networks is to visually show the overlap between them. Therefore, only the values ​​of the network name, the strength and frequency of its signal will be required.

First you need to prepare a screen for drawing. To do this, use the Canvas element, on which the graphics are onPaint when the onPaint signal onPaint . Also, with the help of the Connections element, the graph is redrawn when changing information about the surrounding Wi-Fi networks. As in the case with the list, the possibility is set to display a message to the user if it is impossible to obtain the necessary data.

Graphic display code
 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() //      } 


The function of drawing graphics can be divided into three parts. First, the drawing field is initialized and cleared. Then coordinates of axes and grids are calculated in accordance with the screen. And finally, the rendering is done.

 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); } 

The coordinates of the axes and grids are calculated by evenly separating the screen space between the channels and possible signal levels. It also uses the above table of division of Wi-Fi channels by frequency.

 //       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; } 

After obtaining the coordinate values ​​in pixels, it becomes possible to draw the axes and the grid. At this stage, the main work falls on the moveTo and lineTo to move the brush and draw a straight line, respectively.

Grid drawing code
 /** *    . * @: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); } 


After the grid is drawn, you can proceed to the display of graphs of surrounding networks. Bezier curves will be used for this:

Network Graphics Drawing Code
 //    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); } } 


At the creation of a network of graphics ends. The result is shown in Figure 3.

Figure 3. Graphic display of Wi-Fi networks.
Figure 3. Graphic display of Wi-Fi networks.

Conclusion


This article showed two ways to obtain information about the surrounding Wi-Fi networks and two ways to display it: a list and a schedule. The full code of the finished application, as always, is available on GitHub .

Questions and ideas arising in the course of development can always be discussed in the Telegram-chat and in the VKontakte group .

Source: https://habr.com/ru/post/345750/


All Articles