diff --git a/hts221/.gitignore b/hts221/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/hts221/.gitignore @@ -0,0 +1 @@ +/build diff --git a/hts221/README.md b/hts221/README.md new file mode 100644 index 0000000..e8544c3 --- /dev/null +++ b/hts221/README.md @@ -0,0 +1,131 @@ +HTS221 driver for Android Things +================================ + +This driver supports STMicroelectronics [HTS221][product_hts221] capacitive digital sensor for +relative humidity and temperature. + +NOTE: these drivers are not production-ready. They are offered as sample +implementations of Android Things user space drivers for common peripherals +as part of the Developer Preview release. There is no guarantee +of correctness, completeness or robustness. + +How to use the driver +--------------------- + +### Gradle dependency + +To use the `hts221` driver, simply add the line below to your project's `build.gradle`, +where `` matches the last version of the driver available on [jcenter][jcenter]. + +``` +dependencies { + compile 'com.google.android.things.contrib:driver-hts221:' +} +``` + +### Sample usage + +```java +import com.google.android.things.contrib.driver.hts221.Hts221; + +// Access the environmental sensor: + +Hts221 mHts221; + +try { + mHts221 = new Hts221(i2cBusName); +} catch (IOException e) { + // Couldn't configure the device... +} + +// Read the current humidity: + +try { + float humidity = mHts221.readHumidity(); +} catch (IOException e) { + // Error reading humidity +} + +// Read the current temperature: + +try { + float temperature = mHts221.readTemperature(); +} catch (IOException e) { + // Error reading temperature +} + +// Close the environmental sensor when finished: + +try { + mHts221.close(); +} catch (IOException e) { + // Error closing sensor +} +``` + +If you need to read sensor values continuously, you can register the HTS221 with the system and +listen for sensor values using the [Sensor APIs][sensors]: + +```java +SensorManager mSensorManager = getSystemService(Context.SENSOR_SERVICE); +SensorEventListener mHumidityListener = ...; +SensorEventListener mTemperatureListener = ...; +Hts221SensorDriver mSensorDriver; + +mSensorManager.registerDynamicSensorCallback(new SensorManager.DynamicSensorCallback() { + @Override + public void onDynamicSensorConnected(Sensor sensor) { + if (sensor.getType() == Sensor.TYPE_RELATIVE_HUMIDITY) { + mSensorManager.registerListener(mHumidityListener, sensor, + SensorManager.SENSOR_DELAY_NORMAL); + } else if (sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE) { + mSensorManager.registerListener(mTemperatureListener, sensor, + SensorManager.SENSOR_DELAY_NORMAL); + } + } +}); + +try { + mSensorDriver = new Hts221SensorDriver(i2cBusName); + mSensorDriver.registerHumiditySensor(); + mSensorDriver.registerTemperatureSensor(); +} catch (IOException e) { + // Error configuring sensor +} + +// Unregister and close the driver when finished: + +mSensorManager.unregisterListener(mHumidityListener); +mSensorManager.unregisterListener(mTemperatureListener); +mSensorDriver.unregisterHumiditySensor(); +mSensorDriver.unregisterTemperatureSensor(); +try { + mSensorDriver.close(); +} catch (IOException e) { + // Error closing sensor +} +``` + +License +------- + +Copyright 2016 Macro Yau + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + +[product_hts221]: http://www.st.com/en/mems-and-sensors/hts221.html +[jcenter]: https://bintray.com/google/androidthings/contrib-driver-hts221/_latestVersion +[sensors]: https://developer.android.com/guide/topics/sensors/sensors_overview.html diff --git a/hts221/build.gradle b/hts221/build.gradle new file mode 100644 index 0000000..3d0d039 --- /dev/null +++ b/hts221/build.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 24 + buildToolsVersion '24.0.3' + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + } +} + +dependencies { + provided 'com.google.android.things:androidthings:0.2-devpreview' + compile 'com.android.support:support-annotations:24.2.0' + + testCompile project(':testingutils') + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' +} diff --git a/hts221/publish.gradle b/hts221/publish.gradle new file mode 100644 index 0000000..3f73c00 --- /dev/null +++ b/hts221/publish.gradle @@ -0,0 +1,97 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * To publish on Bintray: + * - set environmental variables BINTRAY_USER and BINTRAY_API_KEY to proper values + * - from this directory: + * ../gradlew -b publish.gradle bintrayUpload + * + */ + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.1' + } +} + +plugins { + id "com.jfrog.bintray" version "1.7" +} + +allprojects { + repositories { + jcenter() + } +} + +apply from: 'build.gradle' +apply plugin: 'maven-publish' + +def packageVersion = '0.1' + +task sourceJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +publishing { + publications { + driverPublish(MavenPublication) { + groupId 'com.google.android.things.contrib' + artifactId "driver-$project.name" + version packageVersion + artifacts = configurations.archives.artifacts + artifact sourceJar + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + configurations.compile.allDependencies.each { + if(it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) + { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_API_KEY') + publications = ['driverPublish'] + + publish = true + + pkg { + repo = 'androidthings' + name = "contrib-driver-$project.name" + userOrg = 'google' + + version { + name = packageVersion + gpg { + sign = true + } + } + } +} diff --git a/hts221/src/main/AndroidManifest.xml b/hts221/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6a041e1 --- /dev/null +++ b/hts221/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221.java b/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221.java new file mode 100644 index 0000000..cb62e4a --- /dev/null +++ b/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221.java @@ -0,0 +1,465 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.hts221; + +import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; + +import com.google.android.things.pio.I2cDevice; +import com.google.android.things.pio.PeripheralManagerService; + +import java.io.IOException; + +/** + * Driver for the HTS221 relative humidity and temperature sensor. + * + * @see HTS221 datasheet + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Hts221 implements AutoCloseable { + + // Sensor constants obtained from the datasheet + /** + * Device ID of the sensor. + */ + public static final int DEVICE_ID = 0xBC; // Value of the WHO_AM_I register + /** + * I2C address of the sensor. + */ + public static final int I2C_ADDRESS = 0x5F; + /** + * Minimum relative humidity that the sensor can measure. + */ + public static final float MIN_HUMIDITY_PERCENT = 0f; + /** + * Maximum relative humidity that the sensor can measure. + */ + public static final float MAX_HUMIDITY_PERCENT = 100f; + /** + * Minimum temperature in Celsius that the sensor can measure. + */ + public static final float MIN_TEMP_C = -40f; + /** + * Maximum temperature in Celsius that the sensor can measure. + */ + public static final float MAX_TEMP_C = 120f; + /** + * Maximum power consumption in micro-amperes when measuring relative humidity and temperature. + */ + public static final float MAX_POWER_CONSUMPTION_UA = 22.5f; + /** + * Maximum frequency of the measurements. + */ + public static final float MAX_FREQ_HZ = 12.5f; + /** + * Minimum frequency of the measurements. + */ + public static final float MIN_FREQ_HZ = 1f; + + /** + * Humidity average configuration. + */ + @IntDef({AV_CONF_AVGH_4, AV_CONF_AVGH_8, AV_CONF_AVGH_16, AV_CONF_AVGH_32, AV_CONF_AVGH_64, + AV_CONF_AVGH_128, AV_CONF_AVGH_256, AV_CONF_AVGH_512}) + public @interface HumidityAverageConfiguration { + } + + public static final int AV_CONF_AVGH_4 = 0b000; + public static final int AV_CONF_AVGH_8 = 0b001; + public static final int AV_CONF_AVGH_16 = 0b010; + public static final int AV_CONF_AVGH_32 = 0b011; // Default + public static final int AV_CONF_AVGH_64 = 0b100; + public static final int AV_CONF_AVGH_128 = 0b101; + public static final int AV_CONF_AVGH_256 = 0b110; + public static final int AV_CONF_AVGH_512 = 0b111; + + /** + * Temperature average configuration. + */ + @IntDef({AV_CONF_AVGT_2, AV_CONF_AVGT_4, AV_CONF_AVGT_8, AV_CONF_AVGT_16, AV_CONF_AVGT_32, + AV_CONF_AVGT_64, AV_CONF_AVGT_128, AV_CONF_AVGT_256}) + public @interface TemperatureAverageConfiguration { + } + + public static final int AV_CONF_AVGT_2 = 0b000; + public static final int AV_CONF_AVGT_4 = 0b001; + public static final int AV_CONF_AVGT_8 = 0b010; + public static final int AV_CONF_AVGT_16 = 0b011; // Default + public static final int AV_CONF_AVGT_32 = 0b100; + public static final int AV_CONF_AVGT_64 = 0b101; + public static final int AV_CONF_AVGT_128 = 0b110; + public static final int AV_CONF_AVGT_256 = 0b111; + + /** + * Power mode. + */ + @IntDef({MODE_POWER_DOWN, MODE_ACTIVE}) + public @interface Mode { + } + + public static final int MODE_POWER_DOWN = 0; + public static final int MODE_ACTIVE = 1; + + /** + * Output data rate configuration. + */ + @IntDef({HTS221_ODR_ONE_SHOT, HTS221_ODR_1_HZ, HTS221_ODR_7_HZ, HTS221_ODR_12_5_HZ}) + public @interface OutputDataRate { + } + + public static final int HTS221_ODR_ONE_SHOT = 0b00; + public static final int HTS221_ODR_1_HZ = 0b01; + public static final int HTS221_ODR_7_HZ = 0b10; + public static final int HTS221_ODR_12_5_HZ = 0b11; + + // Registers + private static final int HTS221_REG_WHO_AM_I = 0x0F; // R + private static final int HTS221_REG_AV_CONF = 0x10; // R/W + private static final int HTS221_REG_CTRL_REG1 = 0x20; // R/W + private static final int HTS221_REG_CTRL_REG2 = 0x21; // R/W + private static final int HTS221_REG_CTRL_REG3 = 0x22; // R/W + private static final int HTS221_REG_STATUS_REG = 0x27; // R + private static final int HTS221_REG_HUMIDITY_OUT_L = 0x28; // R + private static final int HTS221_REG_HUMIDITY_OUT_H = 0x29; // R + private static final int HTS221_REG_TEMP_OUT_L = 0x2A; // R + private static final int HTS221_REG_TEMP_OUT_H = 0x2B; // R + + // Calibration registers + private static final int HTS221_REG_H0_RH_X2 = 0x30; + private static final int HTS221_REG_H1_RH_X2 = 0x31; + private static final int HTS221_REG_T0_DEGC_X8 = 0x32; + private static final int HTS221_REG_T1_DEGC_X8 = 0x33; + private static final int HTS221_REG_T1_T0_MSB = 0x35; + private static final int HTS221_REG_H0_T0_OUT_L = 0x36; + private static final int HTS221_REG_H0_T0_OUT_H = 0x37; + private static final int HTS221_REG_H1_T0_OUT_L = 0x3A; + private static final int HTS221_REG_H1_T0_OUT_H = 0x3B; + private static final int HTS221_REG_T0_OUT_L = 0x3C; + private static final int HTS221_REG_T0_OUT_H = 0x3D; + private static final int HTS221_REG_T1_OUT_L = 0x3E; + private static final int HTS221_REG_T1_OUT_H = 0x3F; + + // Bit masks for CTRL_REG1 + private static final int HTS221_POWER_DOWN_MASK = 0b10000000; + private static final int HTS221_BDU_MASK = 0b00000100; + private static final int HTS221_ODR_MASK = 0b00000011; + + private I2cDevice mDevice; + + private final byte[] mBuffer = new byte[2]; // For reading registers + + private int mMode; + private boolean mBlockDataUpdate; + private int mOutputDataRate; + private float[] mTemperatureCalibration, mHumidityCalibration; // Calibration parameters + + /** + * Creates a new HTS221 sensor driver connected on the given bus. + * + * @param bus the I2C bus the sensor is connected to + * @throws IOException + */ + public Hts221(String bus) throws IOException { + PeripheralManagerService pioService = new PeripheralManagerService(); + I2cDevice device = pioService.openI2cDevice(bus, I2C_ADDRESS); + try { + connect(device); + } catch (IOException | RuntimeException e) { + try { + close(); + } catch (IOException | RuntimeException ignored) { + } + throw e; + } + } + + /** + * Creates a new HTS221 sensor driver connected to the given I2C device. + * + * @param device the I2C device of the sensor + * @throws IOException + */ + /*package*/ Hts221(I2cDevice device) throws IOException { + connect(device); + } + + private void connect(I2cDevice device) throws IOException { + mDevice = device; + + if ((mDevice.readRegByte(HTS221_REG_WHO_AM_I) & 0xFF) != DEVICE_ID) { + throw new IllegalStateException("I2C device is not HTS221 sensor"); + } + + setAveragedSamples(AV_CONF_AVGH_32, AV_CONF_AVGT_16); + setBlockDataUpdate(true); + setOutputDataRate(HTS221_ODR_1_HZ); + setMode(MODE_ACTIVE); + + readCalibration(); + } + + /** + * Sets the power mode of the sensor as power-down or active. + * + * @param mode must be either {@link #MODE_POWER_DOWN} or {@link #MODE_ACTIVE} + * @throws IOException + */ + public void setMode(@Mode int mode) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(HTS221_REG_CTRL_REG1) & 0xFF; + if (mode == MODE_POWER_DOWN) { + regCtrl &= (~HTS221_POWER_DOWN_MASK & 0xFF); + } else { + regCtrl |= HTS221_POWER_DOWN_MASK; + } + mDevice.writeRegByte(HTS221_REG_CTRL_REG1, (byte) regCtrl); + mMode = mode; + } + + /** + * Returns the power mode of the sensor. + * + * @return true if the sensor is active + * @see #setMode(int) + */ + public boolean isEnabled() { + return mMode == MODE_ACTIVE; + } + + /** + * Sets the block data update (BDU) bit in the control register 1 (CTRL_REG1) of the sensor. + * Reading MSB and LSB of different samples can be avoided by enabling BDU. + * + * @param enabled enable BDU if true + * @throws IOException + */ + public void setBlockDataUpdate(boolean enabled) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(HTS221_REG_CTRL_REG1) & 0xFF; + regCtrl &= (~HTS221_BDU_MASK & 0xFF); + if (enabled) { + regCtrl |= HTS221_BDU_MASK; + } + mDevice.writeRegByte(HTS221_REG_CTRL_REG1, (byte) regCtrl); + mBlockDataUpdate = enabled; + } + + /** + * Returns the status of the block data update mode. + * + * @return true if the block data update mode is enabled + * @see #setBlockDataUpdate(boolean) + */ + public boolean isBlockDataUpdateEnabled() { + return mBlockDataUpdate; + } + + /** + * Configures the output data rate (ODR) of the humidity and temperature measurements. + * + * @param outputDataRate the configuration must be one of {@link #HTS221_ODR_ONE_SHOT}, + * {@link #HTS221_ODR_1_HZ}, {@link #HTS221_ODR_7_HZ} or + * {@link #HTS221_ODR_12_5_HZ} + * @throws IOException + */ + public void setOutputDataRate(@OutputDataRate int outputDataRate) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(HTS221_REG_CTRL_REG1) & 0xFF; + regCtrl &= (~HTS221_ODR_MASK & 0xFF); + regCtrl |= outputDataRate; + mDevice.writeRegByte(HTS221_REG_CTRL_REG1, (byte) regCtrl); + mOutputDataRate = outputDataRate; + } + + /** + * Returns the configured output data rate. + * + * @return the output data rate + * @see #setOutputDataRate(int) + */ + public int getOutputDataRate() { + return mOutputDataRate; + } + + /** + * Configures the number of averaged samples for the humidity and temperature measurements. + * + * @param humidityAverage the humidity average configuration must be one of the + * AV_CONF_AVGH* constants + * @param temperatureAverage the temperature average configuration must be one of the + * AV_CONF_AVGT* constants + * @throws IOException + */ + public void setAveragedSamples(@HumidityAverageConfiguration int humidityAverage, + @TemperatureAverageConfiguration int temperatureAverage) + throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(HTS221_REG_AV_CONF) & 0xC0; + regCtrl |= humidityAverage | (temperatureAverage << 3); + mDevice.writeRegByte(HTS221_REG_AV_CONF, (byte) (regCtrl)); + } + + /** + * Closes the driver and its underlying device. + */ + @Override + public void close() throws IOException { + if (mDevice != null) { + try { + setMode(MODE_POWER_DOWN); + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + private void readCalibration() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + mBuffer[0] = mDevice.readRegByte(HTS221_REG_H0_RH_X2); + mBuffer[1] = mDevice.readRegByte(HTS221_REG_H1_RH_X2); + int h0 = (mBuffer[0] & 0xFF); + int h1 = (mBuffer[1] & 0xFF); + + int h0T0Out = (short) readRegister(HTS221_REG_H0_T0_OUT_L); + int h1T0Out = (short) readRegister(HTS221_REG_H1_T0_OUT_L); + + int t0 = mDevice.readRegByte(HTS221_REG_T0_DEGC_X8) & 0xFF; + int t1 = mDevice.readRegByte(HTS221_REG_T1_DEGC_X8) & 0xFF; + int msb = mDevice.readRegByte(HTS221_REG_T1_T0_MSB) & 0x0F; + t0 |= (msb & 0x03) << 8; + t1 |= (msb & 0x0C) << 6; + + int t0Out = (short) readRegister(HTS221_REG_T0_OUT_L); + int t1Out = (short) readRegister(HTS221_REG_T1_OUT_L); + + mHumidityCalibration = calibrateHumidityParameters(h0, h1, h0T0Out, h1T0Out); + mTemperatureCalibration = calibrateTemperatureParameters(t0, t1, t0Out, t1Out); + } + + @VisibleForTesting + static float[] calibrateHumidityParameters(int h0, int h1, int h0T0Out, int h1T0Out) { + float[] humidityParameters = new float[2]; + humidityParameters[0] = ((h1 - h0) / 2.0f) / (h1T0Out - h0T0Out); + humidityParameters[1] = (h0 / 2.0f) - (humidityParameters[0] * h0T0Out); + return humidityParameters; + } + + @VisibleForTesting + static float[] calibrateTemperatureParameters(int t0, int t1, int t0Out, int t1Out) { + float[] temperatureParameters = new float[2]; + temperatureParameters[0] = ((t1 - t0) / 8.0f) / (t1Out - t0Out); + temperatureParameters[1] = (t0 / 8.0f) - (temperatureParameters[0] * t0Out); + return temperatureParameters; + } + + /** + * Reads the current humidity. + * + * @return the current relative humidity + */ + public float readHumidity() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + if (isHumidityDataAvailable()) { + int rawHumidity = (short) readRegister(HTS221_REG_HUMIDITY_OUT_L); + return compensateSample(rawHumidity, mHumidityCalibration); + } else { + throw new IOException("Humidity data is not yet available"); + } + } + + /** + * Reads the current temperature. + * + * @return the current temperature in degrees Celsius + */ + public float readTemperature() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + if (isTemperatureDataAvailable()) { + int rawTemp = (short) readRegister(HTS221_REG_TEMP_OUT_L); + return compensateSample(rawTemp, mTemperatureCalibration); + } else { + throw new IOException("Temperature data is not yet available"); + } + } + + /** + * Reads 16 bits from two 8-bit registers at the given address. + * + * @param address the address of the least significant byte (LSB) + * @throws IOException + */ + private int readRegister(int address) throws IOException { + mDevice.readRegBuffer(address | 0x80, mBuffer, 2); + return ((mBuffer[1] & 0xFF) << 8) | (mBuffer[0] & 0xFF); + } + + /** + * Validates the availability of updated humidity data. + * + * @return true if the humidity data is updated + * @throws IOException + */ + private boolean isHumidityDataAvailable() throws IOException { + return (mDevice.readRegByte(HTS221_REG_STATUS_REG) & 0x02) != 0; + } + + /** + * Validates the availability of updated temperature data. + * + * @return true if the temperature data is updated + * @throws IOException + */ + private boolean isTemperatureDataAvailable() throws IOException { + return (mDevice.readRegByte(HTS221_REG_STATUS_REG) & 0x01) != 0; + } + + /** + * Returns the sensor reading compensated with the given calibration parameters. + * + * @param rawValue the raw sensor reading value + * @param calibration the calibration parameters, where calibration[0] is the slope + * and calibration[1] is the intercept + * @return the sensor reading compensated with calibration parameters + */ + @VisibleForTesting + static float compensateSample(int rawValue, float[] calibration) { + return rawValue * calibration[0] + calibration[1]; + } + +} diff --git a/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221SensorDriver.java b/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221SensorDriver.java new file mode 100644 index 0000000..9c80610 --- /dev/null +++ b/hts221/src/main/java/com/google/android/things/contrib/driver/hts221/Hts221SensorDriver.java @@ -0,0 +1,233 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.hts221; + +import android.hardware.Sensor; + +import com.google.android.things.userdriver.UserDriverManager; +import com.google.android.things.userdriver.UserSensor; +import com.google.android.things.userdriver.UserSensorDriver; +import com.google.android.things.userdriver.UserSensorReading; + +import java.io.IOException; +import java.util.UUID; + +/** + * User-space driver for interfacing the HTS221 relative humidity and temperature sensor to the + * Android SensorManager framework. + */ +public class Hts221SensorDriver implements AutoCloseable { + + private static final String DRIVER_VENDOR = "STMicroelectronics"; + private static final String DRIVER_NAME = "HTS221"; + private static final int DRIVER_MIN_DELAY_US = Math.round(1000000.0f / Hts221.MAX_FREQ_HZ); + private static final int DRIVER_MAX_DELAY_US = Math.round(1000000.0f / Hts221.MIN_FREQ_HZ); + + private Hts221 mDevice; + + private HumidityUserDriver mHumidityUserDriver; + private TemperatureUserDriver mTemperatureUserDriver; + + /** + * Creates a new framework sensor driver connected on the given bus. + * The driver emits {@link Sensor} with humidity and temperature data when registered. + * + * @param bus the I2C bus the sensor is connected to + * @throws IOException + * @see #registerHumiditySensor() + * @see #registerTemperatureSensor() + */ + public Hts221SensorDriver(String bus) throws IOException { + mDevice = new Hts221(bus); + } + + /** + * Closes the driver and its underlying device. + * + * @throws IOException + */ + @Override + public void close() throws IOException { + unregisterTemperatureSensor(); + unregisterHumiditySensor(); + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + /** + * Registers a {@link UserSensor} that pipes humidity readings into the Android SensorManager. + * + * @see #unregisterHumiditySensor() + */ + public void registerHumiditySensor() { + if (mDevice == null) { + throw new IllegalStateException("Cannot register closed driver"); + } + + if (mHumidityUserDriver == null) { + mHumidityUserDriver = new HumidityUserDriver(); + UserDriverManager.getManager().registerSensor(mHumidityUserDriver.getUserSensor()); + } + } + + /** + * Registers a {@link UserSensor} that pipes temperature readings into the Android SensorManager. + * + * @see #unregisterTemperatureSensor() + */ + public void registerTemperatureSensor() { + if (mDevice == null) { + throw new IllegalStateException("Cannot register closed driver"); + } + + if (mTemperatureUserDriver == null) { + mTemperatureUserDriver = new TemperatureUserDriver(); + UserDriverManager.getManager().registerSensor(mTemperatureUserDriver.getUserSensor()); + } + } + + /** + * Unregisters the humidity {@link UserSensor}. + */ + public void unregisterHumiditySensor() { + if (mHumidityUserDriver != null) { + UserDriverManager.getManager().unregisterSensor(mHumidityUserDriver.getUserSensor()); + mHumidityUserDriver = null; + } + } + + /** + * Unregisters the temperature {@link UserSensor}. + */ + public void unregisterTemperatureSensor() { + if (mTemperatureUserDriver != null) { + UserDriverManager.getManager().unregisterSensor(mTemperatureUserDriver.getUserSensor()); + mTemperatureUserDriver = null; + } + } + + private void maybeSleep() throws IOException { + if ((mTemperatureUserDriver == null || !mTemperatureUserDriver.isEnabled()) && + (mHumidityUserDriver == null || !mHumidityUserDriver.isEnabled())) { + mDevice.setMode(Hts221.MODE_POWER_DOWN); + } else { + mDevice.setMode(Hts221.MODE_ACTIVE); + } + } + + private class HumidityUserDriver extends UserSensorDriver { + + private static final float DRIVER_MAX_RANGE = Hts221.MAX_HUMIDITY_PERCENT; + private static final float DRIVER_RESOLUTION = 0.004f; + private static final float DRIVER_POWER = Hts221.MAX_POWER_CONSUMPTION_UA / 1000f; + private static final int DRIVER_VERSION = 1; + private static final String DRIVER_REQUIRED_PERMISSION = ""; + + private boolean mEnabled; + private UserSensor mUserSensor; + + private UserSensor getUserSensor() { + if (mUserSensor == null) { + mUserSensor = UserSensor.builder() + .setType(Sensor.TYPE_RELATIVE_HUMIDITY) + .setName(DRIVER_NAME) + .setVendor(DRIVER_VENDOR) + .setVersion(DRIVER_VERSION) + .setMaxRange(DRIVER_MAX_RANGE) + .setResolution(DRIVER_RESOLUTION) + .setPower(DRIVER_POWER) + .setMinDelay(DRIVER_MIN_DELAY_US) + .setRequiredPermission(DRIVER_REQUIRED_PERMISSION) + .setMaxDelay(DRIVER_MAX_DELAY_US) + .setUuid(UUID.randomUUID()) + .setDriver(this) + .build(); + } + return mUserSensor; + } + + @Override + public UserSensorReading read() throws IOException { + return new UserSensorReading(new float[]{mDevice.readHumidity()}); + } + + @Override + public void setEnabled(boolean enabled) throws IOException { + mEnabled = enabled; + maybeSleep(); + } + + private boolean isEnabled() { + return mEnabled; + } + + } + + private class TemperatureUserDriver extends UserSensorDriver { + + private static final float DRIVER_MAX_RANGE = Hts221.MAX_TEMP_C; + private static final float DRIVER_RESOLUTION = 0.016f; + private static final float DRIVER_POWER = Hts221.MAX_POWER_CONSUMPTION_UA / 1000f; + private static final int DRIVER_VERSION = 1; + private static final String DRIVER_REQUIRED_PERMISSION = ""; + + private boolean mEnabled; + private UserSensor mUserSensor; + + private UserSensor getUserSensor() { + if (mUserSensor == null) { + mUserSensor = UserSensor.builder() + .setType(Sensor.TYPE_AMBIENT_TEMPERATURE) + .setName(DRIVER_NAME) + .setVendor(DRIVER_VENDOR) + .setVersion(DRIVER_VERSION) + .setMaxRange(DRIVER_MAX_RANGE) + .setResolution(DRIVER_RESOLUTION) + .setPower(DRIVER_POWER) + .setMinDelay(DRIVER_MIN_DELAY_US) + .setRequiredPermission(DRIVER_REQUIRED_PERMISSION) + .setMaxDelay(DRIVER_MAX_DELAY_US) + .setUuid(UUID.randomUUID()) + .setDriver(this) + .build(); + } + return mUserSensor; + } + + @Override + public UserSensorReading read() throws IOException { + return new UserSensorReading(new float[]{mDevice.readTemperature()}); + } + + @Override + public void setEnabled(boolean enabled) throws IOException { + mEnabled = enabled; + maybeSleep(); + } + + private boolean isEnabled() { + return mEnabled; + } + + } + +} diff --git a/hts221/src/test/java/com/google/android/things/contrib/driver/hts221/Hts221Test.java b/hts221/src/test/java/com/google/android/things/contrib/driver/hts221/Hts221Test.java new file mode 100644 index 0000000..1d429f6 --- /dev/null +++ b/hts221/src/test/java/com/google/android/things/contrib/driver/hts221/Hts221Test.java @@ -0,0 +1,265 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.hts221; + +import com.google.android.things.pio.I2cDevice; + +import static com.google.android.things.contrib.driver.testutils.BitsMatcher.hasBitsSet; + +import junit.framework.Assert; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.byteThat; +import static org.mockito.Matchers.eq; + +public class Hts221Test { + + @Mock + I2cDevice mI2c; + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException mExpectedException = ExpectedException.none(); + + @Test + public void testCompensateHumidity() { + // Example from the datasheet + // http://www.st.com/resource/en/datasheet/hts221.pdf + int h0T0Out = (short) 0x4000; + int h1T0Out = (short) 0x6000; + int hOut = (short) 0x5000; + int h0 = 40; + int h1 = 80; + + float[] calibration = Hts221.calibrateHumidityParameters(h0, h1, h0T0Out, h1T0Out); + Assert.assertEquals(30.0f, Hts221.compensateSample(hOut, calibration)); + } + + @Test + public void testCompensateTemperature() { + // Example from the datasheet + // http://www.st.com/resource/en/datasheet/hts221.pdf + int t0Out = (short) 300; + int t1Out = (short) 500; + int tOut = (short) 400; + int t0 = 80; + int t1 = 160; + + float[] calibration = Hts221.calibrateTemperatureParameters(t0, t1, t0Out, t1Out); + Assert.assertEquals(15.0f, Hts221.compensateSample(tOut, calibration)); + } + + @Test + public void close() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + Mockito.verify(mI2c).close(); + } + + @Test + public void close_safeToCallTwice() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + hts221.close(); // Should not throw + } + + @Test + public void setMode() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + + Mockito.reset(mI2c); + + hts221.setMode(Hts221.MODE_ACTIVE); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x80))); + } + + @Test + public void setMode_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.setMode(Hts221.MODE_ACTIVE); + } + + @Test + public void setBlockDataUpdate() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + + // Disable BDU + hts221.setBlockDataUpdate(false); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x00))); + + Mockito.reset(mI2c); + + // Enable BDU + hts221.setBlockDataUpdate(true); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x04))); + } + + @Test + public void setBlockDataUpdate_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.setBlockDataUpdate(true); + } + + @Test + public void setOutputDataRate() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + + Mockito.reset(mI2c); + + // One-shot + hts221.setOutputDataRate(Hts221.HTS221_ODR_ONE_SHOT); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x00))); + + Mockito.reset(mI2c); + + // 1 Hz + hts221.setOutputDataRate(Hts221.HTS221_ODR_1_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x01))); + + Mockito.reset(mI2c); + + // 7 Hz + hts221.setOutputDataRate(Hts221.HTS221_ODR_7_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x02))); + + Mockito.reset(mI2c); + + // 12.5 Hz + hts221.setOutputDataRate(Hts221.HTS221_ODR_12_5_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x03))); + } + + @Test + public void setOutputDataRate_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.setOutputDataRate(Hts221.HTS221_ODR_12_5_HZ); + } + + @Test + public void setAveragedSamples() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + + // AVGH_64 + AVGT_2 + hts221.setAveragedSamples(Hts221.AV_CONF_AVGH_64, Hts221.AV_CONF_AVGT_2); + Mockito.verify(mI2c).writeRegByte(eq(0x10), byteThat(hasBitsSet((byte) 0x04))); + + Mockito.reset(mI2c); + + // AVGH_512 + AVGT_256 + hts221.setAveragedSamples(Hts221.AV_CONF_AVGH_512, Hts221.AV_CONF_AVGT_256); + Mockito.verify(mI2c).writeRegByte(eq(0x10), byteThat(hasBitsSet((byte) 0x3F))); + } + + @Test + public void setAveragedSamples_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.setAveragedSamples(Hts221.AV_CONF_AVGH_512, Hts221.AV_CONF_AVGT_256); + } + + @Test + public void readHumidity() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x02); + hts221.readHumidity(); + Mockito.verify(mI2c).readRegBuffer(eq(0x28 | 0x80), any(byte[].class), eq(2)); + } + + @Test + public void readHumidity_throwsIfDataNotYetAvailable() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x00); + mExpectedException.expect(IOException.class); + mExpectedException.expectMessage("Humidity data is not yet available"); + hts221.readHumidity(); + } + + @Test + public void readHumidity_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.readHumidity(); + } + + @Test + public void readTemperature() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x01); + hts221.readTemperature(); + Mockito.verify(mI2c).readRegBuffer(eq(0x2A | 0x80), any(byte[].class), eq(2)); + } + + @Test + public void readTemperature_throwsIfDataNotYetAvailable() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x00); + mExpectedException.expect(IOException.class); + mExpectedException.expectMessage("Temperature data is not yet available"); + hts221.readTemperature(); + } + + @Test + public void readTemperature_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBC); + Hts221 hts221 = new Hts221(mI2c); + hts221.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + hts221.readTemperature(); + } + +} diff --git a/lps25h/.gitignore b/lps25h/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lps25h/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lps25h/README.md b/lps25h/README.md new file mode 100644 index 0000000..f27b5a9 --- /dev/null +++ b/lps25h/README.md @@ -0,0 +1,130 @@ +LPS25H driver for Android Things +================================ + +This driver supports STMicroelectronics [LPS25H][product_lps25h] MEMS pressure sensor. + +NOTE: these drivers are not production-ready. They are offered as sample +implementations of Android Things user space drivers for common peripherals +as part of the Developer Preview release. There is no guarantee +of correctness, completeness or robustness. + +How to use the driver +--------------------- + +### Gradle dependency + +To use the `lps25h` driver, simply add the line below to your project's `build.gradle`, +where `` matches the last version of the driver available on [jcenter][jcenter]. + +``` +dependencies { + compile 'com.google.android.things.contrib:driver-lps25h:' +} +``` + +### Sample usage + +```java +import com.google.android.things.contrib.driver.hts221.Lps25h; + +// Access the environmental sensor: + +Lps25h mLps25h; + +try { + mLps25h = new Lps25h(i2cBusName); +} catch (IOException e) { + // Couldn't configure the device... +} + +// Read the current pressure: + +try { + float pressure = mLps25h.readPressure(); +} catch (IOException e) { + // Error reading pressure +} + +// Read the current temperature: + +try { + float temperature = mLps25h.readTemperature(); +} catch (IOException e) { + // Error reading temperature +} + +// Close the pressure sensor when finished: + +try { + mLps25h.close(); +} catch (IOException e) { + // Error closing sensor +} +``` + +If you need to read sensor values continuously, you can register the LPS25H with the system and +listen for sensor values using the [Sensor APIs][sensors]: + +```java +SensorManager mSensorManager = getSystemService(Context.SENSOR_SERVICE); +SensorEventListener mPressureListener = ...; +SensorEventListener mTemperatureListener = ...; +Lps25hSensorDriver mSensorDriver; + +mSensorManager.registerDynamicSensorCallback(new SensorManager.DynamicSensorCallback() { + @Override + public void onDynamicSensorConnected(Sensor sensor) { + if (sensor.getType() == Sensor.TYPE_PRESSURE) { + mSensorManager.registerListener(mPressureListener, sensor, + SensorManager.SENSOR_DELAY_NORMAL); + } else if (sensor.getType() == Sensor.TYPE_AMBIENT_TEMPERATURE) { + mSensorManager.registerListener(mTemperatureListener, sensor, + SensorManager.SENSOR_DELAY_NORMAL); + } + } +}); + +try { + mSensorDriver = new Lps25hSensorDriver(i2cBusName); + mSensorDriver.registerPressureSensor(); + mSensorDriver.registerTemperatureSensor(); +} catch (IOException e) { + // Error configuring sensor +} + +// Unregister and close the driver when finished: + +mSensorManager.unregisterListener(mPressureListener); +mSensorManager.unregisterListener(mTemperatureListener); +mSensorDriver.unregisterPressureSensor(); +mSensorDriver.unregisterTemperatureSensor(); +try { + mSensorDriver.close(); +} catch (IOException e) { + // Error closing sensor +} +``` + +License +------- + +Copyright 2016 Macro Yau + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + +[product_lps25h]: http://www.st.com/en/mems-and-sensors/lps25h.html +[jcenter]: https://bintray.com/google/androidthings/contrib-driver-lps25h/_latestVersion +[sensors]: https://developer.android.com/guide/topics/sensors/sensors_overview.html diff --git a/lps25h/build.gradle b/lps25h/build.gradle new file mode 100644 index 0000000..3d0d039 --- /dev/null +++ b/lps25h/build.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 24 + buildToolsVersion '24.0.3' + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + } +} + +dependencies { + provided 'com.google.android.things:androidthings:0.2-devpreview' + compile 'com.android.support:support-annotations:24.2.0' + + testCompile project(':testingutils') + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' +} diff --git a/lps25h/publish.gradle b/lps25h/publish.gradle new file mode 100644 index 0000000..3f73c00 --- /dev/null +++ b/lps25h/publish.gradle @@ -0,0 +1,97 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * To publish on Bintray: + * - set environmental variables BINTRAY_USER and BINTRAY_API_KEY to proper values + * - from this directory: + * ../gradlew -b publish.gradle bintrayUpload + * + */ + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.1' + } +} + +plugins { + id "com.jfrog.bintray" version "1.7" +} + +allprojects { + repositories { + jcenter() + } +} + +apply from: 'build.gradle' +apply plugin: 'maven-publish' + +def packageVersion = '0.1' + +task sourceJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +publishing { + publications { + driverPublish(MavenPublication) { + groupId 'com.google.android.things.contrib' + artifactId "driver-$project.name" + version packageVersion + artifacts = configurations.archives.artifacts + artifact sourceJar + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + configurations.compile.allDependencies.each { + if(it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) + { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_API_KEY') + publications = ['driverPublish'] + + publish = true + + pkg { + repo = 'androidthings' + name = "contrib-driver-$project.name" + userOrg = 'google' + + version { + name = packageVersion + gpg { + sign = true + } + } + } +} diff --git a/lps25h/src/main/AndroidManifest.xml b/lps25h/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c2f256 --- /dev/null +++ b/lps25h/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25h.java b/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25h.java new file mode 100644 index 0000000..768e2a0 --- /dev/null +++ b/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25h.java @@ -0,0 +1,411 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.lps25h; + +import android.support.annotation.IntDef; + +import com.google.android.things.pio.I2cDevice; +import com.google.android.things.pio.PeripheralManagerService; + +import java.io.IOException; + +/** + * Driver for the LPS25H pressure sensor. + * + * @see LPS25H datasheet + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Lps25h implements AutoCloseable { + + // Sensor constants obtained from the datasheet + /** + * Device ID of the sensor. + */ + public static final int DEVICE_ID = 0xBD; // Value of the WHO_AM_I register + /** + * I2C address of the sensor. + */ + public static final int I2C_ADDRESS = 0x5C; + /** + * Minimum pressure in hectopascals that the sensor can measure. + */ + public static final float MIN_PRESSURE_HPA = 260f; + /** + * Maximum pressure in hectopascals that the sensor can measure. + */ + public static final float MAX_PRESSURE_HPA = 1260f; + /** + * Minimum temperature in Celsius that the sensor can measure. + */ + public static final float MIN_TEMP_C = -30f; + /** + * Maximum temperature in Celsius that the sensor can measure. + */ + public static final float MAX_TEMP_C = 105f; + /** + * Maximum power consumption in micro-amperes when measuring pressure and temperature. + */ + public static final float MAX_POWER_CONSUMPTION_UA = 25f; + /** + * Maximum frequency of the measurements. + */ + public static final float MAX_FREQ_HZ = 25f; + /** + * Minimum frequency of the measurements. + */ + public static final float MIN_FREQ_HZ = 1f; + + /** + * Pressure average configuration. + */ + @IntDef({RES_CONF_AVGP_8, RES_CONF_AVGP_32, RES_CONF_AVGP_128, RES_CONF_AVGP_512}) + public @interface PressureAverageConfiguration { + } + + public static final int RES_CONF_AVGP_8 = 0b00; + public static final int RES_CONF_AVGP_32 = 0b01; // Default + public static final int RES_CONF_AVGP_128 = 0b10; + public static final int RES_CONF_AVGP_512 = 0b11; + + /** + * Temperature average configuration. + */ + @IntDef({RES_CONF_AVGT_8, RES_CONF_AVGT_16, RES_CONF_AVGT_32, RES_CONF_AVGT_64}) + public @interface TemperatureAverageConfiguration { + } + + public static final int RES_CONF_AVGT_8 = 0b00; + public static final int RES_CONF_AVGT_16 = 0b01; // Default + public static final int RES_CONF_AVGT_32 = 0b10; + public static final int RES_CONF_AVGT_64 = 0b11; + + /** + * Power mode. + */ + @IntDef({MODE_POWER_DOWN, MODE_ACTIVE}) + public @interface Mode { + } + + public static final int MODE_POWER_DOWN = 0; + public static final int MODE_ACTIVE = 1; + + /** + * Output data rate configuration. + */ + @IntDef({LPS25H_ODR_ONE_SHOT, LPS25H_ODR_1_HZ, LPS25H_ODR_7_HZ, LPS25H_ODR_12_5_HZ, + LPS25H_ODR_25_HZ}) + public @interface OutputDataRate { + } + + public static final int LPS25H_ODR_ONE_SHOT = 0b000; + public static final int LPS25H_ODR_1_HZ = 0b001; + public static final int LPS25H_ODR_7_HZ = 0b010; + public static final int LPS25H_ODR_12_5_HZ = 0b011; + public static final int LPS25H_ODR_25_HZ = 0b100; + + // Registers + private static final int LPS25H_REG_REF_P_XL = 0x08; // R/W + private static final int LPS25H_REG_REF_P_L = 0x09; // R/W + private static final int LPS25H_REG_REF_P_H = 0x0A; // R/W + private static final int LPS25H_REG_WHO_AM_I = 0x0F; // R + private static final int LPS25H_REG_RES_CONF = 0x10; // R/W + private static final int LPS25H_REG_CTRL_REG1 = 0x20; // R/W + private static final int LPS25H_REG_CTRL_REG2 = 0x21; // R/W + private static final int LPS25H_REG_CTRL_REG3 = 0x22; // R/W + private static final int LPS25H_REG_CTRL_REG4 = 0x23; // R/W + private static final int LPS25H_REG_INT_CFG = 0x24; // R/W + private static final int LPS25H_REG_INT_SOURCE = 0x25; // R + private static final int LPS25H_REG_STATUS_REG = 0x27; // R + private static final int LPS25H_REG_PRESS_OUT_XL = 0x28; // R + private static final int LPS25H_REG_PRESS_OUT_L = 0x29; // R + private static final int LPS25H_REG_PRESS_OUT_H = 0x2A; // R + private static final int LPS25H_REG_TEMP_OUT_L = 0x2B; // R + private static final int LPS25H_REG_TEMP_OUT_H = 0x2C; // R + private static final int LPS25H_REG_FIFO_CTRL = 0x2E; // R/W + private static final int LPS25H_REG_FIFO_STATUS = 0x2F; // R + private static final int LPS25H_REG_THS_P_L = 0x30; // R/W + private static final int LPS25H_REG_THS_P_H = 0x31; // R/W + private static final int LPS25H_REG_RPDS_L = 0x39; // R/W + private static final int LPS25H_REG_RPDS_H = 0x3A; // R/W + + // Bit masks for CTRL_REG1 + private static final int LPS25H_POWER_DOWN_MASK = 0b10000000; + private static final int LPS25H_BDU_MASK = 0b00000100; + private static final int LPS25H_ODR_MASK = 0b01110000; + + private I2cDevice mDevice; + + private final byte[] mBuffer = new byte[3]; // For reading registers + + private int mMode; + private boolean mBlockDataUpdate; + private int mOutputDataRate; + + /** + * Creates a new LPS25H sensor driver connected on the given bus. + * + * @param bus the I2C bus the sensor is connected to + * @throws IOException + */ + public Lps25h(String bus) throws IOException { + PeripheralManagerService pioService = new PeripheralManagerService(); + I2cDevice device = pioService.openI2cDevice(bus, I2C_ADDRESS); + try { + connect(device); + } catch (IOException | RuntimeException e) { + try { + close(); + } catch (IOException | RuntimeException ignored) { + } + throw e; + } + } + + /** + * Creates a new LPS25H sensor driver connected to the given I2C device. + * + * @param device the I2C device of the sensor + * @throws IOException + */ + /*package*/ Lps25h(I2cDevice device) throws IOException { + connect(device); + } + + private void connect(I2cDevice device) throws IOException { + mDevice = device; + + if ((mDevice.readRegByte(LPS25H_REG_WHO_AM_I) & 0xFF) != DEVICE_ID) { + throw new IllegalStateException("I2C device is not LPS25H sensor"); + } + + setBlockDataUpdate(true); + setOutputDataRate(LPS25H_ODR_1_HZ); + setMode(MODE_ACTIVE); + + // Suggested configuration in the datasheet + mDevice.writeRegByte(LPS25H_REG_RES_CONF, (byte) 0x05); + mDevice.writeRegByte(LPS25H_REG_FIFO_CTRL, (byte) 0xC0); + mDevice.writeRegByte(LPS25H_REG_CTRL_REG2, (byte) 0x40); + } + + /** + * Sets the power mode of the sensor as power-down or active. + * + * @param mode must be either {@link #MODE_POWER_DOWN} or {@link #MODE_ACTIVE} + * @throws IOException + */ + public void setMode(@Mode int mode) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(LPS25H_REG_CTRL_REG1) & 0xFF; + if (mode == MODE_POWER_DOWN) { + regCtrl &= (~LPS25H_POWER_DOWN_MASK & 0xFF); + } else { + regCtrl |= LPS25H_POWER_DOWN_MASK; + } + mDevice.writeRegByte(LPS25H_REG_CTRL_REG1, (byte) regCtrl); + mMode = mode; + } + + /** + * Returns the power mode of the sensor. + * + * @return true if the sensor is active + * @see #setMode(int) + */ + public boolean isEnabled() { + return mMode == MODE_ACTIVE; + } + + /** + * Sets the block data update (BDU) bit in the control register 1 (CTRL_REG1) of the sensor. + * Reading MSB and LSB of different samples can be avoided by enabling BDU. + * + * @param enabled enable BDU if true + * @throws IOException + */ + public void setBlockDataUpdate(boolean enabled) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(LPS25H_REG_CTRL_REG1) & 0xFF; + regCtrl &= (~LPS25H_BDU_MASK & 0xFF); + if (enabled) { + regCtrl |= LPS25H_BDU_MASK; + } + mDevice.writeRegByte(LPS25H_REG_CTRL_REG1, (byte) regCtrl); + mBlockDataUpdate = enabled; + } + + /** + * Returns the status of the block data update mode. + * + * @return true if the block data update mode is enabled + * @see #setBlockDataUpdate(boolean) + */ + public boolean isBlockDataUpdateEnabled() { + return mBlockDataUpdate; + } + + /** + * Configures the output data rate (ODR) of the pressure and temperature measurements. + * + * @param outputDataRate the configuration must be one of {@link #LPS25H_ODR_ONE_SHOT}, + * {@link #LPS25H_ODR_1_HZ}, {@link #LPS25H_ODR_7_HZ}, + * {@link #LPS25H_ODR_12_5_HZ} or {@link #LPS25H_ODR_25_HZ} + * @throws IOException + */ + public void setOutputDataRate(@OutputDataRate int outputDataRate) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(LPS25H_REG_CTRL_REG1) & 0xFF; + regCtrl &= (~LPS25H_ODR_MASK & 0xFF); + regCtrl |= (outputDataRate << 4); + mDevice.writeRegByte(LPS25H_REG_CTRL_REG1, (byte) regCtrl); + mOutputDataRate = outputDataRate; + } + + /** + * Returns the configured output data rate. + * + * @return the output data rate + * @see #setOutputDataRate(int) + */ + public int getOutputDataRate() { + return mOutputDataRate; + } + + /** + * Configures the number of averaged samples for the pressure and temperature measurements. + * + * @param pressureAverage the pressure average configuration must be one of the + * RES_CONF_AVGP* constants + * @param temperatureAverage the temperature average configuration must be one of the + * RES_CONF_AVGT* constants + * @throws IOException + */ + public void setAveragedSamples(@PressureAverageConfiguration int pressureAverage, + @TemperatureAverageConfiguration int temperatureAverage) + throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + int regCtrl = mDevice.readRegByte(LPS25H_REG_RES_CONF) & 0xF0; + regCtrl |= pressureAverage | (temperatureAverage << 2); + mDevice.writeRegByte(LPS25H_REG_RES_CONF, (byte) (regCtrl)); + } + + /** + * Closes the driver and its underlying device. + */ + @Override + public void close() throws IOException { + if (mDevice != null) { + try { + setMode(MODE_POWER_DOWN); + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + /** + * Reads the current pressure. + * + * @return the current pressure in hectopascals or millibars + */ + public float readPressure() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + if (isPressureDataAvailable()) { + int rawPressure = readThreeRegisters(LPS25H_REG_PRESS_OUT_XL); + return (float) rawPressure / 4096f; + } else { + throw new IOException("Pressure data is not yet available"); + } + } + + /** + * Reads the current temperature. + * + * @return the current temperature in degrees Celsius + */ + public float readTemperature() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device is already closed"); + } + + if (isTemperatureDataAvailable()) { + int rawTemp = (short) readTwoRegisters(LPS25H_REG_TEMP_OUT_L); + return (float) rawTemp / 480f + 42.5f; + } else { + throw new IOException("Temperature data is not yet available"); + } + } + + /** + * Reads 16 bits from two 8-bit registers at the given address. + * + * @param address the address of the least significant byte (LSB) + * @throws IOException + */ + private int readTwoRegisters(int address) throws IOException { + mDevice.readRegBuffer(address | 0x80, mBuffer, 2); // 0x80 for auto increment in the address + return ((mBuffer[1] & 0xFF) << 8) | (mBuffer[0] & 0xFF); + } + + /** + * Reads 24 bits from three 8-bit registers at the given address. + * + * @param address the address of the least significant byte (LSB) + * @throws IOException + */ + private int readThreeRegisters(int address) throws IOException { + mDevice.readRegBuffer(address | 0x80, mBuffer, 3); // 0x80 for auto increment in the address + return ((mBuffer[2] & 0xFF) << 16) | ((mBuffer[1] & 0xFF) << 8) | (mBuffer[0] & 0xFF); + } + + /** + * Validates the availability of updated pressure data. + * + * @return true if the pressure data is updated + * @throws IOException + */ + private boolean isPressureDataAvailable() throws IOException { + return (mDevice.readRegByte(LPS25H_REG_STATUS_REG) & 0x02) != 0; + } + + /** + * Validates the availability of updated temperature data. + * + * @return true if the temperature data is updated + * @throws IOException + */ + private boolean isTemperatureDataAvailable() throws IOException { + return (mDevice.readRegByte(LPS25H_REG_STATUS_REG) & 0x01) != 0; + } + +} diff --git a/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25hSensorDriver.java b/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25hSensorDriver.java new file mode 100644 index 0000000..f966434 --- /dev/null +++ b/lps25h/src/main/java/com/google/android/things/contrib/driver/lps25h/Lps25hSensorDriver.java @@ -0,0 +1,233 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.lps25h; + +import android.hardware.Sensor; + +import com.google.android.things.userdriver.UserDriverManager; +import com.google.android.things.userdriver.UserSensor; +import com.google.android.things.userdriver.UserSensorDriver; +import com.google.android.things.userdriver.UserSensorReading; + +import java.io.IOException; +import java.util.UUID; + +/** + * User-space driver for interfacing the LPS25H pressure sensor to the Android SensorManager + * framework. + */ +public class Lps25hSensorDriver implements AutoCloseable { + + private static final String DRIVER_VENDOR = "STMicroelectronics"; + private static final String DRIVER_NAME = "LPS25H"; + private static final int DRIVER_MIN_DELAY_US = Math.round(1000000.0f / Lps25h.MAX_FREQ_HZ); + private static final int DRIVER_MAX_DELAY_US = Math.round(1000000.0f / Lps25h.MIN_FREQ_HZ); + + private Lps25h mDevice; + + private PressureUserDriver mPressureUserDriver; + private TemperatureUserDriver mTemperatureUserDriver; + + /** + * Creates a new framework sensor driver connected on the given bus. + * The driver emits {@link Sensor} with pressure and temperature data when registered. + * + * @param bus the I2C bus the sensor is connected to + * @throws IOException + * @see #registerPressureSensor() + * @see #registerTemperatureSensor() + */ + public Lps25hSensorDriver(String bus) throws IOException { + mDevice = new Lps25h(bus); + } + + /** + * Closes the driver and its underlying device. + * + * @throws IOException + */ + @Override + public void close() throws IOException { + unregisterPressureSensor(); + unregisterTemperatureSensor(); + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + /** + * Registers a {@link UserSensor} that pipes pressure readings into the Android SensorManager. + * + * @see #unregisterPressureSensor() + */ + public void registerPressureSensor() { + if (mDevice == null) { + throw new IllegalStateException("Cannot register closed driver"); + } + + if (mPressureUserDriver == null) { + mPressureUserDriver = new PressureUserDriver(); + UserDriverManager.getManager().registerSensor(mPressureUserDriver.getUserSensor()); + } + } + + /** + * Registers a {@link UserSensor} that pipes temperature readings into the Android SensorManager. + * + * @see #unregisterTemperatureSensor() + */ + public void registerTemperatureSensor() { + if (mDevice == null) { + throw new IllegalStateException("Cannot register closed driver"); + } + + if (mTemperatureUserDriver == null) { + mTemperatureUserDriver = new TemperatureUserDriver(); + UserDriverManager.getManager().registerSensor(mTemperatureUserDriver.getUserSensor()); + } + } + + /** + * Unregisters the pressure {@link UserSensor}. + */ + public void unregisterPressureSensor() { + if (mPressureUserDriver != null) { + UserDriverManager.getManager().unregisterSensor(mPressureUserDriver.getUserSensor()); + mPressureUserDriver = null; + } + } + + /** + * Unregisters the temperature {@link UserSensor}. + */ + public void unregisterTemperatureSensor() { + if (mTemperatureUserDriver != null) { + UserDriverManager.getManager().unregisterSensor(mTemperatureUserDriver.getUserSensor()); + mTemperatureUserDriver = null; + } + } + + private void maybeSleep() throws IOException { + if ((mTemperatureUserDriver == null || !mTemperatureUserDriver.isEnabled()) && + (mPressureUserDriver == null || !mPressureUserDriver.isEnabled())) { + mDevice.setMode(Lps25h.MODE_POWER_DOWN); + } else { + mDevice.setMode(Lps25h.MODE_ACTIVE); + } + } + + private class PressureUserDriver extends UserSensorDriver { + + private static final float DRIVER_MAX_RANGE = Lps25h.MAX_PRESSURE_HPA; + private static final float DRIVER_RESOLUTION = 0.0002f; + private static final float DRIVER_POWER = Lps25h.MAX_POWER_CONSUMPTION_UA / 1000f; + private static final int DRIVER_VERSION = 1; + private static final String DRIVER_REQUIRED_PERMISSION = ""; + + private boolean mEnabled; + private UserSensor mUserSensor; + + private UserSensor getUserSensor() { + if (mUserSensor == null) { + mUserSensor = UserSensor.builder() + .setType(Sensor.TYPE_PRESSURE) + .setName(DRIVER_NAME) + .setVendor(DRIVER_VENDOR) + .setVersion(DRIVER_VERSION) + .setMaxRange(DRIVER_MAX_RANGE) + .setResolution(DRIVER_RESOLUTION) + .setPower(DRIVER_POWER) + .setMinDelay(DRIVER_MIN_DELAY_US) + .setRequiredPermission(DRIVER_REQUIRED_PERMISSION) + .setMaxDelay(DRIVER_MAX_DELAY_US) + .setUuid(UUID.randomUUID()) + .setDriver(this) + .build(); + } + return mUserSensor; + } + + @Override + public UserSensorReading read() throws IOException { + return new UserSensorReading(new float[]{mDevice.readPressure()}); + } + + @Override + public void setEnabled(boolean enabled) throws IOException { + mEnabled = enabled; + maybeSleep(); + } + + private boolean isEnabled() { + return mEnabled; + } + + } + + private class TemperatureUserDriver extends UserSensorDriver { + + private static final float DRIVER_MAX_RANGE = Lps25h.MAX_TEMP_C; + private static final float DRIVER_RESOLUTION = 0.002f; + private static final float DRIVER_POWER = Lps25h.MAX_POWER_CONSUMPTION_UA / 1000f; + private static final int DRIVER_VERSION = 1; + private static final String DRIVER_REQUIRED_PERMISSION = ""; + + private boolean mEnabled; + private UserSensor mUserSensor; + + private UserSensor getUserSensor() { + if (mUserSensor == null) { + mUserSensor = UserSensor.builder() + .setType(Sensor.TYPE_AMBIENT_TEMPERATURE) + .setName(DRIVER_NAME) + .setVendor(DRIVER_VENDOR) + .setVersion(DRIVER_VERSION) + .setMaxRange(DRIVER_MAX_RANGE) + .setResolution(DRIVER_RESOLUTION) + .setPower(DRIVER_POWER) + .setMinDelay(DRIVER_MIN_DELAY_US) + .setRequiredPermission(DRIVER_REQUIRED_PERMISSION) + .setMaxDelay(DRIVER_MAX_DELAY_US) + .setUuid(UUID.randomUUID()) + .setDriver(this) + .build(); + } + return mUserSensor; + } + + @Override + public UserSensorReading read() throws IOException { + return new UserSensorReading(new float[]{mDevice.readTemperature()}); + } + + @Override + public void setEnabled(boolean enabled) throws IOException { + mEnabled = enabled; + maybeSleep(); + } + + private boolean isEnabled() { + return mEnabled; + } + + } + +} diff --git a/lps25h/src/test/java/com/google/android/things/contrib/driver/lps25h/Lps25hTest.java b/lps25h/src/test/java/com/google/android/things/contrib/driver/lps25h/Lps25hTest.java new file mode 100644 index 0000000..3acdb4e --- /dev/null +++ b/lps25h/src/test/java/com/google/android/things/contrib/driver/lps25h/Lps25hTest.java @@ -0,0 +1,243 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.lps25h; + +import com.google.android.things.pio.I2cDevice; + +import static com.google.android.things.contrib.driver.testutils.BitsMatcher.hasBitsSet; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.byteThat; +import static org.mockito.Matchers.eq; + +public class Lps25hTest { + + @Mock + I2cDevice mI2c; + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException mExpectedException = ExpectedException.none(); + + @Test + public void close() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + Mockito.verify(mI2c).close(); + } + + @Test + public void close_safeToCallTwice() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + lps25h.close(); // Should not throw + } + + @Test + public void setMode() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + + Mockito.reset(mI2c); + + lps25h.setMode(Lps25h.MODE_ACTIVE); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x80))); + } + + @Test + public void setMode_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.setMode(Lps25h.MODE_ACTIVE); + } + + @Test + public void setBlockDataUpdate() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + + // Disable BDU + lps25h.setBlockDataUpdate(false); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x00))); + + Mockito.reset(mI2c); + + // Enable BDU + lps25h.setBlockDataUpdate(true); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) 0x04))); + } + + @Test + public void setBlockDataUpdate_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.setBlockDataUpdate(true); + } + + @Test + public void setOutputDataRate() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + + Mockito.reset(mI2c); + + // One-shot + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_ONE_SHOT); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) (0x00 << 4)))); + + Mockito.reset(mI2c); + + // 1 Hz + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_1_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) (0x01 << 4)))); + + Mockito.reset(mI2c); + + // 7 Hz + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_7_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) (0x02 << 4)))); + + Mockito.reset(mI2c); + + // 12.5 Hz + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_12_5_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) (0x03 << 4)))); + + Mockito.reset(mI2c); + + // 25 Hz + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_25_HZ); + Mockito.verify(mI2c).writeRegByte(eq(0x20), byteThat(hasBitsSet((byte) (0x04 << 4)))); + } + + @Test + public void setOutputDataRate_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.setOutputDataRate(Lps25h.LPS25H_ODR_25_HZ); + } + + @Test + public void setAveragedSamples() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + + Mockito.reset(mI2c); + + // AVGP_32 + AVGT_16 + lps25h.setAveragedSamples(Lps25h.RES_CONF_AVGP_32, Lps25h.RES_CONF_AVGT_16); + Mockito.verify(mI2c).writeRegByte(eq(0x10), byteThat(hasBitsSet((byte) 0x05))); + + Mockito.reset(mI2c); + + // AVGP_512 + AVGT_64 + lps25h.setAveragedSamples(Lps25h.RES_CONF_AVGP_512, Lps25h.RES_CONF_AVGT_64); + Mockito.verify(mI2c).writeRegByte(eq(0x10), byteThat(hasBitsSet((byte) 0x08))); + } + + @Test + public void setAveragedSamples_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.setAveragedSamples(Lps25h.RES_CONF_AVGP_512, Lps25h.RES_CONF_AVGT_64); + } + + @Test + public void readPressure() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x02); + lps25h.readPressure(); + Mockito.verify(mI2c).readRegBuffer(eq(0x28 | 0x80), any(byte[].class), eq(3)); + } + + @Test + public void readPressure_throwsIfDataNotYetAvailable() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x00); + mExpectedException.expect(IOException.class); + mExpectedException.expectMessage("Pressure data is not yet available"); + lps25h.readPressure(); + } + + @Test + public void readPressure_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.readPressure(); + } + + @Test + public void readTemperature() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x01); + lps25h.readTemperature(); + Mockito.verify(mI2c).readRegBuffer(eq(0x2B | 0x80), any(byte[].class), eq(2)); + } + + @Test + public void readTemperature_throwsIfDataNotYetAvailable() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + Mockito.when(mI2c.readRegByte(0x27)).thenReturn((byte) 0x00); + mExpectedException.expect(IOException.class); + mExpectedException.expectMessage("Temperature data is not yet available"); + lps25h.readTemperature(); + } + + @Test + public void readTemperature_throwsIfClosed() throws IOException { + Mockito.when(mI2c.readRegByte(0x0F)).thenReturn((byte) 0xBD); + Lps25h lps25h = new Lps25h(mI2c); + lps25h.close(); + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device is already closed"); + lps25h.readTemperature(); + } + +} diff --git a/sensehat/README.md b/sensehat/README.md index 97e3c63..b9f0302 100644 --- a/sensehat/README.md +++ b/sensehat/README.md @@ -1,10 +1,13 @@ -Sense Hat driver for Android Things +Sense HAT driver for Android Things ===================================== -This driver provides easy access to the peripherals available on the Raspberry Pi [Sense Hat][product]: -- 8x8 LED matrix -- TODO: 5 buttons joystick -- TODO: Sensors +This driver provides easy access to the peripherals available on the Raspberry Pi [Sense HAT][product]: + +- 8×8 RGB LED matrix +- 5-button miniature joystick +- ST LPS25H barometric pressure and temperature sensor +- ST HTS221 relative humidity and temperature sensor +- TODO: ST LSM9DS1 inertial measurement sensor NOTE: these drivers are not production-ready. They are offered as sample diff --git a/sensehat/build.gradle b/sensehat/build.gradle index 02cf853..b62be5e 100644 --- a/sensehat/build.gradle +++ b/sensehat/build.gradle @@ -33,6 +33,9 @@ dependencies { provided 'com.google.android.things:androidthings:0.2-devpreview' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' + + compile project(':hts221') + compile project(':lps25h') } android { diff --git a/sensehat/src/androidTest/java/com/google/android/things/contrib/driver/sensehat/SenseHatDeviceTest.java b/sensehat/src/androidTest/java/com/google/android/things/contrib/driver/sensehat/SenseHatDeviceTest.java index df80356..accc035 100644 --- a/sensehat/src/androidTest/java/com/google/android/things/contrib/driver/sensehat/SenseHatDeviceTest.java +++ b/sensehat/src/androidTest/java/com/google/android/things/contrib/driver/sensehat/SenseHatDeviceTest.java @@ -26,6 +26,10 @@ import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import com.google.android.things.contrib.driver.hts221.Hts221; +import com.google.android.things.contrib.driver.lps25h.Lps25h; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,37 +39,67 @@ @RunWith(AndroidJUnit4.class) @SmallTest public class SenseHatDeviceTest { + @Test public void senseHat_DisplayColor() throws IOException { // Color the LED matrix. - LedMatrix display = SenseHat.openDisplay(); - + SenseHat senseHat = new SenseHat(); + LedMatrix display = senseHat.openDisplay(); display.draw(Color.MAGENTA); + // Close the display when done. - display.close(); + senseHat.close(); } @Test public void senseHat_DisplayDrawable() throws IOException { Context context = InstrumentationRegistry.getTargetContext(); + // Display a drawable on the LED matrix. - LedMatrix display = SenseHat.openDisplay(); + SenseHat senseHat = new SenseHat(); + LedMatrix display = senseHat.openDisplay(); display.draw(context.getDrawable(android.R.drawable.ic_secure)); + // Close the display when done. - display.close(); + senseHat.close(); } @Test public void senseHat_DisplayGradient() throws IOException { // Display a gradient on the LED matrix. - LedMatrix display = SenseHat.openDisplay(); + SenseHat senseHat = new SenseHat(); + LedMatrix display = senseHat.openDisplay(); Bitmap bitmap = Bitmap.createBitmap(SenseHat.DISPLAY_WIDTH, SenseHat.DISPLAY_HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(); paint.setShader(new RadialGradient(4, 4, 4, Color.RED, Color.BLUE, Shader.TileMode.CLAMP)); canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint); display.draw(bitmap); + // Close the display when done. - display.close(); + senseHat.close(); } + + @Test + public void senseHat_ReadPressureSensor() throws IOException { + // Prints LPS25H barometric pressure and temperature sensor readings to LogCat + Lps25h lps25h = SenseHat.openPressureSensor(); + float pressure = lps25h.readPressure(); + Log.i("LPS25H", String.format("Barometric Pressure: %.1f", pressure) + " hPa"); + float temperature = lps25h.readTemperature(); + Log.i("LPS25H", String.format("Temperature: %.1f", temperature) + " °C"); + lps25h.close(); + } + + @Test + public void senseHat_ReadHumiditySensor() throws IOException { + // Prints HTS221 relative humidity and temperature sensor readings to LogCat + Hts221 hts221 = SenseHat.openHumiditySensor(); + float humidity = hts221.readHumidity(); + Log.i("HTS221", String.format("Relative Humidity: %.1f", humidity) + " %"); + float temperature = hts221.readTemperature(); + Log.i("HTS221", String.format("Temperature: %.1f", temperature) + " °C"); + hts221.close(); + } + } diff --git a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/Joystick.java b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/Joystick.java new file mode 100644 index 0000000..0056d4d --- /dev/null +++ b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/Joystick.java @@ -0,0 +1,158 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.sensehat; + +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.google.android.things.pio.Gpio; +import com.google.android.things.pio.GpioCallback; +import com.google.android.things.pio.PeripheralManagerService; + +import java.io.IOException; + +/** + * Driver for Raspberry Pi Sense HAT 5-button miniature joystick. + * + * @see Sense HAT joystick Linux kernel driver + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Joystick { + + private static final String TAG = "SenseHatJoystick"; + + public static final int KEY_RELEASED = 0; + public static final int KEY_PRESSED_DOWN = 1; + public static final int KEY_PRESSED_RIGHT = 2; + public static final int KEY_PRESSED_UP = 4; + public static final int KEY_PRESSED_ENTER = 8; + public static final int KEY_PRESSED_LEFT = 16; + + private static final int JOYSTICK_REG_KEYS = 0xF2; + + private Gpio mInterruptGpio; + private OnButtonEventListener mListener; + private int mPrevKey; + + /** + * Interface definition for a callback to be invoked when a button event occurs. + */ + public interface OnButtonEventListener { + /** + * Called when a button event occurs. + * + * @param key the KEY_PRESSED_* key code of the button for which the event + * occurred + * @param pressed true if the button is now pressed + */ + void onButtonEvent(int key, boolean pressed); + } + + /** + * Creates a new joystick driver. + * + * @param interruptPin the interrupt GPIO pin the joystick controller is connected to + * @throws IOException + */ + public Joystick(String interruptPin) throws IOException { + PeripheralManagerService pioService = new PeripheralManagerService(); + Gpio interruptGpio = pioService.openGpio(interruptPin); + try { + connect(interruptGpio); + } catch (IOException | RuntimeException e) { + close(); + throw e; + } + } + + /** + * Constructor invoked from unit tests. + */ + @VisibleForTesting + /*package*/ Joystick(Gpio interruptGpio) throws IOException { + connect(interruptGpio); + } + + private void connect(Gpio interruptGpio) throws IOException { + mInterruptGpio = interruptGpio; + mInterruptGpio.setDirection(Gpio.DIRECTION_IN); + mInterruptGpio.setEdgeTriggerType(Gpio.EDGE_BOTH); + mInterruptGpio.registerGpioCallback(mInterruptCallback); + } + + /** + * Local callback to monitor GPIO edge events. + */ + private GpioCallback mInterruptCallback = new GpioCallback() { + @Override + public boolean onGpioEdge(Gpio gpio) { + try { + boolean trigger = gpio.getValue(); + if (trigger) { + int key = SenseHat.i2cDevice.readRegByte(JOYSTICK_REG_KEYS) & 0x7F; + if (key == KEY_RELEASED) { + performButtonEvent(mPrevKey, false); + } else { + performButtonEvent(key, true); + } + mPrevKey = key; + } + } catch (IOException e) { + Log.e(TAG, "Error reading button state", e); + } + + return true; + } + }; + + /** + * Sets the listener to be called when a button event is occurred. + * + * @param listener button event listener to be invoked + */ + public void setOnButtonEventListener(OnButtonEventListener listener) { + mListener = listener; + } + + /** + * Closes the driver and its underlying device. + * + * @throws IOException + */ + protected void close() throws IOException { + mListener = null; + + if (mInterruptGpio != null) { + mInterruptGpio.unregisterGpioCallback(mInterruptCallback); + try { + mInterruptGpio.close(); + } finally { + mInterruptGpio = null; + } + } + } + + /** + * Invokes button event callback. + */ + private void performButtonEvent(int key, boolean state) { + if (mListener != null) { + mListener.onButtonEvent(key, state); + } + } + +} diff --git a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/JoystickDriver.java b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/JoystickDriver.java new file mode 100644 index 0000000..d05c275 --- /dev/null +++ b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/JoystickDriver.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016 Macro Yau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.sensehat; + +import android.view.InputDevice; +import android.view.KeyEvent; + +import com.google.android.things.userdriver.InputDriver; +import com.google.android.things.userdriver.UserDriverManager; + +import java.io.IOException; + +/** + * User-space driver to process button events from the Raspberry Pi Sense HAT joystick and forward + * them to the Android input framework. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class JoystickDriver { + + // Driver parameters + private static final String DRIVER_NAME = "SenseHatJoystick"; + private static final int DRIVER_VERSION = 1; + + // Key code for driver to emulate + private static final int KEY_CODE_UP = KeyEvent.KEYCODE_DPAD_UP; + private static final int KEY_CODE_DOWN = KeyEvent.KEYCODE_DPAD_DOWN; + private static final int KEY_CODE_LEFT = KeyEvent.KEYCODE_DPAD_LEFT; + private static final int KEY_CODE_RIGHT = KeyEvent.KEYCODE_DPAD_RIGHT; + private static final int KEY_CODE_ENTER = KeyEvent.KEYCODE_DPAD_CENTER; + + private Joystick mDevice; + private InputDriver mDriver; + + /** + * Creates a new framework input driver for the joystick. The driver emits + * {@link KeyEvent} with the directional pad key codes when registered. + * + * @param interruptPin the interrupt GPIO pin the joystick controller is connected to + * @return the new input driver instance + * @throws IOException + * @see #register + */ + public JoystickDriver(String interruptPin) throws IOException { + mDevice = new Joystick(interruptPin); + } + + /** + * Closes the driver and its underlying device. + * + * @throws IOException + */ + protected void close() throws IOException { + unregister(); + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + /** + * Registers the driver in the framework. + */ + public void register() { + if (mDevice == null) { + throw new IllegalStateException("Cannot registered closed driver"); + } + + if (mDriver == null) { + mDriver = build(mDevice); + UserDriverManager.getManager().registerInputDriver(mDriver); + } + } + + /** + * Unregisters the driver from the framework. + */ + public void unregister() { + if (mDriver != null) { + UserDriverManager.getManager().registerInputDriver(mDriver); + mDriver = null; + } + } + + static InputDriver build(Joystick joystick) { + final InputDriver inputDriver = InputDriver.builder(InputDevice.SOURCE_CLASS_BUTTON) + .setName(DRIVER_NAME) + .setVersion(DRIVER_VERSION) + .setKeys(new int[]{KEY_CODE_UP, KEY_CODE_DOWN, KEY_CODE_LEFT, KEY_CODE_RIGHT, KEY_CODE_ENTER}) + .build(); + + joystick.setOnButtonEventListener(new Joystick.OnButtonEventListener() { + @Override + public void onButtonEvent(int key, boolean pressed) { + int keyAction = pressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; + int keyCode = -1; + switch (key) { + case Joystick.KEY_PRESSED_UP: + keyCode = KEY_CODE_UP; + break; + case Joystick.KEY_PRESSED_DOWN: + keyCode = KEY_CODE_DOWN; + break; + case Joystick.KEY_PRESSED_LEFT: + keyCode = KEY_CODE_LEFT; + break; + case Joystick.KEY_PRESSED_RIGHT: + keyCode = KEY_CODE_RIGHT; + break; + case Joystick.KEY_PRESSED_ENTER: + keyCode = KEY_CODE_ENTER; + break; + default: + break; + } + if (keyCode != -1) { + inputDriver.emit(new KeyEvent[]{ + new KeyEvent(keyAction, keyCode) + }); + } + } + }); + + return inputDriver; + } + +} diff --git a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/LedMatrix.java b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/LedMatrix.java index 67a4ff3..2e92026 100644 --- a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/LedMatrix.java +++ b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/LedMatrix.java @@ -21,80 +21,50 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; -import com.google.android.things.pio.I2cDevice; -import com.google.android.things.pio.PeripheralManagerService; - import java.io.IOException; /** - * Driver for the SenseHat LED matrix. + * Driver for Raspberry Pi Sense HAT 8×8 RGB LED matrix. */ -public class LedMatrix implements AutoCloseable { +public class LedMatrix { + public static final int WIDTH = 8; public static final int HEIGHT = 8; - private static final int BUFFER_SIZE = WIDTH * HEIGHT * 3 + 1; - private byte[] mBuffer = new byte[BUFFER_SIZE]; - - private I2cDevice mDevice; - - /** - * Create a new LED matrix driver connected on the given I2C bus. - * @param bus I2C bus the sensor is connected to. - * @throws IOException - */ - public LedMatrix(String bus) throws IOException { - PeripheralManagerService pioService = new PeripheralManagerService(); - mDevice = pioService.openI2cDevice(bus, SenseHat.I2C_ADDRESS); - } - /* package */ LedMatrix(I2cDevice device) { - mDevice = device; - } + private static final int BUFFER_SIZE = WIDTH * HEIGHT * 3 + 1; - /** - * Close the driver and the underlying device. - * @throws IOException - */ - @Override - public void close() throws IOException { - if (mDevice != null) { - try { - mDevice.close(); - } finally { - mDevice = null; - } - } - } + private byte[] mBuffer = new byte[BUFFER_SIZE]; /** - * Draw the given color to the LED matrix. - * @param color Color to draw + * Draws the given color to the LED matrix. + * + * @param color color to draw * @throws IOException */ public void draw(int color) throws IOException { mBuffer[0] = 0; float a = Color.alpha(color) / 255.f; - byte r = (byte)((int)(Color.red(color)*a)>>3); - byte g = (byte)((int)(Color.green(color)*a)>>3); - byte b = (byte)((int)(Color.blue(color)*a)>>3); + byte r = (byte) ((int) (Color.red(color) * a) >> 3); + byte g = (byte) ((int) (Color.green(color) * a) >> 3); + byte b = (byte) ((int) (Color.blue(color) * a) >> 3); for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { - mBuffer[1+x+WIDTH*0+3*WIDTH*y] = r; - mBuffer[1+x+WIDTH*1+3*WIDTH*y] = g; - mBuffer[1+x+WIDTH*2+3*WIDTH*y] = b; + mBuffer[1 + x + WIDTH * 0 + 3 * WIDTH * y] = r; + mBuffer[1 + x + WIDTH * 1 + 3 * WIDTH * y] = g; + mBuffer[1 + x + WIDTH * 2 + 3 * WIDTH * y] = b; } } - mDevice.write(mBuffer, mBuffer.length); + SenseHat.i2cDevice.write(mBuffer, mBuffer.length); } /** - * Draw the given drawable to the LED matrix. - * @param drawable Drawable to draw + * Draws the given drawable to the LED matrix. + * + * @param drawable drawable to draw * @throws IOException */ public void draw(Drawable drawable) throws IOException { - Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, - Bitmap.Config.ARGB_8888); + Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, WIDTH, HEIGHT); drawable.draw(canvas); @@ -102,22 +72,23 @@ public void draw(Drawable drawable) throws IOException { } /** - * Draw the given bitmap to the LED matrix. - * @param bitmap Bitmap to draw + * Draws the given bitmap to the LED matrix. + * + * @param bitmap bitmap to draw * @throws IOException */ public void draw(Bitmap bitmap) throws IOException { - Bitmap dest = Bitmap.createScaledBitmap(bitmap, 8, 8, true); mBuffer[0] = 0; for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { int p = bitmap.getPixel(x, y); float a = Color.alpha(p) / 255.f; - mBuffer[1+x+WIDTH*0+3*WIDTH*y] = (byte)((int)(Color.red(p)*a)>>3); - mBuffer[1+x+WIDTH*1+3*WIDTH*y] = (byte)((int)(Color.green(p)*a)>>3); - mBuffer[1+x+WIDTH*2+3*WIDTH*y] = (byte)((int)(Color.blue(p)*a)>>3); + mBuffer[1 + x + WIDTH * 0 + 3 * WIDTH * y] = (byte) ((int) (Color.red(p) * a) >> 3); + mBuffer[1 + x + WIDTH * 1 + 3 * WIDTH * y] = (byte) ((int) (Color.green(p) * a) >> 3); + mBuffer[1 + x + WIDTH * 2 + 3 * WIDTH * y] = (byte) ((int) (Color.blue(p) * a) >> 3); } } - mDevice.write(mBuffer, mBuffer.length); + SenseHat.i2cDevice.write(mBuffer, mBuffer.length); } + } diff --git a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/SenseHat.java b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/SenseHat.java index c3bed66..b22e7d9 100644 --- a/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/SenseHat.java +++ b/sensehat/src/main/java/com/google/android/things/contrib/driver/sensehat/SenseHat.java @@ -1,5 +1,6 @@ /* * Copyright 2016 Google Inc. + * Copyright 2017 Macro Yau * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +17,149 @@ package com.google.android.things.contrib.driver.sensehat; +import android.graphics.Color; + +import com.google.android.things.contrib.driver.hts221.Hts221; +import com.google.android.things.contrib.driver.hts221.Hts221SensorDriver; +import com.google.android.things.contrib.driver.lps25h.Lps25h; +import com.google.android.things.contrib.driver.lps25h.Lps25hSensorDriver; +import com.google.android.things.pio.I2cDevice; +import com.google.android.things.pio.PeripheralManagerService; + import java.io.IOException; /** - * Driver factory for the Sense Hat. + * Driver factory for Raspberry Pi Sense HAT. */ @SuppressWarnings({"unused", "WeakerAccess"}) -public class SenseHat { - public static final int I2C_ADDRESS = 0x46; - public static final String BUS_DISPLAY = "I2C1"; +public class SenseHat implements AutoCloseable { + public static final int DISPLAY_WIDTH = LedMatrix.WIDTH; public static final int DISPLAY_HEIGHT = LedMatrix.HEIGHT; - public static LedMatrix openDisplay() throws IOException { - return new LedMatrix(BUS_DISPLAY); + private static final String I2C_BUS = "I2C1"; + private static final int I2C_ADDRESS = 0x46; + + private static final String JOYSTICK_INTERRUPT = "BCM23"; // Interrupt pin for joystick events + + private static final int SENSE_HAT_REG_WHO_AM_I = 0xF0; + + protected static I2cDevice i2cDevice; + + private LedMatrix mLedMatrix; + private Joystick mJoystick; + private JoystickDriver mJoystickDriver; + + public SenseHat() throws IOException { + PeripheralManagerService pioService = new PeripheralManagerService(); + i2cDevice = pioService.openI2cDevice(I2C_BUS, I2C_ADDRESS); + + if (!isAttached()) { + throw new IOException("Sense HAT is not attached"); + } + } + + @Override + public void close() throws IOException { + closeDisplay(); + closeJoystick(); + closeJoystickDriver(); + + if (i2cDevice != null) { + try { + i2cDevice.close(); + } finally { + i2cDevice = null; + } + } + } + + /** + * Checks whether Sense HAT is attached to Raspberry Pi correctly. + * + * @return true if attached + * @throws IOException + */ + private boolean isAttached() throws IOException { + return i2cDevice != null && i2cDevice.readRegByte(SENSE_HAT_REG_WHO_AM_I) == 's'; + } + + // 8×8 RGB LED matrix + + public LedMatrix openDisplay() throws IOException { + if (mLedMatrix == null) { + mLedMatrix = new LedMatrix(); + } + + return mLedMatrix; + } + + public void closeDisplay() throws IOException { + if (mLedMatrix != null) { + try { + mLedMatrix.draw(Color.BLACK); + } finally { + mLedMatrix = null; + } + } + } + + // 5-button miniature joystick + + public Joystick openJoystick() throws IOException { + if (mJoystick == null) { + mJoystick = new Joystick(JOYSTICK_INTERRUPT); + } + + return mJoystick; } + + public void closeJoystick() throws IOException { + if (mJoystick != null) { + try { + mJoystick.close(); + } finally { + mJoystick = null; + } + } + } + + public JoystickDriver createJoystickDriver() throws IOException { + if (mJoystickDriver == null) { + mJoystickDriver = new JoystickDriver(JOYSTICK_INTERRUPT); + } + + return mJoystickDriver; + } + + public void closeJoystickDriver() throws IOException { + if (mJoystickDriver != null) { + try { + mJoystickDriver.close(); + } finally { + mJoystickDriver = null; + } + } + } + + // ST LPS25H barometric pressure and temperature sensor + + public static Lps25h openPressureSensor() throws IOException { + return new Lps25h(I2C_BUS); + } + + public static Lps25hSensorDriver createPressureSensorDriver() throws IOException { + return new Lps25hSensorDriver(I2C_BUS); + } + + // ST HTS221 relative humidity and temperature sensor + + public static Hts221 openHumiditySensor() throws IOException { + return new Hts221(I2C_BUS); + } + + public static Hts221SensorDriver createHumiditySensorDriver() throws IOException { + return new Hts221SensorDriver(I2C_BUS); + } + } diff --git a/settings.gradle b/settings.gradle index 8a4e7eb..380abf7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,8 @@ include ':bmx280' include ':cap12xx' include ':gps' include ':ht16k33' +include ':hts221' +include ':lps25h' include ':mma7660fc' include ':pwmservo' include ':pwmspeaker'