Refactoring of the data ingestion method.
* no more ByteBuffer, but a fixed size byte array that gets flushed everytime it's needed * log of activity data to a separate file (no DB integration yet) * the size of the buffer must be a multiple of 3 (1 minute of data = 3 bytes) and 20 (the normal size of the chunks we get from the device) * better logging and more comments in code
This commit is contained in:
parent
5d950dc407
commit
8b268a676c
|
@ -28,8 +28,32 @@
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<appender name="ACTIVITY-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${GB_LOGFILES_DIR}/activity.log</file>
|
||||||
|
|
||||||
|
<lazy>true</lazy>
|
||||||
|
<!-- encoders are by default assigned the type
|
||||||
|
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.log.zip</fileNamePattern>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||||
|
<maxFileSize>5MB</maxFileSize>
|
||||||
|
</triggeringPolicy>
|
||||||
|
<encoder>
|
||||||
|
<!--<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>-->
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} %msg%n</pattern>
|
||||||
|
<!--<pattern>%date [%thread] %-5level %logger{25} - %msg%n</pattern>-->
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="activity" level="DEBUG" additivity="false">
|
||||||
|
<appender-ref ref="ACTIVITY-FILE" />
|
||||||
|
</logger>
|
||||||
|
|
||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
<appender-ref ref="FILE" />
|
<appender-ref ref="FILE" />
|
||||||
</root>
|
</root>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -39,15 +39,13 @@ public class GBApplication extends Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupLogging() {
|
private void setupLogging() {
|
||||||
if (isFileLoggingEnabled()) {
|
File dir = getExternalFilesDir(null);
|
||||||
File dir = getExternalFilesDir(null);
|
if (dir != null && !dir.exists()) {
|
||||||
if (dir != null && !dir.exists()) {
|
dir.mkdirs();
|
||||||
dir.mkdirs();
|
}
|
||||||
}
|
// used by assets/logback.xml since the location cannot be statically determined
|
||||||
// used by assets/logback.xml since the location cannot be statically determined
|
System.setProperty("GB_LOGFILES_DIR", dir.getAbsolutePath());
|
||||||
System.setProperty("GB_LOGFILES_DIR", dir.getAbsolutePath());
|
if (!isFileLoggingEnabled()) {
|
||||||
} else {
|
|
||||||
System.setProperty("GB_LOGFILES_DIR", "/dev/null"); // just to please logback configuration, not used at all
|
|
||||||
try {
|
try {
|
||||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||||
root.detachAppender("FILE");
|
root.detachAppender("FILE");
|
||||||
|
|
|
@ -2,7 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.miband;
|
||||||
|
|
||||||
import android.bluetooth.BluetoothGatt;
|
import android.bluetooth.BluetoothGatt;
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
import android.bluetooth.BluetoothGattDescriptor;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -16,7 +15,6 @@ import java.util.GregorianCalendar;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBCommand;
|
import nodomain.freeyourgadget.gadgetbridge.GBCommand;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBDevice.State;
|
import nodomain.freeyourgadget.gadgetbridge.GBDevice.State;
|
||||||
|
@ -45,8 +43,21 @@ import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.getNotific
|
||||||
public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class);
|
||||||
|
private static final Logger ACTIVITYLOG = LoggerFactory.getLogger("activity");
|
||||||
|
|
||||||
private ByteBuffer activityDataHolder = null;
|
//temporary buffer, size is 60 because we want to store complete minutes (1 minute = 3 bytes)
|
||||||
|
private static final int activityDataHolderSize = 60;
|
||||||
|
private byte[] activityDataHolder = new byte[activityDataHolderSize];
|
||||||
|
//index of the buffer above
|
||||||
|
private int activityDataHolderProgress = 0;
|
||||||
|
//number of bytes we will get in a single data transfer, used as counter
|
||||||
|
private int activityDataRemainingBytes = 0;
|
||||||
|
//same as above, but remains untouched for the ack message
|
||||||
|
private int activityDataUntilNextHeader = 0;
|
||||||
|
//timestamp of the single data transfer, incremented to store each minute's data
|
||||||
|
private GregorianCalendar activityDataTimestampProgress = null;
|
||||||
|
//same as above, but remains untouched for the ack message
|
||||||
|
private GregorianCalendar activityDataTimestampToAck = null;
|
||||||
|
|
||||||
|
|
||||||
public MiBandSupport() {
|
public MiBandSupport() {
|
||||||
|
@ -284,7 +295,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
* @param builder
|
* @param builder
|
||||||
*/
|
*/
|
||||||
private MiBandSupport setCurrentTime(TransactionBuilder builder) {
|
private MiBandSupport setCurrentTime(TransactionBuilder builder) {
|
||||||
Calendar now = Calendar.getInstance();
|
Calendar now = GregorianCalendar.getInstance();
|
||||||
byte[] time = new byte[]{
|
byte[] time = new byte[]{
|
||||||
(byte) (now.get(Calendar.YEAR) - 2000),
|
(byte) (now.get(Calendar.YEAR) - 2000),
|
||||||
(byte) now.get(Calendar.MONTH),
|
(byte) now.get(Calendar.MONTH),
|
||||||
|
@ -419,94 +430,97 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleActivityNotif(byte[] value) {
|
private void handleActivityNotif(byte[] value) {
|
||||||
LOG.info("NOTIF GOT " + value.length + " BYTES.");
|
LOG.info("handleActivityNotif GOT " + value.length + " BYTES.");
|
||||||
if (this.activityDataHolder == null && value.length == 11 ) {
|
if (value.length == 11 ) {
|
||||||
//I know what to do:
|
// byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
|
||||||
// byte 0 is the data type
|
int dataType = value[0];
|
||||||
int dataType = value[0];
|
// byte 1 to 6 represent a timestamp
|
||||||
// byte 1 to 6 represent a timestamp
|
GregorianCalendar timestamp = new GregorianCalendar(value[1]+2000,
|
||||||
GregorianCalendar timestamp = new GregorianCalendar(value[1]+2000,
|
value[2],
|
||||||
value[2],
|
value[3],
|
||||||
value[3],
|
value[4],
|
||||||
value[4],
|
value[5],
|
||||||
value[5],
|
value[6]);
|
||||||
value[6]);
|
|
||||||
// no idea about the following two bytes
|
|
||||||
int i = value[7] & 0xff;
|
|
||||||
int j = value[8] & 0xff;
|
|
||||||
// but combined they tell us how to proceed:
|
|
||||||
int k = i | (j << 8);
|
|
||||||
if (dataType == 1) {
|
|
||||||
k *= 3;
|
|
||||||
}
|
|
||||||
int totalDataToRead = k;
|
|
||||||
|
|
||||||
// no idea about the following two bytes
|
// counter of all data held by the band
|
||||||
int i1 = value[9] & 0xff ;
|
int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
|
||||||
int j1 = value[10] & 0xff;
|
totalDataToRead *= (dataType == 1) ? 3 : 1;
|
||||||
|
|
||||||
// but combined they tell us how to proceed:
|
|
||||||
int k1 = i1 | (j1 << 8);
|
|
||||||
if (dataType == 1) {
|
|
||||||
k1 *= 3;
|
|
||||||
}
|
|
||||||
int dataUntilNextHeader = k1;
|
|
||||||
|
|
||||||
// there is a total of totalDataToRead that will come in (3 bytes per minute),
|
// counter of this data block
|
||||||
// however, after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
|
int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
|
||||||
// as we just did
|
dataUntilNextHeader *= (dataType ==1) ? 3 : 1;
|
||||||
|
|
||||||
LOG.info("total data to read: "+ totalDataToRead +" len: " + (totalDataToRead / 3) + " minute(s)");
|
|
||||||
LOG.info("data to read until next header: "+ dataUntilNextHeader +" len: " + (dataUntilNextHeader / 3) + " minute(s)");
|
|
||||||
LOG.info("RAW DATA: i="+ i +" j="+ j +" k="+ k +" i1="+ i1 +" j1="+ j1 +" k1="+ k1 +"");
|
|
||||||
LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() + " magic byte: " + dataUntilNextHeader);
|
|
||||||
if (createActivityDataHolder(dataUntilNextHeader)) {
|
|
||||||
sendAckDataTransfer(timestamp, dataUntilNextHeader);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (byte b: value){
|
|
||||||
this.activityDataHolder.put(b);
|
|
||||||
}
|
|
||||||
LOG.info("Buffer remaining bytes: " + this.activityDataHolder.remaining());
|
|
||||||
if (this.activityDataHolder.remaining() == 0) {
|
|
||||||
consumeActivityDataHolder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean createActivityDataHolder(int size) {
|
// there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1),
|
||||||
if(this.activityDataHolder == null) {
|
// these chunks are usually 20 bytes long and grouped in blocks
|
||||||
this.activityDataHolder = ByteBuffer.allocate(size);
|
// after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
|
||||||
return true;
|
// as we just did
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void consumeActivityDataHolder() {
|
LOG.info("total data to read: "+ totalDataToRead +" len: " + (totalDataToRead / 3) + " minute(s)");
|
||||||
int currentSize = this.activityDataHolder.capacity();
|
LOG.info("data to read until next header: "+ dataUntilNextHeader +" len: " + (dataUntilNextHeader / 3) + " minute(s)");
|
||||||
this.activityDataHolder.rewind();
|
LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() + " magic byte: " + dataUntilNextHeader);
|
||||||
/*
|
|
||||||
intensity = byte1;
|
this.activityDataRemainingBytes = this.activityDataUntilNextHeader = dataUntilNextHeader;
|
||||||
steps = byte2;
|
this.activityDataTimestampProgress = this.activityDataTimestampToAck = timestamp;
|
||||||
category = byte0;
|
|
||||||
*/
|
} else {
|
||||||
for ( int i = 0 ; i < currentSize; i+=3 ) {
|
bufferActivityData(value);
|
||||||
LOG.info("index: "+i/3+" category:"+this.activityDataHolder.get()+" intensity:"+this.activityDataHolder.get()+" steps:"+this.activityDataHolder.get());
|
|
||||||
}
|
}
|
||||||
this.activityDataHolder = null;
|
if (this.activityDataRemainingBytes == 0) {
|
||||||
|
sendAckDataTransfer(this.activityDataTimestampToAck, this.activityDataUntilNextHeader);
|
||||||
|
flushActivityDataHolder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bufferActivityData(byte[] value) {
|
||||||
|
|
||||||
|
if (this.activityDataRemainingBytes >= value.length) {
|
||||||
|
//I don't like this clause, but until we figure out why we get different data sometimes this should work
|
||||||
|
if (value.length == 20 || value.length == this.activityDataRemainingBytes) {
|
||||||
|
System.arraycopy(value, 0, this.activityDataHolder, this.activityDataHolderProgress, value.length);
|
||||||
|
this.activityDataHolderProgress +=value.length;
|
||||||
|
this.activityDataRemainingBytes -= value.length;
|
||||||
|
|
||||||
|
if (this.activityDataHolderSize == this.activityDataHolderProgress) {
|
||||||
|
flushActivityDataHolder();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the lenght of the chunk is not what we expect. We need to make sense of this data
|
||||||
|
LOG.warn("GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: " + value.length + ", EXPECTED LENGTH: " + this.activityDataRemainingBytes);
|
||||||
|
for (byte b: value){
|
||||||
|
LOG.warn("DATA: " + String.format("0x%8x", b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushActivityDataHolder() {
|
||||||
|
GregorianCalendar timestamp = this.activityDataTimestampProgress;
|
||||||
|
for (int i=0; i<this.activityDataHolder.length; i+=3) {
|
||||||
|
ACTIVITYLOG.info(
|
||||||
|
" timestamp:"+DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() +
|
||||||
|
" category:"+ this.activityDataHolder[i]+
|
||||||
|
" intensity:"+this.activityDataHolder[i+1]+
|
||||||
|
" steps:"+this.activityDataHolder[i+2]
|
||||||
|
);
|
||||||
|
timestamp.add(Calendar.MINUTE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activityDataHolderProgress = 0;
|
||||||
|
this.activityDataTimestampProgress = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleControlPointResult(byte[] value, int status) {
|
private void handleControlPointResult(byte[] value, int status) {
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
for (byte b: value){
|
LOG.warn("Could not write to the control point.");
|
||||||
LOG.info("3GOT DATA:" + String.format("0x%20x", b));
|
}
|
||||||
}
|
LOG.info("handleControlPoint got status:" + status);
|
||||||
} else {
|
for (byte b: value){
|
||||||
LOG.info("BOOOOOOOOO");
|
LOG.info("handleControlPoint GOT DATA:" + String.format("0x%8x", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
private void sendAckDataTransfer(Calendar time, int unknown_code) {
|
private void sendAckDataTransfer(Calendar time, int bytesTransferred) {
|
||||||
byte[] ack = new byte[]{
|
byte[] ack = new byte[]{
|
||||||
MiBandService.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
|
MiBandService.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
|
||||||
(byte) (time.get(Calendar.YEAR) - 2000),
|
(byte) (time.get(Calendar.YEAR) - 2000),
|
||||||
|
@ -515,10 +529,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
(byte) time.get(Calendar.HOUR_OF_DAY),
|
(byte) time.get(Calendar.HOUR_OF_DAY),
|
||||||
(byte) time.get(Calendar.MINUTE),
|
(byte) time.get(Calendar.MINUTE),
|
||||||
(byte) time.get(Calendar.SECOND),
|
(byte) time.get(Calendar.SECOND),
|
||||||
(byte) (unknown_code & 0xff),
|
(byte) (bytesTransferred & 0xff),
|
||||||
(byte) (0xff & (unknown_code >> 8))
|
(byte) (0xff & (bytesTransferred >> 8))
|
||||||
};
|
};
|
||||||
LOG.info("WRITING: " + DateFormat.getDateTimeInstance().format(time.getTime()).toString());
|
|
||||||
try {
|
try {
|
||||||
TransactionBuilder builder = performInitialized("send acknowledge");
|
TransactionBuilder builder = performInitialized("send acknowledge");
|
||||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), ack);
|
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), ack);
|
||||||
|
@ -577,4 +590,4 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||||
protected TransactionBuilder createTransactionBuilder(String taskName) {
|
protected TransactionBuilder createTransactionBuilder(String taskName) {
|
||||||
return new MiBandTransactionBuilder(taskName);
|
return new MiBandTransactionBuilder(taskName);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue