Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / WinMobile

Programming control point application using the UPnP Control Point API

4.75/5 (12 votes)
13 Jul 2009CPOL52 min read 151.6K   13.1K  
The article describes how to use the Microsoft's UPnP Control Point API for finding and controlling UPnP devices, and includes a description of simple library to facilitate the application of Control Point API in your own programs, together with an example of MFC and WinForms applications.
Note: This is updated version of article. In original version I presented code in which I used the ATL library for simplyfying COM programming. ATL is really useful library but I thought that many programmers could not use this library because of lack one in the version of Visual Studio which they use (Express Editions). Thus I created new version of UPnPCpLib library without help of ATL (what made COM programming more difficult, of course :). Instead, I used STL library. ATL version of my library (with the same functionality) is still available for download.
But this is not all. New library's version is more object oriented and easier for use, I hope :). Moreover, I created new Windows Forms "Finder" application with the help of new version (STL) of UPnPCpLib library and Visual C#. "Finder" for .net uses managed wrapper over native library - "FindManager.net". You can use this class library in applications written in any .net language like C# or Visual Basic.    

Download    

Contents  

Image 1

Introduction

The article describes how to use the Microsoft's UPnP Control Point API [1] for finding and controlling UPnP devices, available in Windows Me, CE .Net, XP and later. It also contains a description of a simple library of classes and functions aimed, at first, to make it easier to use Control Point API in your own applications, secondly, to help build a model of the structure of detected UPnP devices. At the end (for the most patient readers ;) is an example of simple MFC and WinForms applications designed to present the possibilities of Control Point API in conjunction with that library, including reading public IP address of Internet Gateway Devices and configure their "Port Forwarding" feature. It should be noted that the UPnP Control Point API can be used both, in scripts embedded in HTML pages and in applications written in C++ or Visual Basic languages. However, this article is intended for C++ developers. I assume that you mastered the basics of using COM interfaces and the STL library. When reading the article might be at hand the documentation of Control Point API [1].    

Before the idea came to write this article, I studied the problems of network programming using the Winsock library. Then it interested me the problem of programmatic read of router’s public IP address on home network. In this case I had been analyzing available solutions such as through SNMP protocol (Simple Network Management Protocol) [5] or using a public server using STUN protocol (Simple Traversal of UDP through NATs) [6], or a simple analysis of packets. My exploration, however, tended towards finding a solution to a simple and independent. I excluded SNMP because I found it difficult to develop, although it allowed for direct communication with the router. In contrast, the service of dedicated server, although easy to implement, it does not meet the condition for direct communication. Then I have interested in Universal Plug and Play technology (UPnP). It turned out that UPnP offers the possibility of direct communication with network device implemented this technology so that it is possible to download various useful information about the device and controlling it by using shared services. The question arises whether the UPnP technology fulfilled my expectations and proved to be a good solution to the problem? I must say that the answer is no :), but the technology itself interested me enough that I decided to learn it more closely and so was born the idea to write this article. Why did I not approve UPnP the perfect solution? First of all, support for UPnP technology, as well as SNMP protocol, in SOHO networking equipment is not an active feature by default. To use it you must properly configure the device. However, UPnP technology gradually gaining in popularity and more and more companies decide on its implementation in their devices. Increasingly, the UPnP features can be found in the network devices of SOHO class such as routers and access points. Secondly, it is necessary to prepare the operating system. Also in this case, the elements of the operating system responsible for handling UPnP technology require configuration by the user. It appears that the easy exercise of UPnP technology faces many obstacles. In the rest of this article I will describe how to deal with these problems. After this introduction, those of you who have not yet hated UPnP ;), I invite to continue reading.

UPnP Technology

UPnP (Universal Plug and Play) is a technology which, speaking briefly and vividly, transfer well-known from the PC the Plug and Play idea to devices capable of communicating through the network. UPnP allows for direct communication of different networked devices, such as intelligent appliances, wireless devices and PCs, for exchange of data and control devices. The process of connecting to the network devices that supports UPnP technology takes place automatically, without the need for manual configuration. The device self-configures, recognizes the environment in which work and then announce its presence and willingness to work, and then detects other devices present in the network. UPnP is independent of the physical medium, the operating system and programming language. UPnP architecture is based on existing standards such as XML and IP, TCP, UDP, HTTP, SSDP protocols. A more precise and comprehensive definition of UPnP technology and a description of its architecture can be found on the website of UPnP Forum organization [3], which deals with the development of UPnP technology, develop standards for this architecture and certification of equipment. As for the usefulness of this technology, in my view, it is interesting implementation of the digital home idea.

UPnP implementation and configuration in Windows XP

UPnP technology is supported by the following systems of Windows family: Millennium Edition, CE .Net, XP and newer (read Vista). Implementation in Windows XP [8] [9] (called UPnP framework) is based on two basic components: system services and COM libraries providing programming interfaces. System services related to UPnP are “Universal Plug and Play Device Host” (upnphost) and “SSDP Discovery Service” (ssdpsrv). As you can see, in the system are two different services related to UPnP. This is because Microsoft divided the functions related to hosting UPnP devices providing the services, and functions for the discovery and control UPnP devices and, as we'll see later, a similar division was introduced in the case of APIs.

The upnphost service allows, generally, run applications performing the role of UPnP devices giving them the necessary functionality compatible with UPnP architecture. So, if you wish to run the UPnP device application, you must first check whether the upnphost service is running. In contrast, the ssdpsrv service lets you find UPnP devices on all networks available in the system. This service through the SSDP protocol (Simple Service Discovery Protocol) [7] detects UPnP devices connected to the network and notifies the client when the device is connected or disconnected. To an application that helps you discover and control UPnP devices could properly operate the ssdpsrv service must be started.

In Windows XP, there are a few programming interfaces to allow the use of UPnP technology developers in their applications. These are the “Network Address Translation Traversal API”, "UPnP Device Host API" and “UPnP Control Point API”. All these interfaces are described in the MSDN [1] [2]. Purpose of the first two of these APIs I will explain briefly, because in this article I won't be describing them, instead I will focus on Control Point API. NAT Traversal API [2] allows applications running on computers in a LAN programmatic configuration of port mapping feature (port forwarding) in equipment of type Internet Gateway Device (IGD), that support network address translation (NAT) and UPnP technology. IGD may be a hardware router in a LAN or a PC with Windows XP and Internet Connection Sharing feature enabled. Excellent article written by Mike O'Neill about NAT Traversal API, titled "Using UPnP for Programmatic Port Forwardings and NAT Traversal" [4], can be found on CodeProject.com. Two other APIs: Device Host and Control Point, as in the case of system services, are two independent interfaces designed for programming in different fields. Device Host API provides support for the creation of applications hosted on Windows and acting as UPnP devices, which are exhibiting their services to other devices or applications of type control point. Device Host API helps the developer build the basic functionality of the device including shared services, and integrate them with the rest of the necessary functions (discover, control and events), which are provided by UPnP framework and system services: upnphost and ssdpsrv.

For painless cooperation UPnP application with Windows XP, beside system services, it is also necessary to properly set up firewall application [10]. SSDP Protocol (ssdpsrv service) uses UDP port 1900 for broadcast and TCP port 2869 for event notification. Thus, in order to ssdpsrv service, and hence the application of type control point, to be able to function properly, in the firewall you should unblock ports 1900 UDP and 2869 TCP. In the version of Service Pack 1 and earlier of Windows XP instead of TCP port 2869 was used TCP port 5000.

At this point it is worth mentioning the possibility of configuring the UPnP framework through the settings in system registry (Configuration Settings) [1]. Some of them concern the SSDP protocol parameters and the rest the overall functionality of UPnP framework. There are a few interesting options among them letting change, for example, the default value of TTL parameter of packets of SSDP protocol which is responsible for the maximum number of hops. Another interesting option is acceptable range of IP addresses during the devices discovery. For detailed description of configuration settings refer to the MSDN.

Now, richer in knowledge about the configuration of UPnP in Windows XP, we can move on to discuss the Control Point API.

Control Point API 

UPnP Control Point API is used to create applications capable of discovering and controlling UPnP devices. In what must be noted that the UPnP device can be dedicated hardware device (such as a router) or application running on a PC. Discovered devices can be connected to any physical network medium, to which has access a computer that is running the application. UPnP framework provides to the application all the necessary mechanisms of finding and controlling UPnP devices as well as mechanisms related to the notifying about events generated by the device and to connecting or disconnecting the devices on the network. The main element of UPnP framework that delivers these mechanisms is a system service ssdpsrv. With Control Point API it is possible to build applications that do not operate as an UPnP device but are performing exclusively tasks of so-called "control points", that is, an application discovering devices and communicating with them in order to use of services and registration of events. Of course, there is no obstacle to device application to enrich its functionality with functions of control point. Control Point interface can be used in applications written in C++ or Visual Basic for MS Windows. There is also the possibility of using this API in scripts written in VBScript scripting language, embedded in HTML files, so you can create applications for the web browser. This is perhaps the simplest and quickest way to create applications of type control point. But in this article I will focus on Control Point API, from the C++ programmer's point of view. Classes, to which the Control Point API refers, are stored in a COM library named upnp.dll. To seamlessly use this library is worth at the beginning install Platform SDK, where are needed header files and libraries. In application's source files must be included the “upnp.h” file.

Basic objects in API

Using the Control Point API we will have to deal with three basic objects closely related to UPnP device architecture:

  • Device Finder,
  • Device,
  • Service.

Device Finder object carries out the tasks related to the discover devices in the network and provide found devices objects to the application. The Device object represents an UPnP device's model, which directly relates to the standard of UPnP device architecture developed by the UPnP Forum organization. From the perspective of a programmer the Device object is a physical UPnP device, with which an application communicates to use its services. In an autonomous Device object (root device) can be embedded member Device objects (child device). The Service object is inseparable and an essential element of a Device object and its task is to provide services to clients (devices, control points). Using the services of the device relies on invoking by control point the so-called "actions" on Service object. One Device object may contain many Service objects.

To better understand the functions of the aforementioned objects and the purpose and functions of individual COM interfaces available in Control Point API let’s consider a model (object) of UPnP device, which presents schematically graph below.

Image 2

There is a picture of the overall scheme of the relationship between Finder, Device and Service objects and the very overall scheme of functioning of the application of type control point. Core functionality of this kind of application includes the implementation of the Finder object, which will provide Device objects to application, and interacting with Device objects. The illustration exposes Device object’s structure, which essential elements are: a collection of member devices – Devices, and collection of services - Services. It is necessary to note that each Device object from the Devices collection may contain their own member Device objects. This means that the structure of the Device object is a tree structure. "Black box" called Service we’ll see at the next picture, which brightens a little of its contents.

Image 3

Service object, which structure shows the picture, represents the service, which Device object offers. Each service has a logically coherent set of actions that are closely associated with the "state variables". Those variables, marked on the picture by symbols "Var1" and "Var2", represent the characteristics of the service. These characteristics (that is variables) accept certain values that clearly define the status of service and also device, to which the service belongs. If the value of the state variable has changed, then this means, that the state of the device also has changed, what will cause indicating this change through notification of the event. Event notifications subscribes and receives the control point application, using a callback object, monitoring in this way, the current state of the device. Knowing the current state, the application can adequately respond to the needs. The device’s service (that is Service object) offers a set of actions possible to comply by using the service. Actions are used by control point application to retrieve information about the condition of device and to command changes in its condition or perform certain tasks that is, speaking briefly, let control device. Control point application, affecting through actions on the state variables, causing some specific reactions of device, for example, reducing the intensity of the light source. Every action may be associated with one or more state variables. This may be so, that the action is not attributed to any variable. On above picture, we have an example where action “Action1” causes a change of the variable “Var1” and action “Action3” changes variable “Var2”, while action “Action2” causes reaction of both variables “Var1” and “Var2”. In fact, Control Point API allows direct reading of the state variable’s value of using the QueryStateVariable method, however, the UPnP Forum organization discourages the use of this function, and recommends the use of actions and event notifications.

Interfaces related to Device Finder object 

Control Point API is a set of several COM interfaces, which can be divided according to type of object with which they are associated. This selection concerns the three basic objects discussed earlier. The first group is the interfaces referring to the "Device Finder" object, which as the name indicates, deals with the discovery of devices, or to be more precise, implements the tasks associated with monitoring processes of connecting and disconnecting devices in the network.

  • IUPnPDeviceFinder
  • IUPnPDeviceFinderCallback

IUPnPDeviceFinder is one of the key interfaces of Control Point API and is used to create a Device Finder object. By means of it you can find the devices through synchronous or asynchronous method using the callback object. In many cases (such as GUI applications) asynchronous method will be a better choice because it does not block the user interface of the program. As a result of the discovery of devices by Device Finder object we get a Device object through the IUPnPDevice interface. There is yet another way to creation of a Device object, which I'll cover later, on the occasion of the presentation of the IUPnPDevice interface.

Remember that before start using the COM library and creation of any object you must initialize the library and after the completion of its work free resources. If you write application for multiple Windows platforms, should be paid attention to the way initialization of COM library. In this case, API’s documentation recommends single threaded apartment model to avoid problems that may arise in Windows Me, if we chose a multi threaded apartment model.

