@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Read-only LE L2CAP CoC fast path for Mentra Live file transfers.
|
|
5
|
+
///
|
|
6
|
+
/// BES sends the same K900 file frames over this channel that it otherwise
|
|
7
|
+
/// sends through the FILE_READ GATT characteristic. The stream is drained on
|
|
8
|
+
/// a dedicated thread so CoreBluetooth credits are returned independently of
|
|
9
|
+
/// React Native and main-thread work.
|
|
10
|
+
final class MentraLiveL2capChannel: NSObject, StreamDelegate {
|
|
11
|
+
private static let readChunkSize = 4096
|
|
12
|
+
private static let maxBufferSize = 64 * 1024
|
|
13
|
+
private static let frameHeaderSize = 5
|
|
14
|
+
private static let frameOverhead = 32
|
|
15
|
+
private static let maxFilePayloadSize = K900ProtocolUtils.FILE_PACK_SIZE
|
|
16
|
+
|
|
17
|
+
private let channel: CBL2CAPChannel
|
|
18
|
+
private let onFileFrame: (Data) -> Void
|
|
19
|
+
private let onClose: () -> Void
|
|
20
|
+
private let stateLock = NSLock()
|
|
21
|
+
|
|
22
|
+
private var receiveBuffer = [UInt8]()
|
|
23
|
+
private var worker: Thread?
|
|
24
|
+
private var closed = false
|
|
25
|
+
private var closeNotified = false
|
|
26
|
+
|
|
27
|
+
init(
|
|
28
|
+
channel: CBL2CAPChannel,
|
|
29
|
+
onFileFrame: @escaping (Data) -> Void,
|
|
30
|
+
onClose: @escaping () -> Void
|
|
31
|
+
) {
|
|
32
|
+
self.channel = channel
|
|
33
|
+
self.onFileFrame = onFileFrame
|
|
34
|
+
self.onClose = onClose
|
|
35
|
+
super.init()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func start() {
|
|
39
|
+
guard worker == nil else { return }
|
|
40
|
+
let thread = Thread { [weak self] in
|
|
41
|
+
self?.runReadLoop()
|
|
42
|
+
}
|
|
43
|
+
thread.name = "MentraLive-L2CAP"
|
|
44
|
+
thread.qualityOfService = .userInitiated
|
|
45
|
+
worker = thread
|
|
46
|
+
thread.start()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func close() {
|
|
50
|
+
stateLock.lock()
|
|
51
|
+
let wasClosed = closed
|
|
52
|
+
closed = true
|
|
53
|
+
stateLock.unlock()
|
|
54
|
+
|
|
55
|
+
guard !wasClosed else { return }
|
|
56
|
+
channel.inputStream?.close()
|
|
57
|
+
channel.outputStream?.close()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private var isClosed: Bool {
|
|
61
|
+
stateLock.lock()
|
|
62
|
+
defer { stateLock.unlock() }
|
|
63
|
+
return closed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private func runReadLoop() {
|
|
67
|
+
guard let input = channel.inputStream, let output = channel.outputStream else {
|
|
68
|
+
notifyClosed()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
input.delegate = self
|
|
73
|
+
input.schedule(in: .current, forMode: .default)
|
|
74
|
+
output.schedule(in: .current, forMode: .default)
|
|
75
|
+
input.open()
|
|
76
|
+
output.open()
|
|
77
|
+
|
|
78
|
+
while !isClosed {
|
|
79
|
+
_ = RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
input.close()
|
|
83
|
+
output.close()
|
|
84
|
+
input.remove(from: .current, forMode: .default)
|
|
85
|
+
output.remove(from: .current, forMode: .default)
|
|
86
|
+
input.delegate = nil
|
|
87
|
+
notifyClosed()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
|
91
|
+
guard aStream === channel.inputStream else { return }
|
|
92
|
+
|
|
93
|
+
switch eventCode {
|
|
94
|
+
case .hasBytesAvailable:
|
|
95
|
+
drainInput()
|
|
96
|
+
case .endEncountered, .errorOccurred:
|
|
97
|
+
close()
|
|
98
|
+
default:
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func drainInput() {
|
|
104
|
+
guard let input = channel.inputStream else { return }
|
|
105
|
+
var chunk = [UInt8](repeating: 0, count: Self.readChunkSize)
|
|
106
|
+
|
|
107
|
+
while input.hasBytesAvailable, !isClosed {
|
|
108
|
+
let count = input.read(&chunk, maxLength: chunk.count)
|
|
109
|
+
if count > 0 {
|
|
110
|
+
appendAndDispatch(Array(chunk.prefix(count)))
|
|
111
|
+
} else if count < 0 {
|
|
112
|
+
close()
|
|
113
|
+
return
|
|
114
|
+
} else {
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func appendAndDispatch(_ bytes: [UInt8]) {
|
|
121
|
+
if receiveBuffer.count + bytes.count > Self.maxBufferSize {
|
|
122
|
+
receiveBuffer.removeAll(keepingCapacity: true)
|
|
123
|
+
}
|
|
124
|
+
guard bytes.count <= Self.maxBufferSize else { return }
|
|
125
|
+
receiveBuffer.append(contentsOf: bytes)
|
|
126
|
+
|
|
127
|
+
while true {
|
|
128
|
+
guard let start = startMarkerIndex() else {
|
|
129
|
+
if receiveBuffer.last == 0x23 {
|
|
130
|
+
receiveBuffer = [0x23]
|
|
131
|
+
} else {
|
|
132
|
+
receiveBuffer.removeAll(keepingCapacity: true)
|
|
133
|
+
}
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if start > 0 {
|
|
138
|
+
receiveBuffer.removeFirst(start)
|
|
139
|
+
}
|
|
140
|
+
guard receiveBuffer.count >= Self.frameHeaderSize else { return }
|
|
141
|
+
|
|
142
|
+
let payloadSize = (Int(receiveBuffer[3]) << 8) | Int(receiveBuffer[4])
|
|
143
|
+
guard payloadSize <= Self.maxFilePayloadSize else {
|
|
144
|
+
receiveBuffer.removeFirst()
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let frameSize = Self.frameOverhead + payloadSize
|
|
149
|
+
guard receiveBuffer.count >= frameSize else { return }
|
|
150
|
+
|
|
151
|
+
guard receiveBuffer[frameSize - 2] == 0x24,
|
|
152
|
+
receiveBuffer[frameSize - 1] == 0x24
|
|
153
|
+
else {
|
|
154
|
+
receiveBuffer.removeFirst()
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onFileFrame(Data(receiveBuffer.prefix(frameSize)))
|
|
159
|
+
receiveBuffer.removeFirst(frameSize)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func startMarkerIndex() -> Int? {
|
|
164
|
+
guard receiveBuffer.count >= 2 else { return nil }
|
|
165
|
+
for index in 0 ..< (receiveBuffer.count - 1)
|
|
166
|
+
where receiveBuffer[index] == 0x23 && receiveBuffer[index + 1] == 0x23
|
|
167
|
+
{
|
|
168
|
+
return index
|
|
169
|
+
}
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func notifyClosed() {
|
|
174
|
+
stateLock.lock()
|
|
175
|
+
guard !closeNotified else {
|
|
176
|
+
stateLock.unlock()
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
closeNotified = true
|
|
180
|
+
stateLock.unlock()
|
|
181
|
+
onClose()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -376,7 +376,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
376
376
|
func dbg1() {}
|
|
377
377
|
func dbg2() {}
|
|
378
378
|
|
|
379
|
-
func requestWifiScan() {}
|
|
379
|
+
func requestWifiScan(scanId _: String?) {}
|
|
380
380
|
|
|
381
381
|
func sendWifiCredentials(_: String, _: String) {}
|
|
382
382
|
|
|
@@ -1066,7 +1066,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1066
1066
|
|
|
1067
1067
|
// MARK: - SGCManager: Network (no WiFi)
|
|
1068
1068
|
|
|
1069
|
-
func requestWifiScan() {}
|
|
1069
|
+
func requestWifiScan(scanId _: String?) {}
|
|
1070
1070
|
func sendWifiCredentials(_: String, _: String) {}
|
|
1071
1071
|
func forgetWifiNetwork(_: String) {}
|
|
1072
1072
|
func sendHotspotState(_: Bool) {}
|
|
@@ -54,6 +54,7 @@ protocol SGCManager {
|
|
|
54
54
|
// MARK: - Audio Control
|
|
55
55
|
|
|
56
56
|
func setMicEnabled(_ enabled: Bool)
|
|
57
|
+
var isMicSuspendedForAudio: Bool { get }
|
|
57
58
|
func sortMicRanking(list: [String]) -> [String]
|
|
58
59
|
|
|
59
60
|
// MARK: - Messaging
|
|
@@ -184,7 +185,7 @@ protocol SGCManager {
|
|
|
184
185
|
|
|
185
186
|
// MARK: - Network Management
|
|
186
187
|
|
|
187
|
-
func requestWifiScan()
|
|
188
|
+
func requestWifiScan(scanId: String?)
|
|
188
189
|
func sendWifiCredentials(_ ssid: String, _ password: String)
|
|
189
190
|
func forgetWifiNetwork(_ ssid: String)
|
|
190
191
|
func sendHotspotState(_ enabled: Bool)
|
|
@@ -217,6 +218,8 @@ protocol SGCManager {
|
|
|
217
218
|
/// doesn't seem to work for concurrency reasons :(
|
|
218
219
|
/// we can make read-only getters for convienence though:
|
|
219
220
|
extension SGCManager {
|
|
221
|
+
var isMicSuspendedForAudio: Bool { false }
|
|
222
|
+
|
|
220
223
|
/// Default: no-op. Only G2 renders positioned text containers; other glasses ignore it.
|
|
221
224
|
func sendPositionedText(
|
|
222
225
|
_: String, x _: Int32, y _: Int32, width _: Int32, height _: Int32,
|
|
@@ -362,7 +365,7 @@ extension SGCManager {
|
|
|
362
365
|
|
|
363
366
|
func sendVoiceActivityDetectionSetting() {}
|
|
364
367
|
|
|
365
|
-
/// Default no-op; Mentra Live
|
|
368
|
+
/// Default no-op; Mentra Live and G2 override to handle phone-detected clock skew.
|
|
366
369
|
func sendSetSystemTime(_: Int64) {
|
|
367
370
|
Bridge.log("SGC: sendSetSystemTime not supported")
|
|
368
371
|
}
|
|
@@ -133,20 +133,39 @@ public enum WifiStatus: CustomStringConvertible, Equatable {
|
|
|
133
133
|
public struct WifiStatusEvent: CustomStringConvertible {
|
|
134
134
|
public let status: WifiStatus
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
/// Glasses-reported provisioning failure reason when THIS event is the verdict of a
|
|
137
|
+
/// failed connect attempt; nil for routine link-state updates. An attempt property,
|
|
138
|
+
/// not a link property — which is why it lives on the event and not on `WifiStatus`:
|
|
139
|
+
/// "connect_timeout" arrives with a disconnected status (never associated), while
|
|
140
|
+
/// "connected_to_other_network" arrives with a *connected* status — the attempt
|
|
141
|
+
/// failed and Android's auto-join left the glasses on (or returned them to) a
|
|
142
|
+
/// different SSID than requested, so the link is genuinely up while the request
|
|
143
|
+
/// genuinely failed. Sent by ASG client builds that include the WiFi error
|
|
144
|
+
/// surfacing (v40+); older glasses never set it.
|
|
145
|
+
public let error: String?
|
|
146
|
+
|
|
147
|
+
public init(status: WifiStatus, error: String? = nil) {
|
|
137
148
|
self.status = status
|
|
149
|
+
self.error = error
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
init(connected: Bool, ssid: String?, localIp: String?) {
|
|
141
153
|
status = WifiStatus.fromStoreFields(connected: connected, ssid: ssid, localIp: localIp) ?? .disconnected
|
|
154
|
+
error = nil
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
init(values: [String: Any]) {
|
|
145
158
|
status = WifiStatus(values: values) ?? .disconnected
|
|
159
|
+
let rawError = stringValue(values, "error")
|
|
160
|
+
error = rawError?.isEmpty == false ? rawError : nil
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
public var values: [String: Any] {
|
|
149
|
-
status.values.merging(["type": "wifi_status_change"]) { _, new in new }
|
|
164
|
+
var body = status.values.merging(["type": "wifi_status_change"]) { _, new in new }
|
|
165
|
+
if let error {
|
|
166
|
+
body["error"] = error
|
|
167
|
+
}
|
|
168
|
+
return body
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
public var description: String {
|
|
@@ -553,6 +553,11 @@ public enum StreamStatus: CustomStringConvertible, Equatable {
|
|
|
553
553
|
|
|
554
554
|
public struct StreamStatusEvent: CustomStringConvertible {
|
|
555
555
|
public let status: StreamStatus
|
|
556
|
+
// True when the glasses will retry the failed publisher themselves
|
|
557
|
+
// (emitting side lands in PR #3488); absent on older firmware and on
|
|
558
|
+
// events not parsed from a glasses status map. Carried here instead of
|
|
559
|
+
// as an `.error` associated value so the public case stays unchanged.
|
|
560
|
+
public private(set) var willRetry: Bool?
|
|
556
561
|
|
|
557
562
|
public init(status: StreamStatus) {
|
|
558
563
|
self.status = status
|
|
@@ -560,6 +565,7 @@ public struct StreamStatusEvent: CustomStringConvertible {
|
|
|
560
565
|
|
|
561
566
|
public init(values: [String: Any]) {
|
|
562
567
|
status = StreamStatus(values: values)
|
|
568
|
+
willRetry = boolValue(values, "willRetry")
|
|
563
569
|
}
|
|
564
570
|
|
|
565
571
|
public var state: StreamState {
|
|
@@ -577,6 +583,9 @@ public struct StreamStatusEvent: CustomStringConvertible {
|
|
|
577
583
|
public var values: [String: Any] {
|
|
578
584
|
var values = status.values
|
|
579
585
|
values["type"] = "stream_status"
|
|
586
|
+
if let willRetry {
|
|
587
|
+
values["willRetry"] = willRetry
|
|
588
|
+
}
|
|
580
589
|
return values
|
|
581
590
|
}
|
|
582
591
|
|
|
@@ -111,7 +111,7 @@ class TTSTools {
|
|
|
111
111
|
voiceStyle: (modelPath as NSString).appendingPathComponent(kFileVoiceStyle)
|
|
112
112
|
)
|
|
113
113
|
var modelConfig = sherpaOnnxOfflineTtsModelConfig(
|
|
114
|
-
numThreads:
|
|
114
|
+
numThreads: 8,
|
|
115
115
|
supertonic: supertonic
|
|
116
116
|
)
|
|
117
117
|
var config = sherpaOnnxOfflineTtsConfig(model: modelConfig, maxNumSentences: 1)
|
|
@@ -24,6 +24,33 @@ class G1Text {
|
|
|
24
24
|
|
|
25
25
|
init() {}
|
|
26
26
|
|
|
27
|
+
/// Converts text to the base Latin glyphs available in G1 firmware.
|
|
28
|
+
/// Newer glasses bypass this G1-only transport and keep the original text.
|
|
29
|
+
static func sanitizeForDisplay(_ text: String) -> String {
|
|
30
|
+
let expanded = text.reduce(into: "") { result, character in
|
|
31
|
+
switch character {
|
|
32
|
+
case "Đ", "Ð": result.append("D")
|
|
33
|
+
case "đ", "ð": result.append("d")
|
|
34
|
+
case "Ł": result.append("L")
|
|
35
|
+
case "ł": result.append("l")
|
|
36
|
+
case "Ø": result.append("O")
|
|
37
|
+
case "ø": result.append("o")
|
|
38
|
+
case "Æ": result.append("AE")
|
|
39
|
+
case "æ": result.append("ae")
|
|
40
|
+
case "Œ": result.append("OE")
|
|
41
|
+
case "œ": result.append("oe")
|
|
42
|
+
case "ẞ": result.append("SS")
|
|
43
|
+
case "ß": result.append("ss")
|
|
44
|
+
case "Þ": result.append("TH")
|
|
45
|
+
case "þ": result.append("th")
|
|
46
|
+
default: result.append(character)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return expanded
|
|
51
|
+
.folding(options: .diacriticInsensitive, locale: Locale(identifier: "en_US_POSIX"))
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
// MARK: - Text Wall Methods
|
|
28
55
|
|
|
29
56
|
// func displayTextWall(_ text: String) {
|
|
@@ -72,7 +99,9 @@ class G1Text {
|
|
|
72
99
|
func chunkTextForTransmission(_ text: String) -> [[UInt8]] {
|
|
73
100
|
// Handle empty or whitespace-only text by sending at least a space
|
|
74
101
|
// This ensures the display gets updated/cleared properly
|
|
75
|
-
let textToSend = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
102
|
+
let textToSend = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
103
|
+
? " "
|
|
104
|
+
: G1Text.sanitizeForDisplay(text)
|
|
76
105
|
guard let textData = textToSend.data(using: .utf8) else { return [] }
|
|
77
106
|
let textBytes = [UInt8](textData)
|
|
78
107
|
let totalChunks = Int(ceil(Double(textBytes.count) / Double(G1Text.MAX_CHUNK_SIZE)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21-beta.0",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -105,7 +105,6 @@
|
|
|
105
105
|
"access": "public",
|
|
106
106
|
"registry": "https://registry.npmjs.org/"
|
|
107
107
|
},
|
|
108
|
-
"dependencies": {},
|
|
109
108
|
"devDependencies": {
|
|
110
109
|
"@types/node": "^25.9.3",
|
|
111
110
|
"expo-module-scripts": "^55.0.2",
|
|
@@ -116,6 +116,16 @@ export function isConnectedWifiStatus(status: WifiStatus): status is ConnectedWi
|
|
|
116
116
|
|
|
117
117
|
export type WifiStatusChangeEvent = WifiStatus & {
|
|
118
118
|
type: "wifi_status_change"
|
|
119
|
+
/**
|
|
120
|
+
* Glasses-reported provisioning failure reason when THIS event is the verdict of a
|
|
121
|
+
* failed connect attempt; absent on routine link-state updates. An attempt property,
|
|
122
|
+
* not a link property — which is why it lives on the event, not on WifiStatus:
|
|
123
|
+
* "connect_timeout" arrives on a disconnected status (never associated), while
|
|
124
|
+
* "connected_to_other_network" arrives on a *connected* status (the attempt failed
|
|
125
|
+
* and the glasses ended up on / fell back to a different SSID than requested).
|
|
126
|
+
* Requires ASG client v40+ — older glasses never send it.
|
|
127
|
+
*/
|
|
128
|
+
error?: string
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
export type HotspotStatus = {state: "disabled"} | {state: "enabled"; ssid: string; password: string; localIp: string}
|
|
@@ -405,6 +415,7 @@ export type SettingsAckSetting =
|
|
|
405
415
|
| "button_video_recording"
|
|
406
416
|
| "button_max_recording_time"
|
|
407
417
|
| "camera_fov"
|
|
418
|
+
| "camera_fov_override"
|
|
408
419
|
| "camera_tuning"
|
|
409
420
|
|
|
410
421
|
export type SettingsAckEvent = {
|
|
@@ -416,6 +427,7 @@ export type SettingsAckEvent = {
|
|
|
416
427
|
fov?: number
|
|
417
428
|
roiPosition?: CameraRoiPositionValue
|
|
418
429
|
hardwareApplied?: boolean
|
|
430
|
+
leaseId?: string
|
|
419
431
|
active?: boolean
|
|
420
432
|
size?: ButtonPhotoSize | string
|
|
421
433
|
width?: number
|
|
@@ -440,8 +452,14 @@ export type SettingsAckSuccessEvent = Omit<SettingsAckEvent, "status"> & {
|
|
|
440
452
|
export type RgbLedAction = "on" | "off"
|
|
441
453
|
export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
|
|
442
454
|
export type PhotoSize = "low" | "medium" | "high" | "max"
|
|
455
|
+
export type PhotoMode = "photo" | "text"
|
|
443
456
|
export type ButtonPhotoSize = "low" | "medium" | "high" | "max"
|
|
444
457
|
|
|
458
|
+
/**
|
|
459
|
+
* @deprecated Sticky action-button photo presets via {@link BluetoothSdkPublicModule.setPhotoCaptureDefaults}
|
|
460
|
+
* are deprecated. Prefer per-request {@link BluetoothSdkPublicModule.requestPhoto} options
|
|
461
|
+
* (e.g. `mode: "text"` for AE ÷3) instead of persisting button-photo tuning on the glasses.
|
|
462
|
+
*/
|
|
445
463
|
export type PhotoCaptureDefaults = {
|
|
446
464
|
size?: PhotoSize
|
|
447
465
|
mfnr?: boolean
|
|
@@ -527,6 +545,13 @@ export type CameraFovResult = {
|
|
|
527
545
|
timestamp: number
|
|
528
546
|
}
|
|
529
547
|
|
|
548
|
+
export type CameraFovOverrideRequest = CameraFovRequest & {
|
|
549
|
+
/** Phone-owned lease used to make delayed releases safe. */
|
|
550
|
+
leaseId: string
|
|
551
|
+
/** Safety TTL; refresh the same lease/configuration to extend without a HAL restart. */
|
|
552
|
+
ttlMs?: number
|
|
553
|
+
}
|
|
554
|
+
|
|
530
555
|
export type CameraFovSetting = {
|
|
531
556
|
fov: number
|
|
532
557
|
roiPosition: CameraRoiPositionValue
|
|
@@ -544,6 +569,9 @@ export type PhotoRequestParams = {
|
|
|
544
569
|
requestId?: string
|
|
545
570
|
appId?: string
|
|
546
571
|
size: PhotoSize
|
|
572
|
+
mode?: PhotoMode
|
|
573
|
+
/** `ble` skips direct upload and forces phone-relayed BLE image transfer. */
|
|
574
|
+
transferMethod?: "auto" | "ble"
|
|
547
575
|
webhookUrl: string | null
|
|
548
576
|
authToken: string | null
|
|
549
577
|
compress: PhotoCompression
|
|
@@ -566,9 +594,12 @@ export type PhotoRequestParams = {
|
|
|
566
594
|
}
|
|
567
595
|
|
|
568
596
|
export type WarmUpCameraParams = {
|
|
597
|
+
/** Supply this when the owner needs to call stopCameraWarmUp during teardown. */
|
|
569
598
|
requestId?: string
|
|
570
599
|
size: PhotoSize
|
|
600
|
+
mode?: PhotoMode
|
|
571
601
|
exposureTimeNs?: number | null
|
|
602
|
+
/** Ready-state hold; defaults to 15 seconds and is capped at 60 seconds by ASG. */
|
|
572
603
|
durationMs?: number
|
|
573
604
|
}
|
|
574
605
|
|
|
@@ -690,11 +721,25 @@ export type StreamResolvedConfig = {
|
|
|
690
721
|
}
|
|
691
722
|
}
|
|
692
723
|
|
|
724
|
+
/** Live encoder telemetry, when the glasses emit it (no firmware does yet). */
|
|
725
|
+
export type StreamLiveStats = {
|
|
726
|
+
/** Current encoded video bitrate in bits per second. */
|
|
727
|
+
bitrate?: number
|
|
728
|
+
/** Current encode frame rate. */
|
|
729
|
+
fps?: number
|
|
730
|
+
droppedFrames?: number
|
|
731
|
+
/** Seconds since the stream started. */
|
|
732
|
+
duration?: number
|
|
733
|
+
/** Device temperature in °C, if the hardware reports it. */
|
|
734
|
+
temperatureC?: number
|
|
735
|
+
}
|
|
736
|
+
|
|
693
737
|
type StreamStatusCommon = {
|
|
694
738
|
type: "stream_status"
|
|
695
739
|
streamId?: string
|
|
696
740
|
timestamp?: number
|
|
697
741
|
resolvedConfig?: StreamResolvedConfig
|
|
742
|
+
stats?: StreamLiveStats
|
|
698
743
|
}
|
|
699
744
|
|
|
700
745
|
export type StreamStatusEvent =
|
|
@@ -817,6 +862,8 @@ export type BluetoothSdkModuleEvents = {
|
|
|
817
862
|
speaking_status: (event: SpeakingStatusEvent) => void
|
|
818
863
|
battery_status: (event: BatteryStatusEvent) => void
|
|
819
864
|
local_transcription: (event: LocalTranscriptionEvent) => void
|
|
865
|
+
phone_notification: (event: PhoneNotificationEvent) => void
|
|
866
|
+
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
|
|
820
867
|
wifi_status_change: (event: WifiStatusChangeEvent) => void
|
|
821
868
|
wifi_scan_result: (event: WifiScanResultEvent) => void
|
|
822
869
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void
|
|
@@ -863,6 +910,23 @@ export interface ExtractionProgressEvent {
|
|
|
863
910
|
totalBytes: number
|
|
864
911
|
}
|
|
865
912
|
|
|
913
|
+
export interface PhoneNotificationEvent {
|
|
914
|
+
notificationId: string
|
|
915
|
+
app: string
|
|
916
|
+
title: string
|
|
917
|
+
content: string
|
|
918
|
+
priority: string
|
|
919
|
+
timestamp: number
|
|
920
|
+
packageName: string
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export interface PhoneNotificationDismissedEvent {
|
|
924
|
+
notificationId: string
|
|
925
|
+
notificationKey: string
|
|
926
|
+
packageName: string
|
|
927
|
+
timestamp: number
|
|
928
|
+
}
|
|
929
|
+
|
|
866
930
|
export type PublicGlassesStatus = Omit<
|
|
867
931
|
GlassesStatus,
|
|
868
932
|
"otaUpdateAvailable" | "otaProgress" | "otaInProgress" | "otaVersionUrl"
|
|
@@ -972,10 +1036,19 @@ export interface BluetoothSdkPublicModule {
|
|
|
972
1036
|
|
|
973
1037
|
setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
|
|
974
1038
|
setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
|
|
1039
|
+
/**
|
|
1040
|
+
* @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
|
|
1041
|
+
* `requestPhoto(...)` options (e.g. `mode: "text"` for AE ÷3, or explicit per-shot
|
|
1042
|
+
* fields). Still functional until removed in a future release.
|
|
1043
|
+
*/
|
|
975
1044
|
setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): Promise<SettingsAckSuccessEvent>
|
|
976
1045
|
setVideoRecordingDefaults(settings: VideoRecordingDefaults): Promise<SettingsAckSuccessEvent>
|
|
977
1046
|
setMaxVideoRecordingDuration(minutes: number): Promise<SettingsAckSuccessEvent>
|
|
978
1047
|
setCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
|
|
1048
|
+
/** One-way FOV command for legacy ASG clients that do not send settings acknowledgements. */
|
|
1049
|
+
setLegacyCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
|
|
1050
|
+
setCameraFovOverride(request: CameraFovOverrideRequest): Promise<CameraFovResult>
|
|
1051
|
+
releaseCameraFovOverride(leaseId: string): Promise<SettingsAckSuccessEvent>
|
|
979
1052
|
/**
|
|
980
1053
|
* Configure camera HAL tuning (ANR / gain) on Mentra Live glasses.
|
|
981
1054
|
*
|
|
@@ -993,6 +1066,8 @@ export interface BluetoothSdkPublicModule {
|
|
|
993
1066
|
queryGalleryStatus(): Promise<GalleryStatusEvent>
|
|
994
1067
|
requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
|
|
995
1068
|
warmUpCamera(params: WarmUpCameraParams): Promise<CameraStatusEvent>
|
|
1069
|
+
/** Release one request-owned warm-up. Opening requests reject with camera_warm_up_cancelled. */
|
|
1070
|
+
stopCameraWarmUp(requestId: string): Promise<void>
|
|
996
1071
|
startVideoRecording(
|
|
997
1072
|
requestId: string,
|
|
998
1073
|
save: boolean,
|
package/src/_internal.ts
CHANGED
|
@@ -9,3 +9,5 @@
|
|
|
9
9
|
export {default} from "./_private/BluetoothSdkModule"
|
|
10
10
|
export type {BluetoothSdkInternalModule} from "./_private/BluetoothSdkModule"
|
|
11
11
|
export * from "./BluetoothSdk.types"
|
|
12
|
+
export {default as MentraLocalNetwork} from "./_private/MentraLocalNetworkModule"
|
|
13
|
+
export * from "./_private/MentraLocalNetworkModule"
|