2015/01/31

無線LANのSSIDをC#から取得する

無線LANのSSIDをC#からAPIで取得するコードを書いてみた。

1. 背景


アプリから無線LANのSSIDを取得したいときがあるが、標準の.NET FrameworkにもWinRTにもそれが可能なAPIは用意されていない。C++にはNative Wifi APIがあるが、このマネージド実装のManaged Wifi APIは開発が止まって久しく、かといってP/Invokeの宣言を起こすのも大変そうなので敬遠していた。実用上はNetshの出力をパースすれば大体足りるし。

それが、ふとMSDNでこんなエントリを見かけた。
「おおっ」と思いつつダウンロードしたが、実体部分はC++のライブラリで、それをC#から使うものだった(タイトルどおりではある)。悔しかったので少し探したところ、幾つかサンプルが挙げられていた。
これらとMSDNを合わせ見た結果、必要なピースは揃ってそうだったので書いてみた。

2. 利用可能なネットワークのSSIDを取得する


手順としては以下のようになる。
  1. WlanOpenHandleでハンドルを取得する。このdwClientVersionはOSによって変わるが、Vista以降は2。
  2. WlanEnumInterfacesでPCにある無線LANインターフェイス(無線LANアダプター)の情報を取得する。
  3. WlanGetAvailableNetworkListで各インターフェイスから見える無線LANネットワークの情報を示すWLAN_AVAILABLE_NETWORKを取得する。これは各インターフェイスの感度にも左右されるが、インターフェイスが複数あれば当然、単数でも試した限りでは重複があり得る。
  4. WLAN_AVAILABLE_NETWORKからSSIDを取得する。
public static IEnumerable<string> GetAvailableNetworkSsids()
{
  var clientHandle = IntPtr.Zero;
  var interfaceList = IntPtr.Zero;
  var availableNetworkList = IntPtr.Zero;

  try
  {
    uint negotiatedVersion;
    if (WlanOpenHandle(
      2, // Client version for Windows Vista and Windows Server 2008
      IntPtr.Zero,
      out negotiatedVersion,
      out clientHandle) != ERROR_SUCCESS)
      yield break;

    if (WlanEnumInterfaces(
      clientHandle,
      IntPtr.Zero,
      out interfaceList) != ERROR_SUCCESS)
      yield break;

    var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);

    Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);

    foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
    {
      if (WlanGetAvailableNetworkList(
        clientHandle,
        interfaceInfo.InterfaceGuid,
        WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_MANUAL_HIDDEN_PROFILES,
        IntPtr.Zero,
        out availableNetworkList) != ERROR_SUCCESS)
        continue;

      var networkList = new WLAN_AVAILABLE_NETWORK_LIST(availableNetworkList);

      foreach (var network in networkList.Network)
      {
        Debug.WriteLine("Interface: {0}, SSID: {1}, Quality: {2}",
          interfaceInfo.strInterfaceDescription,
          network.dot11Ssid,
          network.wlanSignalQuality);

        yield return network.dot11Ssid.ToString();
      }
    }
  }
  finally
  {
    if (availableNetworkList != IntPtr.Zero)
      WlanFreeMemory(availableNetworkList);

    if (interfaceList != IntPtr.Zero)
      WlanFreeMemory(interfaceList);

    if (clientHandle != IntPtr.Zero)
      WlanCloseHandle(clientHandle, IntPtr.Zero);
  }
}
P/Invokeの宣言はレポジトリの方を参照。

3. 接続中のネットワークのSSIDを取得する


手順は2.までは上と同じ。
  1. WlanOpenHandleでハンドルを取得する。
  2. WlanEnumInterfacesでPCにある無線LANインターフェイスの情報を取得する。
  3. WlanQueryInterfaceで各インターフェイスの現在の接続状況を示すWLAN_CONNECTION_ATTRIBUTESを取得する。このためにはOpCodeにwlan_intf_opcode_current_connectionを指定する。
  4. WLAN_CONNECTION_ATTRIBUTESのisStateで接続中かどうか判別できるので、接続中ならwlanAssociationAttributesのWLAN_ASSOCIATION_ATTRIBUTESからSSIDを取得する。
