This document shows how to build UPnP control point applications using the Applied Informatics Universal Plug and Play framework.
When building a control point, the following steps must be performed to set up the UPnP framework.
These steps are explained in the following sections.
The following sections explain the necessary steps to build a control point application.
In order to invoke actions on a device, and to receive event notifications from a device, C++ classes must be generated from the respective UPnP service descriptions. This is done with the UPnPGen and RemoteGenNG tools. Please refer to the UPnP Control And Eventing Tutorial And User Guide for more information. Note that in case of a control point, the RemoteGenNG tool must be invoked with —mode=client or —mode=both in order to create the Proxy and ClientHelper classes.
The SOAP Transport is required to send UPnP control requests to devices. Setting up the SOAP Transport is straightforward — all that must be done is registering the Transport Factory for the SOAP Transport with the RemotingNG ORB:
Poco::UPnP::SOAP::TransportFactory::registerFactory();
To set up the SSDPResponder, first a multicast socket must be created and configured. Then, a Poco::UPnP::SSDP::SSDPResponder instance is created using the multicast socket. A delegate function must be registered to receive notifications about discovered devices and services (advertisements). The multicast group for the SSDP protocol must be joined, and the SSDPResponder is started.
Poco::Net::NetworkInterface mcastInterface(findActiveNetworkInterface()); Poco::Net::IPAddress ipAddress(mcastInterface.address()); Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT); Poco::Net::MulticastSocket socket(sa); socket.setTimeToLive(4); socket.setInterface(mcastInterface); SSDPResponder ssdpResponder(_timer, socket); ssdpResponder.advertisementSeen += Poco::delegate(this, &NetworkLightController::onAdvertisementSeen); socket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface); ssdpResponder.start();
The findActiveNetworkInterface() method searches for a suitable network interface. This is necessary, as the IP address of the network interface being used must be known in order to receive events from UPnP devices. The method looks like this:
Poco::Net::NetworkInterface findActiveNetworkInterface() { Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list(); for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it) { if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it; } throw Poco::IOException("No configured Ethernet interface found"); }
The onAdvertisementSeen() callback method will be discussed later.
Finally, a search request for a specific device type is sent.
ssdpResponder.search("urn:schemas-upnp-org:device:DimmableLight:1");
In order to receive event notifications from devices, a HTTP server must be set up and configured to handle incoming GENA requests. Also, the GENA Listener must be registered with the RemotingNG ORB in order to properly process the incoming event notifications.
Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT); Poco::Net::SocketAddress httpSA(ipAddress, httpSocket.address().port()); Poco::UPnP::GENA::Listener::Ptr pGENAListener = new Poco::UPnP::GENA::Listener(httpSA.toString(), _timer); Poco::Net::HTTPServer httpServer(new RequestHandlerFactory(*pGENAListener), httpSocket, new Poco::Net::HTTPServerParams); httpServer.start(); Poco::RemotingNG::ORB::instance().registerListener(pGENAListener);
The RequestHandlerFactory class must create a Poco::UPnP::GENA::RequestHandler instance for every incoming NOTIFY request. If required by the application, it can also handle other requests (e.g. for web pages, etc.), but for a control point, handling NOTIFY requests is sufficient.
class RequestHandlerFactory: public Poco::Net::HTTPRequestHandlerFactory { public: RequestHandlerFactory(Poco::UPnP::GENA::Listener& genaListener): _genaListener(genaListener) { } Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest& request) { if (request.getMethod() == "NOTIFY") { return new Poco::UPnP::GENA::RequestHandler(_genaListener); } else return 0; } private: Poco::UPnP::GENA::Listener& _genaListener; };
Once a suitable device or service has been discovered, the device description (and optionally, the service descriptions) must be downloaded from the device and proxy objects for the required services must be created.
This is done in the callback method for the advertisementSeen event provided by the SSDPListener.
In the following example, device type advertisements are handled, and if the correct device type (urn:schemas-upnp-org:device:DimmableLight:1) has been discovered, the device description is downloaded from the device. This is done in a separate task, in order to return from the callback method as fast as possible. In order to fire up the download task, the Poco::Util::Timer instance otherwise used by the SSDPResponder is reused.
void onAdvertisementSeen(Advertisement::Ptr& pAdvertisement) { if (pAdvertisement->type() == Advertisement::AD_DEVICE_TYPE) { if (pAdvertisement->notificationType() == "urn:schemas-upnp-org:device:DimmableLight:1") { _timer.schedule(new FetchDeviceDescriptionTask(*this, pAdvertisement->location()), Poco::Timestamp()); } } }
The FetchDeviceDescriptionTask class simply calls the fetchDeviceDescription() method in the application class.
class FetchDeviceDescriptionTask: public Poco::Util::TimerTask { public: FetchDeviceDescriptionTask(NetworkLightController& nlc, const std::string& location): _nlc(nlc), _location(location) { } void run() { _nlc.fetchDeviceDescription(_location); } private: NetworkLightController& _nlc; std::string _location; };
The fetchDeviceDescription() method is where all the work is done. First, the XML device description is downloaded from the discovered device. The device description is loaded into a Poco::Util::XMLConfiguration object in order to simplify the extraction of the URLs for control and eventing.
Next, the code iterates over all service elements in the device description and extracts the URLs for the two services needed by the application. The URL is then used to create a RemotingNG Proxy object for invoking the service. In the case of the Dimming service, the Proxy object is also set up for receiving event notifications.
void fetchDeviceDescription(const std::string& location) { Poco::URI uri(location); Poco::Net::HTTPClientSession cs(uri.getHost(), uri.getPort()); Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, uri.getPathEtc(), Poco::Net::HTTPRequest::HTTP_1_1); cs.sendRequest(request); Poco::Net::HTTPResponse response; std::istream& istr = cs.receiveResponse(response); if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_OK) { Poco::AutoPtr<Poco::Util::XMLConfiguration> pDeviceDescription = new Poco::Util::XMLConfiguration(istr); Poco::URI baseURL(pDeviceDescription->getString("URLBase")); int i = 0; while (pDeviceDescription->hasProperty(Poco::format("device.serviceList.service[%d].serviceType", i))) { std::string serviceType = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceType", i)); std::string controlURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].controlURL", i)); std::string eventSubURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].eventSubURL", i), ""); if (serviceType == "urn:schemas-upnp-org:service:SwitchPower:1") { if (!_pSwitchPower) { Poco::URI switchPowerControlURL(baseURL, controlURL); _pSwitchPower = UPnPS::LightingControls1::SwitchPowerClientHelper::find(switchPowerControlURL.toString()); } } else if (serviceType == "urn:schemas-upnp-org:service:Dimming:1") { if (!_pDimming) { Poco::URI dimmingControlURL(baseURL, controlURL); _pDimming = UPnPS::LightingControls1::DimmingClientHelper::find(dimmingControlURL.toString()); Poco::URI dimmingEventSubURL(baseURL, eventSubURL); _pDimming.cast<UPnPS::LightingControls1::DimmingProxy>()->remoting__setEventURI(dimmingEventSubURL); } } i++; } } }
In order to receive event notifications via a Proxy object, a delegate must be registered with the respective event member of the Proxy, and the Proxy object must be enabled to receive events by calling the remoting__enableEvents() method, passing a pointer to the Poco::UPnP::GENA::Listener as argument.
_pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged); _pDimming->remoting__enableEvents(pGENAListener);
The event callback method is straightforward:
void onLoadLevelStatusChanged(const Poco::UInt8& level) { std::cout << "Load level changed to " << static_cast<unsigned>(level) << std::endl; }
Controlling the device is done by simply invoking the Proxy object's member functions.
_pSwitchPower->setTarget(true); _pDimming->startRampUp();
It is possible to control the HTTP timeout for sending control requests, and to enable or disable HTTP/1.1 persistent connections. This is done via the Proxy's Transport object. The Proxy's Transport object can be obtained by calling the remoting__transport() member function, and casting the result to a Poco::UPnP::SOAP::Transport.
static_cast<Poco::UPnP::SOAP::Transport&>(_pSwitchPower->remoting__transport()).setTimeout(Poco::Timespan(10, 0));
Following is the complete source code for the NetworkLightController sample used in this document.
#include "Poco/Util/Application.h" #include "Poco/Util/Timer.h" #include "Poco/Util/TimerTask.h" #include "Poco/Util/XMLConfiguration.h" #include "Poco/UPnP/SSDP/SSDPResponder.h" #include "Poco/UPnP/SSDP/Advertisement.h" #include "Poco/UPnP/SOAP/TransportFactory.h" #include "Poco/UPnP/GENA/Listener.h" #include "Poco/UPnP/GENA/RequestHandler.h" #include "Poco/UPnP/URN.h" #include "Poco/Net/MulticastSocket.h" #include "Poco/Net/HTTPClientSession.h" #include "Poco/Net/HTTPServer.h" #include "Poco/Net/HTTPRequestHandlerFactory.h" #include "Poco/Net/HTTPServerRequest.h" #include "Poco/URI.h" #include "Poco/Delegate.h" #include "Poco/AutoPtr.h" #include "Poco/Format.h" #include "Poco/Mutex.h" #include "Poco/Event.h" #include "UPnPS/LightingControls1/SwitchPowerProxy.h" #include "UPnPS/LightingControls1/DimmingProxy.h" #include "UPnPS/LightingControls1/SwitchPowerClientHelper.h" #include "UPnPS/LightingControls1/DimmingClientHelper.h" #include <iostream> using Poco::Util::Application; using Poco::UPnP::SSDP::SSDPResponder; using Poco::UPnP::SSDP::Advertisement; class RequestHandlerFactory: public Poco::Net::HTTPRequestHandlerFactory { public: RequestHandlerFactory(Poco::UPnP::GENA::Listener& genaListener): _genaListener(genaListener), _logger(Poco::Logger::get("RequestHandlerFactory")) { } Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest& request) { const std::string& path = request.getURI(); const std::string& method = request.getMethod(); if (_logger.information()) { _logger.information(method + " " + path + " (" + request.clientAddress().toString() + ")"); } if (method == "NOTIFY") { return new Poco::UPnP::GENA::RequestHandler(_genaListener); } else return 0; } private: Poco::UPnP::GENA::Listener& _genaListener; Poco::Logger& _logger; }; class NetworkLightController: public Application { public: NetworkLightController() { } ~NetworkLightController() { } protected: class FetchDeviceDescriptionTask: public Poco::Util::TimerTask { public: FetchDeviceDescriptionTask(NetworkLightController& nlc, const std::string& location): _nlc(nlc), _location(location) { } void run() { _nlc.fetchDeviceDescription(_location); } private: NetworkLightController& _nlc; std::string _location; }; void fetchDeviceDescription(const std::string& location) { logger().information("Retrieving device description..."); Poco::URI uri(location); Poco::Net::HTTPClientSession cs(uri.getHost(), uri.getPort()); Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, uri.getPathEtc(), Poco::Net::HTTPRequest::HTTP_1_1); cs.sendRequest(request); Poco::Net::HTTPResponse response; std::istream& istr = cs.receiveResponse(response); if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_OK) { Poco::AutoPtr<Poco::Util::XMLConfiguration> pDeviceDescription = new Poco::Util::XMLConfiguration(istr); Poco::FastMutex::ScopedLock lock(_mutex); Poco::URI baseURL(pDeviceDescription->getString("URLBase")); int i = 0; while (pDeviceDescription->hasProperty(Poco::format("device.serviceList.service[%d].serviceType", i))) { std::string serviceType = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceType", i)); std::string controlURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].controlURL", i)); std::string eventSubURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].eventSubURL", i), ""); logger().information(Poco::format("Found: %s.", serviceType)); if (serviceType == "urn:schemas-upnp-org:service:SwitchPower:1") { if (!_pSwitchPower) { Poco::URI switchPowerControlURL(baseURL, controlURL); _pSwitchPower = UPnPS::LightingControls1::SwitchPowerClientHelper::find(switchPowerControlURL.toString()); _switchPowerFound.set(); } } else if (serviceType == "urn:schemas-upnp-org:service:Dimming:1") { if (!_pDimming) { Poco::URI dimmingControlURL(baseURL, controlURL); _pDimming = UPnPS::LightingControls1::DimmingClientHelper::find(dimmingControlURL.toString()); Poco::URI dimmingEventSubURL(baseURL, eventSubURL); _pDimming.cast<UPnPS::LightingControls1::DimmingProxy>()->remoting__setEventURI(dimmingEventSubURL); _dimmingFound.set(); } } i++; } } } void onAdvertisementSeen(Advertisement::Ptr& pAdvertisement) { if (pAdvertisement->type() == Advertisement::AD_DEVICE_TYPE) { if (pAdvertisement->notificationType() == "urn:schemas-upnp-org:device:DimmableLight:1") { std::string location = pAdvertisement->location(); logger().information(Poco::format("Found DimmableLight device at %s.", location)); Poco::FastMutex::ScopedLock lock(_mutex); if (!_pSwitchPower || !_pDimming) { _timer.schedule(new FetchDeviceDescriptionTask(*this, location), Poco::Timestamp()); } } } } void onLoadLevelStatusChanged(const Poco::UInt8& level) { logger().information(Poco::format("Load level changed to %u.", static_cast<unsigned>(level))); } Poco::Net::NetworkInterface findActiveNetworkInterface() { Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list(); for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it) { if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it; } throw Poco::IOException("No configured Ethernet interface found"); } int main(const std::vector<std::string>& args) { // Register SOAP transport Poco::UPnP::SOAP::TransportFactory::registerFactory(); // Find suitable multicast interface Poco::Net::NetworkInterface mcastInterface(findActiveNetworkInterface()); Poco::Net::IPAddress ipAddress(mcastInterface.address()); logger().information(Poco::format("Using multicast network interface %s (%s).", mcastInterface.name(), ipAddress.toString())); // Set up multicast socket and SSDPResponder Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT); Poco::Net::MulticastSocket socket(sa); socket.setTimeToLive(4); socket.setInterface(mcastInterface); SSDPResponder ssdpResponder(_timer, socket); ssdpResponder.advertisementSeen += Poco::delegate(this, &NetworkLightController::onAdvertisementSeen); socket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface); ssdpResponder.start(); // Search for DimmableLight devices ssdpResponder.search("urn:schemas-upnp-org:device:DimmableLight:1"); // Set up HTTP server for GENA and GENA Listener Poco::Net::ServerSocket httpSocket(Poco::Net::SocketAddress(ipAddress, 0)); Poco::Net::SocketAddress httpSA(ipAddress, httpSocket.address().port()); Poco::UPnP::GENA::Listener::Ptr pGENAListener = new Poco::UPnP::GENA::Listener(httpSA.toString(), _timer); Poco::Net::HTTPServer httpServer(new RequestHandlerFactory(*pGENAListener), httpSocket, new Poco::Net::HTTPServerParams); httpServer.start(); Poco::RemotingNG::ORB::instance().registerListener(pGENAListener); // Wait until services have been found logger().information("Waiting for services..."); _switchPowerFound.wait(); _dimmingFound.wait(); // Subscribe to events logger().information("Subscribing for events..."); _pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged); _pDimming->remoting__enableEvents(pGENAListener); bool status = false; _pSwitchPower->getStatus(status); logger().information(std::string("The light is ") + (status ? "ON" : "OFF") + "."); bool quit = false; while (!quit) { std::cout << "Enter command (0, 1, +, -, >, <, Q): " << std::flush; char cmd; std::cin >> cmd; try { switch (cmd) { case '0': _pSwitchPower->setTarget(false); break; case '1': _pSwitchPower->setTarget(true); break; case '+': _pDimming->stepUp(); break; case '-': _pDimming->stepDown(); break; case '>': _pDimming->startRampUp(); break; case '<': _pDimming->startRampDown(); break; case 'Q': quit = true; break; default: std::cout << "Unknown command: " << cmd << std::endl; break; } } catch (Poco::Exception& exc) { logger().log(exc); } } // Shut down _pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged); httpServer.stop(); ssdpResponder.stop(); socket.leaveGroup(ssdpResponder.groupAddress().host(), mcastInterface); ssdpResponder.advertisementSeen -= Poco::delegate(this, &NetworkLightController::onAdvertisementSeen); Poco::RemotingNG::ORB::instance().shutdown(); return Application::EXIT_OK; } private: Poco::Util::Timer _timer; std::string _usn; UPnPS::LightingControls1::ISwitchPower::Ptr _pSwitchPower; UPnPS::LightingControls1::IDimming::Ptr _pDimming; Poco::Event _switchPowerFound; Poco::Event _dimmingFound; Poco::FastMutex _mutex; friend class FetchDeviceDescriptionTask; }; POCO_APP_MAIN(NetworkLightController)