@offline-protocol/mesh-sdk 0.2.1 → 0.2.3

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.
Files changed (322) hide show
  1. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +1 -1
  2. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  3. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -1
  4. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +1 -1
  5. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +1 -1
  6. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  7. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values +0 -0
  8. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  9. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.s +1 -1
  10. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  11. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  12. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  13. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  14. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  15. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  16. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  17. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  18. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  19. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  20. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values +0 -0
  21. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  22. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  23. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab.values.at +0 -0
  24. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  25. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  26. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  27. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  28. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values +0 -0
  29. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  30. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.s +1 -1
  31. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  32. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  33. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  34. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  35. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  36. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values +0 -0
  37. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  38. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s +1 -1
  39. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  40. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  41. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  42. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  43. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  44. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  45. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  46. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values +0 -0
  47. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  48. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.s +1 -1
  49. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  50. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  51. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  52. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  53. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  54. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$AdvertisementCacheEntry.class +0 -0
  55. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$Companion.class +0 -0
  56. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$MeshObservation.class +0 -0
  57. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$PendingFragment.class +0 -0
  58. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$fragmentPollingRunnable$1.class +0 -0
  59. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$gattClientCallback$1.class +0 -0
  60. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$gattServerCallback$1.class +0 -0
  61. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$resolveTargetAddress$$inlined$sortedBy$1.class +0 -0
  62. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$routingCleanupRunnable$1.class +0 -0
  63. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$scanWatchdogRunnable$1.class +0 -0
  64. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startAdvertising$1.class +0 -0
  65. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startConnectionMonitor$1.class +0 -0
  66. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager$startScanning$1.class +0 -0
  67. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/BleManager.class +0 -0
  68. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$Companion.class +0 -0
  69. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$messagePollingRunnable$1.class +0 -0
  70. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$pingRunnable$1.class +0 -0
  71. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager$webSocketListener$1.class +0 -0
  72. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/InternetManager.class +0 -0
  73. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/LogThrottler.class +0 -0
  74. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$Companion.class +0 -0
  75. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$Constants.class +0 -0
  76. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$ParsedConfig.class +0 -0
  77. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$1.class +0 -0
  78. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$3$1.class +0 -0
  79. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule$create$5$1.class +0 -0
  80. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolModule.class +0 -0
  81. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/OfflineProtocolPackage.class +0 -0
  82. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$AlreadyRunning.class +0 -0
  83. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$InvalidState.class +0 -0
  84. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$NotAvailable.class +0 -0
  85. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$NotRunning.class +0 -0
  86. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$PermissionDenied.class +0 -0
  87. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$PlatformError.class +0 -0
  88. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException$StartFailed.class +0 -0
  89. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportException.class +0 -0
  90. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportManager.class +0 -0
  91. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportManagerListener.class +0 -0
  92. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/TransportState.class +0 -0
  93. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$Companion.class +0 -0
  94. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$messagePollingRunnable$1.class +0 -0
  95. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$p2pReceiver$1.class +0 -0
  96. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$startPeerDiscovery$1.class +0 -0
  97. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager$stopPeerDiscovery$1.class +0 -0
  98. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/WifiDirectManager.class +0 -0
  99. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/ble/MeshConnectionRegistry.class +0 -0
  100. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshAdvertisementData$Companion.class +0 -0
  101. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshAdvertisementData.class +0 -0
  102. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$Companion.class +0 -0
  103. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$ConnectionIntent.class +0 -0
  104. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshConfig.class +0 -0
  105. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshDecision.class +0 -0
  106. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$MeshRole.class +0 -0
  107. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$PeerMetrics.class +0 -0
  108. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$PeerState.class +0 -0
  109. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$RebalanceDirective.class +0 -0
  110. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$RemoteCandidate.class +0 -0
  111. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController$ScoreWeights.class +0 -0
  112. package/android/build/tmp/kotlin-classes/debug/com/offlineprotocol/mesh/MeshController.class +0 -0
  113. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/AckConfig$Companion.class +0 -0
  114. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/AckConfig.class +0 -0
  115. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/BleFragment$Companion.class +0 -0
  116. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/BleFragment.class +0 -0
  117. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupConfig$Companion.class +0 -0
  118. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupConfig.class +0 -0
  119. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupStats$Companion.class +0 -0
  120. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DedupStats.class +0 -0
  121. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Disposable$Companion.class +0 -0
  122. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Disposable.class +0 -0
  123. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DorsConfig$Companion.class +0 -0
  124. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/DorsConfig.class +0 -0
  125. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/EventCallback$Companion.class +0 -0
  126. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/EventCallback.class +0 -0
  127. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverter$DefaultImpls.class +0 -0
  128. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverter.class +0 -0
  129. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterBoolean.class +0 -0
  130. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterCallbackInterface.class +0 -0
  131. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterFloat.class +0 -0
  132. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalShort.class +0 -0
  133. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalString.class +0 -0
  134. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeBleFragment.class +0 -0
  135. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeFileProgress.class +0 -0
  136. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeInternetMessage.class +0 -0
  137. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeRouteEntry.class +0 -0
  138. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeTransportMetrics.class +0 -0
  139. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalTypeWifiDirectMessage.class +0 -0
  140. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalUByte.class +0 -0
  141. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterOptionalULong.class +0 -0
  142. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterRustBuffer.class +0 -0
  143. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceString.class +0 -0
  144. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeMessageStats.class +0 -0
  145. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeNetworkLink.class +0 -0
  146. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeNetworkNode.class +0 -0
  147. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceTypeRouteEntry.class +0 -0
  148. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterSequenceUByte.class +0 -0
  149. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterShort.class +0 -0
  150. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterString.class +0 -0
  151. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeAckConfig.class +0 -0
  152. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeBleFragment.class +0 -0
  153. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDedupConfig.class +0 -0
  154. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDedupStats.class +0 -0
  155. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeDorsConfig.class +0 -0
  156. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeEventCallback.class +0 -0
  157. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeFileProgress.class +0 -0
  158. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeGradientRoutingConfig.class +0 -0
  159. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeInternetMessage.class +0 -0
  160. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeMessagePriority.class +0 -0
  161. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeMessageStats.class +0 -0
  162. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkLink.class +0 -0
  163. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkNode.class +0 -0
  164. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeNetworkTopology.class +0 -0
  165. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeOfflineProtocol.class +0 -0
  166. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypePathConfig.class +0 -0
  167. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypePeerDevice.class +0 -0
  168. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolConfig.class +0 -0
  169. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolConfigExtended.class +0 -0
  170. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolError.class +0 -0
  171. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeProtocolState.class +0 -0
  172. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRelayConfig.class +0 -0
  173. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRelayPriority.class +0 -0
  174. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeReliabilityConfig.class +0 -0
  175. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRetryConfig.class +0 -0
  176. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRouteEntry.class +0 -0
  177. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeRoutingStats.class +0 -0
  178. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportConfig.class +0 -0
  179. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportMetrics.class +0 -0
  180. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeTransportType.class +0 -0
  181. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterTypeWifiDirectMessage.class +0 -0
  182. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUByte.class +0 -0
  183. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUInt.class +0 -0
  184. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterULong.class +0 -0
  185. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FfiConverterUShort.class +0 -0
  186. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FileProgress$Companion.class +0 -0
  187. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/FileProgress.class +0 -0
  188. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ForeignBytes$ByValue.class +0 -0
  189. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ForeignBytes.class +0 -0
  190. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/GradientRoutingConfig$Companion.class +0 -0
  191. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/GradientRoutingConfig.class +0 -0
  192. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/IntegrityCheckingUniffiLib.class +0 -0
  193. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternalException.class +0 -0
  194. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternetMessage$Companion.class +0 -0
  195. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/InternetMessage.class +0 -0
  196. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/JavaLangRefCleanable.class +0 -0
  197. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/JavaLangRefCleaner.class +0 -0
  198. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessagePriority$Companion.class +0 -0
  199. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessagePriority.class +0 -0
  200. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessageStats$Companion.class +0 -0
  201. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/MessageStats.class +0 -0
  202. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkLink$Companion.class +0 -0
  203. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkLink.class +0 -0
  204. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkNode$Companion.class +0 -0
  205. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkNode.class +0 -0
  206. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkTopology$Companion.class +0 -0
  207. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NetworkTopology.class +0 -0
  208. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/NoHandle.class +0 -0
  209. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol$Companion.class +0 -0
  210. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol$UniffiCleanAction.class +0 -0
  211. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocol.class +0 -0
  212. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocolInterface$Companion.class +0 -0
  213. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/OfflineProtocolInterface.class +0 -0
  214. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/Offline_protocolKt.class +0 -0
  215. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PathConfig$Companion.class +0 -0
  216. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PathConfig.class +0 -0
  217. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PeerDevice$Companion.class +0 -0
  218. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/PeerDevice.class +0 -0
  219. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfig$Companion.class +0 -0
  220. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfig.class +0 -0
  221. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfigExtended$Companion.class +0 -0
  222. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolConfigExtended.class +0 -0
  223. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$AlreadyStarted.class +0 -0
  224. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$ErrorHandler.class +0 -0
  225. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$InvalidConfiguration.class +0 -0
  226. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$InvalidState.class +0 -0
  227. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$NotStarted.class +0 -0
  228. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$Other.class +0 -0
  229. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException$SendFailed.class +0 -0
  230. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolException.class +0 -0
  231. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolState$Companion.class +0 -0
  232. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ProtocolState.class +0 -0
  233. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayConfig$Companion.class +0 -0
  234. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayConfig.class +0 -0
  235. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayPriority$Companion.class +0 -0
  236. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RelayPriority.class +0 -0
  237. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ReliabilityConfig$Companion.class +0 -0
  238. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/ReliabilityConfig.class +0 -0
  239. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RetryConfig$Companion.class +0 -0
  240. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RetryConfig.class +0 -0
  241. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RouteEntry$Companion.class +0 -0
  242. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RouteEntry.class +0 -0
  243. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RoutingStats$Companion.class +0 -0
  244. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RoutingStats.class +0 -0
  245. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$ByReference.class +0 -0
  246. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$ByValue.class +0 -0
  247. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer$Companion.class +0 -0
  248. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/RustBuffer.class +0 -0
  249. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportConfig$Companion.class +0 -0
  250. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportConfig.class +0 -0
  251. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportMetrics$Companion.class +0 -0
  252. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportMetrics.class +0 -0
  253. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportType$Companion.class +0 -0
  254. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/TransportType.class +0 -0
  255. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceClone.class +0 -0
  256. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceEventCallbackMethod0.class +0 -0
  257. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCallbackInterfaceFree.class +0 -0
  258. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner$Cleanable.class +0 -0
  259. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner$Companion.class +0 -0
  260. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiCleaner.class +0 -0
  261. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteF32.class +0 -0
  262. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteF64.class +0 -0
  263. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI16.class +0 -0
  264. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI32.class +0 -0
  265. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI64.class +0 -0
  266. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteI8.class +0 -0
  267. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteRustBuffer.class +0 -0
  268. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU16.class +0 -0
  269. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU32.class +0 -0
  270. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU64.class +0 -0
  271. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteU8.class +0 -0
  272. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureCompleteVoid.class +0 -0
  273. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallback.class +0 -0
  274. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallbackStruct$UniffiByValue.class +0 -0
  275. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureDroppedCallbackStruct.class +0 -0
  276. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF32$UniffiByValue.class +0 -0
  277. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF32.class +0 -0
  278. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF64$UniffiByValue.class +0 -0
  279. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultF64.class +0 -0
  280. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI16$UniffiByValue.class +0 -0
  281. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI16.class +0 -0
  282. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI32$UniffiByValue.class +0 -0
  283. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI32.class +0 -0
  284. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI64$UniffiByValue.class +0 -0
  285. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI64.class +0 -0
  286. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI8$UniffiByValue.class +0 -0
  287. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultI8.class +0 -0
  288. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultRustBuffer$UniffiByValue.class +0 -0
  289. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultRustBuffer.class +0 -0
  290. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU16$UniffiByValue.class +0 -0
  291. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU16.class +0 -0
  292. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU32$UniffiByValue.class +0 -0
  293. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU32.class +0 -0
  294. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU64$UniffiByValue.class +0 -0
  295. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU64.class +0 -0
  296. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU8$UniffiByValue.class +0 -0
  297. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultU8.class +0 -0
  298. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultVoid$UniffiByValue.class +0 -0
  299. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiForeignFutureResultVoid.class +0 -0
  300. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiHandleMap.class +0 -0
  301. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiJnaCleanable.class +0 -0
  302. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiJnaCleaner.class +0 -0
  303. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiLib.class +0 -0
  304. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiNullRustCallStatusErrorHandler.class +0 -0
  305. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus$ByValue.class +0 -0
  306. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus$Companion.class +0 -0
  307. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatus.class +0 -0
  308. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustCallStatusErrorHandler.class +0 -0
  309. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiRustFutureContinuationCallback.class +0 -0
  310. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiVTableCallbackInterfaceEventCallback$UniffiByValue.class +0 -0
  311. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiVTableCallbackInterfaceEventCallback.class +0 -0
  312. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/UniffiWithHandle.class +0 -0
  313. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/WifiDirectMessage$Companion.class +0 -0
  314. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/WifiDirectMessage.class +0 -0
  315. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$onEvent.class +0 -0
  316. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$uniffiClone.class +0 -0
  317. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback$uniffiFree.class +0 -0
  318. package/android/build/tmp/kotlin-classes/debug/uniffi/offline_protocol/uniffiCallbackInterfaceEventCallback.class +0 -0
  319. package/android/src/main/java/com/offlineprotocol/BleManager.kt +571 -34
  320. package/ios/BleManager.swift +427 -14
  321. package/package.json +1 -1
  322. /package/android/build/tmp/kotlin-classes/debug/META-INF/{offlineprotocol_react-native_debug.kotlin_module → offline-protocol_mesh-sdk_debug.kotlin_module} +0 -0
@@ -71,16 +71,21 @@ class BleManager(
71
71
  private const val FRAGMENT_POLL_INTERVAL_MS = 100L // 100ms
72
72
  private const val MAX_FRAGMENT_SIZE = 185
73
73
  private const val CONNECTION_TIMEOUT_MS = 10000L
74
- private const val SCAN_WATCHDOG_INTERVAL_MS = 20000L
74
+ private const val SCAN_WATCHDOG_INTERVAL_MS = 30000L // Match iOS timing
75
75
  private const val SCAN_WATCHDOG_HEARTBEAT_MS = 10000L
76
76
  private const val MAX_CONNECTIONS_PER_DEVICE = 4
77
77
  private const val ADVERTISE_RESTART_MIN_MS = 200L
78
78
  private const val ADVERTISE_RESTART_MAX_MS = 1200L
79
79
  private const val MIN_ADVERTISE_INTERVAL_MS = 1500L
80
+ private const val MIN_RECONNECT_INTERVAL_MS = 5_000L // Match iOS timing
81
+ private const val MAX_RECONNECT_INTERVAL_MS = 60_000L
82
+ private const val MAX_CONNECTION_RETRIES = 5
80
83
 
81
84
  // Adaptive Scan Configuration
82
- /** Minimum RSSI to consider for connection (filter weak signals early) */
85
+ /** Minimum RSSI to consider for connection (filter weak signals early) - matches iOS */
83
86
  private const val ADAPTIVE_MIN_RSSI = -85
87
+ /** Absolute minimum RSSI below which we refuse to connect - matches iOS */
88
+ private const val MINIMUM_RSSI_TO_CONNECT = -90
84
89
  /** Peer count threshold below which we process all advertisements */
85
90
  private const val ADAPTIVE_LOW_DENSITY_THRESHOLD = 10
86
91
  /** Peer count threshold above which we apply maximum throttling */
@@ -91,6 +96,24 @@ class BleManager(
91
96
  private const val ADAPTIVE_COOLDOWN_PER_DEVICE_MS = 30_000L
92
97
  /** Interval for updating visible peer count estimate (ms) */
93
98
  private const val ADAPTIVE_PEER_COUNT_WINDOW_MS = 5_000L
99
+ /** Rate limit for unknown device GATT verification attempts (ms) */
100
+ private const val UNKNOWN_DEVICE_RATE_LIMIT_MS = 5_000L // More aggressive for cross-platform discovery
101
+ /** Minimum RSSI for unknown device to be considered for GATT verification */
102
+ private const val UNKNOWN_DEVICE_MIN_RSSI = -80 // More lenient for cross-platform discovery
103
+ /** Maximum unknown device verification attempts per minute */
104
+ private const val MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE = 10
105
+ /** Proactive scan refresh interval even when discoveries are occurring (ms) */
106
+ private const val PROACTIVE_SCAN_REFRESH_MS = 60_000L
107
+ /** Force a complete BLE stack refresh periodically even when things seem healthy (ms) */
108
+ private const val FORCED_BLE_REFRESH_MS = 120_000L
109
+ /** Maximum consecutive scan restarts before resetting BLE adapter */
110
+ private const val MAX_CONSECUTIVE_SCAN_RESTARTS = 3
111
+ /** Backoff period after resetting BLE adapter (ms) */
112
+ private const val ADAPTER_RESET_BACKOFF_MS = 45_000L
113
+ /** Connection monitor interval for periodic reconnection attempts (ms) */
114
+ private const val CONNECTION_MONITOR_INTERVAL_MS = 5_000L
115
+ /** Initial aggressive discovery phase duration (ms) - more frequent scanning initially */
116
+ private const val AGGRESSIVE_DISCOVERY_PHASE_MS = 30_000L
94
117
  }
95
118
 
96
119
  // MARK: - Properties
@@ -129,6 +152,7 @@ class BleManager(
129
152
  private data class MeshObservation(val advertisement: MeshAdvertisementData, val rssi: Int?, val timestamp: Long)
130
153
  private val pendingFragments = ConcurrentHashMap<String, MutableList<PendingFragment>>()
131
154
  private val PENDING_FRAGMENT_TIMEOUT_MS = 5000L
155
+ private val connectionRetryCount = ConcurrentHashMap<String, Int>()
132
156
  private val LOAD_SATURATION_COUNT = 20
133
157
  private val MESH_OBSERVATION_TTL_MS = 120_000L
134
158
  private val deviceIdResolutionAttempts = ConcurrentHashMap<String, Long>()
@@ -147,6 +171,23 @@ class BleManager(
147
171
  /** Last time we updated the peer count estimate */
148
172
  @Volatile private var lastPeerCountUpdate: Long = 0L
149
173
  @Volatile private var lastMeshAdvertisement: MeshAdvertisementData? = null
174
+ /** Rate limiting for unknown connectable devices that need GATT verification */
175
+ private val unknownDeviceAttempts = ConcurrentHashMap<String, Long>()
176
+ /** Last time we proactively refreshed the scan */
177
+ @Volatile private var lastProactiveScanRefresh: Long = 0L
178
+ /** Last time we performed a forced BLE refresh */
179
+ @Volatile private var lastForcedBleRefresh: Long = 0L
180
+ /** Tracks recently seen advertisements to avoid duplicate processing (hash, timestamp) */
181
+ private data class AdvertisementCacheEntry(val hash: Int, val timestamp: Long)
182
+ private val recentAdvertisementHashes = ConcurrentHashMap<String, AdvertisementCacheEntry>()
183
+ /** Tracks connected centrals (devices that connected to our GATT server) for connection count */
184
+ private val connectedCentrals = ConcurrentHashMap<String, Long>()
185
+ /** Counter for consecutive scan restarts without discoveries */
186
+ @Volatile private var scanRestartCount = 0
187
+ /** Last time we reset the BLE adapter */
188
+ @Volatile private var lastAdapterReset: Long = 0L
189
+ /** Connection monitor runnable for periodic reconnection attempts */
190
+ private var connectionMonitorRunnable: Runnable? = null
150
191
 
151
192
  // Fragment polling
152
193
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -214,13 +255,58 @@ class BleManager(
214
255
  Log.w(TAG, "Restarting BLE scan after ${idleMs}ms of inactivity")
215
256
  emitDiagnostic("warning", "Restarting BLE scan due to inactivity", mapOf("idleMs" to idleMs))
216
257
  }
258
+ scanRestartCount++
217
259
  restartScanning("watchdog")
260
+ evaluateBleHealthAfterRestart()
218
261
  return
219
262
  }
220
263
  mainHandler.postDelayed(this, SCAN_WATCHDOG_HEARTBEAT_MS)
221
264
  }
222
265
  }
