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
parent
d62946df63
commit
20c4e49fe1
|
@ -242,4 +242,20 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl
|
||||||
builder.append(')');
|
builder.append(')');
|
||||||
return builder.toString();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,7 @@ public interface DBHandler {
|
||||||
void addGBActivitySamples(ActivitySample[] activitySamples);
|
void addGBActivitySamples(ActivitySample[] activitySamples);
|
||||||
|
|
||||||
SQLiteDatabase getWritableDatabase();
|
SQLiteDatabase getWritableDatabase();
|
||||||
|
|
||||||
|
void changeStoredSamplesType(int timestampFrom, int timestampTo, byte kind, SampleProvider provider);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,38 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
|
|
||||||
public class HealthSampleProvider implements SampleProvider {
|
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;
|
protected final float movementDivisor = 8000f;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int normalizeType(byte rawType) {
|
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
|
@Override
|
||||||
public byte toRawActivityKind(int activityKind) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
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 (!mDatalogSessions.containsKey(id)) {
|
||||||
if (uuid.equals(UUID_ZERO) && log_tag == 81) {
|
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 {
|
} else {
|
||||||
mDatalogSessions.put(id, new DatalogSession(id, uuid, log_tag, item_type, item_size));
|
mDatalogSessions.put(id, new DatalogSession(id, uuid, log_tag, item_type, item_size));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue