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.
here
Daniele Gobbetti 2016-02-09 17:52:21 +01:00
parent d62946df63
commit 20c4e49fe1
7 changed files with 251 additions and 99 deletions

View File

@ -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();
}
}
}

View File

@ -29,4 +29,7 @@ public interface DBHandler {
void addGBActivitySamples(ActivitySample[] activitySamples);
SQLiteDatabase getWritableDatabase();
void changeStoredSamplesType(int timestampFrom, int timestampTo, byte kind, SampleProvider provider);
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}