@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.
Files changed (83) hide show
  1. package/README.md +6 -2
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +239 -52
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +47 -4
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +6 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +381 -97
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +16 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +293 -174
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  19. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
  20. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  21. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
  24. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +31 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  29. package/build/BluetoothSdk.types.d.ts +71 -1
  30. package/build/BluetoothSdk.types.d.ts.map +1 -1
  31. package/build/BluetoothSdk.types.js.map +1 -1
  32. package/build/_internal.d.ts +2 -0
  33. package/build/_internal.d.ts.map +1 -1
  34. package/build/_internal.js +2 -0
  35. package/build/_internal.js.map +1 -1
  36. package/build/_private/BluetoothSdkModule.d.ts +27 -2
  37. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  38. package/build/_private/BluetoothSdkModule.js +20 -0
  39. package/build/_private/BluetoothSdkModule.js.map +1 -1
  40. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  41. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  42. package/build/_private/MentraLocalNetworkModule.js +3 -0
  43. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  44. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  45. package/build/_private/cameraRequestPayload.js +1 -0
  46. package/build/_private/cameraRequestPayload.js.map +1 -1
  47. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  48. package/build/_private/photoRequestPayload.js +2 -0
  49. package/build/_private/photoRequestPayload.js.map +1 -1
  50. package/build/index.d.ts +1 -1
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +9 -0
  53. package/build/index.js.map +1 -1
  54. package/expo-module.config.json +1 -0
  55. package/ios/BluetoothSdkModule.swift +72 -0
  56. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  57. package/ios/Source/Bridge.swift +72 -18
  58. package/ios/Source/DeviceManager.swift +52 -3
  59. package/ios/Source/DeviceStore.swift +6 -1
  60. package/ios/Source/MentraBluetoothSDK.swift +298 -21
  61. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  62. package/ios/Source/camera/CameraModels.swift +22 -3
  63. package/ios/Source/sgcs/G1.swift +2 -2
  64. package/ios/Source/sgcs/G2.swift +19 -7
  65. package/ios/Source/sgcs/Mach1.swift +1 -1
  66. package/ios/Source/sgcs/MentraLive.swift +456 -52
  67. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  68. package/ios/Source/sgcs/MentraNex.swift +1 -1
  69. package/ios/Source/sgcs/Nimo.swift +1 -1
  70. package/ios/Source/sgcs/SGCManager.swift +5 -2
  71. package/ios/Source/sgcs/Simulated.swift +1 -1
  72. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  73. package/ios/Source/streaming/StreamModels.swift +9 -0
  74. package/ios/Source/tts/TTSTools.swift +1 -1
  75. package/ios/Source/utils/G1Text.swift +30 -1
  76. package/package.json +1 -2
  77. package/src/BluetoothSdk.types.ts +75 -0
  78. package/src/_internal.ts +2 -0
  79. package/src/_private/BluetoothSdkModule.ts +68 -13
  80. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  81. package/src/_private/cameraRequestPayload.ts +1 -0
  82. package/src/_private/photoRequestPayload.ts +3 -3
  83. package/src/index.ts +10 -0
