|
|
|
|
@@ -25,6 +25,7 @@ public final class BleCentralClient {
|
|
|
|
|
void onConnected();
|
|
|
|
|
void onPing();
|
|
|
|
|
void onFeedJson(String json);
|
|
|
|
|
void onWifiRequest(boolean requested);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final UUID CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
|
|
|
|
@@ -46,6 +47,7 @@ public final class BleCentralClient {
|
|
|
|
|
private boolean scanning = false;
|
|
|
|
|
private BluetoothDevice lastDevice = null;
|
|
|
|
|
private BluetoothGatt gatt = null;
|
|
|
|
|
private BluetoothGattDescriptor pendingWifiCccd = null;
|
|
|
|
|
private boolean subscribed = false;
|
|
|
|
|
|
|
|
|
|
private long lastNotificationAtMs = 0L;
|
|
|
|
|
@@ -265,6 +267,7 @@ public final class BleCentralClient {
|
|
|
|
|
if (scanning) return;
|
|
|
|
|
|
|
|
|
|
subscribed = false;
|
|
|
|
|
pendingWifiCccd = null;
|
|
|
|
|
stopLivenessCheck();
|
|
|
|
|
lastNotificationAtMs = 0L;
|
|
|
|
|
lastPingAtMs = 0L;
|
|
|
|
|
@@ -347,6 +350,24 @@ public final class BleCentralClient {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BluetoothGattCharacteristic wifiRequestTx = service.getCharacteristic(Constants.WIFI_REQUEST_TX_UUID);
|
|
|
|
|
if (wifiRequestTx != null) {
|
|
|
|
|
Log.i(Constants.TAG_BLE, "Found characteristic " + Constants.WIFI_REQUEST_TX_UUID_STR + " (WIFI_REQUEST_TX)");
|
|
|
|
|
Log.i(Constants.TAG_BLE, "Enabling notifications for WIFI_REQUEST_TX");
|
|
|
|
|
boolean wifiNotifOk = gatt.setCharacteristicNotification(wifiRequestTx, true);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "setCharacteristicNotification(WIFI_REQUEST_TX)=" + wifiNotifOk);
|
|
|
|
|
|
|
|
|
|
BluetoothGattDescriptor wifiCccd = wifiRequestTx.getDescriptor(CCCD_UUID);
|
|
|
|
|
if (wifiCccd == null) {
|
|
|
|
|
Log.w(Constants.TAG_BLE, "Missing WIFI_REQUEST_TX CCCD (0x2902)");
|
|
|
|
|
} else {
|
|
|
|
|
wifiCccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
|
|
|
|
pendingWifiCccd = wifiCccd;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Log.w(Constants.TAG_BLE, "Missing characteristic " + Constants.WIFI_REQUEST_TX_UUID_STR + " (WIFI_REQUEST_TX)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BluetoothGattCharacteristic feedTx = service.getCharacteristic(Constants.FEED_TX_UUID);
|
|
|
|
|
if (feedTx == null) {
|
|
|
|
|
closeGattInternal();
|
|
|
|
|
@@ -358,7 +379,7 @@ public final class BleCentralClient {
|
|
|
|
|
Log.i(Constants.TAG_BLE, "Enabling notifications for FEED_TX");
|
|
|
|
|
|
|
|
|
|
boolean notifOk = gatt.setCharacteristicNotification(feedTx, true);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "setCharacteristicNotification=" + notifOk);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "setCharacteristicNotification(FEED_TX)=" + notifOk);
|
|
|
|
|
|
|
|
|
|
BluetoothGattDescriptor cccd = feedTx.getDescriptor(CCCD_UUID);
|
|
|
|
|
if (cccd == null) {
|
|
|
|
|
@@ -368,7 +389,7 @@ public final class BleCentralClient {
|
|
|
|
|
}
|
|
|
|
|
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
|
|
|
|
boolean writeOk = gatt.writeDescriptor(cccd);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "writeDescriptor(CCCD)=" + writeOk);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "writeDescriptor(FEED CCCD)=" + writeOk);
|
|
|
|
|
if (!writeOk) {
|
|
|
|
|
closeGattInternal();
|
|
|
|
|
scheduleReconnect("CCCD write failed to start");
|
|
|
|
|
@@ -380,61 +401,93 @@ public final class BleCentralClient {
|
|
|
|
|
if (descriptor == null) return;
|
|
|
|
|
if (!CCCD_UUID.equals(descriptor.getUuid())) return;
|
|
|
|
|
|
|
|
|
|
Log.i(Constants.TAG_BLE, "onDescriptorWrite CCCD status=" + status);
|
|
|
|
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
|
|
|
BluetoothGattCharacteristic ch = descriptor.getCharacteristic();
|
|
|
|
|
UUID chUuid = ch != null ? ch.getUuid() : null;
|
|
|
|
|
Log.i(Constants.TAG_BLE, "onDescriptorWrite CCCD status=" + status + " char=" + chUuid);
|
|
|
|
|
|
|
|
|
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
|
|
|
|
if (Constants.FEED_TX_UUID.equals(chUuid)) {
|
|
|
|
|
closeGattInternal();
|
|
|
|
|
scheduleReconnect("CCCD write failed: " + status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (Constants.WIFI_REQUEST_TX_UUID.equals(chUuid)) {
|
|
|
|
|
Log.w(Constants.TAG_BLE, "WIFI_REQUEST_TX CCCD write failed: " + status);
|
|
|
|
|
pendingWifiCccd = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
closeGattInternal();
|
|
|
|
|
scheduleReconnect("CCCD write failed: " + status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Constants.FEED_TX_UUID.equals(chUuid)) {
|
|
|
|
|
subscribed = true;
|
|
|
|
|
lastNotificationAtMs = System.currentTimeMillis();
|
|
|
|
|
lastPingAtMs = 0L;
|
|
|
|
|
startLivenessCheck();
|
|
|
|
|
if (callback != null) callback.onStatus("Connected", null, true);
|
|
|
|
|
} else {
|
|
|
|
|
closeGattInternal();
|
|
|
|
|
scheduleReconnect("CCCD write failed: " + status);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void handleCharacteristicChanged(UUID uuid, byte[] value) {
|
|
|
|
|
if (!Constants.FEED_TX_UUID.equals(uuid)) return;
|
|
|
|
|
lastNotificationAtMs = System.currentTimeMillis();
|
|
|
|
|
|
|
|
|
|
FeedReassembler.Result result = reassembler.onNotification(value, lastNotificationAtMs);
|
|
|
|
|
if (result == null) {
|
|
|
|
|
// Diagnostic: log occasionally so we can infer the framing coming from iPhone.
|
|
|
|
|
if (lastNotificationAtMs - lastUnparsedLogAtMs > 1000L) {
|
|
|
|
|
lastUnparsedLogAtMs = lastNotificationAtMs;
|
|
|
|
|
String hint = "";
|
|
|
|
|
if (value != null && value.length >= 9) {
|
|
|
|
|
long msgId = ((long) (value[0] & 0xFF))
|
|
|
|
|
| ((long) (value[1] & 0xFF) << 8)
|
|
|
|
|
| ((long) (value[2] & 0xFF) << 16)
|
|
|
|
|
| ((long) (value[3] & 0xFF) << 24);
|
|
|
|
|
int type = value[4] & 0xFF;
|
|
|
|
|
int idx = (value[5] & 0xFF) | ((value[6] & 0xFF) << 8);
|
|
|
|
|
int cnt = (value[7] & 0xFF) | ((value[8] & 0xFF) << 8);
|
|
|
|
|
hint = " msgId=" + (msgId & 0xFFFFFFFFL) + " type=" + type + " chunk=" + idx + "/" + cnt;
|
|
|
|
|
if (pendingWifiCccd != null) {
|
|
|
|
|
boolean wifiWriteOk = gatt.writeDescriptor(pendingWifiCccd);
|
|
|
|
|
Log.i(Constants.TAG_BLE, "writeDescriptor(WIFI CCCD)=" + wifiWriteOk);
|
|
|
|
|
if (!wifiWriteOk) {
|
|
|
|
|
Log.w(Constants.TAG_BLE, "WIFI_REQUEST_TX CCCD write failed to start");
|
|
|
|
|
pendingWifiCccd = null;
|
|
|
|
|
}
|
|
|
|
|
Log.d(Constants.TAG_FEED, "Unparsed notify len=" + (value != null ? value.length : 0) + hint + " hex=" + hexPrefix(value, 16));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.isPing) {
|
|
|
|
|
Log.d(Constants.TAG_FEED, "PING");
|
|
|
|
|
lastPingAtMs = lastNotificationAtMs;
|
|
|
|
|
if (callback != null) callback.onPing();
|
|
|
|
|
return;
|
|
|
|
|
if (Constants.WIFI_REQUEST_TX_UUID.equals(chUuid)) {
|
|
|
|
|
Log.i(Constants.TAG_BLE, "Subscribed to WIFI_REQUEST_TX");
|
|
|
|
|
pendingWifiCccd = null;
|
|
|
|
|
}
|
|
|
|
|
if (result.jsonOrNull != null) {
|
|
|
|
|
Log.i(Constants.TAG_FEED, "Reassembled JSON (" + result.jsonOrNull.length() + " bytes)");
|
|
|
|
|
Log.i(Constants.TAG_FEED, "RAW_JSON_BEGIN");
|
|
|
|
|
logLarge(Constants.TAG_FEED, result.jsonOrNull);
|
|
|
|
|
Log.i(Constants.TAG_FEED, "RAW_JSON_END");
|
|
|
|
|
if (callback != null) callback.onFeedJson(result.jsonOrNull);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void handleCharacteristicChanged(UUID uuid, byte[] value) {
|
|
|
|
|
if (Constants.WIFI_REQUEST_TX_UUID.equals(uuid)) {
|
|
|
|
|
boolean requested = value != null && value.length > 0 && (value[0] & 0xFF) != 0;
|
|
|
|
|
Log.i(Constants.TAG_BLE, "WIFI_REQUEST_TX notify=" + requested + " raw=" + hexPrefix(value, 1));
|
|
|
|
|
if (callback != null) callback.onWifiRequest(requested);
|
|
|
|
|
} else if (Constants.FEED_TX_UUID.equals(uuid)) {
|
|
|
|
|
lastNotificationAtMs = System.currentTimeMillis();
|
|
|
|
|
|
|
|
|
|
FeedReassembler.Result result = reassembler.onNotification(value, lastNotificationAtMs);
|
|
|
|
|
if (result == null) {
|
|
|
|
|
// Diagnostic: log occasionally so we can infer the framing coming from iPhone.
|
|
|
|
|
if (lastNotificationAtMs - lastUnparsedLogAtMs > 1000L) {
|
|
|
|
|
lastUnparsedLogAtMs = lastNotificationAtMs;
|
|
|
|
|
String hint = "";
|
|
|
|
|
if (value != null && value.length >= 9) {
|
|
|
|
|
long msgId = ((long) (value[0] & 0xFF))
|
|
|
|
|
| ((long) (value[1] & 0xFF) << 8)
|
|
|
|
|
| ((long) (value[2] & 0xFF) << 16)
|
|
|
|
|
| ((long) (value[3] & 0xFF) << 24);
|
|
|
|
|
int type = value[4] & 0xFF;
|
|
|
|
|
int idx = (value[5] & 0xFF) | ((value[6] & 0xFF) << 8);
|
|
|
|
|
int cnt = (value[7] & 0xFF) | ((value[8] & 0xFF) << 8);
|
|
|
|
|
hint = " msgId=" + (msgId & 0xFFFFFFFFL) + " type=" + type + " chunk=" + idx + "/" + cnt;
|
|
|
|
|
}
|
|
|
|
|
Log.d(Constants.TAG_FEED, "Unparsed notify len=" + (value != null ? value.length : 0) + hint + " hex=" + hexPrefix(value, 16));
|
|
|
|
|
}
|
|
|
|
|
} else if (result.isPing) {
|
|
|
|
|
Log.d(Constants.TAG_FEED, "PING");
|
|
|
|
|
lastPingAtMs = lastNotificationAtMs;
|
|
|
|
|
if (callback != null) callback.onPing();
|
|
|
|
|
} else if (result.jsonOrNull != null) {
|
|
|
|
|
Log.i(Constants.TAG_FEED, "Reassembled JSON (" + result.jsonOrNull.length() + " bytes)");
|
|
|
|
|
Log.i(Constants.TAG_FEED, "RAW_JSON_BEGIN");
|
|
|
|
|
logLarge(Constants.TAG_FEED, result.jsonOrNull);
|
|
|
|
|
Log.i(Constants.TAG_FEED, "RAW_JSON_END");
|
|
|
|
|
if (callback != null) callback.onFeedJson(result.jsonOrNull);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void closeGattInternal() {
|
|
|
|
|
subscribed = false;
|
|
|
|
|
pendingWifiCccd = null;
|
|
|
|
|
stopLivenessCheck();
|
|
|
|
|
handler.removeCallbacks(connectTimeoutRunnable);
|
|
|
|
|
if (gatt != null) {
|
|
|
|
|
|