diff --git a/bh1750/.gitignore b/bh1750/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/bh1750/.gitignore @@ -0,0 +1 @@ +/build diff --git a/bh1750/CHANGELOG.md b/bh1750/CHANGELOG.md new file mode 100644 index 0000000..818958f --- /dev/null +++ b/bh1750/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +## [0.1] - 2017-07-17 +- initial version diff --git a/bh1750/README.md b/bh1750/README.md new file mode 100644 index 0000000..f445a4c --- /dev/null +++ b/bh1750/README.md @@ -0,0 +1,114 @@ +BH1750 driver for Android Things +================================ + +This driver supports ROHM [BH1750][product_bh1750] ambient light 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 `bh1750` 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-bh1750:' +} +``` + +### Sample usage + +```java +import com.google.android.things.contrib.driver.bh1750.Bh1750; + +// Access the ambient light sensor: + +Bh1750 mBh1750; + +try { + mBh1750 = new Bh1750(i2cBusName); +} catch (IOException e) { + // couldn't configure the device... +} + +// Read the current light level: + +try { + float lightLevel = mBh1750.readLightLevel(); +} catch (IOException e) { + // error reading light level +} + +// Close the ambient light sensor when finished: + +try { + mBh1750.close(); +} catch (IOException e) { + // error closing sensor +} +``` + +If you need to read sensor values continuously, you can register the BH1750 with the system and +listen for sensor values using the [Sensor APIs][sensors]: +```java +SensorManager mSensorManager = getSystemService(Context.SENSOR_SERVICE); +SensorEventListener mListener = ...; +Bh1750SensorDriver mSensorDriver; + +mSensorManager.registerDynamicSensorCallback(new SensorManager.DynamicSensorCallback() { + @Override + public void onDynamicSensorConnected(Sensor sensor) { + if (sensor.getType() == Sensor.TYPE_LIGHT) { + mSensorManager.registerListener(mListener, sensor, + SensorManager.SENSOR_DELAY_NORMAL); + } + } +}); + +try { + mSensorDriver = new Bh1750SensorDriver(i2cBusName); + mSensorDriver.registerLightSensor(); +} catch (IOException e) { + // Error configuring sensor +} + +// Unregister and close the driver when finished: + +mSensorManager.unregisterListener(mListener); +mSensorDriver.unregisterLightSensor(); +try { + mSensorDriver.close(); +} catch (IOException e) { + // error closing sensor +} +``` + +License +------- + +Copyright 2017 Google Inc. + +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_bh1750]: http://cpre.kmutnb.ac.th/esl/learning/bh1750-light-sensor/bh1750fvi-e_datasheet.pdf +[jcenter]: https://bintray.com/google/androidthings/contrib-driver-bh1750/_latestVersion +[sensors]: https://developer.android.com/guide/topics/sensors/sensors_overview.html diff --git a/bh1750/build.gradle b/bh1750/build.gradle new file mode 100644 index 0000000..ddc5cfe --- /dev/null +++ b/bh1750/build.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2017 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.4-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/bh1750/gradle.properties b/bh1750/gradle.properties new file mode 100644 index 0000000..d656986 --- /dev/null +++ b/bh1750/gradle.properties @@ -0,0 +1,2 @@ +TYPE="ambient light sensor" +ARTIFACT_VERSION=0.1 diff --git a/bh1750/publish-settings.gradle b/bh1750/publish-settings.gradle new file mode 100644 index 0000000..373d66d --- /dev/null +++ b/bh1750/publish-settings.gradle @@ -0,0 +1,3 @@ +rootProject.buildFileName = '../publish.gradle' +include ':testingutils' +project(':testingutils').projectDir = new File(settingsDir, '../testingutils') diff --git a/bh1750/src/main/AndroidManifest.xml b/bh1750/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3635ede --- /dev/null +++ b/bh1750/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750.java b/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750.java new file mode 100644 index 0000000..3e87384 --- /dev/null +++ b/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750.java @@ -0,0 +1,246 @@ +/* + * Copyright 2017 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. + */ + +package com.google.android.things.contrib.driver.bh1750; + +import android.annotation.SuppressLint; +import android.support.annotation.IntDef; + +import com.google.android.things.pio.I2cDevice; +import com.google.android.things.pio.PeripheralManagerService; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Driver for the BH1750 digital ambient light sensor. + */ +@SuppressWarnings({"unused"}) +public class Bh1750 implements AutoCloseable { + + private static final String TAG = Bh1750.class.getSimpleName(); + + /** + * Default I2C address for the sensor. + */ + public static final int DEFAULT_I2C_ADDRESS = 0x23; + + /** + * Alternative I2C address for the sensor. + */ + public static final int ALTERNATIVE_I2C_ADDRESS = 0x5c; + + // Sensor constants from the datasheet. + // http://cpre.kmutnb.ac.th/esl/learning/bh1750-light-sensor/bh1750fvi-e_datasheet.pdf + + /** + * Minimum light in Lux the sensor can measure. + */ + private static final float MIN_LIGHT_LX = 1f; + /** + * Maximum light in Lux the sensor can measure. + */ + public static final float MAX_LIGHT_LX = 65535f; + + /** + * Maximum power consumption in micro-amperes. + */ + public static final float MAX_POWER_CONSUMPTION_UA = 190f; + + /** + * Maximum frequency of the measurements. + */ + public static final float MAX_FREQ_HZ = 60f; + + /** + * Minimum frequency of the measurements. + */ + public static final float MIN_FREQ_HZ = 8f; + + /** + * Measurement mode. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONTINUOUS_HIGH_RES_MODE, CONTINUOUS_HIGH_RES_MODE_2, CONTINUOUS_LOW_RES_MODE, + ONE_TIME_HIGH_RES_MODE, ONE_TIME_HIGH_RES_MODE_2, ONE_TIME_LOW_RES_MODE}) + public @interface Resolution { + } + + // Start measurement at 1lx resolution. Measurement time is approx 120ms. + public static final int CONTINUOUS_HIGH_RES_MODE = 0x10; + + // Start measurement at 0.5lx resolution. Measurement time is approx 120ms. + public static final int CONTINUOUS_HIGH_RES_MODE_2 = 0x11; + + // Start measurement at 4lx resolution. Measurement time is approx 16ms. + public static final int CONTINUOUS_LOW_RES_MODE = 0x13; + + //Start measurement at 1lx resolution. Measurement time is approx 120ms. + //Device is automatically set to Power Down after measurement. + public static final int ONE_TIME_HIGH_RES_MODE = 0x20; + + // Start measurement at 0.5lx resolution. Measurement time is approx 120ms. + // Device is automatically set to Power Down after measurement. + public static final int ONE_TIME_HIGH_RES_MODE_2 = 0x21; + + // Start measurement at 1lx resolution. Measurement time is approx 120ms. + // Device is automatically set to Power Down after measurement. + public static final int ONE_TIME_LOW_RES_MODE = 0x23; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({POWER_DOWN, POWER_ON}) + public @interface Mode { + } + + // Commands + // No active state + public static final int POWER_DOWN = 0x00; + + // Waiting for measurement command + public static final int POWER_ON = 0x01; + + // Reset data register value - not accepted in POWER_DOWN mode + public static final int RESET = 0x07; + + // Registers + private static final int BH1750_DATA_REGISTER = 0x00; + private static final int BH1750_MEASUREMENT_TIME_REGISTER = 0x45; + + private I2cDevice mDevice; + private int mMode = -1; + private int mResolution = -1; + private final byte[] mBuffer = new byte[2]; // for reading sensor values + + /** + * Create a new BH1750 sensor driver connected on the given bus. + * + * @param bus I2C bus the sensor is connected to. + * @throws IOException + */ + public Bh1750(String bus) throws IOException { + this(bus, DEFAULT_I2C_ADDRESS); + } + + /** + * Create a new BH1750 sensor driver connected on the given bus and address. + * + * @param bus I2C bus the sensor is connected to. + * @param address I2C address of the sensor. + * @throws IOException + */ + public Bh1750(String bus, int address) throws IOException { + PeripheralManagerService pioService = new PeripheralManagerService(); + I2cDevice device = pioService.openI2cDevice(bus, address); + try { + connect(device); + } catch (IOException | RuntimeException e) { + try { + close(); + } catch (IOException | RuntimeException ignored) { + } + throw e; + } + } + + /** + * Create a new BH1750 sensor driver connected to the given I2c device. + * + * @param device I2C device of the sensor. + * @throws IOException + */ + /*package*/ Bh1750(I2cDevice device) throws IOException { + connect(device); + } + + private void connect(I2cDevice device) throws IOException { + mDevice = device; + + setMode(POWER_ON); + setResolution(CONTINUOUS_HIGH_RES_MODE); + + // Issue a soft reset + mDevice.write(new byte[]{(byte) RESET}, 1); + } + + /** + * Set the sleep mode of the sensor. + * + * @param mode sleep mode. + * @throws IOException + */ + @SuppressLint("WrongConstant") + public void setMode(@Mode int mode) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device not open"); + } + mDevice.write(new byte[]{(byte) mode}, 1); + if(mResolution != -1) { + setResolution(mResolution); + } + mMode = mode; + } + + /** + * Set the resolution mode of the sensor. + * + * @param resolution resolution mode. + * @throws IOException + */ + public void setResolution(@Resolution int resolution) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device not open"); + } + mDevice.write(new byte[]{(byte) resolution}, 1); + mResolution = resolution; + } + + public float readLightLevel() throws IOException { + if (mDevice == null) { + throw new IllegalStateException("I2C device not open"); + } + + int rawLevel; + + // Read two bytes, which are low and high parts of sensor value + mDevice.read(mBuffer, 2); + rawLevel = mBuffer[0] & 0xFF; // Unsigned int + rawLevel <<= 8; + rawLevel |= (mBuffer[1] & 0xFF); // Unsigned int + + return this.convertRawValueToLux(rawLevel); + } + + public float convertRawValueToLux(int rawLevel){ + return rawLevel / 1.2f; + } + + + /** + * Close the driver and the underlying device. + */ + @Override + public void close() throws IOException { + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } +} + diff --git a/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750SensorDriver.java b/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750SensorDriver.java new file mode 100644 index 0000000..7c9ff7f --- /dev/null +++ b/bh1750/src/main/java/com/google/android/things/contrib/driver/bh1750/Bh1750SensorDriver.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017 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. + */ + +package com.google.android.things.contrib.driver.bh1750; + +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; + +/** + * Driver for the BH1750 digital light sensor. + */ +public class Bh1750SensorDriver implements AutoCloseable { + + private static final String TAG = Bh1750SensorDriver.class.getSimpleName(); + + // DRIVER parameters + // documented at https://source.android.com/devices/sensors/hal-interface.html#sensor_t + private static final String DRIVER_VENDOR = "ROHM"; + private static final String DRIVER_NAME = "BH1750"; + private static final int DRIVER_MIN_DELAY_US = Math.round(1000000.f / Bh1750.MAX_FREQ_HZ); + private static final int DRIVER_MAX_DELAY_US = Math.round(1000000.f / Bh1750.MIN_FREQ_HZ); + + private Bh1750 mDevice; + private LightUserDriver mLightUserDriver; + + /** + * Create a new framework sensor driver connected on the given bus. + * The driver emits {@link android.hardware.Sensor} with light data when + * registered. + * @param bus I2C bus the sensor is connected to. + * @throws IOException + */ + public Bh1750SensorDriver(String bus) throws IOException { + mDevice = new Bh1750(bus); + } + + /** + * Create a new framework sensor driver connected on the given bus and address. + * The driver emits {@link android.hardware.Sensor} with light data when + * registered. + * @param bus I2C bus the sensor is connected to. + * @param address I2C address of the sensor. + * @throws IOException + */ + public Bh1750SensorDriver(String bus, int address) throws IOException { + mDevice = new Bh1750(bus, address); + } + + /** + * Close the driver and the underlying device. + */ + @Override + public void close() throws IOException { + unregisterLightSensor(); + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } + + /** + * Register a {@link UserSensor} that pipes light level readings into the Android SensorManager. + * @see #registerLightSensor() + */ + public void registerLightSensor() { + if (mDevice == null) { + throw new IllegalStateException("cannot register closed driver"); + } + + if (mLightUserDriver == null) { + mLightUserDriver = new LightUserDriver(); + UserDriverManager.getManager().registerSensor(mLightUserDriver.getUserSensor()); + } + } + + /** + * Unregister the light {@link UserSensor}. + */ + public void unregisterLightSensor() { + if (mLightUserDriver != null) { + UserDriverManager.getManager().unregisterSensor(mLightUserDriver.getUserSensor()); + mLightUserDriver = null; + } + } + + private void maybeSleep() throws IOException { + if (mLightUserDriver == null || !mLightUserDriver.isEnabled()) { + mDevice.setMode(Bh1750.POWER_DOWN); + } else { + mDevice.setMode(Bh1750.POWER_ON); + } + } + + private class LightUserDriver extends UserSensorDriver { + // DRIVER parameters + // documented at https://source.android.com/devices/sensors/hal-interface.html#sensor_t + private static final float DRIVER_MAX_RANGE = Bh1750.MAX_LIGHT_LX; + private static final float DRIVER_RESOLUTION = 0.5f; + private static final float DRIVER_POWER = Bh1750.MAX_POWER_CONSUMPTION_UA / 1000.f; + 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 = new UserSensor.Builder() + .setType(Sensor.TYPE_LIGHT) + .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.readLightLevel()}); + } + + @Override + public void setEnabled(boolean enabled) throws IOException { + mEnabled = enabled; + maybeSleep(); + } + + private boolean isEnabled() { + return mEnabled; + } + } + +} + diff --git a/bh1750/src/test/java/com/google/android/things/contrib/driver/bh1750/Bh1750Test.java b/bh1750/src/test/java/com/google/android/things/contrib/driver/bh1750/Bh1750Test.java new file mode 100644 index 0000000..ba91124 --- /dev/null +++ b/bh1750/src/test/java/com/google/android/things/contrib/driver/bh1750/Bh1750Test.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017 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. + */ + +package com.google.android.things.contrib.driver.bh1750; + +import com.google.android.things.pio.I2cDevice; + +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.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; + +public class Bh1750Test { + + @Mock + private I2cDevice mI2c; + + @Rule + public MockitoRule mMokitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException mExpectedException = ExpectedException.none(); + + @Test + public void close() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.close(); + Mockito.verify(mI2c).close(); + } + + @Test + public void close_safeToCallTwice() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.close(); + bh1750.close(); // should not throw + Mockito.verify(mI2c, times(1)).close(); + } + + @Test + public void initialization() throws IOException { + new Bh1750(mI2c); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.POWER_ON}), eq(1)); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.CONTINUOUS_HIGH_RES_MODE}), eq(1)); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.RESET}), eq(1)); + } + + @Test + public void setMode() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + Mockito.reset(mI2c); + + bh1750.setMode(Bh1750.POWER_ON); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.POWER_ON}), eq(1)); + + Mockito.reset(mI2c); + + bh1750.setMode(Bh1750.POWER_DOWN); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.POWER_DOWN}), eq(1)); + } + + @Test + public void setMode_throwsIfClosed() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.close(); + mExpectedException.expect(IllegalStateException.class); + bh1750.setMode(Bh1750.POWER_ON); + } + + @Test + public void setResolution() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + Mockito.reset(mI2c); + + bh1750.setResolution(Bh1750.ONE_TIME_HIGH_RES_MODE); + Mockito.verify(mI2c).write(aryEq(new byte[]{Bh1750.ONE_TIME_HIGH_RES_MODE}), eq(1));} + + @Test + public void setResolution_throwsIfClosed() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.close(); + mExpectedException.expect(IllegalStateException.class); + bh1750.setResolution(Bh1750.ONE_TIME_HIGH_RES_MODE); + } + + @Test + public void readLightLevel() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.readLightLevel(); + + Mockito.verify(mI2c).read(any(byte[].class), eq(2)); + } + + @Test + public void readLightLevel_throwsIfClosed() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + bh1750.close(); + + mExpectedException.expect(IllegalStateException.class); + mExpectedException.expectMessage("I2C device not open"); + bh1750.readLightLevel(); + } + + @Test + public void convertRawLightToLux() throws IOException { + Bh1750 bh1750 = new Bh1750(mI2c); + Assert.assertEquals(100.0f, bh1750.convertRawValueToLux(120), 0.01); + } +} diff --git a/settings.gradle b/settings.gradle index e1d773e..03248b3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,7 @@ include ':adcv2x' include ':apa102' include ':button' +include ':bh1750' include ':bmx280' include ':cap12xx' include ':gps'