@offline-protocol/mesh-sdk 0.2.1 → 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.
|
@@ -698,7 +698,11 @@ class BleManager(
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
|
|
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)
|
|
702
706
|
|
|
703
707
|
// Reduced logging
|
|
704
708
|
} catch (e: SecurityException) {
|
|
@@ -762,6 +766,21 @@ class BleManager(
|
|
|
762
766
|
.build()
|
|
763
767
|
}
|
|
764
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
|
+
|
|
765
784
|
private fun handleScanResult(result: ScanResult) {
|
|
766
785
|
val device = result.device
|
|
767
786
|
val rssi = result.rssi
|
package/ios/BleManager.swift
CHANGED
|
@@ -161,6 +161,9 @@ public class BleManager: NSObject, TransportManager {
|
|
|
161
161
|
private let MAX_CONSECUTIVE_SCAN_RESTARTS = 3
|
|
162
162
|
private let CENTRAL_RESET_BACKOFF: TimeInterval = 45.0
|
|
163
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
|
|
164
167
|
|
|
165
168
|
// MARK: - Thread helpers
|
|
166
169
|
@inline(__always)
|
|
@@ -387,6 +390,7 @@ public class BleManager: NSObject, TransportManager {
|
|
|
387
390
|
pendingFragments.removeAll()
|
|
388
391
|
pendingOutboundFragments.removeAll()
|
|
389
392
|
lastSeenMeshAdvertisements.removeAll()
|
|
393
|
+
unknownDeviceAttempts.removeAll()
|
|
390
394
|
pendingAdvertiseRestart?.cancel()
|
|
391
395
|
pendingAdvertiseRestart = nil
|
|
392
396
|
lastAdvertiseRestartAt = nil
|
|
@@ -477,8 +481,12 @@ public class BleManager: NSObject, TransportManager {
|
|
|
477
481
|
scanRestartCount = 0
|
|
478
482
|
}
|
|
479
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.
|
|
480
488
|
central.scanForPeripherals(
|
|
481
|
-
withServices:
|
|
489
|
+
withServices: nil,
|
|
482
490
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
483
491
|
)
|
|
484
492
|
isScanning = true
|
|
@@ -1216,6 +1224,9 @@ public class BleManager: NSObject, TransportManager {
|
|
|
1216
1224
|
|
|
1217
1225
|
private func pruneMeshObservations(now: Date = Date()) {
|
|
1218
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 }
|
|
1219
1230
|
}
|
|
1220
1231
|
|
|
1221
1232
|
// MARK: - Adaptive Scan Methods
|
|
@@ -1319,6 +1330,88 @@ public class BleManager: NSObject, TransportManager {
|
|
|
1319
1330
|
|
|
1320
1331
|
return normalizedHash < skipProbability
|
|
1321
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
|
+
}
|
|
1322
1415
|
|
|
1323
1416
|
private func identifierForNodeHash(_ nodeHash: UInt64) -> UUID? {
|
|
1324
1417
|
for (identifier, observation) in lastSeenMeshAdvertisements where observation.advertisement.nodeIdHash == nodeHash {
|
|
@@ -1499,6 +1592,20 @@ extension BleManager: CBCentralManagerDelegate {
|
|
|
1499
1592
|
// Adaptive scanning: track discoveries for density estimation
|
|
1500
1593
|
recordDiscoveryForDensity(now: now)
|
|
1501
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
|
+
|
|
1502
1609
|
// Adaptive scanning: early RSSI filtering in dense networks
|
|
1503
1610
|
if shouldFilterByRssi(rssiValue) {
|
|
1504
1611
|
if logThrottler.shouldLog(key: "adaptive_rssi_filter", interval: 10) {
|
package/package.json
CHANGED