Setting Up the Device Model

Before We Start

Please note that in case you are writing client-side software you rarely need to worry about how the device model gets built. But if you are implementing a server-side UPnP device, this is something you should know. In addition, you should first skim through the Tutorial for Building a UPnP Device if you haven't already.

How HDeviceHost builds a device tree

When you initialize an Herqq::Upnp::HDeviceHost you have to provide it:

You can find more information about setting up the Herqq::Upnp::HDeviceHost in Device Hosting, but what is relevant here is that you provide two pieces of information that must match; the C++ classes created by the device model creator must reflect the description documents and vice versa.

During its initialization an HDeviceHost scans the provided description document(s) and whenever it encounters a definition of a UPnP device it invokes the device model creator you have provided to create an instance of HServerDevice matching the device definition in the description document. Similarly, whenever it encounters a UPnP service it invokes the device model creator to create an instance of HServerService.

When creating a device the HDeviceHost provides an HDeviceInfo object to the creator, which contains the information read from the description document. And again, when creating a service an HServiceInfo object is provided to the creator. Based on the information in the info objects the device model creator is expected to create an HServerDevice or HServerService instance if it can, or return null otherwise. If the device model creator returns null the HDeviceHost creates an instance of a default type used to represent the encountered UPnP device or service.

Consider an example of a simple device model creator,

 #include <HUpnpCore/HDeviceModelCreator>

 class MyCreator : public Herqq::Upnp::HDeviceModelCreator
 {

 private:

   // overridden from HDeviceModelCreator
   virtual MyCreator* newInstance() const
   {
       return new MyCreator();
   }

 public:

   // overridden from HDeviceModelCreator
   virtual MyHServerDevice* createDevice(const Herqq::Upnp::HDeviceInfo& info) const
   {
       if (info.deviceType().toString() == "urn:herqq-org:device:MyDevice:1")
       {
           return new MyHServerDevice();
       }

       return 0;
   }

   // overridden from HDeviceModelCreator
   virtual MyHServerService* createService(
       const Herqq::Upnp::HServiceInfo& serviceInfo,
       const Herqq::Upnp::HDeviceInfo& parentDeviceInfo) const
   {
       if (serviceInfo.serviceType().toString() == "urn:herqq-org:service:MyService:1")
       {
           return new HMyServerService();
       }

       // Note, parentDeviceInfo is not needed in this case, but there are
       // scenarios when it is mandatory to know information of the parent
       // device to create the correct HServerService type.

       return 0;
   }
 };
Note:
The HDeviceHost takes ownership of the returned objects. However, the HDeviceHost will not destroy any of them until it is being deleted.

Plugging in Custom Functionality

In the UPnP architecture the actions represent the functionality of a device. Actions are contained by services and services are contained by devices. Thus, custom functionality means custom actions, which are logically placed into custom services. Custom services, on the other hand, don't have to reside inside custom devices. This is because ultimately a device is only a container for services and that is certainly something the default device types used by HUPnP can handle.

So, to plug-in custom functionality you need a custom service type, which main purpose is to map callable entities to UPnP action names.

Note:
A callable entity is a C++ concept that is used to refer to anything that can be called with the operator(), such as a normal function, functor or a member function.

In order to do the mapping, you need to override Herqq::Upnp::HServerService::createActionInvokes() in your custom HServerService type.

Consider an example,

 Herqq::Upnp::HServerService::HActionInvokes MyConnectionManagerService::createActionInvokes()
 {
   Herqq::Upnp::HServerService::HActionInvokes retVal;

   retVal.insert("GetProtocolInfo",
       HActionInvoke(this, &MyConnectionManagerService::getProtocolInfo));

   retVal.insert("PrepareForConnection",
       HActionInvoke(this, &MyConnectionManagerService::prepareForConnection));

   retVal.insert("ConnectionComplete",
       HActionInvoke(this, &MyConnectionManagerService::connectionComplete));

   retVal.insert("GetCurrentConnectionIDs",
       HActionInvoke(this, &MyConnectionManagerService::getCurrentConnectionIDs));

   retVal.insert("GetCurrentConnectionInfo",
       HActionInvoke(this, &MyConnectionManagerService::getCurrentConnectionInfo));

   return retVal;
 }

