Android Things

Simple Things – Part 4

In December 2016 Google released the first developer preview of Android Things and Android-based operating system designed for IoT devices, formerly known as Brillo. In this series we’ll take a look at some of the basics of Android Things and create a simple weather station.

Previously in this series we’ve looked at how to collect and display the temperature on a Raspberry Pi with a Rainbow HAT running Android Things developer preview. In this final article of the series we’ll extend this further to include barometric pressure, as well.

We’ll start by seeing how we can actually retrieve the pressure data – this uses the same BMP280 sensor from which we obtain the temperature data, and the process of registering for regular updates is exactly the same as we did previously for getting temperature updates. The changes to our DataCollector are highlighted:

class RainbowHatDataCollector implements DataCollector {
    private final Context context;
    private final RainbowHatFactory factory;

    private Bmx280SensorDriver sensorDriver = null;
    private DynamicSensorCallback dynamicSensorCallback;
    private SensorEventListener temperatureListener;
    private SensorEventListener pressureListener;

    RainbowHatDataCollector(Context context, RainbowHatFactory factory) {
        this.context = context;
        this.factory = factory;
    }

    @Override
    public void register(final Consumer temperatureConsumer, final Consumer pressureConsumer) {
        registerSensorListeners(temperatureConsumer, pressureConsumer);
        registerSensors();
    }