#define _WIN32_DCOM    // for CoInitializeEx
#include <objbase.h>

HRESULT hr;
hr = CoInitializeEx(0, COINIT_APARTMENTTHREADED);
if(hr == S_OK)
{
    // ... code using COM library
}
CoUninitialize();

In the case of console application or GUI application dedicated to run on Windows Me the more appropriate threading model for COM library will be single threading model rather than multi threading. To initialize COM library for use single threading model You should specify COINIT_APARTMENTTHREADED flag in CoInitializeEx function call. On the other hand, to use multi threading model of COM library it is neccessary to specify COINIT_MULTITHREADED flag.  

Let us return to the Device Finder object. After creating an object we can access it through the IUPnPDeviceFinder interface. By choosing the synchronous method we have available two functions belonging to this interface. With FindByType function you can search for a device of specific type - if you'll pass the specific name of a type (e.g. urn:schemas-upnp-org:device:BinaryLight:1), or any type if you'll pass the type common to all UPnP devices: "upnp:rootdevice". This function returns a collection of found devices in the form of IUPnPDevices interface. Second option is the FindByUDN function that gets as a parameter the device’s unique name UDN (Unique Device Name), which, unfortunately, you must know earlier, while returns the device object in the form of IUPnPDevice interface. The disadvantage of the synchronous method is long time waiting for results, by which program’s execution is suspended even for a few dozen seconds. I don’t include example of synchronous method, because now come to significantly more instructive asynchronous method, which will be illustrated by example.

Asynchronous finding consists of four main steps. To better understand the whole process of asynchronous search, I will use the picture, which, I hope, is worth a thousand words ;).

Image 4

The first step is to create a callback object, which will receive search results (items 1, 2, 3). In the next step we call CreateAsyncFind function passing to it pointer to callback object (item 5). The next step is to start the actual search using StartAsyncFind function (item 6). The last step is to return the search result from UPnP framework to callback object (item 7). Now I will explain in more detail the individual steps.

For using asynchronous method, you'll need a callback object. To create an instance of such object you must first prepare a class implementing IUPnPDeviceFinderCallback interface (item 1). At this point, you can implement in your own class, by oneself, all the functions of the IUnknown interface and its descendant IUPnPDeviceFinderCallback, but… ATL library brings us easier solution. If our callback class will be inherit from CComObjectRootEx class belonging to ATL, then we won't have to implement the functions of IUnknown interface, if in this way prepared class we derive from CComObject template class. Thanks to that, developer is exempt from the implementation of details common to the COM objects, relying on CComObject class in the reference counter management and object instance creation, and can focus on the details of implementation of IUPnPDeviceFinderCallback interface. Let's see an example of class, which I called DevFinderCallback (version without ATL).

// IUPnPDeviceFinderCallback implementation, without ATL

#include <upnp.h>

class DevFinderCallback : public IUPnPDeviceFinderCallback
{
    private:
    long _refcount; // object's reference counter
    
    public:
    DevFinderCallback() {_refcount = 0;}
    virtual ~DevFinderCallback() {}
    
    // IUnknown implementation
    virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject)
    {
        HRESULT result = ppvObject == 0 ? E_POINTER : S_OK;
        
        if(result == S_OK)
        {
            if(riid == IID_IUnknown || riid == IID_IUPnPDeviceFinderCallback)
            {
                *ppvObject = static_cast<IUPnPDeviceFinderCallback*>(this);
                this->AddRef();
            }
            else
            {
                *ppvObject = 0;
                result = E_NOINTERFACE;
            }
        }
        
        return result;
    }
    
    virtual unsigned long _stdcall AddRef()
    {
        return ::InterlockedIncrement(&_refcount);
    }
    
    virtual unsigned long _stdcall Release()
    {
        if(::InterlockedDecrement(&_refcount) == 0L)
        {
            delete this;
            return 0; // object deleted !!!
        }
        
        return _refcount;
    }
    
    // IUPnPDeviceFinderCallback implementation
    virtual HRESULT __stdcall DeviceAdded(long findid, IUPnPDevice* idev)
    {
        // process IUPnPDevice pointer when device was added
        return S_OK;
    }
    
    virtual HRESULT __stdcall DeviceRemoved(long findid, BSTR devudn)
    {
        // do something when device was removed
        return S_OK;
    }
    
    virtual HRESULT __stdcall SearchComplete(long findid)
    {
        // do something when search is complete
        return S_OK;
    }
};

IUnknown interface functions which should be implemented in your own callback class are:

  • QueryInterface,
  • AddRef,
  • Release.

These functions are called by COM library or user in case of creating and deleting object. In QueryInterface, at the request with specified proper interface identifier, pointer to the callback object is returned. AddRef and Release respectively increases or decreases object's reference counter. These operations thanks to the "Interlocked..." functions are thread safe. When counter is equal to zero, object can be deleted

IUPnPDeviceFinderCallback interface functions which should be implemented in your own callback class are:

  • DeviceAdded,
  • DeviceRemoved,
  • SearchComplete.

These functions are called in callback object by the UPnP framework at the moment, when searched device will be found or again connected to the network (DeviceAdded), when previously founded device will be disconnected from the network (DeviceRemoved) or when the search initiated by StartAsyncFind function call will be terminated (SearchComplete). The return value of these functions is irrelevant to the UPnP framework.

The next example shows how to create a callback object of DevFinderCallback class (item 2).

// Instantiate the callback object

DevFinderCallback* cback = new DevFinderCallback();

// after object's creating reference counter is initially equal to zero
// so, it is neccessary to increment it.
// be sure to release on finish,
// once created object should not be deleted directly (delete cback)
// instead use Release function.
cback->AddRef();

Thus, we have created a callback object (item 3), so now we can concern the creation of a Device Finder object. This object we create using COM library (item 4):

// Instantiate the device finder object

#include <upnp.h>

HRESULT hr;
IUPnPDeviceFinder* _ifinder = 0;

hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER,
                      IID_IUPnPDeviceFinder, (void**)&_ifinder);
if(hr == S_OK)
{
    // use finder object here
}

After creating of Device Finder (item 4) and callback (item 3) objects it is possible to start searching of devices. Before we begin the actual search, it is necessary, using CreateAsyncFind function (item 5) of IUPnPDeviceFinder interface, to specify the type of searched devices (as in the case of FindByType function), pass to UPnP framework a pointer to callback object, and pass an empty pointer of type long, which will be the session identifier. After return of CreateAsyncFind function we get the value of session identifier, which is needed to call another function of Device Finder object - StartAsyncFind. This function call begins the actual search (item 6). The StartAsyncFind function immediately returns value of type HRESULT indicating whether the call was faultlessly executed and the search request forwarded to UPnP framework. Since then, the callback object is awaiting results. Using once received session identifier we can repeat the call to StartAsyncFind function. If we create more search sessions using CreateAsyncFind (passing every time another session identifier), then the callback object can identify each session by using its identifier, which will be passed to callback object by the UPnP framework during returning search results. At any time after calling StartAsyncFind function we can interrupt the search by using the CancelAsyncFind function of Finder object passing as a parameter the identifier of session, we want to finish. Let's see an example:

// start asynchronous find for all root devices

#include <upnp.h>

HRESULT hr;
// Device Finder object interface
IUPnPDeviceFinder* _ifinder = 0;
// device type to find
std::wstring devtypestr(L"upnp:rootdevice");
BSTR devtype = SysAllocString(devtypestr.c_str());
// search identifier
long _findhandle;

hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER,
                      IID_IUPnPDeviceFinder, (void**)&_ifinder);
if(hr == S_OK)
{
    // prepare search
    // cback is DevFinderCallack pointer obtained earlier
    hr = _ifinder->CreateAsyncFind(devtype, 0, cback, &_findhandle);
    if(hr == S_OK)
    {
        // start search
        hr = _ifinder->StartAsyncFind(_findhandle);
        if(hr == S_OK)
        {
            // do something if success or simply return
        }
    }
}

SysFreeString(devtype);

If in the application we’ll create once the Finder object and will start the search, then the UPnP framework will be for our application, until of its completion, monitoring network and relaying the results to the callback object. When a specific type of device will be found or will be connected to the network, then UPnP framework will call DeviceAdded function of callback object passing IUPnPDevice interface of this device’s object (item 7). The SearchComplete function of callback object is called by the framework after the completion of the initial phase of search initiated by a Finder object by StartAsyncFind function. The duration of this phase can amount to a few minutes. The SearchComplete function’s call doesn’t mean the end of the process of discovering devices. We can finish this process calling Finder’s CancelAsyncFind function.

When the application is being closed and we were used the asynchronous method of search, be sure to destroy COM objects, which were created, that is Device Finder and callback. Don't directly delete callback object, instead use Release function.

// cancelling search and destroing objects

// release DevFinderCallback object
if(cback != 0)
    cback->Release();

// cancel search and release Device Finder object
if(_ifinder != 0)
{
    _ifinder->CancelAsyncFind(_finderhandle);
    _ifinder->Release();
}

Interfaces related to Device object

The next group is related to Device object:

  • IUPnPDevice,
  • IUPnPDevices,
  • IUPnPDeviceDocumentAccess,
  • IUPnPDescriptionDocument,
  • IUPnPDescriptionDocumentCallback.

The most important in this group is IUPnPDevice interface through which you can gain access to the Device object. It is not possible to direct create this interface, because it is provided by the UPnP framework as a result of searching devices available on the network. Through received pointer to the interface of device object, by using this interface’s functions, can to read a lot of information about the device (properties) such as its unique name, model name, the name of the manufacturer or URL, which will open in a browser an access to the device configuration options.

Furthermore, the IUPnPDevice interface’s functions allow the examination of the device’s structure that is its internal logic structure:

  • get_HasChildren,
  • get_Children,
  • get_Services.

With get_HasChildren function we can find out whether the device contains member devices (child devices) at all. If the get_HasChildren function returns logical value "true", then you can get a collection of member devices calling get_Children. A collection of member devices is returned in the form of IUPnPDevices interface. Please note, that the logical structure of a device is tree, therefore, every member device may include a collection of its own member devices. Most conveniently is to deal with the identification of the tree structure through recursion, i.e. call get_Children function in the auxiliary recurrent function. The get_Services function returns us a collection of device’s services - Service objects, in the form of IUPnPServices interface. More about interfaces associated with the Service object will be later in this article.

Let’s see in example the process of identifying device’s structure.

// examination of structure of root device

#include <upnp.h>

// _idevice is pointer to root device

void EnumerateDevices(IUPnPDevice* _idevice)
{
    IUPnPDevices* children = 0; // collection of member devices
    VARIANT_BOOL bcheck = 0;
    
    // first check if device has children
    if(_idevice->get_HasChildren(&bcheck) == S_OK && bcheck != 0)
    {
        // if device has children then get collection interface
        if(_idevice->get_Children(&children) == S_OK)
        {
            // device has children and collection's pointer is valid
            HRESULT hr = S_OK;
            IUnknown* ienum = 0; // for obtain the enumerator
            long devscount = 0;        // count of member devices

            // get count of member devices
            children->get_Count(&devscount);
            
            // enumerate and add devices
            // get helper interface
            if(children->get__NewEnum(&ienum) == S_OK)
            {
                IEnumUnknown* icol = 0; // collection enumerator
                hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
                
                if(hr == S_OK)
                {
                    IUnknown* iitem = 0; // collection’s item
                    IUPnPDevice* ichild = 0; // member device

                    // enumerate collection
                    icol->Reset();
                    while(icol->Next(1, &iitem, 0) == S_OK)
                    {
                        // get member device interface
                        hr = iitem->QueryInterface(IID_IUPnPDevice, (void**)&ichild);
                        
                        if(hr == S_OK)
                        {
                            // here we can do something with device's pointer

                            // continue recursive
                            EnumerateDevices(ichild);

                            ichild->Release();
                            ichild = 0;
                        }
                        
                        iitem->Release();
                        iitem = 0;
                    }
                    
                    icol->Release();
                }
                
                ienum->Release();
            }
            
            children->Release();
        }
    }
}

Description document

One of the most important and fundamental elements of the UPnP device architecture is the "description document". This is simply an XML document (hereafter I will be using the concept of "document"), where are written all the information about the device and its structure. The document is required element of the device and its format and structure is specified by standard of UPnP Forum organization [3]. Document in the form of an XML file is prepared by the manufacturer of the device and placed in the device.

The information about the device stored in a document is the specific properties of the device, the same that can be read using the IUPnPDevice interface. The most essential thing in the document is that it contains a complete description of the whole logical structure of the device that is a description of the member devices tree together with their properties and collection of services. An important conclusion results from it: if the document has been downloaded, then analyzing it, you can read all the information about the device without the necessity of the access to Device objects and using the interfaces.

Let's see how to obtain access to the description document. Helpful interface in this task is IUPnPDeviceDocumentAccess. It has only one function GetDocumentURL which, as you may guess, returns the URL of the document. We will get the pointer to IUPnPDeviceDocumentAccess through the IUPnPDevice interface.

// retrieving of description document URL

#include <upnp.h>

// idev is pointer to root device

