@offline-protocol/mesh-sdk 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +963 -106
- package/android/src/main/java/com/offlineprotocol/BleManager.kt +67 -3
- package/ios/BleManager.swift +169 -6
- package/package.json +1 -1
|
@@ -113,6 +113,8 @@ class BleManager(
|
|
|
113
113
|
private var gattServer: BluetoothGattServer? = null
|
|
114
114
|
private var messageCharacteristic: BluetoothGattCharacteristic? = null
|
|
115
115
|
private var deviceIdCharacteristic: BluetoothGattCharacteristic? = null
|
|
116
|
+
@Volatile private var isGattServiceReady = false
|
|
117
|
+
@Volatile private var pendingAdvertiseReason: String? = null
|
|
116
118
|
|
|
117
119
|
// Connection registry keeps track of client/server links and desired roles.
|
|
118
120
|
private val connections = MeshConnectionRegistry()
|
|
@@ -412,6 +414,8 @@ class BleManager(
|
|
|
412
414
|
// Close GATT server
|
|
413
415
|
gattServer?.close()
|
|
414
416
|
gattServer = null
|
|
417
|
+
isGattServiceReady = false
|
|
418
|
+
pendingAdvertiseReason = null
|
|
415
419
|
|
|
416
420
|
updateState(TransportState.STOPPED)
|
|
417
421
|
protocol.bleStatusChanged(false)
|
|
@@ -508,6 +512,9 @@ class BleManager(
|
|
|
508
512
|
|
|
509
513
|
private fun setupGattServer() {
|
|
510
514
|
try {
|
|
515
|
+
// Reset flag - service registration is asynchronous
|
|
516
|
+
isGattServiceReady = false
|
|
517
|
+
|
|
511
518
|
gattServer = bluetoothManager.openGattServer(context, gattServerCallback)
|
|
512
519
|
|
|
513
520
|
// Create message characteristic (write without response + notify)
|
|
@@ -530,10 +537,11 @@ class BleManager(
|
|
|
530
537
|
service.addCharacteristic(messageCharacteristic)
|
|
531
538
|
service.addCharacteristic(deviceIdCharacteristic)
|
|
532
539
|
|
|
533
|
-
// Add service to GATT server
|
|
540
|
+
// Add service to GATT server (asynchronous - callback in onServiceAdded)
|
|
534
541
|
gattServer?.addService(service)
|
|
535
542
|
|
|
536
|
-
Log.i(TAG, "GATT server
|
|
543
|
+
Log.i(TAG, "GATT server setup initiated, waiting for service registration callback...")
|
|
544
|
+
emitDiagnostic("info", "GATT server setup initiated")
|
|
537
545
|
} catch (e: SecurityException) {
|
|
538
546
|
Log.e(TAG, "Permission denied while setting up GATT server", e)
|
|
539
547
|
throw e
|
|
@@ -644,6 +652,16 @@ class BleManager(
|
|
|
644
652
|
private fun startAdvertising(reason: String = "manual") {
|
|
645
653
|
if (isAdvertising) return
|
|
646
654
|
|
|
655
|
+
// Wait for GATT service to be ready before advertising
|
|
656
|
+
if (!isGattServiceReady) {
|
|
657
|
+
pendingAdvertiseReason = reason
|
|
658
|
+
if (logThrottler.shouldLog("advert_waiting_gatt", intervalMs = 5000)) {
|
|
659
|
+
Log.i(TAG, "Waiting for GATT service to be ready before advertising (reason: $reason)")
|
|
660
|
+
emitDiagnostic("info", "Waiting for GATT service registration", mapOf("reason" to reason))
|
|
661
|
+
}
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
|
|
647
665
|
try {
|
|
648
666
|
val settings = AdvertiseSettings.Builder()
|
|
649
667
|
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
@@ -680,7 +698,11 @@ class BleManager(
|
|
|
680
698
|
}
|
|
681
699
|
}
|
|
682
700
|
|
|
683
|
-
|
|
701
|
+
// Include scan response with service UUID for iOS compatibility
|
|
702
|
+
// iOS's CoreBluetooth actively queries for scan responses and has known issues
|
|
703
|
+
// recognizing 128-bit service UUIDs from Android's main advertisement packet
|
|
704
|
+
val scanResponse = buildScanResponse()
|
|
705
|
+
bluetoothLeAdvertiser?.startAdvertising(settings, advertiseData, scanResponse, advertiseCallback)
|
|
684
706
|
|
|
685
707
|
// Reduced logging
|
|
686
708
|
} catch (e: SecurityException) {
|
|
@@ -744,6 +766,21 @@ class BleManager(
|
|
|
744
766
|
.build()
|
|
745
767
|
}
|
|
746
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Builds the scan response data for BLE advertising.
|
|
771
|
+
*
|
|
772
|
+
* iOS's CoreBluetooth actively queries for scan responses during BLE scanning.
|
|
773
|
+
* Including the service UUID in the scan response makes Android devices more
|
|
774
|
+
* reliably visible to iOS devices, which have known issues recognizing 128-bit
|
|
775
|
+
* service UUIDs from Android's main advertisement packet format.
|
|
776
|
+
*/
|
|
777
|
+
private fun buildScanResponse(): AdvertiseData {
|
|
778
|
+
return AdvertiseData.Builder()
|
|
779
|
+
.setIncludeDeviceName(false)
|
|
780
|
+
.addServiceUuid(ParcelUuid(SERVICE_UUID))
|
|
781
|
+
.build()
|
|
782
|
+
}
|
|
783
|
+
|
|
747
784
|
private fun handleScanResult(result: ScanResult) {
|
|
748
785
|
val device = result.device
|
|
749
786
|
val rssi = result.rssi
|
|
@@ -1456,6 +1493,33 @@ class BleManager(
|
|
|
1456
1493
|
// MARK: - GATT Server Callback
|
|
1457
1494
|
|
|
1458
1495
|
private val gattServerCallback = object : BluetoothGattServerCallback() {
|
|
1496
|
+
override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
|
|
1497
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1498
|
+
Log.i(TAG, "✅ GATT service added successfully: ${service?.uuid}")
|
|
1499
|
+
emitDiagnostic("info", "GATT service registered successfully", mapOf(
|
|
1500
|
+
"serviceUUID" to (service?.uuid?.toString() ?: "unknown")
|
|
1501
|
+
))
|
|
1502
|
+
isGattServiceReady = true
|
|
1503
|
+
|
|
1504
|
+
// Start advertising now that the service is ready
|
|
1505
|
+
val reason = pendingAdvertiseReason
|
|
1506
|
+
if (reason != null) {
|
|
1507
|
+
pendingAdvertiseReason = null
|
|
1508
|
+
Log.i(TAG, "📡 Starting deferred advertising after GATT service ready")
|
|
1509
|
+
mainHandler.post {
|
|
1510
|
+
startAdvertising("gatt_service_ready")
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
} else {
|
|
1514
|
+
Log.e(TAG, "❌ Error adding GATT service: status=$status")
|
|
1515
|
+
emitDiagnostic("error", "Error adding GATT service", mapOf(
|
|
1516
|
+
"status" to status,
|
|
1517
|
+
"serviceUUID" to (service?.uuid?.toString() ?: "unknown")
|
|
1518
|
+
))
|
|
1519
|
+
isGattServiceReady = false
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1459
1523
|
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
|
1460
1524
|
when (newState) {
|
|
1461
1525
|
BluetoothProfile.STATE_CONNECTED -> {
|
package/ios/BleManager.swift
CHANGED
|
@@ -121,6 +121,8 @@ public class BleManager: NSObject, TransportManager {
|
|
|
121
121
|
private var isAdvertising = false
|
|
122
122
|
private var centralReady = false
|
|
123
123
|
private var peripheralReady = false
|
|
124
|
+
private var isGattServiceReady = false
|
|
125
|
+
private var pendingAdvertiseAfterServiceReady = false
|
|
124
126
|
private var subscribedCentrals: Set<UUID> = []
|
|
125
127
|
private var lastMeshAdvertisement: MeshAdvertisementData?
|
|
126
128
|
|
|
@@ -159,6 +161,9 @@ public class BleManager: NSObject, TransportManager {
|
|
|
159
161
|
private let MAX_CONSECUTIVE_SCAN_RESTARTS = 3
|
|
160
162
|
private let CENTRAL_RESET_BACKOFF: TimeInterval = 45.0
|
|
161
163
|
private let MINIMUM_RSSI_TO_CONNECT: Int16 = -90
|
|
164
|
+
/// Rate limiting for unknown connectable devices that need GATT verification
|
|
165
|
+
private var unknownDeviceAttempts: [UUID: Date] = [:]
|
|
166
|
+
private let UNKNOWN_DEVICE_RATE_LIMIT: TimeInterval = 10.0
|
|
162
167
|
|
|
163
168
|
// MARK: - Thread helpers
|
|
164
169
|
@inline(__always)
|
|
@@ -385,6 +390,7 @@ public class BleManager: NSObject, TransportManager {
|
|
|
385
390
|
pendingFragments.removeAll()
|
|
386
391
|
pendingOutboundFragments.removeAll()
|
|
387
392
|
lastSeenMeshAdvertisements.removeAll()
|
|
393
|
+
unknownDeviceAttempts.removeAll()
|
|
388
394
|
pendingAdvertiseRestart?.cancel()
|
|
389
395
|
pendingAdvertiseRestart = nil
|
|
390
396
|
lastAdvertiseRestartAt = nil
|
|
@@ -397,6 +403,8 @@ public class BleManager: NSObject, TransportManager {
|
|
|
397
403
|
|
|
398
404
|
centralReady = false
|
|
399
405
|
peripheralReady = false
|
|
406
|
+
isGattServiceReady = false
|
|
407
|
+
pendingAdvertiseAfterServiceReady = false
|
|
400
408
|
|
|
401
409
|
updateState(.stopped)
|
|
402
410
|
emitDiagnostic("info", "BLE transport stopped")
|
|
@@ -473,8 +481,12 @@ public class BleManager: NSObject, TransportManager {
|
|
|
473
481
|
scanRestartCount = 0
|
|
474
482
|
}
|
|
475
483
|
|
|
484
|
+
// Scan without service UUID filter for iOS ↔ Android interoperability
|
|
485
|
+
// iOS's scanForPeripherals(withServices:) has known issues recognizing 128-bit
|
|
486
|
+
// service UUIDs from Android advertisements. Scanning with nil allows us to see
|
|
487
|
+
// all peripherals and filter in the discovery callback instead.
|
|
476
488
|
central.scanForPeripherals(
|
|
477
|
-
withServices:
|
|
489
|
+
withServices: nil,
|
|
478
490
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
479
491
|
)
|
|
480
492
|
isScanning = true
|
|
@@ -705,6 +717,16 @@ public class BleManager: NSObject, TransportManager {
|
|
|
705
717
|
|
|
706
718
|
setupGattServer()
|
|
707
719
|
|
|
720
|
+
// Wait for GATT service to be ready before advertising
|
|
721
|
+
guard isGattServiceReady else {
|
|
722
|
+
pendingAdvertiseAfterServiceReady = true
|
|
723
|
+
if logThrottler.shouldLog(key: "advert_waiting_gatt", interval: 5) {
|
|
724
|
+
print("[BleManager] Waiting for GATT service to be ready before advertising (reason: \(reason))")
|
|
725
|
+
emitDiagnostic("info", "Waiting for GATT service registration", context: ["reason": reason])
|
|
726
|
+
}
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
|
|
708
730
|
let meshData = meshController.advertisement()
|
|
709
731
|
lastMeshAdvertisement = meshData
|
|
710
732
|
var advertisementData: [String: Any] = [
|
|
@@ -763,10 +785,13 @@ public class BleManager: NSObject, TransportManager {
|
|
|
763
785
|
|
|
764
786
|
private func setupGattServer() {
|
|
765
787
|
guard let peripheral = peripheralManager else { return }
|
|
766
|
-
if messageCharacteristic != nil && deviceIdCharacteristic != nil {
|
|
788
|
+
if messageCharacteristic != nil && deviceIdCharacteristic != nil && isGattServiceReady {
|
|
767
789
|
return
|
|
768
790
|
}
|
|
769
791
|
|
|
792
|
+
// Reset flag - service registration is asynchronous
|
|
793
|
+
isGattServiceReady = false
|
|
794
|
+
|
|
770
795
|
// Create message characteristic (write without response + notify)
|
|
771
796
|
messageCharacteristic = CBMutableCharacteristic(
|
|
772
797
|
type: MESSAGE_CHAR_UUID,
|
|
@@ -788,10 +813,10 @@ public class BleManager: NSObject, TransportManager {
|
|
|
788
813
|
let service = CBMutableService(type: SERVICE_UUID, primary: true)
|
|
789
814
|
service.characteristics = [messageCharacteristic!, deviceIdCharacteristic!]
|
|
790
815
|
|
|
791
|
-
// Add service to peripheral manager
|
|
816
|
+
// Add service to peripheral manager (asynchronous - callback in peripheralManager(_:didAdd:error:))
|
|
792
817
|
peripheral.add(service)
|
|
793
|
-
print("[BleManager] GATT server
|
|
794
|
-
emitDiagnostic("info", "GATT server
|
|
818
|
+
print("[BleManager] GATT server setup initiated, waiting for service registration callback...")
|
|
819
|
+
emitDiagnostic("info", "GATT server setup initiated")
|
|
795
820
|
}
|
|
796
821
|
|
|
797
822
|
private func startFragmentPolling() {
|
|
@@ -1199,6 +1224,9 @@ public class BleManager: NSObject, TransportManager {
|
|
|
1199
1224
|
|
|
1200
1225
|
private func pruneMeshObservations(now: Date = Date()) {
|
|
1201
1226
|
lastSeenMeshAdvertisements = lastSeenMeshAdvertisements.filter { now.timeIntervalSince($0.value.timestamp) <= MESH_OBSERVATION_TTL }
|
|
1227
|
+
|
|
1228
|
+
// Also clean up stale unknown device rate limiting entries
|
|
1229
|
+
unknownDeviceAttempts = unknownDeviceAttempts.filter { now.timeIntervalSince($0.value) <= UNKNOWN_DEVICE_RATE_LIMIT * 3 }
|
|
1202
1230
|
}
|
|
1203
1231
|
|
|
1204
1232
|
// MARK: - Adaptive Scan Methods
|
|
@@ -1302,6 +1330,88 @@ public class BleManager: NSObject, TransportManager {
|
|
|
1302
1330
|
|
|
1303
1331
|
return normalizedHash < skipProbability
|
|
1304
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
// MARK: - Smart Filtering for iOS ↔ Android Interoperability
|
|
1335
|
+
|
|
1336
|
+
/// Determines if a discovered peripheral should be processed.
|
|
1337
|
+
/// This implements smart filtering since we scan without a service UUID filter
|
|
1338
|
+
/// (required for iOS ↔ Android interoperability).
|
|
1339
|
+
///
|
|
1340
|
+
/// Accepts:
|
|
1341
|
+
/// - Devices advertising our service UUID (iOS devices)
|
|
1342
|
+
/// - Devices with our service data
|
|
1343
|
+
/// - Previously discovered mesh devices
|
|
1344
|
+
/// - Unknown connectable devices (rate-limited, verified via GATT)
|
|
1345
|
+
private func shouldProcessDiscoveredPeripheral(
|
|
1346
|
+
peripheral: CBPeripheral,
|
|
1347
|
+
advertisementData: [String: Any],
|
|
1348
|
+
rssi: Int16,
|
|
1349
|
+
now: Date
|
|
1350
|
+
) -> Bool {
|
|
1351
|
+
// 1. Check if device is advertising our service UUID
|
|
1352
|
+
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
|
1353
|
+
if serviceUUIDs.contains(SERVICE_UUID) {
|
|
1354
|
+
return true
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// 2. Check for our service data (may come from scan response)
|
|
1359
|
+
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
|
|
1360
|
+
if serviceData[SERVICE_UUID] != nil {
|
|
1361
|
+
return true
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// 3. Check if this is a previously discovered mesh device
|
|
1366
|
+
if lastSeenMeshAdvertisements[peripheral.identifier] != nil {
|
|
1367
|
+
return true
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// 4. Check if we already have this peripheral in our discovered list
|
|
1371
|
+
// (previously connected or successfully verified via GATT)
|
|
1372
|
+
if discoveredPeripherals[peripheral.identifier] != nil {
|
|
1373
|
+
return true
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// 5. Check if we already have a device ID mapping for this peripheral
|
|
1377
|
+
if connections.peripheralDeviceId(for: peripheral.identifier) != nil ||
|
|
1378
|
+
connections.centralDeviceId(for: peripheral.identifier) != nil {
|
|
1379
|
+
return true
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 6. For unknown connectable devices, allow with rate limiting
|
|
1383
|
+
// These will be verified via GATT service discovery after connection
|
|
1384
|
+
let isConnectable: Bool
|
|
1385
|
+
if #available(iOS 13.0, *) {
|
|
1386
|
+
isConnectable = (advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber)?.boolValue ?? false
|
|
1387
|
+
} else {
|
|
1388
|
+
isConnectable = true
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if isConnectable {
|
|
1392
|
+
// Rate limit unknown device connection attempts
|
|
1393
|
+
if let lastAttempt = unknownDeviceAttempts[peripheral.identifier],
|
|
1394
|
+
now.timeIntervalSince(lastAttempt) < UNKNOWN_DEVICE_RATE_LIMIT {
|
|
1395
|
+
return false
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Only process strong signals for unknown devices
|
|
1399
|
+
if rssi >= -75 {
|
|
1400
|
+
unknownDeviceAttempts[peripheral.identifier] = now
|
|
1401
|
+
if logThrottler.shouldLog(key: "unknown_connectable_\(peripheral.identifier.uuidString)", interval: 30) {
|
|
1402
|
+
print("[BleManager] Allowing unknown connectable device for GATT verification: \(peripheral.identifier) RSSI=\(rssi)")
|
|
1403
|
+
emitDiagnostic("debug", "Allowing unknown device for GATT verification", context: [
|
|
1404
|
+
"identifier": peripheral.identifier.uuidString,
|
|
1405
|
+
"rssi": rssi
|
|
1406
|
+
])
|
|
1407
|
+
}
|
|
1408
|
+
return true
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Filter out all other devices (not our mesh network)
|
|
1413
|
+
return false
|
|
1414
|
+
}
|
|
1305
1415
|
|
|
1306
1416
|
private func identifierForNodeHash(_ nodeHash: UInt64) -> UUID? {
|
|
1307
1417
|
for (identifier, observation) in lastSeenMeshAdvertisements where observation.advertisement.nodeIdHash == nodeHash {
|
|
@@ -1482,6 +1592,20 @@ extension BleManager: CBCentralManagerDelegate {
|
|
|
1482
1592
|
// Adaptive scanning: track discoveries for density estimation
|
|
1483
1593
|
recordDiscoveryForDensity(now: now)
|
|
1484
1594
|
|
|
1595
|
+
// Smart filtering for iOS ↔ Android interoperability
|
|
1596
|
+
// Since we scan without a service UUID filter (for Android compatibility),
|
|
1597
|
+
// we need to filter discovered peripherals here instead.
|
|
1598
|
+
let shouldProcess = shouldProcessDiscoveredPeripheral(
|
|
1599
|
+
peripheral: peripheral,
|
|
1600
|
+
advertisementData: advertisementData,
|
|
1601
|
+
rssi: rssiValue,
|
|
1602
|
+
now: now
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
if !shouldProcess {
|
|
1606
|
+
return
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1485
1609
|
// Adaptive scanning: early RSSI filtering in dense networks
|
|
1486
1610
|
if shouldFilterByRssi(rssiValue) {
|
|
1487
1611
|
if logThrottler.shouldLog(key: "adaptive_rssi_filter", interval: 10) {
|
|
@@ -1523,7 +1647,20 @@ extension BleManager: CBCentralManagerDelegate {
|
|
|
1523
1647
|
pruneMeshObservations(now: now)
|
|
1524
1648
|
meshController.observeAdvertisement(meshMetadata, rssi: Int(rssiValue))
|
|
1525
1649
|
|
|
1526
|
-
|
|
1650
|
+
// When there's no metadata (iOS/Android advertising without service data),
|
|
1651
|
+
// still try to connect - metadata will be exchanged via GATT after connection
|
|
1652
|
+
let decision: MeshController.MeshDecision
|
|
1653
|
+
if meshMetadata == nil {
|
|
1654
|
+
// No metadata in advertisement - allow basic connection to exchange info via GATT
|
|
1655
|
+
decision = MeshController.MeshDecision(
|
|
1656
|
+
intent: .intraCluster,
|
|
1657
|
+
reason: "no_metadata_in_advert",
|
|
1658
|
+
evictPeerId: nil
|
|
1659
|
+
)
|
|
1660
|
+
} else {
|
|
1661
|
+
decision = meshController.shouldInitiateOutbound(metadata: meshMetadata, rssi: Int(rssiValue))
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1527
1664
|
guard decision.intent != .rejected else {
|
|
1528
1665
|
if logThrottler.shouldLog(key: "mesh_skip_\(peripheral.identifier.uuidString)", interval: 15) {
|
|
1529
1666
|
print("[BleManager] Skipping \(peripheral.identifier) due to \(decision.reason)")
|
|
@@ -1892,6 +2029,32 @@ extension BleManager: CBPeripheralManagerDelegate {
|
|
|
1892
2029
|
}
|
|
1893
2030
|
}
|
|
1894
2031
|
|
|
2032
|
+
public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
2033
|
+
if let error = error {
|
|
2034
|
+
print("[BleManager] ❌ Error adding GATT service: \(error)")
|
|
2035
|
+
emitDiagnostic("error", "Error adding GATT service", context: [
|
|
2036
|
+
"error": error.localizedDescription,
|
|
2037
|
+
"serviceUUID": service.uuid.uuidString
|
|
2038
|
+
])
|
|
2039
|
+
isGattServiceReady = false
|
|
2040
|
+
return
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
print("[BleManager] ✅ GATT service added successfully: \(service.uuid)")
|
|
2044
|
+
emitDiagnostic("info", "GATT service registered successfully", context: [
|
|
2045
|
+
"serviceUUID": service.uuid.uuidString
|
|
2046
|
+
])
|
|
2047
|
+
|
|
2048
|
+
isGattServiceReady = true
|
|
2049
|
+
|
|
2050
|
+
// Start advertising now that the service is ready
|
|
2051
|
+
if pendingAdvertiseAfterServiceReady {
|
|
2052
|
+
pendingAdvertiseAfterServiceReady = false
|
|
2053
|
+
print("[BleManager] 📡 Starting deferred advertising after GATT service ready")
|
|
2054
|
+
startAdvertising(reason: "gatt_service_ready")
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1895
2058
|
public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
1896
2059
|
for request in requests {
|
|
1897
2060
|
print("[BleManager] 📨 GATT WRITE REQUEST from \(request.central.identifier), char: \(request.characteristic.uuid), size: \(request.value?.count ?? 0)")
|
package/package.json
CHANGED