public static IEnumerable<string> GetConnectedNetworkSsids()
{
  var clientHandle = IntPtr.Zero;
  var interfaceList = IntPtr.Zero;
  var queryData = IntPtr.Zero;

  try
  {
    uint negotiatedVersion;
    if (WlanOpenHandle(
      2, // Client version for Windows Vista and Windows Server 2008
      IntPtr.Zero,
      out negotiatedVersion,
      out clientHandle) != ERROR_SUCCESS)
      yield break;

    if (WlanEnumInterfaces(
      clientHandle,
      IntPtr.Zero,
      out interfaceList) != ERROR_SUCCESS)
      yield break;

    var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);

    Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);

    foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
    {
      uint dataSize;
      if (WlanQueryInterface(
        clientHandle,
        interfaceInfo.InterfaceGuid,
        WLAN_INTF_OPCODE.wlan_intf_opcode_current_connection,
        IntPtr.Zero,
        out dataSize,
        ref queryData,
        IntPtr.Zero) != ERROR_SUCCESS) // If not connected to a network, ERROR_INVALID_STATE will be returned.
        continue;

      var connection = (WLAN_CONNECTION_ATTRIBUTES)Marshal.PtrToStructure(queryData, typeof(WLAN_CONNECTION_ATTRIBUTES));
      if (connection.isState != WLAN_INTERFACE_STATE.wlan_interface_state_connected)
        continue;

      var association = connection.wlanAssociationAttributes;

      Debug.WriteLine("Interface: {0}, SSID: {1}, BSSID: {2}, Signal: {3}",
        interfaceInfo.strInterfaceDescription,
        association.dot11Ssid,
        association.dot11Bssid,
        association.wlanSignalQuality);

      yield return association.dot11Ssid.ToString();
    }
  }
  finally
  {
    if (queryData != IntPtr.Zero)
      WlanFreeMemory(queryData);

    if (interfaceList != IntPtr.Zero)
      WlanFreeMemory(interfaceList);

    if (clientHandle != IntPtr.Zero)
      WlanCloseHandle(clientHandle, IntPtr.Zero);
  }
}
なお、3.のWlanQueryInterfaceはそのインターフェイスが接続中でないときはERROR_INVALID_STATEを返してくる。

4. まとめ


Windows 7とWindows 8.1、ついでにWindows 10 TPで確認したところ問題なさそうだったので、とりあえずこれで行けると思う。

しかしAPIで取得できるとコマンド出力をパースするよりわずかでも速いし、気持ちすっきりするのがいい。まあP/Invokeのマーシャリングは結構泥臭いけど。

[追記] UTF-8のSSID

SSIDというとASCII文字列と思われがちだが、IEEE 802.11の規格を読むとSSIDは0-32のOctet stringとされていて、平たくいうと長さ0-32のバイト配列とされている。これは現在はUTF-8も使えることになっていて、当然日本語を使うこともできる(それで駄洒落を作るのが少しネタになっていた)。

Native Wifi APIでSSIDの情報を格納するのはDOT11_SSIDだが、この説明でも"The SSID that is specified by the ucSSID member is not a null-terminated ASCII string."と釘が刺されている。

このDOT11_SSIDに対応するP/Invoke用の構造体を初め以下のように書いていた。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
  public uint uSSIDLength;

  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
  public string ucSSID;

  public override string ToString()
  {
    if ((ucSSID == null) || (ucSSID.Length < (int)uSSIDLength))
      return null;

    return ucSSID.Substring(0, (int)uSSIDLength);
  }
}
これには問題があって、uSSIDLengthの示す長さを勘違いしていることもさることながら、実際に試してみるとUnmanagedType.ByValTStrでは正しくマーシャリングされず、「の」が「縺ョ」のように文字化けを起こす。これを回避するには、ucSSIDをまずはバイト配列にマーシャリングした上で、これから文字列に変換すればうまく行った。
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] ucSSID;

public override string ToString()
{
  return Encoding.UTF8.GetString(ucSSID, 0, (int)uSSIDLength);
}
さらに、規格に忠実にバイト配列としても取り出せるようにまとめ直せば、こんな感じ。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
  public uint uSSIDLength;

  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
  public byte[] ucSSID;

  public byte[] ToBytes()
  {
    return (ucSSID != null)
      ? ucSSID.Take((int)uSSIDLength).ToArray()
      : null;
  }

  public override string ToString()
  {
    return (ucSSID != null)
      ? Encoding.UTF8.GetString(ToSsidBytes())
      : null;
  }
}
ちなみに、Windows 8.1のNetshの出力を見るとUnmanagedType.ByValTStrを使ったときと同じように文字化けする。一方、設定のネットワークから見ると文字化けしないので、NetshのUTF-8対応が遅れているようで、Windows 10 TPでも解消してないらしい。
ということで、UTF-8対応ができるという点が、現状APIによる方法が明確に優れている点になる。

[追記] ライブラリ公開

ライブラリとしてまとめたので、コードをなるべく合わせるよう修正した。

2015/01/28

Windows 10 TPのインストール時のエラー