void GetDeviceDocumentAccessURL(IUPnPDevice* idev)
{
    HRESULT hr;
    IUPnPDeviceDocumentAccess* idoc = 0;
    
    // URL will be stored into this variable
    BSTR btmp = 0;
    
    // query for IUPnPDeviceDocumentAccess
    hr = idev->QueryInterface(IID_IUPnPDeviceDocumentAccess, (void**)&idoc);
    
    if(hr == S_OK)
    {
        // get URL and write to BSTR
        idoc->GetDocumentURL(&btmp);

        // do something with retrieved URL

        // on finish release resources
        SysFreeString(btmp);
        
        idoc->Release();
    }
}

I mentioned above that there is another way to obtain a Device object, other than that through the Device Finder object. It relies on the use of the IUPnPDescriptionDocument interface. However, it makes sense only when we know in advance the description document’s URL of device, which the object we are interested in. To obtain a pointer to IUPnPDescriptionDocument you need to create a new object through the COM library and CoCreateInstance function. The IUPnPDescriptionDocument interface has functions to load the document into memory, in synchronous or asynchronous manner, using a callback object (class implementing IUPnPDescriptionDocumentCallback). Only after loading the document into memory, you can call functions returning a pointer to IUPnPDevice. It is a pity that although the apparent document loading into memory by this method, IUPnPDescriptionDocument interface does not offer function allowing direct access to the content of the document.

Interfaces related to Service object

Last group of interfaces belonging to Control Point API is related to Service object.

  • IUPnPService,
  • IUPnPServices,
  • IUPnPServiceCallback.

Access to the Service object can be obtained only through a Device object. The primary interface of Service object is IUPnPService. To obtain a collection of services - Service objects of given device, you should call the get_Services function of IUPnPDevice interface. This function returns the services collection object, to which we have access through an IUPnPServices interface.

// retrieving of service objects

#include <upnp.h>

// idev is pointer to root device

void EnumerateServices(IUPnPDevice* idev)
{
    HRESULT hr = S_OK;
    long srvcount = 0;             // count of services
    IUnknown* ienum = 0;           // for obtain the enumerator
    IUPnPServices* isrvs = 0;      // the collection of services
    
    // get collection interface
    if(idev->get_Services(&isrvs) != S_OK)
        return;
    
    // get number of services
    isrvs->get_Count(&srvcount);
    
    // get helper interface
    if(isrvs->get__NewEnum(&ienum) == S_OK)
    {
        IEnumUnknown *icol = 0; // the enumerator of collection
        
        hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
        if(hr == S_OK)
        {
            IUnknown* iitem = 0;       // collection’s item
            IUPnPService* isrv = 0;    // service object
            BSTR btmp = 0;
            
            // enumerate collection
            icol->Reset();
            while(icol->Next(1, &iitem, 0) == S_OK)
            {
                // get interface to service object
                hr = iitem->QueryInterface(IID_IUPnPService, (void**)&isrv);
                
                if(hr == S_OK)
                {
                    // do something with service object
                    // for example get service id
                    isrv->get_Id(&btmp);

                    SysFreeString(btmp);
                    isrv->Release();
                }
                iitem->Release();
            }
            icol->Release();
        }
        ienum->Release();
    }
    isrvs->Release();
}

The IUPnPService interface has two very interesting functions that allow for interaction with the device (controlling device), that is the use of the services and read values of the state variables. The first primary function is InvokeAction, which I will spare more space, and the second is QueryStateVariable through which you can simply read the current value of the state variable.

Using InvokeAction function is not too easy, so, at the beginning, I beg to quote the function’s declaration:

HRESULT InvokeAction
(
    BSTR        bstrActionName,     // [in] name of action to invoke
    VARIANT     varInActionArgs,    // [in] array of input arguments of action
    VARIANT*    pvarOutActionArgs,  // [out] array of main output values of action
    VARIANT*    pvarRetVal          // [out] one action’s output value
);

You can see that the function requires four arguments. Their detailed definitions can be found in the API’s documentation on MSDN [1]. But I would like to draw attention to the fact that calling this function, usually we passing on the name of action (bstrActionName - required argument) and an array of input arguments of action (varInActionArgs - required argument), while the left two arguments point to empty variables of type VARIANT. Of course, this is not only the format but the most popular.

The main problem with the InvokeAction function is that before calling it you need to know not only the name of action to invoke but also the number and types of its arguments. Where from to take them? The answer is: from the description document of a Service object! And here another problem arises because none of the Control Point API interfaces does not have function that allows us to read the content of the document directly. You have to cope with it alone. Knowing the document’s URL you can individually, using the HTTP protocol, download from device the document file and then parse it.

Each service, like the device itself, is described in the XML file - description document, dedicated to given service. The structure of this document must also be compatible with the standard of UPnP Forum organization [3]. Document of service includes data of actions, their names and arguments and details about state variables associated with actions. Structure of the service’s document consists of two lists: actions and state variables. For each action is specified its name and list of arguments. If the action has arguments, then are given their names, direction of passing (input / output) and the name of related state variable. For the state variables are defined such data as: name, type, allowed values, minimum and maximum value, step and the default value.

The service's document file can be downloaded from the device. But where to find the URLs of these documents? The URL is saved in the relative form (usually but not necessarily), in a device’s document, in part related to given service, in tag called <SCPDURL>. The base part of URL, in accordance with the standard, should be placed in the document of device, in tag called <URLBase>.

Thus, the scenario of getting data about actions may appear as follows:

  • read the URL of device’s document in a way given above in description of IUPnPDeviceDocumentAccess interface,
  • download the device’s description document file,
  • examining the content of device’s document read the URL of the service’s document from SCPDURL tag (if the URL is relative then append it to the base URL read out from the URLBase tag),
  • download the service’s description document file,
  • examining the content of service’s document read the data of the action it interested to us.

How it results from the scenario, we meet the need for parsing XML files. Fortunately, there are many excellent and free XML parsers for C++.

In example, I show you how to, with the help of HTTP protocol, using only the Winsock library, download the document file from the device.

// downloading description document
// sizes of buffers and URL are exemplary

#pragma comment(lib, "ws2_32") // required linked library
#include <winsock2.h>
#include <iostream>

using namespace std;

// initialize Winsock library
WSADATA wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);

// path of document from its URL
string path("/document.xml");

// prepare inet address from URL of document
sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // put here ip address of device
addr.sin_family = AF_INET;
addr.sin_port = htons(56616); // put here port for connection

const int rbsize = 4096;    // internal buffer size
char rbuff[rbsize] = {0};    // internal temporary receive buffer
int rbshift = 0;            // index in internal buffer of current begin of free space
int b = 0;                    // bytes curently received
int tb = 0;                    // bytes totally received
string respbuff;            // response buffer
string headertail("\r\n\r\n"); // request tail
string document;            // document content


// prepare string of HTTP GET command
ostringstream os;
os << "GET " << path << " HTTP/1.1\r\nHost: " << inet_ntoa(addr.sin_addr)
<< ':' << ntohs(addr.sin_port)
<< headertail;

// create TCP socket
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// connect socket
connect(s, (sockaddr*)&_addr, sizeof(sockaddr_in));

// send request
b = send(s, os.str().c_str(), os.str().length(), 0);

// receive response data - document
while((b = recv(s, rbuff + rbshift, rbsize - rbshift, 0)) != SOCKET_ERROR)
{
    // finish loop if connection has been gracefully closed
    if(b == 0)
    break;
    
    // sum of all received chars
    tb += b;
    // sum of currently received chars
    rbshift += b;
    
    // temporary buffer has been filled
    // thus copy data to response buffer
    if(rbshift == rbsize)
    {
        respbuff.append(rbuff, rbshift);
        
        // reset current counter
        rbshift = 0;
    }
}

// close connection gracefully
shutdown(s, SD_SEND);
while(int bc = recv(s, rbuff, rbsize, 0))
if(bc == SOCKET_ERROR) break;
closesocket(s);

// analyse received data
if(tb > 0)
{
    // copy any remaining data to response buffer
    if(rbshift > 0)
    respbuff.append(rbuff, rbshift);
    
    // check response code in header
    if(respbuff.substr(0, respbuff.find("\r\n")).find("200 OK") != string::npos)
    {
        int cntlen_comp = 0;    // computed response's content length
        int cntlen_get = 0;        // retrieved response's content length
        string::size_type pos;
        string::size_type posdata = respbuff.find(headertail);
        
        if(posdata != string::npos)
        {
            // compute content length
            cntlen_comp = tb - (posdata + headertail.length());
            
            if(cntlen_comp > 0)
            {
                if(b == 0)
                {
                    // connection has been gracefully closed
                    // thus received data should be valid
                    cntlen_get = cntlen_comp;
                }
                else
                {
                    // get content length from http header
                    // to check if number of received data is equal to number of sent data
                    string header = respbuff.substr(0, posdata);
                    transform(header.begin(), header.end(), header.begin(), tolower);
                    if((pos = header.find("content-length:")) != string::npos)
                    istringstream(header.substr(pos, header.find("\r\n", pos)).substr(15)) >> cntlen_get;
                }
                
                // if number of received bytes is valid then save document content
                if(cntlen_comp == cntlen_get)
                document.assign(respbuff.begin() + posdata + headertail.length(), respbuff.end());
            }
        }
    }
}

// cleanup Winsock library
WSACleanup();

Now, we know how to obtain data of the action, which we want to invoke. Now we can go back to the InvokeAction function. Before the call, on the basis of acquired data of the action, you need to prepare arguments to pass in. Passing the name of action is trivial. The varInActionArgs argument is type of VARIANT and contains an array of type SAFEARRAY which elements are input arguments of action. If any action not require passing input arguments, then it is sufficient to pass an empty variable of type VARIANT. Array of type SAFEARRAY stores individual elements (action’s arguments) as variables of type VARIANT. The pvarOutActionArgs argument points to an empty variable of type VARIANT, while after function’s return it contains an array of type SAFEARRAY, whose elements (of type VARIANT) are the output values of action. The number and the types of these elements are consistent with data of the action placed in service’s description document. The last argument pvarRetVal also points to an empty variable of type VARIANT, while at the end of the call it contains a single value of the return of action.

While using the InvokeAction function we will have to deal with variables of type VARIANT and SAFEARRAY. Using these types is not very convenient because their variables require initialization and releasing of resources. Here comes again with the help the ATL library. Thanks to ATL types, above all, developer does not need to remember about the releasing of resources and initialization is done automatically. For the VARIANT type we have adequate wrapper called CComVariant and for the SAFEARRAY corresponding type CComSafeArray. ATL's CComBSTR type wraps BSTR type. Application of ATL types considerably simplifies the code and makes it clearer. But from the reason about which I wrote in the note at the beginning of article I will present the code without ATL types.

In the following example I will show you how to use the InvokeAction function to execute sample action. Suppose that the device I want to control is a router (IGD) on a home network, and its service I want to use is the service of type urn:schemas-upnp-org:service:WANPPPConnection:1. This service provides, inter alia, an action called GetSpecificPortMappingEntry, which is used to retrieve a single, specific entry from the list of port mappings of the router. This action has three input arguments and returns five values containing data of list entry. Action’s data read from the document of service presents the table below:

Name of state variable
related to argument
Passing direction
of argument
Argument type Allowed values
RemoteHost in string may be empty string
ExternalPort in ui2
PortMappingProtocol in string TCP, UDP
InternalPort out ui2
InternalClient out string
PortMappingEnabled out boolean
PortMappingDescription out string
PortMappingLeaseDuration out ui4 0 means permanent

The order of input arguments of action read from the service’s document is important. In the same order they should be placed in the array passed in the varInActionArgs variable to InvokeAction function. A little more about the types of arguments passed to the action later in this article.

I assume that earlier I received from the Device Finder object the Device object in the form of a pointer to IUPnPDevice interface and next with its help I obtained from the Device object the pointer to IUPnPService interface of service interesting to me.

// Invoking service’s action

// _iservice is pointer to service’s interface (IUPnPService*)

#include <upnp.h>

// helper array for input arguments
// first argument's type in pair - type of argument
// second argument's type in pair - value of argument
vector< pair<unsigned short, wstring> > _in;

typedef vector< pair<unsigned short, wstring> >::const_iterator ArgsIterator;

// helper array for output arguments
vector< pair<unsigned short, wstring> > _out;

// first argument of type string (VT_BSTR) – remote host
_in.push_back(pair<unsigned short, wstring>(VT_BSTR, L""));

// second argument of type ui2 (VT_UI2) – external port
_in.push_back(pair<unsigned short, wstring>(VT_UI2, L"8839"));

// third argument of type string (VT_BSTR) – port mapping protocol
_in.push_back(pair<unsigned short, wstring>(VT_BSTR, L"TCP"));

// action name
BSTR actionName = SysAllocString(L"GetSpecificPortMappingEntry");

HRESULT hr = S_OK;
long inargscount = 0;            // number of input arguments

VARIANT    inargs;                    // Invoke argument in
VARIANT outargs;                // Invoke argument out
VARIANT retval;                    // Invoke argument ret
VARIANT inval;                    // temp input value
SAFEARRAY* arr_inargs = 0;        // array for input arguments
VARIANT outval;                    // temp output value
SAFEARRAY* arr_outargs = 0;        // array for output arguments