@@ -0,0 +1,333 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import android.net.ConnectivityManager
4
+ import android.net.Network
5
+ import android.net.NetworkCapabilities
6
+ import android.net.NetworkRequest
7
+ import android.net.wifi.WifiNetworkSpecifier
8
+ import android.os.Build
9
+ import android.util.Base64
10
+ import android.util.Log
11
+ import expo.modules.kotlin.Promise
12
+ import expo.modules.kotlin.modules.Module
13
+ import expo.modules.kotlin.modules.ModuleDefinition
14
+ import java.io.File
15
+ import java.io.FileOutputStream
16
+ import java.net.HttpURLConnection
17
+ import java.net.URL
18
+ import java.util.concurrent.ConcurrentHashMap
19
+ import java.util.concurrent.Executors
20
+
21
+ /** MentraOS-internal transport for glasses-local traffic without process network binding. */
22
+ class MentraLocalNetworkModule : Module() {
23
+ private companion object {
24
+ const val TAG = "MentraLocalNetwork"
25
+ }
26
+
27
+ private val lock = Any()
28
+ private val executor = Executors.newCachedThreadPool()
29
+ private val activeConnections = ConcurrentHashMap<String, HttpURLConnection>()
30
+ private var localNetwork: Network? = null
31
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
32
+ private var pendingConnectPromise: Promise? = null
33
+
34
+ override fun definition() = ModuleDefinition {
35
+ Name("MentraLocalNetwork")
36
+ Events("downloadProgress", "networkLost")
37
+
38
+ AsyncFunction("connect") { ssid: String, password: String, promise: Promise ->
39
+ connect(ssid, password, promise)
40
+ }
41
+
42
+ AsyncFunction("request") {
43
+ requestId: String,
44
+ url: String,
45
+ method: String,
46
+ headers: Map<String, String>,
47
+ body: String?,
48
+ timeoutMs: Int,
49
+ promise: Promise ->
50
+ executor.execute { performRequest(requestId, url, method, headers, body, timeoutMs, promise) }
51
+ }
52
+
53
+ AsyncFunction("download") {
54
+ requestId: String,
55
+ url: String,
56
+ destination: String,
57
+ headers: Map<String, String>,
58
+ connectionTimeoutMs: Int,
59
+ readTimeoutMs: Int,
60
+ promise: Promise ->
61
+ executor.execute {
62
+ performDownload(
63
+ requestId,
64
+ url,
65
+ destination,
66
+ headers,
67
+ connectionTimeoutMs,
68
+ readTimeoutMs,
69
+ promise,
70
+ )
71
+ }
72
+ }
73
+
74
+ AsyncFunction("cancel") { requestId: String -> cancel(requestId) }
75
+ AsyncFunction("disconnect") { disconnect() }
76
+
77
+ OnDestroy { disconnect() }
78
+ }
79
+
80
+ private fun connectivityManager(): ConnectivityManager =
81
+ requireNotNull(appContext.reactContext?.getSystemService(ConnectivityManager::class.java)) {
82
+ "ConnectivityManager is unavailable"
83
+ }
84
+
85
+ private fun connect(ssid: String, password: String, promise: Promise) {
86
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
87
+ promise.reject("ERR_UNSUPPORTED_ANDROID", "Scoped local WiFi requires Android 10 or newer", null)
88
+ return
89
+ }
90
+
91
+ disconnect()
92
+ val manager = connectivityManager()
93
+ val specifier =
94
+ WifiNetworkSpecifier.Builder()
95
+ .setSsid(ssid)
96
+ .setWpa2Passphrase(password)
97
+ .build()
98
+ val request =
99
+ NetworkRequest.Builder()
100
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
101
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
102
+ .setNetworkSpecifier(specifier)
103
+ .build()
104
+
105
+ val callback =
106
+ object : ConnectivityManager.NetworkCallback() {
107
+ override fun onAvailable(network: Network) {
108
+ val connectPromise: Promise?
109
+ synchronized(lock) {
110
+ if (networkCallback !== this) {
111
+ Log.d(TAG, "Ignoring onAvailable from a stale network request")
112
+ return
113
+ }
114
+ localNetwork = network
115
+ connectPromise = pendingConnectPromise
116
+ pendingConnectPromise = null
117
+ }
118
+ Log.i(
119
+ TAG,
120
+ "Captured glasses WiFi network=$network processBoundNetwork=${manager.boundNetworkForProcess}",
121
+ )
122
+ connectPromise?.resolve(mapOf("connected" to true, "ssid" to ssid))
123
+ }
124
+
125
+ override fun onUnavailable() {
126
+ synchronized(lock) {
127
+ if (networkCallback !== this) {
128
+ Log.d(TAG, "Ignoring onUnavailable from a stale network request")
129
+ return
130
+ }
131
+ }
132
+ rejectPendingConnect("ERR_LOCAL_WIFI_UNAVAILABLE", "Could not connect to $ssid")
133
+ clearNetworkCallback(this)
134
+ }
135
+
136
+ override fun onLost(network: Network) {
137
+ synchronized(lock) {
138
+ if (networkCallback !== this || localNetwork != network) {
139
+ Log.d(TAG, "Ignoring onLost from a stale network request")
140
+ return
141
+ }
142
+ localNetwork = null
143
+ }
144
+ cancelAll()
145
+ clearNetworkCallback(this)
146
+ sendEvent("networkLost", mapOf("ssid" to ssid))
147
+ }
148
+ }
149
+
150
+ synchronized(lock) {
151
+ pendingConnectPromise = promise
152
+ networkCallback = callback
153
+ }
154
+ try {
155
+ manager.requestNetwork(request, callback, 30_000)
156
+ } catch (error: Exception) {
157
+ clearNetworkCallback(callback)
158
+ rejectPendingConnect("ERR_LOCAL_WIFI_REQUEST", error.message ?: "Local WiFi request failed", error)
159
+ }
160
+ }
161
+
162
+ private fun performRequest(
163
+ requestId: String,
164
+ url: String,
165
+ method: String,
166
+ headers: Map<String, String>,
167
+ body: String?,
168
+ timeoutMs: Int,
169
+ promise: Promise,
170
+ ) {
171
+ var connection: HttpURLConnection? = null
172
+ try {
173
+ connection = openConnection(requestId, url)
174
+ connection.requestMethod = method
175
+ connection.connectTimeout = timeoutMs
176
+ connection.readTimeout = timeoutMs
177
+ headers.forEach(connection::setRequestProperty)
178
+ if (body != null) {
179
+ connection.doOutput = true
180
+ connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
181
+ }
182
+ val status = connection.responseCode
183
+ val stream = if (status >= 400) connection.errorStream else connection.inputStream
184
+ val bytes = stream?.use { it.readBytes() } ?: ByteArray(0)
185
+ promise.resolve(
186
+ mapOf(
187
+ "status" to status,
188
+ "headers" to responseHeaders(connection),
189
+ "bodyBase64" to Base64.encodeToString(bytes, Base64.NO_WRAP),
190
+ ),
191
+ )
192
+ } catch (error: Exception) {
193
+ promise.reject("ERR_LOCAL_HTTP", error.message ?: "Local HTTP request failed", error)
194
+ } finally {
195
+ activeConnections.remove(requestId)
196
+ connection?.disconnect()
197
+ }
198
+ }
199
+
200
+ private fun performDownload(
201
+ requestId: String,
202
+ url: String,
203
+ destination: String,
204
+ headers: Map<String, String>,
205
+ connectionTimeoutMs: Int,
206
+ readTimeoutMs: Int,
207
+ promise: Promise,
208
+ ) {
209
+ var connection: HttpURLConnection? = null
210
+ try {
211
+ connection = openConnection(requestId, url)
212
+ connection.requestMethod = "GET"
213
+ connection.connectTimeout = connectionTimeoutMs
214
+ connection.readTimeout = readTimeoutMs
215
+ headers.forEach(connection::setRequestProperty)
216
+ val status = connection.responseCode
217
+ if (status !in 200..299) throw IllegalStateException("HTTP $status")
218
+ val contentLength = connection.contentLengthLong
219
+ val responseHeaders = responseHeaders(connection)
220
+
221
+ // Match RNFS's begin callback contract before the first response byte. Gallery
222
+ // resumable downloads need the real status and Content-Range to validate segments.
223
+ sendEvent(
224
+ "downloadProgress",
225
+ mapOf(
226
+ "requestId" to requestId,
227
+ "bytesWritten" to 0L,
228
+ "contentLength" to contentLength,
229
+ "statusCode" to status,
230
+ "headers" to responseHeaders,
231
+ ),
232
+ )
233
+
234
+ val destinationFile = File(destination)
235
+ destinationFile.parentFile?.mkdirs()
236
+ var bytesWritten = 0L
237
+ connection.inputStream.use { input ->
238
+ FileOutputStream(destinationFile, false).use { output ->
239
+ val buffer = ByteArray(64 * 1024)
240
+ while (true) {
241
+ val count = input.read(buffer)
242
+ if (count < 0) break
243
+ output.write(buffer, 0, count)
244
+ bytesWritten += count
245
+ sendEvent(
246
+ "downloadProgress",
247
+ mapOf(
248
+ "requestId" to requestId,
249
+ "bytesWritten" to bytesWritten,
250
+ "contentLength" to contentLength,
251
+ ),
252
+ )
253
+ }
254
+ }
255
+ }
256
+ promise.resolve(
257
+ mapOf(
258
+ "statusCode" to status,
259
+ "bytesWritten" to bytesWritten,
260
+ "headers" to responseHeaders,
261
+ ),
262
+ )
263
+ } catch (error: Exception) {
264
+ promise.reject("ERR_LOCAL_DOWNLOAD", error.message ?: "Local download failed", error)
265
+ } finally {
266
+ activeConnections.remove(requestId)
267
+ connection?.disconnect()
268
+ }
269
+ }
270
+
271
+ private fun openConnection(requestId: String, url: String): HttpURLConnection {
272
+ val network = synchronized(lock) { localNetwork }
273
+ ?: throw IllegalStateException("Local WiFi is not connected")
274
+ val connection = network.openConnection(URL(url)) as HttpURLConnection
275
+ activeConnections[requestId] = connection
276
+ return connection
277
+ }
278
+
279
+ private fun responseHeaders(connection: HttpURLConnection): Map<String, String> =
280
+ connection.headerFields
281
+ .filterKeys { it != null }
282
+ .mapValues { (_, values) -> values.joinToString(", ") }
283
+
284
+ private fun cancel(requestId: String) {
285
+ activeConnections.remove(requestId)?.disconnect()
286
+ }
287
+
288
+ private fun cancelAll() {
289
+ activeConnections.keys.toList().forEach(::cancel)
290
+ }
291
+
292
+ private fun disconnect() {
293
+ cancelAll()
294
+ val manager = appContext.reactContext?.getSystemService(ConnectivityManager::class.java)
295
+ val callback: ConnectivityManager.NetworkCallback?
296
+ synchronized(lock) {
297
+ callback = networkCallback
298
+ networkCallback = null
299
+ localNetwork = null
300
+ }
301
+ if (callback != null && manager != null) {
302
+ try {
303
+ manager.unregisterNetworkCallback(callback)
304
+ } catch (_: IllegalArgumentException) {
305
+ // Already unregistered by the platform.
306
+ }
307
+ }
308
+ rejectPendingConnect("ERR_LOCAL_WIFI_CANCELLED", "Local WiFi connection cancelled")
309
+ }
310
+
311
+ private fun clearNetworkCallback(callback: ConnectivityManager.NetworkCallback) {
312
+ val manager = appContext.reactContext?.getSystemService(ConnectivityManager::class.java)
313
+ synchronized(lock) {
314
+ if (networkCallback == callback) networkCallback = null
315
+ }
316
+ if (manager != null) {
317
+ try {
318
+ manager.unregisterNetworkCallback(callback)
319
+ } catch (_: IllegalArgumentException) {
320
+ // Callback was not registered or has already been removed.
321
+ }
322
+ }
323
+ }
324
+
325
+ private fun rejectPendingConnect(code: String, message: String, cause: Throwable? = null) {
326
+ val promise: Promise?
327
+ synchronized(lock) {
328
+ promise = pendingConnectPromise
329
+ pendingConnectPromise = null
330
+ }
331
+ promise?.reject(code, message, cause)
332
+ }
333
+ }
@@ -167,6 +167,17 @@ data class CameraFovResult(
167
167
  }
