@mentra/bluetooth-sdk 0.1.9 → 0.1.10

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 (147) hide show
  1. package/README.md +116 -18
  2. package/android/build.gradle +289 -16
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/build.gradle +95 -6
  5. package/android/settings.gradle +3 -0
  6. package/android/silero/LICENSE.md +17 -0
  7. package/android/silero/build.gradle +42 -0
  8. package/android/silero/proguard-rules.pro +24 -0
  9. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  10. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  11. package/android/silero/src/main/assets/LICENSE +21 -0
  12. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  13. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  14. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  15. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  16. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  17. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  18. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  19. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  20. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
  23. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
  24. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
  25. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
  26. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
  27. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
  29. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
  30. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
  31. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
  32. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
  33. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
  34. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
  36. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
  37. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
  42. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
  43. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
  44. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
  45. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
  46. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
  47. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
  48. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
  49. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
  50. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  51. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
  52. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  53. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
  54. package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  55. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
  56. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
  57. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
  58. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
  59. package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
  62. package/android/src/test/resources/avif_with_exif.avif +0 -0
  63. package/build/BluetoothSdk.types.d.ts +241 -43
  64. package/build/BluetoothSdk.types.d.ts.map +1 -1
  65. package/build/BluetoothSdk.types.js +11 -10
  66. package/build/BluetoothSdk.types.js.map +1 -1
  67. package/build/_private/BluetoothSdkModule.d.ts +36 -23
  68. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  69. package/build/_private/BluetoothSdkModule.js +30 -52
  70. package/build/_private/BluetoothSdkModule.js.map +1 -1
  71. package/build/_private/photoRequestPayload.d.ts +4 -0
  72. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  73. package/build/_private/photoRequestPayload.js +25 -0
  74. package/build/_private/photoRequestPayload.js.map +1 -0
  75. package/build/index.d.ts +1 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +23 -0
  78. package/build/index.js.map +1 -1
  79. package/build/react/useMentraBluetooth.d.ts +2 -2
  80. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  81. package/build/react/useMentraBluetooth.js +3 -3
  82. package/build/react/useMentraBluetooth.js.map +1 -1
  83. package/ios/BluetoothSdkModule.swift +174 -70
  84. package/ios/LocalPhotoUploadServer.swift +370 -105
  85. package/ios/MentraBluetoothSDK.podspec +3 -1
  86. package/ios/MentraPhotoReceiverModule.swift +62 -10
  87. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  88. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
  89. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
  90. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  91. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  92. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  93. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  94. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  95. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  96. package/ios/Source/BluetoothSdkDefaults.swift +6 -0
  97. package/ios/Source/Bridge.swift +58 -19
  98. package/ios/Source/DeviceManager.swift +149 -17
  99. package/ios/Source/DeviceStore.swift +16 -6
  100. package/ios/Source/MentraBluetoothSDK.swift +803 -47
  101. package/ios/Source/ObservableStore.swift +6 -0
  102. package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
  103. package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
  104. package/ios/Source/controllers/ControllerManager.swift +4 -3
  105. package/ios/Source/controllers/R1.swift +3 -3
  106. package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
  107. package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
  108. package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
  109. package/ios/Source/services/PhoneMic.swift +1 -0
  110. package/ios/Source/sgcs/G1.swift +2 -2
  111. package/ios/Source/sgcs/G2.swift +645 -403
  112. package/ios/Source/sgcs/Mach1.swift +2 -2
  113. package/ios/Source/sgcs/MentraLive.swift +812 -319
  114. package/ios/Source/sgcs/MentraNex.swift +412 -182
  115. package/ios/Source/sgcs/SGCManager.swift +57 -3
  116. package/ios/Source/sgcs/Simulated.swift +7 -3
  117. package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
  118. package/ios/Source/status/DeviceStatus.swift +974 -0
  119. package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
  120. package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
  121. package/ios/Source/stt/STTTools.swift +27 -12
  122. package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
  123. package/ios/Source/tts/TTSTools.swift +164 -0
  124. package/ios/Source/utils/JSCExperiment.swift +7 -7
  125. package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
  126. package/ios/Source/utils/MessageChunker.swift +78 -21
  127. package/ios/Source/utils/TarBz2Extractor.swift +53 -2
  128. package/package.json +1 -1
  129. package/plugin/build/withAndroid.js +68 -4
  130. package/src/BluetoothSdk.types.ts +331 -59
  131. package/src/_private/BluetoothSdkModule.ts +113 -102
  132. package/src/_private/photoRequestPayload.ts +28 -0
  133. package/src/index.ts +58 -15
  134. package/src/react/useMentraBluetooth.ts +6 -4
  135. package/android/.project +0 -28
  136. package/android/lc3Lib/.project +0 -28
  137. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
  138. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
  139. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
  140. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  141. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  142. package/ios/Source/Status/DeviceStatus.swift +0 -471
  143. /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
  144. /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
  145. /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
  146. /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
  147. /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