long arr_index[1];                // current index of safe array


// get number of arguments
inargscount = _in.size();

// initialize variants
VariantInit(&inargs);
VariantInit(&outargs);
VariantInit(&retval);

// create input array
SAFEARRAYBOUND bounds[1];
bounds[0].lLbound = 0;
bounds[0].cElements = inargscount;
arr_inargs = SafeArrayCreate(VT_VARIANT, 1, bounds);

if(arr_inargs != 0)
{
    // fill input safe array
    ArgsIterator argi = _in.begin();
    for(long i = 0; i < inargscount; ++i, ++argi)
    {
        arr_index[0] = i;
        
        VariantInit(&inval);
        inval.vt = VT_BSTR;
        V_BSTR(&inval) = SysAllocString((*argi).second.c_str()); // value of arg
        
        if(V_BSTR(&inval) != 0)
        {
            hr = S_OK;
            
            VARTYPE vt = (*argi).first; // type of arg
            if(vt != VT_BSTR)
            // change type if other than VT_BSTR
            hr = VariantChangeType(&inval, &inval, VARIANT_NOUSEROVERRIDE, vt);
            
            if(hr == S_OK)
            SafeArrayPutElement(arr_inargs, arr_index, (void*)&inval);
        }
        
        VariantClear(&inval);
    }
    
    // assign input array to input argument
    inargs.vt = VT_ARRAY | VT_VARIANT;
    V_ARRAY(&inargs) = arr_inargs;
    
    // invoke action
    hr = _iservice->InvokeAction(actionName, inargs, &outargs, &retval);
    
    if(hr == S_OK) // invoke succeeded
    {
        // data from outargs - output arguments
        arr_outargs = V_ARRAY(&outargs);
        
        long arrsize = 0;
        VARTYPE vartype;
        
        // get size of array of output arguments
        if(SafeArrayGetUBound(arr_outargs, 1, &arrsize) == S_OK)
        {
            // read arguments from array
            for(long i = 0; i <= arrsize; ++i)
            {
                arr_index[0] = i;
                
                VariantInit(&outval);
                
                // get current argument from array
                hr = SafeArrayGetElement(arr_outargs, arr_index, (void*)&outval);
                if(hr == S_OK)
                {
                    vartype = outval.vt;
                    
                    // change type to VT_BSTR (string)
                    if(outval.vt != VT_BSTR)
                    hr = VariantChangeType(&outval, &outval, VARIANT_ALPHABOOL, VT_BSTR);
                    
                    // put current argument to helper utput array
                    if(hr == S_OK)
                    _out.push_back(pair<unsigned short, wstring>(vartype, V_BSTR(&outval)));
                }
                
                VariantClear(&outval);
            }
        }
        
        // data from retval
        if(retval.vt != VT_EMPTY)
        {
            hr = S_OK;
            vartype = retval.vt;
            
            if(retval.vt != VT_BSTR)
            hr = VariantChangeType(&retval, &retval, VARIANT_ALPHABOOL, VT_BSTR);
            
            if(hr == S_OK)
            _out.push_back(pair<unsigned short, wstring>(vartype, V_BSTR(&retval)));
        }
    }
    else // invoke failed
    {
        // do something if error occured
    }
    
    // destroy input array
    SafeArrayDestroy(arr_inargs);
    // array has been destroyed thus set related variant to empty
    // to avoid destroing array again during clear of variant
    inargs.vt = VT_EMPTY;
}

// clear variants
VariantClear(&inargs);
VariantClear(&outargs);
VariantClear(&retval);

SysFreeString(actionName);

Reading information about any action from the service’s document, may be noticed that notation of the names of types of action’s arguments differs from notation that is used in C++ code operating on type VARIANT and using Control Point API. For example, instead of notation "VT_BSTR" we meet notation "string". The reason for this is the fact that the standards of UPnP Forum are independent of the programming language and in the case of types are based on XML standard. In such situations is useful a "crib", which can prompts what type we face. In the following table I completed the list of names of XML types used in the service’s document and their equivalents used with type VARIANT in Control Point API. It is a slightly extended (more convenient) version of the table, which can be found in the documentation on MSDN [1].

XML Data Type

IDL Data Type

Type of VARIANT

bin.base64

SAFEARRAY

VT_ARRAY | VT_UI1

bin.hex

SAFEARRAY

VT_ARRAY | VT_UI1

boolean

VARIANT_BOOL

VT_BOOL

char

wchar_t

VT_UI2

date

DATE

VT_DATE

dateTime

DATE

VT_DATE

dateTime.tz

DATE

VT_DATE

fixed.14.4

CY

VT_CY

float

float

VT_R4

i1

char

VT_I1

i2

short

VT_I2

i4

long

VT_I4

int

long

VT_I4

number

BSTR

VT_BSTR

r4

float

VT_R4

r8

double

VT_R8

string

BSTR

VT_BSTR

time

DATE

VT_DATE

time.tz

DATE

VT_DATE

ui1

unsigned char

VT_UI1

ui2

unsigned short

VT_UI2

ui4

unsigned long

VT_UI4

uri

BSTR

VT_BSTR

uuid

BSTR

VT_BSTR

At the end of considerations about the InvokeAction function it’s worth to draw attention to the error codes returned by this function in the form of HRESULT, because they may be helpful in determining the cause of error if the function call fails. The error codes can be found in the upnp.h file or in the documentation [1]. 

I mentioned earlier one more interesting function of IUPnPService interface: QueryStateVariable. It may be useful in certain cases, when is needed, at the moment, the value of the state variable, which is normally available only after the relevant event has been generated. But in general, such cases should not be much, so more effective way to read the values of the state variables is the receiving of events notifications generated at the moment of the change of variable’s value.

To receive notifications of events generated by the services is used the callback object, which should be created from class implementing IUPnPServiceCallback interface. It has two functions, of which the most interesting is function StateVariableChanged, called by the UPnP framework at the moment of change the service’s state (change of the value of the state variable). The second function is ServiceInstanceDied, which is used to notify the control point application of the unavailability of service. UPnP framework calls StateVariableChanged function to notify the application about change the value of the state variable, passing a pointer to the related service object (IUPnPService*) as well as the name and current value of the changed variable. A class of object receiving events notifications we create like a class of callback object for Device Finder.

// IUPnPServiceCallback implementation

#include <upnp.h>      // IUPnPServiceCallback

// IUPnPServiceCallback implementation
class SrvEventCallback : public IUPnPServiceCallback
{
    private:
    long _refcount; // object's reference counter
    
    public:
    SrvEventCallback() : _refcount(0) {}
    virtual ~SrvEventCallback() {}
    
    // IUnknown implementation
    virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject)
    {
        HRESULT result = ppvObject == 0 ? E_POINTER : S_OK;
        
        if(result == S_OK)
        {
            if(riid == IID_IUnknown || riid == IID_IUPnPServiceCallback)
            {
                *ppvObject = static_cast<IUPnPServiceCallback*>(this);
                this->AddRef();
            }
            else
            {
                *ppvObject = 0;
                result = E_NOINTERFACE;
            }
        }
        
        return result;
    }
    
    virtual unsigned long _stdcall AddRef()
    {
        return ::InterlockedIncrement(&_refcount);
    }
    
    virtual unsigned long _stdcall Release()
    {
        if(::InterlockedDecrement(&_refcount) == 0L)
        {
            delete this;
            return 0; // object deleted !!!
        }
        
        return _refcount;
    }
    
    // IUPnPServiceCallback implementation
    virtual HRESULT _stdcall StateVariableChanged(IUPnPService* isrv, LPCWSTR varname, VARIANT varvalue)
    {
        // process current value of state variable
        return S_OK;
    }
    
    virtual HRESULT _stdcall ServiceInstanceDied(IUPnPService* isrv)
    {
        // do something when service was died
        return S_OK;
    }
};

The return value of the IUPnPServiceCallback interface functions should be equal to zero (S_OK).

Before the callback object will be able to receive events notifications it is necessary to register this object in the service, i.e. pass to its (service) object the pointer to a callback object using AddCallback function of IUPnPService interface.

// Instantiate and registering the callback object for receiving events notifications

// isrv is pointer to service’s interface (IUPnPService*)

SrvEventCallback* cback = new SrvEventCallback();
if(cback != 0)
{
    // reference counter is 0 thus increment
    // be sure to release on finish
    cback->AddRef();
    
    // register callback with service object
    isrv->AddCallback(cback);
}

As you can see, analyzing the above example and IUPnPService interface functions, it is not obvious from which device (if we control more than one and we created only one callback object) comes received event notification. This doubt arises from the fact that in the function IUPnPServiceCallback::StateVariableChanged we get (from UPnP framework) only a pointer to the interface of service object. In contrast, the service object does not have a function, which returns a pointer to the object of hosting device. Therefore, we do not have the possibility of direct reference to a device object from its service level. In the next part of this article I will propose solution to the problem of callback objects (IUPnPServiceCallback) management for a number of controlled devices.

UPnPCpLib framework 

For ease of use Control Point API I created a simple framework, which, I hope :), simplifies the creation of control point application. It contains a few simple classes that define the most important objects of UPnP architecture as well as set of utility functions, interfaces and structures that are helpful at accomplishment of the basic tasks of control point application. All of the examples presented so far come from the source files of framework, and all issues that were raised so far are applicable in functions of this library.

The main idea of this framework is to create a model of UPnP device and to provide the tools to handle this model. Framework can be used to build console or GUI applications as well as the native win32/MFC or managed WinForms applications. The main native library consists of a header file upnpcplib.h and source file upnpcplib.cpp. The code is commented, what should ease of use the library. For parsing XML files I used CMarkup software by First Objective Software, Inc. under the conditions of "CMarkup Evaluation License Agreement". To compile the library, you must add to the application project the markup.h and markup.cpp files. Library uses STL strings in CMarkup software, so You should specify preprocessor definition MARKUP_STL in project's properties. I beg to note that UPnPCpLib framework is not a commercial product and can be used in your own applications on rules of CPOL license, under the condition of keep terms of "CMarkup Evaluation License Agreement".

I will present here shortly the key elements of library, but I encourage interested to review the source code. After a brief description I will present examples. In order to supplement the information contained in the following description, please familiarize with the comments and the implementation details in the library's source files. 

UPnPCpLib library contains four main classes defining the four most important objects of UPnP architecture.

  • FindManager - refers to the Device Finder object.
  • Device - corresponds to UPnP device.
  • Service - corresponds to service object of UPnP device.
  • Action - corresponds to service's action

Besides, contains two helper classes:

  • DevFinderCallback - implements IUPnPDeviceFinderCallback interface.
  • SrvEventCallback - implements IUPnPServiceCallback interface.

Moreover, it contains structure DocAccessData which helps parsing of description documents.

Below they are the two general schemes of relationships between library’s objects. First scheme presents centralized model which key feature is that the collection of devices is managed by FindManager object.

Image 5

Second scheme presents diffused model which key feature is that the collection of devices is managed outside of FindManager by object of class implementing IFinderCallbackClient interface.

Image 6

FindManager class 

The FindManager is the primary class and is used to manage the Device Finder object. Automatically creates and destroys the Finder object and controls the process of discovering. It's the only class which objects You can create by oneself. The rest of objects are created by framework automatically and can be accessed via other objects methods.

// ============== FindManager class ============== //


// class for manage IUPnPFinder* object and devices collection.
// To manage external device's collection outside this class
// create your own class implementing IFinderCallbackClient interface
// then call FindManager::Init passing pointer to your own object.
// Else, pass pointer to client of type IFinderManagerClient
// to manage collection internally and notify client about events.

class FindManager : public IFinderCallbackClient, public IProcessDevice
{
    public:
    FindManager();
    virtual ~FindManager();
    
    // devices collection will be managed internaly.
    // pass interface of client which will be notified about events related to collection
    bool Init(IFinderManagerClient* client, const wstring& devicetype = L"upnp:rootdevice");
    // external collection.
    // pass pointer to your own class implementing IFinderCallbackClient.
    bool Init(IFinderCallbackClient* client, const wstring& devicetype = L"upnp:rootdevice");
    
    // starts new search
    bool Start();
    // stops current search
    bool Stop();
    
    // access to devices collection
    // if collection is managed externally then throws exception
    const DeviceArray* GetCollection() const;
    DeviceArrayIterator GetCollectionBegin() const;
    DeviceArrayIterator GetCollectionEnd() const;
    const Device* GetDevice(unsigned int index) const;
    const Device* operator[] (unsigned int index) const;
    int GetCollectionCount() const;
    bool IsCollectionEmpty() const;
    
    // current search identifier
    long GetFindId();
    
    // when devices collection is managed externally in your own class
    // then you should manually add callback to services events using Service::SetCallbackClient.
    // this function is used only if FindManager manages devices collection.
    // adds callback to services and notify given client about events related to service.
    // if client will be set to null pointer then callback won't be added to the collection
    // and client won't be notified about events.
    void SetServiceEventClientPtr(IServiceCallbackClient* client);
    
    // IFinderCallbackClient implementation
    // calls EnterCriticalSection and LeaveCriticalSection
    void Lock();
    void UnLock();
    
    // standard UPnP devices types
    static const wchar_t* device_type[];
    static wstring GetRootDeviceType();
    static int TypesCount();
    
