[TUT] Android Things – Temperature Sensor, I2C on the Rainbow Hat

This post will explain how to use the I2C protocol to get input into your Android Things application. We’ll discuss the Raspberry Pi Rainbow Hat temperature sensor which we’ll use to measure air temperature surrounding your pi.

Hardware Prerequisite:

We have the Rainbow Hat connected to the top of our raspberry pi.

Software Prerequisite:

An AndroidThings project already set up in Android Studio, if you aren’t sure how to get your project to this point you can view the ‘first Android Things app’ blog for instructions.

Let’s get started.

First we create an instance of PeripheralManagerService. This is the AndroidThings SDK class that allows us to open connections to different board pins using the different available protocols.

PeripheralManagerService service = new PeripheralManagerService();

Using this instance, we want to open an I2C connection to our temperature sensor, the open method takes two parameters, first is the pin address and the second is the individual I2C peripheral address.

public class MainActivity extends Activity {

    private static final String I2C_ADDRESS = "I2C1";
    private static final int BMP280_TEMPERATURE_SENSOR_SLAVE = 0x77;

    private I2cDevice bus;

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

        PeripheralManagerService service = new PeripheralManagerService();
        try {
            bus = service.openI2cDevice(I2C_ADDRESS, BMP280_TEMPERATURE_SENSOR_SLAVE);
        } catch (IOException e) {
            throw new IllegalStateException(I2C_ADDRESS + " bus slave "
                                                + BMP280_TEMPERATURE_SENSOR_SLAVE + " connection cannot be opened.", e);
        }
  }
}

Since we opened the connection to our pin in the onCreate method, we should use the symmetrically matching pair from the Android Lifecycle to close the connection. Therefore we close the I2C bus connection in onDestroy.

    @Override
    protected void onDestroy() {
        try {
            bus.close();
        } catch (IOException e) {
            Log.e("TUT", I2C_ADDRESS + " bus slave "
                + BMP280_TEMPERATURE_SENSOR_SLAVE + "connection cannot be closed, you may experience errors on next launch.", e);
        }
        super.onDestroy();
    }

Note: All the specific information about this temperature sensor that we are about to discuss, if you are looking for it as a reference, is in the datasheet here.

The datasheet explains that whenever we read the temperature from the sensor it needs to be individually calibrated against an algorithm of the peripheral manufactures creation. This algorithm has 4 inputs, the temperature and 3 calibration numbers we need to read off the sensor.

We read the calibration data with the following code. Here we use the I2C API method read Reg Word, this method takes an address as a parameter and gives us back 1 short of data. We do this 3 times to read and store the calibration data into an array. Notice that we are doing this in onCreate so that the calibration data is read and ready before we start reading the temperature.

// as a field

    private final short[] calibrationData = new short[3];

// in onCreate
     ...
        try {
            calibrationData[0] = bus.readRegWord(REGISTER_TEMPERATURE_CALIBRATION_1);
            calibrationData[1] = bus.readRegWord(REGISTER_TEMPERATURE_CALIBRATION_2);
            calibrationData[2] = bus.readRegWord(REGISTER_TEMPERATURE_CALIBRATION_3);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot read calibration data, can't read temperature without it.", e);
        }

Let’s create a handler that we can post runnables to, these runnables will read the temperature at a fixed rate. We create the handler using the MainThreads looper, meaning we post messages on the MainThread, reading the temperature is a lightweight task and this gives a straight forward implementation.

// as a field
private Handler handler;

// in onCreate
 …
handler = new Handler(Looper.getMainLooper());

The datasheet tells us how to read the temperature. It shows that the most significant byte of the temperature is in a register with the name 0XFA and then two further bytes are needed for a full reading. It then mentions to read chapter 3.9 which has some further caveats on reading temperature, we’ll come to them in a second.

We implement this with the code below. The read reg buffer method takes 3 arguments, first the address to start reading at which we know from the datasheet is 0xFA, then an output variable which is the buffer it will write into and third argument is how many byte registers to read which the datasheet tells us is 3. When this executes it puts the raw temperature sensor into our data byte array.

            byte[] data = new byte[3];
            try {
                bus.readRegBuffer(0xFA, data, 3);
            } catch (IOException e) {
                Log.e("TUT", "Cannot read temperature from bus.", e);
            }