Windows 10 TPのインストール時に遭遇したエラーについての記録。

1. エラーの状況


最近のWindowsのインストールで問題が出ることはあまりないですが、久々に謎なエラーに遭遇しました。前提として、UEFI Onlyに設定したThinkPad X230でUEFI用に作成したUSBメモリから起動してのインストールです。

状況としては、
  1. Build 9926の日本語版のISOファイルからUSBメモリを作成し、これから起動しようとしたところ、「お使いのPCに必要なメディアドライバーがありません。」というエラーが表示されて進めない。


  2. IntelのIRSTのドライバーファイルを読み込ませてみたが、変化なし。たぶんこのメッセージどおりの問題ではない。
  3. ISOファイルのハッシュ値を確認したが、ダウンロードサイトに示されたとおり。
  4. USBメモリを作成し直してみたが、エラーは変わらず。そもそもUEFI用のUSBメモリの作成方法は簡単で(後述)、間違う余地は少ない。
  5. 英語版のISOファイルからUSBメモリを作成してみたが、"A media driver your computer needs is missing."という同内容のエラーが出る。


  6. 単純な問題ではない気配がしてきたので、ISOファイルをDVD-Rに焼いてインストールしたところ、問題なく終了。問題はUSBメモリの起動にある。
  7. 残しておいたBuild 9879のISOファイルからUSBメモリを作成してみたが、エラーは変わらず。Build 9926特有の問題ではないが、しかし以前は問題なかったはず。
  8. ふとUSBメモリを差すコネクタをX230の向かって左側から右側に変えたところ、エラーは出ず、ハードウェアと関係があることが判明。正確には左側2箇所のコネクタ(USB.3.0)のどちらでもエラーが出るが、右側1箇所のコネクタ(USB2.0)では出ない。なお、左側でも通常時は問題ないし、左側で作成したUSBメモリで右側から起動もできる。
  9. 確認のため、USBメモリをそれまでのPicoDrive F3 32GBから、Express RC8 25GBとJetFlash 760 16GB(いずれもUSB3.0)に変えてみたが、エラーの出る条件は変わらず。USBメモリ側の問題ではない。
  10. さらに確認のため、USBメモリをPicoBoost 8GB(USB 2.0)に変えてみたところ、左右どちらでもエラーは出ず。エラーが出るのはUSB3.0の場合のみ。
  11. 念のため、Windows 8.1 Enterprise評価版のISOファイルを使ってみたところ、USB3.0のUSBメモリでも左右どちらでもエラーは出ず。エラーが出るのはWindows 10 TPの場合と判明。
以上をまとめると、
  • X230の左側のUSBコネクタ(USB3.0)
  • USB 3.0のUSBメモリから
  • Windows 10 TPのインストーラーを起動
しようとした場合にこのエラーが出る、ということになります。

真っ先に疑われるのはX230のUSBコネクタの劣化あるいは埃詰まりで、通常時には問題ないものがインストール時に顕在化するというのは昔からあることですが、
  • 製造から1年足らずで、2箇所のコネクタに同じ問題が出るものか。
  • USB3.0用のメス側の接点はベロの先端に、かつX230の場合は下向きに付いているので、埃は付着しにくい。
ということがあって、保留です。

一つ明らかなのは、Windows 10 TPのインストーラーはUSB3.0の認識に何か変更があったっぽい、ということですが、まあ問題に遭遇でもしない限り無駄知識ですけど。

[追記1]

コメントいただいた情報によればX1 CarbonのUSB3.0コネクタでも同じエラーが出るそうで、かつX230の右側のコネクタはUSB2.0なので、エラーが出る条件としてはUSB3.0がやはりキーのようです。

[追記2]

Microsoft CommunityのWindows Insider Programにポストしました。
[追記3]

Insider PreviewのBuild 10074のISOではこのエラーは出なくなりました(日本語版、英語版とも)。問題は解決されたようです。

2. UEFI用のUSBメモリの作成


なお、UEFIではブートに関するノウハウが従来と変わりました。パーティションがアクティブであること、専用のブートセクタであることは必要なくなりました。代わりにパーティションがFAT32であることが必須です。

したがってブータブルなUSBメモリを作成するには、とにかくFAT32でフォーマットして、ISOファイル(x64版であること)の中身をコピーするだけです。特別なツールやDiskPartを使う場面は基本的にないと思います。

この点に関して、Microsoftの高橋さんが書かれている方法のうちアクティブとブートセクタの部分は、従来の場合と両対応にするために必要なことであって、UEFIだけ考える場合は必要ないと思います。

2015/01/26

Windows 10 TPのバージョン判定(続)

