Sailfish OS用のWi-Fiネットワークスキャナーの開発

はじめに



時々、仕事の問題を解決するときに、周囲のWi-Fiネットワークに関する情報が必要になることがあります:チャネル、電力、暗号化の種類など。そして、人気のあるAndroidとiOSのユーティリティが多数ある場合、Sailfish OSには1つしかありません。 今日、このユーティリティを例として使用して、周囲のWi-Fiネットワークとその表示に関する情報の受信を、リストとグラフィックの2つの方法で分析します。



資料を学習する前に、Sailfish OSおよびwpa_cli



ユーティリティーの開発に関する基本的な知識があることをお勧めします。



Wi-Fiネットワーク情報の取得



デバイスを取り巻くWi-Fiネットワークに関する情報を取得する主な方法は2つありますwpa_cli



ユーティリティを使用するか、 MeeGo.Connman



モジュールから文書化されていないTechnologyModel



要素をMeeGo.Connman



ます。



最初の方法は、「額」ソリューションです。 Sailfish OSでwpa_cli



ユーティリティを使用することは、他の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








このアプローチには2つの主な問題があります。 まず、それを使用するにはスーパーユーザー権限が必要です。 次に、コンソールコマンドの結果の読み取りを実装する必要があります。



最初の問題は、プログラムの起動時にユーザーにパスワードを要求し、 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(); }
      
      







スキャン結果を読み取るには、1つの機能でも十分です。



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







ただし、アプリケーションの起動時にスーパーユーザー特権を要求すると、平均的なユーザーが混乱する可能性があるため、ルートアクセスを必要とする機能の使用を避ける必要があります。 したがって、周囲のWi-Fiネットワークに関する情報を取得するタスクの一部として、残念ながら標準ドキュメントには記載されていないMeeGo.Connman



モジュールを使用することをお勧めします。



それを使用するには、次の行を追加します。



 import MeeGo.Connman 0.2
      
      





このモジュールは、タスクに必要なTechnologyModel



要素を提供します。これにより、周囲のWi-Fiネットワークに関する構造化された情報をQMLファイルの数行で取得できます。



 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に記載されているフィールドは、スキャナーにとって最も重要です。



表1. Wi-Fiネットワークスキャナーで使用されるフィールド。
フィールド名 データ型 説明
お名前 QString ネットワーク名
頻度 quint16 信号周波数
強さ 聖人 信号強度
bssid QString Bssid
保安 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ネットワークをリストで表示

図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をオンにする必要性に関するメッセージ。

図2. Wi-Fiをオンにする必要性に関するメッセージ。



グラフィカルデータ表示



Wi-Fiネットワークに関する情報をグラフィカルに表示する目的は、ネットワーク間のオーバーラップを視覚的に示すことです。 したがって、ネットワーク名、信号の強度、周波数の値のみが必要になります。



まず、描画用の画面を準備する必要があります。 これを行うには、 onPaint



信号onPaint



ときにグラフがonPaint



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







チャートレンダリング機能は3つの部分に分けることができます。 最初に、描画フィールドが初期化され、クリアされます。 次に、軸とグリッドの座標が画面に従って計算されます。 そして最後に、レンダリングが完了しました。



 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



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ネットワークのグラフィカル表示。

図3. Wi-Fiネットワークのグラフィカル表示。



おわりに



この記事では、周囲のWi-Fiネットワークに関する情報を取得する2つの方法と、それを表示する2つの方法、リストとグラフを示しました。 完成したアプリケーションの完全なコードは、いつものようにGitHubで入手できます



開発中に生じる質問やアイデアは、 TelegramチャットVKontakteグループでいつでも議論できます。



All Articles