Our sampled raw temperature then needs to be compensated as explained in the datasheet. It uses the compensation data we read earlier. This is a very specific piece of code to this sensor and I’ve written it out from the datasheet but moved it to a separate class. It’s not something that needs to be learnt to understand I 2 C. However here is the code here. Once we’ve done this the value that comes out the other side is our temperature reading in degrees celsius.

class Bmp280DataSheet {

    /**
     * The BMP280 output consists of the Analog to Digital Converter output values.
     * However, each sensing element behaves differently,
     * and actual temperature must be calculated using a set of calibration parameters.
     * <p>
     * The calibration parameters are programmed into the devices’
     * non-volatile memory during production and cannot be altered.
     *
     * @param data            a2d output values
     * @param calibrationData constants from your specific peripherals mem, always size 3
     * @return temperature in degrees celsius
     */
    static float readTemperatureFrom(byte[] data, short[] calibrationData) {
        return compensateTemperature(readSample(data), calibrationData);
    }

    private static int readSample(byte[] data) {
        // msb[7:0] lsb[7:0] xlsb[7:4]
        int msb = data[0] & 0xff;
        int lsb = data[1] & 0xff;
        int xlsb = data[2] & 0xf0;
        // Convert to 20bit integer
        return (msb << 16 | lsb << 8 | xlsb) >> 4;
    }

    /**
     * Compensation formula from the BMP280 datasheet.
     * https://cdn-shop.adafruit.com/datasheets/BST-BMP280-DS001-11.pdf
     *
     * @param rawTemp         should be 20 bit format, positive, stored in a 32 bit signed integer
     * @param calibrationData constants from your specific peripherals mem, always size 3
     * @return temperature reading in degrees celcius
     */
    private static float compensateTemperature(int rawTemp, short[] calibrationData) {
        float digT1 = calibrationData[0];
        float digT2 = calibrationData[1];
        float digT3 = calibrationData[2];
        float adcT = (float) rawTemp;

        float varX1 = adcT / 16384f - digT1 / 1024f;
        float varX2 = varX1 * digT2;

        float varY1 = adcT / 131072f - digT1 / 8192f;
        float varY2 = varY1 * varY1;
        float varY3 = varY2 * digT3;

        return (varX2 + varY3) / 5120f;
    }
}

We can now use this conversion to get the temperature from our raw data reading.

            if (data.length != 0) {
                float temperature = Bmp280DataSheet.readTemperatureFrom(data, calibrationData);
                Log.d("TUT", "Got temperature of: " + temperature);
            }

To get this running in a constant loop. Hook up the runnable to run when the app starts adding the following code to the onStart method. Have the runnable repeat itself by posting again every time it gets to the end. And for best practice always remember to remove callbacks using the Android lifecycle symmetrically matching method which in this case is onStop.

    @Override
    protected void onStart() {
        super.onStart();
        handler.post(readTemperature);
    }

    private final Runnable readTemperature = new Runnable() {
        @Override
        public void run() {
            byte[] data = new byte[REGISTER_TEMPERATURE_RAW_VALUE_SIZE];
            try {
                bus.readRegBuffer(REGISTER_TEMPERATURE_RAW_VALUE_START, data, REGISTER_TEMPERATURE_RAW_VALUE_SIZE);
            } catch (IOException e) {
                Log.e("TUT", "Cannot read temperature from bus.", e);
            }
            if (data.length != 0) {
                float temperature = Bmp280DataSheet.readTemperatureFrom(data, calibrationData);
                Log.d("TUT", "Got temperature of: " + temperature);
            }

            handler.postDelayed(readTemperature, TimeUnit.HOURS.toMillis(1));
        }
    };

    @Override
    protected void onStop() {
        handler.removeCallbacks(readTemperature);
        super.onStop();

That’s it!

You now understand how the Android Things I2C input communication with peripherals works. You can interface with a temperature sensor reading its words and buffers. This knowledge also transfers to reading other datasheets and coding other I2C peripherals such as accelerometers, pressure sensors or fingerprint readers.

The source is on GitHub and all the code is available here.

If you are more of an audible learning, or prefer video content. You can check out my Android Things introductory video course on Caster.io.