-
Notifications
You must be signed in to change notification settings - Fork 60
/
BTCommandManager.java
391 lines (330 loc) · 14.9 KB
/
BTCommandManager.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package com.betomaluje.miband.bluetooth;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.betomaluje.miband.ActionCallback;
import com.betomaluje.miband.NotifyListener;
import com.betomaluje.miband.model.Profile;
import com.betomaluje.miband.model.Protocol;
import com.betomaluje.miband.models.ActivityData;
import com.betomaluje.miband.sqlite.ActivitySQLite;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
public class BTCommandManager {
private static final String TAG = BTCommandManager.class.getSimpleName();
private ActionCallback currentCallback;
private QueueConsumer mQueueConsumer;
public HashMap<UUID, NotifyListener> notifyListeners = new HashMap<UUID, NotifyListener>();
private Context context;
private BluetoothGatt gatt;
public BTCommandManager(Context context, BluetoothGatt gatt) {
this.context = context;
this.gatt = gatt;
mQueueConsumer = new QueueConsumer(context, this);
Thread t = new Thread(mQueueConsumer);
t.start();
}
public void queueTask(final BLETask task) {
mQueueConsumer.add(task);
}
public QueueConsumer getmQueueConsumer() {
return mQueueConsumer;
}
public void writeAndRead(final UUID uuid, byte[] valueToWrite, final ActionCallback callback) {
ActionCallback readCallback = new ActionCallback() {
@Override
public void onSuccess(Object characteristic) {
BTCommandManager.this.readCharacteristic(uuid, callback);
}
@Override
public void onFail(int errorCode, String msg) {
callback.onFail(errorCode, msg);
}
};
this.writeCharacteristic(uuid, valueToWrite, readCallback);
}
/**
* Sends a command to the Mi Band
*
* @param uuid the {@link Profile} used
* @param value the values to send
* @param callback
*/
public void writeCharacteristic(UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
this.onFail(-1, "BluetoothGattCharacteristic " + uuid + " doesn't exist");
return;
}
chara.setValue(value);
if (!this.gatt.writeCharacteristic(chara)) {
this.onFail(-1, "gatt.writeCharacteristic() return false");
} else {
onSuccess(chara);
}
} catch (Throwable tr) {
Log.e(TAG, "writeCharacteristic", tr);
this.onFail(-1, tr.getMessage());
}
}
public boolean writeCharacteristicWithResponse(UUID service, UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(service).getCharacteristic(uuid);
if (null == chara) {
return false;
}
chara.setValue(value);
return this.gatt.writeCharacteristic(chara);
} catch (Throwable tr) {
return false;
}
}
public boolean writeCharacteristicWithResponse(UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
return false;
}
chara.setValue(value);
return this.gatt.writeCharacteristic(chara);
} catch (Throwable tr) {
return false;
}
}
/**
* Reads a command from the Mi Band
*
* @param uuid the {@link Profile} used
* @param callback
*/
public void readCharacteristic(UUID uuid, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
this.onFail(-1, "BluetoothGattCharacteristic " + uuid + " doesn't exist");
return;
}
if (!this.gatt.readCharacteristic(chara)) {
this.onFail(-1, "gatt.readCharacteristic() return false");
}
} catch (Throwable tr) {
Log.e(TAG, "readCharacteristic", tr);
this.onFail(-1, tr.getMessage());
}
}
public boolean readCharacteristicWithResponse(UUID uuid, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
return false;
}
return this.gatt.readCharacteristic(chara);
} catch (Throwable tr) {
Log.e(TAG, "readCharacteristic", tr);
return false;
}
}
/**
* Reads the bluetooth's received signal strength indication
*
* @param callback
*/
public void readRssi(ActionCallback callback) {
try {
this.currentCallback = callback;
this.gatt.readRemoteRssi();
} catch (Throwable tr) {
Log.e(TAG, "readRssi", tr);
this.onFail(-1, tr.getMessage());
}
}
public void setNotifyListener(UUID characteristicId, NotifyListener listener) {
if (this.notifyListeners.containsKey(characteristicId))
return;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(characteristicId);
if (chara == null)
return;
this.gatt.setCharacteristicNotification(chara, true);
BluetoothGattDescriptor descriptor = chara.getDescriptor(Profile.UUID_DESCRIPTOR_UPDATE_NOTIFICATION);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
this.gatt.writeDescriptor(descriptor);
this.notifyListeners.put(characteristicId, listener);
}
public void setHighLatency() {
writeCharacteristic(Profile.UUID_CHAR_LE_PARAMS, Protocol.HIGH_LATENCY_LEPARAMS, null);
}
public void onSuccess(Object data) {
if (this.currentCallback != null) {
ActionCallback callback = this.currentCallback;
this.currentCallback = null;
callback.onSuccess(data);
}
}
public void onFail(int errorCode, String msg) {
if (this.currentCallback != null) {
ActionCallback callback = this.currentCallback;
this.currentCallback = null;
callback.onFail(errorCode, msg);
}
}
public void handleControlPointResult(byte[] value) {
if (value != null) {
for (byte b : value) {
Log.i(TAG, "handleControlPoint GOT DATA:" + String.format("0x%8x", b));
}
} else {
Log.e(TAG, "handleControlPoint GOT null");
}
}
//ACTIVITY DATA
//temporary buffer, size is a multiple of 60 because we want to store complete minutes (1 minute = 3 bytes)
private static final int activityDataHolderSize = 3 * 60 * 4; // 8h
private static class ActivityStruct {
public byte[] activityDataHolder = new byte[activityDataHolderSize];
//index of the buffer above
public int activityDataHolderProgress = 0;
//number of bytes we will get in a single data transfer, used as counter
public int activityDataRemainingBytes = 0;
//same as above, but remains untouched for the ack message
public int activityDataUntilNextHeader = 0;
//timestamp of the single data transfer, incremented to store each minute's data
public GregorianCalendar activityDataTimestampProgress = null;
//same as above, but remains untouched for the ack message
public GregorianCalendar activityDataTimestampToAck = null;
}
private ActivityStruct activityStruct;
public void handleActivityNotif(byte[] value) {
boolean firstChunk = activityStruct == null;
if (firstChunk) {
activityStruct = new ActivityStruct();
}
if (value.length == 11) {
// byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
int dataType = value[0];
// byte 1 to 6 represent a timestamp
GregorianCalendar timestamp = parseTimestamp(value, 1);
// counter of all data held by the band
int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
totalDataToRead *= (dataType == 1) ? 3 : 1;
// counter of this data block
int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
dataUntilNextHeader *= (dataType == 1) ? 3 : 1;
// there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1),
// these chunks are usually 20 bytes long and grouped in blocks
// after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
// as we just did
if (firstChunk && dataUntilNextHeader != 0) {
String message = String.format("About to transfer %1$s of data starting from %2$s",
(totalDataToRead / 3),
DateFormat.getDateTimeInstance().format(timestamp.getTime()));
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
Log.i(TAG, "total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)");
Log.i(TAG, "data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / 3) + " minute(s)");
Log.i(TAG, "TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()) + " magic byte: " + dataUntilNextHeader);
activityStruct.activityDataRemainingBytes = activityStruct.activityDataUntilNextHeader = dataUntilNextHeader;
activityStruct.activityDataTimestampToAck = (GregorianCalendar) timestamp.clone();
activityStruct.activityDataTimestampProgress = timestamp;
} else {
bufferActivityData(value);
}
Log.e(TAG, "activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
if (activityStruct.activityDataRemainingBytes == 0) {
sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
}
}
private GregorianCalendar parseTimestamp(byte[] value, int offset) {
GregorianCalendar timestamp = new GregorianCalendar(
value[offset] + 2000,
value[offset + 1],
value[offset + 2],
value[offset + 3],
value[offset + 4],
value[offset + 5]);
return timestamp;
}
private void bufferActivityData(byte[] value) {
if (activityStruct.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 == activityStruct.activityDataRemainingBytes) {
System.arraycopy(value, 0, activityStruct.activityDataHolder, activityStruct.activityDataHolderProgress, value.length);
activityStruct.activityDataHolderProgress += value.length;
activityStruct.activityDataRemainingBytes -= value.length;
if (this.activityDataHolderSize == activityStruct.activityDataHolderProgress) {
flushActivityDataHolder();
}
} else {
// the length of the chunk is not what we expect. We need to make sense of this data
Log.w(TAG, "GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: " + value.length + ", EXPECTED LENGTH: " + activityStruct.activityDataRemainingBytes);
for (byte b : value) {
Log.w(TAG, "DATA: " + String.format("0x%8x", b));
}
}
} else {
Log.e(TAG, "error buffering activity data: remaining bytes: " + activityStruct.activityDataRemainingBytes + ", received: " + value.length);
}
}
private void flushActivityDataHolder() {
if (activityStruct == null) {
Log.d(TAG, "nothing to flush, struct is already null");
return;
}
byte category, intensity, steps;
ActivitySQLite dbHandler = ActivitySQLite.getInstance(context);
for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) { //TODO: check if multiple of 3, if not something is wrong
category = activityStruct.activityDataHolder[i];
intensity = activityStruct.activityDataHolder[i + 1];
steps = activityStruct.activityDataHolder[i + 2];
dbHandler.saveActivity((int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000),
ActivityData.PROVIDER_MIBAND,
intensity,
steps,
category);
activityStruct.activityDataTimestampProgress.add(Calendar.MINUTE, 1);
}
activityStruct.activityDataHolderProgress = 0;
}
private void sendAckDataTransfer(Calendar time, int bytesTransferred) {
byte[] ack = new byte[]{
Protocol.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
(byte) (time.get(Calendar.YEAR) - 2000),
(byte) time.get(Calendar.MONTH),
(byte) time.get(Calendar.DATE),
(byte) time.get(Calendar.HOUR_OF_DAY),
(byte) time.get(Calendar.MINUTE),
(byte) time.get(Calendar.SECOND),
(byte) (bytesTransferred & 0xff),
(byte) (0xff & (bytesTransferred >> 8))
};
final List<BLEAction> list = new ArrayList<>();
list.add(new WriteAction(Profile.UUID_CHAR_CONTROL_POINT, ack));
BLETask task = new BLETask(list);
try {
queueTask(task);
} catch (NullPointerException e) {
} finally {
// flush to the DB after sending the ACK
flushActivityDataHolder();
//The last data chunk sent by the miband has always length 0.
//When we ack this chunk, the transfer is done.
if (bytesTransferred == 0) {
activityStruct = null;
onSuccess("sync complete");
}
}
}
}