@mentra/bluetooth-sdk 0.1.0

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 (278) hide show
  1. package/README.md +278 -0
  2. package/android/build.gradle +167 -0
  3. package/android/lc3Lib/CMakeLists.txt +6 -0
  4. package/android/lc3Lib/build.gradle +109 -0
  5. package/android/lc3Lib/proguard-rules.pro +33 -0
  6. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  7. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  8. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  26. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  81. package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
  82. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  83. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  86. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  119. package/android/settings.gradle +1 -0
  120. package/android/src/main/AndroidManifest.xml +40 -0
  121. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
  122. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
  123. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
  124. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
  125. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
  126. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
  127. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
  128. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  129. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
  130. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
  131. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
  132. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
  133. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
  134. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
  135. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
  136. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
  137. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
  138. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
  139. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
  140. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
  141. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
  142. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
  143. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
  144. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  145. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  146. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
  147. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
  148. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
  149. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  150. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
  151. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
  152. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
  153. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
  154. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
  155. package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  156. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  157. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  158. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  159. package/android/src/main/res/values/strings.xml +4 -0
  160. package/app.plugin.js +1 -0
  161. package/build/BluetoothSdk.types.d.ts +498 -0
  162. package/build/BluetoothSdk.types.d.ts.map +1 -0
  163. package/build/BluetoothSdk.types.js +23 -0
  164. package/build/BluetoothSdk.types.js.map +1 -0
  165. package/build/BluetoothSdkModule.d.ts +71 -0
  166. package/build/BluetoothSdkModule.d.ts.map +1 -0
  167. package/build/BluetoothSdkModule.js +106 -0
  168. package/build/BluetoothSdkModule.js.map +1 -0
  169. package/build/index.d.ts +3 -0
  170. package/build/index.d.ts.map +1 -0
  171. package/build/index.js +5 -0
  172. package/build/index.js.map +1 -0
  173. package/expo-module.config.json +9 -0
  174. package/ios/BluetoothSdkModule.swift +665 -0
  175. package/ios/MentraBluetoothSDK.podspec +77 -0
  176. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  177. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  178. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  179. package/ios/Packages/CoreObjC/attdet.c +92 -0
  180. package/ios/Packages/CoreObjC/attdet.h +44 -0
  181. package/ios/Packages/CoreObjC/bits.c +375 -0
  182. package/ios/Packages/CoreObjC/bits.h +315 -0
  183. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  184. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  185. package/ios/Packages/CoreObjC/common.h +151 -0
  186. package/ios/Packages/CoreObjC/energy.c +70 -0
  187. package/ios/Packages/CoreObjC/energy.h +43 -0
  188. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  189. package/ios/Packages/CoreObjC/lc3.c +704 -0
  190. package/ios/Packages/CoreObjC/lc3.h +313 -0
  191. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  192. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  193. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  194. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  195. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  196. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  197. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  198. package/ios/Packages/CoreObjC/mdct.c +469 -0
  199. package/ios/Packages/CoreObjC/mdct.h +57 -0
  200. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  201. package/ios/Packages/CoreObjC/meson.build +61 -0
  202. package/ios/Packages/CoreObjC/plc.c +61 -0
  203. package/ios/Packages/CoreObjC/plc.h +57 -0
  204. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  205. package/ios/Packages/CoreObjC/sns.c +880 -0
  206. package/ios/Packages/CoreObjC/sns.h +103 -0
  207. package/ios/Packages/CoreObjC/spec.c +907 -0
  208. package/ios/Packages/CoreObjC/spec.h +119 -0
  209. package/ios/Packages/CoreObjC/tables.c +3457 -0
  210. package/ios/Packages/CoreObjC/tables.h +94 -0
  211. package/ios/Packages/CoreObjC/tns.c +457 -0
  212. package/ios/Packages/CoreObjC/tns.h +99 -0
  213. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  214. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  215. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
  216. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
  217. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
  218. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
  219. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
  220. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  221. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  222. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  223. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  224. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
  225. package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
  226. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
  227. package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
  228. package/ios/Packages/VAD/Data/Configuration.swift +68 -0
  229. package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
  230. package/ios/Packages/VAD/Data/Record.swift +13 -0
  231. package/ios/Packages/VAD/Data/Result.swift +22 -0
  232. package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
  233. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  234. package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
  235. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
  236. package/ios/Packages/libbz2/module.modulemap +5 -0
  237. package/ios/Packages/libbz2/shim.h +1 -0
  238. package/ios/Source/Bridge.swift +353 -0
  239. package/ios/Source/Bridging-Header.h +16 -0
  240. package/ios/Source/DeviceManager.swift +1478 -0
  241. package/ios/Source/DeviceStore.swift +295 -0
  242. package/ios/Source/MentraBluetoothSDK.swift +2439 -0
  243. package/ios/Source/ObservableStore.swift +88 -0
  244. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  245. package/ios/Source/controllers/ControllerManager.swift +187 -0
  246. package/ios/Source/controllers/R1.swift +827 -0
  247. package/ios/Source/services/PhoneMic.swift +662 -0
  248. package/ios/Source/sgcs/Frame.swift +651 -0
  249. package/ios/Source/sgcs/G1.swift +2505 -0
  250. package/ios/Source/sgcs/G2.swift +3730 -0
  251. package/ios/Source/sgcs/Mach1.swift +543 -0
  252. package/ios/Source/sgcs/MentraLive.swift +4708 -0
  253. package/ios/Source/sgcs/MentraNex.swift +2479 -0
  254. package/ios/Source/sgcs/SGCManager.swift +243 -0
  255. package/ios/Source/sgcs/Simulated.swift +270 -0
  256. package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
  257. package/ios/Source/stt/STTTools.swift +154 -0
  258. package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
  259. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  260. package/ios/Source/utils/Constants.swift +74 -0
  261. package/ios/Source/utils/Enums.swift +95 -0
  262. package/ios/Source/utils/G1Text.swift +2067 -0
  263. package/ios/Source/utils/JSCExperiment.swift +241 -0
  264. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  265. package/ios/Source/utils/MessageChunker.swift +164 -0
  266. package/ios/Source/utils/Models.swift +135 -0
  267. package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
  268. package/ios/Source/utils/TarBz2Extractor.swift +206 -0
  269. package/package.json +78 -0
  270. package/plugin/build/index.d.ts +6 -0
  271. package/plugin/build/index.js +12 -0
  272. package/plugin/build/withAndroid.d.ts +4 -0
  273. package/plugin/build/withAndroid.js +80 -0
  274. package/plugin/build/withIos.d.ts +4 -0
  275. package/plugin/build/withIos.js +64 -0
  276. package/src/BluetoothSdk.types.ts +581 -0
  277. package/src/BluetoothSdkModule.ts +259 -0
  278. package/src/index.ts +4 -0
