Attempting UPnP on Windows Phone 7.5 (Mango), Part 1: SSDP Discovery


[Update: SSDP is WORKING now: all code updated]

I have been on a mission for maybe six months now to get something resembling a UPnP stack working on Windows Phone Mango. This post covers Discovery.

First off some basics: read the UPnP spec (download this and read documents/UPnP-arch-DeviceArchitecture-v1.1-20081015.pdf), which I found surpisingly straight-forward for a hard-core specification. Next download the Intel UPnP tools, which includes the awesome Device Spy application. Oh and get some UPnP devices on your home network (although you probably have many already even if you didn't know). For me the target of my work has been my Sonos hardware, but on my network I also have a UPnP router and my PCs that respond as various UPnP devices.

UPnP consists of a few basic operations. Here they are, and how successful (or otherwise) I have been on the phone with them to date:

  • Discovery: the finding of devices on the network, using the SSDP protocol. I had been utterly unsuccessful but thanks to Tracey Trewin from Visual Studio it is working, see below.
  • Invocation: the calling of UPnP methods on a device, via http/SOAP. This I have been 100% successful with, which is good as without this then it doesnt matter if I can get the other basics working or not.
  • Eventing: the registering of events such that a control point can be notified of events from a device. This does not appear to be possible on Windows Phone Mango as the equivalent of bind is not available.

This post is concerned with the first item: the discovery of devices, using SSDP.

First off here is the consumer code form a standard Silverlight main page, with a button and textblock added:

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            SSDPFinder finder = new SSDPFinder();
            string item;
 
            item = "urn:schemas-upnp-org:device:ZonePlayer:1";  // Sonos hardware
            item = "urn:schemas-upnp-org:device:Basic:1";       // eg my Home Server
            item = "urn:schemas-upnp-org:device:MediaServer:1"// eg my PCs
            item = "ssdp:all"                                   // everything (NOT * as it was previously)
            
            finder.FindAsync(item, 4, (findresult) =>
                {
                    Dispatcher.BeginInvoke(() =>
                        {
                            var newservice = HandleSSDPResponse(findresult);
                            if (newservice != null)
                            {
                                textBlock1.Text += "\r\n" + newservice;
                                Debug.WriteLine(newservice);
                            }
                        });
                });
        }
 
        private List<string> RootDevicesSoFar = new List<string>();
 
        // Primitive SSDP response handler
        private string HandleSSDPResponse(string response)
        {
            StringReader reader = new StringReader(response);
            List<string> lines = new List<string>();
            string line;
            for (; ; )
            {
                line = reader.ReadLine();
                if (line == null)
                    break;
                if (line != "")
                    lines.Add(line);
            }
            // Note ToLower addition!
            string location = lines.Where(lin => lin.ToLower().StartsWith("location:")).FirstOrDefault();             // Only record the first time we see each location             if (!RootDevicesSoFar.Contains(location))             {                 RootDevicesSoFar.Add(location);                 return location;             }             else             {                 return null;             }         }

and here SSDP Discovery:

using System;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace SSDPTest
{
    public class SSDPFinder
    {
        public void FindAsync(string whatToFind, int seconds, Action<string> FoundCallback)
        {
            const string multicastIP = "239.255.255.250";
            const int multicastPort = 1900;
            const int unicastPort = 1901;
            const int MaxResultSize = 8000;
 
            if (seconds < 1 || seconds > 4)
                throw new ArgumentOutOfRangeException();
 
            string find = "M-SEARCH * HTTP/1.1\r\n" +
               "HOST: 239.255.255.250:1900\r\n" +
               "MAN: \"ssdp:discover\"\r\n" +
               "MX: " + seconds.ToString() + "\r\n" +
               "ST: " + whatToFind + "\r\n" +
               "\r\n";
 
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            byte [] MulticastData = Encoding.UTF8.GetBytes(find);
            socket.SendBufferSize = MulticastData.Length;
            SocketAsyncEventArgs sendEvent = new SocketAsyncEventArgs();
            sendEvent.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(multicastIP), multicastPort);
            sendEvent.SetBuffer(MulticastData, 0, MulticastData.Length);
            sendEvent.Completed += new EventHandler<SocketAsyncEventArgs>((sender, e) =>
                {
                    if (e.SocketError!=SocketError.Success)
                    {
                        Debug.WriteLine("Socket error {0}", e.SocketError);
                    }
                    else
                    {
                        if (e.LastOperation == SocketAsyncOperation.SendTo)
                        {
                            // When the initial multicast is done, get rady to receive responses
                            e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, unicastPort);
                            socket.ReceiveBufferSize = MaxResultSize;
                            byte[] receiveBuffer = new byte[MaxResultSize];
                            e.SetBuffer(receiveBuffer, 0, MaxResultSize);
                            socket.ReceiveFromAsync(e);
                        }
                        else if (e.LastOperation == SocketAsyncOperation.ReceiveFrom)
                        {
                            // Got a response, so decode it
                            string result = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
                            if (result.StartsWith("HTTP/1.1 200 OK"))
                            {
                                Debug.WriteLine(result);
                                FoundCallback(result);
                            }
                            else
                            {
                                Debug.WriteLine("INVALID SEARCH RESPONSE");
                            }
 
                            // And kick off another read
                            socket.ReceiveFromAsync(e);
                        }
                    }
                });
 
            // Set a one-shot timer for double the Search time, to be sure we are done before we stop everything
            TimerCallback cb = new TimerCallback((state) =>
                {
                    socket.Close();
                });
            Timer timer = new Timer(cb, nullTimeSpan.FromSeconds(seconds*2), new TimeSpan(-1));
 
            // Kick off the initial Send
            socket.SendToAsync(sendEvent);
        }
    }
}
 
Comments (20)
  1. DustoMan says:

    You know who you should talk to?  There's a guy who for a while worked on a UPnP add-in for WHS 1.0.  The add-in would monitor your network for ports being opened and would tell you if your router when down.  He stop working on it when things started to transition to what would become WHS 2011.

    routercontrol.codeplex.com

  2. Andy-Pennell says:

    Thanks DustomMan but this appears to have nothing to do with the phone. UPnP on Windows is nice and easy as there is a good API available since Windows XP. On the phone there is no equivalent.

  3. bswapeax says:

    Any progress on this issue… Did you try your code on Windows 7 (C#)  and not on the phone ?

  4. Greg Menounos says:

    You're never setting keepsearching to false so your code is continuously allocating 4K of memory in a loop until you run out:

    while (keepsearching)

               {

                   try

                   {

                       byte[] buffer = new byte[4096];

                       MulticastSocket.BeginReceiveFromGroup(buffer, 0, buffer.Length, DoneReceiveFromGroup, buffer);

                   }

    }

    Also I think you should rewrite this loop so that you don't begin another async read until the previous one has completed.

  5. Andy-Pennell says:

    bswapeax: No, I never tried it there, never had the need.

    Greg: doh, that would explain why I run out of memory. The real version of this code has a timeout in that loop that clear keepsearching. Your theory on the async read is interesting, I'll try that.

  6. Brian Claridge says:

    Any updates on this?

  7. Andy-Pennell says:

    No updates from me anyway, I solved my problem in a different way. I was hoping someone else would figure out what I was doing wrong.

  8. Brian Claridge says:

    I don't think you did anything wrong at all. It's as if the phone is completely blocking receiving certain broadcasts. How did you end up solving your problem?

  9. Andy-Pennell says:

    For my scenario I know the port number and the uri of the XML on the device, so I do an ugly port scan to find matching devices. Its slow, and it crashes the [LKG29-vintage] emulator. It wont work for finding UPnP devices generally, but if you know the specifics of the device it is better than nothing.

  10. Andy-Pennell says:

    Hot news: SSDP IS POSSIBLE! The trick is to avoid using the UdpAnySourceMulticastClient class, and use a regular Socket, sending to the multicast address of 239.255.255.250. Hopefully I'll post a code update soon.

  11. Thanks for finding this out! I have SSDP discovery working fine too now, following your instructions. I certainly pulled my hair for a few weeks there.

  12. Jan Pieter says:

    Thanx for this post. Is there a part 2 coming soon?

  13. Andy-Pennell says:

    Jan: yes, Part 2 is Actions, which I am working on now. I am not using the first two code versions that I have already shipped in an app, third time is a charm: I am using the Async CTP to make the code much cleaner and easier. However it is taking me a while. Sometime in January with luck I should be posting something.

  14. Alberto Viceconti says:

    Hi Andy,

    I am trying to implement the SSDP discovery on a domotic bus installed at my house. I've tried your code and the message reaches the bus and the latter replies however no data is ever received by the phone.

    I made a desktop version of your code which could be reused entirely with a single call addition. Namely a Socket.Bind call. This made me think about the LocalEndpoint which you are not allowed to fiddle with on WP7. I have noticed infact that after the SendToAsync the socket has a m_LocalEndpoint wich is the phone local ip and port 0. Not to my surprise the desktop version only works if is bound on the local ip on the unicast port. Otherwise I see the exact same result that I get on the phone namely send OK but no receive.

    Do you have any pointers? Thanks in advance.

  15. Alberto Viceconti says:

    Hi Andy,

    I am trying to implement the SSDP discovery on a domotic bus installed at my house. I've tried your code and the message reaches the bus and the latter replies however no data is ever received by the phone.

  16. Andy-Pennell says:

    Alberto: sorry no idea on that

  17. HI Andy:

    I am implement the SSDP on WP7.5. I used the regular Socket send SEARCH * M message to the group and i Can get the repons. I also create a UdpAnySourceMulticastClient so that i can receive message from the group when device add or remove.

    In the UPnP-arch-DeviceArchitecture-v1.1, it said : the host receive the SEARCH message and send the NOTIFY message to the control with the unicast. I want to konw , how host send the message. Which prot it used.  

  18. Paul Sinnema says:

    Hi Andy,

    I've tried you're code and don't understand how it could have worked at all. The SSDPFinder in the Click routines is declared locally and is disposed at the end of the routine. The Async operation is never called. When I place the SSDPFinder outside the method I sometimes get result but not always. It looks as if the send of the M-SEARCH is not always successfull. In those cases the Timer closes the socket and an OperationAborted is reported.

    Regards

    Paul

  19. Andy-Pennell says:

    An updated version of this code, with a simple sample, is on Codeplex now at http://wpupnp.codeplex.com/

Comments are closed.

Skip to main content