223
266
 
267
+ /**
268
+ * Evaluates BLE stack health after consecutive restarts and resets adapter if needed.
269
+ * This mirrors iOS's evaluateCentralHealthAfterRestart mechanism.
270
+ */
271
+ private fun evaluateBleHealthAfterRestart() {
272
+ if (scanRestartCount < MAX_CONSECUTIVE_SCAN_RESTARTS) {
273
+ return
274
+ }
275
+
276
+ val now = System.currentTimeMillis()
277
+ if (lastAdapterReset > 0 && now - lastAdapterReset < ADAPTER_RESET_BACKOFF_MS) {
278
+ return
279
+ }
280
+
281
+ Log.w(TAG, "Resetting BLE stack due to repeated scan stalls (restartCount=$scanRestartCount)")
282
+ emitDiagnostic("warning", "Resetting BLE stack due to repeated scan stalls", mapOf(
283
+ "restartCount" to scanRestartCount
284
+ ))
285
+
286
+ lastAdapterReset = now
287
+ scanRestartCount = 0
288
+
289
+ // Force stop and restart everything
290
+ mainHandler.post {
291
+ if (state == TransportState.RUNNING) {
292
+ stopScanning("ble_reset")
293
+ stopAdvertising()
294
+
295
+ // Re-initialize scanner and advertiser
296
+ bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
297
+ bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
298
+
299
+ // Restart after a brief delay
300
+ mainHandler.postDelayed({
301
+ if (state == TransportState.RUNNING) {
302
+ startScanning("ble_reset")
303
+ startAdvertising("ble_reset")
304
+ }
305
+ }, 1000)
306
+ }
307
+ }
308
+ }
309
+
224
310
  // Metrics