    private:
    bool Init(const wstring& devicetype);
    
    // releases finder callback
    void ReleaseCallback();
    
    // IProcessDevice implementation
    // for assign callback's client to each service object in devices tree
    virtual void ProcessDevice(const Device* dev, void* param, int procid);
    
    // IFinderCallbackClient implementation
    virtual void DeviceAdded(long findid, IUPnPDevice* idev);
    virtual void DeviceRemoved(long findid, const wstring& devname);
    virtual void SearchComplete(long findid);
    
    void RemoveAllDevices();
    
    private:
    DevFinderCallback*            _findercallback;        // IUPnPDeviceFinderCallback implementation
    IUPnPDeviceFinder*            _ifinder;                // IUPnPDeviceFinder interface
    DeviceArray                    _devs;                    // device's collection
    long                        _finderhandle;            // IUPnPDeviceFinder find handle
    IFinderManagerClient*        _findermanagerclient;    // pointer to client receiving events related to changes
    // in devices collection managed internally by FindManager
    IFinderCallbackClient*        _findercallbackclient;    // pointer to client which manages devices collection
    IServiceCallbackClient*        _srveventclient;        // pointer to client receiving services events
    bool                        _externalcollection;    // true if collection is managed externally
    
    CRITICAL_SECTION            _cs;                    // for synchronize access to devices collection
    
    FindManager(const FindManager& srcobj) {}
    FindManager& operator= (const FindManager& srcobj) {return *this;}
};

Using FindManager is pretty simple. After creating the object just call appropriate Init overload function and next Start. Start and Stop functions respectively run and stop the process of discovering devices. FindManager allows managing an internal collection of detected devices (objects of Device class). You can also store a collection of devices discovered by FindManager outside, in your own class implementing IFinderCallbackClient interface. In the case when we want to FindManager will manage the devices collection then we should pass pointer to IFinderManagerClient object to FindManager::Init function. To gain access to internal collection the GetCollection or GetDevice functions should be called. FindManager managing a collection can cooperate with the client implementing IFinderManagerClient interface, so that the client will be notified about events of beginning and finishing the process of discovering as well as adding and removing object from collection. Devices objects in the internal collection of FindManager receive events notifications generated in physical UPnP devices. FindManager can pass these notifications, to an external client, which implements IServiceCallbackClient interface. However, earlier you must use the FindManager::SetServiceEventClientPtr function passing a pointer to client object.

In the case when we want to FindManager will manage only the Device Finder object and we would like to put the collection of discovered devices outside, in your own class, then it should implement the IFinderCallbackClient interface. Then, after creating the FindManager object, just call its function Init passing pointer to object of your own class.

FindManager uses the DevFinderCallback class to receive from UPnP framework (Device Finder object) the results of searching devices in the network.

Device class

Device is the second class, in addition to FindManager, defining the one of the basic elements of UPnP architecture – the UPnP device. This class reflects a tree structure of the device and its functionality in accordance with the standard of device architecture of the UPnP Forum organization.

// class representing UPnP device
class Device
{
    public:
    explicit Device(IUPnPDevice* idev, const Device* parentdev = 0);
    ~Device();
    
    wstring GetUDN() const;
    wstring GetFriendlyName() const;
    wstring GetType() const;
    
    // access to list of services
    int GetServiceListCount() const;
    ServiceIterator GetServiceListBegin() const;
    ServiceIterator GetServiceListEnd() const;
    const Service* GetService(unsigned int index) const;
    
    // access to list of devices
    int GetDeviceListCount() const;
    DeviceIterator GetDeviceListBegin() const;
    DeviceIterator GetDeviceListEnd() const;
    const Device* GetDevice(unsigned int index) const;
    bool IsDeviceListEmpty() const;
    
    // access to list of icons
    int GetIconsListCount() const;
    IconIterator GetIconsListBegin() const;
    IconIterator GetIconsListEnd() const;
    const IconParam& GetIcon(unsigned int index) const;
    bool IsIconsListEmpty() const;
    
    // gets device icon URL
    // passing width & height equals to 0 function returns icon with standard parameters,
    // if it exists (mimetype = image/png).
    // otherwise icon is returned as specified, if any exists.
    // works on root device
    wstring GetDeviceIconURL(int iwidth, int iheight, int idepth) const;
    
    const Device* GetParentDevice() const;
    const Device* GetRootDevice() const;
    bool IsRoot() const;
    
    // with AddRef, don't forget to release interface when unused
    void GetInterface(IUPnPDevice** idev) const;
    
    // returns structure contains data which helps manipulate descr documents
    const DocAccessData* GetAccessData() const;
    
    // retrieves description document url from this UPnP device
    wstring GetDevDocAccessURL() const;
    // retrieves description document content from this UPnP device
    wstring GetDeviceDocument() const;
    
    // retrieves info about this UPnP device
    bool GetDeviceInfo(InfoData& data) const;
    
    // recursive process device object tree
    // see above instructions about IProcessDevice
    void EnumerateDevices(IProcessDevice* iproc, void* param, int procid) const;
    
    private:
    // enumerates member devices of this UPnP device
    // and stores members in collection
    // calls EnumSrv
    bool EnumDev();
    // enumerates member services of this UPnP device
    // and stores collection in corresponding device object
    bool EnumSrv();
    
    // creates new device object and add one to collection
    // with AddRef()
    void AddDevice(IUPnPDevice* idev);
    // creates new service object and add one to collection
    // with AddRef()
    void AddService(IUPnPService* isrv);
    
    bool SetUDN();
    void GenerateFriendlyName();
    wstring GetDocURL() const;
    bool SetType();
    
    void RemoveAllDevices();
    void RemoveAllServices();
    
    private:
    IUPnPDevice*    _idevice;        // COM interface of UPnP device
    const Device*    _parent;        // parent device object
    wstring            _udn;            // Unique Device Name of UPnP device
    wstring            _name;            // friendly name of UPnP device
    wstring            _type;            // type of UPnP device
    ServiceList        _services;        // list of service objects representing hosted UPnP services
    DeviceList        _devices;        // list of device objects representing hosted UPnP devices
    DocAccessData    _accessdata;    // helper data to maniplulate description documents
    IconList        _icons;            // list of icon resources for UPnP device
};

The structure of the object of Device class.

Image 7

Device class contains three collections:

  • DeviceList - of objects of type Device representing the member devices,
  • ServiceList - of objects of type Service representing the member services,
  • IconList - of structures of type IconParam containing icons parameters.

Furthermore, the Device contains information about the name of the device and data concerning the description document in the structure DocAccessData. Also stores a pointer to the object’s interface of appropriate device and pointer to the parent object (if not root device).

In Service object is stored a collection of objects of actions available in the service. About Service class more lately.

Device has a number of functions for the manipulation of the above-mentioned collections: adding, removing and enumerating items. It has also EnumerateDevices function, which recursively enumerate objects in the tree structure of member devices. With this function must cooperate the object of class implementing IProcessDevice interface (client), whose function ProcessDevice will be called while enumerating in order to pass for processing the pointer to currently enumerated device. The enumerating can be started from any node of the tree and is continued in depth of the structure. You can pass to it any object, which will be processed together with the object of the device. In addition, you can pass a parameter of integer type, of general purpose, helpful for example, to control the function’s behavior.

Service class

The Service class represents the object of service of the UPnP device.

// class representing UPnP service
class Service
{
    friend class Device;
    
    public:
    ~Service();
    
    // access to list of actions
    // each service may have 0 or more actions
    int GetActionCount() const;
    ActionIterator GetCollectionBegin() const;
    ActionIterator GetCollectionEnd() const;
    const Action& GetAction(unsigned int index) const;
    const Action& GetAction(const wstring& aname) const;
    
    wstring GetServiceID() const;
    wstring GetServiceTypeID() const;
    
    // if returns zero then error occurred
    long GetLastTransportStatus() const;
    
    const Device& GetParentDevice() const;
    
    // with AddRef, don't forget to release interface when unused
    void GetInterface(IUPnPService** isrv) const;
    
    // adds callback for events and sets its client
    bool SetCallbackClient(IServiceCallbackClient* iclient);
    
    // returns descr document url of this service
    wstring GetScpdURL() const;
    // returns descr document content of this service
    wstring GetScpdContent() const;
    // returns structure contains data which helps manipulate descr documents
    const DocAccessData* GetAccessData() const;
    
    // retrieves info about this UPnP service
    bool GetServiceInfo(InfoData& data) const;
    
    // retrieves info about service's state variables
    // each service must have one or more state variables
    bool GetServiceVariables(VarData& data) const;
    
    private:
    Service(IUPnPService* isrv, const Device& parentdev);
    
    // retrieves scpd info, url and document content
    bool SetAccessData();
    
    // reads actions names
    bool EnumActions();
    
    bool SetServiceID();
    
    wstring                _name;            // service Id
    wstring                _typeid;        // service type Id
    ActionList            _actions;        // list of UPnP service actions
    const Device&        _parent;        // parent device object
    IUPnPService*        _iservice;        // COM interface of UPnP service
    DocAccessData        _accessdata;    // uri and content of document describing UPnP service
    SrvEventCallback*    _isrvcback;        // IUPnPServiceCallback object
    
    Service(const Service& srcobj) : _parent(srcobj._parent) {}
    Service& operator= (const Service& srcobj) {return *this;}
};

Service class contains a list of actions objects (ActionList) belonging to the service it represents, and also stores the URL and content of service’s description document, where are stored all the data needed to perform the action.

Each Service object creates its own instance of SrvEventCallback class (implementing IUPnPServiceCallback interface) to receive event notifications from services. Received notifications are sent farther to the client of callback object, i.e. to the object of class implementing IServiceCallbackClient interface. The callback object (SrvEventCallback), receiving a notification from UPnP framework about the event in the service, receives at the same time a pointer to service object (IUPnPService*). But passing notification farther to the client, instead of a received pointer the pointer to Service object (its host) is passed, to allow the client identification of device, which has generated the event. Client of SrvEventCallback object, processing notification, can determine the source of event on the basis of received pointer to Service object and available in it the pointer to a parent object (Device).

Action class

Action object contains informations about one's input arguments and has function Invoke which invokes appropriate action on parent service.

// class representing member action of UPnP service
class Action
{
    friend class Service;
    
    public:
    // name of this action
    wstring GetName() const;
    
    const Service& GetParentService() const;
    
    // number of arguments (input & output)
    // returns -1 if error occured
    int GetArgsCount();
    // number of input arguments
    // returns -1 if error occured
    int GetInArgsCount();
    
    // retrieves this action info
    bool GetInfo(/*out*/InfoDataList& inflist) const;
    
    // sets array of input arguments types and values
    bool SetInArgs(const StrList& args);
    // sets value of argument at specified index in input arguments array
    bool SetInArgs(const wstring& arg, unsigned int index);
    // gets array of input arguments types and values
    bool GetInArgs(ArgsArray& args);
    // gets input argument at specified index in input arguments array
    bool GetInArgs(InfoDataItem& arg, unsigned int index);
    
    // invokes this action on parent UPnP service
    // argsout    - array of values of output arguments or error message.
    //              on error argsout contains only error messages of InvokeAction
    // return:    -1 = error, >0 = number of output arguments + returned value (if any)
    //            -2 = argsout contains only returned value
    int Invoke(ArgsArray& argsout) const;
    
    private:
    wstring            _name;                // action's name
    const Service*    _parent;            // this action's parent service
    int                _argcount;            // number of input arguments
    int                _inargcount;        // number of all arguments, input and output
    bool            _complete;            // true if number of arguments has been retrieved
    bool            _inargscomplete;    // true if number of input arguments has been retrieved
    ArgsArray        _in;                // array of input arguments (types and values)
    
    bool SetArgsCount();
    bool InitInArgsList();
    
    Action(const Service* srv, const wstring& name);
};

Utility functions

CString GetErrorMessage(HRESULT hr);

Returns description of the error on the basis of retrieved COM error code.

VARTYPE GetVariantType(const wstring& vtype);

Converts the name of a type of state variable (of action’s argument) to corresponding VARIANT type.

wstring GetTypeDescr(VARTYPE vtype);

Converts description of state variable type to variant type.

bool IsICSConnEnabled();

Checks whether the ICS feature is enabled, and whether the ICS uses public and private connections. If the ICS is on, you should not manually enable the Windows Firewall exception for the UPnP framework. This turns off firewall protection for the UPnP ports on all network interfaces, including the ICS public interface. This could expose the computer directly to the Internet.

int CheckFirewallPortState(long number, transport_protocol protocol);

Checks the state of a given port in firewall. If the function returns zero, it means that the error occurred. The value of 1 means that the port is unblocked, while the value of -1 means that the port is blocked. You should pass udp_protocol value to protocol argument for UDP protocol and tcp_protocol value for TCP protocol.

bool ControlUPnPPorts(bool open);

Unblock or block in firewall the ports used by UPnP framework: 2869 TCP and 1900 UDP. To function has effect, an application must work with administrator privileges.

void CheckServiceState(const wstring& srvname, /*out*/srvstate& state);

Checks and returns in the form of description the state of specified system service.

bool ControlSSDPService(bool start);

