@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 +67 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum OtaManifestDefaults {
|
|
4
|
+
private static let sdkOtaReleaseBaseUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
5
|
+
// Keep prod as the legacy-device fallback: pre-override ASG builds ignore
|
|
6
|
+
// ota_start.ota_version_url and use their compiled MentraOS default.
|
|
7
|
+
static let prodOtaVersionUrl = "https://ota.mentraglass.com/prod_live_version.json"
|
|
8
|
+
|
|
9
|
+
static func defaultOtaVersionUrl() throws -> String {
|
|
10
|
+
guard let sdkVersion = BluetoothSdkDefaults.sdkVersion,
|
|
11
|
+
!sdkVersion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
12
|
+
else {
|
|
13
|
+
throw BluetoothError(
|
|
14
|
+
code: "missing_sdk_version",
|
|
15
|
+
message: "Cannot determine Bluetooth SDK version for the default OTA manifest URL."
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
return "\(sdkOtaReleaseBaseUrl)/bluetooth-sdk-\(sdkVersion)-version.json"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
struct OtaManifestApp: Decodable {
|
|
23
|
+
let versionCode: Int?
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
struct MtkPatch: Decodable {
|
|
27
|
+
let startFirmware: String
|
|
28
|
+
|
|
29
|
+
enum CodingKeys: String, CodingKey {
|
|
30
|
+
case startFirmware = "start_firmware"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
struct BesFirmware: Decodable {
|
|
35
|
+
let version: String?
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
struct OtaManifest: Decodable {
|
|
39
|
+
let apps: [String: OtaManifestApp]?
|
|
40
|
+
let mtkPatches: [MtkPatch]?
|
|
41
|
+
let besFirmware: BesFirmware?
|
|
42
|
+
let versionCode: Int?
|
|
43
|
+
|
|
44
|
+
enum CodingKeys: String, CodingKey {
|
|
45
|
+
case apps
|
|
46
|
+
case mtkPatches = "mtk_patches"
|
|
47
|
+
case besFirmware = "bes_firmware"
|
|
48
|
+
case versionCode
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
enum OtaManifestChecker {
|
|
53
|
+
private static let asgClientPackage = "com.mentra.asg_client"
|
|
54
|
+
|
|
55
|
+
static func normalizeHttpUrl(_ value: String) throws -> String {
|
|
56
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
57
|
+
guard !trimmed.isEmpty else {
|
|
58
|
+
throw BluetoothError(code: "invalid_ota_url", message: "OTA version URL must be a non-empty http(s) URL.")
|
|
59
|
+
}
|
|
60
|
+
guard let url = URL(string: trimmed),
|
|
61
|
+
let scheme = url.scheme?.lowercased(),
|
|
62
|
+
(scheme == "http" || scheme == "https"),
|
|
63
|
+
url.host?.isEmpty == false
|
|
64
|
+
else {
|
|
65
|
+
throw BluetoothError(code: "invalid_ota_url", message: "OTA version URL must be a valid http(s) URL.")
|
|
66
|
+
}
|
|
67
|
+
return url.absoluteString
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static func fetch(_ otaVersionUrl: String) async throws -> OtaManifest {
|
|
71
|
+
guard let url = URL(string: otaVersionUrl) else {
|
|
72
|
+
throw BluetoothError(code: "invalid_ota_url", message: "OTA version URL must be a valid http(s) URL.")
|
|
73
|
+
}
|
|
74
|
+
let (data, response) = try await URLSession.shared.data(from: url)
|
|
75
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
76
|
+
throw BluetoothError(code: "ota_manifest_request_failed", message: "OTA manifest request failed.")
|
|
77
|
+
}
|
|
78
|
+
guard (200 ... 299).contains(httpResponse.statusCode) else {
|
|
79
|
+
throw BluetoothError(
|
|
80
|
+
code: "ota_manifest_request_failed",
|
|
81
|
+
message: "OTA manifest request failed with HTTP \(httpResponse.statusCode)."
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return try JSONDecoder().decode(OtaManifest.self, from: data)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static func hasUpdate(
|
|
88
|
+
currentBuildNumber: String,
|
|
89
|
+
currentMtkVersion: String,
|
|
90
|
+
currentBesVersion: String,
|
|
91
|
+
manifest: OtaManifest
|
|
92
|
+
) throws -> Bool {
|
|
93
|
+
try hasApkUpdate(currentBuildNumber: currentBuildNumber, manifest: manifest) ||
|
|
94
|
+
hasMtkUpdate(patches: manifest.mtkPatches, currentVersion: currentMtkVersion) ||
|
|
95
|
+
hasBesUpdate(besFirmware: manifest.besFirmware, currentVersion: currentBesVersion)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static func hasMtkPatches(_ manifest: OtaManifest) -> Bool {
|
|
99
|
+
!(manifest.mtkPatches?.isEmpty ?? true)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static func hasBesFirmware(_ manifest: OtaManifest) -> Bool {
|
|
103
|
+
manifest.besFirmware != nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private static func latestAppInfo(_ manifest: OtaManifest) throws -> OtaManifestApp {
|
|
107
|
+
if let app = manifest.apps?[asgClientPackage], app.versionCode != nil {
|
|
108
|
+
return app
|
|
109
|
+
}
|
|
110
|
+
if let versionCode = manifest.versionCode {
|
|
111
|
+
return OtaManifestApp(versionCode: versionCode)
|
|
112
|
+
}
|
|
113
|
+
throw BluetoothError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static func hasApkUpdate(currentBuildNumber: String, manifest: OtaManifest) throws -> Bool {
|
|
117
|
+
guard let currentVersion = Int(currentBuildNumber) else {
|
|
118
|
+
throw BluetoothError(
|
|
119
|
+
code: "invalid_glasses_version",
|
|
120
|
+
message: "Cannot check OTA update because glasses build number is invalid."
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
guard let serverVersion = try latestAppInfo(manifest).versionCode else {
|
|
124
|
+
throw BluetoothError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
125
|
+
}
|
|
126
|
+
return serverVersion > currentVersion
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static func hasMtkUpdate(patches: [MtkPatch]?, currentVersion: String) throws -> Bool {
|
|
130
|
+
guard let patches, !patches.isEmpty else { return false }
|
|
131
|
+
guard !currentVersion.isEmpty else { return false }
|
|
132
|
+
|
|
133
|
+
return patches.contains { patch in
|
|
134
|
+
if patch.startFirmware == currentVersion {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
let serverDate = patch.startFirmware.contains("_")
|
|
138
|
+
? String(patch.startFirmware.split(separator: "_").last ?? "")
|
|
139
|
+
: patch.startFirmware
|
|
140
|
+
return serverDate == currentVersion
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static func hasBesUpdate(besFirmware: BesFirmware?, currentVersion: String) throws -> Bool {
|
|
145
|
+
guard let besFirmware else { return false }
|
|
146
|
+
guard let serverVersion = besFirmware.version, !serverVersion.isEmpty else {
|
|
147
|
+
throw BluetoothError(code: "invalid_ota_manifest", message: "OTA manifest bes_firmware.version is missing.")
|
|
148
|
+
}
|
|
149
|
+
if currentVersion.isEmpty {
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
return compareVersions(serverVersion, currentVersion) > 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private static func compareVersions(_ version1: String, _ version2: String) -> Int {
|
|
156
|
+
if version1.contains("."), version2.contains(".") {
|
|
157
|
+
let parts1 = version1.split(separator: ".").map { Int($0) ?? 0 }
|
|
158
|
+
let parts2 = version2.split(separator: ".").map { Int($0) ?? 0 }
|
|
159
|
+
for index in 0 ..< max(parts1.count, parts2.count) {
|
|
160
|
+
let value1 = index < parts1.count ? parts1[index] : 0
|
|
161
|
+
let value2 = index < parts2.count ? parts2[index] : 0
|
|
162
|
+
if value1 != value2 {
|
|
163
|
+
return value1 - value2
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return 0
|
|
167
|
+
}
|
|
168
|
+
return version1.compare(version2).rawValue
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
public enum PhotoSize: String {
|
|
4
|
-
case
|
|
4
|
+
case low
|
|
5
5
|
case medium
|
|
6
|
-
case
|
|
7
|
-
case
|
|
6
|
+
case high
|
|
7
|
+
case max
|
|
8
|
+
|
|
9
|
+
public static func normalizeLegacy(_ value: String?) -> String {
|
|
10
|
+
switch value {
|
|
11
|
+
case "small":
|
|
12
|
+
return PhotoSize.low.rawValue
|
|
13
|
+
case "large":
|
|
14
|
+
return PhotoSize.high.rawValue
|
|
15
|
+
case "full":
|
|
16
|
+
return PhotoSize.max.rawValue
|
|
17
|
+
default:
|
|
18
|
+
return value ?? PhotoSize.medium.rawValue
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public init(normalizedRawValue value: String?) {
|
|
23
|
+
let normalized = PhotoSize.normalizeLegacy(value)
|
|
24
|
+
self = PhotoSize(rawValue: normalized) ?? .medium
|
|
25
|
+
}
|
|
8
26
|
}
|
|
9
27
|
|
|
10
28
|
public enum ButtonPhotoSize: String {
|
|
11
|
-
case
|
|
29
|
+
case low
|
|
12
30
|
case medium
|
|
13
|
-
case
|
|
31
|
+
case high
|
|
14
32
|
case max
|
|
33
|
+
|
|
34
|
+
public static func normalizeLegacy(_ value: String?) -> String {
|
|
35
|
+
PhotoSize.normalizeLegacy(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public init(normalizedRawValue value: String?) {
|
|
39
|
+
let normalized = ButtonPhotoSize.normalizeLegacy(value)
|
|
40
|
+
self = ButtonPhotoSize(rawValue: normalized) ?? .medium
|
|
41
|
+
}
|
|
15
42
|
}
|
|
16
43
|
|
|
17
44
|
public enum PhotoCompression: String {
|
|
@@ -21,10 +48,66 @@ public enum PhotoCompression: String {
|
|
|
21
48
|
}
|
|
22
49
|
|
|
23
50
|
public struct ButtonPhotoSettings {
|
|
24
|
-
public let size: ButtonPhotoSize
|
|
51
|
+
public let size: ButtonPhotoSize?
|
|
52
|
+
public let mfnr: Bool?
|
|
53
|
+
public let zsl: Bool?
|
|
54
|
+
public let noiseReduction: Bool?
|
|
55
|
+
public let edgeEnhancement: Bool?
|
|
56
|
+
public let ispDigitalGain: Int?
|
|
57
|
+
public let ispAnalogGain: String?
|
|
58
|
+
public let aeExposureDivisor: Int?
|
|
59
|
+
public let isoCap: Int?
|
|
60
|
+
public let compress: String?
|
|
61
|
+
public let sound: Bool?
|
|
62
|
+
public let resetCaptureTuning: Bool?
|
|
25
63
|
|
|
26
|
-
public init(
|
|
64
|
+
public init(
|
|
65
|
+
size: ButtonPhotoSize?,
|
|
66
|
+
mfnr: Bool? = nil,
|
|
67
|
+
zsl: Bool? = nil,
|
|
68
|
+
noiseReduction: Bool? = nil,
|
|
69
|
+
edgeEnhancement: Bool? = nil,
|
|
70
|
+
ispDigitalGain: Int? = nil,
|
|
71
|
+
ispAnalogGain: String? = nil,
|
|
72
|
+
aeExposureDivisor: Int? = nil,
|
|
73
|
+
isoCap: Int? = nil,
|
|
74
|
+
compress: String? = nil,
|
|
75
|
+
sound: Bool? = nil,
|
|
76
|
+
resetCaptureTuning: Bool? = nil
|
|
77
|
+
) {
|
|
27
78
|
self.size = size
|
|
79
|
+
self.mfnr = mfnr
|
|
80
|
+
self.zsl = zsl
|
|
81
|
+
self.noiseReduction = noiseReduction
|
|
82
|
+
self.edgeEnhancement = edgeEnhancement
|
|
83
|
+
self.ispDigitalGain = ispDigitalGain
|
|
84
|
+
self.ispAnalogGain = ispAnalogGain
|
|
85
|
+
self.aeExposureDivisor = aeExposureDivisor
|
|
86
|
+
self.isoCap = isoCap
|
|
87
|
+
self.compress = compress
|
|
88
|
+
self.sound = sound
|
|
89
|
+
self.resetCaptureTuning = resetCaptureTuning
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static func from(params: [String: Any]) -> ButtonPhotoSettings {
|
|
93
|
+
let size = (params["size"] as? String).map { ButtonPhotoSize(normalizedRawValue: $0) }
|
|
94
|
+
let aeExposureDivisor =
|
|
95
|
+
optionalIntValue(params, "aeExposureDivisor").flatMap { $0 > 1 ? $0 : nil }
|
|
96
|
+
let isoCap = optionalIntValue(params, "isoCap").flatMap { $0 > 0 ? $0 : nil }
|
|
97
|
+
return ButtonPhotoSettings(
|
|
98
|
+
size: size,
|
|
99
|
+
mfnr: optionalBoolValue(params, "mfnr"),
|
|
100
|
+
zsl: optionalBoolValue(params, "zsl"),
|
|
101
|
+
noiseReduction: optionalBoolValue(params, "noiseReduction"),
|
|
102
|
+
edgeEnhancement: optionalBoolValue(params, "edgeEnhancement"),
|
|
103
|
+
ispDigitalGain: optionalIntValue(params, "ispDigitalGain"),
|
|
104
|
+
ispAnalogGain: optionalStringValue(params, "ispAnalogGain"),
|
|
105
|
+
aeExposureDivisor: aeExposureDivisor,
|
|
106
|
+
isoCap: isoCap,
|
|
107
|
+
compress: optionalStringValue(params, "compress"),
|
|
108
|
+
sound: optionalBoolValue(params, "sound"),
|
|
109
|
+
resetCaptureTuning: optionalBoolValue(params, "resetCaptureTuning")
|
|
110
|
+
)
|
|
28
111
|
}
|
|
29
112
|
}
|
|
30
113
|
|
|
@@ -152,6 +235,14 @@ public struct PhotoRequest {
|
|
|
152
235
|
public let exposureTimeNs: Double?
|
|
153
236
|
/// Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure.
|
|
154
237
|
public let iso: Int?
|
|
238
|
+
public let aeExposureDivisor: Int?
|
|
239
|
+
public let isoCap: Int?
|
|
240
|
+
public let noiseReduction: Bool?
|
|
241
|
+
public let edgeEnhancement: Bool?
|
|
242
|
+
public let mfnr: Bool?
|
|
243
|
+
public let zsl: Bool?
|
|
244
|
+
public let ispDigitalGain: Int?
|
|
245
|
+
public let ispAnalogGain: String?
|
|
155
246
|
|
|
156
247
|
public init(
|
|
157
248
|
requestId: String,
|
|
@@ -164,7 +255,15 @@ public struct PhotoRequest {
|
|
|
164
255
|
save: Bool = false,
|
|
165
256
|
sound: Bool,
|
|
166
257
|
exposureTimeNs: Double? = nil,
|
|
167
|
-
iso: Int? = nil
|
|
258
|
+
iso: Int? = nil,
|
|
259
|
+
aeExposureDivisor: Int? = nil,
|
|
260
|
+
isoCap: Int? = nil,
|
|
261
|
+
noiseReduction: Bool? = nil,
|
|
262
|
+
edgeEnhancement: Bool? = nil,
|
|
263
|
+
mfnr: Bool? = nil,
|
|
264
|
+
zsl: Bool? = nil,
|
|
265
|
+
ispDigitalGain: Int? = nil,
|
|
266
|
+
ispAnalogGain: String? = nil
|
|
168
267
|
) {
|
|
169
268
|
self.requestId = requestId
|
|
170
269
|
self.appId = appId
|
|
@@ -177,6 +276,117 @@ public struct PhotoRequest {
|
|
|
177
276
|
self.sound = sound
|
|
178
277
|
self.exposureTimeNs = exposureTimeNs
|
|
179
278
|
self.iso = iso
|
|
279
|
+
self.aeExposureDivisor = aeExposureDivisor
|
|
280
|
+
self.isoCap = isoCap
|
|
281
|
+
self.noiseReduction = noiseReduction
|
|
282
|
+
self.edgeEnhancement = edgeEnhancement
|
|
283
|
+
self.mfnr = mfnr
|
|
284
|
+
self.zsl = zsl
|
|
285
|
+
self.ispDigitalGain = ispDigitalGain
|
|
286
|
+
self.ispAnalogGain = ispAnalogGain
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public static func from(params: [String: Any]) -> PhotoRequest {
|
|
290
|
+
let sizeRaw = params["size"] as? String ?? "medium"
|
|
291
|
+
let compressRaw = params["compress"] as? String ?? "none"
|
|
292
|
+
let exposureTimeNs: Double?
|
|
293
|
+
switch params["exposureTimeNs"] {
|
|
294
|
+
case let value as Double:
|
|
295
|
+
exposureTimeNs = value.isFinite && value > 0 ? value : nil
|
|
296
|
+
case let value as Int:
|
|
297
|
+
exposureTimeNs = value > 0 ? Double(value) : nil
|
|
298
|
+
case let value as NSNumber:
|
|
299
|
+
let d = value.doubleValue
|
|
300
|
+
exposureTimeNs = d.isFinite && d > 0 ? d : nil
|
|
301
|
+
default:
|
|
302
|
+
exposureTimeNs = nil
|
|
303
|
+
}
|
|
304
|
+
let iso: Int?
|
|
305
|
+
switch params["iso"] {
|
|
306
|
+
case let value as Int:
|
|
307
|
+
iso = value > 0 ? value : nil
|
|
308
|
+
case let value as Double:
|
|
309
|
+
iso = value.isFinite && value > 0 && value < Double(Int.max) ? Int(value) : nil
|
|
310
|
+
case let value as NSNumber:
|
|
311
|
+
let intValue = value.intValue
|
|
312
|
+
iso = intValue > 0 ? intValue : nil
|
|
313
|
+
default:
|
|
314
|
+
iso = nil
|
|
315
|
+
}
|
|
316
|
+
func optionalInt(_ key: String, min: Int = Int.min, filter: (Int) -> Bool = { _ in true }) -> Int? {
|
|
317
|
+
guard params.keys.contains(key) else { return nil }
|
|
318
|
+
switch params[key] {
|
|
319
|
+
case let value as Int:
|
|
320
|
+
return filter(value) ? value : nil
|
|
321
|
+
case let value as Double:
|
|
322
|
+
guard value.isFinite, value >= Double(min) else { return nil }
|
|
323
|
+
return filter(Int(value)) ? Int(value) : nil
|
|
324
|
+
case let value as NSNumber:
|
|
325
|
+
let intValue = value.intValue
|
|
326
|
+
return filter(intValue) ? intValue : nil
|
|
327
|
+
default:
|
|
328
|
+
return nil
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
func optionalBool(_ key: String) -> Bool? {
|
|
332
|
+
guard params.keys.contains(key) else { return nil }
|
|
333
|
+
return params[key] as? Bool
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return PhotoRequest(
|
|
337
|
+
requestId: params["requestId"] as? String ?? "",
|
|
338
|
+
appId: params["appId"] as? String ?? "",
|
|
339
|
+
size: PhotoSize(normalizedRawValue: sizeRaw),
|
|
340
|
+
webhookUrl: params["webhookUrl"] as? String,
|
|
341
|
+
authToken: (params["authToken"] as? String)?.nilIfBlank,
|
|
342
|
+
compress: PhotoCompression(rawValue: compressRaw),
|
|
343
|
+
flash: params["flash"] as? Bool ?? true,
|
|
344
|
+
save: (params["save"] as? Bool) ?? (params["saveToGallery"] as? Bool) ?? false,
|
|
345
|
+
sound: params["sound"] as? Bool ?? true,
|
|
346
|
+
exposureTimeNs: exposureTimeNs,
|
|
347
|
+
iso: iso,
|
|
348
|
+
aeExposureDivisor: optionalInt("aeExposureDivisor", min: 2) { $0 > 1 },
|
|
349
|
+
isoCap: optionalInt("isoCap", min: 1) { $0 > 0 },
|
|
350
|
+
noiseReduction: optionalBool("noiseReduction"),
|
|
351
|
+
edgeEnhancement: optionalBool("edgeEnhancement"),
|
|
352
|
+
mfnr: optionalBool("mfnr"),
|
|
353
|
+
zsl: optionalBool("zsl"),
|
|
354
|
+
ispDigitalGain: optionalInt("ispDigitalGain"),
|
|
355
|
+
ispAnalogGain: params["ispAnalogGain"] as? String
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
func appendScanFields(to json: inout [String: Any]) {
|
|
360
|
+
if let aeExposureDivisor {
|
|
361
|
+
json["aeExposureDivisor"] = aeExposureDivisor
|
|
362
|
+
}
|
|
363
|
+
if let isoCap {
|
|
364
|
+
json["isoCap"] = isoCap
|
|
365
|
+
}
|
|
366
|
+
if let noiseReduction {
|
|
367
|
+
json["noiseReduction"] = noiseReduction
|
|
368
|
+
}
|
|
369
|
+
if let edgeEnhancement {
|
|
370
|
+
json["edgeEnhancement"] = edgeEnhancement
|
|
371
|
+
}
|
|
372
|
+
if let mfnr {
|
|
373
|
+
json["mfnr"] = mfnr
|
|
374
|
+
}
|
|
375
|
+
if let zsl {
|
|
376
|
+
json["zsl"] = zsl
|
|
377
|
+
}
|
|
378
|
+
if let ispDigitalGain {
|
|
379
|
+
json["ispDigitalGain"] = ispDigitalGain
|
|
380
|
+
}
|
|
381
|
+
if let ispAnalogGain {
|
|
382
|
+
json["ispAnalogGain"] = ispAnalogGain
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private extension String {
|
|
388
|
+
var nilIfBlank: String? {
|
|
389
|
+
isEmpty ? nil : self
|
|
180
390
|
}
|
|
181
391
|
}
|
|
182
392
|
|
|
@@ -229,9 +439,12 @@ public struct VideoRecordingRequest {
|
|
|
229
439
|
public let width: Int
|
|
230
440
|
public let height: Int
|
|
231
441
|
public let fps: Int
|
|
442
|
+
// Optional auto-stop timer in minutes; 0 = record until stopped/interrupted.
|
|
443
|
+
public let maxRecordingTimeMinutes: Int
|
|
232
444
|
|
|
233
445
|
public init(
|
|
234
|
-
requestId: String, save: Bool, sound: Bool, width: Int = 0, height: Int = 0, fps: Int = 0
|
|
446
|
+
requestId: String, save: Bool, sound: Bool, width: Int = 0, height: Int = 0, fps: Int = 0,
|
|
447
|
+
maxRecordingTimeMinutes: Int = 0
|
|
235
448
|
) {
|
|
236
449
|
self.requestId = requestId
|
|
237
450
|
self.save = save
|
|
@@ -239,6 +452,7 @@ public struct VideoRecordingRequest {
|
|
|
239
452
|
self.width = width
|
|
240
453
|
self.height = height
|
|
241
454
|
self.fps = fps
|
|
455
|
+
self.maxRecordingTimeMinutes = maxRecordingTimeMinutes
|
|
242
456
|
}
|
|
243
457
|
}
|
|
244
458
|
|
|
@@ -280,6 +494,50 @@ public struct VideoRecordingStatusEvent: CustomStringConvertible {
|
|
|
280
494
|
}
|
|
281
495
|
}
|
|
282
496
|
|
|
497
|
+
public struct MediaUploadEvent: CustomStringConvertible {
|
|
498
|
+
public let values: [String: Any]
|
|
499
|
+
|
|
500
|
+
public init(values: [String: Any]) {
|
|
501
|
+
self.values = values
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
public var type: String {
|
|
505
|
+
stringValue(values, "type") ?? ""
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
public var requestId: String {
|
|
509
|
+
stringValue(values, "requestId") ?? ""
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public var mediaUrl: String? {
|
|
513
|
+
stringValue(values, "mediaUrl")
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public var errorMessage: String? {
|
|
517
|
+
stringValue(values, "errorMessage")
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public var mediaType: Int? {
|
|
521
|
+
intValue(values["mediaType"])
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
public var timestamp: Int {
|
|
525
|
+
intValue(values["timestamp"]) ?? Int(Date().timeIntervalSince1970 * 1000)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public var isSuccess: Bool {
|
|
529
|
+
type == "media_success"
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public var isVideo: Bool {
|
|
533
|
+
mediaType == 2
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public var description: String {
|
|
537
|
+
"MediaUploadEvent(requestId: \(requestId), type: \(type), mediaType: \(mediaType ?? -1))"
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
283
541
|
public enum PhotoResponse: CustomStringConvertible, Equatable {
|
|
284
542
|
public enum State: String {
|
|
285
543
|
case success
|
|
@@ -18,10 +18,7 @@ protocol ControllerManager {
|
|
|
18
18
|
|
|
19
19
|
// MARK: - Camera & Media
|
|
20
20
|
|
|
21
|
-
func requestPhoto(
|
|
22
|
-
_ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
|
|
23
|
-
compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
24
|
-
)
|
|
21
|
+
func requestPhoto(_ request: PhotoRequest)
|
|
25
22
|
func startStream(_ message: [String: Any])
|
|
26
23
|
func stopStream()
|
|
27
24
|
func sendStreamKeepAlive(_ message: [String: Any])
|
|
@@ -75,7 +72,7 @@ protocol ControllerManager {
|
|
|
75
72
|
func sendWifiCredentials(_ ssid: String, _ password: String)
|
|
76
73
|
func forgetWifiNetwork(_ ssid: String)
|
|
77
74
|
func sendHotspotState(_ enabled: Bool)
|
|
78
|
-
func sendOtaStart()
|
|
75
|
+
func sendOtaStart(otaVersionUrl: String?)
|
|
79
76
|
func sendOtaQueryStatus()
|
|
80
77
|
|
|
81
78
|
// MARK: - User Context (for crash reporting)
|
|
@@ -504,10 +504,7 @@ class R1: NSObject, ControllerManager {
|
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
507
|
-
func requestPhoto(
|
|
508
|
-
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
509
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
510
|
-
) {}
|
|
507
|
+
func requestPhoto(_: PhotoRequest) {}
|
|
511
508
|
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
512
509
|
func stopVideoRecording(requestId _: String) {}
|
|
513
510
|
func startStream(_: [String: Any]) {}
|
|
@@ -543,7 +540,7 @@ class R1: NSObject, ControllerManager {
|
|
|
543
540
|
func sendWifiCredentials(_: String, _: String) {}
|
|
544
541
|
func forgetWifiNetwork(_: String) {}
|
|
545
542
|
func sendHotspotState(_: Bool) {}
|
|
546
|
-
func sendOtaStart() {}
|
|
543
|
+
func sendOtaStart(otaVersionUrl: String?) {}
|
|
547
544
|
func sendOtaQueryStatus() {}
|
|
548
545
|
func sendUserEmailToGlasses(_: String) {}
|
|
549
546
|
func queryGalleryStatus() {}
|
|
@@ -244,6 +244,7 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
244
244
|
case photoResponse(PhotoResponseEvent)
|
|
245
245
|
case photoStatus(PhotoStatusEvent)
|
|
246
246
|
case videoRecordingStatus(VideoRecordingStatusEvent)
|
|
247
|
+
case mediaUpload(MediaUploadEvent)
|
|
247
248
|
case rgbLedControlResponse(RgbLedControlResponseEvent)
|
|
248
249
|
case streamStatus(StreamStatusEvent)
|
|
249
250
|
case keepAliveAck(KeepAliveAckEvent)
|
|
@@ -277,6 +278,8 @@ public enum BluetoothEvent: CustomStringConvertible {
|
|
|
277
278
|
event.description
|
|
278
279
|
case let .videoRecordingStatus(event):
|
|
279
280
|
event.description
|
|
281
|
+
case let .mediaUpload(event):
|
|
282
|
+
event.description
|
|
280
283
|
case let .rgbLedControlResponse(event):
|
|
281
284
|
event.description
|
|
282
285
|
case let .streamStatus(event):
|
|
@@ -6,6 +6,7 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
6
6
|
|
|
7
7
|
private var centralManager: CBCentralManager?
|
|
8
8
|
private var state: CBManagerState = .unknown
|
|
9
|
+
private var listeners: [UUID: (CBManagerState) -> Void] = [:]
|
|
9
10
|
|
|
10
11
|
override private init() {
|
|
11
12
|
super.init()
|
|
@@ -19,6 +20,25 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
19
20
|
|
|
20
21
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
21
22
|
state = central.state
|
|
23
|
+
for listener in Array(listeners.values) {
|
|
24
|
+
listener(state)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@discardableResult
|
|
29
|
+
func addStateListener(_ listener: @escaping (CBManagerState) -> Void) -> UUID {
|
|
30
|
+
// `listeners` is unsynchronized; it is only safe because the central
|
|
31
|
+
// manager queue is `.main` and all callers are main-thread bound.
|
|
32
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
33
|
+
let id = UUID()
|
|
34
|
+
listeners[id] = listener
|
|
35
|
+
listener(state)
|
|
36
|
+
return id
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func removeStateListener(_ id: UUID) {
|
|
40
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
41
|
+
listeners[id] = nil
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
func requirePoweredOn(operation: String) throws {
|