5,783,659 members and growing! (15,748 online)
Email Password   helpLost your password?
General Programming » Internet / Network » Internet & Network     Beginner License: The Code Project Open License (CPOL)

Programming control point application using the UPnP Control Point API

By amatecki

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 application.
C++, Windows (WinXP, Win2003, Vista, Windows), Win Mobile (CE .NET 4.0, CE .NET 4.1, CE .NET 4.2, Win Mobile), COM, Win32, Visual Studio (Visual Studio, VS.NET2003), MFC, ATL, Dev

Posted: 28 Jun 2008
Updated: 28 Jun 2008
Views: 9,893
Bookmarked: 34 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
20 votes for this Article.
Popularity: 6.03 Rating: 4.64 out of 5
2 votes, 10.0%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
0 votes, 0.0%
4
18 votes, 90.0%
5
Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report This Article

Contents

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 application 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 ATL 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.

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.

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 execution 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(NULL, COINIT_APARTMENTTHREADED);
if(SUCCEEDED(hr))
{
    // ... code using COM library
}
CoUninitialize();

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 ;).

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 its 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.

// IUPnPDeviceFinderCallback implementation

#include <atlbase.h>
#include <atlcom.h>    // CComObjectRootEx
#include <upnp.h>      // IUPnPDeviceFinderCallback

class DevFinderCallback :
public CComObjectRootEx<CComSingleThreadModel>,
public IUPnPDeviceFinderCallback
{
    public:
    DevFinderCallback() {};
    ~DevFinderCallback() {};
    
    BEGIN_COM_MAP(DevFinderCallback)
    COM_INTERFACE_ENTRY(IUPnPDeviceFinderCallback)
    END_COM_MAP()
    
    virtual HRESULT __stdcall DeviceAdded(long finddata, IUPnPDevice* idev)
    {
        // process IUPnPDevice pointer when device was added
        return S_OK;
    }
    virtual HRESULT __stdcall DeviceRemoved(long finddata, BSTR devudn)
    {
        // do something when device was removed
        return S_OK;
    }
    virtual HRESULT __stdcall SearchComplete(long finddata)
    {
        // do something when search is complete
        return S_OK;
    }
};

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

// for CComObject::CreateInstance
// declare this variable as global in your source file
CComModule _Module;

CComObject<DevFinderCallback> *cback = NULL;
// if returned pointer is equal to NULL
// it means then error occured in function CreateInstance
CComObject<DevFinderCallback>::CreateInstance(&cback);
if(cback != NULL)
    // ref count is 0 thus increment, be sure to release on finish
    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 = NULL;

hr = CoCreateInstance(CLSID_UPnPDeviceFinder, NULL, CLSCTX_SERVER,
                      IID_IUPnPDeviceFinder, (LPVOID*)&_ifinder);
