Windows ストアアプリでUPnPデバイスを発見する

UPnP(Universal Plug and Play)対応のデバイスをWindows ストアプリで発見する方法を紹介します。UPnPアプリを発見するプロトコルであるSSDPを使ってデバイスの定義を取得する方法です。DLNA対応機器や、Philips Hueなど、この解説を使ってアドレスを取得することができます。

SSDPは、UDPのMulticast Group通信を使っており、ストアアプリでは、DatagramSocketを使用します。

DatagramSocket ssdpSocket = new DatagramSocket();

と、データグラムソケットを一つ作成します。そして、データグラムソケットに、UPnPデバイスからの応答を受け取るためのハンドラ―を登録します。

ssdpSocket.MessageReceived += ssdpSocket_MessageReceived;

SSDPは、239.255.255.250、1900というグループアドレス、グループポートを使います。この情報を使ってマルチキャストグループにジョインします。

    var ssdpGroup = new HostName("239.255.255.250");
    string ssdpGroupPort = "1900";
    await ssdpSocket.BindEndpointAsync(null, "");
    ssdpSocket.JoinMulticastGroup();

そして、SSDPの規約に従った、デバイス発見の為の送信メッセージを組立て送信します。

    string discoverPacket = "M-SEARCH * HTTP/1.1\r\n";
    discoverPacket += "HOST: " + ssdpGroup .DisplayName +":" + ssdpGroupPort + "\r\n";
    discoverPacket += "ST: upnp:rootdevice\r\n";
    discoverPacket += "MAN: \"ssdp:discover\"\r\n";
    discoverPacket += "MX: 3:\r\n\r\n";
    var stream = await ssdpSocket.GetOutputStreamAsync(ssdpGroup, ssdpGroupPort);

    var writer = new DataWriter(stream) { UnicodeEncoding = UnicodeEncoding.Utf8 };
    writer.WriteString(discoverPacket);
    await writer.StoreAsync();

はい、これで、準備OK。このメッセージを送信後、同一ネットワークにある、SSDP対応デバイスから応答メッセージが先ほど登録したハンドラ―に送られてきます。

次に、メッセージ受信用のハンドラ―です。

async void hueSocket_MessageReceived(DatagramSocket sender, DatagramSocketMessageReceivedEventArgs args)
{
    var reader = args.GetDataReader();
    uint length = reader.UnconsumedBufferLength;
    try
    {
        // 受信メッセージの取り出し
        var receivedMessage = reader.ReadString(length);
        // 受信メッセージ解析
        var response = new Dictionary<string, string>();
        string resultCode = "";
        string resultStatus = "";
        foreach (var msgLine in receivedMessage.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
        {
            if (msgLine.Contains(":"))
            {
                int colonIndex = msgLine.IndexOf(":");
                if (colonIndex + 1 == msgLine.Length)
                {
                    response.Add(msgLine.Substring(0, colonIndex), "");
                }
                else
                {
                   response.Add(msgLine.Substring(0, colonIndex), msgLine.Substring(colonIndex + 2));
                }
            }
            else if (msgLine.StartsWith("HTTP"))
            {
                var strings = msgLine.Split(' ');
                resultCode = strings[1];
                resultStatus = strings[2];
           }
      }
      if (resultCode == "200" && resultStatus == "OK")
       {
            // いちおう、メッセージが正しいかも確認しておいて…
            // メッセージの中にあるLOCATIONの値を取り出す
           var upnpDeviceIp = "";
           if (response.ContainsKey("LOCATION"))
           {
               var strings = response["LOCATION"].Split(':');
               if (strings.Length == 3)
               {
                    // LOCATIONの値が、UPnPデバイスの定義に関するURLである。そのURLを使って、UPnPの定義を取得
                   upnpDeviceIp = strings[0] + ":" + strings[1] + ":" + strings[2];
                   if (upnpDeviceIp.EndsWith(".xml"))
                    {
                        var httpClient = new System.Net.Http.HttpClient();
                        var deviceDescripXml = await XmlDocument.LoadFromUriAsync(new Uri(upnpDeviceIp));
                        // 中身を解析して必要な情報を確認

最後のXMLコンテンツの中に、色々な定義がはいっています。Philips Hueの場合は、LOCATIONの値が、https://<device's ip address:80/description.xml になっていて、description.xmlより前をREST APIのアドレスとして使えば、ストアアプリからリモートコントロールが可能です。
SSDPに応答するデバイスが沢山ネット上で参照できる場合、それらのデバイスが一斉に応答を返してくるので、メッセージ、及び、LOCATIONで示されたコンテンツの内容を確認して、制御したいデバイスを見つけなければなりません。

一旦見つかったらssdpSocketは必要ないのでDisposeしておきましょう。