diff options
author | Trygve Laugstøl <trygvis@inamo.no> | 2015-01-11 12:28:55 +0100 |
---|---|---|
committer | Trygve Laugstøl <trygvis@inamo.no> | 2015-01-11 12:28:55 +0100 |
commit | 17a1f7227c8c3872fce7bbcc2f5cd46540f9ac52 (patch) | |
tree | 20c2c99e710fd1db438a49029177552204a63591 | |
parent | 4a2ca2d94c827566f8682e8dbd6fbdf17d70b4dd (diff) | |
download | io.trygvis.soilmoisture-android-17a1f7227c8c3872fce7bbcc2f5cd46540f9ac52.tar.gz io.trygvis.soilmoisture-android-17a1f7227c8c3872fce7bbcc2f5cd46540f9ac52.tar.bz2 io.trygvis.soilmoisture-android-17a1f7227c8c3872fce7bbcc2f5cd46540f9ac52.tar.xz io.trygvis.soilmoisture-android-17a1f7227c8c3872fce7bbcc2f5cd46540f9ac52.zip |
o Reading values from the soil sensor.
o Rewrote the database schema to match the new device+sensors model.
o Storing samples in the database.
o To be able to reuse BT callbacks, added a way to always to directly to the next step instead of waiting for an event.
20 files changed, 708 insertions, 293 deletions
diff --git a/app/app.iml b/app/app.iml index 7b810b7..7d4711d 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,14 +1,19 @@ <?xml version="1.0" encoding="UTF-8"?> <module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4"> <component name="FacetManager"> + <facet type="android-gradle" name="Android-Gradle"> + <configuration> + <option name="GRADLE_PROJECT_PATH" value=":app" /> + </configuration> + </facet> <facet type="android" name="Android"> <configuration> - <option name="SELECTED_BUILD_VARIANT" value="debug" /> - <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> - <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> - <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" /> - <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" /> - <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugTestSources" /> + <option name="SELECTED_BUILD_VARIANT" value="level18Debug" /> + <option name="ASSEMBLE_TASK_NAME" value="assembleLevel18Debug" /> + <option name="COMPILE_JAVA_TASK_NAME" value="compileLevel18DebugSources" /> + <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleLevel18DebugTest" /> + <option name="SOURCE_GEN_TASK_NAME" value="generateLevel18DebugSources" /> + <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateLevel18DebugTestSources" /> <option name="ALLOW_USER_CONFIGURATION" value="false" /> <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> @@ -16,28 +21,44 @@ <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" /> </configuration> </facet> - <facet type="android-gradle" name="Android-Gradle"> - <configuration> - <option name="GRADLE_PROJECT_PATH" value=":app" /> - </configuration> - </facet> </component> <component name="NewModuleRootManager" inherit-compiler-output="false"> - <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" /> + <output url="file://$MODULE_DIR$/build/intermediates/classes/level18/debug" /> <exclude-output /> <content url="file://$MODULE_DIR$"> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/debug" isTestSource="true" generated="true" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/debug" type="java-test-resource" /> - <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/debug" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/level18/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/level18/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/level18/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/level18/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/level18/debug" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/level18/debug" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/jni" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18Debug/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/level18/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/level18/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/level18/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/level18/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/level18/debug" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/level18/debug" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/jni" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/level18/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/jni" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestLevel18/rs" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" /> @@ -83,7 +104,7 @@ <excludeFolder url="file://$MODULE_DIR$/build/retrolambda" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" /> </content> - <orderEntry type="jdk" jdkName="Android API 18 Platform" jdkType="Android SDK" /> + <orderEntry type="jdk" jdkName="Android API 19 Platform" jdkType="Android SDK" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="library" exported="" name="answers-1.0.2" level="project" /> <orderEntry type="library" exported="" name="flyway-core-3.1" level="project" /> diff --git a/app/build.gradle b/app/build.gradle index 9df79ba..cb80b6c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ buildscript { classpath 'me.tatarka:gradle-retrolambda:2.5.0' } } + apply plugin: 'com.android.application' apply plugin: 'io.fabric' apply plugin: 'me.tatarka.retrolambda' @@ -18,13 +19,12 @@ repositories { } android { - compileSdkVersion 18 + compileSdkVersion 19 buildToolsVersion "19.1.0" - defaultConfig { - applicationId "io.trygvis.soilmoisture" - minSdkVersion 18 - targetSdkVersion 18 + applicationId 'io.trygvis.soilmoisture' + minSdkVersion 19 + targetSdkVersion 19 versionCode 1 versionName "1.0" } @@ -34,11 +34,20 @@ android { // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + lintOptions { + abortOnError false + } + productFlavors { + level18 { + minSdkVersion 18 + applicationId 'io.trygvis.soilmoisture' + targetSdkVersion 18 + } + } } dependencies { @@ -48,5 +57,5 @@ dependencies { } compile 'org.sqldroid:sqldroid:1.0.3' compile 'org.flywaydb:flyway-core:3.1' -// compile 'net.sourceforge.streamsupport:streamsupport:1.1.2' + // compile 'net.sourceforge.streamsupport:streamsupport:1.1.2' } diff --git a/app/src/main/assets/db/migration/sm/V001.003__new_baseline.sql b/app/src/main/assets/db/migration/sm/V001.003__new_baseline.sql new file mode 100644 index 0000000..c120b08 --- /dev/null +++ b/app/src/main/assets/db/migration/sm/V001.003__new_baseline.sql @@ -0,0 +1,22 @@ +drop table soil_sample; +drop table soil_monitor; + +create table sm_device( + id integer not null primary key autoincrement, + bt_device integer not null references bt_device(id), + useful boolean +); + +create table sm_sensor( + id integer not null primary key autoincrement, + sm_device integer not null references soil_device(id), + idx integer not null, + constraint uq_sensor_idx unique(sm_device, idx) +); + +create table soil_sample( + id integer not null primary key autoincrement, + sm_sensor integer not null references sm_sensor(id), + timestamp timestamp, + value integer +); diff --git a/app/src/main/java/io/trygvis/android/Consumer.java b/app/src/main/java/io/trygvis/android/Consumer.java new file mode 100644 index 0000000..bf8c5e5 --- /dev/null +++ b/app/src/main/java/io/trygvis/android/Consumer.java @@ -0,0 +1,5 @@ +package io.trygvis.android; + +public interface Consumer<T> { + void accept(T t); +} diff --git a/app/src/main/java/io/trygvis/android/bt/BtCallback.java b/app/src/main/java/io/trygvis/android/bt/BtCallback.java index 8dab149..71f7d56 100644 --- a/app/src/main/java/io/trygvis/android/bt/BtCallback.java +++ b/app/src/main/java/io/trygvis/android/bt/BtCallback.java @@ -4,16 +4,18 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; -import static io.trygvis.android.bt.BtPromise.*; +import static io.trygvis.android.bt.BtPromise.PromiseResult; public class BtCallback { + public final boolean stopOnFailure; public final String name; - public BtCallback(String name) { + public BtCallback(boolean stopOnFailure, String name) { + this.stopOnFailure = stopOnFailure; this.name = name; } - public PromiseResult onConnectionStateChange(BluetoothGatt gatt, int newState) { + public PromiseResult onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { throw new NotOverriddenException(); } @@ -49,6 +51,10 @@ public class BtCallback { throw new NotOverriddenException(); } + public PromiseResult onDirect(Object value) { + throw new NotOverriddenException(); + } + public void onFailure() { throw new NotOverriddenException(); } diff --git a/app/src/main/java/io/trygvis/android/bt/BtDevice.java b/app/src/main/java/io/trygvis/android/bt/BtDevice.java index 1c7666e..d8dfefc 100644 --- a/app/src/main/java/io/trygvis/android/bt/BtDevice.java +++ b/app/src/main/java/io/trygvis/android/bt/BtDevice.java @@ -3,6 +3,7 @@ package io.trygvis.android.bt; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; +import android.database.sqlite.SQLiteDatabase; import android.util.Log; import java.util.Date; @@ -21,23 +22,25 @@ public class BtDevice<A extends BtDevice.BtDeviceWrapper<A>> { private final boolean seenBefore; private final Date firstSeen; private Date lastSeen; + private boolean connected; public static interface BtDeviceWrapper<A extends BtDevice.BtDeviceWrapper<A>> { BtDevice<A> getBtDevice(); } - BtDevice(DefaultBtService btService, BluetoothDevice bluetoothDevice, + BtDevice(DefaultBtService btService, BluetoothDevice bluetoothDevice, SQLiteDatabase db, BtService.BtDbIntegration<A> btDbIntegration, long id, Integer rssi, BtScanResult scanResult, boolean seenBefore, Date firstSeen, Date lastSeen) { this.btService = btService; this.bluetoothDevice = bluetoothDevice; - this.tag = btDbIntegration.createTag(this); this.id = id; this.rssi = rssi; this.scanResult = scanResult; this.seenBefore = seenBefore; this.firstSeen = firstSeen; this.lastSeen = lastSeen; + + this.tag = btDbIntegration.createTag(db, this); } public long getId() { @@ -60,6 +63,10 @@ public class BtDevice<A extends BtDevice.BtDeviceWrapper<A>> { return rssi; } + public BtScanResult getScanResult() { + return scanResult; + } + public boolean isSeenBefore() { return seenBefore; } @@ -78,8 +85,15 @@ public class BtDevice<A extends BtDevice.BtDeviceWrapper<A>> { public synchronized boolean connect(BtPromise executor) { Log.i(TAG, "connect(), address=" + bluetoothDevice.getAddress()); - BluetoothGattCallback callback = executor.asCallback(); - gatt = bluetoothDevice.connectGatt(btService, false, callback); + BluetoothGattCallback callback = executor.asCallback(bluetoothDevice.getAddress()); + gatt = bluetoothDevice.connectGatt(btService, false, new WrappingBluetoothGattCallback(callback) { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + BtDevice.this.connected = newState == BluetoothGatt.STATE_CONNECTED; + + super.onConnectionStateChange(gatt, status, newState); + } + }); return true; } @@ -90,8 +104,8 @@ public class BtDevice<A extends BtDevice.BtDeviceWrapper<A>> { } } - public BtScanResult getScanResult() { - return scanResult; + public boolean connected() { + return connected; } @Override diff --git a/app/src/main/java/io/trygvis/android/bt/BtPromise.java b/app/src/main/java/io/trygvis/android/bt/BtPromise.java index 55b6315..6af62c0 100644 --- a/app/src/main/java/io/trygvis/android/bt/BtPromise.java +++ b/app/src/main/java/io/trygvis/android/bt/BtPromise.java @@ -10,9 +10,11 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.Queue; import io.trygvis.android.F2; +import io.trygvis.android.F3; import io.trygvis.android.Function; import static io.trygvis.android.bt.BtPromise.EventType.onCharacteristicChanged; @@ -21,6 +23,7 @@ import static io.trygvis.android.bt.BtPromise.EventType.onCharacteristicWrite; import static io.trygvis.android.bt.BtPromise.EventType.onConnectionStateChange; import static io.trygvis.android.bt.BtPromise.EventType.onDescriptorRead; import static io.trygvis.android.bt.BtPromise.EventType.onDescriptorWrite; +import static io.trygvis.android.bt.BtPromise.EventType.onDirect; import static io.trygvis.android.bt.BtPromise.EventType.onFailure; import static io.trygvis.android.bt.BtPromise.EventType.onFinally; import static io.trygvis.android.bt.BtPromise.EventType.onReliableWriteCompleted; @@ -28,20 +31,48 @@ import static io.trygvis.android.bt.BtPromise.EventType.onServicesDiscovered; public class BtPromise { private final static String TAG = BtPromise.class.getSimpleName(); - private final Queue<BtCallback> actionQ = new ArrayDeque<>(); + private final List<BtCallback> actionQ = new ArrayList<>(); private final Queue<BtCallback> failureQ = new ArrayDeque<>(); private final Queue<BtCallback> finallyQ = new ArrayDeque<>(); - public static final PromiseResult continueDownChain = new Continue(); - public static final PromiseResult doneWithChain = new Done(); + private static final PromiseResult waitForNextEvent = new WaitForNextEvent(); + private static final PromiseResult stop = new Stop(); - public abstract static class PromiseResult { + private Boolean stopOnFailure; + + public static class PromiseResult { + private PromiseResult() { + } + + public static PromiseResult waitForNextEvent() { + return waitForNextEvent; + } + + public static PromiseResult continueDirectly(Object value) { + return new ContinueDirectly(value); + } + + public static PromiseResult stop() { + return stop; + } + + public static PromiseResult detour(BtPromise promise) { + return new Detour(promise); + } } - private static class Continue extends PromiseResult { + private static class WaitForNextEvent extends PromiseResult { } - private static class Done extends PromiseResult { + private static class ContinueDirectly extends PromiseResult { + private final Object value; + + private ContinueDirectly(Object value) { + this.value = value; + } + } + + private static class Stop extends PromiseResult { } private static class Detour extends PromiseResult { @@ -52,18 +83,32 @@ public class BtPromise { } } - public synchronized BtPromise onConnectionStateChange(F2<BluetoothGatt, Integer, PromiseResult> callback) { - actionQ.add(new BtCallback("onConnectionStateChange") { + private boolean stopOnFailure() { + if(stopOnFailure != null) { + boolean b = stopOnFailure; + stopOnFailure = null; + return b; + } + return false; + } + + public BtPromise ignoreFailureForNext() { + stopOnFailure = true; + return this; + } + + public synchronized BtPromise onConnectionStateChange(F3<BluetoothGatt, Integer, Integer, PromiseResult> callback) { + actionQ.add(new BtCallback(stopOnFailure(), "onConnectionStateChange") { @Override - public PromiseResult onConnectionStateChange(BluetoothGatt gatt, int newState) { - return callback.apply(gatt, newState); + public PromiseResult onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + return callback.apply(gatt, status,newState); } }); return this; } public synchronized BtPromise onServicesDiscovered(Function<BluetoothGatt, PromiseResult> callback) { - actionQ.add(new BtCallback("onServicesDiscovered") { + actionQ.add(new BtCallback(stopOnFailure(), "onServicesDiscovered") { @Override public PromiseResult onServicesDiscovered(BluetoothGatt gatt) { return callback.apply(gatt); @@ -73,7 +118,7 @@ public class BtPromise { } public synchronized BtPromise onCharacteristicRead(F2<BluetoothGatt, BluetoothGattCharacteristic, PromiseResult> callback) { - actionQ.add(new BtCallback("onCharacteristicRead") { + actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicRead") { @Override public PromiseResult onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); @@ -83,7 +128,7 @@ public class BtPromise { } public synchronized BtPromise onCharacteristicWrite(F2<BluetoothGatt, BluetoothGattCharacteristic, PromiseResult> callback) { - actionQ.add(new BtCallback("onCharacteristicWrite") { + actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicWrite") { @Override public PromiseResult onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); @@ -93,7 +138,7 @@ public class BtPromise { } public synchronized BtPromise onCharacteristicChanged(F2<BluetoothGatt, BluetoothGattCharacteristic, PromiseResult> callback) { - actionQ.add(new BtCallback("onCharacteristicChanged") { + actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicChanged") { @Override public PromiseResult onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); @@ -103,7 +148,7 @@ public class BtPromise { } public synchronized BtPromise onDescriptorRead(F2<BluetoothGatt, BluetoothGattDescriptor, PromiseResult> callback) { - actionQ.add(new BtCallback("onDescriptorRead") { + actionQ.add(new BtCallback(stopOnFailure(), "onDescriptorRead") { @Override public PromiseResult onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { return callback.apply(gatt, descriptor); @@ -113,7 +158,7 @@ public class BtPromise { } public synchronized BtPromise onDescriptorWrite(F2<BluetoothGatt, BluetoothGattDescriptor, PromiseResult> callback) { - actionQ.add(new BtCallback("onDescriptorWrite") { + actionQ.add(new BtCallback(stopOnFailure(), "onDescriptorWrite") { @Override public PromiseResult onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { return callback.apply(gatt, descriptor); @@ -123,7 +168,7 @@ public class BtPromise { } public synchronized BtPromise onReliableWriteCompleted(Function<BluetoothGatt, PromiseResult> callback) { - actionQ.add(new BtCallback("onReliableWriteCompleted") { + actionQ.add(new BtCallback(stopOnFailure(), "onReliableWriteCompleted") { @Override public PromiseResult onReliableWriteCompleted(BluetoothGatt gatt) { return callback.apply(gatt); @@ -133,7 +178,7 @@ public class BtPromise { } public synchronized BtPromise onReadRemoteRssi(F2<BluetoothGatt, Integer, PromiseResult> callback) { - actionQ.add(new BtCallback("onReadRemoteRssi") { + actionQ.add(new BtCallback(stopOnFailure(), "onReadRemoteRssi") { @Override public PromiseResult onReadRemoteRssi(BluetoothGatt gatt, int rssi) { return callback.apply(gatt, rssi); @@ -142,8 +187,18 @@ public class BtPromise { return this; } + public synchronized BtPromise onDirect(Function<Object, PromiseResult> callback) { + actionQ.add(new BtCallback(stopOnFailure(), "onDirect") { + @Override + public PromiseResult onDirect(Object value) { + return callback.apply(value); + } + }); + return this; + } + public synchronized BtPromise onFailure(Runnable callback) { - failureQ.add(new BtCallback("onFailure") { + failureQ.add(new BtCallback(stopOnFailure(), "onFailure") { @Override public void onFailure() { callback.run(); @@ -153,7 +208,7 @@ public class BtPromise { } public synchronized BtPromise onFinally(Runnable callback) { - finallyQ.add(new BtCallback("finally") { + finallyQ.add(new BtCallback(stopOnFailure(), "finally") { @Override public void onFinally() { callback.run(); @@ -180,12 +235,8 @@ public class BtPromise { return s.toString(); } - BtBluetoothGattCallback asCallback() { - return new BtBluetoothGattCallback(actionQ, failureQ, finallyQ); - } - - public PromiseResult toDetour() { - return new Detour(this); + BtBluetoothGattCallback asCallback(String address) { + return new BtBluetoothGattCallback(address, actionQ, failureQ, finallyQ); } enum EventType { @@ -200,16 +251,18 @@ public class BtPromise { onReadRemoteRssi, + onDirect, onFailure, onFinally, } private static PromiseResult callCallback(EventType key, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, - BluetoothGattDescriptor descriptor, int newState, BtCallback btCallback) { + BluetoothGattDescriptor descriptor, int status, int newState, + BtCallback btCallback, Object value) { switch (key) { case onConnectionStateChange: - return btCallback.onConnectionStateChange(gatt, newState); + return btCallback.onConnectionStateChange(gatt, status, newState); case onServicesDiscovered: return btCallback.onServicesDiscovered(gatt); case onCharacteristicRead: @@ -225,6 +278,8 @@ public class BtPromise { case onReliableWriteCompleted: return btCallback.onReliableWriteCompleted(gatt); + case onDirect: + return btCallback.onDirect(value); case onFailure: btCallback.onFailure(); return null; @@ -239,23 +294,27 @@ public class BtPromise { static class BtBluetoothGattCallback extends BluetoothGattCallback { - private ArrayDeque<BtCallback> actionQ; + private final String address; + private List<BtCallback> actionQ; + private ListIterator<BtCallback> it; private ArrayDeque<BtCallback> failureQ; ArrayDeque<BtCallback> finallyQ; -// private ArrayDeque<BtCallback> initialActionQ; private List<String> events = new ArrayList<>(); - private BtBluetoothGattCallback(Queue<BtCallback> actionQ, Queue<BtCallback> failureQ, Queue<BtCallback> finallyQ) { - this.actionQ = new ArrayDeque<>(actionQ); + private BtBluetoothGattCallback(String address, List<BtCallback> actionQ, Queue<BtCallback> failureQ, Queue<BtCallback> finallyQ) { + this.address = address; + this.actionQ = new ArrayList<>(actionQ); this.failureQ = new ArrayDeque<>(failureQ); this.finallyQ = new ArrayDeque<>(finallyQ); + this.it = actionQ.listIterator(); } - void onEvent(EventType key, String values, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int status, int newState) { + void onEvent(EventType key, String values, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, + BluetoothGattDescriptor descriptor, int status, int newState, Object value) { boolean success = status == BluetoothGatt.GATT_SUCCESS; events.add(key + "(" + values + "), success=" + success); - Log.i(TAG, key + "(" + values + "), success=" + success); + Log.i(TAG, "event: " + key + "(" + values + "), success=" + success); if (!success) { doFailure(); @@ -264,31 +323,33 @@ public class BtPromise { BtCallback btCallback; synchronized (this) { - if (actionQ.isEmpty()) { + if (!it.hasNext()) { Log.d(TAG, "All Bluetooth actions are done"); doFinally(); return; } + btCallback = it.next(); + Log.i(TAG, "Executing bt action: " + btCallback.name); try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } - btCallback = actionQ.remove(); - Log.i(TAG, "Executing bt action: " + btCallback.name); } try { - PromiseResult result = callCallback(key, gatt, characteristic, descriptor, newState, btCallback); + PromiseResult result = callCallback(key, gatt, characteristic, descriptor, status, newState, btCallback, + value); - if (result instanceof Done) { - Log.i(TAG, "The chain is done."); + if (result instanceof Stop) { + Log.i(TAG, "The chain want to stop."); doFinally(); } else if (result instanceof Detour) { Log.i(TAG, "Adding detour"); BtPromise promise = ((Detour) result).promise; + events.add("detour " + promise.actionQ.size()); if (!promise.failureQ.isEmpty()) { Log.i(TAG, "Ignoring " + promise.failureQ.size() + " items from the failure Q"); } @@ -296,12 +357,19 @@ public class BtPromise { Log.i(TAG, "Ignoring " + promise.finallyQ.size() + " items from the finally Q"); } - for (BtCallback callback : promise.actionQ) { - actionQ.addFirst(callback); + synchronized (this) { + for (BtCallback cb : promise.actionQ) { + it.add(cb); + } } + } else if (result instanceof ContinueDirectly) { + value = ((ContinueDirectly) result).value; + onEvent(onDirect, "value=" + value, null, null, null, BluetoothGatt.GATT_SUCCESS, 0, + value); + return; } - if (actionQ.isEmpty()) { + if (!it.hasNext()) { Log.i(TAG, "The queue is empty"); } } catch (NotOverriddenException e) { @@ -313,14 +381,15 @@ public class BtPromise { private void doFailure() { StringBuilder msg = new StringBuilder(); + msg.append("Address: ").append(address).append("\n"); msg.append("Expected events: \n"); for (BtCallback cb : actionQ) { - msg.append(" ").append(cb.name).append("\n"); + msg.append("- ").append(cb.name).append("\n"); } msg.append("Actual events: \n"); for (String event : events) { - msg.append(" ").append(event).append("\n"); + msg.append("- ").append(event).append("\n"); } Log.w(TAG, msg.toString()); @@ -329,7 +398,7 @@ public class BtPromise { for (BtCallback callback = failureQ.poll(); callback != null; callback = failureQ.poll()) { try { - callCallback(onFailure, null, null, null, 0, callback); + callCallback(onFailure, null, null, null, 0, 0, callback, null); } catch (NotOverriddenException e) { return; } @@ -345,7 +414,7 @@ public class BtPromise { for (BtCallback callback = finallyQ.poll(); callback != null; callback = finallyQ.poll()) { try { - callCallback(onFinally, null, null, null, 0, callback); + callCallback(onFinally, null, null, null, 0, 0, callback, null); } catch (NotOverriddenException e) { return; } @@ -358,42 +427,42 @@ public class BtPromise { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - onEvent(onConnectionStateChange, "status=" + status + ", newState=" + newState, gatt, null, null, status, newState); + onEvent(onConnectionStateChange, "status=" + status + ", newState=" + newState, gatt, null, null, status, newState, null); } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { - onEvent(onServicesDiscovered, "status=" + status, gatt, null, null, status, 9); + onEvent(onServicesDiscovered, "status=" + status, gatt, null, null, status, 9, null); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - onEvent(onCharacteristicRead, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0); + onEvent(onCharacteristicRead, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0, null); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - onEvent(onCharacteristicWrite, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0); + onEvent(onCharacteristicWrite, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0, null); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - onEvent(onCharacteristicChanged, "characteristic=" + characteristic.getUuid(), gatt, characteristic, null, BluetoothGatt.GATT_SUCCESS, 0); + onEvent(onCharacteristicChanged, "characteristic=" + characteristic.getUuid(), gatt, characteristic, null, BluetoothGatt.GATT_SUCCESS, 0, null); } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - onEvent(onDescriptorRead, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0); + onEvent(onDescriptorRead, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0, null); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - onEvent(onDescriptorWrite, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0); + onEvent(onDescriptorWrite, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0, null); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { - onEvent(onReliableWriteCompleted, "status=" + status, gatt, null, null, status, 0); + onEvent(onReliableWriteCompleted, "status=" + status, gatt, null, null, status, 0, null); } @Override diff --git a/app/src/main/java/io/trygvis/android/bt/BtService.java b/app/src/main/java/io/trygvis/android/bt/BtService.java index fb11027..ff2c68c 100644 --- a/app/src/main/java/io/trygvis/android/bt/BtService.java +++ b/app/src/main/java/io/trygvis/android/bt/BtService.java @@ -33,7 +33,7 @@ public interface BtService<A extends BtDevice.BtDeviceWrapper<A>> { <T> T runTx(Function<SQLiteDatabase, T> action); public static interface BtDbIntegration<A extends BtDevice.BtDeviceWrapper<A>> { - A createTag(BtDevice<A> a); + A createTag(SQLiteDatabase db, BtDevice<A> a); } public static class BtServiceListenerBroadcastReceiver extends BroadcastReceiver { diff --git a/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java b/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java index 80a195a..cc7d484 100644 --- a/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java +++ b/app/src/main/java/io/trygvis/android/bt/DefaultBtService.java @@ -289,7 +289,7 @@ public class DefaultBtService<A extends BtDevice.BtDeviceWrapper<A>> extends Ser Log.i(TAG, "New device: " + address + ", seenBefore=" + seenBefore); cursor.close(); - return new BtDevice<>(this, bluetoothDevice, btDbIntegration, id, rssi, scanResult, + return new BtDevice<>(this, bluetoothDevice, db, btDbIntegration, id, rssi, scanResult, seenBefore, firstSeen, lastSeen); }); diff --git a/app/src/main/java/io/trygvis/android/bt/WrappingBluetoothGattCallback.java b/app/src/main/java/io/trygvis/android/bt/WrappingBluetoothGattCallback.java new file mode 100644 index 0000000..8d790d1 --- /dev/null +++ b/app/src/main/java/io/trygvis/android/bt/WrappingBluetoothGattCallback.java @@ -0,0 +1,60 @@ +package io.trygvis.android.bt; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +class WrappingBluetoothGattCallback extends BluetoothGattCallback { + + private final BluetoothGattCallback wrapped; + + public WrappingBluetoothGattCallback(BluetoothGattCallback wrapped) { + this.wrapped = wrapped; + } + + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + wrapped.onConnectionStateChange(gatt, status, newState); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + wrapped.onServicesDiscovered(gatt, status); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + wrapped.onCharacteristicRead(gatt, characteristic, status); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + wrapped.onCharacteristicWrite(gatt, characteristic, status); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + wrapped.onCharacteristicChanged(gatt, characteristic); + } + + @Override + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + wrapped.onDescriptorRead(gatt, descriptor, status); + } + + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + wrapped.onDescriptorWrite(gatt, descriptor, status); + } + + @Override + public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { + wrapped.onReliableWriteCompleted(gatt, status); + } + + @Override + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + wrapped.onReadRemoteRssi(gatt, rssi, status); + } +} diff --git a/app/src/main/java/io/trygvis/soilmoisture/DefaultSoilMoistureService.java b/app/src/main/java/io/trygvis/soilmoisture/DefaultSoilMoistureService.java index a2723d5..3ff6e66 100644 --- a/app/src/main/java/io/trygvis/soilmoisture/DefaultSoilMoistureService.java +++ b/app/src/main/java/io/trygvis/soilmoisture/DefaultSoilMoistureService.java @@ -9,12 +9,17 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Intent; import android.content.ServiceConnection; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.os.IBinder; import android.util.Log; import java.util.ArrayList; import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -25,15 +30,24 @@ import io.trygvis.android.bt.BtService; import io.trygvis.android.bt.DefaultBtService; import io.trygvis.bluetooth.TrygvisIoUuids; -import static io.trygvis.android.bt.BtPromise.continueDownChain; -import static io.trygvis.android.bt.BtPromise.doneWithChain; +import static io.trygvis.android.bt.BtPromise.PromiseResult.continueDirectly; +import static io.trygvis.android.bt.BtPromise.PromiseResult.detour; +import static io.trygvis.android.bt.BtPromise.PromiseResult.stop; +import static io.trygvis.android.bt.BtPromise.PromiseResult.waitForNextEvent; import static io.trygvis.android.bt.BtService.BtServiceListenerBroadcastReceiver; +import static io.trygvis.bluetooth.TrygvisIoUuids.CLIENT_CHARACTERISTIC_CONFIG; import static io.trygvis.soilmoisture.SmDevice.GetSensorCountRes; +import static io.trygvis.soilmoisture.SmDevice.GetValueRes; import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.GET_SENSOR_COUNT; +import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.GET_VALUE; +import static io.trygvis.soilmoisture.SmDevice.createGetSensorCountReq; +import static io.trygvis.soilmoisture.SmDevice.createGetValueReq; +import static io.trygvis.soilmoisture.SmDevice.parseResponse; +import static java.lang.String.valueOf; +import static java.lang.System.currentTimeMillis; public class DefaultSoilMoistureService extends Service implements SoilMoistureService { private final static String TAG = DefaultSoilMoistureService.class.getSimpleName(); - private final static int DEFAULT_WARNING_LEVEL = 750; private final IBinder binder = new LocalBinder<>(this); @@ -58,7 +72,7 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS @Override public void onServiceConnected(ComponentName componentName, IBinder service) { btService = ((LocalBinder<BtService<SmDevice>>) service).getService(); - boolean ok = btService.initialize(DefaultSoilMoistureService.this::onNewDevice); + boolean ok = btService.initialize(DefaultSoilMoistureService.this::createTag); sendBroadcast(createReady(ok)); } @@ -98,28 +112,16 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS BtDevice<SmDevice> btDevice = btService.getDevice(address); SmDevice smDevice = btDevice.getTag(); + sendBroadcast(createNewDevice(address)); + if (!smDevice.isProbed()) { Log.i(TAG, "Probing " + address + ", name=" + btDevice.getName()); BtPromise executor = new BtPromise(). - onConnectionStateChange((gatt, newState) -> { - //noinspection SimplifiableIfStatement - if (newState == BluetoothGatt.STATE_CONNECTED) { - Log.i(TAG, "Connected to " + address + ", getting services"); - return gatt.discoverServices() ? continueDownChain : doneWithChain; - } else { - Log.i(TAG, "Disconnected from " + address + ", trying again"); - - return new BtPromise().onConnectionStateChange((gatt2, newState2) -> { - if (newState2 == BluetoothGatt.STATE_CONNECTED) { - Log.i(TAG, "Connected to " + address + ", getting services"); - return continueDownChain; - } - - Log.i(TAG, "Could still not connect to " + address + ", failing."); - - return doneWithChain; - }).toDetour(); - } + ignoreFailureForNext(). + onConnectionStateChange(DefaultSoilMoistureService::defaultConnectCallback). + onDirect(v -> { + BluetoothGatt gatt = (BluetoothGatt) v; + return gatt.discoverServices() ? waitForNextEvent() : stop(); }). onServicesDiscovered(gatt -> { Log.i(TAG, "Services discovered"); @@ -127,38 +129,37 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE); if (service == null) { - return doneWithChain; + return stop(); } BluetoothGattCharacteristic soilMoisture = service.getCharacteristic(TrygvisIoUuids.Characteristics.SOIL_MOISTURE); if (soilMoisture == null) { - return doneWithChain; + return stop(); } BluetoothGattDescriptor ccg = soilMoisture.getDescriptor(TrygvisIoUuids.CLIENT_CHARACTERISTIC_CONFIG); ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.setCharacteristicNotification(soilMoisture, true); - return gatt.writeDescriptor(ccg) ? continueDownChain : doneWithChain; + return gatt.writeDescriptor(ccg) ? waitForNextEvent() : stop(); }). onDescriptorWrite((gatt, descriptor) -> { Log.i(TAG, "Notifications enabled, getting sensor count"); BluetoothGattCharacteristic c = descriptor.getCharacteristic(); - c.setValue(SmDevice.createGetSensorCountReq()); - return gatt.writeCharacteristic(c) ? continueDownChain : doneWithChain; + c.setValue(createGetSensorCountReq()); + return gatt.writeCharacteristic(c) ? waitForNextEvent() : stop(); }). - onCharacteristicWrite((gatt, characteristic) -> continueDownChain). + onCharacteristicWrite((gatt, characteristic) -> waitForNextEvent()). onCharacteristicChanged((gatt, characteristic) -> { - GetSensorCountRes getSensorCountRes = SmDevice.parseResponse(characteristic.getValue(), + GetSensorCountRes getSensorCountRes = parseResponse(characteristic.getValue(), GET_SENSOR_COUNT, GetSensorCountRes.class); Log.i(TAG, "The device has " + getSensorCountRes.count + " sensors."); - smDevice.setIsUseful(true); - smDevice.setSensorCount(getSensorCountRes.count); + markDeviceAsUseful(smDevice, getSensorCountRes.count); - return doneWithChain; + return stop(); }). onFinally(() -> { btDevice.disconnect(); @@ -167,28 +168,69 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS smDevice.setIsUseful(false); } - btService.runTx(db -> { - if (!btDevice.isSeenBefore() && smDevice.isUseful()) { - for (SmSensor soilMonitor : smDevice.getSensors()) { - ContentValues values = new ContentValues(); - values.put("bt_device", btDevice.getId()); - values.put("warning_level", DEFAULT_WARNING_LEVEL); - db.insert("soil_monitor", null, values); - } - } - - return null; - }); - sendBroadcast(createNewDevice(address)); }); btDevice.connect(executor); - } else { - sendBroadcast(createNewDevice(address)); } } }; + private void markDeviceAsUseful(SmDevice device, int sensorCount) { + btService.runTx(db -> { + device.setIsUseful(true); + + // Find all already registered sensors and register the rest + Map<Integer, Long> indexes = new HashMap<>(); + Cursor cursor = db.query(Tables.T_SM_SENSOR, new String[]{Tables.C_ID, Tables.C_INDEX}, + Tables.C_SM_DEVICE + "=?", new String[]{valueOf(device.id)}, null, null, null); + while (cursor.moveToNext()) { + indexes.put(cursor.getInt(1), cursor.getLong(0)); + } + + ContentValues values = new ContentValues(); + values.put(Tables.C_USEFUL, true); + db.update(Tables.T_SM_DEVICE, values, "id=?", new String[]{valueOf(device.id)}); + + for (int i = 0; i < sensorCount; i++) { + Long id = indexes.get(i); + + if (id == null) { + values = new ContentValues(); + values.put(Tables.C_SM_DEVICE, device.id); + values.put(Tables.C_INDEX, i); + id = db.insert(Tables.T_SM_SENSOR, null, values); + } + + device.addSensor(new SmSensor(device, id, (byte) i)); + } + + return null; + }); + } + + public static BtPromise.PromiseResult defaultConnectCallback(BluetoothGatt gatt, int status, Integer newState) { + Log.i(TAG, "defaultConnectCallback: status=" + status + ", newState=" + newState); + String address = gatt.getDevice().getAddress(); + //noinspection SimplifiableIfStatement + if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { + Log.i(TAG, "Connected to " + address); + return continueDirectly(gatt); + } else { + Log.i(TAG, "Disconnected from " + address + ", trying again"); + + return detour(new BtPromise().onConnectionStateChange((gatt2, status2, newState2) -> { + if (newState2 == BluetoothGatt.STATE_CONNECTED) { + Log.i(TAG, "Connected to " + address); + return continueDirectly(gatt); + } + + Log.i(TAG, "Could still not connect to " + address + ", failing."); + + return stop(); + })); + } + } + // ----------------------------------------------------------------------- // SmDevicesManager Implementation // ----------------------------------------------------------------------- @@ -223,13 +265,82 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS } // ----------------------------------------------------------------------- - // Event creation and dispatching + // // ----------------------------------------------------------------------- - private SmDevice onNewDevice(BtDevice<SmDevice> btDevice) { - return new SmDevice(btDevice); + private SmDevice createTag(SQLiteDatabase db, BtDevice<SmDevice> btDevice) { + Cursor cursor = db.query(Tables.T_SM_DEVICE, new String[]{Tables.C_ID}, Tables.C_BT_DEVICE + "=?", + new String[]{valueOf(btDevice.getId())}, null, null, null); + + long id; + if (cursor.moveToNext()) { + id = cursor.getLong(0); + } else { + ContentValues values = new ContentValues(); + values.put(Tables.C_BT_DEVICE, btDevice.getId()); + id = db.insert(Tables.T_SM_DEVICE, null, values); + } + + return new SmDevice(this, btDevice, id); + } + + void handleNewSensorValueReady(SmSensor sensor, int value) { + long timestamp = currentTimeMillis(); + btService.runTx(db -> { + ContentValues values = new ContentValues(); + values.put(Tables.C_SM_SENSOR, sensor.getId()); + values.put(Tables.C_TIMESTAMP, timestamp); + values.put(Tables.C_VALUE, value); + return db.insert(Tables.T_SOIL_SAMPLE, null, values); + }); + + sensor.updateLastValue(new Date(timestamp), value); + sendBroadcast(createNewSample(sensor)); + } + + void readCurrentValue(SmSensor sensor) { + BtPromise promise = new BtPromise(). + onConnectionStateChange(DefaultSoilMoistureService::defaultConnectCallback). + onDirect(v -> { + BluetoothGatt gatt = (BluetoothGatt) v; + return gatt.discoverServices() ? waitForNextEvent() : stop(); + }). + onServicesDiscovered(gatt -> { + BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE); + BluetoothGattCharacteristic soilMoisture = service.getCharacteristic(TrygvisIoUuids.Characteristics.SOIL_MOISTURE); + + BluetoothGattDescriptor ccg = soilMoisture.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG); + ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + gatt.setCharacteristicNotification(soilMoisture, true); + + return gatt.writeDescriptor(ccg) ? waitForNextEvent() : stop(); + }). + onDescriptorWrite((gatt, descriptor) -> { + BluetoothGattCharacteristic c = descriptor.getCharacteristic(); + c.setValue(createGetValueReq((byte) sensor.getIndex())); + return gatt.writeCharacteristic(c) ? waitForNextEvent() : stop(); + }). + onCharacteristicWrite((gatt, characteristic) -> waitForNextEvent()). + onCharacteristicChanged((gatt, characteristic) -> { + GetValueRes getSensorCountRes = parseResponse(characteristic.getValue(), + GET_VALUE, GetValueRes.class); + + handleNewSensorValueReady(sensor, getSensorCountRes.value); + + gatt.disconnect(); + + return stop(); + }). + onFinally(() -> { + }); + + sensor.getDevice().getBtDevice().connect(promise); } + // ----------------------------------------------------------------------- + // Event creation and dispatching + // ----------------------------------------------------------------------- + private Intent createReady(boolean success) { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "ready"). @@ -252,15 +363,24 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS putExtra("address", address); } + private Intent createNewSample(SmSensor sensor) { + return new Intent(SoilMoistureListener.INTENT_NAME). + putExtra("event", "newSample"). + putExtra("address", sensor.getDevice().getBtDevice().getAddress()). + putExtra("index", sensor.getIndex()); + } + public static void dispatchEvent(Intent intent, SoilMoistureListener listener) { String event = intent.getStringExtra("event"); Log.i(TAG, "Dispatching event " + intent.getAction() + "/" + event); switch (event) { case "ready": - listener.onReady(intent.getBooleanExtra("success", false)); + listener.onReady( + intent.getBooleanExtra("success", false)); break; case "newDevice": - listener.onNewDevice(intent.getStringExtra("address")); + listener.onNewDevice( + intent.getStringExtra("address")); break; case "scanStarted": listener.onScanStarted(); @@ -268,6 +388,11 @@ public class DefaultSoilMoistureService extends Service implements SoilMoistureS case "scanStopped": listener.onScanStopped(); break; + case "newSample": + listener.onNewSample( + intent.getStringExtra("address"), + intent.getIntExtra("index", -1)); + break; default: break; } diff --git a/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java b/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java index 7aa3534..3a88f6c 100644 --- a/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java +++ b/app/src/main/java/io/trygvis/soilmoisture/MainActivity.java @@ -16,9 +16,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.ListAdapter; -import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; @@ -209,9 +207,18 @@ public class MainActivity extends ListActivity { } } - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - stopScan(); + // ----------------------------------------------------------------------- + // + // ----------------------------------------------------------------------- + + private void onDeviceClick(SmDevice device) { + Log.i(TAG, "onDeviceClick, device=" + device.getBtDevice().getId()); + } + + private void onSensorClick(SmSensor sensor) { + Log.i(TAG, "onSensorClick, device=" + sensor.getDevice().getBtDevice().getId() + "/" + sensor.getIndex()); + + sensor.readCurrentValue(); } // ----------------------------------------------------------------------- @@ -223,16 +230,25 @@ public class MainActivity extends ListActivity { final TextView deviceAddress; final TextView rssi; final TextView info; - final ProgressBar spinner; - final Button connect; DeviceItem(View view) { this.deviceName = (TextView) view.findViewById(R.id.device_name); this.deviceAddress = (TextView) view.findViewById(R.id.device_address); this.rssi = (TextView) view.findViewById(R.id.device_rssi); this.info = (TextView) view.findViewById(R.id.device_info); - this.spinner = (ProgressBar) view.findViewById(R.id.device_spinner); - this.connect = (Button) view.findViewById(R.id.button_connect); + } + } + + static class SensorItem { + final SmSensor sensor; + final TextView description; + final ProgressBar sensorProgress; + + SensorItem(SmSensor sensor, View view) { + this.sensor = sensor; + this.description = (TextView) view.findViewById(R.id.description); + this.sensorProgress = (ProgressBar) view.findViewById(R.id.sensor_progress); + sensorProgress.setMax(1024); } } @@ -254,18 +270,24 @@ public class MainActivity extends ListActivity { for (SmDevice d : devices) { (d.isUseful() ? usefulDevices : unusefulDevices).add(d); } - List<SmSensor> monitors = new ArrayList<>(); + List<SmSensor> sensors = new ArrayList<>(); for (SmDevice d : devices) { - monitors.addAll(d.getSensors()); + if (d.isUseful()) { + sensors.addAll(d.getSensors()); + } } if (groupByDevice) { - current.addAll(usefulDevices); +// current.addAll(usefulDevices); + for (SmDevice device : usefulDevices) { + current.add(device); + current.addAll(device.getSensors()); + } if (showAll) { current.addAll(unusefulDevices); } } else { - current.addAll(monitors); + current.addAll(sensors); } dataSetObservable.notifyChanged(); } @@ -374,50 +396,77 @@ public class MainActivity extends ListActivity { if (o instanceof SmDevice) { return getSmDeviceView((SmDevice) o, view); } else if (o instanceof SmSensor) { - return getSoilMonitorView((SmSensor) o, view); + return getSoilSensorView((SmSensor) o, view); } throw new RuntimeException("Not implemented"); } private View getSmDeviceView(SmDevice smDevice, View view) { - - DeviceItem item; if (view == null) { - view = inflater.inflate(R.layout.listitem_device, null); - item = new DeviceItem(view); - view.setTag(item); - view.setClickable(false); - } else { - item = (DeviceItem) view.getTag(); + view = inflater.inflate(R.layout.fragment_device, null); + view.setTag(new DeviceItem(view)); + view.setClickable(true); + view.setOnClickListener(v -> onDeviceClick(smDevice)); } + DeviceItem item = (DeviceItem) view.getTag(); + if (smDevice.getName() != null) { item.deviceName.setText(smDevice.getName()); } else { item.deviceName.setText(R.string.unknown_device); } - item.deviceAddress.setText(smDevice.getBtDevice().getAddress()); + String address = smDevice.getBtDevice().getAddress(); + + if (!smDevice.isProbed()) { + address += " not probed"; + } else if (smDevice.isUseful()) { + address += " useful"; + } else { + address += " not useful"; + } + + item.deviceAddress.setText(address); item.rssi.setText(getText(R.string.rssi) + ": " + (smDevice.getBtDevice().getRssi() != 0 ? valueOf(smDevice.getBtDevice().getRssi()) : getText(R.string.unknown))); - if (smDevice.isUseful()) { - item.info.setText("number of sensors: " + smDevice.getSensors().size()); + boolean useful = smDevice.isUseful(); + + if (useful) { + item.info.setText("Number of sensors: " + smDevice.getSensors().size()); } else { item.info.setText(""); } - boolean useful = smDevice.isUseful(); - item.spinner.setVisibility(useful ? View.GONE : View.VISIBLE); - item.connect.setVisibility(useful ? View.VISIBLE : View.GONE); - view.setClickable(useful); +// view.setClickable(useful); return view; } - private View getSoilMonitorView(SmSensor smSensor, View view) { - throw new RuntimeException("Not implemented"); + private View getSoilSensorView(SmSensor smSensor, View view) { + if (view == null) { + view = inflater.inflate(R.layout.fragment_sensor, null); + view.setTag(new SensorItem(smSensor, view)); + view.setClickable(true); + view.setOnClickListener(v -> onSensorClick(smSensor)); + } + + SensorItem item = (SensorItem) view.getTag(); + + Integer value = smSensor.getLastValue(); + String text = "Connected: " + smSensor.getDevice().getBtDevice().connected(); + text += ", value: " + (value == null ? "Unknown" : value); + item.description.setText(text); + + if (value != null) { + item.sensorProgress.setProgress(value); + } else { + item.sensorProgress.setIndeterminate(true); + } + + return view; } } @@ -459,5 +508,10 @@ public class MainActivity extends ListActivity { deviceList.devices.add(device); deviceList.sort(); } + + @Override + public void onNewSample(String address, int sensor) { + deviceList.notifyDataSetChanged(); + } } } diff --git a/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java b/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java index b51b3fa..4cb25f3 100644 --- a/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java +++ b/app/src/main/java/io/trygvis/soilmoisture/SmDevice.java @@ -18,16 +18,22 @@ import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.SET_WARNING_VALUE; class SmDevice implements BtDevice.BtDeviceWrapper<SmDevice> { private final static String TAG = SmDevice.class.getSimpleName(); + final DefaultSoilMoistureService smService; + private final BtDevice<SmDevice> btDevice; + final long id; + private String name; private Boolean isUseful; private List<SmSensor> sensors = new ArrayList<>(); - public SmDevice(BtDevice<SmDevice> btDevice) { + public SmDevice(DefaultSoilMoistureService smService, BtDevice<SmDevice> btDevice, long id) { + this.smService = smService; this.btDevice = btDevice; + this.id = id; Log.i(TAG, "new device"); name = btDevice.getName(); @@ -63,14 +69,21 @@ class SmDevice implements BtDevice.BtDeviceWrapper<SmDevice> { } public List<SmSensor> getSensors() { + if (!isUseful()) { + throw new IllegalStateException("Not a useful device"); + } return sensors; } + void addSensor(SmSensor sensor) { + sensors.add(sensor); + } + // ----------------------------------------------------------------------- - // + // Message parsing and handling. // ----------------------------------------------------------------------- - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "StatementWithEmptyBody"}) public static <T> T parseResponse(byte[] bytes, SmCmdCode code, Class<T> klass) { byte c = bytes[0]; @@ -78,10 +91,10 @@ class SmDevice implements BtDevice.BtDeviceWrapper<SmDevice> { throw new RuntimeException("Expected response of type " + code + ", got " + c); } - Object value = null; if (c == GET_SENSOR_COUNT.code) { return (T) new GetSensorCountRes(bytes[1]); } else if (c == GET_VALUE.code) { + return (T) new GetValueRes((bytes[2] & 0xff) << 8 | (bytes[1] & 0xff)); } else if (c == SET_WARNING_VALUE.code) { } else if (c == GET_WARNING_VALUE.code) { } else if (c == SET_SENSOR_NAME.code) { @@ -91,13 +104,6 @@ class SmDevice implements BtDevice.BtDeviceWrapper<SmDevice> { throw new RuntimeException("Unknown code: " + c); } - public void setSensorCount(int count) { - sensors = new ArrayList<>(); - for (int index = 0; index < count; index++) { - sensors.add(new SmSensor(this, index)); - } - } - public static class GetSensorCountRes { public final int count; @@ -106,6 +112,14 @@ class SmDevice implements BtDevice.BtDeviceWrapper<SmDevice> { } } + public static class GetValueRes { + public final int value; + + public GetValueRes(int value) { + this.value = value; + } + } + public static byte[] createGetSensorCountReq() { return new byte[]{ GET_SENSOR_COUNT.code diff --git a/app/src/main/java/io/trygvis/soilmoisture/SmSensor.java b/app/src/main/java/io/trygvis/soilmoisture/SmSensor.java index ec4b423..2a0d9cd 100644 --- a/app/src/main/java/io/trygvis/soilmoisture/SmSensor.java +++ b/app/src/main/java/io/trygvis/soilmoisture/SmSensor.java @@ -5,38 +5,57 @@ import java.util.Date; class SmSensor { private final SmDevice device; - private final int index; + public final long id; + + public final int index; + + private String name; private Date timestamp; - private int lastValue; + private Integer lastValue; - SmSensor(SmDevice device, int index) { + SmSensor(SmDevice device, long id, int index) { this.device = device; + this.id = id; this.index = index; + this.name = "Sensor #" + index; } public SmDevice getDevice() { return device; } + public long getId() { + return id; + } + public int getIndex() { return index; } - public int getLastValue() { - return lastValue; + public String getName() { + return name; } - public void setLastValue(int lastValue) { - this.lastValue = lastValue; + public Integer getLastValue() { + return lastValue; } public Date getTimestamp() { return timestamp; } - public void setTimestamp(Date timestamp) { + // ----------------------------------------------------------------------- + // + // ----------------------------------------------------------------------- + + public void readCurrentValue() { + device.smService.readCurrentValue(this); + } + + void updateLastValue(Date timestamp, int lastValue) { this.timestamp = timestamp; + this.lastValue = lastValue; } } diff --git a/app/src/main/java/io/trygvis/soilmoisture/SoilMoistureService.java b/app/src/main/java/io/trygvis/soilmoisture/SoilMoistureService.java index 8100649..643fa08 100644 --- a/app/src/main/java/io/trygvis/soilmoisture/SoilMoistureService.java +++ b/app/src/main/java/io/trygvis/soilmoisture/SoilMoistureService.java @@ -25,7 +25,7 @@ public interface SoilMoistureService { public static final IntentFilter INTENT_FILTER = new IntentFilter(INTENT_NAME); - public void onReceive(Context context, Intent intent) { + public final void onReceive(Context context, Intent intent) { if (!intent.getAction().equals(INTENT_NAME)) { return; } @@ -44,5 +44,8 @@ public interface SoilMoistureService { public void onScanStopped() { } + + public void onNewSample(String address, int sensor) { + } } } diff --git a/app/src/main/java/io/trygvis/soilmoisture/Tables.java b/app/src/main/java/io/trygvis/soilmoisture/Tables.java new file mode 100644 index 0000000..56e5041 --- /dev/null +++ b/app/src/main/java/io/trygvis/soilmoisture/Tables.java @@ -0,0 +1,24 @@ +package io.trygvis.soilmoisture; + +public class Tables { + public static String T_SM_DEVICE = "sm_device"; + public static String T_SM_SENSOR = "sm_sensor"; + public static String T_SOIL_SAMPLE = "soil_sample"; + + // sm_device + public static String C_ID = "id"; + public static String C_BT_DEVICE = "bt_device"; + public static String C_USEFUL = "useful"; +// public static String C_NAME = "name"; + + // sm_sensor + public static String C_SM_DEVICE = "sm_device"; + // name + public static String C_WARNING_VALUE = "warning_value"; + + // soil_sample + public static String C_SM_SENSOR = "sm_sensor"; + public static String C_INDEX = "idx"; + public static String C_TIMESTAMP = "timestamp"; + public static String C_VALUE = "value"; +} diff --git a/app/src/main/res/layout/fragment_device.xml b/app/src/main/res/layout/fragment_device.xml new file mode 100644 index 0000000..e6a2f25 --- /dev/null +++ b/app/src/main/res/layout/fragment_device.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:clickable="true"> + + <TextView + android:id="@+id/device_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="24sp" + android:layout_alignParentTop="true" + android:layout_marginTop="0dp"/> + + <TextView + android:id="@+id/device_address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="12sp" + android:layout_below="@+id/device_name" + android:layout_alignParentStart="true"/> + + <TextView + android:id="@+id/device_rssi" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="12sp" + android:layout_below="@+id/device_address" + android:layout_alignParentStart="true"/> + + <TextView + android:id="@+id/device_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="12sp" + android:layout_below="@+id/device_rssi" + android:layout_alignParentStart="true"/> + +</RelativeLayout> diff --git a/app/src/main/res/layout/fragment_gauge.xml b/app/src/main/res/layout/fragment_gauge.xml deleted file mode 100644 index ff1df86..0000000 --- a/app/src/main/res/layout/fragment_gauge.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clickable="true" - android:focusable="true" - > - - <TextView - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:text="Gauge: XX" - android:id="@+id/gauge_title" - android:layout_gravity="center_horizontal" - android:gravity="start"/> - - <SeekBar - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:id="@+id/gauge_seek_bar" - android:layout_gravity="center_horizontal"/> -</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_sensor.xml b/app/src/main/res/layout/fragment_sensor.xml new file mode 100644 index 0000000..82d0d77 --- /dev/null +++ b/app/src/main/res/layout/fragment_sensor.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_height="wrap_content" + android:layout_width="wrap_content"> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_marginTop="0dp"/> + + <ProgressBar + style="?android:attr/progressBarStyleHorizontal" + android:id="@+id/sensor_progress" + android:layout_width="fill_parent" + android:layout_height="20dp" + android:layout_below="@id/description"/> + +</RelativeLayout> diff --git a/app/src/main/res/layout/listitem_device.xml b/app/src/main/res/layout/listitem_device.xml deleted file mode 100644 index 44021d9..0000000 --- a/app/src/main/res/layout/listitem_device.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_height="wrap_content" - android:layout_width="wrap_content"> - - <TextView - android:id="@+id/device_name" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="24sp" - android:layout_alignParentTop="true" - android:layout_marginTop="0dp" - android:layout_toStartOf="@+id/spacer"/> - - <TextView - android:id="@+id/device_address" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="12sp" - android:layout_below="@+id/device_name" - android:layout_alignParentStart="true" - android:layout_toStartOf="@+id/spacer"/> - - <TextView - android:id="@+id/device_rssi" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="12sp" - android:layout_below="@+id/device_address" - android:layout_alignParentStart="true" - android:layout_toStartOf="@+id/spacer"/> - - <TextView - android:id="@+id/device_info" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textSize="12sp" - android:layout_below="@+id/device_rssi" - android:layout_alignParentStart="true" - android:layout_toStartOf="@+id/spacer"/> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:id="@+id/spacer" - android:layout_alignParentEnd="true" - android:layout_alignParentTop="true" - android:layout_centerInParent="true"> - - <Button - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:text="@string/connect" - android:id="@+id/button_connect" - /> - - <ProgressBar - style="?android:attr/progressBarStyleLarge" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/device_spinner" - android:visibility="gone"/> - - </LinearLayout> - -</RelativeLayout> |