From 20c4e49fe14ca729cd7729537553a9f20584a69c Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Tue, 9 Feb 2016 17:52:21 +0100 Subject: [PATCH] Refactoring of the Pebble Health steps data receiver. Added logic to deal with pebble health sleep data. Added database helper to change the type of a range of samples (needed for sleep data). Fixes to the Pebble Health sample provider. --- .../database/ActivityDatabaseHandler.java | 16 +++ .../gadgetbridge/database/DBHandler.java | 3 + .../devices/pebble/HealthSampleProvider.java | 25 +++- .../devices/pebble/DatalogSessionHealth.java | 96 --------------- .../pebble/DatalogSessionHealthSleep.java | 91 ++++++++++++++ .../pebble/DatalogSessionHealthSteps.java | 115 ++++++++++++++++++ .../devices/pebble/PebbleProtocol.java | 4 +- 7 files changed, 251 insertions(+), 99 deletions(-) delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealth.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java index d0263b57..32bbc828 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java @@ -242,4 +242,20 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl builder.append(')'); return builder.toString(); } + + @Override + public void changeStoredSamplesType(int timestampFrom, int timestampTo, byte kind, SampleProvider provider) { + try (SQLiteDatabase db = this.getReadableDatabase()) { + String sql = "UPDATE " + TABLE_GBACTIVITYSAMPLES + " SET " + KEY_TYPE +"= ? WHERE " + + KEY_PROVIDER + " = ? AND " + + KEY_TIMESTAMP + " >= ? AND "+ KEY_TIMESTAMP + " < ? ;"; //do not use BETWEEN because the range is inclusive in that case! + + SQLiteStatement statement = db.compileStatement(sql); + statement.bindLong(1, kind); + statement.bindLong(2, provider.getID()); + statement.bindLong(3, timestampFrom); + statement.bindLong(4, timestampTo); + statement.execute(); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java index 54e33829..52c4ba10 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java @@ -29,4 +29,7 @@ public interface DBHandler { void addGBActivitySamples(ActivitySample[] activitySamples); SQLiteDatabase getWritableDatabase(); + + void changeStoredSamplesType(int timestampFrom, int timestampTo, byte kind, SampleProvider provider); + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java index 5da0e230..c557bbad 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java @@ -4,17 +4,38 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public class HealthSampleProvider implements SampleProvider { + public static final byte TYPE_DEEP_SLEEP = 5; + public static final byte TYPE_LIGHT_SLEEP = 4; + public static final byte TYPE_ACTIVITY = -1; protected final float movementDivisor = 8000f; @Override public int normalizeType(byte rawType) { - return ActivityKind.TYPE_UNKNOWN; + switch (rawType) { + case TYPE_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + case TYPE_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + case TYPE_ACTIVITY: + default: + return ActivityKind.TYPE_UNKNOWN; + } } @Override public byte toRawActivityKind(int activityKind) { - return ActivityKind.TYPE_UNKNOWN; + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return TYPE_ACTIVITY; + case ActivityKind.TYPE_DEEP_SLEEP: + return TYPE_DEEP_SLEEP; + case ActivityKind.TYPE_LIGHT_SLEEP: + return TYPE_LIGHT_SLEEP; + case ActivityKind.TYPE_UNKNOWN: // fall through + default: + return TYPE_ACTIVITY; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealth.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealth.java deleted file mode 100644 index 12d036a8..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealth.java +++ /dev/null @@ -1,96 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; - - -import android.database.sqlite.SQLiteDatabase; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.util.UUID; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.util.GB; - -public class DatalogSessionHealth extends DatalogSession { - - private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealth.class); - - public DatalogSessionHealth(byte id, UUID uuid, int tag, byte item_type, short item_size) { - super(id, uuid, tag, item_type, item_size); - taginfo = "(health)"; - } - - @Override - public boolean handleMessage(ByteBuffer datalogMessage, int length) { - LOG.info(GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); - - int timestamp; - byte unknownC, recordLength, recordNum; - short unknownA; - int beginOfPacketPosition, beginOfSamplesPosition; - - byte steps, orientation; //possibly - short intensity; // possibly - - int initialPosition = datalogMessage.position(); - if (0 == (length % itemSize)) { // one datalog message may contain several packets - for (int packet = 0; packet < (length / itemSize); packet++) { - beginOfPacketPosition = initialPosition + packet * itemSize; - datalogMessage.position(beginOfPacketPosition); - unknownA = datalogMessage.getShort(); - timestamp = datalogMessage.getInt(); - unknownC = datalogMessage.get(); - recordLength = datalogMessage.get(); - recordNum = datalogMessage.get(); - - beginOfSamplesPosition = datalogMessage.position(); - DBHandler dbHandler = null; - try { - dbHandler = GBApplication.acquireDB(); - try (SQLiteDatabase db = dbHandler.getWritableDatabase()) { // explicitly keep the db open while looping over the samples - - ActivitySample[] samples = new ActivitySample[recordNum]; - SampleProvider sampleProvider = new HealthSampleProvider(); - - for (int j = 0; j < recordNum; j++) { - datalogMessage.position(beginOfSamplesPosition + j * recordLength); - steps = datalogMessage.get(); - orientation = datalogMessage.get(); - if (j < (recordNum - 1)) { - //TODO:apparently last minute data do not contain intensity. I guess we are reading it wrong but this approach is our best bet ATM - intensity = datalogMessage.getShort(); - } else { - intensity = 0; - } - samples[j] = new GBActivitySample( - sampleProvider, - timestamp, - intensity, - (short) (steps & 0xff), - (byte) ActivityKind.TYPE_ACTIVITY); - timestamp += 60; - } - - dbHandler.addGBActivitySamples(samples); - } - } catch (Exception ex) { - LOG.debug(ex.getMessage()); - return false;//NACK, so that we get the data again - } finally { - if (dbHandler != null) { - dbHandler.release(); - } - } - - } - } - return true;//ACK by default - } -} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java new file mode 100644 index 00000000..55351024 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java @@ -0,0 +1,91 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + +import android.database.sqlite.SQLiteDatabase; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +class DatalogSessionHealthSleep extends DatalogSession { + + private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSleep.class); + + public DatalogSessionHealthSleep(byte id, UUID uuid, int tag, byte item_type, short item_size) { + super(id, uuid, tag, item_type, item_size); + taginfo = "(health - sleep)"; + } + + @Override + public boolean handleMessage(ByteBuffer datalogMessage, int length) { + LOG.info("DATALOG " + taginfo + GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); + + int initialPosition = datalogMessage.position(); + int beginOfRecordPosition; + short recordVersion; //probably + + if (0 != (length % itemSize)) + return false;//malformed message? + + int recordCount = length / itemSize; + SleepRecord[] sleepRecords = new SleepRecord[recordCount]; + + for (int recordIdx = 0; recordIdx < recordCount; recordIdx++) { + beginOfRecordPosition = initialPosition + recordIdx * itemSize; + datalogMessage.position(beginOfRecordPosition);//we may not consume all the bytes of a record + recordVersion = datalogMessage.getShort(); + if (recordVersion!=1) + return false;//we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + + sleepRecords[recordIdx] = new SleepRecord(datalogMessage.getInt(), + datalogMessage.getInt(), + datalogMessage.getInt(), + datalogMessage.getInt()); + + } + + store(sleepRecords); + return true;//ACK by default + } + + private void store(SleepRecord[] sleepRecords) { + DBHandler dbHandler = null; + SampleProvider sampleProvider = new HealthSampleProvider(); + GB.toast("We don't know how to store deep sleep from the pebble yet.", Toast.LENGTH_LONG, GB.INFO); + try { + dbHandler = GBApplication.acquireDB(); + for (SleepRecord sleepRecord: sleepRecords) { + dbHandler.changeStoredSamplesType(sleepRecord.bedTimeStart,sleepRecord.bedTimeEnd, sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP),sampleProvider); + } + }catch (Exception ex) { + LOG.debug(ex.getMessage()); + } finally { + if (dbHandler != null) { + dbHandler.release(); + } + } + } + + private class SleepRecord { + int offsetUTC; //probably + int bedTimeStart; + int bedTimeEnd; + int deepSleepSeconds; + + public SleepRecord(int offsetUTC, int bedTimeStart, int bedTimeEnd, int deepSleepSeconds) { + this.offsetUTC = offsetUTC; + this.bedTimeStart = bedTimeStart; + this.bedTimeEnd = bedTimeEnd; + this.deepSleepSeconds = deepSleepSeconds; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java new file mode 100644 index 00000000..6ce8bd09 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java @@ -0,0 +1,115 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + + +import android.database.sqlite.SQLiteDatabase; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class DatalogSessionHealthSteps extends DatalogSession { + + private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSteps.class); + + public DatalogSessionHealthSteps(byte id, UUID uuid, int tag, byte item_type, short item_size) { + super(id, uuid, tag, item_type, item_size); + taginfo = "(health - steps)"; + } + + @Override + public boolean handleMessage(ByteBuffer datalogMessage, int length) { + LOG.info("DATALOG " + taginfo + GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); + + int timestamp; + byte recordLength, recordNum; + short recordVersion; //probably + int beginOfPacketPosition, beginOfRecordPosition; + + int initialPosition = datalogMessage.position(); + if (0 != (length % itemSize)) + return false;//malformed message? + + int packetCount = length / itemSize; + + for (int packetIdx = 0; packetIdx < packetCount; packetIdx++) { + beginOfPacketPosition = initialPosition + packetIdx * itemSize; + datalogMessage.position(beginOfPacketPosition);//we may not consume all the records of a packet + + recordVersion = datalogMessage.getShort(); + + if (recordVersion != 5) + return false; //we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + + timestamp = datalogMessage.getInt(); + datalogMessage.get(); //unknown, throw away + recordLength = datalogMessage.get(); + recordNum = datalogMessage.get(); + + beginOfRecordPosition = datalogMessage.position(); + StepsRecord[] stepsRecords = new StepsRecord[recordNum]; + + for (int recordIdx = 0; recordIdx < recordNum; recordIdx++) { + datalogMessage.position(beginOfRecordPosition + recordIdx * recordLength); //we may not consume all the bytes of a record + stepsRecords[recordIdx] = new StepsRecord(timestamp, datalogMessage.get(), datalogMessage.get(), datalogMessage.getShort(), datalogMessage.get(), datalogMessage.get()); + timestamp += 60; + } + + store(stepsRecords); + } + return true;//ACK by default + } + + private void store(StepsRecord[] stepsRecords) { + + DBHandler dbHandler = null; + SampleProvider sampleProvider = new HealthSampleProvider(); + + ActivitySample[] samples = new ActivitySample[stepsRecords.length]; + for (int j = 0; j < stepsRecords.length; j++) { + StepsRecord stepsRecord = stepsRecords[j]; + samples[j] = new GBActivitySample( + sampleProvider, + stepsRecord.timestamp, + stepsRecord.intensity, + (short) (stepsRecord.steps & 0xff), + sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY)); + } + + try { + dbHandler = GBApplication.acquireDB(); + dbHandler.addGBActivitySamples(samples); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } finally { + if (dbHandler != null) { + dbHandler.release(); + } + } + } + + private class StepsRecord { + int timestamp; + byte steps; + byte orientation; + short intensity; + + public StepsRecord(int timestamp, byte steps, byte orientation, short intensity, byte throwAway1, byte throwAway2) { + this.timestamp = timestamp; + this.steps = steps; + this.orientation = orientation; + this.intensity = intensity; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index bb898856..5b6ee85d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -1821,7 +1821,9 @@ public class PebbleProtocol extends GBDeviceProtocol { LOG.info("DATALOG OPENSESSION. id=" + (id & 0xff) + ", App UUID=" + uuid.toString() + ", log_tag=" + log_tag + ", item_type=" + item_type + ", itemSize=" + item_size); if (!mDatalogSessions.containsKey(id)) { if (uuid.equals(UUID_ZERO) && log_tag == 81) { - mDatalogSessions.put(id, new DatalogSessionHealth(id, uuid, log_tag, item_type, item_size)); + mDatalogSessions.put(id, new DatalogSessionHealthSteps(id, uuid, log_tag, item_type, item_size)); + } else if (uuid.equals(UUID_ZERO) && log_tag == 83) { + mDatalogSessions.put(id, new DatalogSessionHealthSleep(id, uuid, log_tag, item_type, item_size)); } else { mDatalogSessions.put(id, new DatalogSession(id, uuid, log_tag, item_type, item_size)); }