@@ -6,10 +6,15 @@ import {
6
6
  BluetoothSdkModuleEvents,
7
7
  BluetoothStatus,
8
8
  ButtonPhotoSize,
9
+ CalendarEvent,
9
10
  CAMERA_FOV_DEFAULT,
10
11
  CAMERA_FOV_MAX,
11
12
  CAMERA_FOV_MIN,
12
- CameraFov,
13
+ CameraFovPreset,
14
+ CameraFovRequest,
15
+ CameraFovResult,
16
+ CameraFovSetting,
17
+ CameraRoiPosition,
13
18
  ConnectOptions,
14
19
  DashboardMenuItem,
15
20
  Device,
@@ -17,17 +22,32 @@ import {
17
22
  GlassesMediaVolumeGetResult,
18
23
  GlassesMediaVolumeSetResult,
19
24
  GlassesStatus,
25
+ GalleryStatusEvent,
26
+ HotspotStatusChangeEvent,
20
27
  MicPreference,
21
28
  ObservableStoreCategory,
29
+ OtaQueryResult,
30
+ OtaStartAckEvent,
22
31
  PhotoRequestParams,
32
+ PhotoSuccessResponseEvent,
23
33
  PublicBluetoothStatus,
24
34
  RgbLedAction,
25
35
  RgbLedColor,
36
+ RgbLedControlSuccessResponseEvent,
26
37
  ScanModelOptions,
27
38
  ScanOptions,
39
+ SettingsAckSuccessEvent,
28
40
  StreamKeepAliveRequest,
29
41
  StreamStartRequest,
42
+ StreamStatusEvent,
43
+ VideoRecordingStartedStatusEvent,
44
+ VideoRecordingSettings,
45
+ VideoRecordingStoppedStatusEvent,
46
+ VersionInfoResult,
47
+ WifiSearchResult,
48
+ WifiStatusChangeEvent,
30
49
  } from "../BluetoothSdk.types"
50
+ import {photoRequestParamsForNative} from "./photoRequestPayload"
31
51
 
32
52
  /**
33
53
  * Private React Native native-module facade.
@@ -78,6 +98,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
78
98
  setAutoBrightness(enabled: boolean): Promise<void>
79
99
  setDashboardPosition(height: number, depth: number): Promise<void>
80
100
  setDashboardMenu(items: DashboardMenuItem[]): Promise<void>
101
+ setCalendarEvents(events: CalendarEvent[]): Promise<void>
81
102
  setHeadUpAngle(angleDegrees: number): Promise<void>
82
103
  setScreenDisabled(disabled: boolean): Promise<void>
83
104
  ping(): Promise<void>
@@ -88,48 +109,54 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
88
109
  sendIncidentId(incidentId: string, apiBaseUrl?: string | null): Promise<void>
89
110
 
90
111
  // WiFi Commands
91
- requestWifiScan(): Promise<void>
92
- sendWifiCredentials(ssid: string, password: string): Promise<void>
93
- forgetWifiNetwork(ssid: string): Promise<void>
94
- setHotspotState(enabled: boolean): Promise<void>
112
+ requestWifiScan(): Promise<WifiSearchResult[]>
113
+ sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
114
+ forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
115
+ setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
116
+ /** Set glasses system clock (Mentra Live only) when phone detects clock skew. */
117
+ setSystemTime(timestampMs: number): Promise<void>
95
118
  /** Logs current WiFi frequency (MHz) and 5 GHz band to Android logcat. */
96
119
  logCurrentWifiFrequency(): Promise<void>
97
120
 
98
121
  // Gallery Commands
99
- setGalleryModeEnabled(enabled: boolean): Promise<void>
122
+ setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
100
123
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
101
- setButtonPhotoSettings(size: ButtonPhotoSize): Promise<void>
102
- setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<void>
103
- setButtonCameraLed(enabled: boolean): Promise<void>
104
- setButtonMaxRecordingTime(minutes: number): Promise<void>
105
- setCameraFov(fov: CameraFov): Promise<void>
106
- queryGalleryStatus(): Promise<void>
107
- requestPhoto(params: PhotoRequestParams): Promise<void>
124
+ setButtonPhotoSettings(size: ButtonPhotoSize): Promise<SettingsAckSuccessEvent>
125
+ setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
126
+ setButtonCameraLed(enabled: boolean): Promise<SettingsAckSuccessEvent>
127
+ setButtonMaxRecordingTime(minutes: number): Promise<SettingsAckSuccessEvent>
128
+ setCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
129
+ queryGalleryStatus(): Promise<GalleryStatusEvent>
130
+ requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
108
131
 
109
132
  // OTA Commands
110
- sendOtaStart(): Promise<void>
111
- sendOtaQueryStatus(): Promise<void>
133
+ sendOtaStart(): Promise<OtaStartAckEvent>
134
+ sendOtaQueryStatus(): Promise<OtaQueryResult>
135
+ /** Re-run glasses-side OTA version check (called after a clock fix invalidates a TLS failure). */
136
+ retryOtaVersionCheck(): Promise<OtaQueryResult>
137
+ checkForOtaUpdate(): Promise<OtaQueryResult>
138
+ startOtaUpdate(): Promise<OtaStartAckEvent>
112
139
 
113
140
  // Version Info Commands
114
- requestVersionInfo(): Promise<void>
141
+ requestVersionInfo(): Promise<VersionInfoResult>
115
142
 
116
143
  // Video Recording Commands
117
- startVideoRecording(requestId: string, save: boolean, sound: boolean): Promise<void>
118
- stopVideoRecording(requestId: string): Promise<void>
144
+ startVideoRecording(
145
+ requestId: string,
146
+ save: boolean,
147
+ sound: boolean,
148
+ settings?: VideoRecordingSettings,
149
+ ): Promise<VideoRecordingStartedStatusEvent>
150
+ stopVideoRecording(requestId: string): Promise<VideoRecordingStoppedStatusEvent>
119
151
 
120
152
  // Stream Commands
121
- startStream(params: StreamStartRequest): Promise<void>
122
- startCloudManagedStream(params: StreamStartRequest): Promise<void>
123
- stopStream(): Promise<void>
124
- sendCloudStreamKeepAlive(params: StreamKeepAliveRequest): Promise<void>
153
+ startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
154
+ startExternallyManagedStream(params: StreamStartRequest): Promise<StreamStatusEvent>
155
+ stopStream(): Promise<StreamStatusEvent>
156
+ sendExternallyManagedStreamKeepAlive(params: StreamKeepAliveRequest): Promise<void>
125
157
 
126
158
  // Microphone Commands
127
- setMicState(
128
- enabled: boolean,
129
- useGlassesMic?: boolean,
130
- sendTranscript?: boolean,
131
- sendLc3Data?: boolean,
132
- ): Promise<void>
159
+ setMicState(enabled: boolean, useGlassesMic?: boolean, sendTranscript?: boolean, sendLc3Data?: boolean): Promise<void>
133
160
  setPreferredMic(preferredMic: MicPreference): Promise<void>
134
161
  restartTranscriber(): Promise<void>
135
162
 
@@ -152,7 +179,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
152
179
  onDurationMs: number,
153
180
  offDurationMs: number,
154
181
  count: number,
155
- ): Promise<void>
182
+ ): Promise<RgbLedControlSuccessResponseEvent>
156
183
 