Windows 10 TPのバージョン判定を新しいBuild 9926で試してみると、

マニフェストファイルにWindows 10のcompatibilityの記述あり。

同なし。

VerifyVersionInfoがGetVersion/GetVersionExと同じ呪縛にかかった模様。意図された動作かどうか、現状分からないが。

最後まで残るとすれば、個人的な予想ではWMIではないかと。WMIはシステム管理にも使われるので、アプリ対策の都合だけで変えにくいと思うので。

2015/01/11

Venue 8 Proと+port

OTGのUSBコネクタで充電とデータ通信が同時にできる+portの話。

1. 顛末


Venue 8 Proは充電とUSBを同時使用できないと書いた後、純正のDell Micro USB Dongle for Data and Chargingを国際発送してくれる店を探してみたが、これが見つからなかったので、結局Mobile Design Labsの+portをオーダーした。本体29ドルに送料10ドルを合わせて39ドル。

これはKickStarterで資金調達して製品化されたもので、動画でもVenue 8 Proでデモしているように、純正を除けば唯一の確実な選択肢、と思われた。


が、実はその数日前に発表されたBuffaloの「Android用充電機能付きUSBハブ」BSH4AMB03BK/Nの対応機種にVenue 8 Proが含まれているのを後になってから知り、さらにACASISのHO27の店頭販売が始まった

12月前半まではモバイルショップを回っても影も形もなかったのに、一気に状況が変わってきた。ということで、このタイミングで+portを買うこともなかったが、来たものは来たものとして使っていく所存。

2. 充電時間


+portはVenue 8 Proを念頭に置いて作られたものらしいから大丈夫だろうとは思ったものの、一応充電時間を比較してみた。
Venue 8 Pro and Plusport

接続方法は以下のとおり。
  1. +portのMicro USBコネクタにVenue 8 Proの付属ACアダプターからのMicro USBケーブルを接続
  2. +portのMicro USBケーブルをVenue 8 ProのMicro USBコネクタに接続
  3. +portのUSBコネクタにLogitecのUSB有線アダプターのLAN-GTJU3を接続
文字にするとややこしいが、実際はそうでもない。ちなみに、+portからACアダプターのケーブルを抜くと+portのUSBコネクタに差したデバイスとの接続が失われるので、+portの動作自体にACアダプターからの電源供給が必要な模様。

比較するのは以下の3ケース。
  • Case 1: 通常どおり付属ACアダプターからのケーブルをVenue 8 Proに直結。負荷として無線LANで接続した状態でYouTubeの動画を流し続ける。

  • Case 2: +portにACアダプターからのケーブルだけ接続した状態で、+portのケーブルをVenue 8 Proに接続し、充電が開始された後、+portのUSBコネクタに有線LANアダプターを接続。負荷として有線LANで接続した状態で(無線LANはオフ)同じくYouTubeの動画を流し続ける。

  • Case 3: +portにACアダプターからのケーブルと有線LANアダプターを両方接続した状態で、+portのケーブルをVenue 8 Proに接続。負荷はCase 2と同じ。
Case 1だけ無線LANだが、そこは仕方ない。Case 2は、これが+portの説明書で示された手順で、かつOTGケーブルには電源を先に接続して充電を開始させた後でデータ通信に切り替えてしまうというテクニックがあるので、それが関係あるか確かめるため。実用上はCase 3で済めば、それに越したことはない。

充電状態の記録には簡単なアプリを作った。
ただ、参照元のデータがリアルタイムに更新されない(時々思い出したようにジャンプする。これは通知領域の電源表示も同様)ことが分かったので、細かい動きは気にしないということで。

結果は以下のとおり。
10%から100%までの充電時間には一応差はある。
  • Case 1: 216分
  • Case 2: 239分
  • Case 3: 236分
が、グラフを見て明らかなとおり、有意な差はないと考えていいと思う。なお、Case 2の終盤が直線なのは値が大きくジャンプしただけで、細かく値が取れていれば他と同様のカーブになったと思う。

結論として、+portにACアダプターからのケーブルとUSBデバイスを両方接続した状態を固定して、+portのケーブルをVenue 8 Proに抜き差しするだけで問題ないと言えよう。

3. ガジェットとして


外装の成形や組み立ての精度は高くないが、機能が果たせればそこは問わない。
Plusport
Plusport
Plusport

ただし、このケーブルの固定方法はよろしくない。
Plusport

外装のチューブが固定されておらず、シールド線が剥き出し。これはそのうち断線するおそれが高いので、何らかの補強策が必要。

まあ壊れたらBuffaloの製品があると考えれば、既に気は楽。