225
311
  private var bytesSent: Long = 0
226
312
  private var bytesReceived: Long = 0
@@ -409,7 +495,15 @@ class BleManager(
409
495
  pendingFragments.clear()
410
496
  pendingOutboundFragments.clear()
411
497
  lastSeenMeshAdvertisements.clear()
498
+ unknownDeviceAttempts.clear()
499
+ recentAdvertisementHashes.clear()
500
+ connectionRetryCount.clear()
501
+ connectedCentrals.clear()
502
+ scanRestartCount = 0
503
+ lastAdapterReset = 0L
412
504
  transportStartAt = 0L
505
+ lastProactiveScanRefresh = 0L
506
+ lastForcedBleRefresh = 0L
413
507
 
414
508
  // Close GATT server
415
509
  gattServer?.close()
@@ -570,9 +664,11 @@ class BleManager(
570
664
  .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
571
665
  .build()
572
666
 
573
- val scanFilter = ScanFilter.Builder()
574
- .setServiceUuid(ParcelUuid(SERVICE_UUID))
575
- .build()
667
+ // CRITICAL FIX: Scan without service UUID filter for iOS ↔ Android interoperability
668
+ // iOS's CoreBluetooth has known issues recognizing 128-bit service UUIDs from Android
669
+ // advertisements, and vice versa. Scanning without filter and filtering in software
670
+ // ensures we discover all mesh devices regardless of platform quirks.
671
+ // We filter in handleScanResult using shouldProcessDiscoveredDevice().
576
672
 
577
673
  scanCallback = object : ScanCallback() {
578
674
  override fun onScanResult(callbackType: Int, result: ScanResult) {
@@ -600,14 +696,28 @@ class BleManager(
600
696
  }
601
697
  }
602
698
 
603
- scanner.startScan(listOf(scanFilter), scanSettings, scanCallback)
699
+ // Scan without filter - we'll filter in software for cross-platform compatibility
700
+ scanner.startScan(null, scanSettings, scanCallback)
604
701
  isScanning = true
605
- lastDiscoveryAt = System.currentTimeMillis()
702
+ val now = System.currentTimeMillis()
703
+ lastDiscoveryAt = now
704
+ lastProactiveScanRefresh = now
705
+ // Reset restart count on non-watchdog starts
706
+ if (reason != "restart_watchdog") {
707
+ scanRestartCount = 0
708
+ }
606
709
  scheduleScanWatchdog()
710
+ startConnectionMonitor()
607
711
  if (logThrottler.shouldLog("scan_started")) {
608
- Log.i(TAG, "BLE scanning started successfully (reason: $reason)")
609
- emitDiagnostic("info", "BLE scanning started", mapOf("reason" to reason))
712
+ Log.i(TAG, "BLE scanning started (no filter, reason: $reason)")
713
+ emitDiagnostic("info", "BLE scanning started", mapOf(
714
+ "reason" to reason,
715
+ "filterless" to true
716
+ ))
610
717
  }
718
+
719
+ // Rehydrate previously connected devices to avoid waiting for advertisements
720
+ rehydratePreviouslyConnectedDevices()
611
721
  } catch (e: SecurityException) {
612
722
  Log.e(TAG, "Permission denied while starting scan", e)
613
723
  emitDiagnostic("error", "Permission denied while starting scan", mapOf("exception" to e.javaClass.simpleName, "message" to (e.message ?: "unknown")))
@@ -615,6 +725,28 @@ class BleManager(
615
725
  }
616
726
  }
617
727
 
728
+ /**
729
+ * Attempts to reconnect to previously known devices without waiting for advertisements.
730
+ * This speeds up rediscovery after app restart or Bluetooth toggle.
731
+ */
732
+ private fun rehydratePreviouslyConnectedDevices() {
733
+ try {
734
+ val bondedDevices = bluetoothAdapter?.bondedDevices ?: return
735
+ for (device in bondedDevices) {
736
+ val address = device.address
737
+ // Only attempt if we previously had this device in our registry
738
+ if (connections.hasDeviceForAddress(address) && connections.getGatt(address) == null) {
739
+ if (logThrottler.shouldLog("rehydrate_$address", intervalMs = 30_000)) {
740
+ Log.d(TAG, "Rehydrating connection to bonded device: $address")
741
+ }
742
+ connectToDevice(device)
743
+ }
744
+ }
745
+ } catch (e: SecurityException) {
746
+ Log.w(TAG, "Cannot access bonded devices for rehydration", e)
747
+ }
748
+ }
749
+
618
750
  private fun stopScanning(reason: String = "manual") {
619
751
  if (!isScanning) return
620
752
 
@@ -623,6 +755,7 @@ class BleManager(
623
755
  scanCallback = null
624
756
  isScanning = false
625
757
  cancelScanWatchdog()
758
+ cancelConnectionMonitor()
626
759
  lastDiscoveryAt = 0L
627
760
  discoveryLogTimestamps.clear()
628
761
  if (logThrottler.shouldLog("scan_stopped")) {
@@ -649,6 +782,94 @@ class BleManager(
649
782
  mainHandler.removeCallbacks(scanWatchdogRunnable)
650
783
  }
651
784
 
785
+ /**
786
+ * Starts the connection monitor that periodically attempts to reconnect to discovered devices.
787
+ * This mirrors iOS's startConnectionMonitor mechanism for more reliable discovery.
788
+ */
789
+ private fun startConnectionMonitor() {
790
+ cancelConnectionMonitor()
791
+
792
+ connectionMonitorRunnable = object : Runnable {
793
+ override fun run() {
794
+ if (state != TransportState.RUNNING) {
795
+ return
796
+ }
797
+
798
+ val now = System.currentTimeMillis()
799
+
800
+ // Check for discovered devices that aren't connected
801
+ for ((address, observation) in lastSeenMeshAdvertisements) {
802
+ // Skip if already connected
803
+ if (connections.getGatt(address) != null) {
804
+ continue
805
+ }
806
+
807
+ // Skip if we've hit connection cap
808
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
809
+ break
810
+ }
811
+
812
+ // Skip if observation is too old
813
+ if (now - observation.timestamp > MESH_OBSERVATION_TTL_MS) {
814
+ continue
815
+ }
816
+
817
+ // Skip if RSSI too weak
818
+ val rssi = observation.rssi ?: continue
819
+ if (rssi < MINIMUM_RSSI_TO_CONNECT) {
820
+ continue
821
+ }
822
+
823
+ // Rate limit attempts to this device
824
+ val lastAttempt = deviceConnectionAttempts[address]
825
+ if (lastAttempt != null && now - lastAttempt < MIN_RECONNECT_INTERVAL_MS) {
826
+ continue
827
+ }
828
+
829
+ // Try to connect
830
+ try {
831
+ val device = bluetoothAdapter?.getRemoteDevice(address) ?: continue
832
+ recordConnectionAttempt(address, now)
833
+ connectToDevice(device)
834
+ } catch (e: Exception) {
835
+ Log.w(TAG, "Connection monitor: failed to get remote device $address", e)
836
+ }
837
+ }
838
+
839
+ // Also check for pending fragments that need device ID resolution
840
+ for (address in pendingFragments.keys) {
841
+ if (connections.deviceIdForAddress(address) != null) {
842
+ continue
843
+ }
844
+
845
+ val lastAttempt = deviceIdResolutionAttempts[address]
846
+ if (lastAttempt != null && now - lastAttempt < MIN_RECONNECT_INTERVAL_MS) {
847
+ continue
848
+ }
849
+
850
+ deviceIdResolutionAttempts[address] = now
851
+ try {
852
+ val device = bluetoothAdapter?.getRemoteDevice(address)
853
+ if (device != null && connections.getGatt(address) == null) {
854
+ connectToDevice(device)
855
+ }
856
+ } catch (e: Exception) {
857
+ Log.w(TAG, "Connection monitor: failed to resolve device ID for $address", e)
858
+ }
859
+ }
860
+
861
+ mainHandler.postDelayed(this, CONNECTION_MONITOR_INTERVAL_MS)
862
+ }
863
+ }
864
+
865
+ mainHandler.postDelayed(connectionMonitorRunnable!!, CONNECTION_MONITOR_INTERVAL_MS)
866
+ }
867
+
868
+ private fun cancelConnectionMonitor() {
869
+ connectionMonitorRunnable?.let { mainHandler.removeCallbacks(it) }
870
+ connectionMonitorRunnable = null
871
+ }
872
+
652
873
  private fun startAdvertising(reason: String = "manual") {
653
874
  if (isAdvertising) return
654
875
 
@@ -674,7 +895,7 @@ class BleManager(
674
895
 
675
896
  advertiseCallback = object : AdvertiseCallback() {
676
897
  override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
677
- Log.i(TAG, "BLE advertising started successfully (reason=$reason)")
898
+ Log.i(TAG, "BLE advertising started successfully (reason=$reason)")
678
899
  isAdvertising = true
679
900
  lastAdvertiseRestartAt = System.currentTimeMillis()
680
901
  emitDiagnostic("info", "BLE advertising started", mapOf("reason" to reason))
@@ -698,7 +919,11 @@ class BleManager(
698
919
  }
699
920
  }
700
921
 
701
- bluetoothLeAdvertiser?.startAdvertising(settings, advertiseData, advertiseCallback)
922
+ // Include scan response with service UUID for iOS compatibility
923
+ // iOS's CoreBluetooth actively queries for scan responses and has known issues
924
+ // recognizing 128-bit service UUIDs from Android's main advertisement packet
925
+ val scanResponse = buildScanResponse()
926
+ bluetoothLeAdvertiser?.startAdvertising(settings, advertiseData, scanResponse, advertiseCallback)
702
927
 
703
928
  // Reduced logging
704
929
  } catch (e: SecurityException) {
@@ -762,6 +987,21 @@ class BleManager(
762
987
  .build()
763
988
  }
764
989
 
990
+ /**
991
+ * Builds the scan response data for BLE advertising.
992
+ *
993
+ * iOS's CoreBluetooth actively queries for scan responses during BLE scanning.
994
+ * Including the service UUID in the scan response makes Android devices more
995
+ * reliably visible to iOS devices, which have known issues recognizing 128-bit
996
+ * service UUIDs from Android's main advertisement packet format.
997
+ */
998
+ private fun buildScanResponse(): AdvertiseData {
999
+ return AdvertiseData.Builder()
1000
+ .setIncludeDeviceName(false)
1001
+ .addServiceUuid(ParcelUuid(SERVICE_UUID))
1002
+ .build()
1003
+ }
1004
+
765
1005
  private fun handleScanResult(result: ScanResult) {
766
1006
  val device = result.device
767
1007
  val rssi = result.rssi
@@ -769,9 +1009,42 @@ class BleManager(
769
1009
  val now = System.currentTimeMillis()
770
1010
  lastDiscoveryAt = now
771
1011
 
1012
+ // Duplicate advertisement detection - avoid processing identical advertisements
1013
+ // This improves performance in dense networks
1014
+ val advertHash = computeAdvertisementHash(result)
1015
+ val cached = recentAdvertisementHashes[address]
1016
+ if (cached != null && cached.hash == advertHash && now - cached.timestamp < 1000L) {
1017
+ return // Skip duplicate advertisement
1018
+ }
1019
+ recentAdvertisementHashes[address] = AdvertisementCacheEntry(advertHash, now)
1020
+
1021
+ // Prune old advertisement cache entries periodically
1022
+ if (recentAdvertisementHashes.size > 100) {
1023
+ val cutoff = now - 30_000L
1024
+ val iterator = recentAdvertisementHashes.entries.iterator()
1025
+ while (iterator.hasNext()) {
1026
+ if (iterator.next().value.timestamp < cutoff) {
1027
+ iterator.remove()
1028
+ }
1029
+ }
1030
+ }
1031
+
772
1032
  // Adaptive scanning: track discoveries for density estimation
773
1033
  recordDiscoveryForDensity(now)
774
1034
 
1035
+ // CRITICAL: Software-based filtering for iOS ↔ Android interoperability
1036
+ // Since we scan without a service UUID filter, we filter here instead.
1037
+ val scanRecord = result.scanRecord
1038
+ val isConnectable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1039
+ result.isConnectable
1040
+ } else {
1041
+ true // Assume connectable on older Android
1042
+ }
1043
+
1044
+ if (!shouldProcessDiscoveredDevice(address, scanRecord, rssi, isConnectable, now)) {
1045
+ return
1046
+ }
1047
+
775
1048
  // Adaptive scanning: early RSSI filtering in dense networks
776
1049
  if (shouldFilterByRssi(rssi)) {
777
1050
  if (logThrottler.shouldLog("adaptive_rssi_filter", intervalMs = 10000)) {
@@ -786,11 +1059,6 @@ class BleManager(
786
1059
  }
787
1060
 
788
1061
  val lastLog = discoveryLogTimestamps[address]
789
- val isConnectable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
790
- result.isConnectable
791
- } else {
792
- null
793
- }
794
1062
  if (lastLog == null || now - lastLog > 30000) {
795
1063
  discoveryLogTimestamps[address] = now
796
1064
  Log.d(TAG, "Discovered device $address RSSI=$rssi (density: $estimatedVisiblePeerCount)")
@@ -800,14 +1068,13 @@ class BleManager(
800
1068
  mapOf(
801
1069
  "address" to address,
802
1070
  "rssi" to rssi,
803
- "connectable" to (isConnectable ?: true),
1071
+ "connectable" to isConnectable,
804
1072
  "visiblePeers" to estimatedVisiblePeerCount
805
1073
  )
806
1074
  )
807
1075
  }
808
1076
  lastSeenRssi[address] = rssi.toShort()
809
1077
 
810
- val scanRecord = result.scanRecord
811
1078
  val serviceData = scanRecord?.getServiceData(ParcelUuid(SERVICE_UUID))
812
1079
  val meshMetadata = MeshAdvertisementData.decode(serviceData)
813
1080
  meshMetadata?.let {
@@ -837,11 +1104,23 @@ class BleManager(
837
1104
  }
838
1105
 
839
1106
  // Adaptive scanning: rate limit connection attempts
840
- if (shouldThrottleConnection(address, now)) {
841
- if (logThrottler.shouldLog("adaptive_throttle_$address", intervalMs = 30000)) {
842
- Log.d(TAG, "Adaptive: throttling connection to $address")
1107
+ // Skip throttling for first-time discoveries with strong signals for faster connection
1108
+ val isFirstDiscovery = !lastSeenMeshAdvertisements.containsKey(address) && connections.getGatt(address) == null
1109
+ val hasStrongSignal = rssi >= -70
1110
+
1111
+ if (!isFirstDiscovery || !hasStrongSignal) {
1112
+ if (shouldThrottleConnection(address, now)) {
1113
+ if (logThrottler.shouldLog("adaptive_throttle_$address", intervalMs = 30000)) {
1114
+ Log.d(TAG, "Adaptive: throttling connection to $address")
1115
+ }
1116
+ return
843
1117
  }
844
- return
1118
+ } else if (isFirstDiscovery && hasStrongSignal) {
1119
+ Log.d(TAG, "Fast-tracking first discovery with strong signal: $address RSSI=$rssi")
1120
+ emitDiagnostic("info", "Fast-tracking first discovery", mapOf(
1121
+ "address" to address,
1122
+ "rssi" to rssi
1123
+ ))
845
1124
  }
846
1125
 
847
1126
  if (!meshController.connectionBudgetAvailable() && decision.evictPeerId != null) {
@@ -879,19 +1158,209 @@ class BleManager(
879
1158
  }
880
1159
 
881
1160
  maybeHandleRebalance("scan")
1161
+
1162
+ // Check if we should proactively refresh the scan
1163
+ maybeProactivelyRefreshScan(now)
1164
+ }
1165
+
1166
+ /**
1167
+ * Determines if a discovered device should be processed.
1168
+ * Implements smart filtering since we scan without a service UUID filter
1169
+ * (required for iOS ↔ Android interoperability).
1170
+ *
1171
+ * Accepts:
1172
+ * - Devices advertising our service UUID
1173
+ * - Devices with our service data
1174
+ * - Previously discovered mesh devices
1175
+ * - Unknown connectable devices (rate-limited, verified via GATT)
1176
+ */
1177
+ private fun shouldProcessDiscoveredDevice(
1178
+ address: String,
1179
+ scanRecord: android.bluetooth.le.ScanRecord?,
1180
+ rssi: Int,
1181
+ isConnectable: Boolean,
1182
+ now: Long
1183
+ ): Boolean {
1184
+ // 1. Check if device is advertising our service UUID
1185
+ val serviceUuids = scanRecord?.serviceUuids
1186
+ if (serviceUuids != null) {
1187
+ for (uuid in serviceUuids) {
1188
+ if (uuid.uuid == SERVICE_UUID) {
1189
+ return true
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ // 2. Check for our service data
1195
+ val serviceData = scanRecord?.getServiceData(ParcelUuid(SERVICE_UUID))
1196
+ if (serviceData != null) {
1197
+ return true
1198
+ }
1199
+
1200
+ // 3. Check if this is a previously discovered mesh device
1201
+ if (lastSeenMeshAdvertisements.containsKey(address)) {
1202
+ return true
1203
+ }
1204
+
1205
+ // 4. Check if we already have a device ID mapping for this device
1206
+ if (connections.deviceIdForAddress(address) != null) {
1207
+ return true
1208
+ }
1209
+
1210
+ // 5. Check if we have an active GATT connection to this device
1211
+ if (connections.getGatt(address) != null) {
1212
+ return true
1213
+ }
1214
+
1215
+ // 6. For unknown connectable devices, allow with rate limiting
1216
+ // These will be verified via GATT service discovery after connection
1217
+ // This is CRITICAL for iOS ↔ Android cross-platform discovery since
1218
+ // each platform may not recognize the other's service UUID format in advertisements
1219
+ if (isConnectable) {
1220
+ // Rate limit unknown device connection attempts
1221
+ val lastAttempt = unknownDeviceAttempts[address]
1222
+ if (lastAttempt != null && now - lastAttempt < UNKNOWN_DEVICE_RATE_LIMIT_MS) {
1223
+ return false
1224
+ }
1225
+
1226
+ // Check global rate limit for unknown device attempts
1227
+ val recentUnknownAttempts = unknownDeviceAttempts.values.count {
1228
+ now - it < 60_000L
1229
+ }
1230
+ if (recentUnknownAttempts >= MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE) {
1231
+ return false
1232
+ }
1233
+
1234
+ // Only process signals above minimum threshold for unknown devices
1235
+ // Use a tiered approach: stronger signals get priority
1236
+ val shouldAttempt = when {
1237
+ rssi >= -70 -> true // Strong signal - always try
1238
+ rssi >= UNKNOWN_DEVICE_MIN_RSSI && recentUnknownAttempts < MAX_UNKNOWN_DEVICE_ATTEMPTS_PER_MINUTE / 2 -> true
1239
+ else -> false
1240
+ }
1241
+
1242
+ if (shouldAttempt) {
1243
+ unknownDeviceAttempts[address] = now
1244
+ if (logThrottler.shouldLog("unknown_connectable_$address", intervalMs = 30_000)) {
1245
+ Log.d(TAG, "Allowing unknown connectable device for GATT verification: $address RSSI=$rssi")
1246
+ emitDiagnostic("debug", "Allowing unknown device for GATT verification", mapOf(
1247
+ "address" to address,
1248
+ "rssi" to rssi,
1249
+ "recentAttempts" to recentUnknownAttempts
1250
+ ))
1251
+ }
1252
+ return true
1253
+ }
1254
+ }
1255
+
1256
+ // Filter out all other devices (not our mesh network)
1257
+ return false
882
1258
  }
883
1259
 
1260
+ /**
1261
+ * Proactively refreshes the scan periodically to ensure we don't miss devices
1262
+ * due to BLE stack issues or cached state.
1263
+ */
1264
+ private fun maybeProactivelyRefreshScan(now: Long) {
1265
+ if (now - lastProactiveScanRefresh >= PROACTIVE_SCAN_REFRESH_MS) {
1266
+ lastProactiveScanRefresh = now
1267
+ if (logThrottler.shouldLog("proactive_scan_refresh", intervalMs = PROACTIVE_SCAN_REFRESH_MS)) {
1268
+ Log.d(TAG, "Proactively refreshing BLE scan")
1269
+ emitDiagnostic("info", "Proactive scan refresh")
1270
+ }
1271
+ restartScanning("proactive_refresh")
1272
+ }
1273
+
1274
+ // Forced complete BLE refresh - more aggressive than proactive refresh
1275
+ // This helps recover from edge cases where the BLE stack becomes stuck
1276
+ val lastForced = if (lastForcedBleRefresh == 0L) transportStartAt else lastForcedBleRefresh
1277
+ if (now - lastForced >= FORCED_BLE_REFRESH_MS) {
1278
+ lastForcedBleRefresh = now
1279
+ if (logThrottler.shouldLog("forced_ble_refresh", intervalMs = FORCED_BLE_REFRESH_MS)) {
1280
+ Log.i(TAG, "Performing forced BLE refresh for reliability")
1281
+ emitDiagnostic("info", "Forced BLE refresh for reliability", mapOf(
1282
+ "connectedPeers" to connections.connectionCount(),
1283
+ "discoveredPeers" to connections.discoveredPeerCount()
1284
+ ))
1285
+ }
1286
+ // Stop and restart both scanning and advertising
1287
+ stopScanning("forced_refresh")
1288
+ refreshAdvertising("forced_refresh")
1289
+ mainHandler.postDelayed({
1290
+ if (state == TransportState.RUNNING) {
1291
+ startScanning("forced_refresh")
1292
+ }
1293
+ }, 500)
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Computes a hash of the advertisement data for duplicate detection.
1299
+ * Uses device address, RSSI bucket, and key advertisement data.
1300
+ */
1301
+ private fun computeAdvertisementHash(result: ScanResult): Int {
1302
+ var hash = result.device.address.hashCode()
1303
+ // Use RSSI buckets of 5 dBm to avoid hash changes from minor signal fluctuations
1304
+ hash = 31 * hash + (result.rssi / 5)
1305
+
1306
+ val scanRecord = result.scanRecord
1307
+ if (scanRecord != null) {
1308
+ // Include service UUIDs
1309
+ scanRecord.serviceUuids?.forEach { uuid ->
1310
+ hash = 31 * hash + uuid.hashCode()
1311
+ }
1312
+
1313
+ // Include service data
1314
+ val serviceData = scanRecord.getServiceData(ParcelUuid(SERVICE_UUID))
1315
+ if (serviceData != null) {
1316
+ hash = 31 * hash + serviceData.contentHashCode()
1317
+ }
1318
+ }
1319
+
1320
+ return hash
1321
+ }
1322
+
1323
+ /** Lock for atomic connection count check and connect operations */
1324
+ private val connectionLock = Any()
1325
+
884
1326
  private fun connectToDevice(device: BluetoothDevice) {
885
1327
  try {
886
- if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
887
- if (logThrottler.shouldLog("mesh_conn_cap", intervalMs = 10000)) {
888
- Log.d(TAG, "Connection cap reached, not connecting to ${device.address}")
1328
+ // Atomic check-and-connect to prevent race conditions
1329
+ synchronized(connectionLock) {
1330
+ // Check RSSI threshold - don't connect to devices with weak signals
1331
+ val rssi = lastSeenRssi[device.address]?.toInt() ?: -60
1332
+ if (rssi < MINIMUM_RSSI_TO_CONNECT) {
1333
+ if (logThrottler.shouldLog("rssi_skip_${device.address}", intervalMs = 10000)) {
1334
+ Log.d(TAG, "Skipping connection to ${device.address} due to weak RSSI ($rssi < $MINIMUM_RSSI_TO_CONNECT)")
1335
+ emitDiagnostic("debug", "Skipping BLE connect due to weak RSSI", mapOf(
1336
+ "address" to device.address,
1337
+ "rssi" to rssi,
1338
+ "threshold" to MINIMUM_RSSI_TO_CONNECT
1339
+ ))
1340
+ }
1341
+ connections.consumePendingRole(device.address)
1342
+ return
889
1343
  }
890
- connections.consumePendingRole(device.address)
891
- return
1344
+
1345
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
1346
+ if (logThrottler.shouldLog("mesh_conn_cap", intervalMs = 10000)) {
1347
+ Log.d(TAG, "Connection cap reached, not connecting to ${device.address}")
1348
+ }
1349
+ connections.consumePendingRole(device.address)
1350
+ return
1351
+ }
1352
+
1353
+ // Double-check we don't already have a connection to this device
1354
+ if (connections.getGatt(device.address) != null) {
1355
+ if (logThrottler.shouldLog("already_connecting_${device.address}", intervalMs = 5000)) {
1356
+ Log.d(TAG, "Already have GATT client for ${device.address}")
1357
+ }
1358
+ return
1359
+ }
1360
+
1361
+ val gatt = device.connectGatt(context, false, gattClientCallback, BluetoothDevice.TRANSPORT_LE)
1362
+ connections.registerGatt(device.address, gatt)
892
1363
  }
893
- val gatt = device.connectGatt(context, false, gattClientCallback, BluetoothDevice.TRANSPORT_LE)
894
- connections.registerGatt(device.address, gatt)
895
1364
 
896
1365
  Log.i(TAG, "Connecting to device: ${device.address}")
897
1366
  emitDiagnostic("info", "Connecting to BLE device", mapOf("address" to device.address))
@@ -902,7 +1371,7 @@ class BleManager(
902
1371
  }
903
1372
  }
904
1373
 
905
- private fun currentConnectionCount(): Int = connections.connectionCount()
1374
+ private fun currentConnectionCount(): Int = connections.connectionCount() + connectedCentrals.size
906
1375
 
907
1376
  private fun refreshSelfMetrics() {
908
1377
  val rssiValues = lastSeenRssi.values.map { it.toInt() }
@@ -971,6 +1440,7 @@ class BleManager(
971
1440
  pendingFragments.remove(address)
972
1441
  pendingOutboundFragments.remove(peerId)
973
1442
  deviceIdResolutionAttempts.remove(address)
1443
+ connectionRetryCount.remove(address)
974
1444
  meshController.registerDisconnection(peerId)
975
1445
  refreshSelfMetrics()
976
1446
 
@@ -1362,6 +1832,13 @@ class BleManager(
1362
1832
 
1363
1833
  /** Checks if we should skip this device based on RSSI filtering. */
1364
1834
  private fun shouldFilterByRssi(rssi: Int): Boolean {
1835
+ // During aggressive discovery phase, don't apply density-based filtering
1836
+ val now = System.currentTimeMillis()
1837
+ if (transportStartAt > 0 && now - transportStartAt < AGGRESSIVE_DISCOVERY_PHASE_MS) {
1838
+ // Only filter out extremely weak signals during aggressive phase
1839
+ return rssi < MINIMUM_RSSI_TO_CONNECT
1840
+ }
1841
+
1365
1842
  // In dense networks, apply stricter RSSI filtering
1366
1843
  val threshold = when {
1367
1844
  estimatedVisiblePeerCount > ADAPTIVE_HIGH_DENSITY_THRESHOLD -> -70
@@ -1373,21 +1850,36 @@ class BleManager(
1373
1850
 
1374
1851
  /** Checks if we should throttle connection attempts based on rate limits. */
1375
1852
  private fun shouldThrottleConnection(address: String, now: Long): Boolean {
1853
+ // During aggressive discovery phase, use much shorter cooldowns
1854
+ val isAggressivePhase = transportStartAt > 0 && now - transportStartAt < AGGRESSIVE_DISCOVERY_PHASE_MS
1855
+
1376
1856
  // Prune old entries
1377
1857
  val oneMinuteAgo = now - 60_000L
1378
1858
  synchronized(globalConnectionAttempts) {
1379
1859
  globalConnectionAttempts.removeAll { it < oneMinuteAgo }
1380
1860
  }
1861
+
1862
+ val effectiveCooldown = if (isAggressivePhase) 5_000L else ADAPTIVE_COOLDOWN_PER_DEVICE_MS
1381
1863
  deviceConnectionAttempts.entries.removeIf {
1382
- now - it.value >= ADAPTIVE_COOLDOWN_PER_DEVICE_MS
1864
+ now - it.value >= effectiveCooldown
1383
1865
  }
1384
1866
 
1385
1867
  // Check per-device cooldown
1386
1868
  val lastAttempt = deviceConnectionAttempts[address]
1387
- if (lastAttempt != null && now - lastAttempt < ADAPTIVE_COOLDOWN_PER_DEVICE_MS) {
1869
+ if (lastAttempt != null && now - lastAttempt < effectiveCooldown) {
1388
1870
  return true
1389
1871
  }
1390
1872
 
1873
+ // During aggressive phase, allow more connection attempts
1874
+ if (isAggressivePhase) {
1875
+ // Allow up to 3x the normal rate during aggressive phase
1876
+ val maxAttempts = ADAPTIVE_MAX_CONNECTIONS_PER_MINUTE * 3
1877
+ if (globalConnectionAttempts.size >= maxAttempts) {
1878
+ return true
1879
+ }
1880
+ return false
1881
+ }
1882
+
1391
1883
  // In dense networks, apply global rate limiting
1392
1884
  if (estimatedVisiblePeerCount > ADAPTIVE_LOW_DENSITY_THRESHOLD) {
1393
1885
  val currentAttempts = globalConnectionAttempts.size
@@ -1518,12 +2010,19 @@ class BleManager(
1518
2010
  gattServer?.cancelConnection(device)
1519
2011
  return
1520
2012
  }
2013
+ // Check connection capacity
2014
+ if (currentConnectionCount() >= MAX_CONNECTIONS_PER_DEVICE) {
2015
+ Log.w(TAG, "Rejecting inbound connection from ${device.address}: connection cap reached")
2016
+ gattServer?.cancelConnection(device)
2017
+ return
2018
+ }
1521
2019
  val role = when (decision.intent) {
1522
2020
  ConnectionIntent.INTER_CLUSTER -> MeshRole.BRIDGE
1523
2021
  ConnectionIntent.INTRA_CLUSTER, ConnectionIntent.REJECTED -> MeshRole.MEMBER
1524
2022
  }
1525
2023
  connections.trackServerConnection(device.address)
1526
2024
  connections.setPendingRole(device.address, role)
2025
+ connectedCentrals[device.address] = System.currentTimeMillis()
1527
2026
  Log.i(TAG, "GATT server: Device connected: ${device.address} (role=$role)")
1528
2027
  emitDiagnostic("info", "Device connected to GATT server", mapOf("address" to device.address))
1529
2028
  }
@@ -1531,6 +2030,7 @@ class BleManager(
1531
2030
  val address = device.address
1532
2031
  connections.untrackServerConnection(address)
1533
2032
  connections.consumePendingRole(address)
2033
+ connectedCentrals.remove(address)
1534
2034
  // Don't immediately remove - connection might be re-established
1535
2035
  // Only remove if it's a permanent error (status != 0)
1536
2036
  if (status != 0 && status != 19) { // Not normal disconnect or connection timeout
@@ -1657,7 +2157,43 @@ class BleManager(
1657
2157
 
1658
2158
  // Try to reconnect if we were connected and state is still running
1659
2159
  if (wasConnected && state == TransportState.RUNNING) {
1660
- // Attempt reconnection after a short delay
2160
+ // Increment retry count and calculate backoff
2161
+ val retryCount = (connectionRetryCount[address] ?: 0) + 1
2162
+ connectionRetryCount[address] = retryCount
2163
+
2164
+ // Give up after max retries
2165
+ if (retryCount > MAX_CONNECTION_RETRIES) {
2166
+ Log.w(TAG, "Max retries ($MAX_CONNECTION_RETRIES) exceeded for $address on disconnect, giving up")
2167
+ emitDiagnostic("warning", "Max connection retries exceeded", mapOf(
2168
+ "address" to address,
2169
+ "retryCount" to retryCount
2170
+ ))
2171
+ connectionRetryCount.remove(address)
2172
+ // Notify peer lost since we're giving up
2173
+ connections.deviceIdForAddress(address)?.let { peerId ->
2174
+ protocol.removeNeighborRoutes(peerId)
2175
+ try {
2176
+ protocol.blePeerLost(peerId)
2177
+ } catch (e: Exception) {
2178
+ Log.e(TAG, "Error notifying peer lost", e)
2179
+ }
2180
+ meshController.registerDisconnection(peerId)
2181
+ refreshSelfMetrics()
2182
+ connections.removeIdentifiersForAddress(address)
2183
+ connections.removeConnectionRole(peerId)
2184
+ mainHandler.post {
2185
+ if (state == TransportState.RUNNING) {
2186
+ refreshAdvertising("disconnect_max_retries")
2187
+ }
2188
+ }
2189
+ maybeHandleRebalance("disconnect_max_retries")
2190
+ }
2191
+ return@onConnectionStateChange
2192
+ }
2193
+
2194
+ // Exponential backoff: 5s, 10s, 20s, 40s, 60s (capped)
2195
+ val backoffInterval = minOf(MAX_RECONNECT_INTERVAL_MS, MIN_RECONNECT_INTERVAL_MS * (1L shl (retryCount - 1)))
2196
+
1661
2197
  mainHandler.postDelayed({
1662
2198
  if (state == TransportState.RUNNING && connections.hasDeviceForAddress(address)) {
1663
2199
  try {
@@ -1669,7 +2205,7 @@ class BleManager(
1669
2205
  Log.e(TAG, "Error reconnecting to device", e)
1670
2206
  }
1671
2207
  }
1672
- }, 1000)
2208
+ }, backoffInterval)
1673
2209
  } else {
1674
2210
  // Notify protocol of peer loss only if we're not reconnecting
1675
2211
  connections.deviceIdForAddress(address)?.let { deviceId ->
@@ -1760,6 +2296,7 @@ class BleManager(
1760
2296
  if (deviceIdValue != null) {
1761
2297
  Log.i(TAG, "📝 Mapping ${gatt.device.address} -> $deviceIdValue")
1762
2298
  connections.setDeviceIdentifier(gatt.device.address, deviceIdValue)
2299
+ connectionRetryCount.remove(gatt.device.address) // Reset retry count on successful connection
1763
2300
 
1764
2301
  val role = connections.consumePendingRole(gatt.device.address) ?: MeshRole.MEMBER
1765
2302
  meshController.registerConnection(deviceIdValue, role)