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
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 (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 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.
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.
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.
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)
{
}
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 ;).
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).
#include <upnp.h>
class DevFinderCallback : public IUPnPDeviceFinderCallback
{
private:
long _refcount;
public:
DevFinderCallback() {_refcount = 0;}
virtual ~DevFinderCallback() {}
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;
}
return _refcount;
}
virtual HRESULT __stdcall DeviceAdded(long findid, IUPnPDevice* idev)
{
return S_OK;
}
virtual HRESULT __stdcall DeviceRemoved(long findid, BSTR devudn)
{
return S_OK;
}
virtual HRESULT __stdcall SearchComplete(long findid)
{
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).
DevFinderCallback* cback = new DevFinderCallback();
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):
#include <upnp.h>
HRESULT hr;
IUPnPDeviceFinder* _ifinder = 0;
hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER,
IID_IUPnPDeviceFinder, (void**)&_ifinder);
if(hr == S_OK)
{
}
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:
#include <upnp.h>
HRESULT hr;
IUPnPDeviceFinder* _ifinder = 0;
std::wstring devtypestr(L"upnp:rootdevice");
BSTR devtype = SysAllocString(devtypestr.c_str());
long _findhandle;
hr = CoCreateInstance(CLSID_UPnPDeviceFinder, 0, CLSCTX_SERVER,
IID_IUPnPDeviceFinder, (void**)&_ifinder);
if(hr == S_OK)
{
hr = _ifinder->CreateAsyncFind(devtype, 0, cback, &_findhandle);
if(hr == S_OK)
{
hr = _ifinder->StartAsyncFind(_findhandle);
if(hr == S_OK)
{
}
}
}
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.
if(cback != 0)
cback->Release();
if(_ifinder != 0)
{
_ifinder->CancelAsyncFind(_finderhandle);
_ifinder->Release();
}
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.
#include <upnp.h>
void EnumerateDevices(IUPnPDevice* _idevice)
{
IUPnPDevices* children = 0;
VARIANT_BOOL bcheck = 0;
if(_idevice->get_HasChildren(&bcheck) == S_OK && bcheck != 0)
{
if(_idevice->get_Children(&children) == S_OK)
{
HRESULT hr = S_OK;
IUnknown* ienum = 0;
long devscount = 0;
children->get_Count(&devscount);
if(children->get__NewEnum(&ienum) == S_OK)
{
IEnumUnknown* icol = 0;
hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
if(hr == S_OK)
{
IUnknown* iitem = 0;
IUPnPDevice* ichild = 0;
icol->Reset();
while(icol->Next(1, &iitem, 0) == S_OK)
{
hr = iitem->QueryInterface(IID_IUPnPDevice, (void**)&ichild);
if(hr == S_OK)
{
EnumerateDevices(ichild);
ichild->Release();
ichild = 0;
}
iitem->Release();
iitem = 0;
}
icol->Release();
}
ienum->Release();
}
children->Release();
}
}
}
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.
#include <upnp.h>
void GetDeviceDocumentAccessURL(IUPnPDevice* idev)
{
HRESULT hr;
IUPnPDeviceDocumentAccess* idoc = 0;
BSTR btmp = 0;
hr = idev->QueryInterface(IID_IUPnPDeviceDocumentAccess, (void**)&idoc);
if(hr == S_OK)
{
idoc->GetDocumentURL(&btmp);
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.
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.
#include <upnp.h>
void EnumerateServices(IUPnPDevice* idev)
{
HRESULT hr = S_OK;
long srvcount = 0;
IUnknown* ienum = 0;
IUPnPServices* isrvs = 0;
if(idev->get_Services(&isrvs) != S_OK)
return;
isrvs->get_Count(&srvcount);
if(isrvs->get__NewEnum(&ienum) == S_OK)
{
IEnumUnknown *icol = 0;
hr = ienum->QueryInterface(IID_IEnumUnknown, (void**)&icol);
if(hr == S_OK)
{
IUnknown* iitem = 0;
IUPnPService* isrv = 0;
BSTR btmp = 0;
icol->Reset();
while(icol->Next(1, &iitem, 0) == S_OK)
{
hr = iitem->QueryInterface(IID_IUPnPService, (void**)&isrv);
if(hr == S_OK)
{
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,
VARIANT varInActionArgs,
VARIANT* pvarOutActionArgs,
VARIANT* pvarRetVal
);
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.
#pragma comment(lib, "ws2_32") // required linked library
#include <winsock2.h>
#include <iostream>
using namespace std;
WSADATA wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
string path("/document.xml");
sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(56616);
const int rbsize = 4096;
char rbuff[rbsize] = {0};
int rbshift = 0;
int b = 0;
int tb = 0;
string respbuff;
string headertail("\r\n\r\n");
string document;
ostringstream os;
os << "GET " << path << " HTTP/1.1\r\nHost: " << inet_ntoa(addr.sin_addr)
<< ':' << ntohs(addr.sin_port)
<< headertail;
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(s, (sockaddr*)&_addr, sizeof(sockaddr_in));
b = send(s, os.str().c_str(), os.str().length(), 0);
while((b = recv(s, rbuff + rbshift, rbsize - rbshift, 0)) != SOCKET_ERROR)
{
if(b == 0)
break;
tb += b;
rbshift += b;
if(rbshift == rbsize)
{
respbuff.append(rbuff, rbshift);
rbshift = 0;
}
}
shutdown(s, SD_SEND);
while(int bc = recv(s, rbuff, rbsize, 0))
if(bc == SOCKET_ERROR) break;
closesocket(s);
if(tb > 0)
{
if(rbshift > 0)
respbuff.append(rbuff, rbshift);
if(respbuff.substr(0, respbuff.find("\r\n")).find("200 OK") != string::npos)
{
int cntlen_comp = 0;
int cntlen_get = 0;
string::size_type pos;
string::size_type posdata = respbuff.find(headertail);
if(posdata != string::npos)
{
cntlen_comp = tb - (posdata + headertail.length());
if(cntlen_comp > 0)
{
if(b == 0)
{
cntlen_get = cntlen_comp;
}
else
{
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(cntlen_comp == cntlen_get)
document.assign(respbuff.begin() + posdata + headertail.length(), respbuff.end());
}
}
}
}
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.
#include <upnp.h>
vector< pair<unsigned short, wstring> > _in;
typedef vector< pair<unsigned short, wstring> >::const_iterator ArgsIterator;
vector< pair<unsigned short, wstring> > _out;
_in.push_back(pair<unsigned short, wstring>(VT_BSTR, L""));
_in.push_back(pair<unsigned short, wstring>(VT_UI2, L"8839"));
_in.push_back(pair<unsigned short, wstring>(VT_BSTR, L"TCP"));
BSTR actionName = SysAllocString(L"GetSpecificPortMappingEntry");
HRESULT hr = S_OK;
long inargscount = 0;
VARIANT inargs;
VARIANT outargs;
VARIANT retval;
VARIANT inval;
SAFEARRAY* arr_inargs = 0;
VARIANT outval;
SAFEARRAY* arr_outargs = 0;
long arr_index[1];
inargscount = _in.size();
VariantInit(&inargs);
VariantInit(&outargs);
VariantInit(&retval);
SAFEARRAYBOUND bounds[1];
bounds[0].lLbound = 0;
bounds[0].cElements = inargscount;
arr_inargs = SafeArrayCreate(VT_VARIANT, 1, bounds);
if(arr_inargs != 0)
{
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());
if(V_BSTR(&inval) != 0)
{
hr = S_OK;
VARTYPE vt = (*argi).first;
if(vt != VT_BSTR)
hr = VariantChangeType(&inval, &inval, VARIANT_NOUSEROVERRIDE, vt);
if(hr == S_OK)
SafeArrayPutElement(arr_inargs, arr_index, (void*)&inval);
}
VariantClear(&inval);
}
inargs.vt = VT_ARRAY | VT_VARIANT;
V_ARRAY(&inargs) = arr_inargs;
hr = _iservice->InvokeAction(actionName, inargs, &outargs, &retval);
if(hr == S_OK)
{
arr_outargs = V_ARRAY(&outargs);
long arrsize = 0;
VARTYPE vartype;
if(SafeArrayGetUBound(arr_outargs, 1, &arrsize) == S_OK)
{
for(long i = 0; i <= arrsize; ++i)
{
arr_index[0] = i;
VariantInit(&outval);
hr = SafeArrayGetElement(arr_outargs, arr_index, (void*)&outval);
if(hr == S_OK)
{
vartype = outval.vt;
if(outval.vt != VT_BSTR)
hr = VariantChangeType(&outval, &outval, VARIANT_ALPHABOOL, VT_BSTR);
if(hr == S_OK)
_out.push_back(pair<unsigned short, wstring>(vartype, V_BSTR(&outval)));
}
VariantClear(&outval);
}
}
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
{
}
SafeArrayDestroy(arr_inargs);
inargs.vt = VT_EMPTY;
}
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.
#include <upnp.h>
class SrvEventCallback : public IUPnPServiceCallback
{
private:
long _refcount;
public:
SrvEventCallback() : _refcount(0) {}
virtual ~SrvEventCallback() {}
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;
}
return _refcount;
}
virtual HRESULT _stdcall StateVariableChanged(IUPnPService* isrv, LPCWSTR varname, VARIANT varvalue)
{
return S_OK;
}
virtual HRESULT _stdcall ServiceInstanceDied(IUPnPService* isrv)
{
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.
SrvEventCallback* cback = new SrvEventCallback();
if(cback != 0)
{
cback->AddRef();
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.
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.
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.
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.
class FindManager : public IFinderCallbackClient, public IProcessDevice
{
public:
FindManager();
virtual ~FindManager();
bool Init(IFinderManagerClient* client, const wstring& devicetype = L"upnp:rootdevice");
bool Init(IFinderCallbackClient* client, const wstring& devicetype = L"upnp:rootdevice");
bool Start();
bool Stop();
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;
long GetFindId();
void SetServiceEventClientPtr(IServiceCallbackClient* client);
void Lock();
void UnLock();
static const wchar_t* device_type[];
static wstring GetRootDeviceType();
static int TypesCount();
private:
bool Init(const wstring& devicetype);
void ReleaseCallback();
virtual void ProcessDevice(const Device* dev, void* param, int procid);
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;
IUPnPDeviceFinder* _ifinder;
DeviceArray _devs;
long _finderhandle;
IFinderManagerClient* _findermanagerclient;
IFinderCallbackClient* _findercallbackclient;
IServiceCallbackClient* _srveventclient;
bool _externalcollection;
CRITICAL_SECTION _cs;
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
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:
explicit Device(IUPnPDevice* idev, const Device* parentdev = 0);
~Device();
wstring GetUDN() const;
wstring GetFriendlyName() const;
wstring GetType() const;
int GetServiceListCount() const;
ServiceIterator GetServiceListBegin() const;
ServiceIterator GetServiceListEnd() const;
const Service* GetService(unsigned int index) const;
int GetDeviceListCount() const;
DeviceIterator GetDeviceListBegin() const;
DeviceIterator GetDeviceListEnd() const;
const Device* GetDevice(unsigned int index) const;
bool IsDeviceListEmpty() const;
int GetIconsListCount() const;
IconIterator GetIconsListBegin() const;
IconIterator GetIconsListEnd() const;
const IconParam& GetIcon(unsigned int index) const;
bool IsIconsListEmpty() const;
wstring GetDeviceIconURL(int iwidth, int iheight, int idepth) const;
const Device* GetParentDevice() const;
const Device* GetRootDevice() const;
bool IsRoot() const;
void GetInterface(IUPnPDevice** idev) const;
const DocAccessData* GetAccessData() const;
wstring GetDevDocAccessURL() const;
wstring GetDeviceDocument() const;
bool GetDeviceInfo(InfoData& data) const;
void EnumerateDevices(IProcessDevice* iproc, void* param, int procid) const;
private:
bool EnumDev();
bool EnumSrv();
void AddDevice(IUPnPDevice* idev);
void AddService(IUPnPService* isrv);
bool SetUDN();
void GenerateFriendlyName();
wstring GetDocURL() const;
bool SetType();
void RemoveAllDevices();
void RemoveAllServices();
private:
IUPnPDevice* _idevice;
const Device* _parent;
wstring _udn;
wstring _name;
wstring _type;
ServiceList _services;
DeviceList _devices;
DocAccessData _accessdata;
IconList _icons;
};
The structure of the object of Device
class.
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.
The Service
class represents the object of service of the UPnP device.
class Service
{
friend class Device;
public:
~Service();
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;
long GetLastTransportStatus() const;
const Device& GetParentDevice() const;
void GetInterface(IUPnPService** isrv) const;
bool SetCallbackClient(IServiceCallbackClient* iclient);
wstring GetScpdURL() const;
wstring GetScpdContent() const;
const DocAccessData* GetAccessData() const;
bool GetServiceInfo(InfoData& data) const;
bool GetServiceVariables(VarData& data) const;
private:
Service(IUPnPService* isrv, const Device& parentdev);
bool SetAccessData();
bool EnumActions();
bool SetServiceID();
wstring _name;
wstring _typeid;
ActionList _actions;
const Device& _parent;
IUPnPService* _iservice;
DocAccessData _accessdata;
SrvEventCallback* _isrvcback;
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
object contains informations about one's input arguments and has function Invoke
which invokes appropriate action on parent service.
class Action
{
friend class Service;
public:
wstring GetName() const;
const Service& GetParentService() const;
int GetArgsCount();
int GetInArgsCount();
bool GetInfo(InfoDataList& inflist) const;
bool SetInArgs(const StrList& args);
bool SetInArgs(const wstring& arg, unsigned int index);
bool GetInArgs(ArgsArray& args);
bool GetInArgs(InfoDataItem& arg, unsigned int index);
int Invoke(ArgsArray& argsout) const;
private:
wstring _name;
const Service* _parent;
int _argcount;
int _inargcount;
bool _complete;
bool _inargscomplete;
ArgsArray _in;
bool SetArgsCount();
bool InitInArgsList();
Action(const Service* srv, const wstring& name);
};
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, 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.
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.
#define _WIN32_DCOM
#include <iostream>
#include "upnpcplib.h"
using namespace UPnPCpLib;
using namespace std;
class FinderClient : public IFinderManagerClient, public IServiceCallbackClient, public IProcessDevice
{
public:
FinderClient()
: _searching(false)
{
_fm = new FindManager();
_fm->Init(this);
_fm->SetServiceEventClientPtr(this);
wcout << L"\nFinderClient was created successfully" << flush;
}
~FinderClient()
{
if(IsSearching())
StopSearch();
delete _fm;
PostQuitMessage(0);
wcout << L"\nFinderClient has been destroyed" << flush;
}
bool StartSearch()
{
return (_searching = _fm->Start());
}
bool StopSearch()
{
return !(_searching = !_fm->Stop());
}
bool IsSearching() {return _searching;}
void InvokeAction(unsigned int devindex, const wstring& actionName, const StrList& args)
{
_action = 0;
const Device* dev = _fm->GetDevice(devindex);
dev->EnumerateDevices(this, (void*)&actionName, 0);
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;
wcout << result;
}
else
wcout << L"\nAction not found" << endl;
}
private:
virtual void ProcessDevice(const Device* dev, void* param, int procid)
{
const wstring& actname = *(const wstring*)param;
ServiceIterator srvi = dev->GetServiceListBegin();
int srvcount = dev->GetServiceListCount();
for(int i = 0; i < srvcount; ++i, ++srvi)
{
try
{
_action = &(*srvi)->GetAction(actname);
break;
}
catch(...)
{
}
}
}
virtual void OnStartFindDevice(long findid)
{
wcout << L"\nSearching started" << flush;
}
virtual void OnStopFindDevice(long findid, bool iscancelled)
{
wcout << L"\nSearching stopped because of " << (iscancelled ? L"cancel" : L"search complete") << flush;
if(!iscancelled)
{
wcout << L"\nSearching will be cancelled" << flush;
this->StopSearch();
}
}
virtual void OnAddDevice(long findid, const Device* dev, int devindex)
{
wcout << L"\nDevice added: " << dev->GetFriendlyName() ;
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)
{
wcout << L"\nDevice removed: " << friendlyname << L"\nSearching is being continued" << flush;
}
virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue)
{
wstring devparent = srv->GetParentDevice().GetFriendlyName();
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)
{
wcout << L"\nService died: " << srv->GetServiceID()
<< "\nIt was member of device: " << srv->GetParentDevice().GetFriendlyName()
<< "\nSearching is being continued" << flush;
}
private:
bool _searching;
FindManager* _fm;
const Action* _action;
};
int main()
{
if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK)
{
CoUninitialize();
return -1;
}
wcout << L"\nCOM library initialized";
WSADATA wdata;
WSAStartup(MAKEWORD(2,2), &wdata);
wcout << L"\nWinsock library initialized";
wcout << L"\nCreating FinderClient object" << flush;
FinderClient* fclnt = new FinderClient();
wcout << L"\nStart finding\n" << flush;
if(fclnt->StartSearch())
{
MSG Message;
while(GetMessage(&Message, 0, 0, 0))
{
DispatchMessage(&Message);
wcout << '.' << flush;
if(!fclnt->IsSearching())
{
wstring name;
StrList args;
name = L"GetExternalIPAddress";
fclnt->InvokeAction(0, name, args);
name = L"GetGenericPortMappingEntry";
args.push_back(L"0");
fclnt->InvokeAction(0, name, args);
wcout << L"\nFinderClient finished work and will be destroyed\n" << flush;
delete fclnt;
fclnt = 0;
}
}
}
delete fclnt;
wcout << L"\nCleaning up Winsock library";
WSACleanup();
wcout << L"\nCleaning up 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:
Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
Manufacturer = LINKSYS
Manufacturer URL = http:
Model = Wireless-G ADSL Home Gateway
Model URL = http:
Model number = WAG200G
Presentation URL = http:
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:
#define _WIN32_DCOM
#include <iostream>
#include "upnpcplib.h"
using namespace UPnPCpLib;
using namespace std;
class FinderHost : public IFinderCallbackClient, public IServiceCallbackClient, public IProcessDevice
{
public:
FinderHost()
: _searching(false)
, _finish(false)
{
_fm = new FindManager();
_fm->Init(this);
wcout << L"FinderHost was created successfully" << endl;
}
~FinderHost()
{
if(IsSearching())
StopSearch();
RemoveAllDevices();
delete _fm;
wcout << L"\nFinderHost has been destroyed" << endl;
}
bool StartSearch()
{
return (_searching = _fm->Start());
}
bool StopSearch()
{
return !(_searching = !_fm->Stop());
}
bool IsSearching() {return _searching;}
void SetFinish() {_finish = true;}
bool IsFinished() {return _finish;}
void Lock() {}
void UnLock() {}
void InvokeAction(int devindex, const wstring& actionName, const StrList& args)
{
_action = 0;
const Device* dev = _devs[devindex];
dev->EnumerateDevices(this, (void*)&actionName, PID_FIND_ACTION);
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;
wcout << result << endl;
}
else
wcout << L"\nAction not found" << endl;
}
private:
static const int PID_ADD_SERVICE_CALLBACK = 0;
static const int PID_FIND_ACTION = 1;
virtual void ProcessDevice(const Device* dev, void* param, int procid)
{
switch(procid)
{
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;
case PID_FIND_ACTION:
{
const wstring& actname = *(const wstring*)param;
ServiceIterator srvi = dev->GetServiceListBegin();
int srvcount = dev->GetServiceListCount();
for(int i = 0; i < srvcount; ++i, ++srvi)
{
try
{
_action = &(*srvi)->GetAction(actname);
break;
}
catch(...)
{
}
}
}
break;
}
}
virtual void DeviceAdded(long findid, IUPnPDevice* idev)
{
Device* dev = new Device(idev);
dev->EnumerateDevices(this, 0, PID_ADD_SERVICE_CALLBACK);
_devs.push_back(dev);
wcout << L"\nNew device added: " << dev->GetFriendlyName() << endl;
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)
{
for(DeviceArray::size_type i = 0; i < _devs.size(); ++i)
{
if(devname == _devs[i]->GetUDN())
{
wstring friendlyname = _devs[i]->GetFriendlyName();
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)
{
wcout << L"\nSearch is complete" << endl;
wcout << L"Searching will be cancelled" << endl;
this->StopSearch();
}
virtual void ServiceEventVariableChanged(const Service* srv, const wstring& varname, const wstring& varvalue)
{
wstring devparent = srv->GetParentDevice().GetFriendlyName();
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;
if(_finish)
PostQuitMessage(0);
}
virtual void ServiceEventInstanceDied(const Service* srv)
{
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;
bool _finish;
FindManager* _fm;
DeviceArray _devs;
const Action* _action;
};
int _tmain(int argc, _TCHAR* argv[])
{
if(CoInitializeEx(0, COINIT_APARTMENTTHREADED) != S_OK)
{
CoUninitialize();
return -1;
}
wcout << L"COM library initialized" << endl;
WSADATA wdata;
WSAStartup(MAKEWORD(2, 2), &wdata);
wcout << L"Winsock library initialized\n"
L"Creating FinderHost object" << endl;
FinderHost* fhost = new FinderHost();
wcout << L"Start finding" << flush;
if(fhost->StartSearch())
{
MSG Message;
while(GetMessage(&Message, NULL, 0, 0))
{
DispatchMessage(&Message);
wcout << '.' << flush;
if(!fhost->IsSearching() && !fhost->IsFinished())
{
wcout << L"\nSearching has been cancelled successfully" << endl;
wstring name;
StrList args;
name = L"AddPortMapping";
args.push_back(L"any");
args.push_back(L"11200");
args.push_back(L"UDP");
args.push_back(L"11200");
args.push_back(L"192.168.0.10");
args.push_back(L"true");
args.push_back(L"add port mapping example");
args.push_back(L"0");
fhost->InvokeAction(0, name, args);
name = L"DeletePortMapping";
args.clear();
args.push_back(L"any");
args.push_back(L"11200");
args.push_back(L"UDP");
fhost->InvokeAction(0, name, args);
wcout << L"\nFinderHost finished work and will be destroyed" << endl;
fhost->SetFinish();
}
}
}
delete fhost;
wcout << L"Cleaning up Winsock library" << endl;
WSACleanup();
wcout << L"Cleaning up COM library" << endl;
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:
Friendly name = Wireless-G ADSL Home Gateway (uuid:8c59ad37-1dd2-11b2-ada1-0018398b6752)
Manufacturer = LINKSYS
Manufacturer URL = http:
Model = Wireless-G ADSL Home Gateway
Model URL = http:
Model number = WAG200G
Presentation URL = http:
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
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.
Type of devices which will be searched can be specified with the help of the filter.
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.
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).
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.
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.
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() | 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. |
Device[int] | Returns device object from collection at specified index. |
CollectionSize | Gets number of devices in collection. |
IsCollectionEmpty | Checks if there are any devices in collection. |
FindId | Gets search session identifier. |
DeviceTypeCount | Gets number of names of devices types in array. (static) |
DeviceTypes | Returns array of names of devices types. (static) |
RootDeviceType | Gets name of basic type of device. (static) |
DeviceType[int] | Gets name of type at specified index in array. (static) |
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. |
Start | Starts searching. Returns true if succeeded. |
Stop | Cancels searching. Returns true if succeeded. |
GetEnumerator | Returns enumerator for devices collection. |
GetCollection | Returns collection of detected devices. |
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. |
OnServiceVariableChanged | Occurs 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. |
OnServiceInstanceDied | Occurs when service is not responding. Passes to handler argument of type ServiceInstanceDiedArgs which contains service object. |
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. |
CheckSystemServiceState | Checks 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). |
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. |
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). |
EnumerateDevices | Processes 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. |
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. |
Action[int] | Gets action object at specified index in array of actions. |
Action[String] | Gets action object about specified name in array of actions. |
ActionsCount | Gets number of actions. |
StateVariables | Gets 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. |
GetEnumerator | Returns enumerator for actions collection. |
GetActions | Returns array of actions objects. |
GetScpdContent | Returns content of description document. |
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. |
Invoke | Invokes 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. |
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
{
[STAThread]
static void Main()
{
}
}
Setting of COM threading model for C# application - multithreaded (in file Program.cs).
static class Program
{
[MTAThread]
static void 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;
}
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);
}
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
{
FindManagerNet _fm;
ServiceNet _srv;
public Form1()
{
InitializeComponent();
_fm = new FindManagerNet(SynchronizationContext.Current);
_fm.OnServiceVariableChanged += new ServiceVariableChangedEvent(_fm_OnServiceVariableChanged);
_fm.OnAddDevice += new AddDeviceEvent(_fm_OnAddDevice);
}
void _fm_OnAddDevice(object sender, AddDeviceArgs e)
{
_fm.Stop();
DeviceNet dev = e.Device;
string requestedService = "urn:upnp-org:serviceId:WANPPPConn1";
dev.OnProcessDevice += new ProcessDeviceEvent(dev_OnProcessDevice);
dev.EnumerateDevices(requestedService, 0);
dev.OnProcessDevice -= new ProcessDeviceEvent(dev_OnProcessDevice);
}
void dev_OnProcessDevice(object sender, ProcessDeviceArgs e)
{
string requestedService = (string)e.Param;
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)
{
if(e.VarName == "PortMappingNumberOfEntries")
labelNumber.Text = e.VarValue;
}
private void button1_Click(object sender, EventArgs e)
{
_fm.Init(FindManagerNet.DeviceTypes[4]);
_fm.Start();
}
private void button2_Click(object sender, EventArgs e)
{
_fm.Stop();
}
private void button3_Click(object sender, EventArgs e)
{
if (_srv != null)
{
ActionNet act = _srv.get_Action("AddPortMapping");
act.ArgumentsIn = new List<string> {"any", "11200", "UDP", "11200", "10.0.0.2", "true", "test add entry", "0"};
List<KeyValuePair<string, string>> outs = null;
act.Invoke(ref outs);
}
}
private void button4_Click(object sender, EventArgs e)
{
if (_srv != null)
{
ActionNet act = _srv.get_Action("DeletePortMapping");
act.ArgumentsIn = new List<string> { "any", "11200", "UDP" };
List<KeyValuePair<string, string>> outs = null;
act.Invoke(ref outs);
}
}
}
}
- 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.
- UPnP APIs, MSDN, http://msdn.microsoft.com/en-us/library/aa382303(VS.85).aspx[^]
- NAT API, MSDN, http://msdn.microsoft.com/en-us/library/aa366187(VS.85).aspx[^]
- UPnP Forum, http://www.upnp.org/[^]
- Using UPnP for Programmatic Port Forwardings and NAT Traversal, Mike O'Neill, PortForward.aspx[^]
- RFC 1067 (SNMP), IETF, http://tools.ietf.org/html/rfc1067[^]
- RFC 3489 (STUN), IETF, http://tools.ietf.org/html/rfc3489[^]
- SSDP, IETF, http://tools.ietf.org/html/draft-cai-ssdp-v1-03[^]
- Description of Universal Plug and Play Features in Windows XP, Microsoft, http://support.microsoft.com/kb/323713[^]
- Universal Plug and Play in Windows XP, MS TechNet, http://technet.microsoft.com/en-us/library/bb457049.aspx[^]
- How Windows Firewall affects the UPnP framework in Windows XP Service Pack 2, Microsoft, http://support.microsoft.com/kb/886257[^]
- Intel Software for UPnP Technology, Intel, http://www.intel.com/cd/ids/developer/asmo-na/eng/downloads/upnp/index.htm[^]
- CMarkup software, First Objective Software, Inc., http://www.firstobject.com/dn_markup.htm[^]