Starts or stops ssdpsrv system service. To function has effect, an application must work with administrator privileges.

Samples 

It’s known that the best is to learn from examples. So now it’s time to show some examples of application of UPnPCpLib library. I will show you how to use the framework in Win32 applications for console because in this case it will be easier to analyze the code. Obviously use of the library in GUI applications (Win32 or MFC or WinForms) will be much more effective. In the next part of the article will be about example of use the library in WinForms managed application. All presented examples are available for download in the form of Visual Studio 2003 projects which may be easily converted to 2008 version.

// sample 1
// In this sample FindManager manages internal devices collection

#define _WIN32_DCOM

#include <iostream>
#include "upnpcplib.h"

using namespace UPnPCpLib;
using namespace std;

// This class hosts FindManager object and uses its ability to manage
// devices collection, thus implements IFinderManagerClient interface
// to receive notifications from FindManager about changes in collection
// and start/stop events. To receive notifications about events
// from device's services this class implements IServiceCallbackClient.

class FinderClient : public IFinderManagerClient, public IServiceCallbackClient, public IProcessDevice
{
    public:
    FinderClient()
    : _searching(false)
    {
        _fm = new FindManager();    // create new FindManager object
        // creates IUPnPDeviceFinderCallback object,
        // pointer to this class is passed, to manage devices internally
        // and receive notifications from FindManager
        _fm->Init(this);
        // sets receiver (this class) of services events
        _fm->SetServiceEventClientPtr(this);
        
        wcout << L"\nFinderClient was created successfully" << flush;
    }
    
    ~FinderClient()
    {
        if(IsSearching())
        StopSearch();
        
        // destroy FindManager object
        delete _fm;
        
        // when all objects were destroyed then
        // post WM_QUIT message to message loop, to finish it
        PostQuitMessage(0);
        
        wcout << L"\nFinderClient has been destroyed" << flush;
    }
    
    bool StartSearch()
    {
        // start finding devices
        // with default argument "upnp:rootdevice"
        return (_searching = _fm->Start());
    }
    
    bool StopSearch()
    {
        // stop finding devices
        return !(_searching = !_fm->Stop());
    }
    
    bool IsSearching() {return _searching;}
    
    void InvokeAction(unsigned int devindex, const wstring& actionName, const StrList& args)
    {
        _action = 0;
        
        // find service related to given action name
        // and if found then save pointer to requested action object.
        // see ProcessDevice function.
        const Device* dev = _fm->GetDevice(devindex);
        dev->EnumerateDevices(this, (void*)&actionName, 0);
        
        // if action found then invoke it
        if(_action != 0)
        {
            wcout << L"\nInvoke action: " << actionName << endl;
            
            wstring result;
            ArgsArray argsout;
            
            const_cast<Action*>(_action)->SetInArgs(args);
            _action->Invoke(argsout);
            
            for(ArgsArray::size_type i = 0; i < argsout.size(); ++i)
            result.append(argsout[i].second).append(L"\n");
            
            wcout << L"action result:" << endl;
            
            // write result to console
            wcout << result;
        }
        else
        wcout << L"\nAction not found" << endl;
    }
    
    private:
    // IProcessDevice interface implementation.
    // This interface is implemented to use Device::EnumerateDevices function
    virtual void ProcessDevice(const Device* dev, void* param, int procid)
    {
        const wstring& actname = *(const wstring*)param;
        
        ServiceIterator srvi = dev->GetServiceListBegin();
        int srvcount = dev->GetServiceListCount();
        
        // enumerate member services of given device
        for(int i = 0; i < srvcount; ++i, ++srvi)
        {
            // check if current service contains requested action
            // if so, then save pointer.
            // btw, sorry for this "exceptional coding" style :)
            try
            {
                _action = &(*srvi)->GetAction(actname);
                break;
            }
            catch(...)
            {
            }
        }
    }
    
    // IFinderManagerClient interface implementation
    virtual void OnStartFindDevice(long findid)
    {
        // called by FindManager when finding was started successfully
        wcout << L"\nSearching started" << flush;
    }
    
    virtual void OnStopFindDevice(long findid, bool iscancelled)
    {
        // called by FindManager when finding was stopped successfully
        
        // display reason of calling
        wcout << L"\nSearching stopped because of " << (iscancelled ? L"cancel" : L"search complete") << flush;
        
        if(!iscancelled)
        {
            // when search is complete (not cancelled)
            // let's break loop and finish application
            wcout << L"\nSearching will be cancelled" << flush;
            this->StopSearch();
        }
    }
    
    virtual void OnAddDevice(long findid, const Device* dev, int devindex)
    {
        // called by FindManager when device object has been added to collection
        
        // display device name
        wcout << L"\nDevice added: " << dev->GetFriendlyName() ;
        
        // display passed device object properties
        InfoData devdata;
        if(dev->GetDeviceInfo(devdata))
        {
            for(InfoIterator ii = devdata.begin(); ii != devdata.end(); ++ii)
            wcout << endl << (*ii).first << L" = " << (*ii).second;
        }
        
        wcout << L"\nSearching is being continued" << flush;
    }
    
    virtual void OnRemoveDevice(long findid, const wstring& devudn, const wstring& friendlyname, int removedindex)
    {
        // called by FindManager when device object has been removed from collection
        
        // display removed device name
        wcout << L"\nDevice removed: " << friendlyname << L"\nSearching is being continued" << flush;
    }
    
    // IServiceCallbackClient interface implementation
    virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue)
    {
        // called by service callback when value of state variable has been changed
        
        // get pointer to parent device
        wstring devparent = srv->GetParentDevice().GetFriendlyName();
        
        // display device's name, service's id, name of state variable
        // and its current value
        wcout << L"\n==> Event fired:\n\tfrom service id: " << srv->GetServiceID()
        << L"\n\tparent device: " << devparent
        << L"\n\tsource state variable name: " << varname
        << L"\n\tcurrent state variable value: " << varvalue
        << L"\nSearching is being continued" << flush;
    }
    
    virtual void ServiceEventInstanceDied(const Service* srv)
    {
        // called by service callback when service is not responding
        wcout << L"\nService died: " << srv->GetServiceID()
        << "\nIt was member of device: " << srv->GetParentDevice().GetFriendlyName()
        << "\nSearching is being continued" << flush;
    }
    
    private:
    bool            _searching;    // helps break message loop
    FindManager*    _fm;        // object of class from UPnPCpLib library
    const Action*    _action;    // requested action name
};


int main()
{
    // initialize COM library.
    // in console application, COM library is initialized
    // to use single threaded concurrency model
    if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK)
    {
        CoUninitialize();
        return -1;
    }
    
    wcout << L"\nCOM library initialized";
    
    // initialize Winsock library
    WSADATA wdata;
    WSAStartup(MAKEWORD(2,2), &wdata);
    
    wcout << L"\nWinsock library initialized";
    wcout << L"\nCreating FinderClient object" << flush;
    
    // create FinderClient object which hosts FinderManager
    FinderClient* fclnt = new FinderClient();
    
    // start finding all root devices
    wcout << L"\nStart finding\n" << flush;
    
    if(fclnt->StartSearch())
    {
        // process messages
        MSG Message;
        while(GetMessage(&Message, 0, 0, 0))
        {
            DispatchMessage(&Message);
            wcout << '.' << flush;
            
            // when FinderClient receives "search complete" notification
            // its function IsSearching returns false
            // and FinderClient will be destroyed and application finished
            if(!fclnt->IsSearching())
            {
                // let's execute some actions
                wstring name;
                StrList args;
                
                // action without input arguments
                name = L"GetExternalIPAddress";
                
                fclnt->InvokeAction(0, name, args);
                
                // it has one input argument of type ui2
                name = L"GetGenericPortMappingEntry";
                args.push_back(L"0"); // entry's index
                
                fclnt->InvokeAction(0, name, args);
                
                wcout << L"\nFinderClient finished work and will be destroyed\n" << flush;
                // in FinderClient destructor WM_QUIT message is posted
                // to break this loop
                delete fclnt;
                fclnt = 0;
            }
        }
    }
    
    delete fclnt;
    
    wcout << L"\nCleaning up Winsock library";
    // free Winsock library
    WSACleanup();
    
    wcout << L"\nCleaning up COM library";
    // free COM library
    CoUninitialize();
    
    wcout << L"\nExiting... bye" << flush;
    
    return 0;
}

Above program can print out on the console the following results:

COM library initialized
Winsock library initialized
Creating FinderClient object
FinderClient was created successfully
Start finding

Searching started............
Device added: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
Description = LINKSYS WAG200G Router
Document URL = http://10.0.0.1:49152/gateway.xml
Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
Manufacturer = LINKSYS
Manufacturer URL = http://www.linksys.com/
Model = Wireless-G ADSL Home Gateway
Model URL = http://www.linksys.com/
Model number = WAG200G
Presentation URL = http://10.0.0.1/index.htm
Serial number = 123456789
Type = urn:schemas-upnp-org:device:InternetGatewayDevice:1
UDN = uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752
UPC = WAG200G
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:L3Forwarding1
parent device: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
source state variable name: DefaultConnectionService
current state variable value:
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANCommonIFC1
parent device: Internet Connection Sharing (uuid:8c59ad36-1dd2-11b2-ada1-0018398b6752)
source state variable name: PhysicalLinkStatus
current state variable value: Up
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANEthLinkC1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: EthernetLinkStatus
current state variable value: Up
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PossibleConnectionTypes
current state variable value: IP_Routed
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: ConnectionStatus
current state variable value: Connected
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: X_Name
current state variable value: Local Area Connection
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: ExternalIPAddress
current state variable value: 57.153.248.16
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PortMappingNumberOfEntries
current state variable value: 2
Searching is being continued.....
Searching stopped because of search complete
Searching will be cancelled
Searching stopped because of cancel.
Invoke action: GetExternalIPAddress
action result:
57.153.248.16

Invoke action: GetGenericPortMappingEntry
action result:

11599
TCP
11599
10.0.0.2
True
BitComet (10.0.0.2:11599) 11599 TCP
0

FinderClient finished work and will be destroyed

FinderClient has been destroyed
Cleaning up Winsock library
Cleaning up COM library
Exiting... bye

And the next sample:

// sample 2
// In this sample devices collection is managed outside of FindManager

#define _WIN32_DCOM

#include <iostream>
#include "upnpcplib.h"

using namespace UPnPCpLib;
using namespace std;


// This class hosts FindManager object and DON'T uses its ability to manage
// devices collection. Instead, this class on its own is managing the collection.
// In this case FindManager is used only for finding devices.
// Thus this class implements IFinderCallbackClient interface
// to receive notifications from UPnP framework about found devices.
// To receive notifications about events from device's services
// this class implements IServiceCallbackClient.

class FinderHost : public IFinderCallbackClient, public IServiceCallbackClient, public IProcessDevice
{
    public:
    FinderHost()
    : _searching(false)
    , _finish(false)
    {
        // create IUPnPDeviceFinderCallback object
        // and pass pointer to FinderHost which will be managing devices collection
        _fm = new FindManager();
        _fm->Init(this);
        
        wcout << L"FinderHost was created successfully" << endl;
    }
    
    ~FinderHost()
    {
        if(IsSearching())
        StopSearch();
        
        // remove devices collection
        RemoveAllDevices();
        
        // destroy FindManager object
        delete _fm;
        
        // when all objects were destroyed then
        // post WM_QUIT message to message loop, to finish it
        //PostQuitMessage(0);
        
        wcout << L"\nFinderHost has been destroyed" << endl;
    }
    
    bool StartSearch()
    {
        // start finding devices
        // with default argument "upnp:rootdevice"
        return (_searching = _fm->Start());
    }
    
    bool StopSearch()
    {
        // stop finding devices
        return !(_searching = !_fm->Stop());
    }
    
    bool IsSearching() {return _searching;}
    
    void SetFinish() {_finish = true;}
    bool IsFinished() {return _finish;}
    
    // IFinderCallbackClient implementation
    // in single threading model it's unneccessary to synchronize access
    // to devices collection, thus function bodies are empty
    void Lock() {}
    void UnLock() {}
    
    void InvokeAction(int devindex, const wstring& actionName, const StrList& args)
    {
        _action = 0;
        
        // find service related to given action name
        // and if found then save pointer to requested action object.
        // see ProcessDevice function.
        const Device* dev = _devs[devindex];
        dev->EnumerateDevices(this, (void*)&actionName, PID_FIND_ACTION);
        
        // if action found then invoke it
        if(_action != 0)
        {
            wcout << L"\nInvoke action: " << actionName << flush;
            
            wstring result;
            ArgsArray argsout;
            
            const_cast<Action*>(_action)->SetInArgs(args);
            _action->Invoke(argsout);
            
            for(ArgsArray::size_type i = 0; i < argsout.size(); ++i)
            result.append(argsout[i].second).append(L"\n");
            
            wcout << L"\naction result:" << endl;
            
            // write result to console
            wcout << result << endl;
        }
        else
        wcout << L"\nAction not found" << endl;
    }
    
    private:
    
    // identifiers used in ProcessDevice
    static const int PID_ADD_SERVICE_CALLBACK = 0;
    static const int PID_FIND_ACTION = 1;
    
