Implementing UPnP Control Points on iOS

Applied Informatics' UPnP framework can be used to implement both servers as well as control points, as demonstrated by the included sample applications (SimpleMediaServer, NetworkLight, NetworkLightController, etc.). An interesting exercise is to implement a UPnP control point on iOS. While the code dealing with network issues like device discovery, control and eventing is the same as on other platforms, the interesting part is integrating the backend part with the user interface.

Due to iOS' excellent support for mixing Objective-C and C++, this is actually pretty easy, especially when compared to Android, where you'll have to implement a JNI layer to connect the Java user interface to the C++ backend. For example, when sending a UPnP control command in reaction to the user tapping a button, you can simple do this in the button's (IBAction) event handler. The interesting part is notifying the user interface code of changes happening in the C++ network code, for example if new network devices have been detected by the device browser, or if UPnP events have been received.

In the following I'll show step-by-step how to build an iPhone control point for the NetworkLight sample server. The complete source code and project file for the application can be downloaded here.

Setting Up UPnP

The first thing we must do in our application is set up the UPnP framework. There's not much to it, basically just registering the UPnP SOAP transport with the Remoting framework. We do this in the AppDelegate's didFinishLaunchingWithOptions method. We'll also need to setup a background thread to browse for network devices. This is done by the DeviceBrowser C++ class, which we also setup here. The rest of the method is standard AppDelegate stuff.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions { Poco::UPnP::SOAP::TransportFactory::registerFactory(); DeviceBrowser::instance().start(); viewController = [[MainViewController alloc] init]; [self.window addSubview:viewController.view]; [self.window makeKeyAndVisible]; return YES; }

Browsing For Devices

The DeviceBrowser code is basically contains the device discovery code from the NetworkLightController sample. It contains a SSDPResponder instance and periodically sends out browse requests to discover new devices. When a device has been discovered, DeviceBrowser fetches the device description using a HTTPClientSession and extracts the control and eventing URLs using a XMLConfiguration in a "creative" way. All discovered devices are stored internally in a std::map, and there's list() method to put all discovered devices into a std::vector.

class DeviceBrowser { public: struct Device { std::string name; Poco::Net::IPAddress address; Poco::Timestamp lastSeen; UPnPS::LightingControls1::ISwitchPower::Ptr pSwitchPower; UPnPS::LightingControls1::IDimming::Ptr pDimming; }; DeviceBrowser(); ~DeviceBrowser(); static DeviceBrowser& instance(); void start(); void stop(); void list(std::vector& devices); protected: void fetchDeviceDescription(const std::string& location); void browse(); void onAdvertisementSeen(Poco::UPnP::SSDP::Advertisement::Ptr& pAd); Poco::Net::NetworkInterface findActiveNetworkInterface(); private: typedef std::map DeviceMap; Poco::UPnP::SSDP::SSDPResponder* _pResponder; DeviceMap _devices; Poco::Util::Timer _timer; Poco::FastMutex _mutex; Poco::Logger& _logger; friend class FetchDeviceDescriptionTask; friend class BrowseTask; };

All devices that we've discovered should be shown to the user, and we'll do this using a TableView. Following code shows how we fill the TableView with the device list from the DeviceBrowser in the DeviceTableViewController class. First, we keep a std::vector in the object to keep track of discovered devices:

@interface DeviceTableViewController : UITableViewController { std::vector devices; } @end

And here is the code to fill the table:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection: (NSInteger)section { DeviceBrowser::instance().list(devices); return devices.size(); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; } cell.textLabel.text = [NSString stringWithUTF8String: devices[indexPath.row].name.c_str()]; cell.detailTextLabel.text = [NSString stringWithUTF8String: devices[indexPath.row].address.toString().c_str()]; return cell; }

In numberOfRowsInSection we first fill our vector with the devices discovered by the DeviceBrowser and then return the size of the vector. Since the Cocoa Touch framework always calls numberOfRowsInSection first when updating the table, we can simply use the vector in cellForRowAtIndexPath.

Now, what happens if we discover new devices (or devices disappear) after we first display the table? To handle that case, we simply use a timer to periodically force an update of the table. This is done in MainViewController's viewDidLoad method: refreshTimer = [NSTimer scheduledTimerWithTimeInterval: 5.0f target: self selector: @selector(refresh:) userInfo: nil repeats: YES];

This will ensure that every 5 seconds the table is updated with the current state of DeviceBrowser.

Controlling Devices

In our application, controlling of the network light is done in the NetworkLightControllerViewController class. The controller is created by the DeviceTableViewController when a device in the table is clicked. The Device object containing the proxies for SwitchPower and Dimming services is passed to the object's initWithDevice method:

@interface NetworkLightControllerViewController : UIViewController { IBOutlet UISwitch* powerSwitch; std::string name; UPnPS::LightingControls1::ISwitchPower::Ptr pSwitchPower; UPnPS::LightingControls1::IDimming::Ptr pDimming; } - (id)initWithDevice: (DeviceBrowser::Device&) device; - (IBAction) powerOnOff:(id)sender; - (IBAction) stepUp:(id)sender; - (IBAction) stepDown:(id)sender; - (IBAction) rampUp:(id)sender; - (IBAction) rampDown:(id)sender; @end

The implementation of the class is straightforward. In initWithDevice we set up the ViewController and store the SwitchPower and Dimming proxy objects. In viewDidLoad we query the device and set the on/off switch accordingly. When a button is pressed we simply invoke the corresponding method on the respective proxy object. The proxy method call needs to be put into a try ... catch block to prevent C++ exceptions from leaking into the Cocoa Touch framework, which would terminate the application. For now we simply ignore exceptions; this should be changed in a real application.

@implementation NetworkLightControllerViewController - (id)initWithDevice: (DeviceBrowser::Device&) device { self = [super initWithNibName:@"NetworkLightControllerViewController" bundle:nil]; if (self) { name = device.name; pSwitchPower = device.pSwitchPower; pDimming = device.pDimming; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.title = [NSString stringWithUTF8String: name.c_str()]; try { bool status; pSwitchPower->getStatus(status); powerSwitch.on = status ? YES : NO; } catch (Poco::Exception&) { } } ... - (IBAction) powerOnOff:(id)sender { try { pSwitchPower->setTarget(powerSwitch.on); } catch (Poco::Exception&) { } } - (IBAction) stepUp:(id)sender { try { pDimming->stepUp(); } catch (Poco::Exception&) { } } ... @end

Now, the only issue we need to solve is how to react to UPnP events sent from the device. We'll look into this in another article soon. A quick hint: After setting up the web server to receive events from UPnP devices, we'll use performSelectorOnMainThread to forward the events to the main thread so that the user interface can be updated.

Again, you can download the complete source code and project file for this application here. A sample network light device implementation for testing the iPhone application is part of the Applied Informatics C++ Libraries and Tools Starter Kit.

Tagged ,