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:
Daniele Gobbetti 2015-06-01 09:42:44 +02:00 committed by cpfeiffer
parent 5d950dc407
commit 8b268a676c
3 changed files with 129 additions and 94 deletions

View File

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

View File

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

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