Improve BLE reconnect and Aris advertising

This commit is contained in:
2026-01-10 01:32:42 +00:00
parent 324b35a464
commit cb6f36924f
4 changed files with 113 additions and 12 deletions

View File

@@ -157,7 +157,7 @@ final class BlePeripheralManager: NSObject, ObservableObject {
return
}
peripheral.startAdvertising([
CBAdvertisementDataLocalNameKey: "GlassNow",
CBAdvertisementDataLocalNameKey: "Aris",
CBAdvertisementDataServiceUUIDsKey: [Self.serviceUUID],
])
}

View File

@@ -14,7 +14,7 @@ struct BleStatusView: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("GlassNow BLE")
Text("Aris BLE")
.font(.title2.bold())
Text("Bluetooth: \(bluetoothStateText)")
.font(.subheadline)

View File

@@ -14,6 +14,7 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@@ -28,8 +29,13 @@ public final class BleCentralClient {
private static final UUID CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private static final long SCAN_TIMEOUT_MS = 10_000L;
private static final long SCAN_TIMEOUT_MS = 30_000L;
private static final long CONNECT_TIMEOUT_MS = 12_000L;
private static final long RECONNECT_DELAY_MS = 1_000L;
private static final long PING_INTERVAL_MS = 15_000L;
private static final int MAX_MISSED_PINGS = 2;
private static final long STALE_TIMEOUT_MS = PING_INTERVAL_MS * MAX_MISSED_PINGS;
private static final long LIVENESS_CHECK_INTERVAL_MS = 5_000L;
private final Context appContext;
private final Callback callback;
@@ -41,10 +47,12 @@ public final class BleCentralClient {
private BluetoothDevice lastDevice = null;
private BluetoothGatt gatt = null;
private boolean subscribed = false;
private int backoffMs = 1000;
private long lastNotificationAtMs = 0L;
private long lastPingAtMs = 0L;
private long lastUnparsedLogAtMs = 0L;
private long lastScanLogAtMs = 0L;
private String lastScanLogAddr = null;
private final Runnable scanTimeoutRunnable = new Runnable() {
@Override
@@ -52,7 +60,7 @@ public final class BleCentralClient {
if (!scanning) return;
Log.i(Constants.TAG_BLE, "Scan timeout; restarting");
stopScanInternal();
scheduleReconnect("Scan timeout");
scheduleScanRestart("Scan timeout");
}
};
@@ -66,6 +74,24 @@ public final class BleCentralClient {
}
};
private final Runnable livenessRunnable = new Runnable() {
@Override
public void run() {
if (gatt == null || !subscribed) return;
long now = System.currentTimeMillis();
long last = lastNotificationAtMs;
long age = last > 0L ? (now - last) : 0L;
if (last > 0L && age > STALE_TIMEOUT_MS) {
long pingAge = lastPingAtMs > 0L ? (now - lastPingAtMs) : age;
Log.w(Constants.TAG_BLE, "Stale BLE (no ping/notify for " + age + "ms; last ping " + pingAge + "ms ago); reconnecting");
closeGattInternal();
scheduleReconnect("Stale (no ping)");
return;
}
handler.postDelayed(this, LIVENESS_CHECK_INTERVAL_MS);
}
};
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
@@ -132,7 +158,6 @@ public final class BleCentralClient {
handler.post(new Runnable() {
@Override
public void run() {
backoffMs = 1000;
startScanInternal("Start");
}
});
@@ -144,7 +169,6 @@ public final class BleCentralClient {
public void run() {
Log.i(Constants.TAG_BLE, "Rescan requested");
closeGattInternal();
backoffMs = 1000;
startScanInternal("Rescan");
}
});
@@ -166,6 +190,7 @@ public final class BleCentralClient {
if (device == null) return;
Advert adv = Advert.parse(scanRecord);
maybeLogScan(device, rssi, scanRecord, adv);
boolean nameMatch = (adv != null && Constants.PERIPHERAL_NAME_HINT.equals(adv.localName))
|| Constants.PERIPHERAL_NAME_HINT.equals(device.getName());
boolean serviceMatch = adv != null && adv.hasService(Constants.SERVICE_UUID);
@@ -183,11 +208,30 @@ public final class BleCentralClient {
connectInternal(device);
}
private void maybeLogScan(BluetoothDevice device, int rssi, byte[] scanRecord, Advert adv) {
long now = System.currentTimeMillis();
String addr = device.getAddress();
boolean addrChanged = addr != null && !addr.equals(lastScanLogAddr);
if (!addrChanged && (now - lastScanLogAtMs) < 1000L) return;
lastScanLogAtMs = now;
lastScanLogAddr = addr;
String advName = adv != null ? adv.localName : null;
String services = adv != null ? formatUuids(adv.services) : "[]";
Log.d(Constants.TAG_BLE,
"Scan result: addr=" + addr
+ " name=" + device.getName()
+ " advName=" + advName
+ " services=" + services
+ " rssi=" + rssi
+ " len=" + (scanRecord != null ? scanRecord.length : 0)
+ " hex=" + hexPrefix(scanRecord, 24));
}
private void scheduleReconnect(String reason) {
if (callback != null) callback.onStatus("Disconnected", reason, true);
int delay = backoffMs;
backoffMs = Math.min(30_000, backoffMs * 2);
long delay = RECONNECT_DELAY_MS;
Log.i(Constants.TAG_BLE, "Reconnect in " + delay + "ms (" + reason + ")");
handler.postDelayed(new Runnable() {
@Override
@@ -197,6 +241,18 @@ public final class BleCentralClient {
}, delay);
}
private void scheduleScanRestart(String reason) {
if (callback != null) callback.onStatus("Scanning…", reason, true);
long delay = RECONNECT_DELAY_MS;
Log.i(Constants.TAG_BLE, "Restart scan in " + delay + "ms (" + reason + ")");
handler.postDelayed(new Runnable() {
@Override
public void run() {
startScanInternal("Scan restart");
}
}, delay);
}
private void startScanInternal(String why) {
if (adapter == null) {
if (callback != null) callback.onStatus("BLE unsupported", "No BluetoothAdapter", true);
@@ -209,6 +265,9 @@ public final class BleCentralClient {
if (scanning) return;
subscribed = false;
stopLivenessCheck();
lastNotificationAtMs = 0L;
lastPingAtMs = 0L;
if (callback != null) callback.onStatus("Scanning…", null, true);
Log.i(Constants.TAG_BLE, "startLeScan (" + why + ")");
// Scan broadly; filter by advertised service UUID and/or local name hint in the callback.
@@ -258,7 +317,6 @@ public final class BleCentralClient {
if (newState == BluetoothProfile.STATE_CONNECTED) {
handler.removeCallbacks(connectTimeoutRunnable);
backoffMs = 1000;
if (callback != null) {
callback.onStatus("Discovering…", null, true);
callback.onConnected();
@@ -325,6 +383,9 @@ public final class BleCentralClient {
Log.i(Constants.TAG_BLE, "onDescriptorWrite CCCD status=" + status);
if (status == BluetoothGatt.GATT_SUCCESS) {
subscribed = true;
lastNotificationAtMs = System.currentTimeMillis();
lastPingAtMs = 0L;
startLivenessCheck();
if (callback != null) callback.onStatus("Connected", null, true);
} else {
closeGattInternal();
@@ -359,6 +420,7 @@ public final class BleCentralClient {
if (result.isPing) {
Log.d(Constants.TAG_FEED, "PING");
lastPingAtMs = lastNotificationAtMs;
if (callback != null) callback.onPing();
return;
}
@@ -373,12 +435,15 @@ public final class BleCentralClient {
private void closeGattInternal() {
subscribed = false;
stopLivenessCheck();
handler.removeCallbacks(connectTimeoutRunnable);
if (gatt != null) {
try {
gatt.disconnect();
} catch (Throwable ignored) {
}
// Hidden API: best-effort cache refresh to avoid stale services on reconnect.
refreshDeviceCache(gatt);
try {
gatt.close();
} catch (Throwable ignored) {
@@ -387,6 +452,15 @@ public final class BleCentralClient {
}
}
private void startLivenessCheck() {
handler.removeCallbacks(livenessRunnable);
handler.postDelayed(livenessRunnable, LIVENESS_CHECK_INTERVAL_MS);
}
private void stopLivenessCheck() {
handler.removeCallbacks(livenessRunnable);
}
@SuppressLint("MissingPermission")
private static boolean startLeScanCompat(UUID[] uuids, BluetoothAdapter.LeScanCallback callback) {
BluetoothAdapter a = BluetoothAdapter.getDefaultAdapter();
@@ -423,6 +497,20 @@ public final class BleCentralClient {
}
}
// Hidden API: clears stale GATT services on some older stacks; best-effort only.
private static void refreshDeviceCache(BluetoothGatt gatt) {
if (gatt == null) return;
try {
Method refresh = gatt.getClass().getMethod("refresh");
if (refresh != null) {
boolean ok = (Boolean) refresh.invoke(gatt);
Log.i(Constants.TAG_BLE, "refreshDeviceCache=" + ok);
}
} catch (Throwable t) {
Log.w(Constants.TAG_BLE, "refreshDeviceCache failed: " + t);
}
}
private static void logLarge(String tag, String s) {
if (s == null) {
Log.d(tag, "null");
@@ -533,4 +621,16 @@ public final class BleCentralClient {
if (b.length > n) sb.append("");
return sb.toString();
}
private static String formatUuids(List<UUID> uuids) {
if (uuids == null || uuids.isEmpty()) return "[]";
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < uuids.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(uuids.get(i));
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -17,7 +17,8 @@ public final class Constants {
public static final UUID FEED_TX_UUID = UUID.fromString(FEED_TX_UUID_STR);
public static final UUID CONTROL_RX_UUID = UUID.fromString(CONTROL_RX_UUID_STR);
public static final String PERIPHERAL_NAME_HINT = "GlassNow";
public static final String PERIPHERAL_NAME_HINT = "Aris";
public static final String LEGACY_PERIPHERAL_NAME_HINT = "GlassNow";
public static final String ACTION_START_HUD = "sh.nym.irisglass.action.START_HUD";
public static final String ACTION_RESCAN = "sh.nym.irisglass.action.RESCAN";