157
184
  // STT Commands
158
185
  setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -161,6 +188,20 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
161
188
  validateSttModel(path: string): Promise<boolean>
162
189
  extractTarBz2(sourcePath: string, destinationPath: string): Promise<boolean>
163
190
 
191
+ // TTS Commands
192
+ setTtsModelDetails(path: string, languageCode: string): Promise<void>
193
+ getTtsModelPath(): Promise<string>
194
+ getTtsModelLanguage(): Promise<string>
195
+ checkTtsModelAvailable(): Promise<boolean>
196
+ validateTtsModel(path: string): Promise<boolean>
197
+ generateTtsAudio(
198
+ text: string,
199
+ modelPath: string,
200
+ outputPath: string,
201
+ speakerId: number,
202
+ speed: number,
203
+ ): Promise<boolean>
204
+
164
205
  // Helper methods for type-safe observable store access
165
206
  updateGlasses(values: Partial<GlassesStatus>): Promise<void>
166
207
  updateBluetoothSettings(values: BluetoothSettingsUpdate): Promise<void>
@@ -186,23 +227,41 @@ const DEFAULT_SCAN_TIMEOUT_MS = 15_000
186
227
 
187
228
  const CAMERA_ROI_MIN = 0
188
229
  const CAMERA_ROI_MAX = 2
230
+ const CAMERA_ROI_POSITION_VALUES: Record<CameraRoiPosition, CameraFovSetting["roiPosition"]> = {
231
+ center: 0,
232
+ bottom: 1,
233
+ top: 2,
234
+ }
235
+
236
+ // Named presets are a convenience layer over the numeric {fov, roiPosition} API.
237
+ // "narrow" uses 82, a device-tested FOV; "standard" matches CAMERA_FOV_DEFAULT.
238
+ const CAMERA_FOV_PRESETS: Record<CameraFovPreset, CameraFovSetting> = {
239
+ narrow: {fov: 82, roiPosition: 0},
240
+ standard: {fov: CAMERA_FOV_DEFAULT, roiPosition: 0},
241
+ wide: {fov: CAMERA_FOV_MAX, roiPosition: 0},
242
+ }
189
243
 
190
244
  function clampInteger(value: number, min: number, max: number): number {
191
245
  return Math.max(min, Math.min(max, Math.round(value)))
192
246
  }
193
247
 
