@mentra/bluetooth-sdk 0.1.14 → 0.1.17
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 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/build/BluetoothSdk.types.d.ts +39 -14
- 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/_internal.d.ts +4 -4
- package/build/_internal.js +4 -4
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +11 -3
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +25 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +8 -2
- 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 +72 -54
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +40 -12
- package/ios/Source/BluetoothSdkDefaults.swift +2 -30
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +72 -12
- package/ios/Source/MentraBluetoothSDK.swift +120 -50
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +75 -9
- package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
- package/ios/Source/events/BluetoothEvents.swift +5 -27
- package/ios/Source/internal/BluetoothAvailability.swift +5 -5
- package/ios/Source/sgcs/G1.swift +51 -1
- package/ios/Source/sgcs/G2.swift +643 -203
- package/ios/Source/sgcs/MentraLive.swift +53 -39
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +12 -5
- package/ios/Source/stt/STTTools.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/package.json +9 -1
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/src/BluetoothSdk.types.ts +42 -17
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +18 -2
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +9 -2
- package/src/index.ts +76 -58
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
func generatedCameraRequestId(_ prefix: String) -> String {
|
|
4
|
+
"\(prefix)-\(Int(Date().timeIntervalSince1970 * 1000))-\(UUID().uuidString.prefix(8))"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
func nonBlankRequestId(_ requestId: String?) -> String? {
|
|
8
|
+
guard let requestId else { return nil }
|
|
9
|
+
let trimmed = requestId.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
10
|
+
return trimmed.isEmpty ? nil : trimmed
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
public enum PhotoSize: String {
|
|
4
14
|
case low
|
|
5
15
|
case medium
|
|
@@ -200,13 +210,13 @@ public struct CameraFovResult: CustomStringConvertible {
|
|
|
200
210
|
|
|
201
211
|
static func from(ack: SettingsAckEvent, fallback: CameraFov) throws -> CameraFovResult {
|
|
202
212
|
if ack.status == "error" {
|
|
203
|
-
throw
|
|
213
|
+
throw BluetoothSdkError(
|
|
204
214
|
code: ack.errorCode ?? "camera_fov_failed",
|
|
205
215
|
message: ack.errorMessage ?? "Camera FOV request failed."
|
|
206
216
|
)
|
|
207
217
|
}
|
|
208
218
|
if !ack.hardwareApplied {
|
|
209
|
-
throw
|
|
219
|
+
throw BluetoothSdkError(
|
|
210
220
|
code: "camera_fov_not_applied",
|
|
211
221
|
message: "Camera FOV was saved but not applied to hardware."
|
|
212
222
|
)
|
|
@@ -223,7 +233,6 @@ public struct CameraFovResult: CustomStringConvertible {
|
|
|
223
233
|
|
|
224
234
|
public struct PhotoRequest {
|
|
225
235
|
public let requestId: String
|
|
226
|
-
public let appId: String
|
|
227
236
|
public let size: PhotoSize
|
|
228
237
|
public let webhookUrl: String?
|
|
229
238
|
public let authToken: String?
|
|
@@ -244,8 +253,7 @@ public struct PhotoRequest {
|
|
|
244
253
|
public let ispAnalogGain: String?
|
|
245
254
|
|
|
246
255
|
public init(
|
|
247
|
-
requestId: String,
|
|
248
|
-
appId: String,
|
|
256
|
+
requestId: String? = nil,
|
|
249
257
|
size: PhotoSize,
|
|
250
258
|
webhookUrl: String? = nil,
|
|
251
259
|
authToken: String? = nil,
|
|
@@ -263,8 +271,7 @@ public struct PhotoRequest {
|
|
|
263
271
|
ispDigitalGain: Int? = nil,
|
|
264
272
|
ispAnalogGain: String? = nil
|
|
265
273
|
) {
|
|
266
|
-
self.requestId = requestId
|
|
267
|
-
self.appId = appId
|
|
274
|
+
self.requestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("photo")
|
|
268
275
|
self.size = size
|
|
269
276
|
self.webhookUrl = webhookUrl
|
|
270
277
|
self.authToken = authToken
|
|
@@ -331,8 +338,7 @@ public struct PhotoRequest {
|
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
return PhotoRequest(
|
|
334
|
-
requestId: params["requestId"] as? String
|
|
335
|
-
appId: params["appId"] as? String ?? "",
|
|
341
|
+
requestId: params["requestId"] as? String,
|
|
336
342
|
size: PhotoSize(normalizedRawValue: sizeRaw),
|
|
337
343
|
webhookUrl: params["webhookUrl"] as? String,
|
|
338
344
|
authToken: (params["authToken"] as? String)?.nilIfBlank,
|
|
@@ -378,6 +384,28 @@ public struct PhotoRequest {
|
|
|
378
384
|
json["ispAnalogGain"] = ispAnalogGain
|
|
379
385
|
}
|
|
380
386
|
}
|
|
387
|
+
|
|
388
|
+
func withRequestId(_ requestId: String) -> PhotoRequest {
|
|
389
|
+
PhotoRequest(
|
|
390
|
+
requestId: requestId,
|
|
391
|
+
size: size,
|
|
392
|
+
webhookUrl: webhookUrl,
|
|
393
|
+
authToken: authToken,
|
|
394
|
+
compress: compress,
|
|
395
|
+
save: save,
|
|
396
|
+
sound: sound,
|
|
397
|
+
exposureTimeNs: exposureTimeNs,
|
|
398
|
+
iso: iso,
|
|
399
|
+
aeExposureDivisor: aeExposureDivisor,
|
|
400
|
+
isoCap: isoCap,
|
|
401
|
+
noiseReduction: noiseReduction,
|
|
402
|
+
edgeEnhancement: edgeEnhancement,
|
|
403
|
+
mfnr: mfnr,
|
|
404
|
+
zsl: zsl,
|
|
405
|
+
ispDigitalGain: ispDigitalGain,
|
|
406
|
+
ispAnalogGain: ispAnalogGain
|
|
407
|
+
)
|
|
408
|
+
}
|
|
381
409
|
}
|
|
382
410
|
|
|
383
411
|
private extension String {
|
|
@@ -722,6 +750,44 @@ public struct PhotoStatusEvent: CustomStringConvertible {
|
|
|
722
750
|
}
|
|
723
751
|
}
|
|
724
752
|
|
|
753
|
+
public struct CameraStatusEvent: CustomStringConvertible {
|
|
754
|
+
public let values: [String: Any]
|
|
755
|
+
|
|
756
|
+
public init(values: [String: Any]) {
|
|
757
|
+
var values = values
|
|
758
|
+
values["type"] = "camera_status"
|
|
759
|
+
self.values = values
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
public var requestId: String {
|
|
763
|
+
stringValue(values, "requestId") ?? ""
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
public var state: String {
|
|
767
|
+
stringValue(values, "state") ?? ""
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
public var timestamp: Int64 {
|
|
771
|
+
if let value = values["timestamp"] as? Int64 { return value }
|
|
772
|
+
if let value = values["timestamp"] as? Int { return Int64(value) }
|
|
773
|
+
if let value = values["timestamp"] as? Double { return Int64(value) }
|
|
774
|
+
if let value = values["timestamp"] as? NSNumber { return value.int64Value }
|
|
775
|
+
return Int64(Date().timeIntervalSince1970 * 1000)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
public var errorCode: String? {
|
|
779
|
+
stringValue(values, "errorCode")
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
public var errorMessage: String? {
|
|
783
|
+
stringValue(values, "errorMessage")
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
public var description: String {
|
|
787
|
+
"CameraStatusEvent(requestId: \(requestId), state: \(state))"
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
725
791
|
public struct GalleryStatusEvent: CustomStringConvertible {
|
|
726
792
|
public let values: [String: Any]
|
|
727
793
|
|
|
@@ -73,28 +73,6 @@ public struct SpeakingStatusEvent: CustomStringConvertible {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
public struct OtaUpdateAvailableEvent: CustomStringConvertible {
|
|
77
|
-
public let versionCode: Int?
|
|
78
|
-
public let versionName: String?
|
|
79
|
-
public let updates: [String]
|
|
80
|
-
public let totalSize: Int?
|
|
81
|
-
public let cacheReady: Bool?
|
|
82
|
-
public let values: [String: Any]
|
|
83
|
-
|
|
84
|
-
public init(values: [String: Any]) {
|
|
85
|
-
versionCode = intValue(values["version_code"])
|
|
86
|
-
versionName = stringValue(values, "version_name")
|
|
87
|
-
updates = values["updates"] as? [String] ?? []
|
|
88
|
-
totalSize = intValue(values["total_size"])
|
|
89
|
-
cacheReady = boolValue(values, "cache_ready")
|
|
90
|
-
self.values = values
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
public var description: String {
|
|
94
|
-
"OtaUpdateAvailableEvent(versionName: \(versionName ?? "unknown"), updates: \(updates.joined(separator: ",")))"
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
76
|
public struct OtaStartAckEvent: CustomStringConvertible {
|
|
99
77
|
public let timestamp: Int?
|
|
100
78
|
public let values: [String: Any]
|
|
@@ -243,12 +221,12 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
243
221
|
case hotspotError(HotspotErrorEvent)
|
|
244
222
|
case photoResponse(PhotoResponseEvent)
|
|
245
223
|
case photoStatus(PhotoStatusEvent)
|
|
224
|
+
case cameraStatus(CameraStatusEvent)
|
|
246
225
|
case videoRecordingStatus(VideoRecordingStatusEvent)
|
|
247
226
|
case mediaUpload(MediaUploadEvent)
|
|
248
227
|
case rgbLedControlResponse(RgbLedControlResponseEvent)
|
|
249
228
|
case streamStatus(StreamStatusEvent)
|
|
250
229
|
case keepAliveAck(KeepAliveAckEvent)
|
|
251
|
-
case otaUpdateAvailable(OtaUpdateAvailableEvent)
|
|
252
230
|
case otaStartAck(OtaStartAckEvent)
|
|
253
231
|
case otaStatus(OtaStatusEvent)
|
|
254
232
|
case settingsAck(SettingsAckEvent)
|
|
@@ -276,6 +254,8 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
276
254
|
event.description
|
|
277
255
|
case let .photoStatus(event):
|
|
278
256
|
event.description
|
|
257
|
+
case let .cameraStatus(event):
|
|
258
|
+
event.description
|
|
279
259
|
case let .videoRecordingStatus(event):
|
|
280
260
|
event.description
|
|
281
261
|
case let .mediaUpload(event):
|
|
@@ -286,8 +266,6 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
286
266
|
event.description
|
|
287
267
|
case let .keepAliveAck(event):
|
|
288
268
|
event.description
|
|
289
|
-
case let .otaUpdateAvailable(event):
|
|
290
|
-
event.description
|
|
291
269
|
case let .otaStartAck(event):
|
|
292
270
|
event.description
|
|
293
271
|
case let .otaStatus(event):
|
|
@@ -317,7 +295,7 @@ public protocol MentraBluetoothSDKDelegate: AnyObject {
|
|
|
317
295
|
func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicLc3 event: MicLc3Event)
|
|
318
296
|
func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didChangeDefaultDevice device: Device?)
|
|
319
297
|
func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didLog message: String)
|
|
320
|
-
func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error:
|
|
298
|
+
func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error: BluetoothSdkError)
|
|
321
299
|
}
|
|
322
300
|
|
|
323
301
|
@MainActor
|
|
@@ -333,5 +311,5 @@ public extension MentraBluetoothSDKDelegate {
|
|
|
333
311
|
func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicLc3 _: MicLc3Event) {}
|
|
334
312
|
func mentraBluetoothSDK(_: MentraBluetoothSDK, didChangeDefaultDevice _: Device?) {}
|
|
335
313
|
func mentraBluetoothSDK(_: MentraBluetoothSDK, didLog _: String) {}
|
|
336
|
-
func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _:
|
|
314
|
+
func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _: BluetoothSdkError) {}
|
|
337
315
|
}
|
|
@@ -49,27 +49,27 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
49
49
|
case .poweredOn:
|
|
50
50
|
return
|
|
51
51
|
case .poweredOff:
|
|
52
|
-
throw
|
|
52
|
+
throw BluetoothSdkError(
|
|
53
53
|
code: "bluetooth_powered_off",
|
|
54
54
|
message: "Turn on phone Bluetooth to \(operation)."
|
|
55
55
|
)
|
|
56
56
|
case .unauthorized:
|
|
57
|
-
throw
|
|
57
|
+
throw BluetoothSdkError(
|
|
58
58
|
code: "bluetooth_unauthorized",
|
|
59
59
|
message: "Allow Bluetooth access to \(operation)."
|
|
60
60
|
)
|
|
61
61
|
case .unsupported:
|
|
62
|
-
throw
|
|
62
|
+
throw BluetoothSdkError(
|
|
63
63
|
code: "bluetooth_unsupported",
|
|
64
64
|
message: "This phone does not support Bluetooth."
|
|
65
65
|
)
|
|
66
66
|
case .resetting, .unknown:
|
|
67
|
-
throw
|
|
67
|
+
throw BluetoothSdkError(
|
|
68
68
|
code: "bluetooth_not_ready",
|
|
69
69
|
message: "Bluetooth is not ready yet. Try again."
|
|
70
70
|
)
|
|
71
71
|
@unknown default:
|
|
72
|
-
throw
|
|
72
|
+
throw BluetoothSdkError(
|
|
73
73
|
code: "bluetooth_unavailable",
|
|
74
74
|
message: "Bluetooth is unavailable. Try again."
|
|
75
75
|
)
|
package/ios/Source/sgcs/G1.swift
CHANGED
|
@@ -785,7 +785,52 @@ class G1: NSObject, SGCManager {
|
|
|
785
785
|
await sendTextWall(text)
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// G1-specific display throttle (300ms, last-wins). G1 firmware can't absorb rapid text-wall
|
|
789
|
+
// updates; coalesce to the latest within a 300ms window. This used to live in the cloud
|
|
790
|
+
// DisplayManager (which fronted G1 before captions moved on-device); it belongs in the G1 SGC
|
|
791
|
+
// because it's a G1 hardware quirk — G2 deliberately does NOT throttle (it must show every
|
|
792
|
+
// caption). The trailing flush always sends the most recent text, so the final caption is never
|
|
793
|
+
// dropped — only intermediate frames within a window are coalesced.
|
|
794
|
+
private var g1TextThrottlePending: String?
|
|
795
|
+
private var g1TextThrottleLastSent: Date = .distantPast
|
|
796
|
+
private var g1TextThrottleScheduled = false
|
|
797
|
+
private let g1TextThrottleWindow: TimeInterval = 0.3
|
|
798
|
+
|
|
799
|
+
/// Drop any pending throttled text-wall flush so it can't later overwrite a newer, non-text
|
|
800
|
+
/// display write (a clear, double-text-wall, or bitmap). The scheduled flush no-ops when
|
|
801
|
+
/// `g1TextThrottlePending` is nil. Called from every G1 display path that bypasses the throttle.
|
|
802
|
+
private func cancelPendingThrottledText() {
|
|
803
|
+
g1TextThrottlePending = nil
|
|
804
|
+
}
|
|
805
|
+
|
|
788
806
|
func sendTextWall(_ text: String) async {
|
|
807
|
+
let now = Date()
|
|
808
|
+
let sinceLast = now.timeIntervalSince(g1TextThrottleLastSent)
|
|
809
|
+
if sinceLast >= g1TextThrottleWindow {
|
|
810
|
+
// Past the window — send now.
|
|
811
|
+
g1TextThrottleLastSent = now
|
|
812
|
+
g1TextThrottlePending = nil
|
|
813
|
+
await flushTextWall(text)
|
|
814
|
+
} else {
|
|
815
|
+
// Inside the window — keep only the latest and schedule one trailing flush.
|
|
816
|
+
g1TextThrottlePending = text
|
|
817
|
+
if !g1TextThrottleScheduled {
|
|
818
|
+
g1TextThrottleScheduled = true
|
|
819
|
+
let wait = g1TextThrottleWindow - sinceLast
|
|
820
|
+
Task { @MainActor [weak self] in
|
|
821
|
+
try? await Task.sleep(nanoseconds: UInt64(wait * 1_000_000_000))
|
|
822
|
+
guard let self = self else { return }
|
|
823
|
+
self.g1TextThrottleScheduled = false
|
|
824
|
+
guard let pending = self.g1TextThrottlePending else { return }
|
|
825
|
+
self.g1TextThrottlePending = nil
|
|
826
|
+
self.g1TextThrottleLastSent = Date()
|
|
827
|
+
await self.flushTextWall(pending)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private func flushTextWall(_ text: String) async {
|
|
789
834
|
let chunks = textHelper.createTextWallChunks(text)
|
|
790
835
|
queueChunks(chunks, sleepAfterMs: 10)
|
|
791
836
|
}
|
|
@@ -799,6 +844,7 @@ class G1: NSObject, SGCManager {
|
|
|
799
844
|
}
|
|
800
845
|
|
|
801
846
|
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
847
|
+
cancelPendingThrottledText() // a newer layout supersedes any pending caption text
|
|
802
848
|
let chunks = textHelper.createDoubleTextWallChunks(textTop: top, textBottom: bottom)
|
|
803
849
|
queueChunks(chunks, sleepAfterMs: 10)
|
|
804
850
|
|
|
@@ -1871,6 +1917,7 @@ extension G1 {
|
|
|
1871
1917
|
// MARK: - Enhanced BMP Display Methods
|
|
1872
1918
|
|
|
1873
1919
|
func displayBitmap(base64ImageData: String, x _: Int32? = nil, y _: Int32? = nil, width _: Int32? = nil, height _: Int32? = nil) async -> Bool {
|
|
1920
|
+
cancelPendingThrottledText() // a bitmap supersedes any pending caption text
|
|
1874
1921
|
guard let bmpData = Data(base64Encoded: base64ImageData) else {
|
|
1875
1922
|
Bridge.log("G1: Failed to decode base64 image data")
|
|
1876
1923
|
return false
|
|
@@ -1885,7 +1932,10 @@ extension G1 {
|
|
|
1885
1932
|
|
|
1886
1933
|
func clearDisplay() {
|
|
1887
1934
|
Bridge.log("G1: clearDisplay() - Using space")
|
|
1888
|
-
|
|
1935
|
+
// Bypass the throttle (a clear must always land) and drop any pending caption so it can't
|
|
1936
|
+
// overwrite the clear after the fact.
|
|
1937
|
+
cancelPendingThrottledText()
|
|
1938
|
+
Task { await flushTextWall(" ") }
|
|
1889
1939
|
}
|
|
1890
1940
|
|
|
1891
1941
|
/// Create a simple test BMP pattern in hex format
|