    // IProcessDevice interface implementation.
    // This interface is implemented for use Device::EnumerateDevices function
    virtual void ProcessDevice(const Device* dev, void* param, int procid)
    {
        switch(procid)
        {
            // for add pointer to client of services callbacks
            case PID_ADD_SERVICE_CALLBACK:
            {
                ServiceIterator srvi = dev->GetServiceListBegin();
                int srvcount = dev->GetServiceListCount();
                
                for(int i = 0; i < srvcount; ++i, ++srvi)
                const_cast<Service*>(*srvi)->SetCallbackClient(this);
            }
            break;
            
            // for find service containing given action name
            case PID_FIND_ACTION:
            {
                const wstring& actname = *(const wstring*)param;
                
                ServiceIterator srvi = dev->GetServiceListBegin();
                int srvcount = dev->GetServiceListCount();
                
                // enumerate member services of given device
                for(int i = 0; i < srvcount; ++i, ++srvi)
                {
                    // check if current service contains requested action
                    // if so, then save pointer.
                    // btw, sorry for this "exceptional coding" style :)
                    try
                    {
                        _action = &(*srvi)->GetAction(actname);
                        break;
                    }
                    catch(...)
                    {
                    }
                }
            }
            break;
        }
    }
    
    // IFinderCallbackClient interface implementation
    
    virtual void DeviceAdded(long findid, IUPnPDevice* idev)
    {
        // called by UPnP framework when the new device was found
        
        // create new root Device object and build its structure
        Device* dev = new Device(idev);
        
        // add callback for services events
        dev->EnumerateDevices(this, 0, PID_ADD_SERVICE_CALLBACK);
        
        // add Device root object to collection
        _devs.push_back(dev);
        
        wcout << L"\nNew device added: " << dev->GetFriendlyName() << endl;
        
        // display info about added device
        InfoData devdata;
        if(dev->GetDeviceInfo(devdata))
        {
            for(InfoIterator ii = devdata.begin(); ii != devdata.end(); ++ii)
            wcout << endl << (*ii).first << L" = " << (*ii).second;
        }
        
        wcout << L"\nSearching is being continued" << flush;
    }
    
    virtual void DeviceRemoved(long findid, const wstring& devname)
    {
        // called by UPnP framework when the device was removed from collection
        
        for(DeviceArray::size_type i = 0; i < _devs.size(); ++i)
        {
            if(devname == _devs[i]->GetUDN())
            {
                wstring friendlyname = _devs[i]->GetFriendlyName();
                
                // remove passed device from devices collection
                delete _devs[i];
                _devs.erase(_devs.begin() + i);
                
                wcout    << L"\nDevice was removed: " << friendlyname
                << L"\nSearching is being continued" << flush;
                
                break;
            }
        }
    }
    
    virtual void SearchComplete(long findid)
    {
        // called by UPnP framework when finding was completed successfully
        
        wcout << L"\nSearch is complete" << endl;
        
        // when search is complete (not cancelled)
        // let's break loop and finish application
        wcout << L"Searching will be cancelled" << endl;
        this->StopSearch();
    }
    
    // IServiceCallbackClient interface implementation
    virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue)
    {
        // called by service callback when value of state variable has been changed
        
        // get parent device name
        wstring devparent = srv->GetParentDevice().GetFriendlyName();
        // display device's name, service's id, name of state variable
        // and its current value
        wcout << L"\n==> Event fired:\n\tfrom service id: " << srv->GetServiceID()
        << L"\n\tparent device: " << devparent
        << L"\n\tsource state variable name: " << varname
        << L"\n\tcurrent state variable value: " << varvalue << endl;
        
        if(_searching)
        wcout << L"Searching is being continued" << flush;
        
        // when all notifications are received then
        // post WM_QUIT message to message loop, to finish it
        if(_finish)
        PostQuitMessage(0);
    }
    virtual void ServiceEventInstanceDied(const Service* srv)
    {
        // called by service callback when service is not responding
        wcout << L"\nService died: " << srv->GetServiceID()
        << L"\nIt was member of device: " << srv->GetParentDevice().GetFriendlyName()
        << L"\nSearching is being continued" << flush;
    }
    
    void RemoveAllDevices()
    {
        if(!_devs.empty())
        {
            for(vector<Device*>::iterator di = _devs.begin(); di != _devs.end(); ++di)
            {
                delete *di;
                *di = 0;
            }
            
            _devs.clear();
        }
    }
    
    private:
    bool            _searching;    // helps break message loop
    bool            _finish;    // helps break message loop
    FindManager*    _fm;        // object of class from UPnPCpLib library
    DeviceArray        _devs;        // devices collection
    const Action*    _action;    // requested action name
};



int _tmain(int argc, _TCHAR* argv[])
{
    // initialize COM library.
    // in console application, COM library is initialized
    // to use single threaded concurrency model
    if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK)
    {
        CoUninitialize();
        return -1;
    }
    
    wcout << L"COM library initialized" << endl;
    
    // initialize Winsock library
    WSADATA wdata;
    WSAStartup(MAKEWORD(2, 2), &wdata);
    
    wcout <<    L"Winsock library initialized\n"
    L"Creating FinderHost object" << endl;
    
    // create FinderHost object which hosts FinderManager
    FinderHost* fhost = new FinderHost();
    
    // start finding all root devices
    wcout << L"Start finding" << flush;
    
    if(fhost->StartSearch())
    {
        // process messages
        MSG Message;
        while(GetMessage(&Message, NULL, 0, 0))
        {
            DispatchMessage(&Message);
            wcout << '.' << flush;
            
            // when FinderHost receives "search complete" notification
            // its function IsSearching returns false
            // and FinderHost will be destroyed and application finished
            if(!fhost->IsSearching() && !fhost->IsFinished())
            {
                wcout << L"\nSearching has been cancelled successfully" << endl;
                
                // let's execute some actions
                wstring name;
                StrList args;
                
                // this action doesn't return any output arguments
                // but fires event from state variable PortMappingNumberOfEntries
                name = L"AddPortMapping";
                args.push_back(L"any");                // remote host, type: string
                args.push_back(L"11200");            // external port, type: ui2
                args.push_back(L"UDP");                // protocol, type: string
                args.push_back(L"11200");            // internal port, type: ui2
                args.push_back(L"192.168.0.10");    // internal client, type: string
                args.push_back(L"true");            // enabled, type: boolean
                args.push_back(L"add port mapping example"); // description, type: string
                args.push_back(L"0");                // lease duration (0 = permanent), type: ui4
                
                fhost->InvokeAction(0, name, args);
                
                // this action also doesn't return any output arguments
                // but fires event from state variable PortMappingNumberOfEntries
                name = L"DeletePortMapping";
                args.clear();
                args.push_back(L"any");            // remote host, type: string
                args.push_back(L"11200");        // external port, type: ui2
                args.push_back(L"UDP");            // protocol, type: string
                
                fhost->InvokeAction(0, name, args);
                
                wcout << L"\nFinderHost finished work and will be destroyed" << endl;
                // in FinderHost ServiceEventVariableChanged WM_QUIT message is posted
                // to break this loop
                fhost->SetFinish();
                //delete fhost;
                //fhost = 0;
            }
        }
    }
    
    delete fhost;
    
    wcout << L"Cleaning up Winsock library" << endl;
    // free Winsock library
    WSACleanup();
    
    wcout << L"Cleaning up COM library" << endl;
    // free COM library
    CoUninitialize();
    
    wcout << L"Exiting... bye" << endl;
    
    return 0;
}

Above program can print out on the console the following results:

COM library initialized
Winsock library initialized
Creating FinderHost object
FinderHost was created successfully
Start finding............
New device added: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)

Description = LINKSYS WAG200G Router
Document URL = http://10.0.0.1:49152/gateway.xml
Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
Manufacturer = LINKSYS
Manufacturer URL = http://www.linksys.com/
Model = Wireless-G ADSL Home Gateway
Model URL = http://www.linksys.com/
Model number = WAG200G
Presentation URL = http://10.0.0.1/index.htm
Serial number = 123456789
Type = urn:schemas-upnp-org:device:InternetGatewayDevice:1
UDN = uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752
UPC = WAG200G
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:L3Forwarding1
parent device: Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
source state variable name: DefaultConnectionService
current state variable value:
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANCommonIFC1
parent device: Internet Connection Sharing (uuid:8c59ad36-1dd2-11b2-ada1-0018398b6752)
source state variable name: PhysicalLinkStatus
current state variable value: Up
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANEthLinkC1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: EthernetLinkStatus
current state variable value: Up
Searching is being continued...........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PossibleConnectionTypes
current state variable value: IP_Routed
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: ConnectionStatus
current state variable value: Connected
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: X_Name
current state variable value: Local Area Connection
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: ExternalIPAddress
current state variable value: 57.154.192.91
Searching is being continued.........
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PortMappingNumberOfEntries
current state variable value: 2
Searching is being continued.....
Search is complete
Searching will be cancelled
.
Searching has been cancelled successfully

Invoke action: AddPortMapping
action result:


Invoke action: DeletePortMapping
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PortMappingNumberOfEntries
current state variable value: 3

action result:


FinderHost finished work and will be destroyed
.....
==> Event fired:
from service id: urn:upnp-org:serviceId:WANPPPConn1
parent device: Internet Connection Sharing (uuid:8c59ad37-1dd2-11b2-ada0-0018398b6752)
source state variable name: PortMappingNumberOfEntries
current state variable value: 2
.
FinderHost has been destroyed
Cleaning up Winsock library
Cleaning up COM library
Exiting... bye

WinForms control point application (Finder.net)

Finally, I would like to present an example of control point application with GUI of type Windows Forms and written in C#. This .net application uses native UPnPCpLib library through the .net class library (FindManager.net dll) which wraps native code. However, due to significantly greater amount of code, in comparison to simple Win32 examples listed above, instead of code I will present screens from application and I'll cover its functions. The source code of application and library is available for download in the form of Visual Studio 2008 projects (Finder.net and FindManager.net). In addition, application’s executable files are attached. Beside WinForms application the MFC version is also available for download (Finder). More examples related to programming control points can be found on MSDN [1] and in Platform SDK (default in path %ProgramFiles%\Microsoft Platform SDK\Samples\NetDS\upnp\GenericUCP\cpp). Moreover, many code samples, tools and other helper stuff you can download from Intel website [11].

In the sample WinForms application I tried to cover as much as possible range of functionality of UPnP control point. My intention was also to create an application simple and useful. Did I succeeded in achieve this intention? Let the readers (users) decide.

Application’s features:

  • discovering devices by type (asynchronously),
  • creation of objects corresponding to structure and functionality of discovered devices,
  • managing a collection of devices objects,
  • presentation of objects structure,
  • displaying information about elements of a device object,
  • controlling devices via actions,
  • receiving events notifications generated by services,
  • viewing and saving the description document,
  • opening URLs related to devices and services,
  • management of ssdpsrv system service,
  • management of ports used by ssdpsrv service,
  • editing configuration entries in the system registry.

The program searches for the specified type of device. If you choose the type "upnp:rootdevice", the scope of the search will cover devices of all basic types. On picture, we can see that the program has found several devices of different types. We can see also information (displayed in the "Event Log" window) coming from the events received during detection of new device.

Image 8

Type of devices which will be searched can be specified with the help of the filter.

Image 9

Let's see how to invoke actions on the example of device of type Internet Gateway Device (router). Actions concern configuration of "Port Forwarding" feature. First, I add a new entry to the port mapping table.

Image 10