194
- function normalizeCameraFov(setting: CameraFov): Required<CameraFov> {
248
+ function normalizeCameraFov(request: CameraFovRequest): CameraFovSetting {
249
+ if ("preset" in request) {
250
+ return CAMERA_FOV_PRESETS[request.preset] ?? CAMERA_FOV_PRESETS.standard
251
+ }
252
+
253
+ const roiPosition = request.roiPosition ?? "center"
254
+
195
255
  return {
196
256
  fov: clampInteger(
197
- Number.isFinite(setting.fov) ? setting.fov : CAMERA_FOV_DEFAULT,
257
+ Number.isFinite(request.fov) ? request.fov : CAMERA_FOV_DEFAULT,
198
258
  CAMERA_FOV_MIN,
199
259
  CAMERA_FOV_MAX,
200
260
  ),
201
- roiPosition: clampInteger(
202
- Number.isFinite(setting.roiPosition ?? 0) ? (setting.roiPosition ?? 0) : 0,
203
- CAMERA_ROI_MIN,
204
- CAMERA_ROI_MAX,
205
- ) as Required<CameraFov>["roiPosition"],
261
+ roiPosition: clampInteger(CAMERA_ROI_POSITION_VALUES[roiPosition] ?? 0, CAMERA_ROI_MIN, CAMERA_ROI_MAX) as
262
+ | 0
263
+ | 1
264
+ | 2,
206
265
  }
207
266
  }
208
267
 
@@ -296,7 +355,9 @@ NativeBluetoothSdkModule.getGlassesStatus = function () {
296
355
  return Promise.resolve(nativeGetGlassesStatus())
297
356
  }
298
357
 
299
- const nativeGetBluetoothStatus = NativeBluetoothSdkModule.getBluetoothStatus.bind(NativeBluetoothSdkModule) as () => MaybePromise<BluetoothStatus>
358
+ const nativeGetBluetoothStatus = NativeBluetoothSdkModule.getBluetoothStatus.bind(
359
+ NativeBluetoothSdkModule,
360
+ ) as () => MaybePromise<BluetoothStatus>
300
361
  NativeBluetoothSdkModule.getBluetoothStatus = function () {
301
362
  return Promise.resolve(nativeGetBluetoothStatus())
302
363
  }
@@ -356,6 +417,10 @@ NativeBluetoothSdkModule.setDashboardMenu = function (items: DashboardMenuItem[]
356
417
  return this.updateBluetoothSettings({menu_apps: items.map(dashboardMenuItemToNative)})
357
418
  }
358
419
 
420
+ NativeBluetoothSdkModule.setCalendarEvents = function (events: CalendarEvent[]) {
421
+ return this.updateBluetoothSettings({calendar_events: events})
422
+ }
423
+
359
424
  NativeBluetoothSdkModule.setHeadUpAngle = function (angleDegrees: number) {
360
425
  return this.updateBluetoothSettings({head_up_angle: angleDegrees})
361
426
  }
@@ -364,39 +429,16 @@ NativeBluetoothSdkModule.setScreenDisabled = function (disabled: boolean) {
364
429
  return this.updateBluetoothSettings({screen_disabled: disabled})
365
430
  }
366
431
 
367
- NativeBluetoothSdkModule.setGalleryModeEnabled = function (enabled: boolean) {
368
- return this.updateBluetoothSettings({gallery_mode: enabled})
369
- }
370
-
371
432
  NativeBluetoothSdkModule.setVoiceActivityDetectionEnabled = function (enabled: boolean) {
372
433
  return this.updateBluetoothSettings({voice_activity_detection_enabled: enabled})
373
434
  }
374
435
 
375
- NativeBluetoothSdkModule.setButtonPhotoSettings = function (size: ButtonPhotoSize) {
376
- return this.updateBluetoothSettings({button_photo_size: size})
377
- }
378
-
379
- NativeBluetoothSdkModule.setButtonVideoRecordingSettings = function (width: number, height: number, fps: number) {
380
- return this.updateBluetoothSettings({
381
- button_video_width: width,
382
- button_video_height: height,
383
- button_video_fps: fps,
384
- })
385
- }
386
-
387
- NativeBluetoothSdkModule.setButtonCameraLed = function (enabled: boolean) {
388
- return this.updateBluetoothSettings({button_camera_led: enabled})
389
- }
390
-
391
- NativeBluetoothSdkModule.setButtonMaxRecordingTime = function (minutes: number) {
392
- return this.updateBluetoothSettings({button_max_recording_time: minutes})
393
- }
394
-
395
- NativeBluetoothSdkModule.setCameraFov = function (fov: CameraFov) {
396
- const setting = normalizeCameraFov(fov)
397
- return this.updateBluetoothSettings({
398
- camera_fov: {fov: setting.fov, roi_position: setting.roiPosition},
399
- })
436
+ const nativeSetCameraFov = NativeBluetoothSdkModule.setCameraFov.bind(NativeBluetoothSdkModule) as unknown as (
437
+ fov: CameraFovSetting,
438
+ ) => MaybePromise<CameraFovResult>
439
+ NativeBluetoothSdkModule.setCameraFov = function (request: CameraFovRequest) {
440
+ const setting = normalizeCameraFov(request)
441
+ return Promise.resolve(nativeSetCameraFov(setting))
400
442
  }
401
443
 
402
444
  NativeBluetoothSdkModule.setMicState = function (
@@ -406,12 +448,7 @@ NativeBluetoothSdkModule.setMicState = function (
406
448
  sendLc3Data?: boolean,
407
449
  ) {
408
450
  return Promise.resolve(
409
- nativeSetMicState(
410
- enabled,
411
- useGlassesMic ?? true,
412
- sendTranscript ?? false,
413
- sendLc3Data ?? false,
414
- ),
451
+ nativeSetMicState(enabled, useGlassesMic ?? true, sendTranscript ?? false, sendLc3Data ?? false),
415
452
  )
416
453
  }
417
454
 
@@ -441,10 +478,7 @@ NativeBluetoothSdkModule.connect = function (device: Device, options?: ConnectOp
441
478
  return this.connectWithOptions(device, {...DEFAULT_CONNECT_OPTIONS, ...options})
442
479
  }
443
480
 
444
- NativeBluetoothSdkModule.scan = async function (
445
- modelOrOptions: DeviceModel | ScanOptions,
446
- options?: ScanModelOptions,
447
- ) {
481
+ NativeBluetoothSdkModule.scan = async function (modelOrOptions: DeviceModel | ScanOptions, options?: ScanModelOptions) {
448
482
  const scanOptions = normalizeScanArgs(modelOrOptions, options)
449
483
  const timeoutMs = normalizeTimeoutMs(scanOptions.timeoutMs ?? scanOptions.timeout, DEFAULT_SCAN_TIMEOUT_MS)
450
484
  let latestResults: Device[] = []
@@ -505,40 +539,17 @@ NativeBluetoothSdkModule.scan = async function (
505
539
  })
506
540
  }
507
541
 
508
- /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
509
- function photoRequestParamsForNative(params: PhotoRequestParams): Record<string, string | number | boolean> {
510
- const payload: Record<string, string | number | boolean> = {
511
- requestId: params.requestId,
512
- appId: params.appId,
513
- size: params.size,
514
- webhookUrl: params.webhookUrl ?? "",
515
- compress: params.compress,
516
- flash: true,
517
- sound: params.sound,
518
- }
519
- if (params.authToken != null && params.authToken.length > 0) {
520
- payload.authToken = params.authToken
521
- }
522
- const exposureTimeNs = params.exposureTimeNs
523
- const hasManualExposure = exposureTimeNs != null && Number.isFinite(exposureTimeNs) && exposureTimeNs > 0
524
- if (hasManualExposure) {
525
- payload.exposureTimeNs = exposureTimeNs
526
- }
527
- if (hasManualExposure && params.iso != null && Number.isFinite(params.iso) && params.iso > 0) {
528
- payload.iso = Math.round(params.iso)
529
- }
530
- return payload
531
- }
532
-
533
542
  const nativeRequestPhoto = NativeBluetoothSdkModule.requestPhoto.bind(NativeBluetoothSdkModule)
534
543
  NativeBluetoothSdkModule.requestPhoto = function (params: PhotoRequestParams) {
535
544
  return nativeRequestPhoto(photoRequestParamsForNative(params) as unknown as PhotoRequestParams)
536
545
  }
537
546
 
538
547
  const nativeStartStream = NativeBluetoothSdkModule.startStream.bind(NativeBluetoothSdkModule)
539
- NativeBluetoothSdkModule.startCloudManagedStream = function (params: StreamStartRequest) {
548
+ NativeBluetoothSdkModule.startExternallyManagedStream = function (params: StreamStartRequest) {
540
549
  return nativeStartStream({...params, keepAliveMode: "external"} as StreamStartRequest)
541
550
  }
551
+ NativeBluetoothSdkModule.checkForOtaUpdate = NativeBluetoothSdkModule.sendOtaQueryStatus.bind(NativeBluetoothSdkModule)
552
+ NativeBluetoothSdkModule.startOtaUpdate = NativeBluetoothSdkModule.sendOtaStart.bind(NativeBluetoothSdkModule)
542
553
 
543
554
  export default NativeBluetoothSdkModule
544
555
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkPublicModule
@@ -0,0 +1,28 @@
1
+ import type {PhotoRequestParams} from "../BluetoothSdk.types"
2
+
3
+ /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
4
+ export function photoRequestParamsForNative(
5
+ params: PhotoRequestParams,
6
+ ): Record<string, string | number | boolean> {
7
+ const payload: Record<string, string | number | boolean> = {
8
+ requestId: params.requestId,
9
+ appId: params.appId,
10
+ size: params.size,
11
+ webhookUrl: params.webhookUrl ?? "",
12
+ compress: params.compress,
13
+ flash: true,
14
+ sound: params.sound,
15
+ }
16
+ if (params.authToken != null && params.authToken.length > 0) {
17
+ payload.authToken = params.authToken
18
+ }
19
+ const exposureTimeNs = params.exposureTimeNs
20
+ const hasManualExposure = exposureTimeNs != null && Number.isFinite(exposureTimeNs) && exposureTimeNs > 0
21
+ if (hasManualExposure) {
22
+ payload.exposureTimeNs = exposureTimeNs
23
+ }
24
+ if (hasManualExposure && params.iso != null && Number.isFinite(params.iso) && params.iso > 0) {
25
+ payload.iso = Math.round(params.iso)
26
+ }
27
+ return payload
28
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import PrivateBluetoothSdkModule from "./_private/BluetoothSdkModule"
2
- import type {
3
- BluetoothSdkEventListener,
4
- BluetoothSdkEventName,
5
- BluetoothSdkPublicModule,
6
- } from "./BluetoothSdk.types"
2
+ import type {BluetoothSdkEventListener, BluetoothSdkEventName, BluetoothSdkPublicModule} from "./BluetoothSdk.types"
7
3
 
8
4
  const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
9
5
  "log",
@@ -18,15 +14,18 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
18
14
  "battery_status",
19
15
  "local_transcription",
20
16
  "wifi_status_change",
17
+ "wifi_scan_result",
21
18
  "hotspot_status_change",
22
19
  "hotspot_error",
23
20
  "photo_response",
24
21
  "photo_status",
22
+ "video_recording_status",
25
23
  "gallery_status",
26
24
  "compatible_glasses_search_stop",
27
25
  "swipe_volume_status",
28
26
  "switch_status",
29
27
  "rgb_led_control_response",
28
+ "settings_ack",
30
29
  "pair_failure",
31
30
  "audio_pairing_needed",
32
31
  "audio_connected",
@@ -34,18 +33,18 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
34
33
  "mic_pcm",
35
34
  "mic_lc3",
36
35
  "stream_status",
36
+ "ota_update_available",
37
+ "ota_start_ack",
38
+ "ota_status",
39
+ "version_info",
40
+ "extraction_progress",
37
41
  ])
38
42
 
39
43
  const addListener: BluetoothSdkPublicModule["addListener"] = (eventName, listener) => {
40
44
  if (!PUBLIC_EVENT_NAMES.has(eventName)) {
41
- throw new Error(
42
- `Unsupported BluetoothSdk event "${eventName}". Use @mentra/bluetooth-sdk/react for status state.`,
43
- )
45
+ throw new Error(`Unsupported BluetoothSdk event "${eventName}". Use @mentra/bluetooth-sdk/react for status state.`)
44
46
  }
45
- return PrivateBluetoothSdkModule.addListener(
46
- eventName,
47
- listener as BluetoothSdkEventListener<BluetoothSdkEventName>,
48
- )
47
+ return PrivateBluetoothSdkModule.addListener(eventName, listener as BluetoothSdkEventListener<BluetoothSdkEventName>)
49
48
  }
50
49
 
51
50
  export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
@@ -72,9 +71,11 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
72
71
  forgetWifiNetwork: PrivateBluetoothSdkModule.forgetWifiNetwork.bind(PrivateBluetoothSdkModule),
73
72
  setHotspotState: PrivateBluetoothSdkModule.setHotspotState.bind(PrivateBluetoothSdkModule),
74
73
  setGalleryModeEnabled: PrivateBluetoothSdkModule.setGalleryModeEnabled.bind(PrivateBluetoothSdkModule),
75
- setVoiceActivityDetectionEnabled: PrivateBluetoothSdkModule.setVoiceActivityDetectionEnabled.bind(PrivateBluetoothSdkModule),
74
+ setVoiceActivityDetectionEnabled:
75
+ PrivateBluetoothSdkModule.setVoiceActivityDetectionEnabled.bind(PrivateBluetoothSdkModule),
76
76
  setButtonPhotoSettings: PrivateBluetoothSdkModule.setButtonPhotoSettings.bind(PrivateBluetoothSdkModule),
77
- setButtonVideoRecordingSettings: PrivateBluetoothSdkModule.setButtonVideoRecordingSettings.bind(PrivateBluetoothSdkModule),
77
+ setButtonVideoRecordingSettings:
78
+ PrivateBluetoothSdkModule.setButtonVideoRecordingSettings.bind(PrivateBluetoothSdkModule),
78
79
  setButtonCameraLed: PrivateBluetoothSdkModule.setButtonCameraLed.bind(PrivateBluetoothSdkModule),
79
80
  setButtonMaxRecordingTime: PrivateBluetoothSdkModule.setButtonMaxRecordingTime.bind(PrivateBluetoothSdkModule),
80
81
  setCameraFov: PrivateBluetoothSdkModule.setCameraFov.bind(PrivateBluetoothSdkModule),
@@ -91,6 +92,21 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
91
92
  setGlassesMediaVolume: PrivateBluetoothSdkModule.setGlassesMediaVolume.bind(PrivateBluetoothSdkModule),
92
93
  rgbLedControl: PrivateBluetoothSdkModule.rgbLedControl.bind(PrivateBluetoothSdkModule),
93
94
  requestVersionInfo: PrivateBluetoothSdkModule.requestVersionInfo.bind(PrivateBluetoothSdkModule),
95
+ checkForOtaUpdate: PrivateBluetoothSdkModule.sendOtaQueryStatus.bind(PrivateBluetoothSdkModule),
96
+ startOtaUpdate: PrivateBluetoothSdkModule.sendOtaStart.bind(PrivateBluetoothSdkModule),
97
+ retryOtaVersionCheck: PrivateBluetoothSdkModule.retryOtaVersionCheck.bind(PrivateBluetoothSdkModule),
98
+ setSttModelDetails: PrivateBluetoothSdkModule.setSttModelDetails.bind(PrivateBluetoothSdkModule),
99
+ getSttModelPath: PrivateBluetoothSdkModule.getSttModelPath.bind(PrivateBluetoothSdkModule),
100
+ checkSttModelAvailable: PrivateBluetoothSdkModule.checkSttModelAvailable.bind(PrivateBluetoothSdkModule),
101
+ validateSttModel: PrivateBluetoothSdkModule.validateSttModel.bind(PrivateBluetoothSdkModule),
102
+ extractTarBz2: PrivateBluetoothSdkModule.extractTarBz2.bind(PrivateBluetoothSdkModule),
103
+ restartTranscriber: PrivateBluetoothSdkModule.restartTranscriber.bind(PrivateBluetoothSdkModule),
104
+ setTtsModelDetails: PrivateBluetoothSdkModule.setTtsModelDetails.bind(PrivateBluetoothSdkModule),
105
+ getTtsModelPath: PrivateBluetoothSdkModule.getTtsModelPath.bind(PrivateBluetoothSdkModule),
106
+ getTtsModelLanguage: PrivateBluetoothSdkModule.getTtsModelLanguage.bind(PrivateBluetoothSdkModule),
107
+ checkTtsModelAvailable: PrivateBluetoothSdkModule.checkTtsModelAvailable.bind(PrivateBluetoothSdkModule),
108
+ validateTtsModel: PrivateBluetoothSdkModule.validateTtsModel.bind(PrivateBluetoothSdkModule),
109
+ generateTtsAudio: PrivateBluetoothSdkModule.generateTtsAudio.bind(PrivateBluetoothSdkModule),
94
110
  })
95
111
 
96
112
  export default BluetoothSdk
@@ -120,7 +136,9 @@ export type {
120
136
  BluetoothSdkSubscription,
121
137
  ButtonPhotoSize,
122
138
  ButtonPressEvent,
123
- CameraFov,
139
+ CameraFovPreset,
140
+ CameraFovRequest,
141
+ CameraFovResult,
124
142
  CameraRoiPosition,
125
143
  CompatibleGlassesSearchStopEvent,
126
144
  ConnectOptions,
@@ -144,20 +162,37 @@ export type {
144
162
  MicMode,
145
163
  MicPcmEvent,
146
164
  MicPreference,
165
+ OtaStartAckEvent,
166
+ OtaStatus,
167
+ OtaStatusEvent,
168
+ OtaQueryResult,
169
+ OtaUpdateAvailableEvent,
170
+ OtaUpdateInfo,
147
171
  PairFailureEvent,
172
+ PhotoCaptureMetadata,
148
173
  PhotoResolvedConfig,
149
174
  PhotoCompression,
175
+ PhotoFpsRange,
176
+ PhotoMeteredPreview,
150
177
  PhotoResponseEvent,
178
+ PhotoRequestedCaptureConfig,
151
179
  PhotoSize,
152
180
  PhotoStatusEvent,
153
181
  PhotoStatusState,
182
+ PhotoSuccessResponseEvent,
154
183
  RgbLedAction,
155
184
  RgbLedColor,
156
185
  RgbLedControlResponseEvent,
186
+ RgbLedControlSuccessResponseEvent,
157
187
  ScanModelOptions,
158
188
  ScanOptions,
159
189
  ScanResultsCallback,
160
190
  SpeakingStatusEvent,
191
+ SettingsAckEvent,
192
+ SettingsAckSetting,
193
+ SettingsAckSuccessEvent,
194
+ SettingsAckSuccessStatus,
195
+ SettingsAckStatus,
161
196
  StreamAudioConfig,
162
197
  StreamResolvedConfig,
163
198
  StreamStartRequest,
@@ -169,6 +204,14 @@ export type {
169
204
  SwipeVolumeStatusEvent,
170
205
  SwitchStatusEvent,
171
206
  TouchEvent,
207
+ VideoRecordingStartedStatusEvent,
208
+ VideoRecordingStatusEvent,
209
+ VideoRecordingStatusState,
210
+ VideoRecordingStoppedStatusEvent,
211
+ VideoRecordingSuccessStatusEvent,
212
+ VersionInfoEvent,
213
+ VersionInfoResult,
214
+ WifiScanResultEvent,
172
215
  VoiceActivityDetectionStatusEvent,
173
216
  WifiSearchResult,
174
217
  WifiStatus,
@@ -2,6 +2,7 @@ import {useState} from "react"
2
2
 
3
3
  import BluetoothSdk from "../index"
4
4
  import {
5
+ DEFAULT_VOICE_ACTIVITY_DETECTION_ENABLED,
5
6
  DeviceModels,
6
7
  createDisconnectedGlassesStatus,
7
8
  isConnectedGlassesConnectionStatus,
@@ -16,6 +17,7 @@ import type {
16
17
  PublicBluetoothStatus,
17
18
  PublicGlassesStatus,
18
19
  MicMode,
20
+ SettingsAckSuccessEvent,
19
21
  WifiSearchResult,
20
22
  WifiStatus,
21
23
  } from "../BluetoothSdk.types"
@@ -134,7 +136,7 @@ export type MentraBluetoothSession = {
134
136
  scan: ScanController
135
137
  sdk: PhoneSdkRuntimeState
136
138
  setDefaultDevice: (device: Device | null) => Promise<void>
137
- setGalleryModeEnabled: (enabled: boolean) => Promise<void>
139
+ setGalleryModeEnabled: (enabled: boolean) => Promise<SettingsAckSuccessEvent>
138
140
  setVoiceActivityDetectionEnabled: (enabled: boolean) => Promise<void>
139
141
  }
140
142
 
@@ -221,7 +223,7 @@ function runtimeGlassesState(status: Partial<PublicGlassesStatus>): GlassesRunti
221
223
  strengthDbm: numberValue((status as Record<string, unknown>).signalStrength),
222
224
  updatedAt: numberValue((status as Record<string, unknown>).signalStrengthUpdatedAt),
223
225
  },
224
- voiceActivityDetectionEnabled: status.voiceActivityDetectionEnabled ?? true,
226
+ voiceActivityDetectionEnabled: status.voiceActivityDetectionEnabled ?? DEFAULT_VOICE_ACTIVITY_DETECTION_ENABLED,
225
227
  wifi: status.wifi ?? {state: "disconnected"},
226
228
  }
227
229
  }
@@ -271,11 +273,11 @@ export function useMentraBluetooth(options: UseMentraBluetoothOptions = {}): Men
271
273
  const [galleryModeApplying, setGalleryModeApplying] = useState(false)
272
274
  const [galleryModeError, setGalleryModeError] = useState<unknown | null>(null)
273
275
 
274
- async function setGalleryModeEnabled(enabled: boolean) {
276
+ async function setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent> {
275
277
  setGalleryModeApplying(true)
276
278
  setGalleryModeError(null)
277
279
  try {
278
- await BluetoothSdk.setGalleryModeEnabled(enabled)
280
+ return await BluetoothSdk.setGalleryModeEnabled(enabled)
279
281
  } catch (error) {
280
282
  setGalleryModeError(error)
281
283
  options.onError?.(error)
package/android/.project DELETED
@@ -1,28 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <projectDescription>
3
- <name>mentra-bluetooth-sdk</name>
4
- <comment>Project mentra-bluetooth-sdk created by Buildship.</comment>
5
- <projects>
6
- </projects>
7
- <buildSpec>
8
- <buildCommand>
9
- <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10
- <arguments>
11
- </arguments>
12
- </buildCommand>
13
- </buildSpec>
14
- <natures>
15
- <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
- </natures>
17
- <filteredResources>
18
- <filter>
19
- <id>1779400133450</id>
20
- <name></name>
21
- <type>30</type>
22
- <matcher>
23
- <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
- <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
- </matcher>
26
- </filter>
27
- </filteredResources>
28
- </projectDescription>
@@ -1,28 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <projectDescription>
3
- <name>lc3Lib</name>
4
- <comment>Project lc3Lib created by Buildship.</comment>
5
- <projects>
6
- </projects>
7
- <buildSpec>
8
- <buildCommand>
9
- <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10
- <arguments>
11
- </arguments>
12
- </buildCommand>
13
- </buildSpec>
14
- <natures>
15
- <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
- </natures>
17
- <filteredResources>
18
- <filter>
19
- <id>1779400133449</id>
20
- <name></name>
21
- <type>30</type>
22
- <matcher>
23
- <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
- <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
- </matcher>
26
- </filter>
27
- </filteredResources>
28
- </projectDescription>