168
168
  }
169
169
 
170
+ enum class PhotoMode(val value: String) {
171
+ PHOTO("photo"),
172
+ TEXT("text");
173
+
174
+ companion object {
175
+ @JvmStatic
176
+ fun fromValue(value: String?): PhotoMode =
177
+ values().firstOrNull { it.value == value } ?: PHOTO
178
+ }
179
+ }
180
+
170
181
  data class PhotoRequest @JvmOverloads constructor(
171
182
  val requestId: String = generatedCameraRequestId("photo"),
172
183
  val size: PhotoSize,
@@ -188,6 +199,9 @@ data class PhotoRequest @JvmOverloads constructor(
188
199
  val ispDigitalGain: Int? = null,
189
200
  val ispAnalogGain: String? = null,
190
201
  val resetCaptureTuning: Boolean? = null,
202
+ val mode: PhotoMode = PhotoMode.PHOTO,
203
+ /** `ble` skips direct upload; `auto` tries Wi-Fi with BLE fallback. */
204
+ val transferMethod: String = "auto",
191
205
  ) {
192
206
  companion object {
193
207
  /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
@@ -225,6 +239,8 @@ data class PhotoRequest @JvmOverloads constructor(
225
239
  compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
226
240
  save = boolValue(values, "save", "saveToGallery") ?: false,
227
241
  sound = boolValue(values, "sound") ?: true,
242
+ mode = PhotoMode.fromValue(stringValue(values, "mode")),
243
+ transferMethod = if (stringValue(values, "transferMethod") == "ble") "ble" else "auto",
228
244
  exposureTimeNs = exposureTimeNs,
229
245
  iso = iso,
230
246
  aeExposureDivisor = aeDivisor,
@@ -1773,7 +1773,7 @@ class G1 : SGCManager() {
1773
1773
  return ""
1774
1774
  }
1775
1775
 
1776
- override fun requestWifiScan() {
1776
+ override fun requestWifiScan(scanId: String?) {
1777
1777
 
1778
1778
  }
1779
1779
 
@@ -2841,7 +2841,7 @@ class G1 : SGCManager() {
2841
2841
  private fun chunkTextForTransmission(text: String?): List<ByteArray> {
2842
2842
  // Handle empty or whitespace-only text by sending at least a space
2843
2843
  // This ensures the display gets updated/cleared properly
2844
- val textToSend = if (text == null || text.trim().isEmpty()) " " else text
2844
+ val textToSend = if (text == null || text.trim().isEmpty()) " " else sanitizeG1DisplayText(text)
2845
2845
  val textBytes = textToSend.toByteArray(StandardCharsets.UTF_8)
2846
2846
  val totalChunks = Math.ceil(textBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
2847
2847
 
@@ -0,0 +1,38 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import java.text.Normalizer
4
+
5
+ private val G1_COMBINING_MARKS = Regex("\\p{M}+")
6
+
7
+ /**
8
+ * Converts text to the base Latin glyphs available in the Even Realities G1 firmware.
9
+ *
10
+ * G1 does not ship glyphs for combining diacritics. Normalize at the device boundary so
11
+ * miniapps can retain the real text everywhere else and newer glasses receive it unchanged.
12
+ */
13
+ internal fun sanitizeG1DisplayText(text: String): String {
14
+ val expanded = buildString(text.length) {
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, "")
38
+ }
@@ -575,16 +575,18 @@ private object DevSettingsProto {
575
575
  /// TimeSync submessage: f1 = (Unix seconds + TZ offset seconds) as Int32, no TZ field.
576
576
  /// Firmware appears to ignore the TZ field, so we pre-shift the timestamp itself
577
577
  /// to make UTC interpretation read as local. Empirically confirmed via probe variants in dbg1().
578
- fun timeSync(magicRandom: Int): ByteArray {
578
+ fun timeSync(
579
+ magicRandom: Int,
580
+ timestampMs: Long = System.currentTimeMillis()
581
+ ): ByteArray {
579
582
  val w = ProtobufWriter()
580
583
  w.writeInt32Field(1, DevCfgCommandId.TIME_SYNC.value)
581
584
  w.writeInt32Field(2, magicRandom)
582
585
 
583
586
  val tsW = ProtobufWriter()
584
- val nowMs = System.currentTimeMillis()
585
- val nowSec = nowMs / 1000
586
- val tzSec = (TimeZone.getDefault().getOffset(nowMs) / 1000).toLong()
587
- tsW.writeInt32Field(1, (nowSec + tzSec).toInt())
587
+ val timestampSec = timestampMs / 1000
588
+ val tzSec = (TimeZone.getDefault().getOffset(timestampMs) / 1000).toLong()
589
+ tsW.writeInt32Field(1, (timestampSec + tzSec).toInt())
588
590
  w.writeMessageField(128, tsW.toByteArray())
589
591
  return w.toByteArray()
590
592
  }
@@ -1707,7 +1709,7 @@ class G2 : SGCManager() {
1707
1709
  payload = payload,
1708
1710
  reserveFlag = true
1709
1711
  )
1710
- sendToGlasses(packets)
1712
+ sendToGlasses(packets, left = true, right = true)
1711
1713
  }
1712
1714
 
1713
1715
  // ---------- Authentication Sequence ----------
@@ -1732,7 +1734,7 @@ class G2 : SGCManager() {
1732
1734
 
1733
1735
  delay(200)
1734
1736
  val timeSync = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
1735
- sendDevSettingsCommand(timeSync)
1737
+ sendDevSettingsCommand(timeSync, left = true, right = true)
1736
1738
 
1737
1739
  // Skip onboarding on connect
1738
1740
  delay(200)
@@ -3763,7 +3765,12 @@ class G2 : SGCManager() {
3763
3765
  /// time-zone travel, or a long sleep where the glasses' clock has drifted.
3764
3766
  fun syncTime() {
3765
3767
  Bridge.log("G2: syncTime()")
3766
- val msg = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
3768
+ sendSetSystemTime(System.currentTimeMillis())
3769
+ }
3770
+
3771
+ override fun sendSetSystemTime(timestampMs: Long) {
3772
+ Bridge.log("G2: sendSetSystemTime()")
3773
+ val msg = DevSettingsProto.timeSync(sendManager.nextMagicRandom(), timestampMs)
3767
3774
  sendDevSettingsCommand(msg, left = true, right = true)
3768
3775
  }
3769
3776
 
@@ -3782,7 +3789,7 @@ class G2 : SGCManager() {
3782
3789
 
3783
3790
  // ---------- SGCManager: Network (G2 has no WiFi) ----------
3784
3791
 
3785
- override fun requestWifiScan() {}
3792
+ override fun requestWifiScan(scanId: String?) {}
3786
3793
  override fun sendWifiCredentials(ssid: String, password: String) {}
3787
3794
  override fun forgetWifiNetwork(ssid: String) {}
3788
3795
  override fun sendHotspotState(enabled: Boolean) {}
@@ -350,7 +350,7 @@ class Mach1 : SGCManager() {
350
350
  destroy()
351
351
  }
352
352
 
353
- override fun requestWifiScan() {
353
+ override fun requestWifiScan(scanId: String?) {
354
354
 
355
355
  }
356
356