@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,2439 @@
1
+ import CoreBluetooth
2
+ import Foundation
3
+
4
+ private func intValue(_ value: Any?) -> Int? {
5
+ if let int = value as? Int { return int }
6
+ if let double = value as? Double { return Int(double) }
7
+ if let number = value as? NSNumber { return number.intValue }
8
+ return nil
9
+ }
10
+
11
+ private func stringValue(_ values: [String: Any], _ keys: String...) -> String? {
12
+ stringValue(values, keys)
13
+ }
14
+
15
+ private func stringValue(_ values: [String: Any], _ keys: [String]) -> String? {
16
+ for key in keys {
17
+ if let value = values[key] as? String {
18
+ return value
19
+ }
20
+ }
21
+ return nil
22
+ }
23
+
24
+ private func boolValue(_ values: [String: Any], _ keys: String...) -> Bool? {
25
+ boolValue(values, keys)
26
+ }
27
+
28
+ private func boolValue(_ values: [String: Any], _ keys: [String]) -> Bool? {
29
+ for key in keys {
30
+ if let value = values[key] as? Bool { return value }
31
+ if let value = values[key] as? NSNumber { return value.boolValue }
32
+ }
33
+ return nil
34
+ }
35
+
36
+ private func hasAnyKey(_ values: [String: Any], _ keys: String...) -> Bool {
37
+ hasAnyKey(values, keys)
38
+ }
39
+
40
+ private func hasAnyKey(_ values: [String: Any], _ keys: [String]) -> Bool {
41
+ keys.contains { values.keys.contains($0) }
42
+ }
43
+
44
+ private func optionalStringValue(_ values: [String: Any], _ keys: String...) -> String? {
45
+ hasAnyKey(values, keys) ? (stringValue(values, keys) ?? "") : nil
46
+ }
47
+
48
+ private func nonEmptyStringValue(_ values: [String: Any], _ keys: String...) -> String? {
49
+ for key in keys {
50
+ guard let value = values[key] as? String else { continue }
51
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
52
+ if !trimmed.isEmpty {
53
+ return value
54
+ }
55
+ }
56
+ return nil
57
+ }
58
+
59
+ private func optionalIntValue(_ values: [String: Any], _ keys: String...) -> Int? {
60
+ guard hasAnyKey(values, keys) else { return nil }
61
+ for key in keys {
62
+ if let value = intValue(values[key]) { return value }
63
+ }
64
+ return nil
65
+ }
66
+
67
+ private func optionalBoolValue(_ values: [String: Any], _ keys: String...) -> Bool? {
68
+ hasAnyKey(values, keys) ? (boolValue(values, keys) ?? false) : nil
69
+ }
70
+
71
+ private func stringListValue(_ values: [String: Any], _ key: String) -> [String] {
72
+ values[key] as? [String] ?? []
73
+ }
74
+
75
+ private func optionalStringListValue(_ values: [String: Any], _ key: String) -> [String]? {
76
+ values.keys.contains(key) ? stringListValue(values, key) : nil
77
+ }
78
+
79
+ private func dictionaryListValue(_ values: [String: Any], _ key: String) -> [[String: Any]] {
80
+ values[key] as? [[String: Any]] ?? []
81
+ }
82
+
83
+ private func optionalDictionaryListValue(_ values: [String: Any], _ key: String) -> [[String: Any]]? {
84
+ values.keys.contains(key) ? dictionaryListValue(values, key) : nil
85
+ }
86
+
87
+ private func putIfNotNil(_ map: inout [String: Any], _ key: String, _ value: Any?) {
88
+ if let value {
89
+ map[key] = value
90
+ }
91
+ }
92
+
93
+ public struct MentraBluetoothSDKConfiguration {
94
+ public static let `default` = MentraBluetoothSDKConfiguration()
95
+
96
+ public init() {}
97
+ }
98
+
99
+ public enum DeviceModel: String {
100
+ case g1
101
+ case g2
102
+ case mentraLive
103
+ case mentraNex
104
+ case mach1
105
+ case z100
106
+ case frame
107
+ case simulated
108
+ case r1
109
+
110
+ public var deviceType: String {
111
+ switch self {
112
+ case .g1:
113
+ DeviceTypes.G1
114
+ case .g2:
115
+ DeviceTypes.G2
116
+ case .mentraLive:
117
+ DeviceTypes.LIVE
118
+ case .mentraNex:
119
+ DeviceTypes.NEX
120
+ case .mach1:
121
+ DeviceTypes.MACH1
122
+ case .z100:
123
+ DeviceTypes.Z100
124
+ case .frame:
125
+ DeviceTypes.FRAME
126
+ case .simulated:
127
+ DeviceTypes.SIMULATED
128
+ case .r1:
129
+ ControllerTypes.R1
130
+ }
131
+ }
132
+
133
+ public static func fromDeviceType(_ deviceType: String?) -> DeviceModel {
134
+ switch deviceType {
135
+ case DeviceTypes.G1:
136
+ .g1
137
+ case DeviceTypes.G2:
138
+ .g2
139
+ case DeviceTypes.LIVE:
140
+ .mentraLive
141
+ case DeviceTypes.NEX:
142
+ .mentraNex
143
+ case DeviceTypes.MACH1:
144
+ .mach1
145
+ case DeviceTypes.Z100:
146
+ .z100
147
+ case DeviceTypes.FRAME:
148
+ .frame
149
+ case DeviceTypes.SIMULATED:
150
+ .simulated
151
+ case ControllerTypes.R1:
152
+ .r1
153
+ default:
154
+ .mentraLive
155
+ }
156
+ }
157
+ }
158
+
159
+ public struct Device: Identifiable, Equatable, CustomStringConvertible {
160
+ public let model: DeviceModel
161
+ public let name: String
162
+ public let identifier: String?
163
+ public let rssi: Int?
164
+ public let id: String
165
+
166
+ public init(
167
+ model: DeviceModel,
168
+ name: String,
169
+ identifier: String? = nil,
170
+ rssi: Int? = nil,
171
+ id: String? = nil
172
+ ) {
173
+ self.model = model
174
+ self.name = name
175
+ self.identifier = identifier
176
+ self.rssi = rssi
177
+ self.id = id ?? identifier.flatMap { $0.isEmpty ? nil : $0 } ?? "\(model.deviceType):\(name)"
178
+ }
179
+
180
+ public var description: String {
181
+ "Device(model: \(model), name: \(name))"
182
+ }
183
+
184
+ var dictionary: [String: Any] {
185
+ var values: [String: Any] = [
186
+ "id": id,
187
+ "model": model.deviceType,
188
+ "name": name,
189
+ ]
190
+ if let identifier, !identifier.isEmpty {
191
+ values["address"] = identifier
192
+ }
193
+ if let rssi {
194
+ values["rssi"] = rssi
195
+ }
196
+ return values
197
+ }
198
+
199
+ init?(values: [String: Any]) {
200
+ guard let model = stringValue(values, "model", "deviceModel", "device_model") else { return nil }
201
+ guard let name = stringValue(values, "name", "deviceName", "device_name") else { return nil }
202
+ let identifier = stringValue(values, "address", "deviceAddress", "device_address").flatMap { $0.isEmpty ? nil : $0 }
203
+ let rssi = intValue(values["rssi"]) ?? intValue(values["signalStrength"]) ?? intValue(values["signal_strength"])
204
+ self.init(
205
+ model: DeviceModel.fromDeviceType(model),
206
+ name: name,
207
+ identifier: identifier,
208
+ rssi: rssi,
209
+ id: stringValue(values, "id")
210
+ )
211
+ }
212
+ }
213
+
214
+ public struct ConnectOptions {
215
+ public let saveAsDefault: Bool
216
+ public let cancelExistingConnectionAttempt: Bool
217
+
218
+ public init(saveAsDefault: Bool = true, cancelExistingConnectionAttempt: Bool = true) {
219
+ self.saveAsDefault = saveAsDefault
220
+ self.cancelExistingConnectionAttempt = cancelExistingConnectionAttempt
221
+ }
222
+ }
223
+
224
+ public struct WifiScanResult: CustomStringConvertible {
225
+ public let ssid: String
226
+ public let requiresPassword: Bool
227
+ public let signalStrength: Int
228
+ public let frequency: Int?
229
+
230
+ public init(ssid: String, requiresPassword: Bool, signalStrength: Int, frequency: Int? = nil) {
231
+ self.ssid = ssid
232
+ self.requiresPassword = requiresPassword
233
+ self.signalStrength = signalStrength
234
+ self.frequency = frequency
235
+ }
236
+
237
+ init(values: [String: Any]) {
238
+ ssid = stringValue(values, "ssid") ?? ""
239
+ requiresPassword = boolValue(values, "requiresPassword", "requires_password", "auth_required") ?? false
240
+ signalStrength = intValue(values["signalStrength"] ?? values["signal_strength"] ?? values["rssi"]) ?? -1
241
+ frequency = intValue(values["frequency"])
242
+ }
243
+
244
+ var dictionary: [String: Any] {
245
+ var values: [String: Any] = [
246
+ "ssid": ssid,
247
+ "requiresPassword": requiresPassword,
248
+ "signalStrength": signalStrength,
249
+ ]
250
+ if let frequency {
251
+ values["frequency"] = frequency
252
+ }
253
+ return values
254
+ }
255
+
256
+ public var description: String {
257
+ "WifiScanResult(ssid: \(ssid), signalStrength: \(signalStrength))"
258
+ }
259
+ }
260
+
261
+ public enum GlassesConnectionState: String, CustomStringConvertible, Equatable {
262
+ case disconnected = "DISCONNECTED"
263
+ case scanning = "SCANNING"
264
+ case connecting = "CONNECTING"
265
+ case bonding = "BONDING"
266
+ case connected = "CONNECTED"
267
+
268
+ public init(_ value: String?) {
269
+ self = Self.fromValue(value) ?? .disconnected
270
+ }
271
+
272
+ public static func fromValue(_ value: String?) -> GlassesConnectionState? {
273
+ guard let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines).uppercased(),
274
+ !normalized.isEmpty
275
+ else {
276
+ return nil
277
+ }
278
+ return Self(rawValue: normalized)
279
+ }
280
+
281
+ public var isConnected: Bool {
282
+ self == .connected
283
+ }
284
+
285
+ public var isBusy: Bool {
286
+ self == .scanning || self == .connecting || self == .bonding
287
+ }
288
+
289
+ func statusValues(connected: Bool, fullyBooted: Bool) -> [String: Any] {
290
+ if self == .connected || connected || fullyBooted {
291
+ return ["state": "connected", "fullyBooted": fullyBooted]
292
+ }
293
+ switch self {
294
+ case .scanning:
295
+ return ["state": "scanning"]
296
+ case .connecting:
297
+ return ["state": "connecting"]
298
+ case .bonding:
299
+ return ["state": "bonding"]
300
+ case .connected:
301
+ return ["state": "connected", "fullyBooted": fullyBooted]
302
+ case .disconnected:
303
+ return ["state": "disconnected"]
304
+ }
305
+ }
306
+
307
+ public var description: String {
308
+ rawValue
309
+ }
310
+ }
311
+
312
+ public struct GlassesStatus: CustomStringConvertible {
313
+ let values: [String: Any]
314
+
315
+ init(values: [String: Any]) {
316
+ self.values = values
317
+ }
318
+
319
+ public func applying(_ update: GlassesStatusUpdate) -> GlassesStatus {
320
+ GlassesStatus(values: values.merging(update.values) { _, new in new })
321
+ }
322
+
323
+ public func withBattery(level: Int, charging: Bool) -> GlassesStatus {
324
+ applying(GlassesStatusUpdate(values: ["batteryLevel": level, "charging": charging]))
325
+ }
326
+
327
+ public func withWifi(_ wifi: WifiStatus) -> GlassesStatus {
328
+ applying(GlassesStatusUpdate(values: wifi.storeValues))
329
+ }
330
+
331
+ public func withHotspot(_ hotspot: HotspotStatus) -> GlassesStatus {
332
+ applying(GlassesStatusUpdate(values: hotspot.storeValues))
333
+ }
334
+
335
+ public func disconnected() -> GlassesStatus {
336
+ applying(GlassesStatusUpdate(values: [
337
+ "connected": false,
338
+ "connectionState": "DISCONNECTED",
339
+ "fullyBooted": false,
340
+ "batteryLevel": -1,
341
+ "charging": false,
342
+ "hotspotEnabled": false,
343
+ "hotspotGatewayIp": "",
344
+ "hotspotPassword": "",
345
+ "hotspotSsid": "",
346
+ "wifiConnected": false,
347
+ "wifiSsid": "",
348
+ "wifiLocalIp": "",
349
+ "signalStrength": -1,
350
+ "signalStrengthUpdatedAt": 0,
351
+ ]))
352
+ }
353
+
354
+ public var fullyBooted: Bool { boolValue(values, "fullyBooted") ?? false }
355
+ public var connected: Bool { boolValue(values, "connected") ?? false }
356
+ public var micEnabled: Bool { boolValue(values, "micEnabled") ?? false }
357
+ public var connectionState: GlassesConnectionState { GlassesConnectionState(stringValue(values, "connectionState")) }
358
+ public var btcConnected: Bool { boolValue(values, "btcConnected") ?? false }
359
+ public var signalStrength: Int { intValue(values["signalStrength"]) ?? -1 }
360
+ public var signalStrengthUpdatedAt: Int { intValue(values["signalStrengthUpdatedAt"]) ?? 0 }
361
+ public var deviceModel: String { stringValue(values, "deviceModel") ?? "" }
362
+ public var androidVersion: String { stringValue(values, "androidVersion") ?? "" }
363
+ public var firmwareVersion: String { stringValue(values, "firmwareVersion", "fwVersion") ?? "" }
364
+ public var besFirmwareVersion: String { stringValue(values, "besFwVersion", "besFirmwareVersion") ?? "" }
365
+ public var mtkFirmwareVersion: String { stringValue(values, "mtkFwVersion", "mtkFirmwareVersion") ?? "" }
366
+ public var btMacAddress: String { stringValue(values, "btMacAddress") ?? "" }
367
+ public var leftMacAddress: String { stringValue(values, "leftMacAddress") ?? "" }
368
+ public var rightMacAddress: String { stringValue(values, "rightMacAddress") ?? "" }
369
+ public var macAddress: String { stringValue(values, "macAddress") ?? "" }
370
+ public var buildNumber: String { stringValue(values, "buildNumber") ?? "" }
371
+ public var otaVersionUrl: String { stringValue(values, "otaVersionUrl") ?? "" }
372
+ public var appVersion: String { stringValue(values, "appVersion") ?? "" }
373
+ public var bluetoothName: String { stringValue(values, "bluetoothName") ?? "" }
374
+ public var serialNumber: String { stringValue(values, "serialNumber") ?? "" }
375
+ public var style: String { stringValue(values, "style") ?? "" }
376
+ public var color: String { stringValue(values, "color") ?? "" }
377
+ public var wifi: WifiStatus { WifiStatus.fromStoreValues(values) ?? .disconnected }
378
+ public var hotspot: HotspotStatus { HotspotStatus.fromStoreValues(values) ?? .disabled }
379
+ public var dictionary: [String: Any] { Self.dictionary(from: values) }
380
+ public var batteryLevel: Int { intValue(values["batteryLevel"]) ?? -1 }
381
+ public var charging: Bool { boolValue(values, "charging") ?? false }
382
+ public var caseBatteryLevel: Int { intValue(values["caseBatteryLevel"]) ?? -1 }
383
+ public var caseCharging: Bool { boolValue(values, "caseCharging") ?? false }
384
+ public var caseOpen: Bool { boolValue(values, "caseOpen") ?? true }
385
+ public var caseRemoved: Bool { boolValue(values, "caseRemoved") ?? true }
386
+ public var headUp: Bool { boolValue(values, "headUp") ?? false }
387
+ public var controllerConnected: Bool { boolValue(values, "controllerConnected") ?? false }
388
+ public var controllerFullyBooted: Bool { boolValue(values, "controllerFullyBooted") ?? false }
389
+ public var controllerMacAddress: String { stringValue(values, "controllerMacAddress") ?? "" }
390
+ public var controllerBatteryLevel: Int { intValue(values["controllerBatteryLevel"]) ?? -1 }
391
+ public var controllerSignalStrength: Int { intValue(values["controllerSignalStrength"]) ?? -1 }
392
+ public var ringSignalStrength: Int { intValue(values["ringSignalStrength"]) ?? -1 }
393
+
394
+ public var description: String {
395
+ values.description
396
+ }
397
+
398
+ static func dictionary(from values: [String: Any]) -> [String: Any] {
399
+ var dictionary = values
400
+ dictionary["connection"] = GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
401
+ connected: boolValue(values, "connected") ?? false,
402
+ fullyBooted: boolValue(values, "fullyBooted") ?? false
403
+ )
404
+ dictionary.removeValue(forKey: "connected")
405
+ dictionary.removeValue(forKey: "fullyBooted")
406
+ dictionary.removeValue(forKey: "connectionState")
407
+ dictionary["wifi"] = (WifiStatus.fromStoreValues(values) ?? .disconnected).values
408
+ dictionary["hotspot"] = (HotspotStatus.fromStoreValues(values) ?? .disabled).values
409
+ dictionary.removeValue(forKey: "wifiConnected")
410
+ dictionary.removeValue(forKey: "wifiSsid")
411
+ dictionary.removeValue(forKey: "wifiLocalIp")
412
+ dictionary.removeValue(forKey: "hotspotEnabled")
413
+ dictionary.removeValue(forKey: "hotspotSsid")
414
+ dictionary.removeValue(forKey: "hotspotPassword")
415
+ dictionary.removeValue(forKey: "hotspotGatewayIp")
416
+ dictionary.removeValue(forKey: "hotspotLocalIp")
417
+ return dictionary
418
+ }
419
+
420
+ static func updateDictionary(from values: [String: Any]) -> [String: Any] {
421
+ var dictionary = values
422
+ if hasAnyKey(values, "connection", "connected", "fullyBooted", "connectionState") {
423
+ dictionary["connection"] = (values["connection"] as? [String: Any])
424
+ ?? GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
425
+ connected: boolValue(values, "connected") ?? false,
426
+ fullyBooted: boolValue(values, "fullyBooted") ?? false
427
+ )
428
+ dictionary.removeValue(forKey: "connected")
429
+ dictionary.removeValue(forKey: "fullyBooted")
430
+ dictionary.removeValue(forKey: "connectionState")
431
+ }
432
+ if hasAnyKey(values, "wifi", "wifiConnected", "wifiSsid", "wifiLocalIp") {
433
+ let wifi = (values["wifi"] as? [String: Any]).flatMap(WifiStatus.init(values:))
434
+ ?? WifiStatus.fromStoreValues(values)
435
+ if let wifi {
436
+ dictionary["wifi"] = wifi.values
437
+ }
438
+ dictionary.removeValue(forKey: "wifiConnected")
439
+ dictionary.removeValue(forKey: "wifiSsid")
440
+ dictionary.removeValue(forKey: "wifiLocalIp")
441
+ }
442
+ if hasAnyKey(values, "hotspot") {
443
+ if let hotspot = (values["hotspot"] as? [String: Any]).flatMap(HotspotStatus.init(values:)) {
444
+ dictionary["hotspot"] = hotspot.values
445
+ }
446
+ } else if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp", "hotspotLocalIp") {
447
+ if let hotspot = HotspotStatus.fromStoreValues(values) {
448
+ dictionary["hotspot"] = hotspot.values
449
+ }
450
+ dictionary.removeValue(forKey: "hotspotEnabled")
451
+ dictionary.removeValue(forKey: "hotspotSsid")
452
+ dictionary.removeValue(forKey: "hotspotPassword")
453
+ dictionary.removeValue(forKey: "hotspotGatewayIp")
454
+ dictionary.removeValue(forKey: "hotspotLocalIp")
455
+ }
456
+ return dictionary
457
+ }
458
+ }
459
+
460
+ public struct BluetoothStatus: CustomStringConvertible {
461
+ let values: [String: Any]
462
+
463
+ init(values: [String: Any]) {
464
+ self.values = Self.normalized(values)
465
+ }
466
+
467
+ private static func normalized(_ values: [String: Any]) -> [String: Any] {
468
+ var normalizedValues = values
469
+ if let searchResults = values["searchResults"] as? [[String: Any]] {
470
+ normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
471
+ }
472
+ return normalizedValues
473
+ }
474
+
475
+ public func applying(_ update: BluetoothStatusUpdate) -> BluetoothStatus {
476
+ BluetoothStatus(values: values.merging(update.values) { _, new in new })
477
+ }
478
+
479
+ public func withDefaultDevice(_ device: Device?) -> BluetoothStatus {
480
+ applying(BluetoothStatusUpdate(values: [
481
+ "default_wearable": device?.model.deviceType ?? "",
482
+ "device_name": device?.name ?? "",
483
+ "device_address": device?.identifier ?? "",
484
+ ]))
485
+ }
486
+
487
+ public var searching: Bool { boolValue(values, "searching") ?? false }
488
+ public var searchingController: Bool { boolValue(values, "searchingController") ?? false }
489
+ public var systemMicUnavailable: Bool { boolValue(values, "systemMicUnavailable") ?? false }
490
+ public var micEnabled: Bool { boolValue(values, "micEnabled") ?? false }
491
+ public var currentMic: String { stringValue(values, "currentMic") ?? "" }
492
+ public var micRanking: [String] { stringListValue(values, "micRanking") }
493
+ public var searchResults: [Device] {
494
+ dictionaryListValue(values, "searchResults").compactMap(Device.init(values:))
495
+ }
496
+ public var wifiScanResults: [WifiScanResult] {
497
+ dictionaryListValue(values, "wifiScanResults").map(WifiScanResult.init(values:))
498
+ }
499
+ public var lastLog: [String] { stringListValue(values, "lastLog") }
500
+ public var otherBtConnected: Bool { boolValue(values, "otherBtConnected") ?? false }
501
+ public var defaultWearable: String { stringValue(values, "default_wearable") ?? "" }
502
+ public var pendingWearable: String { stringValue(values, "pending_wearable") ?? "" }
503
+ public var deviceName: String { stringValue(values, "device_name") ?? "" }
504
+ public var deviceAddress: String { stringValue(values, "device_address") ?? "" }
505
+ public var defaultController: String { stringValue(values, "default_controller") ?? "" }
506
+ public var pendingController: String { stringValue(values, "pending_controller") ?? "" }
507
+ public var controllerDeviceName: String { stringValue(values, "controller_device_name") ?? "" }
508
+ public var screenDisabled: Bool { boolValue(values, "screen_disabled") ?? false }
509
+ public var preferredMic: String { stringValue(values, "preferred_mic") ?? "auto" }
510
+ public var sensingEnabled: Bool { boolValue(values, "sensing_enabled") ?? true }
511
+ public var powerSavingMode: Bool { boolValue(values, "power_saving_mode") ?? false }
512
+ public var brightness: Int { intValue(values["brightness"]) ?? 50 }
513
+ public var autoBrightness: Bool { boolValue(values, "auto_brightness") ?? true }
514
+ public var dashboardHeight: Int { intValue(values["dashboard_height"]) ?? 4 }
515
+ public var dashboardDepth: Int { intValue(values["dashboard_depth"]) ?? 2 }
516
+ public var headUpAngle: Int { intValue(values["head_up_angle"]) ?? 30 }
517
+ public var contextualDashboard: Bool { boolValue(values, "contextual_dashboard") ?? true }
518
+ public var galleryModeAuto: Bool { boolValue(values, "gallery_mode") ?? true }
519
+ public var buttonPhotoSize: ButtonPhotoSize {
520
+ ButtonPhotoSize(rawValue: stringValue(values, "button_photo_size") ?? "") ?? .medium
521
+ }
522
+ public var buttonCameraLed: Bool { boolValue(values, "button_camera_led") ?? true }
523
+ public var buttonMaxRecordingTime: Int { intValue(values["button_max_recording_time"]) ?? 10 }
524
+ public var buttonVideoWidth: Int { intValue(values["button_video_width"]) ?? 1280 }
525
+ public var buttonVideoHeight: Int { intValue(values["button_video_height"]) ?? 720 }
526
+ public var buttonVideoFps: Int { intValue(values["button_video_fps"]) ?? 30 }
527
+ public var shouldSendPcm: Bool { boolValue(values, "should_send_pcm") ?? false }
528
+ public var shouldSendLc3: Bool { boolValue(values, "should_send_lc3") ?? false }
529
+ public var shouldSendTranscript: Bool { boolValue(values, "should_send_transcript") ?? false }
530
+ public var bypassVad: Bool { boolValue(values, "bypass_vad") ?? false }
531
+ public var offlineCaptionsRunning: Bool { boolValue(values, "offline_captions_running") ?? false }
532
+ public var localSttFallbackActive: Bool { boolValue(values, "local_stt_fallback_active") ?? false }
533
+ public var shouldSendBootingMessage: Bool { boolValue(values, "shouldSendBootingMessage") ?? true }
534
+
535
+ public var defaultDevice: Device? {
536
+ guard !defaultWearable.isEmpty else { return nil }
537
+ return Device(
538
+ model: DeviceModel.fromDeviceType(defaultWearable),
539
+ name: deviceName,
540
+ identifier: deviceAddress.isEmpty ? nil : deviceAddress
541
+ )
542
+ }
543
+
544
+ public var description: String {
545
+ values.description
546
+ }
547
+ }
548
+
549
+ public struct GlassesStatusUpdate: CustomStringConvertible {
550
+ let values: [String: Any]
551
+
552
+ init(values: [String: Any]) {
553
+ self.values = values
554
+ }
555
+
556
+ public var fullyBooted: Bool? { optionalBoolValue(values, "fullyBooted") }
557
+ public var connected: Bool? { optionalBoolValue(values, "connected") }
558
+ public var micEnabled: Bool? { optionalBoolValue(values, "micEnabled") }
559
+ public var connectionState: GlassesConnectionState? {
560
+ GlassesConnectionState.fromValue(optionalStringValue(values, "connectionState"))
561
+ }
562
+ public var btcConnected: Bool? { optionalBoolValue(values, "btcConnected") }
563
+ public var signalStrength: Int? { optionalIntValue(values, "signalStrength") }
564
+ public var signalStrengthUpdatedAt: Int? { optionalIntValue(values, "signalStrengthUpdatedAt") }
565
+ public var deviceModel: String? { optionalStringValue(values, "deviceModel") }
566
+ public var androidVersion: String? { optionalStringValue(values, "androidVersion") }
567
+ public var firmwareVersion: String? { optionalStringValue(values, "firmwareVersion", "fwVersion") }
568
+ public var besFirmwareVersion: String? { optionalStringValue(values, "besFwVersion", "besFirmwareVersion") }
569
+ public var mtkFirmwareVersion: String? { optionalStringValue(values, "mtkFwVersion", "mtkFirmwareVersion") }
570
+ public var btMacAddress: String? { optionalStringValue(values, "btMacAddress") }
571
+ public var leftMacAddress: String? { optionalStringValue(values, "leftMacAddress") }
572
+ public var rightMacAddress: String? { optionalStringValue(values, "rightMacAddress") }
573
+ public var macAddress: String? { optionalStringValue(values, "macAddress") }
574
+ public var buildNumber: String? { optionalStringValue(values, "buildNumber") }
575
+ public var otaVersionUrl: String? { optionalStringValue(values, "otaVersionUrl") }
576
+ public var appVersion: String? { optionalStringValue(values, "appVersion") }
577
+ public var bluetoothName: String? { optionalStringValue(values, "bluetoothName") }
578
+ public var serialNumber: String? { optionalStringValue(values, "serialNumber") }
579
+ public var style: String? { optionalStringValue(values, "style") }
580
+ public var color: String? { optionalStringValue(values, "color") }
581
+ public var wifi: WifiStatus? {
582
+ if let wifi = values["wifi"] as? [String: Any] {
583
+ return WifiStatus(values: wifi)
584
+ }
585
+ if hasAnyKey(values, "wifiConnected", "wifiSsid", "wifiLocalIp") {
586
+ return WifiStatus.fromStoreValues(values)
587
+ }
588
+ return nil
589
+ }
590
+ public var hotspot: HotspotStatus? {
591
+ if let hotspot = values["hotspot"] as? [String: Any] {
592
+ return HotspotStatus(values: hotspot)
593
+ }
594
+ if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp", "hotspotLocalIp") {
595
+ return HotspotStatus.fromStoreValues(values)
596
+ }
597
+ return nil
598
+ }
599
+ public var dictionary: [String: Any] { GlassesStatus.updateDictionary(from: values) }
600
+ public var batteryLevel: Int? { optionalIntValue(values, "batteryLevel") }
601
+ public var charging: Bool? { optionalBoolValue(values, "charging") }
602
+ public var caseBatteryLevel: Int? { optionalIntValue(values, "caseBatteryLevel") }
603
+ public var caseCharging: Bool? { optionalBoolValue(values, "caseCharging") }
604
+ public var caseOpen: Bool? { optionalBoolValue(values, "caseOpen") }
605
+ public var caseRemoved: Bool? { optionalBoolValue(values, "caseRemoved") }
606
+ public var headUp: Bool? { optionalBoolValue(values, "headUp") }
607
+ public var controllerConnected: Bool? { optionalBoolValue(values, "controllerConnected") }
608
+ public var controllerFullyBooted: Bool? { optionalBoolValue(values, "controllerFullyBooted") }
609
+ public var controllerMacAddress: String? { optionalStringValue(values, "controllerMacAddress") }
610
+ public var controllerBatteryLevel: Int? { optionalIntValue(values, "controllerBatteryLevel") }
611
+ public var controllerSignalStrength: Int? { optionalIntValue(values, "controllerSignalStrength") }
612
+ public var ringSignalStrength: Int? { optionalIntValue(values, "ringSignalStrength") }
613
+
614
+ public var description: String {
615
+ values.description
616
+ }
617
+ }
618
+
619
+ public struct BluetoothStatusUpdate: CustomStringConvertible {
620
+ let values: [String: Any]
621
+
622
+ init(values: [String: Any]) {
623
+ var normalizedValues = values
624
+ if let searchResults = values["searchResults"] as? [[String: Any]] {
625
+ normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
626
+ }
627
+ self.values = normalizedValues
628
+ }
629
+
630
+ public var searching: Bool? { optionalBoolValue(values, "searching") }
631
+ public var searchingController: Bool? { optionalBoolValue(values, "searchingController") }
632
+ public var systemMicUnavailable: Bool? { optionalBoolValue(values, "systemMicUnavailable") }
633
+ public var micEnabled: Bool? { optionalBoolValue(values, "micEnabled") }
634
+ public var currentMic: String? { optionalStringValue(values, "currentMic") }
635
+ public var micRanking: [String]? { optionalStringListValue(values, "micRanking") }
636
+ public var searchResults: [Device]? {
637
+ optionalDictionaryListValue(values, "searchResults")?.compactMap(Device.init(values:))
638
+ }
639
+ public var wifiScanResults: [WifiScanResult]? {
640
+ optionalDictionaryListValue(values, "wifiScanResults")?.map(WifiScanResult.init(values:))
641
+ }
642
+ public var lastLog: [String]? { optionalStringListValue(values, "lastLog") }
643
+ public var otherBtConnected: Bool? { optionalBoolValue(values, "otherBtConnected") }
644
+ public var defaultWearable: String? { optionalStringValue(values, "default_wearable") }
645
+ public var pendingWearable: String? { optionalStringValue(values, "pending_wearable") }
646
+ public var deviceName: String? { optionalStringValue(values, "device_name") }
647
+ public var deviceAddress: String? { optionalStringValue(values, "device_address") }
648
+ public var defaultController: String? { optionalStringValue(values, "default_controller") }
649
+ public var pendingController: String? { optionalStringValue(values, "pending_controller") }
650
+ public var controllerDeviceName: String? { optionalStringValue(values, "controller_device_name") }
651
+ public var screenDisabled: Bool? { optionalBoolValue(values, "screen_disabled") }
652
+ public var preferredMic: String? { optionalStringValue(values, "preferred_mic") }
653
+ public var sensingEnabled: Bool? { optionalBoolValue(values, "sensing_enabled") }
654
+ public var powerSavingMode: Bool? { optionalBoolValue(values, "power_saving_mode") }
655
+ public var brightness: Int? { optionalIntValue(values, "brightness") }
656
+ public var autoBrightness: Bool? { optionalBoolValue(values, "auto_brightness") }
657
+ public var dashboardHeight: Int? { optionalIntValue(values, "dashboard_height") }
658
+ public var dashboardDepth: Int? { optionalIntValue(values, "dashboard_depth") }
659
+ public var headUpAngle: Int? { optionalIntValue(values, "head_up_angle") }
660
+ public var contextualDashboard: Bool? { optionalBoolValue(values, "contextual_dashboard") }
661
+ public var galleryModeAuto: Bool? { optionalBoolValue(values, "gallery_mode") }
662
+ public var buttonPhotoSize: ButtonPhotoSize? {
663
+ optionalStringValue(values, "button_photo_size").flatMap(ButtonPhotoSize.init(rawValue:))
664
+ }
665
+ public var buttonCameraLed: Bool? { optionalBoolValue(values, "button_camera_led") }
666
+ public var buttonMaxRecordingTime: Int? { optionalIntValue(values, "button_max_recording_time") }
667
+ public var buttonVideoWidth: Int? { optionalIntValue(values, "button_video_width") }
668
+ public var buttonVideoHeight: Int? { optionalIntValue(values, "button_video_height") }
669
+ public var buttonVideoFps: Int? { optionalIntValue(values, "button_video_fps") }
670
+ public var shouldSendPcm: Bool? { optionalBoolValue(values, "should_send_pcm") }
671
+ public var shouldSendLc3: Bool? { optionalBoolValue(values, "should_send_lc3") }
672
+ public var shouldSendTranscript: Bool? { optionalBoolValue(values, "should_send_transcript") }
673
+ public var bypassVad: Bool? { optionalBoolValue(values, "bypass_vad") }
674
+ public var offlineCaptionsRunning: Bool? { optionalBoolValue(values, "offline_captions_running") }
675
+ public var localSttFallbackActive: Bool? { optionalBoolValue(values, "local_stt_fallback_active") }
676
+ public var shouldSendBootingMessage: Bool? { optionalBoolValue(values, "shouldSendBootingMessage") }
677
+
678
+ public var description: String {
679
+ values.description
680
+ }
681
+ }
682
+
683
+ public struct DisplayTextRequest {
684
+ public let text: String
685
+ public let x: Int
686
+ public let y: Int
687
+ public let size: Int
688
+
689
+ public init(text: String, x: Int = 0, y: Int = 0, size: Int = 24) {
690
+ self.text = text
691
+ self.x = x
692
+ self.y = y
693
+ self.size = size
694
+ }
695
+
696
+ var dictionary: [String: Any] {
697
+ [
698
+ "text": text,
699
+ "x": x,
700
+ "y": y,
701
+ "size": size,
702
+ ]
703
+ }
704
+ }
705
+
706
+ public struct DisplayEventRequest {
707
+ public let values: [String: Any]
708
+
709
+ public init(values: [String: Any]) {
710
+ self.values = values
711
+ }
712
+ }
713
+
714
+ public struct DashboardPositionRequest {
715
+ public let height: Int
716
+ public let depth: Int
717
+
718
+ public init(height: Int, depth: Int) {
719
+ self.height = height
720
+ self.depth = depth
721
+ }
722
+ }
723
+
724
+ public struct DashboardMenuItem {
725
+ public let title: String
726
+ public let packageName: String
727
+ public let values: [String: Any]
728
+
729
+ public init(title: String, packageName: String, values: [String: Any] = [:]) {
730
+ self.title = title
731
+ self.packageName = packageName
732
+ self.values = values
733
+ }
734
+
735
+ var dictionary: [String: Any] {
736
+ values.merging(["title": title, "packageName": packageName]) { _, new in new }
737
+ }
738
+ }
739
+
740
+ public enum GalleryMode {
741
+ case auto
742
+ case manual
743
+ }
744
+
745
+ public enum PhotoSize: String {
746
+ case small
747
+ case medium
748
+ case large
749
+ case full
750
+ }
751
+
752
+ public enum ButtonPhotoSize: String {
753
+ case small
754
+ case medium
755
+ case large
756
+ }
757
+
758
+ public enum PhotoCompression: String {
759
+ case none
760
+ case medium
761
+ case heavy
762
+ }
763
+
764
+ public struct ButtonPhotoSettings {
765
+ public let size: ButtonPhotoSize
766
+
767
+ public init(size: ButtonPhotoSize) {
768
+ self.size = size
769
+ }
770
+ }
771
+
772
+ public struct ButtonVideoRecordingSettings {
773
+ public let width: Int
774
+ public let height: Int
775
+ public let fps: Int
776
+
777
+ public init(width: Int, height: Int, fps: Int) {
778
+ self.width = width
779
+ self.height = height
780
+ self.fps = fps
781
+ }
782
+ }
783
+
784
+ public enum CameraFov {
785
+ case standard
786
+ case wide
787
+
788
+ var value: [String: Int] {
789
+ switch self {
790
+ case .standard:
791
+ ["fov": 118, "roi_position": 0]
792
+ case .wide:
793
+ ["fov": 118, "roi_position": 0]
794
+ }
795
+ }
796
+ }
797
+
798
+ public struct MicConfiguration {
799
+ public let sendPcmData: Bool
800
+ public let sendTranscript: Bool
801
+ public let bypassVad: Bool
802
+ public let sendLc3Data: Bool
803
+
804
+ public init(
805
+ sendPcmData: Bool,
806
+ sendTranscript: Bool,
807
+ bypassVad: Bool,
808
+ sendLc3Data: Bool = false
809
+ ) {
810
+ self.sendPcmData = sendPcmData
811
+ self.sendTranscript = sendTranscript
812
+ self.bypassVad = bypassVad
813
+ self.sendLc3Data = sendLc3Data
814
+ }
815
+ }
816
+
817
+ public enum MicPreference: String {
818
+ case auto
819
+ case phone
820
+ case glasses
821
+ case bluetooth
822
+ }
823
+
824
+ public struct PhotoRequest {
825
+ public let requestId: String
826
+ public let appId: String
827
+ public let size: PhotoSize
828
+ public let webhookUrl: String?
829
+ public let authToken: String?
830
+ public let compress: PhotoCompression?
831
+ public let flash: Bool
832
+ public let sound: Bool
833
+
834
+ public init(
835
+ requestId: String,
836
+ appId: String,
837
+ size: PhotoSize,
838
+ webhookUrl: String? = nil,
839
+ authToken: String? = nil,
840
+ compress: PhotoCompression? = nil,
841
+ flash: Bool,
842
+ sound: Bool
843
+ ) {
844
+ self.requestId = requestId
845
+ self.appId = appId
846
+ self.size = size
847
+ self.webhookUrl = webhookUrl
848
+ self.authToken = authToken
849
+ self.compress = compress
850
+ self.flash = flash
851
+ self.sound = sound
852
+ }
853
+ }
854
+
855
+ public struct StreamVideoConfig {
856
+ public let width: Int?
857
+ public let height: Int?
858
+ public let bitrate: Int?
859
+ public let frameRate: Int?
860
+
861
+ public init(
862
+ width: Int? = nil,
863
+ height: Int? = nil,
864
+ bitrate: Int? = nil,
865
+ frameRate: Int? = nil
866
+ ) {
867
+ self.width = width
868
+ self.height = height
869
+ self.bitrate = bitrate
870
+ self.frameRate = frameRate
871
+ }
872
+
873
+ var dictionary: [String: Any] {
874
+ var values: [String: Any] = [:]
875
+ if let width { values["width"] = width }
876
+ if let height { values["height"] = height }
877
+ if let bitrate { values["bitrate"] = bitrate }
878
+ if let frameRate { values["frameRate"] = frameRate }
879
+ return values
880
+ }
881
+
882
+ init?(values: [String: Any]?) {
883
+ guard let values else { return nil }
884
+ self.init(
885
+ width: intValue(values["width"] ?? values["w"]),
886
+ height: intValue(values["height"] ?? values["h"]),
887
+ bitrate: intValue(values["bitrate"] ?? values["br"]),
888
+ frameRate: intValue(values["frameRate"] ?? values["fr"])
889
+ )
890
+ }
891
+ }
892
+
893
+ public struct StreamAudioConfig {
894
+ public let bitrate: Int?
895
+ public let sampleRate: Int?
896
+ public let echoCancellation: Bool?
897
+ public let noiseSuppression: Bool?
898
+
899
+ public init(
900
+ bitrate: Int? = nil,
901
+ sampleRate: Int? = nil,
902
+ echoCancellation: Bool? = nil,
903
+ noiseSuppression: Bool? = nil
904
+ ) {
905
+ self.bitrate = bitrate
906
+ self.sampleRate = sampleRate
907
+ self.echoCancellation = echoCancellation
908
+ self.noiseSuppression = noiseSuppression
909
+ }
910
+
911
+ var dictionary: [String: Any] {
912
+ var values: [String: Any] = [:]
913
+ if let bitrate { values["bitrate"] = bitrate }
914
+ if let sampleRate { values["sampleRate"] = sampleRate }
915
+ if let echoCancellation { values["echoCancellation"] = echoCancellation }
916
+ if let noiseSuppression { values["noiseSuppression"] = noiseSuppression }
917
+ return values
918
+ }
919
+
920
+ init?(values: [String: Any]?) {
921
+ guard let values else { return nil }
922
+ self.init(
923
+ bitrate: intValue(values["bitrate"] ?? values["br"]),
924
+ sampleRate: intValue(values["sampleRate"] ?? values["sr"]),
925
+ echoCancellation: values["echoCancellation"] as? Bool ?? values["ec"] as? Bool,
926
+ noiseSuppression: values["noiseSuppression"] as? Bool ?? values["ns"] as? Bool
927
+ )
928
+ }
929
+ }
930
+
931
+ public struct StreamRequest {
932
+ public let streamUrl: String
933
+ public let streamId: String
934
+ public let keepAlive: Bool
935
+ public let keepAliveIntervalSeconds: Int
936
+ public let flash: Bool
937
+ public let sound: Bool
938
+ public let video: StreamVideoConfig?
939
+ public let audio: StreamAudioConfig?
940
+ public let extraValues: [String: Any]
941
+
942
+ public init(
943
+ streamUrl: String,
944
+ streamId: String = "",
945
+ keepAlive: Bool = true,
946
+ keepAliveIntervalSeconds: Int = 15,
947
+ flash: Bool = true,
948
+ sound: Bool = true,
949
+ video: StreamVideoConfig? = nil,
950
+ audio: StreamAudioConfig? = nil,
951
+ extraValues: [String: Any] = [:]
952
+ ) {
953
+ self.streamUrl = streamUrl
954
+ self.streamId = streamId
955
+ self.keepAlive = keepAlive
956
+ self.keepAliveIntervalSeconds = keepAliveIntervalSeconds
957
+ self.flash = flash
958
+ self.sound = sound
959
+ self.video = video
960
+ self.audio = audio
961
+ self.extraValues = extraValues
962
+ }
963
+
964
+ init(values: [String: Any]) {
965
+ self.init(
966
+ streamUrl: values["streamUrl"] as? String
967
+ ?? values["rtmpUrl"] as? String
968
+ ?? values["srtUrl"] as? String
969
+ ?? values["whipUrl"] as? String
970
+ ?? "",
971
+ streamId: values["streamId"] as? String ?? "",
972
+ keepAlive: values["keepAlive"] as? Bool ?? true,
973
+ keepAliveIntervalSeconds: intValue(values["keepAliveIntervalSeconds"]) ?? 15,
974
+ flash: values["flash"] as? Bool ?? true,
975
+ sound: values["sound"] as? Bool ?? true,
976
+ video: StreamVideoConfig(values: (values["video"] ?? values["v"]) as? [String: Any]),
977
+ audio: StreamAudioConfig(values: (values["audio"] ?? values["a"]) as? [String: Any]),
978
+ extraValues: values
979
+ )
980
+ }
981
+
982
+ public var values: [String: Any] {
983
+ var values = extraValues
984
+ values["type"] = "start_stream"
985
+ values["streamUrl"] = streamUrl
986
+ values["streamId"] = streamId
987
+ values["keepAlive"] = keepAlive
988
+ values["keepAliveIntervalSeconds"] = keepAliveIntervalSeconds
989
+ values["flash"] = flash
990
+ values["sound"] = sound
991
+ if let videoValues = video?.dictionary, !videoValues.isEmpty {
992
+ values["video"] = videoValues
993
+ }
994
+ if let audioValues = audio?.dictionary, !audioValues.isEmpty {
995
+ values["audio"] = audioValues
996
+ }
997
+ return values
998
+ }
999
+ }
1000
+
1001
+ public struct StreamKeepAliveRequest {
1002
+ public let streamId: String
1003
+ public let ackId: String
1004
+ public let extraValues: [String: Any]
1005
+
1006
+ public init(streamId: String, ackId: String, extraValues: [String: Any] = [:]) {
1007
+ self.streamId = streamId
1008
+ self.ackId = ackId
1009
+ self.extraValues = extraValues
1010
+ }
1011
+
1012
+ init(values: [String: Any]) {
1013
+ self.init(
1014
+ streamId: values["streamId"] as? String ?? "",
1015
+ ackId: values["ackId"] as? String ?? "",
1016
+ extraValues: values
1017
+ )
1018
+ }
1019
+
1020
+ public var values: [String: Any] {
1021
+ var values = extraValues
1022
+ values["type"] = "keep_stream_alive"
1023
+ values["streamId"] = streamId
1024
+ values["ackId"] = ackId
1025
+ return values
1026
+ }
1027
+ }
1028
+
1029
+ public enum RgbLedAction: String {
1030
+ case on
1031
+ case off
1032
+ }
1033
+
1034
+ public enum RgbLedColor: String {
1035
+ case red
1036
+ case green
1037
+ case blue
1038
+ case orange
1039
+ case white
1040
+ }
1041
+
1042
+ public struct RgbLedRequest {
1043
+ public let requestId: String
1044
+ public let packageName: String?
1045
+ public let action: RgbLedAction
1046
+ public let color: RgbLedColor?
1047
+ public let ontime: Int
1048
+ public let offtime: Int
1049
+ public let count: Int
1050
+
1051
+ public init(
1052
+ requestId: String,
1053
+ packageName: String?,
1054
+ action: RgbLedAction,
1055
+ color: RgbLedColor?,
1056
+ ontime: Int,
1057
+ offtime: Int,
1058
+ count: Int
1059
+ ) {
1060
+ self.requestId = requestId
1061
+ self.packageName = packageName
1062
+ self.action = action
1063
+ self.color = color
1064
+ self.ontime = ontime
1065
+ self.offtime = offtime
1066
+ self.count = count
1067
+ }
1068
+ }
1069
+
1070
+ public struct VideoRecordingRequest {
1071
+ public let requestId: String
1072
+ public let save: Bool
1073
+ public let flash: Bool
1074
+ public let sound: Bool
1075
+
1076
+ public init(requestId: String, save: Bool, flash: Bool, sound: Bool) {
1077
+ self.requestId = requestId
1078
+ self.save = save
1079
+ self.flash = flash
1080
+ self.sound = sound
1081
+ }
1082
+ }
1083
+
1084
+ public struct ButtonPressEvent: CustomStringConvertible {
1085
+ public let buttonId: String
1086
+ public let pressType: String
1087
+ public let timestamp: Int?
1088
+
1089
+ public init(buttonId: String, pressType: String, timestamp: Int? = nil) {
1090
+ self.buttonId = buttonId
1091
+ self.pressType = pressType
1092
+ self.timestamp = timestamp
1093
+ }
1094
+
1095
+ public var description: String {
1096
+ "ButtonPressEvent(buttonId: \(buttonId), pressType: \(pressType))"
1097
+ }
1098
+ }
1099
+
1100
+ public struct TouchEvent: CustomStringConvertible {
1101
+ public let values: [String: Any]
1102
+
1103
+ public init(values: [String: Any]) {
1104
+ self.values = values
1105
+ }
1106
+
1107
+ public var deviceModel: String? {
1108
+ stringValue(values, "device_model", "deviceModel")
1109
+ }
1110
+
1111
+ public var gestureName: String? {
1112
+ stringValue(values, "gesture_name", "gestureName")
1113
+ }
1114
+
1115
+ public var timestamp: Int? {
1116
+ intValue(values["timestamp"])
1117
+ }
1118
+
1119
+ public var isSwipe: Bool {
1120
+ gestureName?.localizedCaseInsensitiveContains("swipe") == true
1121
+ }
1122
+
1123
+ public var description: String {
1124
+ "TouchEvent(gestureName: \(gestureName ?? "unknown"))"
1125
+ }
1126
+ }
1127
+
1128
+ public enum WifiStatus: CustomStringConvertible, Equatable {
1129
+ public enum State: String {
1130
+ case disconnected
1131
+ case connected
1132
+ }
1133
+
1134
+ case disconnected
1135
+ case connected(ssid: String, localIp: String?)
1136
+
1137
+ private init?(connected: Bool, ssid: String?, localIp: String?) {
1138
+ if connected {
1139
+ guard
1140
+ let ssid = ssid?.trimmingCharacters(in: .whitespacesAndNewlines),
1141
+ !ssid.isEmpty
1142
+ else {
1143
+ return nil
1144
+ }
1145
+ let trimmedLocalIp = localIp?.trimmingCharacters(in: .whitespacesAndNewlines)
1146
+ self = .connected(
1147
+ ssid: ssid,
1148
+ localIp: trimmedLocalIp?.isEmpty == false ? trimmedLocalIp : nil
1149
+ )
1150
+ } else {
1151
+ self = .disconnected
1152
+ }
1153
+ }
1154
+
1155
+ init?(values: [String: Any]) {
1156
+ if let nested = values["wifi"] as? [String: Any] {
1157
+ guard let wifi = WifiStatus(values: nested) else {
1158
+ return nil
1159
+ }
1160
+ self = wifi
1161
+ return
1162
+ }
1163
+
1164
+ if let state = stringValue(values, "state")?.lowercased() {
1165
+ switch state {
1166
+ case State.connected.rawValue:
1167
+ guard let wifi = WifiStatus(
1168
+ connected: true,
1169
+ ssid: nonEmptyStringValue(values, "ssid"),
1170
+ localIp: nonEmptyStringValue(values, "localIp")
1171
+ ) else {
1172
+ return nil
1173
+ }
1174
+ self = wifi
1175
+ case State.disconnected.rawValue:
1176
+ self = .disconnected
1177
+ default:
1178
+ return nil
1179
+ }
1180
+ return
1181
+ }
1182
+
1183
+ return nil
1184
+ }
1185
+
1186
+ static func fromStoreValues(_ values: [String: Any]) -> WifiStatus? {
1187
+ guard let connected = boolValue(values, "wifiConnected") else { return nil }
1188
+ return fromStoreFields(
1189
+ connected: connected,
1190
+ ssid: nonEmptyStringValue(values, "wifiSsid"),
1191
+ localIp: nonEmptyStringValue(values, "wifiLocalIp")
1192
+ )
1193
+ }
1194
+
1195
+ static func fromStoreFields(connected: Bool, ssid: String?, localIp: String?) -> WifiStatus? {
1196
+ WifiStatus(connected: connected, ssid: ssid, localIp: localIp)
1197
+ }
1198
+
1199
+ public var state: State {
1200
+ switch self {
1201
+ case .disconnected:
1202
+ .disconnected
1203
+ case .connected:
1204
+ .connected
1205
+ }
1206
+ }
1207
+
1208
+ public var isConnected: Bool {
1209
+ if case .connected = self {
1210
+ return true
1211
+ }
1212
+ return false
1213
+ }
1214
+
1215
+ public var values: [String: Any] {
1216
+ switch self {
1217
+ case .disconnected:
1218
+ return ["state": State.disconnected.rawValue]
1219
+ case let .connected(ssid, localIp):
1220
+ var values: [String: Any] = [
1221
+ "state": State.connected.rawValue,
1222
+ "ssid": ssid,
1223
+ ]
1224
+ if let localIp = localIp {
1225
+ values["localIp"] = localIp
1226
+ }
1227
+ return values
1228
+ }
1229
+ }
1230
+
1231
+ var storeValues: [String: Any] {
1232
+ switch self {
1233
+ case .disconnected:
1234
+ [
1235
+ "wifiConnected": false,
1236
+ "wifiSsid": "",
1237
+ "wifiLocalIp": "",
1238
+ ]
1239
+ case let .connected(ssid, localIp):
1240
+ [
1241
+ "wifiConnected": true,
1242
+ "wifiSsid": ssid,
1243
+ "wifiLocalIp": localIp ?? "",
1244
+ ]
1245
+ }
1246
+ }
1247
+
1248
+ public var description: String {
1249
+ switch self {
1250
+ case .disconnected:
1251
+ "WifiStatus(disconnected)"
1252
+ case let .connected(ssid, localIp):
1253
+ "WifiStatus(connected: \(ssid), localIp: \(localIp ?? "unknown"))"
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ public struct WifiStatusEvent: CustomStringConvertible {
1259
+ public let status: WifiStatus
1260
+
1261
+ public init(status: WifiStatus) {
1262
+ self.status = status
1263
+ }
1264
+
1265
+ init(connected: Bool, ssid: String?, localIp: String?) {
1266
+ self.status = WifiStatus.fromStoreFields(connected: connected, ssid: ssid, localIp: localIp) ?? .disconnected
1267
+ }
1268
+
1269
+ init(values: [String: Any]) {
1270
+ self.status = WifiStatus(values: values) ?? .disconnected
1271
+ }
1272
+
1273
+ public var values: [String: Any] {
1274
+ status.values.merging(["type": "wifi_status_change"]) { _, new in new }
1275
+ }
1276
+
1277
+ public var description: String {
1278
+ "WifiStatusEvent(\(status))"
1279
+ }
1280
+ }
1281
+
1282
+ public enum HotspotStatus: CustomStringConvertible, Equatable {
1283
+ public enum State: String {
1284
+ case disabled
1285
+ case enabled
1286
+ }
1287
+
1288
+ case disabled
1289
+ case enabled(ssid: String, password: String, localIp: String)
1290
+
1291
+ public init?(enabled: Bool, ssid: String?, password: String?, localIp: String?) {
1292
+ if enabled {
1293
+ guard
1294
+ let ssid = ssid?.trimmingCharacters(in: .whitespacesAndNewlines),
1295
+ !ssid.isEmpty,
1296
+ let password = password?.trimmingCharacters(in: .whitespacesAndNewlines),
1297
+ !password.isEmpty,
1298
+ let localIp = localIp?.trimmingCharacters(in: .whitespacesAndNewlines),
1299
+ !localIp.isEmpty
1300
+ else {
1301
+ return nil
1302
+ }
1303
+ self = .enabled(ssid: ssid, password: password, localIp: localIp)
1304
+ } else {
1305
+ self = .disabled
1306
+ }
1307
+ }
1308
+
1309
+ public init?(values: [String: Any]) {
1310
+ if let nested = values["hotspot"] as? [String: Any] {
1311
+ self.init(values: nested)
1312
+ return
1313
+ }
1314
+
1315
+ guard let state = stringValue(values, "state")?.lowercased() else {
1316
+ return nil
1317
+ }
1318
+
1319
+ switch state {
1320
+ case State.enabled.rawValue:
1321
+ self.init(
1322
+ enabled: true,
1323
+ ssid: nonEmptyStringValue(values, "ssid"),
1324
+ password: nonEmptyStringValue(values, "password"),
1325
+ localIp: nonEmptyStringValue(values, "localIp")
1326
+ )
1327
+ case State.disabled.rawValue:
1328
+ self = .disabled
1329
+ default:
1330
+ return nil
1331
+ }
1332
+ }
1333
+
1334
+ static func fromStoreValues(_ values: [String: Any]) -> HotspotStatus? {
1335
+ guard let enabled = boolValue(values, "hotspotEnabled") else {
1336
+ return nil
1337
+ }
1338
+ return fromStoreFields(
1339
+ enabled: enabled,
1340
+ ssid: nonEmptyStringValue(values, "hotspotSsid"),
1341
+ password: nonEmptyStringValue(values, "hotspotPassword"),
1342
+ localIp: nonEmptyStringValue(values, "hotspotGatewayIp", "hotspotLocalIp")
1343
+ )
1344
+ }
1345
+
1346
+ static func fromStoreFields(enabled: Bool, ssid: String?, password: String?, localIp: String?) -> HotspotStatus? {
1347
+ HotspotStatus(enabled: enabled, ssid: ssid, password: password, localIp: localIp)
1348
+ }
1349
+
1350
+ var storeValues: [String: Any] {
1351
+ switch self {
1352
+ case .disabled:
1353
+ [
1354
+ "hotspotEnabled": false,
1355
+ "hotspotSsid": "",
1356
+ "hotspotPassword": "",
1357
+ "hotspotGatewayIp": "",
1358
+ ]
1359
+ case let .enabled(ssid, password, localIp):
1360
+ [
1361
+ "hotspotEnabled": true,
1362
+ "hotspotSsid": ssid,
1363
+ "hotspotPassword": password,
1364
+ "hotspotGatewayIp": localIp,
1365
+ ]
1366
+ }
1367
+ }
1368
+
1369
+ public var values: [String: Any] {
1370
+ switch self {
1371
+ case .disabled:
1372
+ ["state": State.disabled.rawValue]
1373
+ case let .enabled(ssid, password, localIp):
1374
+ [
1375
+ "state": State.enabled.rawValue,
1376
+ "ssid": ssid,
1377
+ "password": password,
1378
+ "localIp": localIp,
1379
+ ]
1380
+ }
1381
+ }
1382
+
1383
+ public var state: State {
1384
+ switch self {
1385
+ case .disabled:
1386
+ .disabled
1387
+ case .enabled:
1388
+ .enabled
1389
+ }
1390
+ }
1391
+
1392
+ public var isEnabled: Bool {
1393
+ if case .enabled = self {
1394
+ return true
1395
+ }
1396
+ return false
1397
+ }
1398
+
1399
+ public var description: String {
1400
+ switch self {
1401
+ case .disabled:
1402
+ "HotspotStatus(disabled)"
1403
+ case let .enabled(ssid, _, localIp):
1404
+ "HotspotStatus(enabled: \(ssid), localIp: \(localIp))"
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ public struct HotspotStatusEvent: CustomStringConvertible {
1410
+ public let status: HotspotStatus
1411
+
1412
+ public init(status: HotspotStatus) {
1413
+ self.status = status
1414
+ }
1415
+
1416
+ init(enabled: Bool, ssid: String?, password: String?, localIp: String?) {
1417
+ self.status = HotspotStatus.fromStoreFields(enabled: enabled, ssid: ssid, password: password, localIp: localIp) ?? .disabled
1418
+ }
1419
+
1420
+ init(values: [String: Any]) {
1421
+ self.status = HotspotStatus(values: values) ?? .disabled
1422
+ }
1423
+
1424
+ public var values: [String: Any] {
1425
+ status.values.merging(["type": "hotspot_status_change"]) { _, new in new }
1426
+ }
1427
+
1428
+ public var description: String {
1429
+ "HotspotStatusEvent(\(status))"
1430
+ }
1431
+ }
1432
+
1433
+ public struct HotspotErrorEvent: CustomStringConvertible {
1434
+ public let values: [String: Any]
1435
+
1436
+ public init(values: [String: Any]) {
1437
+ self.values = values
1438
+ }
1439
+
1440
+ public var message: String? {
1441
+ stringValue(values, "error_message", "message", "error")
1442
+ }
1443
+
1444
+ public var timestamp: Int? {
1445
+ intValue(values["timestamp"])
1446
+ }
1447
+
1448
+ public var description: String {
1449
+ "HotspotErrorEvent(message: \(message ?? "unknown"))"
1450
+ }
1451
+ }
1452
+
1453
+ public enum PhotoResponse: CustomStringConvertible, Equatable {
1454
+ public enum State: String {
1455
+ case success
1456
+ case error
1457
+ }
1458
+
1459
+ case success(requestId: String, photoUrl: String, timestamp: Int)
1460
+ case error(requestId: String, errorCode: String?, errorMessage: String, timestamp: Int)
1461
+
1462
+ public init(values: [String: Any]) {
1463
+ let requestId = stringValue(values, "requestId", "request_id") ?? ""
1464
+ let timestamp = intValue(values["timestamp"]) ?? Int(Date().timeIntervalSince1970 * 1000)
1465
+ let state = stringValue(values, "state", "status")?.lowercased()
1466
+ if state == State.success.rawValue || boolValue(values, "success") == true {
1467
+ self = .success(
1468
+ requestId: requestId,
1469
+ photoUrl: stringValue(values, "photoUrl", "photo_url") ?? "",
1470
+ timestamp: timestamp
1471
+ )
1472
+ } else {
1473
+ self = .error(
1474
+ requestId: requestId,
1475
+ errorCode: stringValue(values, "errorCode", "error_code"),
1476
+ errorMessage: stringValue(values, "errorMessage", "error_message", "error") ?? "Unknown photo error",
1477
+ timestamp: timestamp
1478
+ )
1479
+ }
1480
+ }
1481
+
1482
+ public var state: State {
1483
+ switch self {
1484
+ case .success:
1485
+ .success
1486
+ case .error:
1487
+ .error
1488
+ }
1489
+ }
1490
+
1491
+ public var requestId: String {
1492
+ switch self {
1493
+ case let .success(requestId, _, _), let .error(requestId, _, _, _):
1494
+ requestId
1495
+ }
1496
+ }
1497
+
1498
+ public var timestamp: Int {
1499
+ switch self {
1500
+ case let .success(_, _, timestamp), let .error(_, _, _, timestamp):
1501
+ timestamp
1502
+ }
1503
+ }
1504
+
1505
+ public var values: [String: Any] {
1506
+ switch self {
1507
+ case let .success(requestId, photoUrl, timestamp):
1508
+ return [
1509
+ "state": State.success.rawValue,
1510
+ "requestId": requestId,
1511
+ "photoUrl": photoUrl,
1512
+ "timestamp": timestamp,
1513
+ ]
1514
+ case let .error(requestId, errorCode, errorMessage, timestamp):
1515
+ var values: [String: Any] = [
1516
+ "state": State.error.rawValue,
1517
+ "requestId": requestId,
1518
+ "errorMessage": errorMessage,
1519
+ "timestamp": timestamp,
1520
+ ]
1521
+ if let errorCode, !errorCode.isEmpty {
1522
+ values["errorCode"] = errorCode
1523
+ }
1524
+ return values
1525
+ }
1526
+ }
1527
+
1528
+ public var description: String {
1529
+ "PhotoResponse(requestId: \(requestId), state: \(state.rawValue))"
1530
+ }
1531
+ }
1532
+
1533
+ public struct PhotoResponseEvent: CustomStringConvertible {
1534
+ public let response: PhotoResponse
1535
+
1536
+ public init(response: PhotoResponse) {
1537
+ self.response = response
1538
+ }
1539
+
1540
+ public init(values: [String: Any]) {
1541
+ self.response = PhotoResponse(values: values)
1542
+ }
1543
+
1544
+ public var requestId: String {
1545
+ response.requestId
1546
+ }
1547
+
1548
+ public var values: [String: Any] {
1549
+ var values = response.values
1550
+ values["type"] = "photo_response"
1551
+ return values
1552
+ }
1553
+
1554
+ public var description: String {
1555
+ "PhotoResponseEvent(requestId: \(requestId), state: \(response.state.rawValue))"
1556
+ }
1557
+ }
1558
+
1559
+ public enum StreamState: String, Equatable {
1560
+ case initializing
1561
+ case streaming
1562
+ case stopping
1563
+ case stopped
1564
+ case reconnecting
1565
+ case reconnected
1566
+ case reconnectFailed = "reconnect_failed"
1567
+ case error
1568
+
1569
+ fileprivate static func from(_ value: String?) -> StreamState? {
1570
+ switch value?.lowercased() {
1571
+ case "initializing", "starting", "connecting":
1572
+ return .initializing
1573
+ case "streaming", "streaming_started", "active":
1574
+ return .streaming
1575
+ case "stopping":
1576
+ return .stopping
1577
+ case "stopped", "not_streaming", "disconnected", "timeout":
1578
+ return .stopped
1579
+ case "reconnecting":
1580
+ return .reconnecting
1581
+ case "reconnected":
1582
+ return .reconnected
1583
+ case "reconnect_failed":
1584
+ return .reconnectFailed
1585
+ case "error", "error_not_streaming":
1586
+ return .error
1587
+ default:
1588
+ return nil
1589
+ }
1590
+ }
1591
+ }
1592
+
1593
+ public enum StreamStatusKind: String, Equatable {
1594
+ case lifecycle
1595
+ case reconnect
1596
+ case error
1597
+ case snapshot
1598
+ }
1599
+
1600
+ public enum StreamStatus: CustomStringConvertible, Equatable {
1601
+ case lifecycle(state: StreamState, streamId: String?, timestamp: Int?)
1602
+ case reconnecting(streamId: String?, attempt: Int, maxAttempts: Int, reason: String, timestamp: Int?)
1603
+ case reconnected(streamId: String?, attempt: Int, timestamp: Int?)
1604
+ case reconnectFailed(streamId: String?, maxAttempts: Int, timestamp: Int?)
1605
+ case error(streamId: String?, errorDetails: String, timestamp: Int?)
1606
+ case snapshot(state: StreamState, streaming: Bool, reconnecting: Bool, streamId: String?, attempt: Int?, timestamp: Int?)
1607
+
1608
+ public init(values: [String: Any]) {
1609
+ let rawState = stringValue(values, "status")
1610
+ let streamId = stringValue(values, "streamId", "stream_id")
1611
+ let timestamp = intValue(values["timestamp"])
1612
+ let attempt = optionalIntValue(values, "attempt")
1613
+ let maxAttempts = optionalIntValue(values, "maxAttempts", "max_attempts") ?? 0
1614
+
1615
+ if hasAnyKey(values, "streaming") || hasAnyKey(values, "reconnecting") {
1616
+ let streaming = boolValue(values, "streaming") == true
1617
+ let reconnecting = boolValue(values, "reconnecting") == true
1618
+ let snapshotState: StreamState = reconnecting ? .reconnecting : (streaming ? .streaming : .stopped)
1619
+ self = .snapshot(
1620
+ state: snapshotState,
1621
+ streaming: streaming,
1622
+ reconnecting: reconnecting,
1623
+ streamId: streamId,
1624
+ attempt: attempt,
1625
+ timestamp: timestamp
1626
+ )
1627
+ return
1628
+ }
1629
+
1630
+ guard let state = StreamState.from(rawState) else {
1631
+ self = .error(
1632
+ streamId: streamId,
1633
+ errorDetails: rawState.map { "Unknown stream status: \($0)" } ?? "Missing stream status",
1634
+ timestamp: timestamp
1635
+ )
1636
+ return
1637
+ }
1638
+
1639
+ switch state {
1640
+ case .reconnecting:
1641
+ self = .reconnecting(
1642
+ streamId: streamId,
1643
+ attempt: attempt ?? 0,
1644
+ maxAttempts: maxAttempts,
1645
+ reason: stringValue(values, "reason") ?? "",
1646
+ timestamp: timestamp
1647
+ )
1648
+ case .reconnected:
1649
+ self = .reconnected(streamId: streamId, attempt: attempt ?? 0, timestamp: timestamp)
1650
+ case .reconnectFailed:
1651
+ self = .reconnectFailed(streamId: streamId, maxAttempts: maxAttempts, timestamp: timestamp)
1652
+ case .error:
1653
+ self = .error(
1654
+ streamId: streamId,
1655
+ errorDetails: stringValue(values, "errorDetails", "error_details", "details", "error", "errorMessage")
1656
+ ?? (rawState == "error_not_streaming" ? "not_streaming" : "Unknown stream error"),
1657
+ timestamp: timestamp
1658
+ )
1659
+ default:
1660
+ self = .lifecycle(state: state, streamId: streamId, timestamp: timestamp)
1661
+ }
1662
+ }
1663
+
1664
+ public var kind: StreamStatusKind {
1665
+ switch self {
1666
+ case .lifecycle:
1667
+ .lifecycle
1668
+ case .reconnecting, .reconnected, .reconnectFailed:
1669
+ .reconnect
1670
+ case .error:
1671
+ .error
1672
+ case .snapshot:
1673
+ .snapshot
1674
+ }
1675
+ }
1676
+
1677
+ public var state: StreamState {
1678
+ switch self {
1679
+ case let .lifecycle(state, _, _):
1680
+ state
1681
+ case .reconnecting:
1682
+ .reconnecting
1683
+ case .reconnected:
1684
+ .reconnected
1685
+ case .reconnectFailed:
1686
+ .reconnectFailed
1687
+ case .error:
1688
+ .error
1689
+ case let .snapshot(state, _, _, _, _, _):
1690
+ state
1691
+ }
1692
+ }
1693
+
1694
+ public var streamId: String? {
1695
+ switch self {
1696
+ case let .lifecycle(_, streamId, _),
1697
+ let .reconnecting(streamId, _, _, _, _),
1698
+ let .reconnected(streamId, _, _),
1699
+ let .reconnectFailed(streamId, _, _),
1700
+ let .error(streamId, _, _),
1701
+ let .snapshot(_, _, _, streamId, _, _):
1702
+ streamId
1703
+ }
1704
+ }
1705
+
1706
+ public var timestamp: Int? {
1707
+ switch self {
1708
+ case let .lifecycle(_, _, timestamp),
1709
+ let .reconnecting(_, _, _, _, timestamp),
1710
+ let .reconnected(_, _, timestamp),
1711
+ let .reconnectFailed(_, _, timestamp),
1712
+ let .error(_, _, timestamp),
1713
+ let .snapshot(_, _, _, _, _, timestamp):
1714
+ timestamp
1715
+ }
1716
+ }
1717
+
1718
+ public var values: [String: Any] {
1719
+ var values: [String: Any] = [
1720
+ "kind": kind.rawValue,
1721
+ "status": state.rawValue,
1722
+ ]
1723
+ if let streamId, !streamId.isEmpty {
1724
+ values["streamId"] = streamId
1725
+ }
1726
+ if let timestamp {
1727
+ values["timestamp"] = timestamp
1728
+ }
1729
+
1730
+ switch self {
1731
+ case .lifecycle:
1732
+ break
1733
+ case let .reconnecting(_, attempt, maxAttempts, reason, _):
1734
+ values["attempt"] = attempt
1735
+ values["maxAttempts"] = maxAttempts
1736
+ values["reason"] = reason
1737
+ case let .reconnected(_, attempt, _):
1738
+ values["attempt"] = attempt
1739
+ case let .reconnectFailed(_, maxAttempts, _):
1740
+ values["maxAttempts"] = maxAttempts
1741
+ case let .error(_, errorDetails, _):
1742
+ values["errorDetails"] = errorDetails
1743
+ case let .snapshot(_, streaming, reconnecting, _, attempt, _):
1744
+ values["streaming"] = streaming
1745
+ values["reconnecting"] = reconnecting
1746
+ if let attempt {
1747
+ values["attempt"] = attempt
1748
+ }
1749
+ }
1750
+
1751
+ return values
1752
+ }
1753
+
1754
+ public var description: String {
1755
+ "StreamStatus(kind: \(kind.rawValue), status: \(state.rawValue), streamId: \(streamId ?? "none"))"
1756
+ }
1757
+ }
1758
+
1759
+ public struct StreamStatusEvent: CustomStringConvertible {
1760
+ public let status: StreamStatus
1761
+
1762
+ public init(status: StreamStatus) {
1763
+ self.status = status
1764
+ }
1765
+
1766
+ public init(values: [String: Any]) {
1767
+ self.status = StreamStatus(values: values)
1768
+ }
1769
+
1770
+ public var state: StreamState {
1771
+ status.state
1772
+ }
1773
+
1774
+ public var streamId: String? {
1775
+ status.streamId
1776
+ }
1777
+
1778
+ public var values: [String: Any] {
1779
+ var values = status.values
1780
+ values["type"] = "stream_status"
1781
+ return values
1782
+ }
1783
+
1784
+ public var description: String {
1785
+ "StreamStatusEvent(kind: \(status.kind.rawValue), status: \(state.rawValue), streamId: \(streamId ?? "none"))"
1786
+ }
1787
+ }
1788
+
1789
+ public struct KeepAliveAckEvent: CustomStringConvertible, Equatable {
1790
+ public let streamId: String
1791
+ public let ackId: String
1792
+ public let timestamp: Int?
1793
+
1794
+ public init(streamId: String, ackId: String, timestamp: Int? = nil) {
1795
+ self.streamId = streamId
1796
+ self.ackId = ackId
1797
+ self.timestamp = timestamp
1798
+ }
1799
+
1800
+ public init(values: [String: Any]) {
1801
+ self.streamId = stringValue(values, "streamId", "stream_id") ?? ""
1802
+ self.ackId = stringValue(values, "ackId", "ack_id") ?? ""
1803
+ self.timestamp = intValue(values["timestamp"])
1804
+ }
1805
+
1806
+ public var values: [String: Any] {
1807
+ var values: [String: Any] = [
1808
+ "type": "keep_alive_ack",
1809
+ "streamId": streamId,
1810
+ "ackId": ackId,
1811
+ ]
1812
+ if let timestamp {
1813
+ values["timestamp"] = timestamp
1814
+ }
1815
+ return values
1816
+ }
1817
+
1818
+ public var description: String {
1819
+ "KeepAliveAckEvent(streamId: \(streamId), ackId: \(ackId))"
1820
+ }
1821
+ }
1822
+
1823
+ public struct BluetoothError: Error, LocalizedError, CustomStringConvertible {
1824
+ public let code: String
1825
+ public let message: String
1826
+
1827
+ public init(code: String, message: String) {
1828
+ self.code = code
1829
+ self.message = message
1830
+ }
1831
+
1832
+ public var errorDescription: String? {
1833
+ message
1834
+ }
1835
+
1836
+ public var description: String {
1837
+ "\(code): \(message)"
1838
+ }
1839
+ }
1840
+
1841
+ private final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
1842
+ static let shared = BluetoothAvailability()
1843
+
1844
+ private var centralManager: CBCentralManager?
1845
+ private var state: CBManagerState = .unknown
1846
+
1847
+ override private init() {
1848
+ super.init()
1849
+ centralManager = CBCentralManager(
1850
+ delegate: self,
1851
+ queue: .main,
1852
+ options: [CBCentralManagerOptionShowPowerAlertKey: false]
1853
+ )
1854
+ state = centralManager?.state ?? .unknown
1855
+ }
1856
+
1857
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
1858
+ state = central.state
1859
+ }
1860
+
1861
+ func requirePoweredOn(operation: String) throws {
1862
+ if let current = centralManager?.state {
1863
+ state = current
1864
+ }
1865
+ switch state {
1866
+ case .poweredOn:
1867
+ return
1868
+ case .poweredOff:
1869
+ throw BluetoothError(
1870
+ code: "bluetooth_powered_off",
1871
+ message: "Turn on phone Bluetooth to \(operation)."
1872
+ )
1873
+ case .unauthorized:
1874
+ throw BluetoothError(
1875
+ code: "bluetooth_unauthorized",
1876
+ message: "Allow Bluetooth access to \(operation)."
1877
+ )
1878
+ case .unsupported:
1879
+ throw BluetoothError(
1880
+ code: "bluetooth_unsupported",
1881
+ message: "This phone does not support Bluetooth."
1882
+ )
1883
+ case .resetting, .unknown:
1884
+ throw BluetoothError(
1885
+ code: "bluetooth_not_ready",
1886
+ message: "Bluetooth is not ready yet. Try again."
1887
+ )
1888
+ @unknown default:
1889
+ throw BluetoothError(
1890
+ code: "bluetooth_unavailable",
1891
+ message: "Bluetooth is unavailable. Try again."
1892
+ )
1893
+ }
1894
+ }
1895
+ }
1896
+
1897
+ public enum ScanStopReason {
1898
+ case completed
1899
+ case cancelled
1900
+ case error
1901
+ }
1902
+
1903
+ public struct LocalTranscriptionEvent: CustomStringConvertible {
1904
+ public let text: String
1905
+ public let isFinal: Bool
1906
+ public let values: [String: Any]
1907
+
1908
+ public init(text: String, isFinal: Bool, values: [String: Any]) {
1909
+ self.text = text
1910
+ self.isFinal = isFinal
1911
+ self.values = values
1912
+ }
1913
+
1914
+ public var description: String {
1915
+ "LocalTranscriptionEvent(text: \(text), isFinal: \(isFinal))"
1916
+ }
1917
+ }
1918
+
1919
+ public enum BluetoothEvent: CustomStringConvertible {
1920
+ case buttonPress(ButtonPressEvent)
1921
+ case touch(TouchEvent)
1922
+ case wifiStatus(WifiStatusEvent)
1923
+ case hotspotStatus(HotspotStatusEvent)
1924
+ case hotspotError(HotspotErrorEvent)
1925
+ case photoResponse(PhotoResponseEvent)
1926
+ case streamStatus(StreamStatusEvent)
1927
+ case keepAliveAck(KeepAliveAckEvent)
1928
+ case localTranscription(LocalTranscriptionEvent)
1929
+ case raw(name: String, values: [String: Any])
1930
+
1931
+ public var description: String {
1932
+ switch self {
1933
+ case let .buttonPress(event):
1934
+ event.description
1935
+ case let .touch(event):
1936
+ event.description
1937
+ case let .wifiStatus(event):
1938
+ event.description
1939
+ case let .hotspotStatus(event):
1940
+ event.description
1941
+ case let .hotspotError(event):
1942
+ event.description
1943
+ case let .photoResponse(event):
1944
+ event.description
1945
+ case let .streamStatus(event):
1946
+ event.description
1947
+ case let .keepAliveAck(event):
1948
+ event.description
1949
+ case let .localTranscription(event):
1950
+ event.description
1951
+ case let .raw(name, values):
1952
+ "\(name): \(values)"
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ @MainActor
1958
+ public protocol MentraBluetoothSDKDelegate: AnyObject {
1959
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didUpdateGlassesStatus status: GlassesStatusUpdate)
1960
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didUpdateBluetoothStatus status: BluetoothStatusUpdate)
1961
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didDiscover device: Device)
1962
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didStopScan reason: ScanStopReason)
1963
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceive event: BluetoothEvent)
1964
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicPcm frame: Data)
1965
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicLc3 frame: Data)
1966
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didChangeDefaultDevice device: Device?)
1967
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didLog message: String)
1968
+ func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error: BluetoothError)
1969
+ }
1970
+
1971
+ @MainActor
1972
+ public extension MentraBluetoothSDKDelegate {
1973
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didUpdateGlassesStatus _: GlassesStatusUpdate) {}
1974
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didUpdateBluetoothStatus _: BluetoothStatusUpdate) {}
1975
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didDiscover _: Device) {}
1976
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didStopScan _: ScanStopReason) {}
1977
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceive _: BluetoothEvent) {}
1978
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicPcm _: Data) {}
1979
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicLc3 _: Data) {}
1980
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didChangeDefaultDevice _: Device?) {}
1981
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didLog _: String) {}
1982
+ func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _: BluetoothError) {}
1983
+ }
1984
+
1985
+ @MainActor
1986
+ public final class MentraBluetoothSDK {
1987
+ public weak var delegate: MentraBluetoothSDKDelegate?
1988
+
1989
+ private let configuration: MentraBluetoothSDKConfiguration
1990
+ private var discoveredDeviceNames = Set<String>()
1991
+ private var bridgeEventSinkId: String?
1992
+ private var storeListenerId: String?
1993
+ private let defaultDeviceKeys: Set<String> = ["default_wearable", "device_name", "device_address"]
1994
+ private var suppressDefaultDeviceEvents = false
1995
+ private var defaultDeviceApplyGeneration = 0
1996
+
1997
+ public init(configuration: MentraBluetoothSDKConfiguration = .default) {
1998
+ self.configuration = configuration
1999
+ _ = BluetoothAvailability.shared
2000
+ bridgeEventSinkId = Bridge.addEventSink { [weak self] eventName, data in
2001
+ Task { @MainActor [weak self] in
2002
+ self?.dispatchBridgeEvent(eventName, data)
2003
+ }
2004
+ }
2005
+ storeListenerId = DeviceStore.shared.store.addListener { [weak self] category, changes in
2006
+ Task { @MainActor [weak self] in
2007
+ self?.dispatchStoreUpdate(category, changes)
2008
+ }
2009
+ }
2010
+ }
2011
+
2012
+ public var glassesStatus: GlassesStatus {
2013
+ GlassesStatus(values: DeviceStore.shared.store.getCategory("glasses"))
2014
+ }
2015
+
2016
+ public var bluetoothStatus: BluetoothStatus {
2017
+ BluetoothStatus(values: DeviceStore.shared.store.getCategory(ObservableStore.bluetoothCategory))
2018
+ }
2019
+
2020
+ public var defaultDevice: Device? {
2021
+ currentDefaultDevice()
2022
+ }
2023
+
2024
+ public func getDefaultDevice() -> Device? {
2025
+ currentDefaultDevice()
2026
+ }
2027
+
2028
+ public func setDefaultDevice(_ device: Device?) {
2029
+ guard let device else {
2030
+ clearDefaultDevice()
2031
+ return
2032
+ }
2033
+ defaultDeviceApplyGeneration += 1
2034
+ let generation = defaultDeviceApplyGeneration
2035
+ suppressDefaultDeviceEvents = true
2036
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", device.model.deviceType)
2037
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", device.name)
2038
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", device.identifier ?? "")
2039
+ finishDefaultDeviceApply(generation: generation)
2040
+ }
2041
+
2042
+ public func clearDefaultDevice() {
2043
+ defaultDeviceApplyGeneration += 1
2044
+ let generation = defaultDeviceApplyGeneration
2045
+ suppressDefaultDeviceEvents = true
2046
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", "")
2047
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
2048
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
2049
+ finishDefaultDeviceApply(generation: generation)
2050
+ }
2051
+
2052
+ public func startScan(model: DeviceModel) throws {
2053
+ if model != .simulated {
2054
+ try BluetoothAvailability.shared.requirePoweredOn(operation: "scan for glasses")
2055
+ }
2056
+ discoveredDeviceNames.removeAll()
2057
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", true)
2058
+ DeviceManager.shared.findCompatibleDevices(model.deviceType)
2059
+ }
2060
+
2061
+ public func stopScan() {
2062
+ DeviceManager.shared.stopScan()
2063
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", false)
2064
+ delegate?.mentraBluetoothSDK(self, didStopScan: .cancelled)
2065
+ }
2066
+
2067
+ public func connect(to device: Device, options: ConnectOptions = ConnectOptions()) throws {
2068
+ if device.model != .simulated {
2069
+ try BluetoothAvailability.shared.requirePoweredOn(operation: "connect to glasses")
2070
+ }
2071
+ let isController = ControllerTypes.ALL.contains(device.model.deviceType)
2072
+ if options.cancelExistingConnectionAttempt {
2073
+ if isController {
2074
+ DeviceManager.shared.disconnectController()
2075
+ } else {
2076
+ cancelConnectionAttempt()
2077
+ }
2078
+ }
2079
+ if options.saveAsDefault && !isController {
2080
+ setDefaultDevice(device)
2081
+ }
2082
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
2083
+ DeviceManager.shared.connectByName(device.name)
2084
+ }
2085
+
2086
+ public func connectDefault(options: ConnectOptions = ConnectOptions()) throws {
2087
+ guard let device = currentDefaultDevice() else {
2088
+ throw BluetoothError(
2089
+ code: "default_device_missing",
2090
+ message: "Set a default glasses device before calling connectDefault."
2091
+ )
2092
+ }
2093
+ if device.model != .simulated {
2094
+ try BluetoothAvailability.shared.requirePoweredOn(operation: "connect to glasses")
2095
+ }
2096
+ if options.cancelExistingConnectionAttempt {
2097
+ cancelConnectionAttempt()
2098
+ }
2099
+ DeviceManager.shared.connectDefault()
2100
+ }
2101
+
2102
+ public func cancelConnectionAttempt() {
2103
+ DeviceManager.shared.disconnect()
2104
+ }
2105
+
2106
+ public func connectSimulated() {
2107
+ DeviceManager.shared.connectSimulated()
2108
+ }
2109
+
2110
+ public func disconnect() {
2111
+ DeviceManager.shared.disconnect()
2112
+ }
2113
+
2114
+ public func forget() {
2115
+ DeviceManager.shared.forget()
2116
+ }
2117
+
2118
+ public func displayText(_ request: DisplayTextRequest) async throws {
2119
+ DeviceManager.shared.displayText(request.dictionary)
2120
+ }
2121
+
2122
+ public func displayEvent(_ request: DisplayEventRequest) async throws {
2123
+ DeviceManager.shared.displayEvent(request.values)
2124
+ }
2125
+
2126
+ public func clearDisplay() async throws {
2127
+ DeviceManager.shared.sgc?.clearDisplay()
2128
+ }
2129
+
2130
+ public func showDashboard() {
2131
+ DeviceManager.shared.showDashboard()
2132
+ }
2133
+
2134
+ public func setBrightness(_ level: Int, autoMode: Bool? = nil) async throws {
2135
+ if let autoMode {
2136
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_brightness", autoMode)
2137
+ }
2138
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "brightness", level)
2139
+ }
2140
+
2141
+ public func setAutoBrightness(enabled: Bool) async throws {
2142
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_brightness", enabled)
2143
+ }
2144
+
2145
+ public func setDashboardPosition(_ request: DashboardPositionRequest) async throws {
2146
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "dashboard_height", request.height)
2147
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "dashboard_depth", request.depth)
2148
+ }
2149
+
2150
+ public func setDashboardMenu(_ items: [DashboardMenuItem]) async throws {
2151
+ DeviceStore.shared.apply(
2152
+ ObservableStore.bluetoothCategory,
2153
+ "menu_apps",
2154
+ items.map(\.dictionary)
2155
+ )
2156
+ }
2157
+
2158
+ public func setHeadUpAngle(_ angleDegrees: Int) async throws {
2159
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "head_up_angle", angleDegrees)
2160
+ }
2161
+
2162
+ public func setScreenDisabled(_ disabled: Bool) async throws {
2163
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "screen_disabled", disabled)
2164
+ }
2165
+
2166
+ public func setGalleryMode(_ mode: GalleryMode) async throws {
2167
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "gallery_mode", mode == .auto)
2168
+ }
2169
+
2170
+ public func setButtonPhotoSettings(_ settings: ButtonPhotoSettings) async throws {
2171
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_photo_size", settings.size.rawValue)
2172
+ }
2173
+
2174
+ public func setButtonVideoRecordingSettings(_ settings: ButtonVideoRecordingSettings) async throws {
2175
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_video_width", settings.width)
2176
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_video_height", settings.height)
2177
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_video_fps", settings.fps)
2178
+ }
2179
+
2180
+ public func setButtonCameraLed(enabled: Bool) async throws {
2181
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_camera_led", enabled)
2182
+ }
2183
+
2184
+ public func setButtonMaxRecordingTime(minutes: Int) async throws {
2185
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "button_max_recording_time", minutes)
2186
+ }
2187
+
2188
+ public func setCameraFov(_ fov: CameraFov) async throws {
2189
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "camera_fov", fov.value)
2190
+ }
2191
+
2192
+ public func setMicState(_ config: MicConfiguration) {
2193
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_pcm", config.sendPcmData)
2194
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_lc3", config.sendLc3Data)
2195
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_transcript", config.sendTranscript)
2196
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "bypass_vad", config.bypassVad)
2197
+ DeviceManager.shared.setMicState()
2198
+ }
2199
+
2200
+ public func setPreferredMic(_ preferredMic: MicPreference) {
2201
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "preferred_mic", preferredMic.rawValue)
2202
+ }
2203
+
2204
+ public func setOwnAppAudioPlaying(_ playing: Bool) {
2205
+ PhoneAudioMonitor.getInstance().setOwnAppAudioPlaying(playing)
2206
+ }
2207
+
2208
+ public func requestWifiScan() {
2209
+ DeviceManager.shared.requestWifiScan()
2210
+ }
2211
+
2212
+ public func sendWifiCredentials(ssid: String, password: String) {
2213
+ DeviceManager.shared.sendWifiCredentials(ssid, password)
2214
+ }
2215
+
2216
+ public func forgetWifiNetwork(ssid: String) {
2217
+ DeviceManager.shared.forgetWifiNetwork(ssid)
2218
+ }
2219
+
2220
+ public func setHotspotState(enabled: Bool) {
2221
+ DeviceManager.shared.setHotspotState(enabled)
2222
+ }
2223
+
2224
+ public func requestPhoto(_ request: PhotoRequest) {
2225
+ DeviceManager.shared.photoRequest(
2226
+ request.requestId,
2227
+ request.appId,
2228
+ request.size.rawValue,
2229
+ request.webhookUrl,
2230
+ request.authToken,
2231
+ request.compress?.rawValue,
2232
+ request.flash,
2233
+ request.sound
2234
+ )
2235
+ }
2236
+
2237
+ public func queryGalleryStatus() {
2238
+ DeviceManager.shared.queryGalleryStatus()
2239
+ }
2240
+
2241
+ public func startStream(_ request: StreamRequest) {
2242
+ DeviceManager.shared.startStream(request.values)
2243
+ }
2244
+
2245
+ public func keepStreamAlive(_ request: StreamKeepAliveRequest) {
2246
+ DeviceManager.shared.keepStreamAlive(request.values)
2247
+ }
2248
+
2249
+ public func rgbLedControl(_ request: RgbLedRequest) {
2250
+ DeviceManager.shared.rgbLedControl(
2251
+ requestId: request.requestId,
2252
+ packageName: request.packageName,
2253
+ action: request.action.rawValue,
2254
+ color: request.color?.rawValue,
2255
+ ontime: request.ontime,
2256
+ offtime: request.offtime,
2257
+ count: request.count
2258
+ )
2259
+ }
2260
+
2261
+ public func stopStream() {
2262
+ DeviceManager.shared.stopStream()
2263
+ }
2264
+
2265
+ public func startVideoRecording(_ request: VideoRecordingRequest) {
2266
+ DeviceManager.shared.startVideoRecording(
2267
+ request.requestId,
2268
+ request.save,
2269
+ request.flash,
2270
+ request.sound
2271
+ )
2272
+ }
2273
+
2274
+ public func stopVideoRecording(requestId: String) {
2275
+ DeviceManager.shared.stopVideoRecording(requestId)
2276
+ }
2277
+
2278
+ public func requestVersionInfo() {
2279
+ DeviceManager.shared.requestVersionInfo()
2280
+ }
2281
+
2282
+ public func sendOtaStart() {
2283
+ DeviceManager.shared.sendOtaStart()
2284
+ }
2285
+
2286
+ public func sendOtaQueryStatus() {
2287
+ DeviceManager.shared.sendOtaQueryStatus()
2288
+ }
2289
+
2290
+ public func sendShutdown() {
2291
+ DeviceManager.shared.sendShutdown()
2292
+ }
2293
+
2294
+ public func sendReboot() {
2295
+ DeviceManager.shared.sendReboot()
2296
+ }
2297
+
2298
+ public func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
2299
+ DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
2300
+ }
2301
+
2302
+ public func invalidate() {
2303
+ if let bridgeEventSinkId {
2304
+ Bridge.removeEventSink(bridgeEventSinkId)
2305
+ self.bridgeEventSinkId = nil
2306
+ }
2307
+ if let storeListenerId {
2308
+ DeviceStore.shared.store.removeListener(storeListenerId)
2309
+ self.storeListenerId = nil
2310
+ }
2311
+ delegate = nil
2312
+ }
2313
+
2314
+ private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
2315
+ switch ObservableStore.normalizeCategory(category) {
2316
+ case "glasses":
2317
+ delegate?.mentraBluetoothSDK(self, didUpdateGlassesStatus: GlassesStatusUpdate(values: glassesStatusChanges(changes)))
2318
+ case ObservableStore.bluetoothCategory:
2319
+ delegate?.mentraBluetoothSDK(self, didUpdateBluetoothStatus: BluetoothStatusUpdate(values: changes))
2320
+ if !suppressDefaultDeviceEvents && changes.keys.contains(where: { defaultDeviceKeys.contains($0) }) {
2321
+ dispatchDefaultDeviceChanged()
2322
+ }
2323
+ dispatchDiscoveredDevices(changes["searchResults"])
2324
+ default:
2325
+ break
2326
+ }
2327
+ }
2328
+
2329
+ private func glassesStatusChanges(_ changes: [String: Any]) -> [String: Any] {
2330
+ var merged = changes
2331
+
2332
+ if changes.keys.contains(where: { ["wifiConnected", "wifiSsid", "wifiLocalIp"].contains($0) }) {
2333
+ merged["wifiConnected"] = DeviceStore.shared.get("glasses", "wifiConnected") as? Bool ?? false
2334
+ merged["wifiSsid"] = DeviceStore.shared.get("glasses", "wifiSsid") as? String ?? ""
2335
+ merged["wifiLocalIp"] = DeviceStore.shared.get("glasses", "wifiLocalIp") as? String ?? ""
2336
+ }
2337
+
2338
+ if changes.keys.contains(where: { ["connected", "fullyBooted", "connectionState"].contains($0) }) {
2339
+ merged["connected"] = GlassesStore.shared.get("glasses", "connected") as? Bool ?? false
2340
+ merged["fullyBooted"] = GlassesStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
2341
+ merged["connectionState"] = GlassesStore.shared.get("glasses", "connectionState") as? String ?? "DISCONNECTED"
2342
+ }
2343
+
2344
+ if changes["signalStrengthUpdatedAt"] != nil, changes["signalStrength"] == nil {
2345
+ merged["signalStrength"] = DeviceStore.shared.get("glasses", "signalStrength") as? Int ?? -1
2346
+ }
2347
+
2348
+ return merged
2349
+ }
2350
+
2351
+ private func dispatchDefaultDeviceChanged() {
2352
+ delegate?.mentraBluetoothSDK(self, didChangeDefaultDevice: currentDefaultDevice())
2353
+ }
2354
+
2355
+ private func finishDefaultDeviceApply(generation: Int) {
2356
+ Task { @MainActor [weak self] in
2357
+ guard let self, generation == self.defaultDeviceApplyGeneration else { return }
2358
+ self.suppressDefaultDeviceEvents = false
2359
+ self.dispatchDefaultDeviceChanged()
2360
+ }
2361
+ }
2362
+
2363
+ private func currentDefaultDevice() -> Device? {
2364
+ let core = DeviceStore.shared.store.getCategory(ObservableStore.bluetoothCategory)
2365
+ guard let model = core["default_wearable"] as? String, !model.isEmpty else { return nil }
2366
+ guard let name = core["device_name"] as? String, !name.isEmpty else { return nil }
2367
+ let identifier = (core["device_address"] as? String).flatMap { $0.isEmpty ? nil : $0 }
2368
+ return Device(
2369
+ model: DeviceModel.fromDeviceType(model),
2370
+ name: name,
2371
+ identifier: identifier
2372
+ )
2373
+ }
2374
+
2375
+ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2376
+ guard let results = rawSearchResults as? [[String: Any]] else { return }
2377
+ for result in results {
2378
+ guard let name = result["deviceName"] as? String ?? result["name"] as? String else { continue }
2379
+ guard discoveredDeviceNames.insert(name).inserted else { continue }
2380
+ guard let device = Device(values: result) else { continue }
2381
+ delegate?.mentraBluetoothSDK(self, didDiscover: device)
2382
+ }
2383
+ }
2384
+
2385
+ private func dispatchBridgeEvent(_ eventName: String, _ data: [String: Any]) {
2386
+ switch eventName {
2387
+ case "log":
2388
+ delegate?.mentraBluetoothSDK(self, didLog: data["message"] as? String ?? data.description)
2389
+ case "button_press":
2390
+ let event = ButtonPressEvent(
2391
+ buttonId: data["buttonId"] as? String ?? "",
2392
+ pressType: data["pressType"] as? String ?? "",
2393
+ timestamp: intValue(data["timestamp"])
2394
+ )
2395
+ delegate?.mentraBluetoothSDK(self, didReceive: .buttonPress(event))
2396
+ case "touch_event":
2397
+ delegate?.mentraBluetoothSDK(self, didReceive: .touch(TouchEvent(values: data)))
2398
+ case "mic_pcm":
2399
+ if let frame = data["pcm"] as? Data {
2400
+ delegate?.mentraBluetoothSDK(self, didReceiveMicPcm: frame)
2401
+ }
2402
+ case "mic_lc3":
2403
+ if let frame = data["lc3"] as? Data {
2404
+ delegate?.mentraBluetoothSDK(self, didReceiveMicLc3: frame)
2405
+ }
2406
+ case "local_transcription":
2407
+ let event = LocalTranscriptionEvent(
2408
+ text: data["text"] as? String ?? "",
2409
+ isFinal: data["isFinal"] as? Bool ?? false,
2410
+ values: data
2411
+ )
2412
+ delegate?.mentraBluetoothSDK(self, didReceive: .localTranscription(event))
2413
+ case "hotspot_status_change":
2414
+ delegate?.mentraBluetoothSDK(self, didReceive: .hotspotStatus(HotspotStatusEvent(values: data)))
2415
+ case "wifi_status_change":
2416
+ delegate?.mentraBluetoothSDK(self, didReceive: .wifiStatus(WifiStatusEvent(values: data)))
2417
+ case "hotspot_error":
2418
+ delegate?.mentraBluetoothSDK(self, didReceive: .hotspotError(HotspotErrorEvent(values: data)))
2419
+ case "photo_response":
2420
+ delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(PhotoResponseEvent(values: data)))
2421
+ case "stream_status":
2422
+ delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(StreamStatusEvent(values: data)))
2423
+ case "keep_alive_ack":
2424
+ delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(KeepAliveAckEvent(values: data)))
2425
+ case "compatible_glasses_search_stop":
2426
+ delegate?.mentraBluetoothSDK(self, didStopScan: .completed)
2427
+ case "pair_failure":
2428
+ delegate?.mentraBluetoothSDK(
2429
+ self,
2430
+ didFail: BluetoothError(
2431
+ code: "pair_failure",
2432
+ message: data["error"] as? String ?? data.description
2433
+ )
2434
+ )
2435
+ default:
2436
+ delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
2437
+ }
2438
+ }
2439
+ }