The above code maps five member functions of the class MyConnectionManagerService to the five action names accordingly. Once the device tree is fully set up and the HServerDevice containing the MyConnectionManagerService is hosted by an HDeviceHost, action invocations are ultimately directed to these mapped member functions. In other words, in this case it is these member functions that have to do whatever it is that these actions are expected to do.

Note:
The callable entity concept detaches invocation logic from what is being invoked and enables these "entities" to be handled by value. It is a very powerful concept that allows you to map anything that can be called following a certain signature under the same interface and copy these entities around by-value.

To give you an idea of the versatility of an callable entity, you could do the above with normal functions too:

 namespace
 {
 int getProtocolInfo(const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs)
 {
 }

 int prepareForConnection(const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs)
 {
 }

 int connectionComplete(const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs)
 {
 }

 int getCurrentConnectionIDs(const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs)
 {
 }

 int getCurrentConnectionInfo(const Herqq::Upnp::HActionArguments& inArgs, Herqq::Upnp::HActionArguments* outArgs)
 {
 }
 }

 Herqq::Upnp::HServerService::HActionInvokes MyConnectionManagerService::createActionInvokes()
 {
   Herqq::Upnp::HServerService::HActionInvokes retVal;

   retVal.insert("GetProtocolInfo", Herqq::Upnp::HActionInvoke(getProtocolInfo));
   retVal.insert("PrepareForConnection", Herqq::Upnp::HActionInvoke(prepareForConnection));
   retVal.insert("ConnectionComplete", Herqq::Upnp::HActionInvoke(connectionComplete));
   retVal.insert("GetCurrentConnectionIDs", Herqq::Upnp::HActionInvoke(getCurrentConnectionIDs));
   retVal.insert("GetCurrentConnectionInfo", Herqq::Upnp::HActionInvoke(getCurrentConnectionInfo));

   return retVal;
 }

And it doesn't stop there. You can use functors as well.

Once you have set up the action mappings, your custom HServerService is ready to be used. However, there is much more you can do with Herqq::Upnp::HDeviceModelInfoProvider::actionsSetupData() to ensure that the service description containing the action definitions is correct. Similarly, you can override Herqq::Upnp::HDeviceModelInfoProvider::stateVariablesSetupData() to provide additional information to HUPnP in order to make sure that the service description document is appropriately set up in terms of state variables as well. This may not be important to you if you are writing a "private" implementation of a service, but it could be very useful if you are writing a "public" library of UPnP devices and services that have to make sure they are appropriately used.

Note:
When implementing a custom HServerService class you are required to implement only the createActionInvokes(). You may provide additional information about the structure and details of a UPnP service via HDeviceModelInfoProvider::actionsSetupData() and HDeviceModelInfoProvider::stateVariablesSetupData(), but those are always optional.

Providing more information to the model setup process

There are a few ways to provide in-depth information of a UPnP device to ensure that a device model will get built as desired. All of the methods described here require HDeviceModelInfoProvider. First, you can override HDeviceModelInfoProvider::embedddedDevicesSetupData() and HDeviceModelInfoProvider::servicesSetupData() methods to specify what embedded devices and services has to be defined in the device description for an instance of the device type to function correctly. Second, you can provide detailed information of expected actions and their arguments when you override HDeviceModelInfoProvider::actionsSetupData(). Third, you can provide detailed information of expected state variables when you override HDeviceModelInfoProvider::stateVariablesSetupData().

Note:
All of these methods and techniques described here are optional.

The benefit of overriding these methods is that HUPnP can ensure that the provided description documents provide everything the respective C++ classes expect. The validation is done by HUPnP during the build of the device model and the build succeeds only if the provided description documents are appropriately defined. This way you never have to validate the device model yourself and you do not have to check everywhere if an embedded device, a service, a required action, an action argument or a state variable is provided. But if your device and service types are created for internal or otherwise controlled use only, implementing your own HDeviceModelInfoProvider may be unnecessary.