Sequence of steps is marked on illustration. (1) Select the name of action, (2) write values of input arguments (names and types of arguments are displayed in both marked windows. (3) After execution of the action, in the "Event Log" window, check the status of action (200 OK) and the current number of entries in the table - "Variable value: 3". When determining the type of argument might be useful the table above.

Now check the content of new entry in the mapping table specifying the number of entry. Previously we read the number of entries (3 entries), thus last will be the number 2 (the first is the number 0).

Image 11

In action’s response we receive content of the desired entry - in the "Properties" window under "Output arguments" caategory or in the "Action's arguments" window.

And finally, action of remove previously added entry.

Image 12

The scenario is similar for case of adding an entry. (1) After selecting the appropriate action, (2) write values of input arguments in "Properties" window. (3) After action invoking, in events window, check the status of action invoke (200 OK) and the current number of entries – "Variable value: 2".

In the options window, you can change the configuration parameters of UPnP framework stored in the system registry.

Image 13

Managed wrapper over UPnPCpLib library (FindManager.net) 

FindManager.net is a class library which wraps native UPnPCpLib library. This class library can be used with any .net application. To do this just add reference to FindManager.net.dll and declare "FindManagerWrapper" namespace name. Class library consists of four main classes:

  • FindManagerNet,
  • DeviceNet,
  • ServiceNet,
  • ActionNet.

These classes corresponds to their native "brothers", respectively FindManager, Device, Service and Action. The basic class is FindManagerNet which serves as devices finder and manages collection of devices objects. Detailed informations about general functionality of these classes are contained above in description of UPnPCpLib framework.

FindManagerNet class

  • constructors
FindManagerNet()Default constructor. Appropriate for console application when COM library was initialized for use single threaded model. It is recommended to avoid using this constructor in GUI applications.
FindManagerNet(SynchronizationContext)Use this constructor in GUI applications when COM library was initialized for use multi threaded model. In this case library's functions called by UPnP framework will be executed in separate threads. Thanks to that constructor events handlers executed in separate threads can safely access controls created in main GUI thread without the need to use InvokeRequired.
 
  • properties  
Device[int] Returns device object from collection at specified index.
CollectionSize Gets number of devices in collection.
IsCollectionEmptyChecks if there are any devices in collection.
FindId Gets search session identifier.
DeviceTypeCountGets number of names of devices types in array. (static)
DeviceTypesReturns array of names of devices types. (static) 
RootDeviceTypeGets name of basic type of device. (static) 
DeviceType[int] Gets name of type at specified index in array. (static) 
 
  • methods
Init(String) Initializes FindManager object and uses specified type of device while searching for devices. Returns true if succeeded.
Init()Initializes FindManager object and uses "upnp:rootdevice" type of device while searching for devices. All basic types will be searched. Returns true if succeeded.
StartStarts searching. Returns true if succeeded.
StopCancels searching. Returns true if succeeded.
GetEnumeratorReturns enumerator for devices collection.
GetCollectionReturns collection of detected devices.
 
  • events
OnStartFind Occurs when search for devices has been started. Passes to handler argument of type StartFindArgs which contains info about identifier of search session.
OnStopFind Occurs when search for devices has been cancelled. Passes to handler argument of type StopFindArgs which contains info about identifier of search session and the reason of cancelling the search.
OnAddDevice Occurs when new device is added to collection. Passes to handler argument of type AddDeviceArgs which contains identifier of search session, device object and index of this object in collection.
OnRemoveDevice Occurs when device is removed from collection. Passes to handler argument of type RemoveDeviceArgs which contains identifier of search session, unique and friendly name of device object and last index of this object in collection.
OnServiceVariableChangedOccurs when state variable's value has been changed. Passes to handler argument of type ServiceVariableChangedArgs which contains service object and state variable's name and value.
OnServiceInstanceDiedOccurs when service is not responding. Passes to handler argument of type ServiceInstanceDiedArgs which contains service object.
 
  • static helper methods
GetErrorMessage Converts hresult of action's invokation to message string. (HRESULT to string)
GetVariantType Converts description of state variable type to variant type. (string to VARTYPE)
GetTypeDescription Converts variant type to description of state variable type. (VARTYPE to string)
IsICSConnectionEnabled Checks for Internet Connection Sharing enabled connections.
CheckSystemServiceStateChecks specified system service state.
ControlSSDPService Starts or stops ssdp system service.
CheckFirewallPortState Checks specified firewall port state. Returns: 0 - error, 1 - enabled, -1 - disabled.
ControlUPnPPorts Opens or closes UPnP ports (2869 TCP, 1900 UDP).
 

DeviceNet class

  • properties
UniqueName Returns Unique Device Name (UDN) of device.
FriendlyName Returns descriptive name of device.
Type Gets type of device.
ParentDevice Returns object of parent device. Null for root device.
RootDevice Returns parent of all devices in hierarchy.
IsRoot Checks if device is root device.
DocumentURL Gets URL of description document.
Info Gets properties of device.
Device[int] Returns device at specified index in devices collection.
DeviceCount Gets number of devices in collection.
IsDeviceCollectionEmpty Checks if there are any devices in collection.
Service[int] Returns service at specified index in collection of services.
ServiceCount Gets number of services in collection.
Icon[int] Returns icon at specified index in collection of icons.
IconCount Gets number of icons in collection.
IsIconCollectionEmpty Checks if there are any icons in collection.
EnumeratorSwitch Gets or sets the value of enum EnumSwitch (one of: Devices, Services, Icons) indicating which collection (respectively devices or services or icons) will be enumerated with the help of enumerator obtained by GetEnumerator method.
 
  • methods
GetEnumerator Returns enumerator for collection which is indicated by the current value of EnumeratorSwitch property. Which collection is currently enumerated depends on enumerator switch.
GetDevices Returns array of devices objects.
GetServices Returns array of services objects.
GetIcons Returns array of icons objects.
GetDocumentContent Returns content of description document.
GetIconURL Returns URL of icon with specified parameters (width, height,  depth). 
EnumerateDevicesProcesses device structure and for each member device OnProcessDevice event is raised. You can pass to this method two helper arguments of general purpose which will be passed as arguments to handler of OnProcessDevice event. Both arguments can be null. 
 
  • events
OnProcessDevice Raised for each member device while processing structure of root device with the help of EnumerateDevices method. Passes to handler argument of type ProcessDeviceArgs which contains device object and two arguments of general purpose.
 

ServiceNet class

  • properties
Action[int] Gets action object at specified index in array of actions.
Action[String] Gets action object about specified name in array of actions.
ActionsCountGets number of actions.
StateVariablesGets collection of state variables. Element of collection contains variable's name and boolean value indicating that variable sends events.
Info Gets service's properties.
ServiceID Gets service's name.
ServiceTypeID Gets service's type.
LastTransportStatus Gets status code of last action.
ParentDevice Gets parent device.
ScpdURL Gets URL of description document.
 
  • methods
GetEnumerator Returns enumerator for actions collection.
GetActions Returns array of actions objects.
GetScpdContent Returns content of description document.
 

ActionNet class

  • properties
Name Gets name of action.
ArgumentCount Gets number of action's input and output arguments.
ArgumentInCount Gets number of action's input arguments.
ArgumentsIn Sets input arguments.
Argument Sets input argument at specified index in array of input arguments.
Arguments Gets input arguments.
Info Gets action's properties.
ParentService Gets parent service object.
 
  • methods
InvokeInvokes this action on parent UPnP service. argsout    - array of values of output arguments and returned value or error message. On error argsout contains only error messages. Returns: -1 on error, >0 means number of output arguments + returned value (if any), -2 if argsout contains only returned value.

Using the FindManagerNet

First of all I would like to notice that FindManagerNet object uses asynchronous method of searching for devices what means that events raised from system UPnP framework and received by FindManagerNet object can be executed in separate threads. Thus, it is better to use FindManagerNet when COM library is configured to use multi threaded model of concurrency. Default, COM library used by .net applications is initialized to use single threaded concurrency model. Which model will be used, it depends from attribute applied to entry point of application. This entry point is Main method in C# and Visual Basic. For use multithreaded model the attribute should be set to [MTAThread] whereas default singlethreaded model is used when the attribute is set to [STAThread].

Default setting of COM threading model for C# application - singlethreaded (in file Program.cs).

static class Program
{
    // The main entry
    [STAThread]
    static void Main()
    {
        // ... body of Main
    }
}

Setting of COM threading model for C# application - multithreaded (in file Program.cs).

static class Program
{
    // The main entry
    [MTAThread]
    static void Main()
    {
        // ... body of Main
    }
}

In GUI application when COM threading model is set to multithreaded, to create FindManagerNet object You should use constructor taking argument of type SynchronizationContext. Thanks to that constructor events handlers executed in separate threads can safely access controls created in main GUI thread without the need to use InvokeRequired. FindManagerNet object should be created inside of main form's thread where this object is used. The point is that FindManagerNet object can't be static when constructor FindManagerNet(SynchronizationContext) is used. The property System.Threading.SynchronizationContext.Current should be called in main GUI thread where handlers of events from FindManagerNet object (like OnAddDevice) can access controls on form.

To create FindManagerNet object, first, define reference of FindManagerNet inside class of form:

public partial class Form1 : Form
{
    FindManagerNet finder;

    // ... rest of class body
}

Next, instantiate FindManagerNet object with the help of constructor which takes argument of type SynchronizationContext - in constructor of form:

public Form1()
{
    InitializeComponent();

    finder = new FindManagerNet(SynchronizationContext.Current);

    // ... remaining instructions
}

After completion of above basics steps You can set up handlers for events and initialize FindManagerNet object as shown in following example in which will be invoked actions of adding and removing entries from port mapping table of Internet Gateway Device.

using FindManagerWrapper;

namespace test_finder
{
    public partial class Form1 : Form
    {
        // define finder object's reference
        FindManagerNet _fm;
        // service which contains needed actions
        // for adding and deleting port entries
        ServiceNet _srv;
        
        public Form1()
        {
            InitializeComponent();

            // create finder and set up needed handlers
            _fm = new FindManagerNet(SynchronizationContext.Current);
            _fm.OnServiceVariableChanged += new ServiceVariableChangedEvent(_fm_OnServiceVariableChanged);
            _fm.OnAddDevice += new AddDeviceEvent(_fm_OnAddDevice);
        }
        
        void _fm_OnAddDevice(object sender, AddDeviceArgs e)
        {
            // when first device has been found
            // then stop searching and find appropriate service
            
            _fm.Stop();

            // get found device object
            DeviceNet dev = e.Device;
            string requestedService = "urn:upnp-org:serviceId:WANPPPConn1";

            // enumerate member devices for find requested service
            dev.OnProcessDevice += new ProcessDeviceEvent(dev_OnProcessDevice);
            dev.EnumerateDevices(requestedService, 0);
            dev.OnProcessDevice -= new ProcessDeviceEvent(dev_OnProcessDevice);
        }
        
        void dev_OnProcessDevice(object sender, ProcessDeviceArgs e)
        {
            // if requested service has been found
            // then save reference
            
            string requestedService = (string)e.Param;
            
            // switch collection which will be enumerated
            // default collection of devices is enumerated
            e.Device.EnumeratorSwitch = EnumSwitch.Services;
            
            foreach(ServiceNet srv in e.Device)
            if (srv.ServiceID == requestedService)
            {
                _srv = srv;
                break;
            }
        }
        
        void _fm_OnServiceVariableChanged(object sender, ServiceVariableChangedArgs e)
        {
            // display current number of port mappings
            
            if(e.VarName == "PortMappingNumberOfEntries")
            labelNumber.Text = e.VarValue;
        }
        
        // start
        private void button1_Click(object sender, EventArgs e)
        {
            _fm.Init(FindManagerNet.DeviceTypes[4]); // InternetGatewayDevice
            _fm.Start();
        }
        
        // stop
        private void button2_Click(object sender, EventArgs e)
        {
            _fm.Stop();
        }
        
        // add entry
        private void button3_Click(object sender, EventArgs e)
        {
            if (_srv != null)
            {
                ActionNet act = _srv.get_Action("AddPortMapping");
                // required input arguments
                act.ArgumentsIn = new List<string> {"any", "11200", "UDP", "11200", "10.0.0.2", "true", "test add entry", "0"};
                
                // this action doesn't returns any output arguments
                // this action causes change of state variable PortMappingNumberOfEntries
                // and raise of event, thus OnServiceVariableChanged will be called
                List<KeyValuePair<string, string>> outs = null;
                act.Invoke(ref outs);
            }
        }
        
        // delete entry
        private void button4_Click(object sender, EventArgs e)
        {
            if (_srv != null)
            {
                ActionNet act = _srv.get_Action("DeletePortMapping");
                // required input arguments
                act.ArgumentsIn = new List<string> { "any", "11200", "UDP" };
                
                // this action doesn't returns any output arguments
                // this action causes change of state variable PortMappingNumberOfEntries
                // and raise of event, thus OnServiceVariableChanged will be called
                List<KeyValuePair<string, string>> outs = null;
                act.Invoke(ref outs);
            }
        }
    }
}

History 

  • 07 Jul 2009
    Fixed issue with long time finding device when found device is "software" device on the same computer where control point application using UPnPCpLib library is running. Added description of using FindManagerNet class.
  • 17 Jun 2009
    Rewrited library's code. Added new STL version of library. Added managed wrapper over native library. Added managed version of "Finder" application and new version of MFC application. 
  • 28 Jun 2008
    Initial release.

References

  1. UPnP APIs, MSDN, http://msdn.microsoft.com/en-us/library/aa382303(VS.85).aspx[^]
  2. NAT API, MSDN, http://msdn.microsoft.com/en-us/library/aa366187(VS.85).aspx[^]
  3. UPnP Forum, http://www.upnp.org/[^]
  4. Using UPnP for Programmatic Port Forwardings and NAT Traversal, Mike O'Neill, PortForward.aspx[^]
  5. RFC 1067 (SNMP), IETF, http://tools.ietf.org/html/rfc1067[^]
  6. RFC 3489 (STUN), IETF, http://tools.ietf.org/html/rfc3489[^]
  7. SSDP, IETF, http://tools.ietf.org/html/draft-cai-ssdp-v1-03[^]
  8. Description of Universal Plug and Play Features in Windows XP, Microsoft, http://support.microsoft.com/kb/323713[^]
  9. Universal Plug and Play in Windows XP, MS TechNet, http://technet.microsoft.com/en-us/library/bb457049.aspx[^]
  10. How Windows Firewall affects the UPnP framework in Windows XP Service Pack 2, Microsoft, http://support.microsoft.com/kb/886257[^]
  11. Intel Software for UPnP Technology, Intel, http://www.intel.com/cd/ids/developer/asmo-na/eng/downloads/upnp/index.htm[^]
  12. CMarkup software, First Objective Software, Inc., http://www.firstobject.com/dn_markup.htm[^]

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)