    private void registerSensorListeners(final Consumer temperatureConsumer, final Consumer pressureConsumer) {
        final SensorManager sensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);
        dynamicSensorCallback = createDynamicSensorCallback(temperatureConsumer, pressureConsumer, sensorManager);
        sensorManager.registerDynamicSensorCallback(dynamicSensorCallback);
    }

    private DynamicSensorCallback createDynamicSensorCallback(final Consumer temperatureConsumer,
                                                              final Consumer pressureConsumer,
                                                              final SensorManager sensorManager) {
        return new DynamicSensorCallback() {
            @Override
            public void onDynamicSensorConnected(Sensor sensor) {
                super.onDynamicSensorConnected(sensor);
                if (sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE) {
                    temperatureListener = factory.getSensorEventListener(temperatureConsumer);
                    sensorManager.registerListener(temperatureListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
                }
                if (sensor.getType() == Sensor.TYPE_PRESSURE) {
                    pressureListener = factory.getSensorEventListener(pressureConsumer);
                    sensorManager.registerListener(pressureListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
                }
            }

            @Override
            public void onDynamicSensorDisconnected(Sensor sensor) {
                if (sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE) {
                    sensorManager.unregisterListener(temperatureListener);
                }
                if (sensor.getType() == Sensor.TYPE_PRESSURE) {
                    sensorManager.unregisterListener(pressureListener);
                }
                super.onDynamicSensorDisconnected(sensor);
            }
        };
    }

    private void registerSensors() {
        try {
            sensorDriver = RainbowHat.createSensorDriver();
            sensorDriver.registerTemperatureSensor();
            sensorDriver.registerPressureSensor();
        } catch (IOException e) {
            sensorDriver = null;
            e.printStackTrace();
        }
    }

    @Override
    public void unregister() {
        try {
            final SensorManager sensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);
            sensorManager.unregisterDynamicSensorCallback(dynamicSensorCallback);
            sensorDriver.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Much of this is simply duplication of what we did previously, we simply attach the new Consumer when the pressure sensor connects. Also, in registerSensors() we need to call registerPressureSensor() to initiate things.

To display the barometric pressure we’re going to use the seven multi-colour LEDs with each one representing a pressure band, as denoted by the pressure value which is physically printed on the Rainbow HAT beneath each LED. For barometric pressure, just knowing the current air pressure is of limited use – it’s useful to know whether the pressure is rising of falling as this provides a better indicator of what the weather is going to be like in the near future. To achieve this, I plan to light the LED representing the current pressure in green, whilst lighting the previous pressure in yellow to give an indication of how the pressure is trending.

The LEDs themselves are actually an APA102 LED strip which is addressed over the Serial Peripheral Interface (SPI) of the Raspberry Pi. SPI is a high speed serial interface through which we can address an unlimited number of individual devices. So SPI enables us to control all of the LEDs in the strip via a single interface object. To make life easier there is also a Rainbow HAT user-space driver which simplifies this further.

Our implementation of this is as follows:

class RainbowHatPressureDisplay implements Consumer {
    private static final int LED_COUNT = 7;
    private static final int SCALE_FACTOR = 10;
    private static final int MIN_PRESSURE = 97;
    private static final int BRIGHTNESS = 1;

    private int[] colours = new int[LED_COUNT];

    private int lastPressure = -1;

    @Override
    public void onCreate() {
        //NO-OP
    }

    @Override
    public void setValue(float pressure) {
        int newPressure = limit(((int) pressure / SCALE_FACTOR) - MIN_PRESSURE);
        setPressure(newPressure);
        if (newPressure != lastPressure) {
            lastPressure = newPressure;
        }
    }

    private int limit(int value) {
        return Math.max(Math.min(value, LED_COUNT), 0);
    }

    private void setPressure(int newPressure) {
        resetColours();
        colours[newPressure] = Color.GREEN;
        if (newPressure > 0 && lastPressure < newPressure) {
            colours[newPressure - 1] = Color.YELLOW;
        } else if (newPressure < colours.length - 1) {
            colours[newPressure + 1] = Color.YELLOW;
        }
        try {
            Apa102 ledStrip = new Apa102(RainbowHat.BUS_LEDSTRIP, Apa102.Mode.BGR, Apa102.Direction.REVERSED);
            ledStrip.setBrightness(BRIGHTNESS);
            ledStrip.write(colours);
            ledStrip.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void resetColours() {
        for (int i = 0; i < colours.length; i++) {
            colours[i] = Color.BLACK;
        }
    }

    @Override
    public void onDestroy() {
        //NO-OP
    }
}

The first thing worth noting is that the LEDs on the Rainbow HAT are addressed from right to left as we view the LEDs - this addressing corresponds to the numbers printed above the LEDs as we look at the board. However, our addressing would be simplified if we could reverse this as the lowest barometric pressure value will correspond to the leftmost LED and the highest value to the rightmost LED. When we create the Apa102 driver in the onCreate() method we specify this reversed direction as a constructor argument. We also specify the colour encoding for the LEDs - on the Rainbow HAT devices we need to send the blue component first, then the green component, then the red component. By specifying this in the constructor, we can pass in standard Java Color values (which are red first, then green, then blue) and the Apa102 driver will convert it for us. Also, we set a default brightness value which will be applied to all LEDs by the Apa102 driver.

setValue() is the method that gets called whenever new pressure data is available, and we first convert this to the index of the LED representing that pressure band. The limit() function constrains this to the supported range and prevents any index out of range errors.

The individual LED colours and states are controlled via an array of int colour values which we can specify using Color constants and values thanks to the colour encoding conversion done by the Apa102 driver.

setPressure() handles the logic around lighting the current pressure band as green, and the previous one as yellow.

Some people may be wondering why I do not keep the Apa102 instance around for the entire lifecycle of the RainbowHatPressureDisplay instance - i.e. create it in onCreate() and destroy it in onDestroy(). The reason for this is that, during my testing I found that, if I kept the instance active, the last value written was only applied when a new value was written. So it would always the the penultimate value sent that was displayed. This may be due to a bug in one of the low-level drivers, and I was able to get around it by actually writing the new colour values twice in succession. However this seemed hacky, so I opted to create a new instance each time which also overcomes the issue.

As before there are some additions to the Factory class which I won't bother including here, but are in the source code).

Finally we just need to wire this up in our MainActivity:

public class MainActivity extends Activity {

    private DataCollector dataCollector;
    private Consumer temperatureConsumer;
    private Consumer pressureConsumer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RainbowHatFactory factory = new RainbowHatFactory(this);
        dataCollector = factory.getDataCollector();
        temperatureConsumer = factory.getTemperatureConsumer();
        pressureConsumer = factory.getPressureConsumer();
        temperatureConsumer.onCreate();
        pressureConsumer.onCreate();
        dataCollector.register(temperatureConsumer, pressureConsumer);
    }

    @Override
    protected void onDestroy() {
        dataCollector.unregister();
        temperatureConsumer.onDestroy();
        pressureConsumer.onDestroy();

        super.onDestroy();
    }
}

That's it - we can now run this and see the barometric pressure is between 1010 and 1020 mbar and falling:

That concludes our look at some of the basics of using Android Things. This is based upon an early developer preview so there are still some rough edges, and I expect to see a huge increase in the number of user-space drivers which will appear in support of this.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. Thanks for posting this. I ran into the same Apa102 issue, but didn’t quite make the connection about the penultimate write being the one that worked. After reading your article, I just added a

    for (int i = 0; i < rainbow.length; i++) { rainbow[i] = ~0; } strip.write(rainbow);

    after every write and that seemed to fix it (not so different from what you did initially). The SPI protocol, and devices that speak it, is dirt simple, and my experience with other SPI devices is that a common issue is they need extra clocks on the bus just to satisfy their internal state machines. Writing ones (or sometimes zeros) seems to make many of them happy. As you say, this is a bug either in the Apa102 code or the underlying SPI code. Thanks again for the article; it was a big help.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.