An example of servicesSetupData():

 Herqq::Upnp::HServicesSetupData MyDeviceModelInfoProvider::servicesSetupData(
     const Herqq::Upnp::HDeviceInfo& info)
 {
  Herqq::Upnp::HServicesSetupData retVal;

  QString type = info.deviceType().toString();
  if (type == "urn:schemas-upnp-org:device:DimmableLight:1")
  {
    retVal.insert(
       Herqq::Upnp::HServiceSetup(
           Herqq::Upnp::HServiceId("urn:schemas-upnp-org:serviceId:SwitchPower"),
           Herqq::Upnp::HResourceType("urn:schemas-upnp-org:service:SwitchPower:1")));

    retVal.insert(
       Herqq::Upnp::HServiceSetup(
           Herqq::Upnp::HServiceId("urn:schemas-upnp-org:serviceId:Dimming"),
           Herqq::Upnp::HResourceType("urn:schemas-upnp-org:service:Dimming:1")));
  }

   return retVal;
 }

The above definition instructs HUPnP to ensure that when the DimmableLight:1 device type is being initialized those two specified services are found in the device description document provided by the user. If either one of them is missing or contains invalid information, the device model build is aborted, the HDeviceHost::init() fails and HDeviceHost::error() returns HDeviceHost::InvalidDeviceDescriptionError. See the documentation of HServiceSetup for more information of what can be validated.

An example of embedddedDevicesSetupData():

 Herqq::Upnp::HDevicesSetupData MyDeviceModelInfoProvider::embedddedDevicesSetupData(
     const HDeviceInfo& info)
 {
  Herqq::Upnp::HDevicesSetupData retVal;

  QString type = info.deviceType().toString();
  if (type == "urn:custom-domain-org:device:MyDeviceType:1")
  {
    retVal.insert(
       Herqq::Upnp::HDeviceSetup(
           Herqq::Upnp::HResourceType("urn:my-domain-org:device:MyDevice_X:1")));

    retVal.insert(
       Herqq::Upnp::HDeviceSetup(
           Herqq::Upnp::HResourceType("urn:my-domain-org:device:MyDevice_Y:1")));
  }

   return retVal;
 }

The above definition instructs HUPnP to ensure that when the MyDeviceType:1 device type is being initialized those two specified embedded devices are found in the device description document provided by the user. If either one of them is missing or contains invalid information, the device model build is aborted, the HDeviceHost::init() fails and HDeviceHost::error() returns HDeviceHost::InvalidDeviceDescriptionError. See the documentation of HDeviceSetup for more information of what can be validated.

An example of stateVariablesSetupData():

 Herqq::Upnp::HStateVariablesSetupData MyDeviceModelInfoProvider::stateVariablesSetupData(
     const Herqq::Upnp::HServiceInfo& serviceInfo, const Herqq::Upnp::HDeviceInfo& parentDeviceInfo)
 {
  Herqq::Upnp::HStateVariablesSetupData retVal;

  if (info.serviceType().compare(
      HResourceType("urn:schemas-upnp-org:service:ConnectionManager:2"),
      HResourceType::Inclusive))
  {
    retVal.insert(HStateVariableInfo(
      "SourceProtocolInfo", HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "SinkProtocolInfo", HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "CurrentConnectionIDs", HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_ConnectionStatus", HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_ConnectionManager",HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_ProtocolInfo", HUpnpDataTypes::string));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_ConnectionID", HUpnpDataTypes::i4));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_AVTransportID", HUpnpDataTypes::i4));

    retVal.insert(HStateVariableInfo(
      "A_ARG_TYPE_RcsID", HUpnpDataTypes::i4));
  }

   return retVal;
 }

The above definition instructs HUPnP to ensure that when the ConnectionManager:2 service type is being initialized all those specified state variables are found in the service description document provided by the user. If any one of them is missing or contains invalid information, the device model build is aborted, the HDeviceHost::init() fails and HDeviceHost::error() returns HDeviceHost::InvalidServiceDescriptionError. See the documentation of HStateVariableInfo for more information of what can be validated.

Finally, you can override HDeviceModelInfoProvider::actionsSetupData() to provide detailed information about the action and its expected arguments. See HActionSetup for more information of this.

Note:
It is planned that in the future this same information could be used to create instances of HUPnP's device model without description documents.
See also:
Device Hosting