Wednesday, September 03, 2008

Driving a digital thermometer with RHQ (Part 2)

After we have installed all prerequisites last time, we can now concentrate on the RHQ plugin itself.

temp_chart.png


I won't explain every little bit and will concentrate mostly on the hardware side. If you want to know more about plugin development, go to the RHQ plugin community pages and make sure to check out my RHQ plugin development series.

Plugin architecture



The following picture shows the (hardware) components involved:
onewire-arch.png

The DS9490 is the USB-1-wire adapter used. It has internally a DS1990 compatible uuid chip, that provides a unique id for the adapter. On the bus we have a DS1820 compatible thermometer and can have other 1-wire chips (future).

So our plugin will follow this hardware setup. For the DS9490 we will have a OneWireAdapterComponent + -Discovery and a separate component for the DS1820 plus its discovery component.

Plugin artifacts



Lets have a look at the individual artifacts of the plugin. I will not show everything in great detail -- check out the full source (see below).

Plugin descriptor



 <server name="OneWireAdapter"
description="A single OneWire(r) adapter hosting many devices"
discovery="OneWireAdapterDiscovery"
class="OneWireAdapterComponent">
 
<plugin-configuration>
<c:simple-property name="type"
displayName="Adapter type" required="true"
default="DS9490"/>
<c:simple-property name="port"
displayName="Adapter port" required="true"
default="USB1"/>
</plugin-configuration>

So we have a Server per USB adapter. The server has two configuration options for the kind of adapter and the port it is hanging on. For now, I have put some defaults in, as the code only supports the DS9490 kind of USB adapters anyway (and with all the legacy free PCs around, does anyone remember serial interfaces?). The server itself is hosting the individual types of chips as services:

      <service name="DS1820" 
description="DS1820 Thermometer chip"
class="DS1820Component"
discovery="DS1820Discovery"
>
<metric property="temperature"
defaultOn="true"
description="Temperature measured"
displayType="summary"/>

</service>
</server>

As the thermometer only returns one value - its temperature, we only have one numerical metric
that we show on the IndicatorCharts page by default (displayType=summary).

If you want to add more kinds of 1-wire (r) devices, you can add them as services next to the DS1820 service.

OneWireDiscovery



Now that we have seen the plugin descriptor, lets discover the USB-adapter: OneWireAdapterDiscovery.discoverResources() looks like this:

        Configuration pluginConfig = context.getDefaultPluginConfiguration();
String device = pluginConfig.getSimple("type").getStringValue();
String port = pluginConfig.getSimple("port").getStringValue();
 
DSPortAdapter adapter = new PDKAdapterUSB();
adapter.selectPort(port);

First we get the config property defaults from the descriptor. Then we create a new adapter object (this is the place where you could call into OneWireAccessProvider.getAdapter() from the 1-wire SDK to use other adapters). And finally we open the port. Next, we probe if there is actually an adapter present (the selectPort() above will probably already fail if none is present):

        boolean found = false;
adapter.beginExclusive(true);
found = adapter.adapterDetected();
adapter.endExclusive();


If everything is ok, we can put the adapter in the list of discovered details.
The unique resource key is determined by reading the uuid from the DS1990 chip in the adapter:

    private String getIdForAdapter(DSPortAdapter adapter)  {
 
Enumeration<OneWireContainer> devices =
adapter.getAllDeviceContainers();
while (devices.hasMoreElements()) {
OneWireContainer cont = devices.nextElement();
String name = cont.getName();
if ("DS1990A".equals(name) ||
"DS2401".equals(name) ||
"DS2411".equals(name)) {

return cont.getAddressAsString();
}
}

We loop over all devices on the bus and check if its a DS1990 or compatible. If found we return the address. We could additionally check if the device is a family 81 device in case that there are more
of those devices present.

At the end of discovery, we free the adapter again:
            adapter.freePort();


OneWireComponent



Now that we have the adapter discovered, lets look at the resource component for it.

First we need to start() it. This is simple. First we read the configuration data again.
Then we just open the port:

        if (adapter == null) {
adapter = new PDKAdapterUSB();
adapter.selectPort(port);
}

The check if the adapter has been set already serves to prevent multiple calls to open the same
port over and over again, which will fail.

Availability reporting uses the same adapterDetected() method as discovery above, so I won't show this again. Same for stop() - this just frees the port again.

In addition two methods are provided for the children:

reopenPort() frees the port and reopens it again. getAdapter() makes the shared adapter available to
the child classes.

DS1820Discovery



This class discovers the DS1820 thermometers.

public class DS1820Discovery implements 
ResourceDiscoveryComponent<OneWireAdapterComponent> {

As we implement the ResourceDiscoveryComponent matching the OneWireAdapterComponent just seen, we have access to this class and especially the getAdapter() method to use this shared adapter instance.

In discoverResources() we loop over the devices on the bus and see if they are of family 10.
In this case, we cast to the appropriate class and use the information from it to create the details:

        DSPortAdapter adapter = context.getParentResourceComponent().getAdapter();
OneWireContainer cont = adapter.getFirstDeviceContainer();
while (cont != null) {
if (cont instanceof OneWireContainer10) {
...
}
cont = adapter.getNextDeviceContainer();


DS1820Component



Finally the resource component also matches the OneWireAdapterComponent. Start()ing it mostly means getting the adapter and the resource key and to otain the right OneWireContainer from the adapter.
Stop() is a noop and availability reporting checks if the container is present or not.

The Implementation of the MeasurementFacet, getValues() does the real work in this class:

            if ("temperature".equals(metric.getName())) {
boolean good = true;
byte[] data = container.readDevice();
 
container.doTemperatureConvert(data);
Thread.sleep(100);
 
double temp = container.getTemperature(data);


As I have seen some strange spikes, I have added a check that if the temperature is above 75C, we don't report it. This could be optimized in a way that we store the last values to see if the high value is feasible. Or we could try to determine this from the device status somehow.

Getting the source



The source is available from RHQ svn, but not enabled in the main build. You have to go into the $RHQ/modules/plugins/onewire directory to build the plugin.

All resources are described on the Plugin page on the RHQ wiki

That's it



That's it - I hope you enjoyed these two postings and I'd love to see contributions to RHQ to drive other devices as well.

1 comment:

Greg said...

you really need to get rhq to support temperature units ;)

Nice :)