diff --git a/IrisCompanion/iris/Bluetooth/BlePeripheralManager.swift b/IrisCompanion/iris/Bluetooth/BlePeripheralManager.swift index 5f4186e..3692605 100644 --- a/IrisCompanion/iris/Bluetooth/BlePeripheralManager.swift +++ b/IrisCompanion/iris/Bluetooth/BlePeripheralManager.swift @@ -157,7 +157,7 @@ final class BlePeripheralManager: NSObject, ObservableObject { return } peripheral.startAdvertising([ - CBAdvertisementDataLocalNameKey: "GlassNow", + CBAdvertisementDataLocalNameKey: "Aris", CBAdvertisementDataServiceUUIDsKey: [Self.serviceUUID], ]) } diff --git a/IrisCompanion/iris/Views/BleStatusView.swift b/IrisCompanion/iris/Views/BleStatusView.swift index e909ddf..18f75fc 100644 --- a/IrisCompanion/iris/Views/BleStatusView.swift +++ b/IrisCompanion/iris/Views/BleStatusView.swift @@ -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) diff --git a/IrisGlass/app/src/main/java/sh/nym/irisglass/BleCentralClient.java b/IrisGlass/app/src/main/java/sh/nym/irisglass/BleCentralClient.java index eb1cf6d..6ca814e 100644 --- a/IrisGlass/app/src/main/java/sh/nym/irisglass/BleCentralClient.java +++ b/IrisGlass/app/src/main/java/sh/nym/irisglass/BleCentralClient.java @@ -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 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(); + } } diff --git a/IrisGlass/app/src/main/java/sh/nym/irisglass/Constants.java b/IrisGlass/app/src/main/java/sh/nym/irisglass/Constants.java index 4111648..5e793de 100644 --- a/IrisGlass/app/src/main/java/sh/nym/irisglass/Constants.java +++ b/IrisGlass/app/src/main/java/sh/nym/irisglass/Constants.java @@ -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";