@mentra/bluetooth-sdk 0.1.19 → 0.1.21-beta.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.
- package/README.md +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +239 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +47 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +6 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +381 -97
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +293 -174
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
- package/build/BluetoothSdk.types.d.ts +71 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +27 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +20 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +1 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +2 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9 -0
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +72 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +72 -18
- package/ios/Source/DeviceManager.swift +52 -3
- package/ios/Source/DeviceStore.swift +6 -1
- package/ios/Source/MentraBluetoothSDK.swift +298 -21
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +22 -3
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +456 -52
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +5 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +9 -0
- package/ios/Source/tts/TTSTools.swift +1 -1
- package/ios/Source/utils/G1Text.swift +30 -1
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +75 -0
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +68 -13
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +1 -0
- package/src/_private/photoRequestPayload.ts +3 -3
- package/src/index.ts +10 -0
|
@@ -334,7 +334,7 @@ class MentraNex : SGCManager() {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// Network Management
|
|
337
|
-
override fun requestWifiScan () { Bridge.log("Nex: requestWifiScan operation not supported") }
|
|
337
|
+
override fun requestWifiScan (scanId: String?) { Bridge.log("Nex: requestWifiScan operation not supported") }
|
|
338
338
|
override fun sendWifiCredentials(ssid: String, password: String) { Bridge.log("Nex: sendWifiCredentials operation not supported") }
|
|
339
339
|
override fun forgetWifiNetwork(ssid: String) { }
|
|
340
340
|
override fun sendHotspotState(enabled: Boolean) { Bridge.log("Nex: sendHotspotState operation not supported") }
|
|
@@ -1427,7 +1427,7 @@ class Nimo : SGCManager() {
|
|
|
1427
1427
|
|
|
1428
1428
|
// ---------- SGCManager: Network (no WiFi) ----------
|
|
1429
1429
|
|
|
1430
|
-
override fun requestWifiScan() {}
|
|
1430
|
+
override fun requestWifiScan(scanId: String?) {}
|
|
1431
1431
|
override fun sendWifiCredentials(ssid: String, password: String) {}
|
|
1432
1432
|
override fun forgetWifiNetwork(ssid: String) {}
|
|
1433
1433
|
override fun sendHotspotState(enabled: Boolean) {}
|
|
@@ -46,6 +46,8 @@ abstract class SGCManager {
|
|
|
46
46
|
|
|
47
47
|
// Audio Control
|
|
48
48
|
abstract fun setMicEnabled(enabled: Boolean)
|
|
49
|
+
open val isMicSuspendedForAudio: Boolean
|
|
50
|
+
get() = false
|
|
49
51
|
abstract fun sortMicRanking(list: MutableList<String>): MutableList<String>
|
|
50
52
|
|
|
51
53
|
// Camera & Media
|
|
@@ -296,12 +298,12 @@ abstract class SGCManager {
|
|
|
296
298
|
abstract fun dbg2()
|
|
297
299
|
|
|
298
300
|
// Network Management
|
|
299
|
-
abstract fun requestWifiScan()
|
|
301
|
+
abstract fun requestWifiScan(scanId: String?)
|
|
300
302
|
abstract fun sendWifiCredentials(ssid: String, password: String)
|
|
301
303
|
abstract fun forgetWifiNetwork(ssid: String)
|
|
302
304
|
abstract fun sendHotspotState(enabled: Boolean)
|
|
303
305
|
|
|
304
|
-
/** Set glasses system clock (Mentra Live
|
|
306
|
+
/** Set glasses system clock (Mentra Live and G2; no-op on other devices). */
|
|
305
307
|
open fun sendSetSystemTime(timestampMs: Long) {
|
|
306
308
|
Bridge.log("SGC: sendSetSystemTime not supported on $type")
|
|
307
309
|
}
|
|
@@ -66,15 +66,31 @@ sealed interface WifiStatus {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
data class WifiStatusEvent(
|
|
69
|
+
data class WifiStatusEvent @JvmOverloads constructor(
|
|
70
70
|
val status: WifiStatus,
|
|
71
|
+
/**
|
|
72
|
+
* Glasses-reported provisioning failure reason when THIS event is the verdict of a
|
|
73
|
+
* failed connect attempt; null for routine link-state updates. An attempt property,
|
|
74
|
+
* not a link property — which is why it lives on the event and not on [WifiStatus]:
|
|
75
|
+
* "connect_timeout" arrives with a disconnected status (never associated), while
|
|
76
|
+
* "connected_to_other_network" arrives with a *connected* status — the attempt
|
|
77
|
+
* failed and Android's auto-join left the glasses on (or returned them to) a
|
|
78
|
+
* different SSID than requested, so the link is genuinely up while the request
|
|
79
|
+
* genuinely failed. Sent by ASG client builds that include the WiFi error
|
|
80
|
+
* surfacing (v40+); older glasses never set it.
|
|
81
|
+
*/
|
|
82
|
+
val error: String? = null,
|
|
71
83
|
) {
|
|
72
|
-
internal constructor(values: Map<String, Any>) : this(
|
|
84
|
+
internal constructor(values: Map<String, Any>) : this(
|
|
85
|
+
WifiStatus.fromMap(values) ?: WifiStatus.Disconnected,
|
|
86
|
+
stringValue(values, "error")?.takeIf { it.isNotEmpty() },
|
|
87
|
+
)
|
|
73
88
|
internal constructor(connected: Boolean, ssid: String?, localIp: String?) : this(
|
|
74
89
|
WifiStatus.fromStoreFields(connected, ssid, localIp) ?: WifiStatus.Disconnected
|
|
75
90
|
)
|
|
76
91
|
|
|
77
|
-
val values: Map<String, Any>
|
|
92
|
+
val values: Map<String, Any>
|
|
93
|
+
get() = status.toEventMap() + (error?.let { mapOf("error" to it) } ?: emptyMap())
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
sealed interface HotspotStatus {
|
|
@@ -436,12 +436,25 @@ sealed interface StreamStatus {
|
|
|
436
436
|
data class StreamStatusEvent(
|
|
437
437
|
val status: StreamStatus,
|
|
438
438
|
) {
|
|
439
|
-
|
|
439
|
+
// True when the glasses will retry the failed publisher themselves
|
|
440
|
+
// (emitting side lands in PR #3488); absent on older firmware and on
|
|
441
|
+
// events not parsed from a glasses status map. Carried here instead of
|
|
442
|
+
// on StreamStatus.Error so the public Error shape stays unchanged.
|
|
443
|
+
var willRetry: Boolean? = null
|
|
444
|
+
private set
|
|
445
|
+
|
|
446
|
+
constructor(values: Map<String, Any>) : this(StreamStatus.fromMap(values)) {
|
|
447
|
+
willRetry = boolValue(values, "willRetry")
|
|
448
|
+
}
|
|
440
449
|
|
|
441
450
|
val state: StreamState get() = status.state
|
|
442
451
|
val streamId: String? get() = status.streamId
|
|
443
452
|
val resolvedConfig: StreamResolvedConfig? get() = status.resolvedConfig
|
|
444
|
-
val values: Map<String, Any>
|
|
453
|
+
val values: Map<String, Any>
|
|
454
|
+
get() = buildMap {
|
|
455
|
+
putAll(status.toEventMap())
|
|
456
|
+
willRetry?.let { put("willRetry", it) }
|
|
457
|
+
}
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
data class KeepAliveAckEvent(
|
|
@@ -9,6 +9,9 @@ import com.k2fsa.sherpa.onnx.OfflineTtsModelConfig
|
|
|
9
9
|
import com.k2fsa.sherpa.onnx.OfflineTtsSupertonicModelConfig
|
|
10
10
|
import com.mentra.bluetoothsdk.Bridge
|
|
11
11
|
import java.io.File
|
|
12
|
+
import java.util.concurrent.Executors
|
|
13
|
+
import java.util.concurrent.ScheduledFuture
|
|
14
|
+
import java.util.concurrent.TimeUnit
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Utilities for offline Sherpa-ONNX Supertonic 3 TTS model management.
|
|
@@ -31,6 +34,21 @@ object TTSTools {
|
|
|
31
34
|
private const val FILE_UNICODE_INDEXER = "unicode_indexer.bin"
|
|
32
35
|
private const val FILE_VOICE_STYLE = "voice.bin"
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Keep the native model warm briefly after use to reduce repeated TTS latency while
|
|
39
|
+
* still reclaiming its memory when speech has been idle for a while.
|
|
40
|
+
*/
|
|
41
|
+
private const val MODEL_IDLE_TIMEOUT_MINUTES = 3L
|
|
42
|
+
private val modelLock = Any()
|
|
43
|
+
private val teardownExecutor =
|
|
44
|
+
Executors.newSingleThreadScheduledExecutor { runnable ->
|
|
45
|
+
Thread(runnable, "MentraTtsIdleTeardown").apply { isDaemon = true }
|
|
46
|
+
}
|
|
47
|
+
private var cachedTts: OfflineTts? = null
|
|
48
|
+
private var cachedModelPath: String? = null
|
|
49
|
+
private var teardownFuture: ScheduledFuture<*>? = null
|
|
50
|
+
private var teardownGeneration = 0L
|
|
51
|
+
|
|
34
52
|
private val REQUIRED_FILES = listOf(
|
|
35
53
|
FILE_DURATION_PREDICTOR,
|
|
36
54
|
FILE_TEXT_ENCODER,
|
|
@@ -42,6 +60,11 @@ object TTSTools {
|
|
|
42
60
|
)
|
|
43
61
|
|
|
44
62
|
fun setTtsModelDetails(context: Context, path: String, languageCode: String) {
|
|
63
|
+
synchronized(modelLock) {
|
|
64
|
+
if (cachedModelPath != null && cachedModelPath != path) {
|
|
65
|
+
releaseCachedModelLocked("model path changed")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
45
68
|
val prefs = getPrefs(context)
|
|
46
69
|
prefs.edit().apply {
|
|
47
70
|
putString(KEY_TTS_MODEL_PATH, path)
|
|
@@ -118,59 +141,104 @@ object TTSTools {
|
|
|
118
141
|
return false
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
val modelDir = File(modelPath)
|
|
122
144
|
val outputFile = File(outputPath)
|
|
123
145
|
outputFile.parentFile?.mkdirs()
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
return try {
|
|
127
|
-
val supertonic = OfflineTtsSupertonicModelConfig(
|
|
128
|
-
durationPredictor = File(modelDir, FILE_DURATION_PREDICTOR).absolutePath,
|
|
129
|
-
textEncoder = File(modelDir, FILE_TEXT_ENCODER).absolutePath,
|
|
130
|
-
vectorEstimator = File(modelDir, FILE_VECTOR_ESTIMATOR).absolutePath,
|
|
131
|
-
vocoder = File(modelDir, FILE_VOCODER).absolutePath,
|
|
132
|
-
ttsJson = File(modelDir, FILE_TTS_JSON).absolutePath,
|
|
133
|
-
unicodeIndexer = File(modelDir, FILE_UNICODE_INDEXER).absolutePath,
|
|
134
|
-
voiceStyle = File(modelDir, FILE_VOICE_STYLE).absolutePath,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
val modelConfig = OfflineTtsModelConfig(
|
|
138
|
-
supertonic = supertonic,
|
|
139
|
-
numThreads = 2,
|
|
140
|
-
provider = "cpu",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
val config = OfflineTtsConfig(
|
|
144
|
-
model = modelConfig,
|
|
145
|
-
maxNumSentences = 1,
|
|
146
|
-
silenceScale = 0.2f,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
tts = OfflineTts(null, config)
|
|
150
|
-
|
|
151
|
-
// sid out of range falls back to F1 (sid 0); clamp 0..9.
|
|
152
|
-
val sid = speakerId.coerceIn(0, tts.numSpeakers() - 1)
|
|
153
|
-
val clampedSpeed = speed.coerceIn(0.5f, 2.0f)
|
|
154
|
-
val lang = languageTagToSupertonicLang(languageTag)
|
|
155
|
-
val genConfig = GenerationConfig(
|
|
156
|
-
sid = sid,
|
|
157
|
-
speed = clampedSpeed,
|
|
158
|
-
extra = mapOf("lang" to lang),
|
|
159
|
-
)
|
|
160
|
-
val audio = tts.generateWithConfig(text = text, config = genConfig)
|
|
161
|
-
val saved = audio.save(outputFile.absolutePath)
|
|
162
|
-
Bridge.log("TTS generated ${outputFile.absolutePath}: saved=$saved sid=$sid lang=$lang")
|
|
163
|
-
saved
|
|
164
|
-
} catch (e: Exception) {
|
|
165
|
-
Bridge.log("TTS_ERROR: ${e.javaClass.simpleName}: ${e.message}")
|
|
166
|
-
e.printStackTrace()
|
|
167
|
-
false
|
|
168
|
-
} finally {
|
|
147
|
+
return synchronized(modelLock) {
|
|
169
148
|
try {
|
|
170
|
-
tts
|
|
149
|
+
val tts = getOrCreateModelLocked(modelPath)
|
|
150
|
+
|
|
151
|
+
// sid out of range falls back to F1 (sid 0); clamp 0..9.
|
|
152
|
+
val sid = speakerId.coerceIn(0, tts.numSpeakers() - 1)
|
|
153
|
+
val clampedSpeed = speed.coerceIn(0.5f, 2.0f)
|
|
154
|
+
val lang = languageTagToSupertonicLang(languageTag)
|
|
155
|
+
val genConfig = GenerationConfig(
|
|
156
|
+
sid = sid,
|
|
157
|
+
speed = clampedSpeed,
|
|
158
|
+
numSteps = 5,
|
|
159
|
+
extra = mapOf("lang" to lang),
|
|
160
|
+
)
|
|
161
|
+
val audio = tts.generateWithConfig(text = text, config = genConfig)
|
|
162
|
+
val saved = audio.save(outputFile.absolutePath)
|
|
163
|
+
Bridge.log("TTS generated ${outputFile.absolutePath}: saved=$saved sid=$sid lang=$lang")
|
|
164
|
+
saved
|
|
171
165
|
} catch (e: Exception) {
|
|
172
|
-
Bridge.log("
|
|
166
|
+
Bridge.log("TTS_ERROR: ${e.javaClass.simpleName}: ${e.message}")
|
|
167
|
+
e.printStackTrace()
|
|
168
|
+
false
|
|
169
|
+
} finally {
|
|
170
|
+
scheduleIdleTeardownLocked()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun getOrCreateModelLocked(modelPath: String): OfflineTts {
|
|
176
|
+
cachedTts?.let { cached ->
|
|
177
|
+
if (cachedModelPath == modelPath) {
|
|
178
|
+
Bridge.log("TTS reusing cached model")
|
|
179
|
+
return cached
|
|
173
180
|
}
|
|
181
|
+
releaseCachedModelLocked("model path changed")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
val modelDir = File(modelPath)
|
|
185
|
+
val supertonic = OfflineTtsSupertonicModelConfig(
|
|
186
|
+
durationPredictor = File(modelDir, FILE_DURATION_PREDICTOR).absolutePath,
|
|
187
|
+
textEncoder = File(modelDir, FILE_TEXT_ENCODER).absolutePath,
|
|
188
|
+
vectorEstimator = File(modelDir, FILE_VECTOR_ESTIMATOR).absolutePath,
|
|
189
|
+
vocoder = File(modelDir, FILE_VOCODER).absolutePath,
|
|
190
|
+
ttsJson = File(modelDir, FILE_TTS_JSON).absolutePath,
|
|
191
|
+
unicodeIndexer = File(modelDir, FILE_UNICODE_INDEXER).absolutePath,
|
|
192
|
+
voiceStyle = File(modelDir, FILE_VOICE_STYLE).absolutePath,
|
|
193
|
+
)
|
|
194
|
+
val modelConfig = OfflineTtsModelConfig(
|
|
195
|
+
supertonic = supertonic,
|
|
196
|
+
numThreads = 8,
|
|
197
|
+
provider = "cpu",
|
|
198
|
+
)
|
|
199
|
+
val config = OfflineTtsConfig(
|
|
200
|
+
model = modelConfig,
|
|
201
|
+
maxNumSentences = 1,
|
|
202
|
+
silenceScale = 0.2f,
|
|
203
|
+
)
|
|
204
|
+
return OfflineTts(null, config).also {
|
|
205
|
+
cachedTts = it
|
|
206
|
+
cachedModelPath = modelPath
|
|
207
|
+
Bridge.log("TTS loaded model; keeping it warm for $MODEL_IDLE_TIMEOUT_MINUTES minutes after use")
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun scheduleIdleTeardownLocked() {
|
|
212
|
+
if (cachedTts == null) return
|
|
213
|
+
teardownFuture?.cancel(false)
|
|
214
|
+
val generation = ++teardownGeneration
|
|
215
|
+
teardownFuture =
|
|
216
|
+
teardownExecutor.schedule(
|
|
217
|
+
{
|
|
218
|
+
synchronized(modelLock) {
|
|
219
|
+
if (generation == teardownGeneration) {
|
|
220
|
+
releaseCachedModelLocked("idle for $MODEL_IDLE_TIMEOUT_MINUTES minutes")
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
MODEL_IDLE_TIMEOUT_MINUTES,
|
|
225
|
+
TimeUnit.MINUTES,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private fun releaseCachedModelLocked(reason: String) {
|
|
230
|
+
teardownFuture?.cancel(false)
|
|
231
|
+
teardownFuture = null
|
|
232
|
+
teardownGeneration++
|
|
233
|
+
val tts = cachedTts
|
|
234
|
+
cachedTts = null
|
|
235
|
+
cachedModelPath = null
|
|
236
|
+
if (tts == null) return
|
|
237
|
+
try {
|
|
238
|
+
tts.release()
|
|
239
|
+
Bridge.log("TTS released cached model: $reason")
|
|
240
|
+
} catch (e: Exception) {
|
|
241
|
+
Bridge.log("TTS release failed: ${e.message}")
|
|
174
242
|
}
|
|
175
243
|
}
|
|
176
244
|
|
|
@@ -2,7 +2,6 @@ package com.mentra.bluetoothsdk
|
|
|
2
2
|
|
|
3
3
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
4
4
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
5
|
-
import expo.modules.kotlin.exception.CodedException
|
|
6
5
|
|
|
7
6
|
data class MentraBluetoothSdkConfig @JvmOverloads constructor(
|
|
8
7
|
val deliverCallbacksOnMainThread: Boolean = true,
|
|
@@ -10,10 +9,10 @@ data class MentraBluetoothSdkConfig @JvmOverloads constructor(
|
|
|
10
9
|
)
|
|
11
10
|
|
|
12
11
|
class BluetoothSdkException(
|
|
13
|
-
code: String,
|
|
12
|
+
val code: String,
|
|
14
13
|
message: String,
|
|
15
14
|
cause: Throwable? = null,
|
|
16
|
-
) :
|
|
15
|
+
) : IllegalStateException(message, cause)
|
|
17
16
|
|
|
18
17
|
enum class DeviceModel(val deviceType: String) {
|
|
19
18
|
G1(DeviceTypes.G1),
|
|
@@ -37,10 +37,10 @@ import org.json.JSONObject;
|
|
|
37
37
|
*/
|
|
38
38
|
public class BlePhotoUploadService {
|
|
39
39
|
private static final String TAG = "BlePhotoUploadService";
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
private static final int
|
|
40
|
+
// AVIF sources already went through a lossy pass on the glasses; re-encode at
|
|
41
|
+
// high-but-not-max quality to avoid inflating upload size. JPEG fast-path
|
|
42
|
+
// payloads from text mode are uploaded as-is.
|
|
43
|
+
private static final int AVIF_TO_JPEG_QUALITY = 90;
|
|
44
44
|
|
|
45
45
|
public interface UploadCallback {
|
|
46
46
|
void onSuccess(String requestId, String responseBody);
|
|
@@ -84,6 +84,18 @@ public class BlePhotoUploadService {
|
|
|
84
84
|
*/
|
|
85
85
|
@VisibleForTesting
|
|
86
86
|
static byte[] convertToJpegPreservingExif(byte[] imageData) throws Exception {
|
|
87
|
+
long conversionStartMs = System.currentTimeMillis();
|
|
88
|
+
if (isJpeg(imageData)) {
|
|
89
|
+
Log.d(
|
|
90
|
+
TAG,
|
|
91
|
+
"BLE relay pass-through: input already JPEG ("
|
|
92
|
+
+ imageData.length
|
|
93
|
+
+ " bytes), skipping decode/re-encode in "
|
|
94
|
+
+ (System.currentTimeMillis() - conversionStartMs)
|
|
95
|
+
+ "ms");
|
|
96
|
+
return imageData;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
File inputFile = File.createTempFile("ble_photo_in_", guessExtension(imageData));
|
|
88
100
|
File outputFile = File.createTempFile("ble_photo_out_", ".jpg");
|
|
89
101
|
try {
|
|
@@ -95,19 +107,39 @@ public class BlePhotoUploadService {
|
|
|
95
107
|
|
|
96
108
|
String imuJson = readImuJsonFromBleImage(imageData, inputFile.getAbsolutePath());
|
|
97
109
|
|
|
110
|
+
long decodeStartMs = System.currentTimeMillis();
|
|
98
111
|
Bitmap bitmap = decodeImage(imageData);
|
|
112
|
+
long decodeDurationMs = System.currentTimeMillis() - decodeStartMs;
|
|
99
113
|
if (bitmap == null) {
|
|
100
114
|
throw new Exception("Failed to decode image data");
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
Log.d(
|
|
104
|
-
|
|
117
|
+
Log.d(
|
|
118
|
+
TAG,
|
|
119
|
+
"AVIF decode complete: "
|
|
120
|
+
+ bitmap.getWidth()
|
|
121
|
+
+ "x"
|
|
122
|
+
+ bitmap.getHeight()
|
|
123
|
+
+ " in "
|
|
124
|
+
+ decodeDurationMs
|
|
125
|
+
+ "ms");
|
|
126
|
+
|
|
127
|
+
long encodeStartMs = System.currentTimeMillis();
|
|
105
128
|
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
|
106
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG,
|
|
129
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, AVIF_TO_JPEG_QUALITY, fos);
|
|
107
130
|
} finally {
|
|
108
131
|
bitmap.recycle();
|
|
109
132
|
}
|
|
133
|
+
long encodeDurationMs = System.currentTimeMillis() - encodeStartMs;
|
|
134
|
+
Log.d(
|
|
135
|
+
TAG,
|
|
136
|
+
"AVIF->JPEG encode complete: quality="
|
|
137
|
+
+ AVIF_TO_JPEG_QUALITY
|
|
138
|
+
+ " in "
|
|
139
|
+
+ encodeDurationMs
|
|
140
|
+
+ "ms");
|
|
110
141
|
|
|
142
|
+
long metadataStartMs = System.currentTimeMillis();
|
|
111
143
|
if (imuJson != null && !imuJson.isEmpty()) {
|
|
112
144
|
logImuData(imuJson);
|
|
113
145
|
// IMU EXIF is enrichment, not the payload: a write failure must not drop the
|
|
@@ -131,8 +163,27 @@ public class BlePhotoUploadService {
|
|
|
131
163
|
+ "). If rawHasExifMarker=true, EXIF may be present but unreadable via"
|
|
132
164
|
+ " ExifInterface on this container.");
|
|
133
165
|
}
|
|
166
|
+
Log.d(
|
|
167
|
+
TAG,
|
|
168
|
+
"AVIF metadata handling complete in "
|
|
169
|
+
+ (System.currentTimeMillis() - metadataStartMs)
|
|
170
|
+
+ "ms");
|
|
134
171
|
|
|
135
|
-
|
|
172
|
+
byte[] jpegData = java.nio.file.Files.readAllBytes(outputFile.toPath());
|
|
173
|
+
Log.d(
|
|
174
|
+
TAG,
|
|
175
|
+
"Phone image conversion complete: AVIF "
|
|
176
|
+
+ imageData.length
|
|
177
|
+
+ " bytes -> JPEG "
|
|
178
|
+
+ jpegData.length
|
|
179
|
+
+ " bytes in "
|
|
180
|
+
+ (System.currentTimeMillis() - conversionStartMs)
|
|
181
|
+
+ "ms (decode="
|
|
182
|
+
+ decodeDurationMs
|
|
183
|
+
+ "ms, encode="
|
|
184
|
+
+ encodeDurationMs
|
|
185
|
+
+ "ms)");
|
|
186
|
+
return jpegData;
|
|
136
187
|
} finally {
|
|
137
188
|
if (!inputFile.delete()) {
|
|
138
189
|
inputFile.deleteOnExit();
|
|
@@ -254,6 +305,12 @@ public class BlePhotoUploadService {
|
|
|
254
305
|
exif.saveAttributes();
|
|
255
306
|
}
|
|
256
307
|
|
|
308
|
+
private static boolean isJpeg(byte[] imageData) {
|
|
309
|
+
return imageData.length >= 2
|
|
310
|
+
&& (imageData[0] & 0xFF) == 0xFF
|
|
311
|
+
&& (imageData[1] & 0xFF) == 0xD8;
|
|
312
|
+
}
|
|
313
|
+
|
|
257
314
|
private static String guessExtension(byte[] imageData) {
|
|
258
315
|
if (imageData.length > 12
|
|
259
316
|
&& imageData[4] == 'f'
|
|
@@ -30,7 +30,9 @@ public class K900ProtocolUtils {
|
|
|
30
30
|
public static final byte CMD_TYPE_BINARY_MSG = BleWireProtocol.CMD_TYPE_BINARY_MSG;
|
|
31
31
|
|
|
32
32
|
// File transfer constants
|
|
33
|
-
|
|
33
|
+
// Negotiated file protocol ceiling. Legacy/GATT transfers remain smaller; 800-byte frames are
|
|
34
|
+
// accepted only after file_payload_v2 negotiation and an open CoC channel.
|
|
35
|
+
public static final int FILE_PACK_SIZE = 800;
|
|
34
36
|
public static final int LENGTH_FILE_START = 2;
|
|
35
37
|
public static final int LENGTH_FILE_TYPE = 1;
|
|
36
38
|
public static final int LENGTH_FILE_PACKSIZE = 2;
|
|
@@ -274,9 +276,6 @@ public class K900ProtocolUtils {
|
|
|
274
276
|
return false;
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
Log.d("K900ProtocolUtils", "isK900ProtocolFormat: " + data[0] + " " + data[1]);
|
|
278
|
-
Log.d("K900ProtocolUtils", "CMD_START_CODE: " + CMD_START_CODE[0] + " " + CMD_START_CODE[1]);
|
|
279
|
-
|
|
280
279
|
return data[0] == CMD_START_CODE[0] &&
|
|
281
280
|
data[1] == CMD_START_CODE[1];
|
|
282
281
|
}
|
|
@@ -792,7 +791,9 @@ public class K900ProtocolUtils {
|
|
|
792
791
|
Log.e("K900ProtocolUtils", "File packet checksum failed. Expected: " +
|
|
793
792
|
String.format("%02X", info.verifyCode) + ", Calculated: " +
|
|
794
793
|
String.format("%02X", calculatedVerify));
|
|
795
|
-
} else
|
|
794
|
+
} else if (info.packIndex == 0
|
|
795
|
+
|| info.packIndex % 32 == 0
|
|
796
|
+
|| info.packIndex == (info.fileSize + FILE_PACK_SIZE - 1) / FILE_PACK_SIZE - 1) {
|
|
796
797
|
Log.d("K900ProtocolUtils", "File packet extracted successfully: index=" + info.packIndex +
|
|
797
798
|
", size=" + info.packSize + ", fileName=" + info.fileName);
|
|
798
799
|
}
|