@mentra/bluetooth-sdk 3.2.1-dev.311 โ 3.2.1
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 +4 -4
- 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/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- 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 +245 -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/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/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- 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/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/MentraLivePairingTargetTest.kt +65 -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 +220 -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 +13 -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 +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- 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/ios/BluetoothSdkModule.swift +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -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 +201 -156
- 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 +269 -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 +24 -0
- 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/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -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/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -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 +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -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 +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Serializes launch/update/exit. A business ACK means acceptance, not visible pixels.
|
|
4
|
+
final class NimoCanvasSession {
|
|
5
|
+
enum Action: Equatable {
|
|
6
|
+
case send(key: Int, frame: Data, ticket: UInt64)
|
|
7
|
+
case reconnect(String)
|
|
8
|
+
case rejected(Int)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private struct Flight {
|
|
12
|
+
let key: Int
|
|
13
|
+
let frame: Data
|
|
14
|
+
let ticket: UInt64
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private var ready = false
|
|
18
|
+
private var transportReady = false
|
|
19
|
+
private var active = false
|
|
20
|
+
private var locked = false
|
|
21
|
+
private var launchBlocked = false
|
|
22
|
+
private var waitingReadiness = false
|
|
23
|
+
private var observedNotReady = false
|
|
24
|
+
private var readinessRetries = 0
|
|
25
|
+
private var probeRetries = 0
|
|
26
|
+
private var exitRequested = false
|
|
27
|
+
private var desired: Data?
|
|
28
|
+
private var accepted: Data?
|
|
29
|
+
private var rejected: Data?
|
|
30
|
+
private var scope: String?
|
|
31
|
+
private var forceRevision: UInt64 = 0
|
|
32
|
+
private var sentRevision: UInt64 = 0
|
|
33
|
+
private var ticket: UInt64 = 0
|
|
34
|
+
private var flight: Flight?
|
|
35
|
+
|
|
36
|
+
func offer(_ frame: Data, scope: String, force: Bool = false) -> [Action] {
|
|
37
|
+
if self.scope != scope || force { forceRevision &+= 1 }
|
|
38
|
+
self.scope = scope
|
|
39
|
+
desired = frame
|
|
40
|
+
exitRequested = false
|
|
41
|
+
return pump()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Enter a blank canvas unless a newer host scene survived the connection change.
|
|
45
|
+
func activate() -> [Action] {
|
|
46
|
+
if desired == nil { desired = Data([0, 0, 1]) }
|
|
47
|
+
exitRequested = false
|
|
48
|
+
return pump()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The firmware validates peer readiness on Launch. Only an explicit NOT_READY
|
|
52
|
+
/// permits a bounded retry; an ambiguous ACK still requires a new transport.
|
|
53
|
+
func retryNotReady() -> [Action] {
|
|
54
|
+
guard transportReady, waitingReadiness, desired != nil, probeRetries < 3 else { return [] }
|
|
55
|
+
probeRetries += 1
|
|
56
|
+
waitingReadiness = false
|
|
57
|
+
ready = true
|
|
58
|
+
return pump()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Only a fresh heartbeat samples both TWS and peer Companion readiness.
|
|
62
|
+
func readiness(_ value: Bool, confirmed: Bool = false) -> [Action] {
|
|
63
|
+
transportReady = value
|
|
64
|
+
if !value { readinessRetries = 0; probeRetries = 0 }
|
|
65
|
+
if !value, flight != nil { return resetLink("Readiness lost during canvas command") }
|
|
66
|
+
if waitingReadiness {
|
|
67
|
+
if !value { observedNotReady = true }
|
|
68
|
+
else if !observedNotReady {
|
|
69
|
+
if !confirmed || readinessRetries >= 3 { return [] }
|
|
70
|
+
readinessRetries += 1
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ready = value
|
|
74
|
+
if !value { active = false; accepted = nil }
|
|
75
|
+
if value { waitingReadiness = false }
|
|
76
|
+
return pump()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Keep only the latest desired scene across a new transport generation.
|
|
80
|
+
func disconnected() {
|
|
81
|
+
ready = false; transportReady = false; active = false; locked = false; launchBlocked = false
|
|
82
|
+
waitingReadiness = false; observedNotReady = false; readinessRetries = 0; probeRetries = 0
|
|
83
|
+
accepted = nil; rejected = nil; flight = nil
|
|
84
|
+
ticket &+= 1; forceRevision &+= 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func exit() -> [Action] {
|
|
88
|
+
desired = nil; scope = nil; accepted = nil; rejected = nil; exitRequested = true
|
|
89
|
+
return pump()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Stock UI reports do not relinquish host ownership. Replay after the current ACK.
|
|
93
|
+
func nativeApp(_ appId: Int, entered: Bool) -> [Action] {
|
|
94
|
+
if (appId == NimoCanvasCodec.appId && !entered) || (appId != NimoCanvasCodec.appId && entered) {
|
|
95
|
+
active = false
|
|
96
|
+
accepted = nil
|
|
97
|
+
return pump()
|
|
98
|
+
}
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func acceptsResponse(key: Int, payload: Data) -> Bool {
|
|
103
|
+
guard let current = flight, current.key == key, let status = payload.first else { return false }
|
|
104
|
+
let echo = key == 4 ? Data([0, 0xFD, 0, 0, 0]) : Data([0, 0xFD])
|
|
105
|
+
if status == 0 { return payload == echo }
|
|
106
|
+
return payload.count == 1 || (payload.count == echo.count && payload.dropFirst() == echo.dropFirst())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func response(key: Int, payload: Data) -> [Action] {
|
|
110
|
+
guard acceptsResponse(key: key, payload: payload), let current = flight, let first = payload.first else { return [] }
|
|
111
|
+
let status = Int(first)
|
|
112
|
+
flight = nil
|
|
113
|
+
if status != 0 {
|
|
114
|
+
switch status {
|
|
115
|
+
case 7:
|
|
116
|
+
ready = false; waitingReadiness = true; observedNotReady = false; active = false; accepted = nil
|
|
117
|
+
case 8:
|
|
118
|
+
locked = true; active = false; accepted = nil
|
|
119
|
+
case 2:
|
|
120
|
+
return [.rejected(status)] + resetLink("Canvas device timeout is ambiguous")
|
|
121
|
+
default:
|
|
122
|
+
if key == 4 { rejected = current.frame }
|
|
123
|
+
else {
|
|
124
|
+
if key == 1 { launchBlocked = true } else { desired = nil }
|
|
125
|
+
active = false; exitRequested = false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [.rejected(status)] + pump()
|
|
129
|
+
}
|
|
130
|
+
switch key {
|
|
131
|
+
case 1: active = true; accepted = nil
|
|
132
|
+
case 4: accepted = current.frame; rejected = nil; readinessRetries = 0; probeRetries = 0
|
|
133
|
+
case 3: active = false; accepted = nil; exitRequested = false
|
|
134
|
+
default: break
|
|
135
|
+
}
|
|
136
|
+
return pump()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func timeout(_ expectedTicket: UInt64) -> [Action] {
|
|
140
|
+
guard flight?.ticket == expectedTicket else { return [] }
|
|
141
|
+
return resetLink("Canvas ACK timeout; reconnect before reusing a command key")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private func resetLink(_ reason: String) -> [Action] {
|
|
145
|
+
ready = false; transportReady = false; active = false; accepted = nil; flight = nil
|
|
146
|
+
ticket &+= 1; forceRevision &+= 1
|
|
147
|
+
return [.reconnect(reason)]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func pump() -> [Action] {
|
|
151
|
+
guard ready, !locked, !launchBlocked, !waitingReadiness, flight == nil else { return [] }
|
|
152
|
+
let key: Int
|
|
153
|
+
if exitRequested, active { key = 3 }
|
|
154
|
+
else if exitRequested { exitRequested = false; return [] }
|
|
155
|
+
else if desired == nil { return [] }
|
|
156
|
+
else if !active { key = 1 }
|
|
157
|
+
else if desired == rejected { return [] }
|
|
158
|
+
else if desired == accepted, sentRevision == forceRevision { return [] }
|
|
159
|
+
else { key = 4 }
|
|
160
|
+
let frame = key == 4 ? desired! : Data()
|
|
161
|
+
if key == 4 { sentRevision = forceRevision }
|
|
162
|
+
ticket &+= 1
|
|
163
|
+
flight = Flight(key: key, frame: frame, ticket: ticket)
|
|
164
|
+
return [.send(key: key, frame: frame, ticket: ticket)]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Combines system-connected classic devices with incoming CoreBluetooth discoveries.
|
|
4
|
+
/// Kept independent of the radio so discovery can be exercised without hardware.
|
|
5
|
+
@MainActor
|
|
6
|
+
final class NimoDiscovery<Peripheral> {
|
|
7
|
+
private(set) var active = false
|
|
8
|
+
private let register: () -> Void
|
|
9
|
+
private let connected: () -> [Peripheral]
|
|
10
|
+
private let scan: () -> Void
|
|
11
|
+
private let stop: () -> Void
|
|
12
|
+
private let name: (Peripheral) -> String?
|
|
13
|
+
private let found: (Peripheral, String, Int?) -> Void
|
|
14
|
+
|
|
15
|
+
init(register: @escaping () -> Void, connected: @escaping () -> [Peripheral],
|
|
16
|
+
scan: @escaping () -> Void, stop: @escaping () -> Void,
|
|
17
|
+
name: @escaping (Peripheral) -> String?, found: @escaping (Peripheral, String, Int?) -> Void)
|
|
18
|
+
{
|
|
19
|
+
self.register = register
|
|
20
|
+
self.connected = connected
|
|
21
|
+
self.scan = scan
|
|
22
|
+
self.stop = stop
|
|
23
|
+
self.name = name
|
|
24
|
+
self.found = found
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func start() {
|
|
28
|
+
active = true
|
|
29
|
+
// Register first so pairing in Settings cannot race the connected snapshot.
|
|
30
|
+
register()
|
|
31
|
+
for peripheral in connected() {
|
|
32
|
+
receive(peripheral)
|
|
33
|
+
}
|
|
34
|
+
if active { scan() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func cancel() {
|
|
38
|
+
active = false
|
|
39
|
+
stop()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func receive(_ peripheral: Peripheral, advertisedName: String? = nil, rssi: Int? = nil) {
|
|
43
|
+
guard active, let name = advertisedName ?? name(peripheral) else { return }
|
|
44
|
+
let lower = name.lowercased()
|
|
45
|
+
// The separate ANCS peripheral does not host the Companion data service.
|
|
46
|
+
guard lower.hasPrefix("nimo"), !lower.hasSuffix("_ble") else { return }
|
|
47
|
+
found(peripheral, name, rssi)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -96,6 +96,10 @@ protocol SGCManager {
|
|
|
96
96
|
|
|
97
97
|
// MARK: - Display Control
|
|
98
98
|
|
|
99
|
+
/// Full-frame adapters preserve the active scene instead of showing temporary overlays.
|
|
100
|
+
var showBrightnessConfirmation: Bool { get }
|
|
101
|
+
var showConnectionConfirmation: Bool { get }
|
|
102
|
+
var sceneHandoffRequiresClear: Bool { get }
|
|
99
103
|
func setBrightness(_ level: Int, autoMode: Bool)
|
|
100
104
|
func clearDisplay()
|
|
101
105
|
func sendText(_ text: String) async
|
|
@@ -228,6 +232,10 @@ protocol SGCManager {
|
|
|
228
232
|
|
|
229
233
|
func sendLoudnessGateSetting()
|
|
230
234
|
|
|
235
|
+
// MARK: - Auto power-off when unworn
|
|
236
|
+
|
|
237
|
+
func sendAutoPowerOffSetting()
|
|
238
|
+
|
|
231
239
|
// MARK: - Mic tuning (super-mode only)
|
|
232
240
|
|
|
233
241
|
func sendMicTuningSetting()
|
|
@@ -254,6 +262,18 @@ protocol SGCManager {
|
|
|
254
262
|
/// doesn't seem to work for concurrency reasons :(
|
|
255
263
|
/// we can make read-only getters for convienence though:
|
|
256
264
|
extension SGCManager {
|
|
265
|
+
var showBrightnessConfirmation: Bool {
|
|
266
|
+
true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var showConnectionConfirmation: Bool {
|
|
270
|
+
true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var sceneHandoffRequiresClear: Bool {
|
|
274
|
+
true
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
var isMicSuspendedForAudio: Bool {
|
|
258
278
|
false
|
|
259
279
|
}
|
|
@@ -435,6 +455,10 @@ extension SGCManager {
|
|
|
435
455
|
|
|
436
456
|
func sendLoudnessGateSetting() {}
|
|
437
457
|
|
|
458
|
+
// MARK: - Auto power-off (default no-op โ Mentra Live supports this)
|
|
459
|
+
|
|
460
|
+
func sendAutoPowerOffSetting() {}
|
|
461
|
+
|
|
438
462
|
// MARK: - Mic tuning (default no-op โ Mentra Live supports this)
|
|
439
463
|
|
|
440
464
|
func sendMicTuningSetting() {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum VuzixConnectionTarget {
|
|
4
|
+
static func identifier(_ name: String) -> String {
|
|
5
|
+
guard let start = name.lastIndex(of: "["),
|
|
6
|
+
let end = name[start...].firstIndex(of: "]")
|
|
7
|
+
else { return name }
|
|
8
|
+
return String(name[name.index(after: start) ..< end])
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static func matches(name: String?, target: String) -> Bool {
|
|
12
|
+
guard let name, !target.isEmpty else { return false }
|
|
13
|
+
return identifier(name) == identifier(target)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import UIKit
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* SherpaOnnxTranscriber handles real-time audio transcription using Sherpa-ONNX.
|
|
@@ -11,24 +10,30 @@ class SherpaOnnxTranscriber {
|
|
|
11
10
|
private static let TAG = "SherpaOnnxTranscriber"
|
|
12
11
|
|
|
13
12
|
private static let SAMPLE_RATE = 16000 // Sherpa-ONNX model's required sample rate
|
|
14
|
-
private
|
|
13
|
+
private final class Session {
|
|
14
|
+
let recognizer: SherpaOnnxRecognizer
|
|
15
|
+
var lastPartialResult = ""
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
private var processingTask: DispatchWorkItem?
|
|
21
|
-
|
|
22
|
-
/// The underlying Sherpa-ONNX objects
|
|
23
|
-
private var recognizer: SherpaOnnxRecognizer?
|
|
17
|
+
init(_ recognizer: SherpaOnnxRecognizer) {
|
|
18
|
+
self.recognizer = recognizer
|
|
19
|
+
}
|
|
20
|
+
}
|
|
24
21
|
|
|
25
|
-
private var
|
|
22
|
+
private lazy var worker = DemandDrivenAudioWorker<Session>(
|
|
23
|
+
makeProcessor: { Self.makeRecognizer().map(Session.init) },
|
|
24
|
+
process: { [weak self] session, data, generation in
|
|
25
|
+
self?.process(session, data: data, generation: generation)
|
|
26
|
+
}
|
|
27
|
+
)
|
|
26
28
|
|
|
27
|
-
///
|
|
28
|
-
|
|
29
|
+
/// Construct the worker on the manager's actor before audio can arrive.
|
|
30
|
+
init() {
|
|
31
|
+
_ = worker
|
|
32
|
+
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
func setActive(_ active: Bool) {
|
|
35
|
+
worker.setActive(active)
|
|
36
|
+
}
|
|
32
37
|
|
|
33
38
|
/// Dynamic model path support
|
|
34
39
|
private static var customModelPath: String? {
|
|
@@ -65,23 +70,9 @@ class SherpaOnnxTranscriber {
|
|
|
65
70
|
return nil
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
init(context: UIViewController) {
|
|
72
|
-
self.context = context
|
|
73
|
-
transcriptionSessionStart = Date()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
deinit {
|
|
77
|
-
shutdown()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Initialize the Sherpa-ONNX recognizer.
|
|
82
|
-
* Loads models and configuration, sets up processing thread.
|
|
83
|
-
*/
|
|
84
|
-
func initialize() {
|
|
73
|
+
/// Runs exclusively on the worker queue, never on the main/audio delivery thread.
|
|
74
|
+
private static func makeRecognizer() -> SherpaOnnxRecognizer? {
|
|
75
|
+
var recognizer: SherpaOnnxRecognizer?
|
|
85
76
|
do {
|
|
86
77
|
var tokensPath: String
|
|
87
78
|
var modelType = "unknown"
|
|
@@ -201,30 +192,26 @@ class SherpaOnnxTranscriber {
|
|
|
201
192
|
Bridge.log("No Sherpa ONNX model available. Transcription will be disabled.")
|
|
202
193
|
Bridge.log("Please download a model using the model downloader in settings.")
|
|
203
194
|
recognizer = nil
|
|
204
|
-
|
|
205
|
-
return
|
|
195
|
+
return nil
|
|
206
196
|
}
|
|
207
197
|
|
|
208
198
|
if recognizer == nil {
|
|
209
199
|
throw NSError(domain: "SherpaOnnxTranscriber", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create recognizer"])
|
|
210
200
|
}
|
|
211
201
|
|
|
212
|
-
startProcessingTask()
|
|
213
|
-
isRunning = true
|
|
214
|
-
|
|
215
202
|
Bridge.log("Sherpa-ONNX ASR initialized successfully with \(modelType) model")
|
|
203
|
+
return recognizer
|
|
216
204
|
|
|
217
205
|
} catch {
|
|
218
206
|
Bridge.log("Failed to initialize Sherpa-ONNX: \(error.localizedDescription)")
|
|
207
|
+
return nil
|
|
219
208
|
}
|
|
220
209
|
}
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
* Handle transcription results - send only to delegate
|
|
224
|
-
*/
|
|
225
|
-
private func handleTranscriptionResult(text: String, isFinal: Bool) {
|
|
226
|
-
// Forward to delegate if set
|
|
211
|
+
private func handleTranscriptionResult(text: String, isFinal: Bool, generation: UInt64) {
|
|
227
212
|
DispatchQueue.main.async { [weak self] in
|
|
213
|
+
// A decode or main-queue delivery may finish after stop/model replacement.
|
|
214
|
+
guard let self, self.worker.isCurrent(generation) else { return }
|
|
228
215
|
if isFinal {
|
|
229
216
|
STTTools.didReceiveFinalTranscription(text)
|
|
230
217
|
} else {
|
|
@@ -233,123 +220,30 @@ class SherpaOnnxTranscriber {
|
|
|
233
220
|
}
|
|
234
221
|
}
|
|
235
222
|
|
|
236
|
-
/**
|
|
237
|
-
* Feed PCM audio data (16-bit little endian) into the transcriber.
|
|
238
|
-
* This method should be called continuously with short chunks (e.g., 100-300ms).
|
|
239
|
-
*
|
|
240
|
-
* Audio is queued directly; microphone VAD gating is not applied in the SDK.
|
|
241
|
-
*/
|
|
242
223
|
func acceptAudio(pcm16le: Data) {
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
queueAudioData(pcm16le)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private func queueAudioData(_ pcm16le: Data) {
|
|
251
|
-
pcmQueue.async { [weak self] in
|
|
252
|
-
guard let self = self else { return }
|
|
253
|
-
|
|
254
|
-
let queueSizeBefore = self.pcmBuffers.count
|
|
255
|
-
self.pcmBuffers.append(pcm16le)
|
|
256
|
-
|
|
257
|
-
// Keep queue size manageable
|
|
258
|
-
if self.pcmBuffers.count > Self.QUEUE_CAPACITY {
|
|
259
|
-
let removedBuffer = self.pcmBuffers.removeFirst()
|
|
260
|
-
Bridge.log("โ ๏ธ Audio queue overflow - dropped buffer of \(removedBuffer.count) bytes")
|
|
261
|
-
}
|
|
262
|
-
}
|
|
224
|
+
worker.accept(pcm16le)
|
|
263
225
|
}
|
|
264
226
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let workItem = DispatchWorkItem { [weak self] in
|
|
274
|
-
self?.runLoop()
|
|
227
|
+
private func process(_ session: Session, data: Data, generation: UInt64) {
|
|
228
|
+
guard worker.isCurrent(generation) else { return }
|
|
229
|
+
let recognizer = session.recognizer
|
|
230
|
+
recognizer.acceptWaveform(samples: toFloatArray(from: data), sampleRate: Self.SAMPLE_RATE)
|
|
231
|
+
while recognizer.isReady() {
|
|
232
|
+
guard worker.isCurrent(generation) else { return }
|
|
233
|
+
recognizer.decode()
|
|
275
234
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Main processing loop that handles transcription in real-time.
|
|
283
|
-
* Pulls audio from queue, feeds into Sherpa, emits partial/final results.
|
|
284
|
-
*/
|
|
285
|
-
private func runLoop() {
|
|
286
|
-
Bridge.log("๐ Sherpa-ONNX processing loop started")
|
|
287
|
-
|
|
288
|
-
while isRunning {
|
|
289
|
-
// Pull data from queue
|
|
290
|
-
var audioData: Data?
|
|
291
|
-
|
|
292
|
-
pcmQueue.sync {
|
|
293
|
-
if !self.pcmBuffers.isEmpty {
|
|
294
|
-
audioData = self.pcmBuffers.removeFirst()
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if let data = audioData {
|
|
299
|
-
// Synchronize access to recognizer to prevent race conditions
|
|
300
|
-
objc_sync_enter(self)
|
|
301
|
-
defer { objc_sync_exit(self) }
|
|
302
|
-
|
|
303
|
-
guard let recognizer = recognizer else {
|
|
304
|
-
Bridge.log("โ ๏ธ Recognizer not available, skipping audio chunk")
|
|
305
|
-
continue
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
do {
|
|
309
|
-
// Convert PCM to float [-1.0, 1.0]
|
|
310
|
-
let floatBuf = toFloatArray(from: data)
|
|
311
|
-
|
|
312
|
-
// Pass audio data to the Sherpa-ONNX stream
|
|
313
|
-
recognizer.acceptWaveform(samples: floatBuf, sampleRate: Self.SAMPLE_RATE)
|
|
314
|
-
|
|
315
|
-
// Decode continuously while model is ready
|
|
316
|
-
var decodeCount = 0
|
|
317
|
-
while recognizer.isReady() {
|
|
318
|
-
recognizer.decode()
|
|
319
|
-
decodeCount += 1
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// If utterance endpoint detected
|
|
323
|
-
if recognizer.isEndpoint() {
|
|
324
|
-
let result = recognizer.getResult()
|
|
325
|
-
let finalText = result.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
326
|
-
|
|
327
|
-
if !finalText.isEmpty {
|
|
328
|
-
handleTranscriptionResult(text: finalText, isFinal: true)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
recognizer.reset() // Start new utterance
|
|
332
|
-
lastPartialResult = ""
|
|
333
|
-
} else {
|
|
334
|
-
// Emit partial results if changed
|
|
335
|
-
let result = recognizer.getResult()
|
|
336
|
-
let partial = result.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
337
|
-
|
|
338
|
-
if partial != lastPartialResult, !partial.isEmpty {
|
|
339
|
-
handleTranscriptionResult(text: partial, isFinal: false)
|
|
340
|
-
lastPartialResult = partial
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} catch {
|
|
344
|
-
Bridge.log("โ Error processing audio: \(error.localizedDescription)")
|
|
345
|
-
}
|
|
346
|
-
} else {
|
|
347
|
-
// Sleep briefly to avoid tight CPU loop if no audio is available
|
|
348
|
-
Thread.sleep(forTimeInterval: 0.01)
|
|
235
|
+
guard worker.isCurrent(generation) else { return }
|
|
236
|
+
let text = recognizer.getResult().text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
237
|
+
if recognizer.isEndpoint() {
|
|
238
|
+
if !text.isEmpty {
|
|
239
|
+
handleTranscriptionResult(text: text, isFinal: true, generation: generation)
|
|
349
240
|
}
|
|
241
|
+
recognizer.reset()
|
|
242
|
+
session.lastPartialResult = ""
|
|
243
|
+
} else if !text.isEmpty, text != session.lastPartialResult {
|
|
244
|
+
handleTranscriptionResult(text: text, isFinal: false, generation: generation)
|
|
245
|
+
session.lastPartialResult = text
|
|
350
246
|
}
|
|
351
|
-
|
|
352
|
-
Bridge.log("ASR processing thread stopped")
|
|
353
247
|
}
|
|
354
248
|
|
|
355
249
|
/**
|
|
@@ -377,45 +271,11 @@ class SherpaOnnxTranscriber {
|
|
|
377
271
|
return samples
|
|
378
272
|
}
|
|
379
273
|
|
|
380
|
-
/**
|
|
381
|
-
* Stop transcription processing.
|
|
382
|
-
* This shuts down the processing thread and releases Sherpa-ONNX resources.
|
|
383
|
-
*/
|
|
384
274
|
func shutdown() {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
isRunning = false
|
|
388
|
-
processingTask?.cancel()
|
|
389
|
-
|
|
390
|
-
// Synchronize access to recognizer during shutdown
|
|
391
|
-
objc_sync_enter(self)
|
|
392
|
-
defer { objc_sync_exit(self) }
|
|
393
|
-
|
|
394
|
-
// The recognizer will be automatically cleaned up by ARC when set to nil
|
|
395
|
-
if recognizer != nil {
|
|
396
|
-
Bridge.log("๐งน Cleaning up Sherpa-ONNX recognizer")
|
|
397
|
-
recognizer = nil
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Clear any remaining audio buffers
|
|
401
|
-
pcmQueue.sync {
|
|
402
|
-
let remainingBuffers = self.pcmBuffers.count
|
|
403
|
-
if remainingBuffers > 0 {
|
|
404
|
-
Bridge.log("๐๏ธ Clearing \(remainingBuffers) remaining audio buffers")
|
|
405
|
-
}
|
|
406
|
-
self.pcmBuffers.removeAll()
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
Bridge.log("โ
SherpaOnnxTranscriber shutdown complete")
|
|
275
|
+
worker.setActive(false)
|
|
410
276
|
}
|
|
411
277
|
|
|
412
|
-
/**
|
|
413
|
-
* Restarts the transcriber after a model change.
|
|
414
|
-
* Shuts down existing resources, clears buffers, and reinitializes the recognizer.
|
|
415
|
-
*/
|
|
416
278
|
func restart() {
|
|
417
|
-
|
|
418
|
-
shutdown()
|
|
419
|
-
initialize()
|
|
279
|
+
worker.restart()
|
|
420
280
|
}
|
|
421
281
|
}
|