@@ -0,0 +1,2479 @@
1
+ //
2
+ // MentraNex.swift
3
+ // MentraOS_Manager
4
+ //
5
+ // Created by Gemini on 2024-07-29.
6
+ //
7
+
8
+ import Combine
9
+ import CoreBluetooth
10
+ import Foundation
11
+ import SwiftProtobuf
12
+ import UIKit
13
+
14
+ /// Helper extension for debugging
15
+ extension Data {
16
+ func toHexString() -> String {
17
+ map { String(format: "%02x", $0) }.joined(separator: " ")
18
+ }
19
+ }
20
+
21
+ /// Nex firmware expects tier 1–3 in protobuf `DisplayDistanceConfig.distance_cm` (name is legacy, not cm).
22
+ /// Keep in sync with `NexProtobufUtils.dashboardDepthToDistanceCm` (Android `NexSGCUtils.kt`).
23
+ enum NexDashboardDisplayWire {
24
+ static let depthMin = 1
25
+ static let depthMax = 3
26
+
27
+ static func depthToWireTier(_ depth: Int) -> UInt32 {
28
+ UInt32(min(max(depth, depthMin), depthMax))
29
+ }
30
+
31
+ /// Read an `Any?` value from the store, default to `depthMin`, and clamp to valid range.
32
+ static func clampDepthFromStore(_ value: Any?) -> Int {
33
+ let raw = value as? Int ?? depthMin
34
+ return min(max(raw, depthMin), depthMax)
35
+ }
36
+ }
37
+
38
+ @MainActor
39
+ @objc(MentraNexSGC)
40
+ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SGCManager {
41
+ func sendIncidentId(_: String, apiBaseUrl _: String?) {}
42
+
43
+ func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
44
+
45
+ func setMicEnabled(_: Bool) {}
46
+
47
+ func requestPhoto(
48
+ _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
49
+ compress _: String?, flash _: Bool, sound _: Bool
50
+ ) {}
51
+
52
+ func startStream(_: [String: Any]) {}
53
+
54
+ func stopStream() {}
55
+
56
+ func sendStreamKeepAlive(_: [String: Any]) {}
57
+
58
+ func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
59
+
60
+ func stopVideoRecording(requestId _: String) {}
61
+
62
+ func sendButtonPhotoSettings() {}
63
+
64
+ func sendButtonVideoRecordingSettings() {}
65
+
66
+ func sendButtonMaxRecordingTime() {}
67
+
68
+ func sendButtonCameraLedSetting() {}
69
+
70
+ func sendCameraFovSetting() {}
71
+
72
+ func setBrightness(_: Int, autoMode _: Bool) {}
73
+
74
+ func sendDoubleTextWall(_: String, _: String) {}
75
+
76
+ func displayBitmap(base64ImageData _: String) async -> Bool {
77
+ return false
78
+ }
79
+
80
+ func showDashboard() {}
81
+
82
+ func setDashboardPosition(_ height: Int, _ depth: Int) {
83
+ // Same order as Android MentraNex: display_height then display_distance.
84
+ updateGlassesDisplayHeight(height)
85
+ updateGlassesDisplayDistance(depth: depth)
86
+ }
87
+
88
+ func setDashboardHeightOnly(_ height: Int) {
89
+ updateGlassesDisplayHeight(height)
90
+ }
91
+
92
+ func setDashboardDepthOnly(_ depth: Int) {
93
+ updateGlassesDisplayDistance(depth: depth)
94
+ }
95
+
96
+ func setHeadUpAngle(_: Int) {}
97
+
98
+ func getBatteryStatus() {}
99
+
100
+ func setSilentMode(_: Bool) {}
101
+
102
+ func exit() {}
103
+
104
+ func sendShutdown() {}
105
+
106
+ func sendReboot() {}
107
+
108
+ func sendRgbLedControl(
109
+ requestId _: String, packageName _: String?, action _: String, color _: String?, ontime _: Int,
110
+ offtime _: Int, count _: Int
111
+ ) {}
112
+
113
+ func forget() {}
114
+
115
+ func connectById(_: String) {}
116
+
117
+ func getConnectedBluetoothName() -> String? {
118
+ return nil
119
+ }
120
+
121
+ func cleanup() {}
122
+
123
+ func ping() {}
124
+ func connectController() {}
125
+ func disconnectController() {}
126
+
127
+ func dbg1() {}
128
+ func dbg2() {}
129
+
130
+ func requestWifiScan() {}
131
+
132
+ func sendWifiCredentials(_: String, _: String) {}
133
+
134
+ func forgetWifiNetwork(_: String) {}
135
+
136
+ func sendHotspotState(_: Bool) {}
137
+
138
+ func sendOtaStart() {}
139
+ func sendOtaQueryStatus() {}
140
+
141
+ func sendUserEmailToGlasses(_: String) {}
142
+
143
+ func queryGalleryStatus() {}
144
+
145
+ @objc static func requiresMainQueueSetup() -> Bool {
146
+ true
147
+ }
148
+
149
+ func sendGalleryMode() {}
150
+
151
+ func requestVersionInfo() {
152
+ Bridge.log("MentraNex: requestVersionInfo - not supported on MentraNex")
153
+ }
154
+
155
+ // MARK: - Properties
156
+
157
+ private var centralManager: CBCentralManager?
158
+
159
+ private var peripheral: CBPeripheral?
160
+ private var writeCharacteristic: CBCharacteristic?
161
+ private var notifyCharacteristic: CBCharacteristic?
162
+ private var _isScanning = false
163
+ private var isConnecting = false
164
+ private var nexReady = false
165
+ private var isDisconnecting = false
166
+ private var reconnectionTimer: Timer?
167
+ private var reconnectionAttempts = 0
168
+ // TODO: change this
169
+ private let maxReconnectionAttempts = -1 // -1 for unlimited
170
+ private let reconnectionInterval: TimeInterval = 5.0 // 5 seconds
171
+ private var peripheralToConnectName: String?
172
+
173
+ // Heartbeat tracking (like Java implementation)
174
+ private var heartbeatCount = 0
175
+ private var lastHeartbeatSentTime: TimeInterval = 0
176
+ private var lastHeartbeatReceivedTime: TimeInterval = 0
177
+
178
+ // Microphone beat system (like Java implementation)
179
+ private var micBeatTimer: Timer?
180
+ private var micBeatCount = 0
181
+ private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes like Java
182
+ private var shouldUseGlassesMic = true
183
+ private var microphoneStateBeforeDisconnection = false
184
+
185
+ // Whitelist system (like Java implementation)
186
+ private var whiteListedAlready = false
187
+ private let WHITELIST_CMD: UInt8 = 0x04
188
+
189
+ /// Protobuf version tracking (like Java implementation)
190
+ private var protobufVersionPosted = false
191
+
192
+ /// Device discovery cache (like MentraLive)
193
+ private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
194
+
195
+ // MARK: - Published Properties (G1-compatible)
196
+
197
+ @Published var vadActive: Bool = false
198
+ @Published var deviceReady: Bool = false
199
+
200
+ // Audio properties (G1-compatible)
201
+ @Published var compressedVoiceData: Data = .init()
202
+ @Published var aiListening: Bool = false
203
+
204
+ // Device info properties
205
+ @Published var deviceFirmwareVersion: String = ""
206
+ @Published var deviceHardwareModel: String = ""
207
+
208
+ // IMU data properties
209
+ @Published var accelerometer: [Float] = [0.0, 0.0, 0.0]
210
+ @Published var gyroscope: [Float] = [0.0, 0.0, 0.0]
211
+ @Published var magnetometer: [Float] = [0.0, 0.0, 0.0]
212
+
213
+ // Button state properties
214
+ @Published var lastButtonPressed: Int = -1
215
+ @Published var lastButtonState: String = ""
216
+
217
+ // Head gesture properties
218
+ @Published var lastHeadGesture: String = ""
219
+ @Published var headUpAngle: Int = 0
220
+
221
+ // Enhanced device persistence (from Java implementation)
222
+ private let PREFS_DEVICE_NAME = "MentraNexLastConnectedDeviceName"
223
+ private let PREFS_DEVICE_ADDRESS = "MentraNexLastConnectedDeviceAddress"
224
+ private let PREFS_DEVICE_ID = "SavedNexIdKey"
225
+ private let SHARED_PREFS_NAME = "NexGlassesPrefs"
226
+
227
+ // Device state tracking (ported from Java)
228
+ private var savedDeviceName: String?
229
+ private var savedDeviceAddress: String?
230
+ private var preferredDeviceId: String?
231
+ private var isKilled = false
232
+ private var scanOnPowerOn = false
233
+
234
+ private let bluetoothQueue = DispatchQueue(label: "MentraNexBluetooth", qos: .userInitiated)
235
+
236
+ /// Protocol-required connectionState (String)
237
+ var connectionState: String = ConnTypes.DISCONNECTED
238
+
239
+ // Protocol-required properties
240
+ var type: String = DeviceTypes.NEX
241
+ var ready: Bool {
242
+ get { nexReady }
243
+ set { nexReady = newValue }
244
+ }
245
+
246
+ var hasMic: Bool = true
247
+
248
+ private var peripheralUUID: UUID? {
249
+ get {
250
+ if let uuidString = UserDefaults.standard.string(forKey: "nexPeripheralUUID") {
251
+ return UUID(uuidString: uuidString)
252
+ }
253
+ return nil
254
+ }
255
+ set {
256
+ if let newValue {
257
+ UserDefaults.standard.set(newValue.uuidString, forKey: "nexPeripheralUUID")
258
+ } else {
259
+ UserDefaults.standard.removeObject(forKey: "nexPeripheralUUID")
260
+ }
261
+ }
262
+ }
263
+
264
+ /// Custom Bluetooth queue for better performance (like G1)
265
+ private static let _bluetoothQueue = DispatchQueue(
266
+ label: "com.mentra.nex.bluetooth", qos: .background
267
+ )
268
+
269
+ static var instance: MentraNexSGC?
270
+
271
+ // MARK: - Singleton Access
272
+
273
+ @objc static func getInstance() -> MentraNexSGC {
274
+ if instance == nil {
275
+ instance = MentraNexSGC()
276
+ }
277
+ return instance!
278
+ }
279
+
280
+ // UUIDs from MentraNexSGC.java
281
+ private let MAIN_SERVICE_UUID = CBUUID(string: "00004860-0000-1000-8000-00805f9b34fb")
282
+ private let WRITE_CHAR_UUID = CBUUID(string: "000071FF-0000-1000-8000-00805f9b34fb")
283
+ private let NOTIFY_CHAR_UUID = CBUUID(string: "000070FF-0000-1000-8000-00805f9b34fb")
284
+
285
+ // Packet types from MentraNexSGC.java
286
+ private let PACKET_TYPE_JSON: UInt8 = 0x01
287
+ private let PACKET_TYPE_PROTOBUF: UInt8 = 0x02
288
+ private let PACKET_TYPE_AUDIO: UInt8 = 0xA0
289
+ private let PACKET_TYPE_IMAGE: UInt8 = 0xB0
290
+
291
+ // MTU Configuration (iOS-optimized)
292
+ private let MTU_MAX_IOS = 185 // iOS maximum (platform limitation)
293
+ private let MTU_DEFAULT = 23 // Default BLE MTU
294
+ private var currentMTU = 23 // Currently negotiated MTU
295
+ private var deviceMaxMTU = 23 // Device's maximum capability
296
+ private var maxChunkSize = 176 // Calculated optimal chunk size
297
+ private var bmpChunkSize = 176 // Image chunk size (iOS-optimized)
298
+
299
+ // MARK: - Command Queue (modeled after ERG1Manager)
300
+
301
+ private struct BufferedCommand {
302
+ let chunks: [[UInt8]]
303
+ let waitTimeMs: Int
304
+ let chunkDelayMs: Int
305
+
306
+ init(chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int = 8) {
307
+ self.chunks = chunks
308
+ self.waitTimeMs = waitTimeMs
309
+ self.chunkDelayMs = chunkDelayMs
310
+ }
311
+ }
312
+
313
+ private actor CommandQueue {
314
+ private var commands: [BufferedCommand] = []
315
+ private var continuations: [CheckedContinuation<BufferedCommand, Never>] = []
316
+
317
+ func enqueue(_ command: BufferedCommand) {
318
+ if let continuation = continuations.first {
319
+ continuations.removeFirst()
320
+ continuation.resume(returning: command)
321
+ } else {
322
+ commands.append(command)
323
+ }
324
+ }
325
+
326
+ func dequeue() async -> BufferedCommand {
327
+ if let command = commands.first {
328
+ commands.removeFirst()
329
+ return command
330
+ }
331
+
332
+ return await withCheckedContinuation { continuation in
333
+ continuations.append(continuation)
334
+ }
335
+ }
336
+ }
337
+
338
+ private let commandQueue = CommandQueue()
339
+ private var isQueueWorkerRunning = false
340
+
341
+ // MARK: - Initialization
342
+
343
+ override private init() {
344
+ super.init()
345
+ Bridge.log("NEX: 🚀 MentraNexSGC initialization started")
346
+
347
+ // Load saved device information (from Java implementation)
348
+ loadSavedDeviceInfo()
349
+
350
+ // Using custom Bluetooth queue for better performance (like G1)
351
+ Bridge.log("NEX: 📱 Creating CBCentralManager with custom Bluetooth queue")
352
+ centralManager = CBCentralManager(delegate: self, queue: MentraNexSGC._bluetoothQueue)
353
+
354
+ Bridge.log("NEX: ✅ MentraNexSGC initialization completed")
355
+ Bridge.log("NEX: 📱 Central Manager created: \(centralManager != nil ? "YES" : "NO")")
356
+ if let centralManager {
357
+ Bridge.log("NEX: 📱 Initial Bluetooth State: \(centralManager.state.rawValue)")
358
+ }
359
+
360
+ Bridge.log(
361
+ "NEX: 💾 Loaded saved device - Name: \(savedDeviceName ?? "None"), Address: \(savedDeviceAddress ?? "None")"
362
+ )
363
+ }
364
+
365
+ private func setupCommandQueue() {
366
+ if isQueueWorkerRunning { return }
367
+ isQueueWorkerRunning = true
368
+
369
+ Task.detached { [weak self] in
370
+ guard let self else { return }
371
+ while true {
372
+ let command = await self.commandQueue.dequeue()
373
+ await self.processCommand(command)
374
+ }
375
+ }
376
+ }
377
+
378
+ private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int = 8) {
379
+ let cmd = BufferedCommand(
380
+ chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: chunkDelayMs
381
+ )
382
+ Task { [weak self] in
383
+ await self?.commandQueue.enqueue(cmd)
384
+ }
385
+ }
386
+
387
+ /// Enhanced method that uses MTU-optimized chunking
388
+ private func queueDataWithOptimalChunking(
389
+ _ data: Data, packetType: UInt8 = 0x02, waitTimeMs: Int = 0
390
+ ) {
391
+ var chunks: [[UInt8]] = []
392
+ let effectiveChunkSize = maxChunkSize - 1 // Reserve 1 byte for packet type
393
+
394
+ // Add packet type as first byte
395
+ var packetData = Data([packetType])
396
+ packetData.append(data)
397
+
398
+ // Split into MTU-optimized chunks
399
+ var offset = 0
400
+ while offset < packetData.count {
401
+ let chunkSize = min(effectiveChunkSize, packetData.count - offset)
402
+ let chunkData = packetData.subdata(in: offset ..< (offset + chunkSize))
403
+ chunks.append(Array(chunkData))
404
+ offset += chunkSize
405
+ }
406
+
407
+ Bridge.log(
408
+ "NEX: 📦 Created \(chunks.count) MTU-optimized chunks (max size: \(effectiveChunkSize) bytes)"
409
+ )
410
+ queueChunks(chunks, waitTimeMs: waitTimeMs)
411
+ }
412
+
413
+ /// Helper method for queueing chunks with optional wait time
414
+ private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0) {
415
+ let cmd = BufferedCommand(chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: 8)
416
+ Task { [weak self] in
417
+ await self?.commandQueue.enqueue(cmd)
418
+ }
419
+ }
420
+
421
+ private func processCommand(_ command: BufferedCommand) async {
422
+ guard let peripheral, let writeCharacteristic else {
423
+ Bridge.log("NEX: ⚠️ processCommand: peripheral/characteristic not ready")
424
+ return
425
+ }
426
+
427
+ // Send each chunk sequentially
428
+ for (index, chunk) in command.chunks.enumerated() {
429
+ let data = Data(chunk)
430
+ Bridge.log(
431
+ "NEX: 📦 Sending chunk \(index) of \(command.chunks.count) to \(peripheral.name ?? "Unknown")"
432
+ )
433
+ Bridge.log("NEX: 📦 Chunk data: \(data.toHexString())")
434
+ peripheral.writeValue(data, for: writeCharacteristic, type: .withResponse)
435
+
436
+ // Delay between chunks except maybe after the last chunk if waitTime will handle it
437
+ if index < command.chunks.count - 1 {
438
+ try? await Task.sleep(nanoseconds: UInt64(command.chunkDelayMs) * 1_000_000)
439
+ }
440
+ }
441
+
442
+ // Optional wait after the command
443
+ if command.waitTimeMs > 0 {
444
+ try? await Task.sleep(nanoseconds: UInt64(command.waitTimeMs) * 1_000_000)
445
+ }
446
+ }
447
+
448
+ // MARK: - Device Persistence (ported from Java)
449
+
450
+ private func loadSavedDeviceInfo() {
451
+ savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
452
+ savedDeviceAddress = UserDefaults.standard.string(forKey: PREFS_DEVICE_ADDRESS)
453
+ preferredDeviceId = UserDefaults.standard.string(forKey: PREFS_DEVICE_ID)
454
+
455
+ Bridge.log(
456
+ "NEX: 💾 Loaded device info - Name: \(savedDeviceName ?? "None"), Address: \(savedDeviceAddress ?? "None"), ID: \(preferredDeviceId ?? "None")"
457
+ )
458
+ }
459
+
460
+ private func savePairedDeviceInfo(name: String?, address: String?) {
461
+ if let name {
462
+ UserDefaults.standard.set(name, forKey: PREFS_DEVICE_NAME)
463
+ savedDeviceName = name
464
+ Bridge.log("NEX: 💾 Saved device name: \(name)")
465
+ }
466
+
467
+ if let address {
468
+ UserDefaults.standard.set(address, forKey: PREFS_DEVICE_ADDRESS)
469
+ savedDeviceAddress = address
470
+ Bridge.log("NEX: 💾 Saved device address: \(address)")
471
+ }
472
+ }
473
+
474
+ @objc func savePreferredDeviceId(_ deviceId: String) {
475
+ UserDefaults.standard.set(deviceId, forKey: PREFS_DEVICE_ID)
476
+ preferredDeviceId = deviceId
477
+ Bridge.log("NEX: 💾 Saved preferred device ID: \(deviceId)")
478
+ }
479
+
480
+ @objc func clearSavedDeviceInfo() {
481
+ UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
482
+ UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_ADDRESS)
483
+ UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_ID)
484
+
485
+ savedDeviceName = nil
486
+ savedDeviceAddress = nil
487
+ preferredDeviceId = nil
488
+ peripheralUUID = nil
489
+
490
+ Bridge.log("NEX: 🗑️ Cleared all saved device information")
491
+ }
492
+
493
+ // MARK: - Enhanced Device Filtering (ported from Java)
494
+
495
+ private func isCompatibleNexDevice(_ deviceName: String) -> Bool {
496
+ // Enhanced filtering logic from Java implementation
497
+ let compatiblePrefixes = [
498
+ "NexSim",
499
+ // "MENTRA",
500
+ // "NEX",
501
+ // "Nex",
502
+ // "MentraNex",
503
+ // "MENTRA_NEX",
504
+ // "Xy_A", // Legacy support
505
+ // "XyBLE_", // Legacy support
506
+ // "MENTRA_LIVE", // Cross-compatibility
507
+ ]
508
+
509
+ for prefix in compatiblePrefixes {
510
+ if deviceName.hasPrefix(prefix) || deviceName.contains(prefix) {
511
+ Bridge.log("NEX: ✅ Device '\(deviceName)' matches compatible prefix: \(prefix)")
512
+ return true
513
+ }
514
+ }
515
+
516
+ return false
517
+ }
518
+
519
+ private func extractDeviceId(from deviceName: String) -> String? {
520
+ // Extract device ID pattern similar to Java implementation
521
+ let patterns = [
522
+ "Mentra_([0-9A-Fa-f]+)",
523
+ "NEX_([0-9A-Fa-f]+)",
524
+ "MENTRA_NEX_([0-9A-Fa-f]+)",
525
+ ]
526
+
527
+ for pattern in patterns {
528
+ let regex = try? NSRegularExpression(pattern: pattern)
529
+ let range = NSRange(deviceName.startIndex ..< deviceName.endIndex, in: deviceName)
530
+ if let match = regex?.firstMatch(in: deviceName, options: [], range: range),
531
+ let matchRange = Range(match.range(at: 1), in: deviceName)
532
+ {
533
+ let deviceId = String(deviceName[matchRange])
534
+ Bridge.log("NEX: 🏷️ Extracted device ID: \(deviceId) from \(deviceName)")
535
+ return deviceId
536
+ }
537
+ }
538
+
539
+ Bridge.log("NEX: ⚠️ Could not extract device ID from: \(deviceName)")
540
+ return nil
541
+ }
542
+
543
+ // MARK: - Connection Logic (enhanced from G1)
544
+
545
+ @objc(connectByName:)
546
+ func connect(name: String) {
547
+ Bridge.log("NEX-CONN: 🔗 connect(name:) called with \(name)")
548
+ if _isScanning {
549
+ stopScan()
550
+ }
551
+ peripheralToConnectName = name
552
+ startScan()
553
+ }
554
+
555
+ private func connectByUUID() -> Bool {
556
+ guard let uuid = peripheralUUID else {
557
+ Bridge.log("NEX-CONN: 🔵 No stored UUID to connect by.")
558
+ return false
559
+ }
560
+
561
+ guard let centralManager else {
562
+ Bridge.log("NEX-CONN: ❌ Central Manager is nil, cannot connect by UUID.")
563
+ return false
564
+ }
565
+
566
+ Bridge.log(
567
+ "NEX-CONN: 🔵 Attempting to retrieve peripheral with stored UUID: \(uuid.uuidString)"
568
+ )
569
+ let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
570
+
571
+ if let peripheralToConnect = peripherals.first {
572
+ Bridge.log(
573
+ "NEX-CONN: 🔵 Found peripheral by UUID: \(peripheralToConnect.name ?? "Unknown"). Initiating connection."
574
+ )
575
+ peripheral = peripheralToConnect
576
+ centralManager.connect(peripheralToConnect, options: nil)
577
+ return true
578
+ } else {
579
+ Bridge.log(
580
+ "NEX-CONN: 🔵 Could not find peripheral for stored UUID. Will proceed to scan."
581
+ )
582
+ return false
583
+ }
584
+ }
585
+
586
+ private func startReconnectionTimer() {
587
+ Bridge.log("NEX-CONN: 🔄 Starting reconnection timer...")
588
+ stopReconnectionTimer() // Ensure no existing timer is running
589
+ reconnectionAttempts = 0
590
+
591
+ DispatchQueue.main.async {
592
+ self.reconnectionTimer = Timer.scheduledTimer(
593
+ timeInterval: self.reconnectionInterval,
594
+ target: self,
595
+ selector: #selector(self.attemptReconnection),
596
+ userInfo: nil,
597
+ repeats: true
598
+ )
599
+ }
600
+ }
601
+
602
+ private func stopReconnectionTimer() {
603
+ if reconnectionTimer != nil {
604
+ Bridge.log("NEX-CONN: 🛑 Stopping reconnection timer.")
605
+ reconnectionTimer?.invalidate()
606
+ reconnectionTimer = nil
607
+ }
608
+ }
609
+
610
+ @objc private func attemptReconnection() {
611
+ if nexReady {
612
+ Bridge.log("NEX-CONN: ✅ Already connected, stopping reconnection attempts.")
613
+ stopReconnectionTimer()
614
+ return
615
+ }
616
+
617
+ if maxReconnectionAttempts != -1, reconnectionAttempts >= maxReconnectionAttempts {
618
+ Bridge.log("NEX-CONN: ❌ Max reconnection attempts reached.")
619
+ stopReconnectionTimer()
620
+ return
621
+ }
622
+
623
+ reconnectionAttempts += 1
624
+ Bridge.log("NEX-CONN: 🔄 Attempting reconnection (\(reconnectionAttempts))...")
625
+ startScan()
626
+ }
627
+
628
+ // MARK: - Public Methods
629
+
630
+ private func startScan() {
631
+ Bridge.log("NEX-CONN: 🔍 startScan called")
632
+
633
+ isDisconnecting = false // Reset intentional disconnect flag
634
+
635
+ guard let centralManager else {
636
+ Bridge.log("NEX-CONN: ❌ Central Manager is nil!")
637
+ return
638
+ }
639
+
640
+ guard centralManager.state == .poweredOn else {
641
+ Bridge.log(
642
+ "NEX-CONN: ❌ Bluetooth not powered on. State: \(centralManager.state.rawValue)"
643
+ )
644
+ return
645
+ }
646
+
647
+ // First, try to reconnect using stored UUID (faster and works in background)
648
+ if connectByUUID() {
649
+ Bridge.log("NEX-CONN: 🔄 Attempting connection with stored UUID. Halting scan.")
650
+ return
651
+ }
652
+
653
+ // If that fails, check for already-connected system devices
654
+ let connectedPeripherals = centralManager.retrieveConnectedPeripherals(withServices: [
655
+ MAIN_SERVICE_UUID,
656
+ ])
657
+ if let targetName = peripheralToConnectName,
658
+ let existingPeripheral = connectedPeripherals.first(where: {
659
+ $0.name?.contains(targetName) == true
660
+ })
661
+ {
662
+ Bridge.log(
663
+ "NEX-CONN: 📱 Found already connected peripheral that matches target: \(existingPeripheral.name ?? "Unknown")"
664
+ )
665
+ if peripheral == nil {
666
+ peripheral = existingPeripheral
667
+ centralManager.connect(existingPeripheral, options: nil)
668
+ return
669
+ }
670
+ }
671
+
672
+ // Check if we have a saved device name to reconnect to (like MentraLive)
673
+ if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
674
+ !savedDeviceName.isEmpty
675
+ {
676
+ Bridge.log("NEX-CONN: 🔄 Looking for saved device: \(savedDeviceName)")
677
+ // This will be handled in didDiscover when the device is found
678
+ }
679
+
680
+ Bridge.log("NEX-CONN: ✅ Bluetooth is powered on, starting scan...")
681
+ _isScanning = true
682
+
683
+ // Scan for ALL devices, not just those with specific services
684
+ // Use same options as G1 scanner for consistency
685
+ let scanOptions: [String: Any] = [
686
+ CBCentralManagerScanOptionAllowDuplicatesKey: false, // Don't allow duplicate advertisements
687
+ ]
688
+ centralManager.scanForPeripherals(withServices: nil, options: scanOptions)
689
+
690
+ Bridge.log("NEX-CONN: 🚀 Scan started successfully")
691
+
692
+ // Re-emit already discovered peripherals (like MentraLive)
693
+ for (_, peripheral) in discoveredPeripherals {
694
+ Bridge.log(
695
+ "NEX-CONN: 📡 (Re-emitting from cache) peripheral: \(peripheral.name ?? "Unknown")"
696
+ )
697
+ if let name = peripheral.name {
698
+ emitDiscoveredDevice(name)
699
+ }
700
+ }
701
+
702
+ // No auto-stop timer (like G1) - manual control
703
+ Bridge.log("NEX-CONN: 💡 To stop scanning manually, call: MentraNexSGC.shared.stopScan()")
704
+ }
705
+
706
+ @objc func stopScan() {
707
+ centralManager?.stopScan()
708
+ _isScanning = false
709
+ Bridge.log("NEX-CONN: 🛑 Stopped scanning.")
710
+ }
711
+
712
+ @objc func isScanning() -> Bool {
713
+ _isScanning
714
+ }
715
+
716
+ @objc func isConnected() -> Bool {
717
+ nexReady && connectionState == ConnTypes.CONNECTED
718
+ }
719
+
720
+ @objc func getConnectionState() -> String {
721
+ switch connectionState {
722
+ case ConnTypes.DISCONNECTED:
723
+ return "disconnected"
724
+ case ConnTypes.CONNECTING:
725
+ return "connecting"
726
+ case ConnTypes.CONNECTED:
727
+ return "connected"
728
+ default:
729
+ return "disconnected"
730
+ }
731
+ }
732
+
733
+ // MARK: - MTU Information Access
734
+
735
+ @objc func getCurrentMTU() -> Int {
736
+ currentMTU
737
+ }
738
+
739
+ @objc func getMaxChunkSize() -> Int {
740
+ maxChunkSize
741
+ }
742
+
743
+ @objc func getDeviceMaxMTU() -> Int {
744
+ deviceMaxMTU
745
+ }
746
+
747
+ @objc func getMTUInfo() -> [String: Any] {
748
+ [
749
+ "current_mtu": currentMTU,
750
+ "device_max_mtu": deviceMaxMTU,
751
+ "max_chunk_size": maxChunkSize,
752
+ "bmp_chunk_size": bmpChunkSize,
753
+ "mtu_negotiated": nexReady,
754
+ ]
755
+ }
756
+
757
+ @objc func findCompatibleDevices() {
758
+ Bridge.log("NEX-DISCOVERY: Finding compatible devices. Clearing connection targets.")
759
+
760
+ // Clear any specific device targets to ensure we are only discovering
761
+ peripheralToConnectName = nil
762
+ clearSavedDeviceInfo() // This clears UserDefaults and our in-memory cache of saved/preferred devices.
763
+
764
+ Task {
765
+ if centralManager == nil {
766
+ centralManager = CBCentralManager(
767
+ delegate: self, queue: bluetoothQueue,
768
+ options: ["CBCentralManagerOptionShowPowerAlertKey": 0]
769
+ )
770
+ // wait for the central manager to be fully initialized before we start scanning:
771
+ try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
772
+ }
773
+
774
+ if centralManager?.state == .poweredOn {
775
+ startScan()
776
+ } else {
777
+ Bridge.log("NEX-DISCOVERY: Bluetooth not ready, will scan on power on.")
778
+ scanOnPowerOn = true
779
+ }
780
+ }
781
+ }
782
+
783
+ func sendTextWall(_ text: String) {
784
+ guard nexReady else {
785
+ Bridge.log("NEX: Not ready to display text. Device not initialized.")
786
+ return
787
+ }
788
+
789
+ Bridge.log("NEX: Displaying text wall: '\(text)'")
790
+
791
+ let displayText = Mentraos_Ble_DisplayText.with {
792
+ $0.text = text
793
+ $0.size = 48
794
+ $0.x = 20
795
+ $0.y = 260
796
+ $0.color = 10000
797
+ }
798
+
799
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
800
+ $0.displayText = displayText
801
+ }
802
+
803
+ let protobufData = try! phoneToGlasses.serializedData()
804
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
805
+ }
806
+
807
+ @objc func displayTextLine(_ text: String) {
808
+ sendTextWall(text)
809
+ }
810
+
811
+ @objc func displayDoubleTextWall(_ textTop: String, textBottom: String) {
812
+ let combinedText = "\(textTop)\n\n\(textBottom)"
813
+ sendTextWall(combinedText)
814
+ }
815
+
816
+ @objc func displayReferenceCardSimple(_ title: String, body: String) {
817
+ let combinedText = "\(title)\n\n\(body)"
818
+ sendTextWall(combinedText)
819
+ }
820
+
821
+ @objc func displayRowsCard(_ rowStrings: [String]) {
822
+ let combinedText = rowStrings.joined(separator: "\n")
823
+ sendTextWall(combinedText)
824
+ }
825
+
826
+ @objc func displayBulletList(_ title: String, bullets: [String]) {
827
+ var text = title
828
+ if !title.isEmpty {
829
+ text += "\n"
830
+ }
831
+ text += bullets.map { "• \($0)" }.joined(separator: "\n")
832
+ sendTextWall(text)
833
+ }
834
+
835
+ @objc func displayScrollingText(_ text: String) {
836
+ guard nexReady else {
837
+ Bridge.log("NEX: Not ready to display scrolling text. Device not initialized.")
838
+ return
839
+ }
840
+
841
+ Bridge.log("NEX: Displaying scrolling text: '\(text)'")
842
+
843
+ let displayScrollingText = Mentraos_Ble_DisplayScrollingText.with {
844
+ $0.text = text
845
+ $0.size = 48
846
+ $0.x = 20
847
+ $0.y = 50
848
+ $0.width = 200
849
+ $0.height = 100
850
+ $0.speed = 50
851
+ $0.pauseMs = 10
852
+ $0.loop = true
853
+ $0.align = .center
854
+ $0.lineSpacing = 2
855
+ }
856
+
857
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
858
+ $0.displayScrollingText = displayScrollingText
859
+ }
860
+
861
+ let protobufData = try! phoneToGlasses.serializedData()
862
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
863
+ }
864
+
865
+ // MARK: - Display Image Commands
866
+
867
+ @objc func displayBitmap(_ bitmap: UIImage) {
868
+ guard nexReady else {
869
+ Bridge.log("NEX: Not ready to display bitmap. Device not initialized.")
870
+ return
871
+ }
872
+
873
+ Bridge.log("NEX: Displaying bitmap image")
874
+
875
+ // Convert UIImage to raw bitmap data
876
+ guard let bmpData = convertUIImageToBmpData(bitmap) else {
877
+ Bridge.log("NEX: Failed to convert UIImage to BMP data")
878
+ return
879
+ }
880
+
881
+ displayBitmapData(bmpData, width: Int(bitmap.size.width), height: Int(bitmap.size.height))
882
+ }
883
+
884
+ @objc func displayBitmapFromData(_ bmpData: Data, width: Int, height: Int) {
885
+ displayBitmapData(bmpData, width: width, height: height)
886
+ }
887
+
888
+ private func displayBitmapData(_ bmpData: Data, width: Int, height: Int) {
889
+ guard nexReady else {
890
+ Bridge.log("NEX: Not ready to display bitmap data. Device not initialized.")
891
+ return
892
+ }
893
+
894
+ Bridge.log("NEX: Displaying bitmap data (\(bmpData.count) bytes, \(width)x\(height))")
895
+
896
+ // Generate stream ID for image transfer
897
+ let streamId = String(format: "%04X", Int.random(in: 0 ... 0xFFFF))
898
+ let totalChunks = Int(ceil(Double(bmpData.count) / Double(bmpChunkSize)))
899
+
900
+ // Send display image command first
901
+ let displayImage = Mentraos_Ble_DisplayImage.with {
902
+ $0.streamID = streamId
903
+ $0.totalChunks = UInt32(totalChunks)
904
+ $0.x = 0
905
+ $0.y = 0
906
+ $0.width = UInt32(width)
907
+ $0.height = UInt32(height)
908
+ $0.encoding = "raw"
909
+ }
910
+
911
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
912
+ $0.msgID = "img_start_1"
913
+ $0.displayImage = displayImage
914
+ }
915
+
916
+ let protobufData = try! phoneToGlasses.serializedData()
917
+ queueDataWithOptimalChunking(
918
+ protobufData, packetType: PACKET_TYPE_PROTOBUF, waitTimeMs: 100
919
+ )
920
+
921
+ // Send image chunks
922
+ sendImageChunks(streamId: streamId, imageData: bmpData)
923
+ }
924
+
925
+ private func sendImageChunks(streamId: String, imageData: Data) {
926
+ let streamIdInt = Int(streamId, radix: 16) ?? 0
927
+ let totalChunks = Int(ceil(Double(imageData.count) / Double(bmpChunkSize)))
928
+
929
+ var chunks: [[UInt8]] = []
930
+
931
+ for i in 0 ..< totalChunks {
932
+ let start = i * bmpChunkSize
933
+ let end = min(start + bmpChunkSize, imageData.count)
934
+ let chunkData = imageData.subdata(in: start ..< end)
935
+
936
+ var header: [UInt8] = [
937
+ PACKET_TYPE_IMAGE, // 0xB0
938
+ UInt8((streamIdInt >> 8) & 0xFF), // Stream ID high byte
939
+ UInt8(streamIdInt & 0xFF), // Stream ID low byte
940
+ UInt8(i & 0xFF), // Chunk index
941
+ ]
942
+ header.append(contentsOf: chunkData)
943
+ chunks.append(header)
944
+ }
945
+
946
+ Bridge.log("NEX: Sending \(chunks.count) image chunks")
947
+ queueChunks(chunks, waitTimeMs: 50)
948
+ }
949
+
950
+ private func convertUIImageToBmpData(_ image: UIImage) -> Data? {
951
+ // This is a simplified conversion - in production you'd want proper BMP encoding
952
+ guard let cgImage = image.cgImage else { return nil }
953
+
954
+ let width = cgImage.width
955
+ let height = cgImage.height
956
+ let bytesPerPixel = 4
957
+ let bytesPerRow = width * bytesPerPixel
958
+ let bitsPerComponent = 8
959
+
960
+ var pixelData = Data(count: width * height * bytesPerPixel)
961
+
962
+ pixelData.withUnsafeMutableBytes { bytes in
963
+ guard
964
+ let context = CGContext(
965
+ data: bytes.bindMemory(to: UInt8.self).baseAddress,
966
+ width: width,
967
+ height: height,
968
+ bitsPerComponent: bitsPerComponent,
969
+ bytesPerRow: bytesPerRow,
970
+ space: CGColorSpaceCreateDeviceRGB(),
971
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
972
+ )
973
+ else { return }
974
+
975
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
976
+ }
977
+
978
+ return pixelData
979
+ }
980
+
981
+ // MARK: - Display Control Commands
982
+
983
+ @objc func clearDisplay() {
984
+ guard nexReady else {
985
+ Bridge.log("NEX: Not ready to clear display. Device not initialized.")
986
+ return
987
+ }
988
+
989
+ Bridge.log("NEX: Clearing display")
990
+
991
+ let clearDisplay = Mentraos_Ble_ClearDisplay()
992
+
993
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
994
+ $0.msgID = "clear_disp_001"
995
+ $0.clearDisplay_p = clearDisplay
996
+ }
997
+
998
+ let protobufData = try! phoneToGlasses.serializedData()
999
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1000
+ }
1001
+
1002
+ @objc func blankScreen() {
1003
+ clearDisplay()
1004
+ }
1005
+
1006
+ @objc func showHomeScreen() {
1007
+ Bridge.log("NEX: Showing home screen")
1008
+ clearDisplay()
1009
+
1010
+ // Send a simple home screen text
1011
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
1012
+ self.sendTextWall("MentraOS Ready")
1013
+ }
1014
+ }
1015
+
1016
+ @objc func exitAllFunctions() {
1017
+ guard nexReady else {
1018
+ Bridge.log("NEX: Not ready to exit functions. Device not initialized.")
1019
+ return
1020
+ }
1021
+
1022
+ Bridge.log("NEX: Exiting all functions")
1023
+
1024
+ // Send exit command (0x18 from Android implementation)
1025
+ let exitCommand: [UInt8] = [0x18]
1026
+ queueChunks([exitCommand], waitTimeMs: 100)
1027
+ }
1028
+
1029
+ // MARK: - Configuration Commands
1030
+
1031
+ @objc func updateGlassesBrightness(_ brightness: Int) {
1032
+ guard nexReady else {
1033
+ Bridge.log("NEX: Not ready to update brightness. Device not initialized.")
1034
+ return
1035
+ }
1036
+
1037
+ // Validate brightness range (0-100)
1038
+ let validBrightness = max(0, min(100, brightness))
1039
+ Bridge.log("NEX: Setting brightness to \(validBrightness)%")
1040
+
1041
+ let brightnessConfig = Mentraos_Ble_BrightnessConfig.with {
1042
+ $0.value = UInt32(validBrightness)
1043
+ }
1044
+
1045
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1046
+ $0.brightness = brightnessConfig
1047
+ }
1048
+
1049
+ let protobufData = try! phoneToGlasses.serializedData()
1050
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1051
+ }
1052
+
1053
+ @objc func updateGlassesAutoBrightness(_ enabled: Bool) {
1054
+ guard nexReady else {
1055
+ Bridge.log("NEX: Not ready to update auto brightness. Device not initialized.")
1056
+ return
1057
+ }
1058
+
1059
+ Bridge.log("NEX: Setting auto brightness to \(enabled)")
1060
+
1061
+ let autoBrightnessConfig = Mentraos_Ble_AutoBrightnessConfig.with {
1062
+ $0.enabled = enabled
1063
+ }
1064
+
1065
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1066
+ $0.autoBrightness = autoBrightnessConfig
1067
+ }
1068
+
1069
+ let protobufData = try! phoneToGlasses.serializedData()
1070
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1071
+ }
1072
+
1073
+ @objc func updateGlassesHeadUpAngle(_ angle: Int) {
1074
+ guard nexReady else {
1075
+ Bridge.log("NEX: Not ready to update head-up angle. Device not initialized.")
1076
+ return
1077
+ }
1078
+
1079
+ // Validate angle range (0-60 degrees)
1080
+ let validAngle = max(0, min(60, angle))
1081
+ Bridge.log("NEX: Setting head-up angle to \(validAngle) degrees")
1082
+
1083
+ let headUpAngleConfig = Mentraos_Ble_HeadUpAngleConfig.with {
1084
+ $0.angle = UInt32(validAngle)
1085
+ }
1086
+
1087
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1088
+ $0.headUpAngle = headUpAngleConfig
1089
+ }
1090
+
1091
+ let protobufData = try! phoneToGlasses.serializedData()
1092
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1093
+ }
1094
+
1095
+ @objc func updateGlassesDisplayHeight(_ height: Int) {
1096
+ guard nexReady else {
1097
+ Bridge.log("NEX: Not ready to update display height. Device not initialized.")
1098
+ return
1099
+ }
1100
+
1101
+ // Validate height range (0-8)
1102
+ let validHeight = max(0, min(8, height))
1103
+ Bridge.log("NEX: Setting display height to \(validHeight)")
1104
+
1105
+ let displayHeightConfig = Mentraos_Ble_DisplayHeightConfig.with {
1106
+ $0.height = UInt32(validHeight)
1107
+ }
1108
+
1109
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1110
+ $0.displayHeight = displayHeightConfig
1111
+ }
1112
+
1113
+ let protobufData = try! phoneToGlasses.serializedData()
1114
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1115
+ }
1116
+
1117
+ private func updateGlassesDisplayDistance(depth: Int) {
1118
+ guard nexReady else {
1119
+ Bridge.log("NEX: Not ready to update display distance. Device not initialized.")
1120
+ return
1121
+ }
1122
+
1123
+ let tier = NexDashboardDisplayWire.depthToWireTier(depth)
1124
+ Bridge.log("NEX: Setting display distance tier \(tier) in distance_cm field (dashboard depth \(depth))")
1125
+
1126
+ let displayDistanceConfig = Mentraos_Ble_DisplayDistanceConfig.with {
1127
+ $0.distanceCm = tier
1128
+ }
1129
+
1130
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1131
+ $0.displayDistance = displayDistanceConfig
1132
+ }
1133
+
1134
+ let protobufData = try! phoneToGlasses.serializedData()
1135
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1136
+ }
1137
+
1138
+ @objc func setMicrophoneEnabled(_ enabled: Bool) {
1139
+ guard nexReady else {
1140
+ Bridge.log("NEX: Not ready to set microphone state. Device not initialized.")
1141
+ return
1142
+ }
1143
+
1144
+ Bridge.log("NEX: Setting microphone enabled: \(enabled)")
1145
+
1146
+ let micStateConfig = Mentraos_Ble_MicStateConfig.with {
1147
+ $0.enabled = enabled
1148
+ }
1149
+
1150
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1151
+ $0.micState = micStateConfig
1152
+ }
1153
+
1154
+ let protobufData = try! phoneToGlasses.serializedData()
1155
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1156
+
1157
+ // Update aiListening state when microphone state changes (G1-compatible)
1158
+ if enabled, !vadActive {
1159
+ // Only set aiListening if VAD isn't already controlling it
1160
+ aiListening = enabled
1161
+ }
1162
+ }
1163
+
1164
+ /// G1-compatible alias for microphone control
1165
+ func setMicEnabled(enabled: Bool) async -> Bool {
1166
+ setMicrophoneEnabled(enabled)
1167
+ return true
1168
+ }
1169
+
1170
+ func sortMicRanking(list: [String]) -> [String] {
1171
+ return list
1172
+ }
1173
+
1174
+ // MARK: - Status Query Commands
1175
+
1176
+ @objc func queryBatteryStatus() {
1177
+ guard nexReady else {
1178
+ Bridge.log("NEX: Not ready to query battery status. Device not initialized.")
1179
+ return
1180
+ }
1181
+
1182
+ Bridge.log("NEX: Querying battery status")
1183
+
1184
+ let batteryStateRequest = Mentraos_Ble_BatteryStateRequest()
1185
+
1186
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1187
+ $0.batteryState = batteryStateRequest
1188
+ }
1189
+
1190
+ let protobufData = try! phoneToGlasses.serializedData()
1191
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1192
+ }
1193
+
1194
+ @objc func queryGlassesInfo() {
1195
+ guard nexReady else {
1196
+ Bridge.log("NEX: Not ready to query glasses info. Device not initialized.")
1197
+ return
1198
+ }
1199
+
1200
+ Bridge.log("NEX: Querying glasses information")
1201
+
1202
+ let glassesInfoRequest = Mentraos_Ble_GlassesInfoRequest()
1203
+
1204
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1205
+ $0.glassesInfo = glassesInfoRequest
1206
+ }
1207
+
1208
+ let protobufData = try! phoneToGlasses.serializedData()
1209
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1210
+ }
1211
+
1212
+ // MARK: - Utility Methods
1213
+
1214
+ @objc func sendPongResponse() {
1215
+ guard nexReady else {
1216
+ Bridge.log("NEX: Not ready to send pong. Device not initialized.")
1217
+ return
1218
+ }
1219
+
1220
+ let timestamp = Date().timeIntervalSince1970 * 1000
1221
+ Bridge.log("NEX: Sending pong response (Time: \(timestamp))")
1222
+
1223
+ let pongResponse = Mentraos_Ble_PongResponse()
1224
+
1225
+ let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
1226
+ $0.pong = pongResponse
1227
+ }
1228
+
1229
+ let protobufData = try! phoneToGlasses.serializedData()
1230
+ queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
1231
+
1232
+ // Notify about heartbeat sent (pong response)
1233
+ notifyHeartbeatSent(timestamp)
1234
+ }
1235
+
1236
+ @objc func isDeviceReady() -> Bool {
1237
+ nexReady && connectionState == ConnTypes.CONNECTED
1238
+ }
1239
+
1240
+ @objc func getDeviceInfo() -> [String: Any] {
1241
+ [
1242
+ "device_ready": nexReady,
1243
+ "connection_state": getConnectionState(),
1244
+ "current_mtu": currentMTU,
1245
+ "device_max_mtu": deviceMaxMTU,
1246
+ "max_chunk_size": maxChunkSize,
1247
+ "bmp_chunk_size": bmpChunkSize,
1248
+ "device_name": peripheral?.name ?? "Unknown",
1249
+ "device_id": peripheral?.identifier.uuidString ?? "Unknown",
1250
+ ]
1251
+ }
1252
+
1253
+ // MARK: - Advanced Display Methods
1254
+
1255
+ @objc func displayCustomContent(_ content: String) {
1256
+ // For now, treat custom content as regular text
1257
+ sendTextWall(content)
1258
+ }
1259
+
1260
+ @objc func setUpdatingScreen(_ updating: Bool) {
1261
+ Bridge.log("NEX: Set updating screen: \(updating)")
1262
+ // This could be used to prevent display updates during certain operations
1263
+ // Implementation depends on specific requirements
1264
+ }
1265
+
1266
+ // MARK: - Data Processing and Event Listeners
1267
+
1268
+ private func processReceivedData(_ data: Data) {
1269
+ guard data.count > 0 else { return }
1270
+
1271
+ let packetType = data[0]
1272
+ Bridge.log("NEX: Processing packet type: 0x\(String(format: "%02X", packetType))")
1273
+
1274
+ switch packetType {
1275
+ case PACKET_TYPE_JSON:
1276
+ if data.count > 1 {
1277
+ let jsonData = data.subdata(in: 1 ..< data.count)
1278
+ processJsonData(jsonData)
1279
+ }
1280
+
1281
+ case PACKET_TYPE_PROTOBUF:
1282
+ if data.count > 1 {
1283
+ let protobufData = data.subdata(in: 1 ..< data.count)
1284
+ processProtobufData(protobufData)
1285
+ }
1286
+
1287
+ case PACKET_TYPE_AUDIO:
1288
+ if data.count > 2 {
1289
+ let sequenceNumber = data[1]
1290
+ let audioData = data.subdata(in: 2 ..< data.count)
1291
+ processAudioData(audioData, sequenceNumber: sequenceNumber)
1292
+ }
1293
+
1294
+ case PACKET_TYPE_IMAGE:
1295
+ processImageData(data)
1296
+
1297
+ default:
1298
+ Bridge.log("NEX: Unknown packet type: 0x\(String(format: "%02X", packetType))")
1299
+ }
1300
+ }
1301
+
1302
+ private func processJsonData(_ jsonData: Data) {
1303
+ guard let jsonString = String(data: jsonData, encoding: .utf8) else {
1304
+ Bridge.log("NEX: Failed to decode JSON data")
1305
+ return
1306
+ }
1307
+
1308
+ Bridge.log("NEX: Processing JSON: \(jsonString)")
1309
+
1310
+ do {
1311
+ guard let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
1312
+ let type = json["type"] as? String
1313
+ else {
1314
+ return
1315
+ }
1316
+
1317
+ switch type {
1318
+ case "battery_status":
1319
+ handleBatteryStatusJson(json)
1320
+ case "device_info":
1321
+ handleDeviceInfoJson(json)
1322
+ case "button_event":
1323
+ handleButtonEventJson(json)
1324
+ case "ping":
1325
+ handlePingJson(json)
1326
+ case "vad_event":
1327
+ handleVadEventJson(json)
1328
+ case "imu_data":
1329
+ handleImuDataJson(json)
1330
+ case "head_gesture":
1331
+ handleHeadGestureJson(json)
1332
+ default:
1333
+ Bridge.log("NEX: Unhandled JSON type: \(type)")
1334
+ }
1335
+ } catch {
1336
+ Bridge.log("NEX: Error parsing JSON: \(error)")
1337
+ }
1338
+ }
1339
+
1340
+ private func processProtobufData(_ protobufData: Data) {
1341
+ do {
1342
+ let glassesToPhone = try Mentraos_Ble_GlassesToPhone(serializedData: protobufData)
1343
+ Bridge.log("NEX: Processing protobuf payload case: \(glassesToPhone.payload)")
1344
+
1345
+ switch glassesToPhone.payload {
1346
+ case let .batteryStatus(batteryStatus):
1347
+ handleBatteryStatusProtobuf(batteryStatus)
1348
+
1349
+ case let .chargingState(chargingState):
1350
+ handleChargingStateProtobuf(chargingState)
1351
+
1352
+ case let .deviceInfo(deviceInfo):
1353
+ handleDeviceInfoProtobuf(deviceInfo)
1354
+
1355
+ case let .headPosition(headPosition):
1356
+ handleHeadPositionProtobuf(headPosition)
1357
+
1358
+ case let .headUpAngleSet(headUpAngleResponse):
1359
+ handleHeadUpAngleResponseProtobuf(headUpAngleResponse)
1360
+
1361
+ case let .ping(pingRequest):
1362
+ handlePingProtobuf(pingRequest)
1363
+
1364
+ case let .vadEvent(vadEvent):
1365
+ handleVadEventProtobuf(vadEvent)
1366
+
1367
+ case let .imageTransferComplete(transferComplete):
1368
+ handleImageTransferCompleteProtobuf(transferComplete)
1369
+
1370
+ case let .imuData(imuData):
1371
+ handleImuDataProtobuf(imuData)
1372
+
1373
+ case let .buttonEvent(buttonEvent):
1374
+ handleButtonEventProtobuf(buttonEvent)
1375
+
1376
+ case let .headGesture(headGesture):
1377
+ handleHeadGestureProtobuf(headGesture)
1378
+
1379
+ // Note: VersionResponse not available in current protobuf structure
1380
+
1381
+ case .none:
1382
+ Bridge.log("NEX: Protobuf payload not set")
1383
+
1384
+ default:
1385
+ Bridge.log("NEX: Unhandled protobuf payload type")
1386
+ }
1387
+
1388
+ } catch {
1389
+ Bridge.log("NEX: Error parsing protobuf data: \(error)")
1390
+ }
1391
+ }
1392
+
1393
+ private func processAudioData(_ audioData: Data, sequenceNumber: UInt8) {
1394
+ Bridge.log(
1395
+ "NEX: Received audio data - sequence: \(sequenceNumber), size: \(audioData.count) bytes"
1396
+ )
1397
+
1398
+ // Update @Published property (G1-compatible approach)
1399
+ // Create packet with sequence number prefix like G1 expects
1400
+ var packetData = Data()
1401
+ packetData.append(sequenceNumber)
1402
+ packetData.append(audioData)
1403
+
1404
+ compressedVoiceData = packetData
1405
+ }
1406
+
1407
+ private func processImageData(_ imageData: Data) {
1408
+ Bridge.log("NEX: Received image data: \(imageData.count) bytes")
1409
+ // Image data processing can be implemented based on specific requirements
1410
+ }
1411
+
1412
+ // MARK: - Protobuf Event Handlers
1413
+
1414
+ private func handleBatteryStatusProtobuf(_ batteryStatus: Mentraos_Ble_BatteryStatus) {
1415
+ let level = Int(batteryStatus.level)
1416
+ let isCharging = batteryStatus.charging
1417
+
1418
+ Bridge.log("NEX: 🔋 Battery Status - Level: \(level)%, Charging: \(isCharging)")
1419
+
1420
+ // Update @Published properties (G1-compatible approach)
1421
+ DeviceStore.shared.apply("glasses", "batteryLevel", level)
1422
+ DeviceStore.shared.apply("glasses", "charging", isCharging)
1423
+ }
1424
+
1425
+ private func handleChargingStateProtobuf(_ chargingState: Mentraos_Ble_ChargingState) {
1426
+ let chargingState = chargingState.state == .charging
1427
+
1428
+ Bridge.log("NEX: 🔌 Charging State: \(chargingState ? "CHARGING" : "NOT_CHARGING")")
1429
+
1430
+ // Update @Published property (G1-compatible approach)
1431
+ DeviceStore.shared.apply("glasses", "charging", chargingState)
1432
+ }
1433
+
1434
+ private func handleDeviceInfoProtobuf(_ deviceInfo: Mentraos_Ble_DeviceInfo) {
1435
+ Bridge.log("NEX: 📱 Device Info: \(deviceInfo)")
1436
+
1437
+ // Update @Published properties (G1-compatible approach)
1438
+ DeviceStore.shared.apply("glasses", "deviceFirmwareVersion", deviceInfo.fwVersion)
1439
+ DeviceStore.shared.apply("glasses", "deviceHardwareModel", deviceInfo.hwModel)
1440
+ }
1441
+
1442
+ private func handleHeadPositionProtobuf(_ headPosition: Mentraos_Ble_HeadPosition) {
1443
+ let angle = Int(headPosition.angle)
1444
+
1445
+ Bridge.log("NEX: 📐 Head Position - Angle: \(angle)°")
1446
+
1447
+ // Update @Published property (G1-compatible approach)
1448
+ headUpAngle = angle
1449
+ }
1450
+
1451
+ private func handleHeadUpAngleResponseProtobuf(_ response: Mentraos_Ble_HeadUpAngleResponse) {
1452
+ let success = response.success
1453
+
1454
+ Bridge.log("NEX: 📐 Head Up Angle Set Response - Success: \(success)")
1455
+
1456
+ // Emit response event
1457
+ let eventBody: [String: Any] = [
1458
+ "head_up_angle_set_result": success,
1459
+ "device_model": "Mentra Display",
1460
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1461
+ ]
1462
+
1463
+ emitEvent("HeadUpAngleResponseEvent", body: eventBody)
1464
+ }
1465
+
1466
+ private func handlePingProtobuf(_: Mentraos_Ble_PingRequest) {
1467
+ let timestamp = Date().timeIntervalSince1970 * 1000
1468
+
1469
+ Bridge.log("NEX: 💓 Received PING from glasses (Time: \(timestamp))")
1470
+
1471
+ // Automatically send pong response
1472
+ sendPongResponse()
1473
+
1474
+ // Emit heartbeat received event
1475
+ let eventBody: [String: Any] = [
1476
+ "heartbeat_received": [
1477
+ "timestamp": timestamp,
1478
+ "device_model": "Mentra Display",
1479
+ ],
1480
+ ]
1481
+
1482
+ emitEvent("HeartbeatReceivedEvent", body: eventBody)
1483
+
1484
+ // Query battery status periodically (every 10 pings like Java implementation)
1485
+ heartbeatCount += 1
1486
+ if heartbeatCount % 10 == 0 {
1487
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
1488
+ self.queryBatteryStatus()
1489
+ }
1490
+ }
1491
+ }
1492
+
1493
+ private func handleVadEventProtobuf(_ vadEvent: Mentraos_Ble_VadEvent) {
1494
+ let vadActiveState = vadEvent.state == .active
1495
+
1496
+ Bridge.log("NEX: 🎤 VAD Event - Voice Activity: \(vadActiveState)")
1497
+
1498
+ // Update @Published properties (G1-compatible approach)
1499
+ vadActive = vadActiveState
1500
+ aiListening = vadActiveState // Mirror G1's aiListening behavior
1501
+ }
1502
+
1503
+ private func handleImageTransferCompleteProtobuf(
1504
+ _ transferComplete: Mentraos_Ble_ImageTransferComplete
1505
+ ) {
1506
+ let status = transferComplete.status
1507
+ let missingChunks = transferComplete.missingChunks
1508
+
1509
+ Bridge.log("NEX: 🖼️ Image Transfer Complete - Status: \(status)")
1510
+
1511
+ switch status {
1512
+ case .ok:
1513
+ Bridge.log("NEX: Image transfer completed successfully")
1514
+ // Clear any pending image chunks
1515
+
1516
+ case .incomplete:
1517
+ Bridge.log("NEX: Image transfer incomplete - Missing chunks: \(missingChunks)")
1518
+ // Could implement chunk retransmission here
1519
+
1520
+ default:
1521
+ Bridge.log("NEX: Unknown image transfer status")
1522
+ }
1523
+
1524
+ // Emit image transfer complete event
1525
+ let eventBody: [String: Any] = [
1526
+ "image_transfer_complete": [
1527
+ "status": status == .ok ? "success" : "incomplete",
1528
+ "missing_chunks": missingChunks,
1529
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1530
+ ],
1531
+ ]
1532
+
1533
+ emitEvent("ImageTransferCompleteEvent", body: eventBody)
1534
+ }
1535
+
1536
+ private func handleImuDataProtobuf(_ imuData: Mentraos_Ble_ImuData) {
1537
+ Bridge.log("NEX: 📊 IMU Data: \(imuData)")
1538
+
1539
+ // Update @Published properties (G1-compatible approach)
1540
+ accelerometer = [imuData.accel.x, imuData.accel.y, imuData.accel.z]
1541
+ gyroscope = [imuData.gyro.x, imuData.gyro.y, imuData.gyro.z]
1542
+ magnetometer = [imuData.mag.x, imuData.mag.y, imuData.mag.z]
1543
+ }
1544
+
1545
+ private func handleButtonEventProtobuf(_ buttonEvent: Mentraos_Ble_ButtonEvent) {
1546
+ let buttonNumber = Int(buttonEvent.button.rawValue)
1547
+ let buttonState = buttonEvent.state
1548
+
1549
+ Bridge.log("NEX: 🔘 Button Event - Button: \(buttonNumber), State: \(buttonState)")
1550
+
1551
+ // Update @Published properties (G1-compatible approach)
1552
+ lastButtonPressed = buttonNumber
1553
+ lastButtonState = "\(buttonState.rawValue)"
1554
+ }
1555
+
1556
+ private func handleHeadGestureProtobuf(_ headGesture: Mentraos_Ble_HeadGesture) {
1557
+ let gestureType = headGesture.gesture
1558
+
1559
+ Bridge.log("NEX: 👤 Head Gesture: \(gestureType)")
1560
+
1561
+ // Update @Published properties (G1-compatible approach)
1562
+ switch gestureType {
1563
+ case .headUp:
1564
+ DeviceStore.shared.apply("glasses", "headUp", true)
1565
+ lastHeadGesture = "headUp"
1566
+ case .nod:
1567
+ lastHeadGesture = "nod"
1568
+ case .shake:
1569
+ lastHeadGesture = "shake"
1570
+ default:
1571
+ Bridge.log("NEX: Unknown head gesture type: \(gestureType)")
1572
+ lastHeadGesture = "unknown"
1573
+ }
1574
+ }
1575
+
1576
+ // MARK: - JSON Event Handlers
1577
+
1578
+ private func handleBatteryStatusJson(_ json: [String: Any]) {
1579
+ let level = json["level"] as? Int ?? -1
1580
+ let isCharging = json["charging"] as? Bool ?? false
1581
+
1582
+ Bridge.log("NEX: 🔋 JSON Battery Status - Level: \(level)%, Charging: \(charging)")
1583
+
1584
+ // Update @Published properties (G1-compatible approach)
1585
+ DeviceStore.shared.apply("glasses", "batteryLevel", level)
1586
+ DeviceStore.shared.apply("glasses", "charging", isCharging)
1587
+ }
1588
+
1589
+ private func handleDeviceInfoJson(_ json: [String: Any]) {
1590
+ Bridge.log("NEX: 📱 JSON Device Info: \(json)")
1591
+
1592
+ let eventBody: [String: Any] = [
1593
+ "device_info": json,
1594
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1595
+ ]
1596
+
1597
+ emitEvent("DeviceInfoEvent", body: eventBody)
1598
+ }
1599
+
1600
+ private func handleButtonEventJson(_ json: [String: Any]) {
1601
+ let buttonId = json["button_id"] as? String ?? "unknown"
1602
+ let pressType = json["press_type"] as? String ?? "short"
1603
+
1604
+ Bridge.log("NEX: 🔘 JSON Button Event - Button: \(buttonId), Type: \(pressType)")
1605
+
1606
+ let eventBody: [String: Any] = [
1607
+ "button_press": [
1608
+ "device_model": "Mentra Display",
1609
+ "button_id": buttonId,
1610
+ "press_type": pressType,
1611
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1612
+ ],
1613
+ ]
1614
+
1615
+ emitEvent("ButtonPressEvent", body: eventBody)
1616
+ }
1617
+
1618
+ private func handlePingJson(_: [String: Any]) {
1619
+ let timestamp = Date().timeIntervalSince1970 * 1000
1620
+
1621
+ Bridge.log("NEX: 💓 JSON PING received (Time: \(timestamp))")
1622
+
1623
+ // Send pong response
1624
+ sendPongResponse()
1625
+
1626
+ // Emit heartbeat received event
1627
+ let eventBody: [String: Any] = [
1628
+ "heartbeat_received": [
1629
+ "timestamp": timestamp,
1630
+ "device_model": "Mentra Display",
1631
+ ],
1632
+ ]
1633
+
1634
+ emitEvent("HeartbeatReceivedEvent", body: eventBody)
1635
+ }
1636
+
1637
+ private func handleVadEventJson(_ json: [String: Any]) {
1638
+ let vadActiveState = json["vad"] as? Bool ?? false
1639
+
1640
+ Bridge.log("NEX: 🎤 JSON VAD Event - Voice Activity: \(vadActiveState)")
1641
+
1642
+ // Update @Published properties (G1-compatible approach)
1643
+ vadActive = vadActiveState
1644
+ aiListening = vadActiveState // Mirror G1's aiListening behavior
1645
+ }
1646
+
1647
+ private func handleImuDataJson(_ json: [String: Any]) {
1648
+ Bridge.log("NEX: 📊 JSON IMU Data: \(json)")
1649
+
1650
+ let eventBody: [String: Any] = [
1651
+ "imu_data": json,
1652
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1653
+ ]
1654
+
1655
+ emitEvent("ImuDataEvent", body: eventBody)
1656
+ }
1657
+
1658
+ private func handleHeadGestureJson(_ json: [String: Any]) {
1659
+ let gesture = json["gesture"] as? String ?? "unknown"
1660
+
1661
+ Bridge.log("NEX: 👤 JSON Head Gesture: \(gesture)")
1662
+
1663
+ let eventBody: [String: Any] = [
1664
+ "head_gesture": [
1665
+ "gesture": gesture,
1666
+ "timestamp": Date().timeIntervalSince1970 * 1000,
1667
+ ],
1668
+ ]
1669
+
1670
+ emitEvent("HeadGestureEvent", body: eventBody)
1671
+ }
1672
+
1673
+ // MARK: - Event Emission Helper
1674
+
1675
+ private func emitEvent(_ eventName: String, body: [String: Any]) {
1676
+ // Use the standardized Bridge.sendTypedMessage helper for consistent type field handling
1677
+ Bridge.sendTypedMessage(eventName, body: body)
1678
+ Bridge.log("NEX: 📡 Emitted \(eventName) via Bridge.sendTypedMessage")
1679
+ }
1680
+
1681
+ // MARK: - Heartbeat Management
1682
+
1683
+ private func notifyHeartbeatSent(_ timestamp: TimeInterval) {
1684
+ lastHeartbeatSentTime = timestamp
1685
+
1686
+ let eventBody: [String: Any] = [
1687
+ "heartbeat_sent": [
1688
+ "timestamp": timestamp,
1689
+ "device_model": "Mentra Display",
1690
+ ],
1691
+ ]
1692
+
1693
+ emitEvent("HeartbeatSentEvent", body: eventBody)
1694
+ }
1695
+
1696
+ private func notifyHeartbeatReceived(_ timestamp: TimeInterval) {
1697
+ lastHeartbeatReceivedTime = timestamp
1698
+
1699
+ let eventBody: [String: Any] = [
1700
+ "heartbeat_received": [
1701
+ "timestamp": timestamp,
1702
+ "device_model": "Mentra Display",
1703
+ ],
1704
+ ]
1705
+
1706
+ emitEvent("HeartbeatReceivedEvent", body: eventBody)
1707
+ }
1708
+
1709
+ @objc func getLastHeartbeatSentTime() -> TimeInterval {
1710
+ lastHeartbeatSentTime
1711
+ }
1712
+
1713
+ @objc func getLastHeartbeatReceivedTime() -> TimeInterval {
1714
+ lastHeartbeatReceivedTime
1715
+ }
1716
+
1717
+ // MARK: - Java-Compatible Initialization Methods
1718
+
1719
+ private func startMicBeat() {
1720
+ Bridge.log("NEX: Starting micbeat (30 min interval)")
1721
+
1722
+ if micBeatCount > 0 {
1723
+ stopMicBeat()
1724
+ }
1725
+
1726
+ // Set mic enabled first (like Java line 1751)
1727
+ setMicrophoneEnabled(true)
1728
+ micBeatCount += 1
1729
+
1730
+ // Schedule periodic mic beat (like Java lines 1753-1762)
1731
+ micBeatTimer = Timer.scheduledTimer(withTimeInterval: MICBEAT_INTERVAL_MS, repeats: true) {
1732
+ [weak self] _ in
1733
+ guard let self else { return }
1734
+ Bridge.log("NEX: SENDING MIC BEAT")
1735
+ self.setMicrophoneEnabled(self.shouldUseGlassesMic)
1736
+ }
1737
+ }
1738
+
1739
+ private func stopMicBeat() {
1740
+ setMicrophoneEnabled(false)
1741
+ micBeatTimer?.invalidate()
1742
+ micBeatTimer = nil
1743
+ micBeatCount = 0
1744
+ Bridge.log("NEX: Stopped mic beat")
1745
+ }
1746
+
1747
+ private func sendWhiteListCommand() {
1748
+ guard !whiteListedAlready else {
1749
+ Bridge.log("NEX: Whitelist already sent, skipping")
1750
+ return
1751
+ }
1752
+ whiteListedAlready = true
1753
+
1754
+ Bridge.log("NEX: Sending whitelist command")
1755
+
1756
+ // Create whitelist JSON exactly like Java (lines 2642-2680)
1757
+ let whitelistJson = createWhitelistJson()
1758
+ let chunks = createWhitelistChunks(json: whitelistJson)
1759
+
1760
+ // Send chunks with delay like Java
1761
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { // 10ms delay
1762
+ self.queueChunks(chunks)
1763
+ }
1764
+ }
1765
+
1766
+ private func createWhitelistJson() -> String {
1767
+ // Exact JSON structure from Java implementation (lines 2653-2680)
1768
+ let whitelistDict: [String: Any] = [
1769
+ "calendar_enable": false,
1770
+ "call_enable": false,
1771
+ "msg_enable": false,
1772
+ "ios_mail_enable": false,
1773
+ "app": [
1774
+ "list": [
1775
+ ["id": "com.augment.os", "name": "AugmentOS"],
1776
+ ],
1777
+ "enable": true,
1778
+ ],
1779
+ ]
1780
+
1781
+ do {
1782
+ let jsonData = try JSONSerialization.data(withJSONObject: whitelistDict)
1783
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1784
+ Bridge.log("NEX: Created whitelist JSON: \(jsonString)")
1785
+ return jsonString
1786
+ }
1787
+ } catch {
1788
+ Bridge.log("NEX: Error creating whitelist JSON: \(error)")
1789
+ }
1790
+
1791
+ return "{}"
1792
+ }
1793
+
1794
+ private func createWhitelistChunks(json: String) -> [[UInt8]] {
1795
+ // Exact chunking logic from Java (lines 2703-2728)
1796
+ guard let jsonData = json.data(using: .utf8) else { return [] }
1797
+
1798
+ let totalChunks = Int(ceil(Double(jsonData.count) / Double(maxChunkSize)))
1799
+ var chunks: [[UInt8]] = []
1800
+
1801
+ for i in 0 ..< totalChunks {
1802
+ let start = i * maxChunkSize
1803
+ let end = min(start + maxChunkSize, jsonData.count)
1804
+ let payloadChunk = jsonData.subdata(in: start ..< end)
1805
+
1806
+ // Create header: [WHITELIST_CMD, total_chunks, chunk_index] (Java lines 2714-2717)
1807
+ var header: [UInt8] = [
1808
+ WHITELIST_CMD, // Command ID (0x04)
1809
+ UInt8(totalChunks), // Total number of chunks
1810
+ UInt8(i), // Current chunk index
1811
+ ]
1812
+
1813
+ // Combine header and payload (Java lines 2720-2725)
1814
+ header.append(contentsOf: payloadChunk)
1815
+ chunks.append(header)
1816
+ }
1817
+
1818
+ Bridge.log("NEX: Created \(chunks.count) whitelist chunks")
1819
+ return chunks
1820
+ }
1821
+
1822
+ private func postProtobufSchemaVersionInfo() {
1823
+ guard !protobufVersionPosted else {
1824
+ Bridge.log("NEX: Protobuf version already posted, skipping")
1825
+ return
1826
+ }
1827
+ protobufVersionPosted = true
1828
+
1829
+ Bridge.log("NEX: 📋 Posting protobuf schema version info")
1830
+
1831
+ // Emit protobuf schema version event like Java (lines 3709-3728)
1832
+ let eventBody: [String: Any] = [
1833
+ "protobuf_schema_version": [
1834
+ "schema_version": 1, // Default version
1835
+ "build_info": "Schema v1 | mentraos_ble.proto",
1836
+ "device_model": "Mentra Display",
1837
+ ],
1838
+ ]
1839
+
1840
+ // emitEvent("ProtobufSchemaVersionEvent", body: eventBody)
1841
+ }
1842
+
1843
+ /// Save microphone state before disconnection (like Java implementation)
1844
+ private func saveMicrophoneStateBeforeDisconnection() {
1845
+ UserDefaults.standard.set(shouldUseGlassesMic, forKey: "microphoneStateBeforeDisconnection")
1846
+ microphoneStateBeforeDisconnection = shouldUseGlassesMic
1847
+ Bridge.log("NEX: Saved microphone state before disconnection: \(shouldUseGlassesMic)")
1848
+ }
1849
+
1850
+ @objc func disconnect() {
1851
+ Bridge.log("NEX: 🔌 User-initiated disconnect")
1852
+ if let peripheral {
1853
+ // Save microphone state before disconnection (like Java implementation)
1854
+ saveMicrophoneStateBeforeDisconnection()
1855
+
1856
+ // Stop mic beat system
1857
+ stopMicBeat()
1858
+
1859
+ isDisconnecting = true
1860
+ connectionState = ConnTypes.DISCONNECTED
1861
+ centralManager?.cancelPeripheralConnection(peripheral)
1862
+ }
1863
+ stopReconnectionTimer()
1864
+ }
1865
+
1866
+ // MARK: - Lifecycle Management (ported from Java)
1867
+
1868
+ @objc func destroy() {
1869
+ Bridge.log("NEX: 💥 Destroying MentraNexSGC instance")
1870
+
1871
+ isKilled = true
1872
+ isDisconnecting = true
1873
+
1874
+ // Stop all timers
1875
+ // Save microphone state before destruction (like Java implementation)
1876
+ saveMicrophoneStateBeforeDisconnection()
1877
+
1878
+ // Stop mic beat system (like Java implementation)
1879
+ stopMicBeat()
1880
+
1881
+ stopReconnectionTimer()
1882
+
1883
+ // Disconnect from peripheral
1884
+ if let peripheral {
1885
+ centralManager?.cancelPeripheralConnection(peripheral)
1886
+ }
1887
+
1888
+ // Stop scanning
1889
+ if _isScanning {
1890
+ stopScan()
1891
+ }
1892
+
1893
+ // Clear all references
1894
+ peripheral = nil
1895
+ writeCharacteristic = nil
1896
+ notifyCharacteristic = nil
1897
+ centralManager?.delegate = nil
1898
+ centralManager = nil
1899
+
1900
+ // Clear discovery cache
1901
+ discoveredPeripherals.removeAll()
1902
+
1903
+ Bridge.log("NEX: ✅ MentraNexSGC destroyed successfully")
1904
+ // Reset initialization flags
1905
+ whiteListedAlready = false
1906
+ protobufVersionPosted = false
1907
+ }
1908
+
1909
+ @objc func reset() {
1910
+ Bridge.log("NEX: 🔄 Resetting MentraNexSGC to fresh state")
1911
+
1912
+ // Disconnect current connection
1913
+ disconnect()
1914
+
1915
+ // Clear all saved device information
1916
+ clearSavedDeviceInfo()
1917
+
1918
+ // Clear discovery cache
1919
+ discoveredPeripherals.removeAll()
1920
+
1921
+ // Reset internal state
1922
+ isKilled = false
1923
+ isDisconnecting = false
1924
+ nexReady = false
1925
+ reconnectionAttempts = 0
1926
+ peripheralToConnectName = nil
1927
+
1928
+ Bridge.log("NEX: ✅ Reset complete - ready for fresh pairing")
1929
+ // Reset initialization flags (like Java implementation)
1930
+ whiteListedAlready = false
1931
+ protobufVersionPosted = false
1932
+ heartbeatCount = 0
1933
+ micBeatCount = 0
1934
+ shouldUseGlassesMic = true
1935
+ microphoneStateBeforeDisconnection = false
1936
+ }
1937
+
1938
+ // MARK: - Helper Methods (like G1)
1939
+
1940
+ private func getConnectedDevices() -> [CBPeripheral] {
1941
+ guard let centralManager else { return [] }
1942
+ // Retrieve peripherals already connected that expose our main service
1943
+ return centralManager.retrieveConnectedPeripherals(withServices: [])
1944
+ }
1945
+
1946
+ private func emitDiscoveredDevice(_ name: String) {
1947
+ // Emit device discovery event using standardized typed message function
1948
+ Bridge.log("NEX: 📡 Emitting discovered device: \(name)")
1949
+ Bridge.sendDiscoveredDevice(DeviceTypes.NEX, name)
1950
+ }
1951
+
1952
+ @objc func checkBluetoothState() {
1953
+ Bridge.log("NEX: 🔍 Checking Bluetooth State...")
1954
+ if let centralManager {
1955
+ Bridge.log("NEX: 📱 Central Manager exists: YES")
1956
+ Bridge.log("NEX: 📱 Current Bluetooth State: \(centralManager.state.rawValue)")
1957
+
1958
+ switch centralManager.state {
1959
+ case .poweredOn:
1960
+ Bridge.log("NEX: ✅ Bluetooth is ready for scanning")
1961
+
1962
+ if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
1963
+ !savedDeviceName.isEmpty
1964
+ {
1965
+ Bridge.log("NEX: 🔄 Looking for saved device: \(savedDeviceName)")
1966
+ // This will be handled in didDiscover when the device is found
1967
+ startScan()
1968
+ }
1969
+ case .poweredOff:
1970
+ Bridge.log("NEX: ❌ Bluetooth is turned off")
1971
+ case .resetting:
1972
+ Bridge.log("NEX: 🔄 Bluetooth is resetting")
1973
+ case .unauthorized:
1974
+ Bridge.log("NEX: ❌ Bluetooth permission denied")
1975
+ case .unsupported:
1976
+ Bridge.log("NEX: ❌ Bluetooth not supported")
1977
+ case .unknown:
1978
+ Bridge.log("NEX: ❓ Bluetooth state unknown")
1979
+ @unknown default:
1980
+ Bridge.log("NEX: ❓ Unknown Bluetooth state: \(centralManager.state.rawValue)")
1981
+ }
1982
+ } else {
1983
+ Bridge.log("NEX: ❌ Central Manager is nil!")
1984
+ }
1985
+ }
1986
+
1987
+ // MARK: - CBCentralManagerDelegate
1988
+
1989
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
1990
+ Bridge.log("NEX: 🔄 Bluetooth state changed to: \(central.state.rawValue)")
1991
+
1992
+ switch central.state {
1993
+ case .poweredOn:
1994
+ Bridge.log("NEX: ✅ Bluetooth is On and ready for scanning")
1995
+ if scanOnPowerOn {
1996
+ Bridge.log("NEX: 🚀 Triggering scan after power on.")
1997
+ scanOnPowerOn = false
1998
+ startScan()
1999
+ }
2000
+ case .poweredOff:
2001
+ Bridge.log("NEX: ❌ Bluetooth is Off - user needs to enable Bluetooth")
2002
+ connectionState = ConnTypes.DISCONNECTED
2003
+ case .resetting:
2004
+ Bridge.log("NEX: 🔄 Bluetooth is resetting - wait for completion")
2005
+ connectionState = ConnTypes.DISCONNECTED
2006
+ case .unauthorized:
2007
+ Bridge.log("NEX: ❌ Bluetooth is unauthorized - check app permissions")
2008
+ connectionState = ConnTypes.DISCONNECTED
2009
+ case .unsupported:
2010
+ Bridge.log("NEX: ❌ Bluetooth is unsupported on this device")
2011
+ connectionState = ConnTypes.DISCONNECTED
2012
+ case .unknown:
2013
+ Bridge.log("NEX: ❓ Bluetooth state is unknown - may be initializing")
2014
+ @unknown default:
2015
+ Bridge.log("NEX: ❓ A new Bluetooth state was introduced: \(central.state.rawValue)")
2016
+ }
2017
+ }
2018
+
2019
+ func centralManager(
2020
+ _: CBCentralManager, didDiscover peripheral: CBPeripheral,
2021
+ advertisementData _: [String: Any], rssi RSSI: NSNumber
2022
+ ) {
2023
+ guard let deviceName = peripheral.name else {
2024
+ // Bridge.log("NEX-CONN: 🚫 Ignoring device with no name")
2025
+ return
2026
+ }
2027
+
2028
+ // guard isCompatibleNexDevice(deviceName) else {
2029
+ // // Bridge.log("NEX-CONN: 🚫 Ignoring incompatible device: \(deviceName)")
2030
+ // return
2031
+ // }
2032
+
2033
+ Bridge.log("NEX-CONN: 🎯 === Compatible Nex Device Found ===")
2034
+ Bridge.log("NEX-CONN: 📱 Device Name: \(deviceName)")
2035
+ Bridge.log("NEX-CONN: 📶 RSSI: \(RSSI) dBm")
2036
+
2037
+ // Store the peripheral in cache (like MentraLive)
2038
+ discoveredPeripherals[deviceName] = peripheral
2039
+
2040
+ // Always emit the discovered device for the UI list
2041
+ emitDiscoveredDevice(deviceName)
2042
+
2043
+ // Auto-connect logic based on target or saved device (from Java MentraNexSGC)
2044
+ var shouldConnect = false
2045
+ var connectionReason = ""
2046
+
2047
+ // Check if this matches our target device name for connection
2048
+ if let targetName = peripheralToConnectName, deviceName.contains(targetName) {
2049
+ shouldConnect = true
2050
+ connectionReason = "Target device name match: \(targetName)"
2051
+ }
2052
+ // Check if this matches our saved device for reconnection
2053
+ else if let savedName = savedDeviceName, deviceName == savedName {
2054
+ shouldConnect = true
2055
+ connectionReason = "Saved device reconnection: \(savedName)"
2056
+ }
2057
+ // Check if this matches preferred device ID
2058
+ else if let preferredId = preferredDeviceId {
2059
+ if let extractedId = extractDeviceId(from: deviceName), extractedId == preferredId {
2060
+ shouldConnect = true
2061
+ connectionReason = "Preferred device ID match: \(preferredId)"
2062
+ }
2063
+ }
2064
+
2065
+ if shouldConnect {
2066
+ connectToFoundDevice(peripheral, reason: connectionReason)
2067
+ }
2068
+ }
2069
+
2070
+ // MARK: - Enhanced Connection Helper
2071
+
2072
+ private func connectToFoundDevice(_ peripheral: CBPeripheral, reason: String) {
2073
+ guard self.peripheral == nil else {
2074
+ Bridge.log(
2075
+ "NEX-CONN: ⚠️ Already connected/connecting to a device, ignoring new connect request for '\(peripheral.name ?? "Unknown")'"
2076
+ )
2077
+ return
2078
+ }
2079
+
2080
+ Bridge.log(
2081
+ "NEX-CONN: 🔗 Connecting to device '\(peripheral.name ?? "Unknown")' - Reason: \(reason)"
2082
+ )
2083
+
2084
+ // Stop scanning since we found our target
2085
+ if _isScanning {
2086
+ stopScan()
2087
+ }
2088
+
2089
+ // Store the peripheral and initiate connection
2090
+ self.peripheral = peripheral
2091
+ isConnecting = true
2092
+ connectionState = ConnTypes.CONNECTING
2093
+
2094
+ // Use connection options for better reliability (from Java implementation)
2095
+ let connectionOptions: [String: Any] = [
2096
+ CBConnectPeripheralOptionNotifyOnConnectionKey: true,
2097
+ CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
2098
+ CBConnectPeripheralOptionNotifyOnNotificationKey: true,
2099
+ ]
2100
+
2101
+ centralManager?.connect(peripheral, options: connectionOptions)
2102
+
2103
+ Bridge.log("NEX-CONN: 🚀 Connection initiated with enhanced options")
2104
+ }
2105
+
2106
+ func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
2107
+ Bridge.log("NEX-CONN: ✅ Successfully connected to \(peripheral.name ?? "unknown device").")
2108
+ isConnecting = false
2109
+ peripheralUUID = peripheral.identifier // Persist UUID
2110
+ stopReconnectionTimer() // Successfully connected, stop trying to reconnect.
2111
+
2112
+ // Enhanced device info saving (from Java implementation)
2113
+ let deviceName = peripheral.name
2114
+ let deviceAddress = peripheral.identifier.uuidString
2115
+
2116
+ // Save all device information for future reconnection
2117
+ savePairedDeviceInfo(name: deviceName, address: deviceAddress)
2118
+
2119
+ // Extract and save device ID if possible
2120
+ if let deviceName, let deviceId = extractDeviceId(from: deviceName) {
2121
+ savePreferredDeviceId(deviceId)
2122
+ }
2123
+
2124
+ Bridge.log("NEX-CONN: 💾 Device information saved for reliable reconnection")
2125
+ peripheral.delegate = self
2126
+ Bridge.log("NEX-CONN: 🔍 Discovering services...")
2127
+ peripheral.discoverServices([MAIN_SERVICE_UUID])
2128
+
2129
+ // Reset any failed connection attempt counters
2130
+ reconnectionAttempts = 0
2131
+ Bridge.log("NEX-CONN: 🔄 Reset reconnection attempts counter")
2132
+ }
2133
+
2134
+ func centralManager(
2135
+ _: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?
2136
+ ) {
2137
+ Bridge.log(
2138
+ "NEX-CONN: ❌ Failed to connect to peripheral \(peripheral.name ?? "Unknown"). Error: \(error?.localizedDescription ?? "unknown")"
2139
+ )
2140
+ isConnecting = false
2141
+ connectionState = ConnTypes.DISCONNECTED
2142
+ self.peripheral = nil // Reset peripheral on failure to allow reconnection
2143
+ // Optionally, start reconnection attempts here
2144
+ if !isDisconnecting, !isKilled {
2145
+ startReconnectionTimer()
2146
+ }
2147
+ }
2148
+
2149
+ func centralManager(
2150
+ _: CBCentralManager, didDisconnectPeripheral disconnectedPeripheral: CBPeripheral,
2151
+ error: Error?
2152
+ ) {
2153
+ Bridge.log(
2154
+ "NEX-CONN: 🔌 Disconnected from peripheral: \(disconnectedPeripheral.name ?? "Unknown")"
2155
+ )
2156
+
2157
+ if let error {
2158
+ Bridge.log("NEX-CONN: ⚠️ Disconnect error: \(error.localizedDescription)")
2159
+ }
2160
+
2161
+ // Reset connection state
2162
+ // Save microphone state before disconnection (like Java implementation)
2163
+ saveMicrophoneStateBeforeDisconnection()
2164
+
2165
+ // Reset protobuf version posted flag for next connection (like Java implementation)
2166
+ protobufVersionPosted = false
2167
+
2168
+ // Stop mic beat system (like Java implementation)
2169
+ stopMicBeat()
2170
+
2171
+ nexReady = false
2172
+ deviceReady = false
2173
+ // batteryLevel = -1
2174
+ // charging = false
2175
+ DeviceStore.shared.apply("glasses", "batteryLevel", -1)
2176
+ DeviceStore.shared.apply("glasses", "charging", false)
2177
+ vadActive = false
2178
+ compressedVoiceData = .init()
2179
+ aiListening = false
2180
+ deviceFirmwareVersion = ""
2181
+ deviceHardwareModel = ""
2182
+ accelerometer = [0.0, 0.0, 0.0]
2183
+ gyroscope = [0.0, 0.0, 0.0]
2184
+ magnetometer = [0.0, 0.0, 0.0]
2185
+ lastButtonPressed = -1
2186
+ lastButtonState = ""
2187
+ lastHeadGesture = ""
2188
+ headUpAngle = 0
2189
+
2190
+ peripheral = nil
2191
+ writeCharacteristic = nil
2192
+ notifyCharacteristic = nil
2193
+ connectionState = ConnTypes.DISCONNECTED
2194
+
2195
+ // Clear command queue if needed
2196
+ if isQueueWorkerRunning {
2197
+ Bridge.log("NEX-CONN: 🧹 Clearing command queue due to disconnection")
2198
+ }
2199
+
2200
+ if !isDisconnecting, !isKilled {
2201
+ Bridge.log("NEX-CONN: 🔄 Unintentional disconnect detected. Attempting reconnection...")
2202
+
2203
+ // Enhanced reconnection strategy from Java implementation
2204
+ if let savedName = savedDeviceName {
2205
+ Bridge.log("NEX-CONN: 🎯 Will attempt to reconnect to saved device: \(savedName)")
2206
+ }
2207
+
2208
+ startReconnectionTimer()
2209
+ } else {
2210
+ Bridge.log(
2211
+ "NEX-CONN: ✅ Intentional disconnect (isDisconnecting: \(isDisconnecting), isKilled: \(isKilled))"
2212
+ )
2213
+
2214
+ if isDisconnecting {
2215
+ // Don't clear device info on intentional disconnect - user might reconnect later
2216
+ Bridge.log("NEX-CONN: 💾 Keeping device info for potential future reconnection")
2217
+ }
2218
+ }
2219
+ }
2220
+
2221
+ // MARK: - MTU Negotiation (iOS-specific implementation)
2222
+
2223
+ private func requestOptimalMTU(for peripheral: CBPeripheral) {
2224
+ Bridge.log("NEX-CONN: negotiating MTU")
2225
+ Bridge.log("NEX: 🔍 iOS MTU Discovery (Platform Limitation: max \(MTU_MAX_IOS) bytes)")
2226
+ Bridge.log("NEX: 🎯 iOS maximum: \(MTU_MAX_IOS) bytes, default: \(MTU_DEFAULT) bytes")
2227
+
2228
+ // iOS MTU is automatically negotiated - we can only discover the current value
2229
+ // No manual MTU request available on iOS (platform limitation)
2230
+
2231
+ // Get current MTU capability (iOS-specific approach)
2232
+ let maxWriteLength = peripheral.maximumWriteValueLength(for: .withResponse)
2233
+ let actualMTU = maxWriteLength + 3 // Add L2CAP header size
2234
+
2235
+ Bridge.log("NEX: 📊 iOS MTU Discovery Results:")
2236
+ Bridge.log("NEX: 📏 Max write length: \(maxWriteLength) bytes")
2237
+ Bridge.log("NEX: 📡 Effective MTU: \(actualMTU) bytes")
2238
+
2239
+ // Validate against iOS limitations
2240
+ let validatedMTU = min(actualMTU, MTU_MAX_IOS)
2241
+ if actualMTU > MTU_MAX_IOS {
2242
+ Bridge.log("NEX: 🔧 Clamping MTU from \(actualMTU) to iOS maximum: \(MTU_MAX_IOS)")
2243
+ }
2244
+
2245
+ // Process MTU result immediately (iOS doesn't have callback like Android)
2246
+ onMTUNegotiated(mtu: validatedMTU, success: true)
2247
+
2248
+ // After MTU is set, start device initialization sequence (from Java implementation)
2249
+ initializeNexDevice()
2250
+ }
2251
+
2252
+ private func onMTUNegotiated(mtu: Int, success: Bool) {
2253
+ Bridge.log("NEX-CONN: 🔄 MTU Negotiation Result: Success=\(success), Device MTU=\(mtu)")
2254
+
2255
+ if success, mtu > MTU_DEFAULT {
2256
+ // Store device capability and calculate actual negotiated MTU
2257
+ deviceMaxMTU = mtu
2258
+ // iOS limitation: Use actual MTU but cap at iOS maximum
2259
+ currentMTU = min(MTU_MAX_IOS, mtu)
2260
+
2261
+ Bridge.log("NEX: 🎯 iOS MTU Configuration Complete:")
2262
+ Bridge.log("NEX: 🍎 iOS Platform Max: \(MTU_MAX_IOS) bytes")
2263
+ Bridge.log("NEX: 📡 Device Supports: \(deviceMaxMTU) bytes")
2264
+ Bridge.log("NEX: 🤝 Final MTU: \(currentMTU) bytes")
2265
+
2266
+ // Calculate optimal chunk sizes based on iOS MTU constraints
2267
+ maxChunkSize = currentMTU - 10 // Reserve 10 bytes for headers
2268
+ bmpChunkSize = currentMTU - 6 // Reserve 6 bytes for image headers
2269
+
2270
+ Bridge.log("NEX: 📦 Optimized Chunk Sizes:")
2271
+ Bridge.log("NEX: 📄 Data Chunk Size: \(maxChunkSize) bytes")
2272
+ Bridge.log("NEX: 🖼️ Image Chunk Size: \(bmpChunkSize) bytes")
2273
+
2274
+ } else {
2275
+ Bridge.log("NEX: ⚠️ MTU negotiation failed or using minimum, applying iOS defaults")
2276
+ currentMTU = MTU_DEFAULT
2277
+ deviceMaxMTU = MTU_DEFAULT
2278
+ maxChunkSize = 20 // Very conservative for 23-byte MTU
2279
+ bmpChunkSize = 20 // Very conservative for 23-byte MTU
2280
+
2281
+ Bridge.log("NEX: 📋 iOS Fallback Configuration:")
2282
+ Bridge.log("NEX: 📊 Default MTU: \(MTU_DEFAULT) bytes")
2283
+ Bridge.log("NEX: 📦 Data Chunk Size: \(maxChunkSize) bytes")
2284
+ Bridge.log("NEX: 🖼️ Image Chunk Size: \(bmpChunkSize) bytes")
2285
+ Bridge.log("NEX: ⚠️ Using minimal chunks due to MTU limitation")
2286
+ }
2287
+
2288
+ // Device is now ready for communication
2289
+ Bridge.log("NEX-CONN: ✅ Device initialization complete - ready for communication")
2290
+ nexReady = true
2291
+ connectionState = ConnTypes.CONNECTED
2292
+
2293
+ // Update @Published property for device ready state
2294
+ deviceReady = true
2295
+
2296
+ // Initialize command queue worker to process queued commands
2297
+ setupCommandQueue()
2298
+
2299
+ // Emit device ready event to React Native
2300
+ // emitDeviceReady()
2301
+ }
2302
+
2303
+ // MARK: - Device Initialization (ported from Java MentraNexSGC)
2304
+
2305
+ private func initializeNexDevice() {
2306
+ Bridge.log("NEX-CONN: 🚀 Starting Nex device initialization (matching Java sequence)")
2307
+
2308
+ // Exact Java initialization sequence from lines 648-691:
2309
+
2310
+ // 1. Do first battery status query (Java line 650)
2311
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { // 10ms delay like Java
2312
+ Bridge.log("NEX: 🔋 Sending first battery status query")
2313
+ self.queryBatteryStatus()
2314
+ }
2315
+
2316
+ // 2. Restore previous microphone state (Java lines 657-665)
2317
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { // 20ms delay
2318
+ let shouldRestoreMic = UserDefaults.standard.bool(
2319
+ forKey: "microphoneStateBeforeDisconnection"
2320
+ )
2321
+ Bridge.log("NEX: 🎤 Restoring microphone state to: \(shouldRestoreMic)")
2322
+
2323
+ if shouldRestoreMic {
2324
+ self.startMicBeat()
2325
+ } else {
2326
+ self.stopMicBeat()
2327
+ }
2328
+ }
2329
+
2330
+ // 3. Enable AugmentOS notification key (Java line 668)
2331
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { // 30ms delay
2332
+ self.sendWhiteListCommand()
2333
+ }
2334
+
2335
+ // 4. Show home screen to turn on the NexGlasses display (Java line 673)
2336
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // 50ms delay
2337
+ self.showHomeScreen()
2338
+ }
2339
+
2340
+ // 5. Post protobuf schema version information (Java lines 684-687)
2341
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // 100ms delay
2342
+ self.postProtobufSchemaVersionInfo()
2343
+ }
2344
+
2345
+ // 6. Query glasses protobuf version from firmware (Java line 690)
2346
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { // 150ms delay
2347
+ self.queryGlassesInfo()
2348
+ }
2349
+
2350
+ Bridge.log("NEX-CONN: ✅ Java-compatible initialization sequence started")
2351
+ }
2352
+
2353
+ private func emitDeviceReady() {
2354
+ let eventBody: [String: Any] = [
2355
+ "device_ready": [
2356
+ "model_name": "Mentra Display",
2357
+ "mtu_negotiated": currentMTU,
2358
+ "max_chunk_size": maxChunkSize,
2359
+ "connection_state": "ready",
2360
+ ],
2361
+ ]
2362
+
2363
+ // Use the standardized Bridge.sendTypedMessage helper for consistent type field handling
2364
+ Bridge.sendTypedMessage("device_ready", body: eventBody)
2365
+ Bridge.log("NEX: 📡 Emitted device ready event with MTU: \(currentMTU)")
2366
+ }
2367
+
2368
+ // MARK: - CBPeripheralDelegate
2369
+
2370
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
2371
+ if let error {
2372
+ Bridge.log("NEX-CONN: ❌ Error discovering services: \(error.localizedDescription)")
2373
+ return
2374
+ }
2375
+
2376
+ guard let services = peripheral.services else {
2377
+ Bridge.log("NEX-CONN: ⚠️ No services found for peripheral.")
2378
+ return
2379
+ }
2380
+ for service in services {
2381
+ if service.uuid == MAIN_SERVICE_UUID {
2382
+ Bridge.log("NEX-CONN: ✅ Found main service. Discovering characteristics...")
2383
+ peripheral.discoverCharacteristics(
2384
+ [WRITE_CHAR_UUID, NOTIFY_CHAR_UUID], for: service
2385
+ )
2386
+ }
2387
+ }
2388
+ }
2389
+
2390
+ func peripheral(
2391
+ _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?
2392
+ ) {
2393
+ if let error {
2394
+ Bridge.log(
2395
+ "NEX-CONN: ❌ Error discovering characteristics: \(error.localizedDescription)"
2396
+ )
2397
+ return
2398
+ }
2399
+
2400
+ guard let characteristics = service.characteristics else {
2401
+ Bridge.log("NEX-CONN: ⚠️ No characteristics found for service \(service.uuid).")
2402
+ return
2403
+ }
2404
+ for characteristic in characteristics {
2405
+ if characteristic.uuid == WRITE_CHAR_UUID {
2406
+ Bridge.log("NEX-CONN: ✅ Found write characteristic.")
2407
+ writeCharacteristic = characteristic
2408
+ } else if characteristic.uuid == NOTIFY_CHAR_UUID {
2409
+ Bridge.log(
2410
+ "NEX-CONN: ✅ Found notify characteristic. Subscribing for notifications."
2411
+ )
2412
+ notifyCharacteristic = characteristic
2413
+ peripheral.setNotifyValue(true, for: characteristic)
2414
+ }
2415
+ }
2416
+
2417
+ if writeCharacteristic != nil, notifyCharacteristic != nil {
2418
+ Bridge.log(
2419
+ "NEX-CONN: ✅ All required characteristics discovered. Proceeding to MTU negotiation."
2420
+ )
2421
+
2422
+ // Start MTU negotiation like Java implementation
2423
+ requestOptimalMTU(for: peripheral)
2424
+ }
2425
+ }
2426
+
2427
+ func peripheral(
2428
+ _: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
2429
+ ) {
2430
+ if let error {
2431
+ Bridge.log("NEX-CONN: ❌ Error on updating value: \(error.localizedDescription)")
2432
+ return
2433
+ }
2434
+
2435
+ guard let data = characteristic.value else {
2436
+ Bridge.log("NEX-CONN: ⚠️ Received notification with no data.")
2437
+ return
2438
+ }
2439
+ Bridge.log("NEX-CONN: 📥 Received data (\(data.count) bytes): \(data.toHexString())")
2440
+
2441
+ // Process the received data based on packet type
2442
+ processReceivedData(data)
2443
+ }
2444
+
2445
+ func peripheral(
2446
+ _: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
2447
+ ) {
2448
+ if let error {
2449
+ Bridge.log(
2450
+ "NEX-CONN: ❌ Error writing value to \(characteristic.uuid): \(error.localizedDescription)"
2451
+ )
2452
+ return
2453
+ }
2454
+ // This log can be very noisy, so it's commented out.
2455
+ // Bridge.log("NEX-CONN: 📤 Successfully wrote value to \(characteristic.uuid).")
2456
+ }
2457
+
2458
+ func peripheral(
2459
+ _: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
2460
+ error: Error?
2461
+ ) {
2462
+ if let error {
2463
+ Bridge.log(
2464
+ "NEX-CONN: ❌ Error changing notification state for \(characteristic.uuid): \(error.localizedDescription)"
2465
+ )
2466
+ return
2467
+ }
2468
+
2469
+ if characteristic.isNotifying {
2470
+ Bridge.log(
2471
+ "NEX-CONN: ✅ Successfully subscribed to notifications for characteristic \(characteristic.uuid.uuidString)."
2472
+ )
2473
+ } else {
2474
+ Bridge.log(
2475
+ "NEX-CONN: unsubscribed from notifications for characteristic \(characteristic.uuid.uuidString)."
2476
+ )
2477
+ }
2478
+ }
2479
+ }