@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -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 +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +16 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +272 -197
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +25 -1
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -29,6 +29,10 @@ internal class ClassicAudioConnectionTracker(
|
|
|
29
29
|
val connected: Boolean
|
|
30
30
|
get() = connectedProfiles.isNotEmpty()
|
|
31
31
|
|
|
32
|
+
/** BLE may already be gone when the user explicitly disconnects Classic audio. */
|
|
33
|
+
@Synchronized
|
|
34
|
+
fun targetForDisconnect(): String? = targetAddress
|
|
35
|
+
|
|
32
36
|
@Synchronized
|
|
33
37
|
fun setTarget(address: String) {
|
|
34
38
|
val normalizedAddress = address.normalizedBluetoothAddress()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothDevice
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.ParcelUuid
|
|
6
|
+
|
|
7
|
+
/** One bounded SDP discovery; a BLE bond alone does not populate Android's Classic UUID cache. */
|
|
8
|
+
internal class ClassicAudioServiceDiscovery(
|
|
9
|
+
private val handler: Handler,
|
|
10
|
+
private val timeoutMs: Long,
|
|
11
|
+
private val log: (String) -> Unit,
|
|
12
|
+
) {
|
|
13
|
+
private class Pending(val device: BluetoothDevice, val ready: () -> Unit) {
|
|
14
|
+
lateinit var timeout: Runnable
|
|
15
|
+
}
|
|
16
|
+
private var pending: Pending? = null
|
|
17
|
+
|
|
18
|
+
fun connectWhenDiscovered(device: BluetoothDevice, ready: () -> Unit) {
|
|
19
|
+
if (hasAudioSink(device)) {
|
|
20
|
+
cancel()
|
|
21
|
+
ready()
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (pending?.device?.address == device.address) return
|
|
25
|
+
cancel()
|
|
26
|
+
val request = Pending(device, ready)
|
|
27
|
+
request.timeout = Runnable {
|
|
28
|
+
if (pending === request) {
|
|
29
|
+
cancel()
|
|
30
|
+
log("SDP timed out for ${device.address}; Classic audio remains disconnected")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
pending = request
|
|
34
|
+
handler.postDelayed(request.timeout, timeoutMs)
|
|
35
|
+
try {
|
|
36
|
+
log("Refreshing Classic service UUIDs for ${device.address}")
|
|
37
|
+
if (!device.fetchUuidsWithSdp()) {
|
|
38
|
+
cancel()
|
|
39
|
+
log("SDP request rejected for ${device.address}; Classic audio remains disconnected")
|
|
40
|
+
}
|
|
41
|
+
} catch (error: Exception) {
|
|
42
|
+
cancel()
|
|
43
|
+
log("SDP request failed for ${device.address}: ${error.message}")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** ACTION_UUID is a completion hint. Read the current OS cache, not a stale intent payload. */
|
|
48
|
+
fun onUuids(device: BluetoothDevice) {
|
|
49
|
+
val request = pending ?: return
|
|
50
|
+
if (device.address != request.device.address) return
|
|
51
|
+
if (hasAudioSink(request.device)) {
|
|
52
|
+
cancel()
|
|
53
|
+
log("Discovered A2DP Sink UUID for ${device.address}")
|
|
54
|
+
request.ready()
|
|
55
|
+
} else {
|
|
56
|
+
// Bonding can announce cached UUIDs before SDP finishes. Keep the original deadline.
|
|
57
|
+
log("UUID event without A2DP Sink for ${device.address}; waiting for SDP within the existing deadline")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fun cancel() {
|
|
62
|
+
pending?.let { handler.removeCallbacks(it.timeout) }
|
|
63
|
+
pending = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
companion object {
|
|
67
|
+
private val AUDIO_SINK = ParcelUuid.fromString("0000110b-0000-1000-8000-00805f9b34fb")
|
|
68
|
+
fun hasAudioSink(device: BluetoothDevice): Boolean = device.uuids?.contains(AUDIO_SINK) == true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1370,8 +1370,8 @@ class G1 : SGCManager() {
|
|
|
1370
1370
|
context!!.registerReceiver(bondingReceiver, filter)
|
|
1371
1371
|
isBondingReceiverRegistered = true
|
|
1372
1372
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1373
|
+
// Keep the target supplied by connectById. device_name still belongs to
|
|
1374
|
+
// the previous pairing until DeviceManager promotes the ready device.
|
|
1375
1375
|
if (!bluetoothAdapter!!.isEnabled()) {
|
|
1376
1376
|
return
|
|
1377
1377
|
}
|
|
@@ -2,37 +2,16 @@ package com.mentra.bluetoothsdk.sgcs
|
|
|
2
2
|
|
|
3
3
|
import java.text.Normalizer
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Embedded G1 1.5.6 Latin glyphs. See notes/g1-character-support.md.
|
|
6
|
+
private const val G1_LATIN_GLYPHS = "ÀÂÇÈÉÊËÎÏÔÙÛÜàçèéêëîïôùûüÿŸÄäÖößẞâÁáÍíÑñúÓóÚⁱIJijŠšŽžÃÅÆÌÐÒÕØÝÞãåæìðòõøýþĄąĆćČčĎďĘęĚěĞğŁłŃńŇňŘřŚśŞşŤťŮůŹźŻżĂăıȘșȚțǖǘǚǜǎǐǒǔŐőŰű"
|
|
7
|
+
private val G1_LATIN_CLUSTERS = Regex("\\p{IsLatin}\\p{M}*")
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
text.forEach { character ->
|
|
16
|
-
when (character) {
|
|
17
|
-
'Đ', 'Ð' -> append('D')
|
|
18
|
-
'đ', 'ð' -> append('d')
|
|
19
|
-
'Ł' -> append('L')
|
|
20
|
-
'ł' -> append('l')
|
|
21
|
-
'Ø' -> append('O')
|
|
22
|
-
'ø' -> append('o')
|
|
23
|
-
'Æ' -> append("AE")
|
|
24
|
-
'æ' -> append("ae")
|
|
25
|
-
'Œ' -> append("OE")
|
|
26
|
-
'œ' -> append("oe")
|
|
27
|
-
'ẞ' -> append("SS")
|
|
28
|
-
'ß' -> append("ss")
|
|
29
|
-
'Þ' -> append("TH")
|
|
30
|
-
'þ' -> append("th")
|
|
31
|
-
else -> append(character)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return Normalizer.normalize(expanded, Normalizer.Form.NFD)
|
|
37
|
-
.replace(G1_COMBINING_MARKS, "")
|
|
9
|
+
/** Preserves mapped Latin glyphs and falls back only for unmapped Latin clusters. */
|
|
10
|
+
internal fun sanitizeG1DisplayText(text: String): String = G1_LATIN_CLUSTERS.replace(text) { match ->
|
|
11
|
+
val composed = Normalizer.normalize(match.value, Normalizer.Form.NFC)
|
|
12
|
+
if (composed.length == 1 && (composed[0] in ' '..'~' || composed[0] in G1_LATIN_GLYPHS)) {
|
|
13
|
+
composed
|
|
14
|
+
} else {
|
|
15
|
+
sanitizeLatinDisplayText(match.value, preserveOtherScripts = true)
|
|
16
|
+
}
|
|
38
17
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk.sgcs
|
|
2
2
|
|
|
3
|
+
import com.mentra.bluetoothsdk.utils.G2Text
|
|
3
4
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
4
5
|
import android.bluetooth.BluetoothAdapter
|
|
5
6
|
import android.bluetooth.BluetoothGatt
|
|
@@ -1363,17 +1364,18 @@ class G2 : SGCManager() {
|
|
|
1363
1364
|
// Map device names to serial numbers (populated from manufacturer data during scan)
|
|
1364
1365
|
private val deviceNameToSerialNumber = mutableMapOf<String, String>()
|
|
1365
1366
|
|
|
1366
|
-
//
|
|
1367
|
+
// Scope each side's address to the selected serial. Legacy global addresses have
|
|
1368
|
+
// no trustworthy owner, so the first connection after upgrading learns them by scan.
|
|
1367
1369
|
private var leftGlassAddress: String?
|
|
1368
1370
|
get() =
|
|
1369
1371
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1370
|
-
.getString(KEY_LEFT_ADDRESS, null)
|
|
1372
|
+
.getString("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID", null)
|
|
1371
1373
|
set(value) {
|
|
1372
1374
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1373
1375
|
.edit()
|
|
1374
1376
|
.apply {
|
|
1375
|
-
if (value != null) putString(KEY_LEFT_ADDRESS, value)
|
|
1376
|
-
else remove(KEY_LEFT_ADDRESS)
|
|
1377
|
+
if (value != null) putString("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID", value)
|
|
1378
|
+
else remove("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID")
|
|
1377
1379
|
}
|
|
1378
1380
|
.apply()
|
|
1379
1381
|
}
|
|
@@ -1381,13 +1383,13 @@ class G2 : SGCManager() {
|
|
|
1381
1383
|
private var rightGlassAddress: String?
|
|
1382
1384
|
get() =
|
|
1383
1385
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1384
|
-
.getString(KEY_RIGHT_ADDRESS, null)
|
|
1386
|
+
.getString("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID", null)
|
|
1385
1387
|
set(value) {
|
|
1386
1388
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1387
1389
|
.edit()
|
|
1388
1390
|
.apply {
|
|
1389
|
-
if (value != null) putString(KEY_RIGHT_ADDRESS, value)
|
|
1390
|
-
else remove(KEY_RIGHT_ADDRESS)
|
|
1391
|
+
if (value != null) putString("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID", value)
|
|
1392
|
+
else remove("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID")
|
|
1391
1393
|
}
|
|
1392
1394
|
.apply()
|
|
1393
1395
|
}
|
|
@@ -1664,8 +1666,15 @@ class G2 : SGCManager() {
|
|
|
1664
1666
|
if (char == null || gatt == null) return
|
|
1665
1667
|
char.value = packet
|
|
1666
1668
|
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
+
// A rejected write never entered Android's queue. Retry on this dedicated
|
|
1670
|
+
// BLE worker before later commands (especially a page replacement) overtake it.
|
|
1671
|
+
var ok = false
|
|
1672
|
+
for (attempt in 0 until 5) {
|
|
1673
|
+
ok = gatt.writeCharacteristic(char)
|
|
1674
|
+
bgcapNoteWriteResult(ok)
|
|
1675
|
+
if (ok) break
|
|
1676
|
+
if (attempt < 4) Thread.sleep(BLE_PACKET_GAP_MS)
|
|
1677
|
+
}
|
|
1669
1678
|
if (leg != null) {
|
|
1670
1679
|
Bridge.log("G2/FILE: write $leg ${packet.size}B -> ${if (ok) "queued" else "DROPPED (stack busy)"}")
|
|
1671
1680
|
}
|
|
@@ -2190,7 +2199,7 @@ class G2 : SGCManager() {
|
|
|
2190
2199
|
val rBorderColor = borderColor ?: defaultTextBorderColor
|
|
2191
2200
|
val rBorderRadius = borderRadius ?: defaultTextBorderRadius
|
|
2192
2201
|
val rPaddingLength = paddingLength ?: defaultTextPaddingLength
|
|
2193
|
-
val content =
|
|
2202
|
+
val content = G2Text.containerContent(text)
|
|
2194
2203
|
|
|
2195
2204
|
// Pure state mutation: update the container's content and schedule its sends; the reconcile
|
|
2196
2205
|
// loop does the actual updateText writes. Reuse an existing container if the rect matches
|
|
@@ -2257,6 +2266,12 @@ class G2 : SGCManager() {
|
|
|
2257
2266
|
}
|
|
2258
2267
|
|
|
2259
2268
|
override fun clearDisplay() {
|
|
2269
|
+
// Expo invokes this on its worker queue; all container mutations must
|
|
2270
|
+
// share Main with reconcileDisplay and scene updates.
|
|
2271
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2272
|
+
mainHandler.post { clearDisplay() }
|
|
2273
|
+
return
|
|
2274
|
+
}
|
|
2260
2275
|
Bridge.log("G2: clearDisplay()")
|
|
2261
2276
|
// Clear the text in place — do NOT shut down + rebuild the EvenHub page. Tearing the page
|
|
2262
2277
|
// down (rebuildPage) kills audio streaming AND triggers a firmware systemExit /
|
|
@@ -2266,9 +2281,22 @@ class G2 : SGCManager() {
|
|
|
2266
2281
|
// several seconds (incident 8164175a). Just blank the text + clear images; the reconcile
|
|
2267
2282
|
// loop pushes the blanked text on a live page, and a dead page is only resurrected for
|
|
2268
2283
|
// meaningful (non-blank) content, so a clear can't churn it back up.
|
|
2269
|
-
for (
|
|
2270
|
-
|
|
2271
|
-
|
|
2284
|
+
for (container in textContainers) {
|
|
2285
|
+
// Erase the firmware's existing text BEFORE forgetting its container.
|
|
2286
|
+
// A queued reconcile cannot do this after the positioned container is removed.
|
|
2287
|
+
// Cover the previous byte range, rather than replacing only its first character.
|
|
2288
|
+
val blank = " ".repeat(maxOf(1, container.content.toByteArray(Charsets.UTF_8).size))
|
|
2289
|
+
container.content = blank
|
|
2290
|
+
container.pendingSends = 1 + EVEN_HUB_RESEND_COUNT
|
|
2291
|
+
if (pageCreated) {
|
|
2292
|
+
val erase = EvenHubProto.updateTextMessage(
|
|
2293
|
+
containerID = container.id,
|
|
2294
|
+
contentOffset = 0,
|
|
2295
|
+
contentLength = blank.length,
|
|
2296
|
+
content = blank
|
|
2297
|
+
)
|
|
2298
|
+
repeat(1 + EVEN_HUB_RESEND_COUNT) { sendEvenHubCommand(erase) }
|
|
2299
|
+
}
|
|
2272
2300
|
}
|
|
2273
2301
|
for (i in imageContainers.indices) {
|
|
2274
2302
|
// The firmware still shows this container's image; emptying bmpData locally never reaches
|
|
@@ -2296,8 +2324,13 @@ class G2 : SGCManager() {
|
|
|
2296
2324
|
textContainers.removeAll { it.id in huskIds }
|
|
2297
2325
|
sceneTextByElement.entries.removeAll { it.value in huskIds }
|
|
2298
2326
|
sceneImageByElement.clear()
|
|
2299
|
-
|
|
2300
|
-
|
|
2327
|
+
// Replace the live page now, while still on Main. A shutdown followed by
|
|
2328
|
+
// reconcile cannot clear this case: reconcile intentionally skips blank pages,
|
|
2329
|
+
// leaving the firmware's old positioned text visible until new speech arrives.
|
|
2330
|
+
// Queue the blank replacement before any following miniapp frame, without
|
|
2331
|
+
// tearing down the page (or its microphone session).
|
|
2332
|
+
Bridge.log("G2: clearDisplay() — replacing page after removing ${huskIds.size} positioned container(s)")
|
|
2333
|
+
if (pageCreated) createPageWithContainers()
|
|
2301
2334
|
}
|
|
2302
2335
|
}
|
|
2303
2336
|
|
|
@@ -2344,6 +2377,15 @@ class G2 : SGCManager() {
|
|
|
2344
2377
|
return false
|
|
2345
2378
|
}
|
|
2346
2379
|
|
|
2380
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2381
|
+
mainHandler.post { applyBitmap(rx, ry, rw, rh, bmpData) }
|
|
2382
|
+
return true
|
|
2383
|
+
}
|
|
2384
|
+
return applyBitmap(rx, ry, rw, rh, bmpData)
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Called only on Main, including the inline scene-frame path.
|
|
2388
|
+
private fun applyBitmap(rx: Int, ry: Int, rw: Int, rh: Int, bmpData: ByteArray): Boolean {
|
|
2347
2389
|
// Pure state mutation: update the target container's bytes and mark it dirty. The reconcile
|
|
2348
2390
|
// loop is the sole sender, so two displayBitmap calls can never overlap a sendImageData and
|
|
2349
2391
|
// clobber the single-slot image ACK — no lock needed. Reuse an existing container if the rect
|
|
@@ -2538,7 +2580,7 @@ class G2 : SGCManager() {
|
|
|
2538
2580
|
@Suppress("NAME_SHADOWING")
|
|
2539
2581
|
val height = minOf(maxOf(height, minTextContainerHeight), 288 - y)
|
|
2540
2582
|
run {
|
|
2541
|
-
val content =
|
|
2583
|
+
val content = G2Text.containerContent(text)
|
|
2542
2584
|
val existingId = sceneTextByElement[elementId]
|
|
2543
2585
|
if (existingId != null) {
|
|
2544
2586
|
val idx = textContainers.indexOfFirst { it.id == existingId }
|
|
@@ -2999,15 +3041,15 @@ class G2 : SGCManager() {
|
|
|
2999
3041
|
}
|
|
3000
3042
|
// Only settle the flag if it's still empty — a displayBitmap during the await would
|
|
3001
3043
|
// have set new bytes, so leave it dirty for the next pass to send the real image.
|
|
3002
|
-
val jj = imageContainers.indexOfFirst { it
|
|
3044
|
+
val jj = imageContainers.indexOfFirst { it === container }
|
|
3003
3045
|
if (jj >= 0 && imageContainers[jj].bmpData.isEmpty()) {
|
|
3004
3046
|
imageContainers[jj].dirty = false
|
|
3005
3047
|
}
|
|
3006
3048
|
continue
|
|
3007
3049
|
}
|
|
3008
3050
|
sendImageData(container.id, container.name, sentBytes)
|
|
3009
|
-
// Re-find by
|
|
3010
|
-
val j = imageContainers.indexOfFirst { it
|
|
3051
|
+
// Re-find by identity: eviction can reuse this ID during the await.
|
|
3052
|
+
val j = imageContainers.indexOfFirst { it === container }
|
|
3011
3053
|
if (j >= 0 && imageContainers[j].bmpData.contentEquals(sentBytes)) {
|
|
3012
3054
|
imageContainers[j].dirty = false
|
|
3013
3055
|
}
|
|
@@ -3677,8 +3719,17 @@ class G2 : SGCManager() {
|
|
|
3677
3719
|
rightAuthenticated = false
|
|
3678
3720
|
startupPageCreated = false
|
|
3679
3721
|
pageCreated = false
|
|
3680
|
-
|
|
3681
|
-
|
|
3722
|
+
// Keep GATT teardown synchronous for manager replacement, but never
|
|
3723
|
+
// remove containers underneath Main's reconcile/scene iteration.
|
|
3724
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
3725
|
+
imageContainers.clear()
|
|
3726
|
+
textContainers.clear()
|
|
3727
|
+
} else {
|
|
3728
|
+
mainHandler.post {
|
|
3729
|
+
imageContainers.clear()
|
|
3730
|
+
textContainers.clear()
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3682
3733
|
dashboardShowing = 0
|
|
3683
3734
|
dashboardOpening = false
|
|
3684
3735
|
heartbeatCounter = 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import java.text.Normalizer
|
|
4
|
+
|
|
5
|
+
private val LATIN_COMBINING_MARKS = Regex("\\p{M}+")
|
|
6
|
+
private val LATIN_LETTERS_WITH_MARKS = Regex("\\p{IsLatin}\\p{M}*")
|
|
7
|
+
|
|
8
|
+
/** Converts accented and extended Latin letters to base glyphs at the device boundary. */
|
|
9
|
+
internal fun sanitizeLatinDisplayText(text: String, preserveOtherScripts: Boolean = false): String {
|
|
10
|
+
val expanded = buildString(text.length) {
|
|
11
|
+
text.forEach { character ->
|
|
12
|
+
when (character) {
|
|
13
|
+
'Đ', 'Ð' -> append('D')
|
|
14
|
+
'đ', 'ð' -> append('d')
|
|
15
|
+
'Ł' -> append('L')
|
|
16
|
+
'ł' -> append('l')
|
|
17
|
+
'Ø' -> append('O')
|
|
18
|
+
'ø' -> append('o')
|
|
19
|
+
'Æ' -> append("AE")
|
|
20
|
+
'æ' -> append("ae")
|
|
21
|
+
'Œ' -> append("OE")
|
|
22
|
+
'œ' -> append("oe")
|
|
23
|
+
'ẞ' -> append("SS")
|
|
24
|
+
'ß' -> append("ss")
|
|
25
|
+
'Þ' -> append("TH")
|
|
26
|
+
'þ' -> append("th")
|
|
27
|
+
else -> append(character)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (preserveOtherScripts) {
|
|
33
|
+
return LATIN_LETTERS_WITH_MARKS.replace(expanded) { match ->
|
|
34
|
+
Normalizer.normalize(match.value, Normalizer.Form.NFD).replace(LATIN_COMBINING_MARKS, "")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Normalizer.normalize(expanded, Normalizer.Form.NFD)
|
|
38
|
+
.replace(LATIN_COMBINING_MARKS, "")
|
|
39
|
+
}
|