As a Java and OSGi developer I’m keenly interested in how layers and levels of abstraction help me do my job. Now that we are entering into the Internet of Things era I’m thinking about how those layers and levels of abstraction should work.
- Board level – like the Kura OSGi project which is based on Pi4j
- OSGi system APIs
- Device level APIs
A Problem Statement
As an application developer, I want to write code that works on a variety of boards and with a variety of different devices. In an ideal world, if I change the board or the devices I shouldn’t have to change anything about my code. Some configuration changes and redeployment may be necessary but the fundamental application code shouldn’t change.
At the Application Level
As a relatively simple example think of a robot with a motor controller. My application might have a line like:
I want the motor at 50% power for 2 seconds. At the application level I don’t care which pin or pins are interoperating with the motor controller. I don’t care if it is using PWM, PAM or PPM. As soon as my application has to know about those levels of details the abstraction and portability are lost.
At the very least this implies that I have a Motor interface that can be injected into my application and that Motor interface is implemented in a separate bundle. OSGi services are ideal for that and provide easy mechanisms for changing implementation bundles and configuration details without having to rewrite my application.
If I decide the motor I’m using for my application isn’t powerful enough or is draining the battery too fast, then I can use a different manufacturer’s motor, change my configuration to use that motor’s OSGi driver bundle, and inject the same Motor interface (service) into my application’s bundle. If that motor uses PPM and my previous motor used PAM, my application is blissfully unaware.
At the Device Level
Obviously differing devices, like motors, are going to use different protocols or signaling. If my first Motor control bundle uses a single GPIO pin then I obviously have to provide that to the bundle. I have to be able to configure that and inject that into the Motor control bundle.
That implies, in turn, that the Motor controller implementation can’t be aware of a DeviceManager or specific pin out configuration on a board by board basis or once again the abstraction and portability are broken. All the Motor controller bundle needs to count on is that I injected a GpioPin that faithfully implements the requirements of the interface.
When I switch to a new motor, I specify a different Motor controller bundle to provide my service interface. It may be that the new bundle uses I2C for signaling instead of GPIO. I make a configuration change that now injects necessary I2C device interface. Again, this relies on having an abstraction for the I2C device.
My application, though, is still motoring along with the Motor interface.
At the Board Level
All board pin outs are not created equal. Even variations of the same board have different pin outs. The RaspberryPi over its various generations has switched the pin outs. A device level driver can’t be aware of all possible configurations for all possible devices and all possible versions of a device.
There are projects like Pi4j and Kura which are working toward these goals right now. The Pi4j project lists some 13 boards and variations that it currently supports. As the library becomes more popular it is likely that board makers will help contribute code to support their own hardware.
This implies that the entire stack requires interface based service injection or at least interfaces. If a GpioPin, as a service, is injected into a bundle, it keeps the concerns separate.
If I’m moving my application from a BeagleBoard to a RaspberryPi, I want to make a configuration change to set up the GpioPin and inject into the Motor controller. In that case I don’t even have to change the configuration of injection of the Motor service interface into my application bundle. The necessary levels of abstraction take care of that for me.
If at the board, device or application level any of the code can access the underlying responsibilities of the other services, the abstraction is broken. This is why service interfaces are so important, they enforce those separations of concerns.
If I have access to a DeviceManager with static factory methods on the other hand, now I’m directly accessing the underlying hardware and its configuration from code. The portability is immediately broken. It also makes it difficult to debug as I can’t tell where in the code a configuration problem might exist. It could be any bundle. It might be multiple bundles accidentally accessing the same pin and reconfiguring it. This might result in intermittent bugs that arise when certain conditions trigger code to configure a pin or to use that pin that is already in use.
Ease of Implementation & Testing
The advent of Pi4j has eased portability issues by providing standard libraries for accessing boards in standard ways. It is still at a low level and provides access to device management directly. It isn’t intended to provide a complete level of abstraction (nor could it really). However, this means that any board maker who wants to have a standard implementation of Pi4j available for their board can take the source, modify it to meet the requirements of their board, and provide it to the community. They merely have to test their implementation against their board with a standard suite of tests to verify that it functions properly.
Currently there is little by way of such standardization for devices. But when those interfaces and services become available, manufacturers of RFID, GPS, motors, and other sensors and actuators will be able to implement the requisite bundles and provide them to developers. Further, they won’t have to start from scratch but will be able to use standard interfaces, pre-existing implementations, and standard tests to ensure that their code operates their device correctly. Additionally, they’ll be able to test their implementation against a variety of common boards and devices to ensure interoperability.
During application development, it will be possible to have simulators and mocks that implement the variety of interfaces and permit testing of applications devoid of underlying hardware concerns. Once the code is working properly, then debugging during installation will be limited to configuration changes.
Libraries, libraries, libraries…
Installing new drivers for various RFID, GPS, motors, sensors and actuators will become as simple as specifying a new Maven bundle and version in one’s POM file and making the requisite configuration changes. Until now we haven’t had the necessary underlying abstraction for things like GpioPin, I2C device, or SPI to even think about standardized libraries for individual sensors and actuators.
Isn’t it brittle? Rigid?
Not really. We’ve lived with this sort of abstraction for a long time. Enterprise applications were forced to such standardization or collapse under their own weight.
Just because a manufacturer provides a standard controller with standard values for the device, it doesn’t mean that it can’t be configurable. In the case of motor controller again, a properties file might specify what the values of 1% to 100% mean in terms of their PWM or PAM values. As a developer, I might find the values don’t map as linearly or non-linearly as my application requires. By changing the configuration file I can modify that dynamic without touching a line of code.
In Karaf, which commonly uses the Felix OSGi container, one can use features files to install both a bundle and its required dependencies and to install a configuration file in an “etc” directory. That configuration file is a properties file with a .cfg extension. When a bundle is loaded, it can look for a properties file identified by its properties id (PID) and the extension of .cfg. If the manufacturer uses com.gizmos.rfid.reader as their properties ID then it would look for a com.gizmos.rfid.reader.cfg file in order to load values or override values.
In the same fashion, a board level configuration for a RaspberryPi could load up the GPIO, I2C and SPI configurations for use at run time.
The critical part to this is that it keeps configuration and operational data out of the code and in easily accessible places.