Generally, building a UPnP device with HUPnP involves two main steps in your part. First, you have to define a UPnP device description document following the specifications set by the UPnP forum. Depending of your UPnP Device Description document, you may need to define one or more UPnP service description documents as well. Second, you have to implement a class for your device and most often one or more classes for each service of your device.
For example, if you want to implement a standard UPnP device named BinaryLight:1, your device description could look something like this:
<?xml version="1.0"?> <root xmlns="urn:schemas-upnp-org:device-1-0"> <specVersion> <major>1</major> <minor>0</minor> </specVersion> <device> <deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType> <friendlyName>UPnP Binary Light</friendlyName> <manufacturer>MyCompany</manufacturer> <manufacturerURL>www.mywebsite.org</manufacturerURL> <modelDescription>New brilliant BinaryLight</modelDescription> <modelName>SuperWhiteLight 4000</modelName> <modelNumber>1</modelNumber> <UDN>uuid:138d3934-4202-45d7-bf35-8b50b0208139</UDN> <serviceList> <service> <serviceType>urn:schemas-upnp-org:service:SwitchPower:1</serviceType> <serviceId>urn:upnp-org:serviceId:SwitchPower:1</serviceId> <SCPDURL>switchpower_scpd.xml</SCPDURL> <controlURL>/control</controlURL> <eventSubURL>/eventing</eventSubURL> </service> </serviceList> </device> </root>
Note that the above is the standard device template for UPnP BinaryLight:1 filled with imaginary information.
Since the BinaryLight:1 defines a service, SwitchPower:1, you have to provide a service description document that could look like this:
<?xml version="1.0"?> <scpd xmlns="urn:schemas-upnp-org:service-1-0"> <specVersion> <major>1</major> <minor>0</minor> </specVersion> <actionList> <action> <name>SetTarget</name> <argumentList> <argument> <name>newTargetValue</name> <relatedStateVariable>Target</relatedStateVariable> <direction>in</direction> </argument> </argumentList> </action> <action> <name>GetTarget</name> <argumentList> <argument> <name>RetTargetValue</name> <relatedStateVariable>Target</relatedStateVariable> <direction>out</direction> </argument> </argumentList> </action> <action> <name>GetStatus</name> <argumentList> <argument> <name>ResultStatus</name> <relatedStateVariable>Status</relatedStateVariable> <direction>out</direction> </argument> </argumentList> </action> </actionList> <serviceStateTable> <stateVariable sendEvents="no"> <name>Target</name> <dataType>boolean</dataType> <defaultValue>0</defaultValue> </stateVariable> <stateVariable sendEvents="yes"> <name>Status</name> <dataType>boolean</dataType> <defaultValue>0</defaultValue> </stateVariable> </serviceStateTable> </scpd>
The above description is the standard service description for the SwitchPower:1 without any vendor specific declarations. For more information about description documents, see the UDA specification, sections 2.3 and 2.5.
HUPnP requires device and service descriptions to be accompanied by corresponding classes. In our example we have to derive a class from Herqq::Upnp::HDevice for the BinaryLight:1 device description and we have to derive a class from Herqq::Upnp::HService for the SwitchPower:1 service description. Note that if your device has no services you do not need to create your own HDevice
type. Similarly, if your service has no actions you do not need to create your own HService
type.
To create a concrete class from Herqq::Upnp::HDevice that exposes services you have to override its protected method Herqq::Upnp::HDevice::createServices(). As the name implies, the purpose of this method is to create HService
objects at run-time that reflect the services defined in the device description.
To create a concrete class from Herqq::Upnp::HService that exposes actions you have to override its protected method Herqq::Upnp::HService::createActions(). The purpose of this method is to create callable entities that will be called when the corresponding UPnP actions are invoked. Note, the UPnP actions of a particular UPnP service are defined in the service's description file and your service implementation has to implement all of them. As a side note, these callable entities are used internally by HUPnP. HUPnP does not expose them directly in the public API.
To continue with the example we have to create two classes, one for the BinaryLight:1 and one for the SwitchPowerService:1. For this example the class declarations are put into the same header file, although in real code you might want to separate them.
mybinarylight.h
#include <HUpnpCore/HDevice> #include <HUpnpCore/HService> class MyBinaryLightDevice : public Herqq::Upnp::HDevice { protected: virtual Herqq::Upnp::HServicesSetupData* createServices(); public: MyBinaryLightDevice(); virtual ~MyBinaryLightDevice(); }; class MySwitchPowerService : public Herqq::Upnp::HService { protected: virtual Herqq::Upnp::HActionsSetupData createActions(); public: MySwitchPowerService(); virtual ~MySwitchPowerService(); };
In turn, the implementation could look something like this:
mybinarylight.cpp
#include "mybinarylight.h" #include <HUpnpCore/HActionsSetupData> #include <HUpnpCore/HServicesSetupData> MyBinaryLightDevice::MyBinaryLightDevice() { } MyBinaryLightDevice::~MyBinaryLightDevice() { } HServicesSetupData* MyBinaryLightDevice::createServices() { Herqq::Upnp::HServicesSetupData* retVal = new Herqq::Upnp::HServicesSetupData(); retVal->insert( new Herqq::Upnp::HServiceSetup( Herqq::Upnp::HServiceId("urn:schemas-upnp-org:serviceId:SwitchPower"), Herqq::Upnp::HResourceType("urn:schemas-upnp-org:service:SwitchPower:1"), new MySwitchPowerService())); // This maps the UPnP service type "urn:schemas-upnp-org:service:SwitchPower:1" // with the standard service ID "urn:schemas-upnp-org:serviceId:SwitchPower" // defined in the device description file to our custom C++ type. // You have to map each UPnP service found in your device description file // to a C++ type derived from Herqq::Upnp::HService. return retVal; } MySwitchPowerService::MySwitchPowerService() { } MySwitchPowerService::~MySwitchPowerService() { } HActionsSetupData MySwitchPowerService::createActions() { Herqq::Upnp::HActionsSetupData retVal; return retVal; }
Those who know UPnP and paid close attention to the above example might have noticed that something was off. Where are the actions?
According to the UPnP Device Architecture (UDA), a service may have zero or more actions. For a device type that has no services or for a device type that has services that in turn have no actions, the above class declarations and definitions are enough.
However, the standard BinaryLight:1 device type specifies the SwitchPower:1 service type that has three actions defined (look back in the service description document). Namely these are SetTarget, GetTarget and GetStatus. To make the example complete the MySwitchPowerService
class requires some additional work. Note that next example shows only one way of making the service complete. There are a few other ways, which will be discussed later in depth.
The complete declaration for MySwitchPowerService
:
mybinarylight.h
#include <HUpnpCore/HService> class MySwitchPowerService : public Herqq::Upnp::HService { protected: virtual Herqq::Upnp::HActionsSetupData createActions(); public: MySwitchPowerService(); virtual ~MySwitchPowerService(); qint32 setTarget( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs); qint32 getTarget( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs); qint32 getStatus( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs); };
The complete definition for MySwitchPowerService
:
mybinarylight.cpp
#include "mybinarylight.h" #include <HUpnpCore/HAction> #include <HUpnpCore/HStateVariable> #include <HUpnpCore/HActionArguments> MySwitchPowerService::MySwitchPowerService() { } MySwitchPowerService::~MySwitchPowerService() { } HService::HActionsSetupData MySwitchPowerService::createActions() { Herqq::Upnp::HActionsSetupData retVal; retVal.insert( Herqq::Upnp::HActionSetup( "SetTarget", Herqq::Upnp::HActionInvoke(this, &MySwitchPowerService::setTarget))); // The above lines map the MySwitchPowerService::setTarget() method to // the action that has the name SetTarget. In essence, this mapping instructs // HUPnP to call this method when the SetTarget action is invoked. // However, note that HActionInvoke accepts any "callable entity", // such as a normal function or a functor. Furthermore, if you use a // method the method does not have to be public. retVal.insert( Herqq::Upnp::HActionSetup( "GetTarget", Herqq::Upnp::HActionInvoke(this, &MySwitchPowerService::getTarget))); retVal.insert( Herqq::Upnp::HActionSetup( "GetStatus", Herqq::Upnp::HActionInvoke(this, &MySwitchPowerService::getStatus))); return retVal; } qint32 MySwitchPowerService::setTarget( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs) { const Herqq::Upnp::HActionArgument* newTargetValueArg = inArgs.get("newTargetValue"); if (!newTargetValueArg) { // If MySwitchPowerService class is not made for direct public use // this check is redundant, since in that case this method is called only by // HUPnP and HUPnP always ensures that the action arguments defined in the // service description are present when an action is invoked. return Herqq::Upnp::HAction::InvalidArgs; } bool newTargetValue = newTargetValueArg->value().toBool(); stateVariableByName("Target")->writable()->setValue(newTargetValue); // The above line modifies the state variable "Target", which reflects the // "target state" of a light device, i.e. if a user wants to turn off a light, the // "target state" is the light turned off whether the light can be turned // off or not. // // Do here whatever that is required to turn on / off the light // (set it to the target state) // // // If it succeeded, we should modify the Status state variable to reflect // the new state of the light. // stateVariableByName("Status")->writable()->setValue(newTargetValue); return Herqq::Upnp::HAction::Success; } qint32 MySwitchPowerService::getTarget( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs) { if (!outArgs) { // See the comments in MySwitchPowerService::setTarget why this // check is here. Basically, this check is redundant if this method // is called only by HUPnP, as HUPnP ensures proper arguments // are always provided when an action is invoked. return Herqq::Upnp::HAction::InvalidArgs; } Herqq::Upnp::HActionArgument* retTargetValue = outArgs->get("RetTargetValue"); if (!retTargetValue) { // See the comments above. The same thing applies here as well. return HAction::InvalidArgs; } bool b = stateVariableByName("Target")->value().toBool(); retTargetValue->setValue(b); return Herqq::Upnp::HAction::Success; } qint32 MySwitchPowerService::getStatus( const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs) { if (!outArgs) { // See the comments in MySwitchPowerService::getTarget(); return HAction::InvalidArgs; } Herqq::Upnp::HActionArgument* resultStatus = outArgs->get("ResultStatus"); if (!resultStatus) { // See the comments above. The same thing applies here as well. return Herqq::Upnp::HAction::InvalidArgs; } bool b = stateVariableByName("Status")->value().toBool(); resultStatus->setValue(b); return Herqq::Upnp::HAction::Success; }
First of all, you may want to skim the discussion in Device Model and Device Hosting to fully understand the comments in the example above. That being said, perhaps the most important issues of building a custom UPnP device using HUPnP can be summarized to:
HDevice
if your device has one or more services and you need to derice from HService
if your service has one or more actions. This is the most common scenario.HDevice
and HService
classes just to be hosted in a Herqq::Upnp::HDeviceHost. Such classes exist only to run your code when UPnP control points interact with them over the network. These types of classes are to be used directly only by HDeviceHost
.HDevice
and HService
classes perhaps to build a higher level public API or just to provide yourself a nicer interface for doing something. This was the case with MySwitchPowerService
class, which extended the HService
interface by providing the possibility of invoking the actions of the service through the setTarget()
, getTarget()
and getStatus()
methods.HDevice
and HService
classes, which means you can interact with your classes while they are being hosted and possibly used from external processes. Custom HDevice
and HService
interfaces may be beneficial in such a case.In any case, the above example demonstrates a fully standard-compliant implementation of BinaryLight:1. The next step is to publish your HDevice
in the network for UPnP control points to discover. You can find the instructions for that in Herqq::Upnp::HDeviceHost and Device Hosting.