add wifi request ble characteristic

This commit is contained in:
2026-01-10 14:23:39 +00:00
parent cb6f36924f
commit 4010ba8870
6 changed files with 197 additions and 47 deletions

View File

@@ -16,6 +16,8 @@ final class BlePeripheralManager: NSObject, ObservableObject {
static let serviceUUID = CBUUID(string: "A0B0C0D0-E0F0-4A0B-9C0D-0E0F1A2B3C4D")
static let feedTxUUID = CBUUID(string: "A0B0C0D1-E0F0-4A0B-9C0D-0E0F1A2B3C4D")
static let controlRxUUID = CBUUID(string: "A0B0C0D2-E0F0-4A0B-9C0D-0E0F1A2B3C4D")
// Read/Notify: 1-byte value (0x00=OFF, 0x01=ON) that requests Glass to enable WiFi.
static let wifiRequestTxUUID = CBUUID(string: "A0B0C0D3-E0F0-4A0B-9C0D-0E0F1A2B3C4D")
private static let restoreIdentifier = "iris.ble.peripheral.v1"
@Published private(set) var bluetoothState: CBManagerState = .unknown
@@ -30,6 +32,7 @@ final class BlePeripheralManager: NSObject, ObservableObject {
@Published private(set) var droppedNotifyPackets: Int = 0
@Published private(set) var lastNotifyAt: Date? = nil
@Published private(set) var lastDataAt: Date? = nil
@Published private(set) var wifiRequested: Bool = false
private let queue = DispatchQueue(label: "iris.ble.peripheral.queue")
private lazy var peripheral = CBPeripheralManager(
@@ -43,10 +46,13 @@ final class BlePeripheralManager: NSObject, ObservableObject {
private var service: CBMutableService?
private var feedTx: CBMutableCharacteristic?
private var controlRx: CBMutableCharacteristic?
private var wifiRequestTx: CBMutableCharacteristic?
private var subscribedCentralIds = Set<UUID>()
private var centralMaxUpdateLength: [UUID: Int] = [:]
private var lastReadValue: Data = Data()
private var wifiRequestValue: Data = Data([0x00])
private var wifiRequestNotifyPending: Bool = false
private struct PendingMessage {
let msgId: UInt32
@@ -101,11 +107,23 @@ final class BlePeripheralManager: NSObject, ObservableObject {
}
}
func setWifiRequested(_ requested: Bool) {
queue.async { [weak self] in
guard let self else { return }
let newValue = Data([requested ? 0x01 : 0x00])
guard newValue != self.wifiRequestValue else { return }
self.wifiRequestValue = newValue
self.publish { self.wifiRequested = requested }
self.flushWifiRequestNotifyIfPossible()
}
}
func copyUUIDsToPasteboard() {
let text = """
SERVICE_UUID=\(Self.serviceUUID.uuidString)
FEED_TX_UUID=\(Self.feedTxUUID.uuidString)
CONTROL_RX_UUID=\(Self.controlRxUUID.uuidString)
WIFI_REQUEST_TX_UUID=\(Self.wifiRequestTxUUID.uuidString)
"""
#if canImport(UIKit)
DispatchQueue.main.async {
@@ -116,6 +134,18 @@ final class BlePeripheralManager: NSObject, ObservableObject {
private func ensureService() {
guard peripheral.state == .poweredOn else { return }
if let existingService = service {
let hasWifiChar = (existingService.characteristics ?? []).contains { $0.uuid == Self.wifiRequestTxUUID }
if wifiRequestTx == nil || !hasWifiChar {
stopAdvertising()
peripheral.removeAllServices()
service = nil
feedTx = nil
controlRx = nil
wifiRequestTx = nil
}
}
guard service == nil else { return }
let feedTx = CBMutableCharacteristic(
@@ -130,11 +160,18 @@ final class BlePeripheralManager: NSObject, ObservableObject {
value: nil,
permissions: [.writeable]
)
let wifiRequestTx = CBMutableCharacteristic(
type: Self.wifiRequestTxUUID,
properties: [.notify, .read],
value: nil,
permissions: [.readable]
)
let service = CBMutableService(type: Self.serviceUUID, primary: true)
service.characteristics = [feedTx, controlRx]
service.characteristics = [feedTx, controlRx, wifiRequestTx]
self.service = service
self.feedTx = feedTx
self.controlRx = controlRx
self.wifiRequestTx = wifiRequestTx
peripheral.add(service)
}
@@ -278,6 +315,20 @@ final class BlePeripheralManager: NSObject, ObservableObject {
publishNotifyQueueDepth()
}
private func flushWifiRequestNotifyIfPossible() {
guard let wifiRequestTx else { return }
let hasSubscribers = !(wifiRequestTx.subscribedCentrals ?? []).isEmpty
guard hasSubscribers else {
wifiRequestNotifyPending = false
return
}
if peripheral.updateValue(wifiRequestValue, for: wifiRequestTx, onSubscribedCentrals: nil) {
wifiRequestNotifyPending = false
} else {
wifiRequestNotifyPending = true
}
}
private func enqueuePendingMessage(_ message: PendingMessage) {
pendingMessages.append(message)
enforceNotifyQueueLimits()
@@ -334,6 +385,7 @@ extension BlePeripheralManager: CBPeripheralManagerDelegate {
self.service = restoredMutableService
self.feedTx = characteristics.first(where: { $0.uuid == Self.feedTxUUID }) as? CBMutableCharacteristic
self.controlRx = characteristics.first(where: { $0.uuid == Self.controlRxUUID }) as? CBMutableCharacteristic
self.wifiRequestTx = characteristics.first(where: { $0.uuid == Self.wifiRequestTxUUID }) as? CBMutableCharacteristic
}
self.publish {
@@ -362,6 +414,7 @@ extension BlePeripheralManager: CBPeripheralManagerDelegate {
self.service = nil
self.feedTx = nil
self.controlRx = nil
self.wifiRequestTx = nil
return
}
self.ensureService()
@@ -380,6 +433,12 @@ extension BlePeripheralManager: CBPeripheralManagerDelegate {
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
if characteristic.uuid == Self.wifiRequestTxUUID {
queue.async { [weak self] in
self?.flushWifiRequestNotifyIfPossible()
}
return
}
guard characteristic.uuid == Self.feedTxUUID else { return }
queue.async { [weak self] in
guard let self = self else { return }
@@ -400,6 +459,9 @@ extension BlePeripheralManager: CBPeripheralManagerDelegate {
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
if characteristic.uuid == Self.wifiRequestTxUUID {
return
}
guard characteristic.uuid == Self.feedTxUUID else { return }
queue.async { [weak self] in
guard let self = self else { return }
@@ -420,17 +482,25 @@ extension BlePeripheralManager: CBPeripheralManagerDelegate {
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
queue.async { [weak self] in
self?.flushPendingMessages()
if let self, self.wifiRequestNotifyPending {
self.flushWifiRequestNotifyIfPossible()
}
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
guard request.characteristic.uuid == Self.feedTxUUID else {
peripheral.respond(to: request, withResult: .requestNotSupported)
return
}
let maxLen = max(0, request.central.maximumUpdateValueLength)
if request.characteristic.uuid == Self.feedTxUUID {
request.value = maxLen > 0 ? lastReadValue.prefix(maxLen) : lastReadValue
peripheral.respond(to: request, withResult: .success)
return
}
if request.characteristic.uuid == Self.wifiRequestTxUUID {
request.value = maxLen > 0 ? wifiRequestValue.prefix(maxLen) : wifiRequestValue
peripheral.respond(to: request, withResult: .success)
return
}
peripheral.respond(to: request, withResult: .requestNotSupported)
}
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {

View File

@@ -34,6 +34,16 @@ struct BleStatusView: View {
ble.start()
}
Toggle(isOn: Binding(get: { ble.wifiRequested }, set: { ble.setWifiRequested($0) })) {
VStack(alignment: .leading, spacing: 2) {
Text("Request Wi-Fi (Glass)")
.font(.headline)
Text(ble.wifiRequested ? "On" : "Off")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
VStack(alignment: .leading, spacing: 8) {
Text("Connection")
.font(.headline)
@@ -86,7 +96,7 @@ struct BleStatusView: View {
VStack(alignment: .leading, spacing: 8) {
Text("UUIDs")
.font(.headline)
Text("Service: \(BlePeripheralManager.serviceUUID.uuidString)\nFEED_TX: \(BlePeripheralManager.feedTxUUID.uuidString)\nCONTROL_RX: \(BlePeripheralManager.controlRxUUID.uuidString)")
Text("Service: \(BlePeripheralManager.serviceUUID.uuidString)\nFEED_TX: \(BlePeripheralManager.feedTxUUID.uuidString)\nCONTROL_RX: \(BlePeripheralManager.controlRxUUID.uuidString)\nWIFI_REQUEST_TX: \(BlePeripheralManager.wifiRequestTxUUID.uuidString)")
.font(.caption)
.textSelection(.enabled)
}

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application

View File

@@ -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,21 +401,56 @@ 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);
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;
}
}
return;
}
if (Constants.WIFI_REQUEST_TX_UUID.equals(chUuid)) {
Log.i(Constants.TAG_BLE, "Subscribed to WIFI_REQUEST_TX");
pendingWifiCccd = null;
}
}
private void handleCharacteristicChanged(UUID uuid, byte[] value) {
if (!Constants.FEED_TX_UUID.equals(uuid)) return;
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);
@@ -415,16 +471,11 @@ public final class BleCentralClient {
}
Log.d(Constants.TAG_FEED, "Unparsed notify len=" + (value != null ? value.length : 0) + hint + " hex=" + hexPrefix(value, 16));
}
return;
}
if (result.isPing) {
} else if (result.isPing) {
Log.d(Constants.TAG_FEED, "PING");
lastPingAtMs = lastNotificationAtMs;
if (callback != null) callback.onPing();
return;
}
if (result.jsonOrNull != null) {
} 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);
@@ -432,9 +483,11 @@ public final class BleCentralClient {
if (callback != null) callback.onFeedJson(result.jsonOrNull);
}
}
}
private void closeGattInternal() {
subscribed = false;
pendingWifiCccd = null;
stopLivenessCheck();
handler.removeCallbacks(connectTimeoutRunnable);
if (gatt != null) {

View File

@@ -1,8 +1,10 @@
package sh.nym.irisglass;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.wifi.WifiManager;
import android.os.HandlerThread;
import android.os.IBinder;
import android.preference.PreferenceManager;
@@ -92,6 +94,11 @@ public final class BleLinkService extends Service implements BleCentralClient.Ca
}
}
@Override
public void onWifiRequest(boolean requested) {
setWifiEnabled(requested);
}
private void maybeNudgeWinnerChanged(FeedEnvelope env) {
if (env == null) return;
@@ -122,4 +129,11 @@ public final class BleLinkService extends Service implements BleCentralClient.Ca
i.putExtra(Constants.EXTRA_WINNER_ID, winnerId);
startService(i);
}
private void setWifiEnabled(boolean enabled) {
WifiManager wifiManager = (WifiManager) this.getSystemService(Context.WIFI_SERVICE);
if (wifiManager != null) {
wifiManager.setWifiEnabled(enabled);
}
}
}

View File

@@ -12,10 +12,12 @@ public final class Constants {
public static final String SERVICE_UUID_STR = "A0B0C0D0-E0F0-4A0B-9C0D-0E0F1A2B3C4D";
public static final String FEED_TX_UUID_STR = "A0B0C0D1-E0F0-4A0B-9C0D-0E0F1A2B3C4D";
public static final String CONTROL_RX_UUID_STR = "A0B0C0D2-E0F0-4A0B-9C0D-0E0F1A2B3C4D";
public static final String WIFI_REQUEST_TX_UUID_STR = "A0B0C0D3-E0F0-4A0B-9C0D-0E0F1A2B3C4D";
public static final UUID SERVICE_UUID = UUID.fromString(SERVICE_UUID_STR);
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 UUID WIFI_REQUEST_TX_UUID = UUID.fromString(WIFI_REQUEST_TX_UUID_STR);
public static final String PERIPHERAL_NAME_HINT = "Aris";
public static final String LEGACY_PERIPHERAL_NAME_HINT = "GlassNow";