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, null, TimeSpan.FromSeconds(seconds*2), new TimeSpan(-1));
// Kick off the initial Send
socket.SendToAsync(sendEvent);
}
}
}
Comments
Anonymous
August 24, 2011
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.comAnonymous
August 25, 2011
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.Anonymous
September 08, 2011
Any progress on this issue... Did you try your code on Windows 7 (C#) and not on the phone ?Anonymous
September 10, 2011
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.Anonymous
September 13, 2011
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.Anonymous
September 24, 2011
Any updates on this?Anonymous
September 27, 2011
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.Anonymous
September 29, 2011
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?Anonymous
September 30, 2011
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.Anonymous
December 11, 2011
The comment has been removedAnonymous
December 14, 2011
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.Anonymous
December 18, 2011
Thanx for this post. Is there a part 2 coming soon?Anonymous
December 22, 2011
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.Anonymous
January 07, 2012
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.Anonymous
January 07, 2012
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.Anonymous
January 08, 2012
Alberto: sorry no idea on thatAnonymous
January 08, 2012
Part 2 is now posted: blogs.msdn.com/.../upnp-on-windows-phone-7-5-part-ii-invoke.aspxAnonymous
February 26, 2012
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.Anonymous
March 06, 2012
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 PaulAnonymous
April 10, 2012
An updated version of this code, with a simple sample, is on Codeplex now at http://wpupnp.codeplex.com/