if(SUCCEEDED(hr))
{
    // 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 order 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 <atlbase.h>    // CComBSTR
#include <upnp.h>

HRESULT hr;
// Device Finder object interface
IUPnPDeviceFinder* _ifinder = NULL;
// device type to find
CComBSTR devtype(L"upnp:rootdevice");
// search identifier
long _findhandle;

hr = CoCreateInstance(CLSID_UPnPDeviceFinder, NULL, CLSCTX_SERVER,
                      IID_IUPnPDeviceFinder, (LPVOID*)&_ifinder);
if(SUCCEEDED(hr))
{
    // prepare search
    // cback is DevFinderCallack pointer obtained earlier
    hr = _ifinder->CreateAsyncFind(devtype, NULL, cback, &_findhandle);
    if(SUCCEEDED(hr))
    {
        // start search
        hr = _ifinder->StartAsyncFind(_findhandle);
        if(SUCCEEDED(hr))
        {
            // do something if success or simply return
        }
        else
            // if failed then cancel search
            _ifinder->CancelAsyncFind(_finderhandle);
    }
}

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:

// destroing objects

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

// release Device Finder object
if(_ifinder != NULL)
{
    _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 a unique name, model designation, the name of the manufacturer or URL, which will open in a browser 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 <atlbase.h>    // CComBSTR
#include <upnp.h>

// idev is pointer to root device

void EnumerateDevices(IUPnPDevice* idev)
{
    HRESULT hr = S_OK;
    IUPnPDevices* children = NULL; // collection of member devices
    VARIANT_BOOL bcheck = 0;
    
    // enumerate devices
    // first check if device has children
    idev->get_HasChildren(&bcheck);
    
    // if device has children then get collection interface
    if(bcheck && idev->get_Children(&children) == S_OK)
    {
        IUnknown* ienum = NULL;        // for obtain the enumerator
        long devscount = 0;        // count of member devices
        
        // get count of member devices
        children->get_Count(&devscount);
        
        // get helper interface
        if(children->get__NewEnum(&ienum) == S_OK)
        {
            IEnumUnknown* icol = NULL; // collection enumerator
            
            // get enumerator interface
            hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
            if(SUCCEEDED(hr))
            {
                IUnknown* iitem = NULL; // collection’s item
                IUPnPDevice* ichild = NULL; // member device
                CComBSTR btmp;
                
                // enumerate collection
                icol->Reset();
                while(icol->Next(1, &iitem, NULL) == S_OK)
                {
                    // get member device interface
                    hr = iitem->QueryInterface(IID_IUPnPDevice,
                                                  (void**)&ichild);
                    if(SUCCEEDED(hr))
                    {
                        // get some member device’s properties
                        btmp.Empty();
                        ichild->get_UniqueDeviceName(&btmp);
                        btmp.Empty();
                        ichild->get_PresentationURL(&btmp);
                        
                        // continue recursive
                        EnumerateDevices(ichild);
                        
                        ichild->Release();
                    }
                    iitem->Release();
                }
                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 <atlbase.h>    // CComBSTR
#include <upnp.h>

// idev is pointer to root device

void GetDeviceDocumentAccessURL(IUPnPDevice* idev)
{
    HRESULT hr = S_OK;
    IUPnPDeviceDocumentAccess* idoc = NULL;
    
    // URL will be stored into this variable
    CComBSTR btmp;
    
    // query for IUPnPDeviceDocumentAccess
    hr = idev->QueryInterface(IID_IUPnPDeviceDocumentAccess, (void**)&idoc);
    
    if(SUCCEEDED(hr))
    {
        // get URL and write to CComBSTR
        idoc->GetDocumentURL(&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 consists in 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 <atlbase.h>    // CComBSTR
#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 = NULL;        // for obtain the enumerator
    IUPnPServices *isrvs = NULL;   // the collection of services
    
    // get collection interface
    if(idev->get_Services(&isrvs) != S_OK)
        return;
    
    // get count of services
    isrvs->get_Count(&srvcount);
    
    // get helper interface
    if(isrvs->get__NewEnum(&ienum) == S_OK)
    {
        IEnumUnknown *icol = NULL; // the enumerator of collection
        
        hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
        if(SUCCEEDED(hr))
        {
            IUnknown *iitem = NULL;       // collection’s item
            IUPnPService *isrv = NULL;    // service object
            CComBSTR btmp;
            
            // enumerate collection
            icol->Reset();
            while(icol->Next(1, &iitem, NULL) == S_OK)
            {
                // get interface to service object
                hr = iitem->QueryInterface(IID_IUPnPService, (void**)&isrv);
                
                if(SUCCEEDED(hr))
                {
                    // do something with service object
                    // for example get service id
                    btmp.Empty();
                    isrv->get_Id(&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 devote 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? 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 contents of the document. You have to cope 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 document files 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>

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

// path of document from its URL
char 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

char rbuff[16384] = {0};         // receive buffer
int recvbufflen = sizeof(rbuff); // receive buffer length
char reqbuff[4096] = {0};        // request buffer
char host[1024] = {0};           // Host part of HTTP GET command
int b = 0;                       // bytes currently received
int tb = 0;                      // bytes totally received
int datalen = 0;                 // pure requested data length

// prepare Host part of HTTP GET command
u_short port = ntohs(addr.sin_port);
if(port > 0 && port < 65535)
    sprintf(host, "%s:%d", inet_ntoa(addr.sin_addr), port);
else
    strcpy(host, inet_ntoa(addr.sin_addr));

// prepare string of HTTP GET command
sprintf(reqbuff, "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", path, host);

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

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

// send request data
send(s, reqbuff, strlen(reqbuff), 0);

// receive response data - document
while(b = recv(s, rbuff + tb, recvbufflen - tb, 0))
{
    if(b == SOCKET_ERROR)
        break;
    tb += b;
}

// check if response data has correct format
if(tb > 0) // reveive succeeded
{
    char header[4096] = {0};
    // copy first line of header response to header variable
    strncpy(header, rbuff, strcspn(rbuff, "\r\n"));
    // check if response code is 200
    if(strstr(header, "200 OK") != NULL)
    {
        char *databegin = NULL;
        // get pointer to begining of actual document
        databegin = strstr(rbuff, "\r\n\r\n") + strlen("\r\n\r\n");
        if(databegin != NULL)
        {
            // length of document
            datalen = tb - (databegin - rbuff);
            if(datalen > 0)
            {
                // do something with document in buffer (rbuff)
                // databegin points to begining of document in rbuff
                // and datalen is length of document in bytes
            }
        }
    }
}

// close connection
shutdown(s, SD_SEND);
while(b = recv(s, rbuff, recvbufflen, 0))
    if(b == SOCKET_ERROR) break;
closesocket(s);

// 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. Earlier, in examples, I used ATL type - CComBSTR, which wraps BSTR type. Application of ATL types considerably simplifies the code and makes it clearer.

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 item from the list of port mappings of the router. This action has three input arguments and returns five values containing data of list item. 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

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

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

#include <atlbase.h>        // CComBSTR
#include <atlcomcli.h>      // CComVariant
#include <atlsafe.h>        // CComSafeArray
#include <upnp.h>

HRESULT hr = S_OK;
CComVariant    vaInArgs;                 // Invoke argument in
// set type of VARIANT, must be array of VARIANTs
vaInArgs.vt = VT_ARRAY | VT_VARIANT | VT_BYREF;
CComVariant vaOutArgs;                   // Invoke argument out
CComVariant vaRetVal;                    // Invoke argument ret
CComVariant vaInVal;                     // temp input value
CComSafeArray<VARIANT> psaInArgs;  // array for input arguments of action
CComVariant vaOutVal;                    // temp output value
CComSafeArray<VARIANT> psaOutArgs; // array for output values of action

// create input array, default elements=0 and lbound=0
// will be destroyed automatically when goes out of scope
hr = psaInArgs.Create();

if(SUCCEEDED(hr))
{
    // fill input array, there is three arguments
    
    // first argument of type string (VT_BSTR) – remote host
    vaInVal.vt = VT_BSTR; // set type
    vaInVal.bstrVal = ::SysAllocString(L””); // may be empty string
    psaInArgs.Add(vaInVal); // add argument to input array
    vaInVal.Clear();
    
    // second argument of type ui2 (VT_UI2) – external port
    vaInVal.vt = VT_UI2; // set type
    vaInVal.uiVal = 8839; // external port number
    psaInArgs.Add(vaInVal);
    vaInVal.Clear();
    
    // third argument of type string (VT_BSTR) – port mapping protocol
    vaInVal.vt = VT_BSTR; // set type
    vaInVal.bstrVal = ::SysAllocString(L”TCP”); // protocol, one of allowed values
    psaInArgs.Add(vaInVal); // add argument to input array
    
    // assign input array to input argument of InvokeAction
    vaInArgs.pparray = psaInArgs.GetSafeArrayPtr();
    
    // action name
    CComBSTR actionName(L”GetSpecificPortMappingEntry”);
    
    // invoke action
    hr = isrv->InvokeAction(actionName, vaInArgs, &vaOutArgs, &vaRetVal);
    
    if(SUCCEEDED(hr))
    {
        // process data from vaOutArgs – action’s output values
        // get array of output values of action
        hr = psaOutArgs.Attach(vaOutArgs.parray);
        
        if(SUCCEEDED(hr))
        {
            // count of output values
            int arrlen = psaOutArgs.GetCount();
            Cstring strTemp; // temporary string for returned values
            
            // read successive returned values of action
            for(int i = 0; i < arrlen; ++i)
            {
                vaOutVal.Clear();
                vaOutVal = psaOutArgs[i]; // get array item (value)
                if(vaOutVal.vt != VT_EMPTY)
                {
                    hr = vaOutVal.ChangeType(VT_BSTR);
                    if(SUCCEEDED(hr))
                    {
                        // write value to temp string
                        strTemp = vaOutVal;
                        // do something with returned value
                    }
                }
            }
            
            psaOutArgs.Detach();
        }
        
        // data from vaRetVal
        if(vaRetVal.vt != VT_EMPTY)
        {
            hr = vaRetVal.ChangeType(VT_BSTR);
            if(SUCCEEDED(hr))
            {
                Cstring strRetVal(vaRetVal);
                // do something with returned value
            }
        }
    }
    else
    {
        // read error code from InvokeAction
    }
}

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 <atlbase.h>
#include <atlcom.h>    // CComObjectRootEx
#include <upnp.h>      // IUPnPServiceCallback

class ServiceEventCallback :
public CComObjectRootEx<CComSingleThreadModel>,
public IUPnPServiceCallback
{
    public:
    ServiceEventCallback() {};
    ~ServiceEventCallback() {};
    
    BEGIN_COM_MAP(ServiceEventCallback)
        COM_INTERFACE_ENTRY(IUPnPServiceCallback)
    END_COM_MAP()
    
    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 *)

// for CComObject::CreateInstance
// declare this variable as global in your source file
CComModule _Module;

CComObject<ServiceEventCallback> *cback = NULL;
// if returned pointer is equal to NULL
// it means then error occured in function CreateInstance
CComObject<ServiceEventCallback>::CreateInstance(&cback);
if(cback != NULL)
{
    // ref count 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 Win32 applications as well as the MFC applications. The 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. 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 source files.

UPnPCpLib library contains three main classes defining the three 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.

Besides, contains two helper classes:

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

The general scheme of relationships and functions of library’s objects.

FindManager class

The FindManager class is used to manage the Device Finder object. Automatically creates and destroys the Finder object and controls the process of discovering.

class FindManager : public IFinderCallbackClient, IProcessDevice
{
public:
    FindManager();
    ~FindManager();
    
    void CreateCallback(IFinderCallbackClient* client);
    void ReleaseCallback();
    bool IsReady();
    bool Start(CComBSTR devicetype = L"upnp:rootdevice");
    bool Stop();
    DevicesArray* GetCollection();
    void SetClientPtr(IFinderManagerClient* client);
    void SetServiceEventClientPtr(IServiceCallbackClient* client);
    
    // IProcessDevice implementation
    virtual void ProcessDevice(Device* dev, void* param, int procid);
    
    // IFinderCallbackClient implementation
    virtual void DeviceAdded(long finddata, IUPnPDevice* idev);
    virtual void DeviceRemoved(long finddata, BSTR devname);
    virtual void SearchComplete(long finddata);
    
private:
    DevFinderCallback* _findercallback; // IUPnPDeviceFinderCallback *
    IUPnPDeviceFinder* _ifinder;        // Device Finder
    long _finderhandle;                 // Device Finder’s find handle
    DevicesArray _devs;                 // devices collection
    IFinderManagerClient* _findermanagerclient;
    IServiceCallbackClient* _srveventclient;
};

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 FindManager object to FindManager::CreateCallback function. To gain access to internal collection the GetCollection function 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. To connect FindManager with its client you can use the SetClientPtr function, to which should be passed the pointer to client object. 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 CreateCallback 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 Device
{
public:
    Device(CComBSTR udname = L"root", Device* parentdev = 0);
    ~Device();
    
    void         GetUDN(BSTR* udname);
    void         GenerateFriendlyName();
    CString      GetFriendlyName();
    Service*     AddService(CComBSTR srvname);
    int          GetServiceListCount();
    POSITION     GetFirstServicePosition();
    Service*     GetNextService(POSITION& pos);
    Device*      AddDevice(CComBSTR devname);
    int          GetDeviceListCount();
    POSITION     GetFirstDevicePosition();
    Device*      GetNextDevice(POSITION& pos);
    Device*      GetParentDevice();
    Device*      GetRootDevice();
    bool         IsRoot();
    void         RemoveAllDevices();
    void         RemoveAllServices();
    void         AddInterface(IUPnPDevice* idev);
    HRESULT      GetInterface(IUPnPDevice** idev);
    void         SetIconsList(IconsList* ilist);
    IconsList*   GetIconsList();