@mentra/bluetooth-sdk 3.3.0-dev.511 → 3.3.0-dev.514
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/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +13 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/MentraLive.swift +16 -145
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "3.3.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.3.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.3.0-dev.514"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.514"
|
|
8
|
+
const val SOURCE_COMMIT: String = "c08afa1e430e63dffd758478435e8d59940d36c7"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.514.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "24a37a482d08ef1a11ff5c96280c641150601b255e336f7723dbf88cb598337e"
|
|
11
11
|
}
|
|
@@ -777,7 +777,8 @@ class MentraLive : SGCManager() {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// Inner class to track incoming file transfers
|
|
780
|
-
|
|
780
|
+
internal class FileTransferSession(var fileName: String, var fileSize: Int, flags: Int = 0) {
|
|
781
|
+
private val dynamicPayload = (flags and 0x0002) != 0
|
|
781
782
|
// NOTE: fileSize may be "fake" (inflated) due to BES firmware workaround
|
|
782
783
|
var actualPackSize =
|
|
783
784
|
0 // Actual pack size from first received packet (for BES lie detection)
|
|
@@ -806,9 +807,8 @@ class MentraLive : SGCManager() {
|
|
|
806
807
|
* Recalculate total packets based on actual pack size from received packet. Called when
|
|
807
808
|
* first packet is received to handle variable pack sizes.
|
|
808
809
|
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* so, totalPackets = fileSize / 400, regardless of actual pack size.
|
|
810
|
+
* Dynamic-payload headers carry the true byte count. Only legacy small packs use
|
|
811
|
+
* the inflated-size workaround (multiples of BES_HARDCODED_PACK_SIZE).
|
|
812
812
|
*/
|
|
813
813
|
fun recalculateTotalPackets(actualPackSize: Int) {
|
|
814
814
|
if (actualPackSize <= 0 || actualPackSize > K900ProtocolUtils.FILE_PACK_SIZE) {
|
|
@@ -819,8 +819,8 @@ class MentraLive : SGCManager() {
|
|
|
819
819
|
|
|
820
820
|
// Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
|
|
821
821
|
val isBesLie =
|
|
822
|
-
(fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
|
|
823
|
-
(actualPackSize
|
|
822
|
+
!dynamicPayload && (fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
|
|
823
|
+
(actualPackSize < BES_HARDCODED_PACK_SIZE)
|
|
824
824
|
|
|
825
825
|
val newTotalPackets: Int
|
|
826
826
|
if (isBesLie) {
|
|
@@ -858,6 +858,10 @@ class MentraLive : SGCManager() {
|
|
|
858
858
|
}
|
|
859
859
|
|
|
860
860
|
fun addPacket(index: Int, data: ByteArray): Boolean {
|
|
861
|
+
if (dynamicPayload && (index < 0 || index >= totalPackets || actualPackSize <= 0 ||
|
|
862
|
+
data.size != minOf(actualPackSize, fileSize - index * actualPackSize))) {
|
|
863
|
+
return false
|
|
864
|
+
}
|
|
861
865
|
if (index >= 0 && index < totalPackets && !receivedPackets.containsKey(index)) {
|
|
862
866
|
receivedPackets[index] = data
|
|
863
867
|
|
|
@@ -10235,7 +10239,7 @@ class MentraLive : SGCManager() {
|
|
|
10235
10239
|
if (incidentRelay.session == null) {
|
|
10236
10240
|
activeFileTransfers.remove(packetInfo.fileName)
|
|
10237
10241
|
incidentRelay.session =
|
|
10238
|
-
FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10242
|
+
FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10239
10243
|
incidentRelay.session!!.recalculateTotalPackets(packetInfo.packSize)
|
|
10240
10244
|
Bridge.log(
|
|
10241
10245
|
"LIVE: 📦 Started BLE incident log transfer: " +
|
|
@@ -10286,7 +10290,7 @@ class MentraLive : SGCManager() {
|
|
|
10286
10290
|
// Get or create session for this transfer
|
|
10287
10291
|
if (photoTransfer.session == null) {
|
|
10288
10292
|
photoTransfer.session =
|
|
10289
|
-
FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10293
|
+
FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10290
10294
|
// Recalculate total packets based on actual pack size (handles variable MTU)
|
|
10291
10295
|
photoTransfer.session!!.recalculateTotalPackets(packetInfo.packSize)
|
|
10292
10296
|
Bridge.log(
|
|
@@ -10372,7 +10376,7 @@ class MentraLive : SGCManager() {
|
|
|
10372
10376
|
var session = activeFileTransfers[packetInfo.fileName]
|
|
10373
10377
|
if (session == null) {
|
|
10374
10378
|
// New file transfer
|
|
10375
|
-
session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10379
|
+
session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10376
10380
|
// Recalculate total packets based on actual pack size (handles variable MTU)
|
|
10377
10381
|
session.recalculateTotalPackets(packetInfo.packSize)
|
|
10378
10382
|
activeFileTransfers[packetInfo.fileName] = session
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
import org.junit.runner.RunWith
|
|
6
|
+
import org.robolectric.RobolectricTestRunner
|
|
7
|
+
import org.robolectric.annotation.Config
|
|
8
|
+
|
|
9
|
+
@RunWith(RobolectricTestRunner::class)
|
|
10
|
+
@Config(sdk = [33])
|
|
11
|
+
class MentraLiveFileTransferSessionTest {
|
|
12
|
+
@Test fun dynamicPayloadUsesTrueSizeEvenWhenDivisibleBy400() {
|
|
13
|
+
for (pack in listOf(244, 474, 800)) {
|
|
14
|
+
val session = MentraLive.FileTransferSession("photo", 80000, 2)
|
|
15
|
+
session.recalculateTotalPackets(pack)
|
|
16
|
+
assertEquals((80000 + pack - 1) / pack, session.totalPackets)
|
|
17
|
+
for (index in 0 until session.totalPackets) {
|
|
18
|
+
assertTrue(session.addPacket(index, ByteArray(minOf(pack, 80000 - index * pack))))
|
|
19
|
+
}
|
|
20
|
+
assertTrue(session.isComplete)
|
|
21
|
+
assertEquals(80000, session.assembleFile()!!.size)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test fun legacyInflatedSizeAndLargeUnflaggedPayloads() {
|
|
26
|
+
// Flag 1 enables batched ACKs but still uses legacy size semantics.
|
|
27
|
+
for ((pack, headerSize, actualSize, packets) in listOf(
|
|
28
|
+
listOf(221, 80000, 43982, 200),
|
|
29
|
+
listOf(400, 80000, 79777, 200),
|
|
30
|
+
listOf(800, 80000, 80000, 100))) {
|
|
31
|
+
val session = MentraLive.FileTransferSession("photo", headerSize, 1)
|
|
32
|
+
session.recalculateTotalPackets(pack)
|
|
33
|
+
assertEquals(packets, session.totalPackets)
|
|
34
|
+
val source = ByteArray(actualSize) { (it % 251).toByte() }
|
|
35
|
+
for (index in (0 until packets).reversed()) {
|
|
36
|
+
val start = index * pack
|
|
37
|
+
assertTrue(session.addPacket(index, source.copyOfRange(start, minOf(start + pack, actualSize))))
|
|
38
|
+
}
|
|
39
|
+
assertTrue(session.isComplete)
|
|
40
|
+
assertArrayEquals(source, session.assembleFile())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test fun malformedOrDuplicatePacketsCannotCompleteFile() {
|
|
45
|
+
val session = MentraLive.FileTransferSession("photo", 1000, 2)
|
|
46
|
+
session.recalculateTotalPackets(800)
|
|
47
|
+
assertFalse(session.addPacket(0, ByteArray(799)))
|
|
48
|
+
assertFalse(session.addPacket(2, ByteArray(200)))
|
|
49
|
+
assertTrue(session.addPacket(1, ByteArray(200)))
|
|
50
|
+
assertFalse(session.isComplete)
|
|
51
|
+
assertFalse(session.addPacket(1, ByteArray(200)))
|
|
52
|
+
assertTrue(session.addPacket(0, ByteArray(800)))
|
|
53
|
+
assertEquals(1000, session.assembleFile()!!.size)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.3.0",
|
|
4
|
-
"releaseIdentity": "3.3.0-dev.
|
|
5
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.3.0-dev.514",
|
|
5
|
+
"releaseSetId": "mentra-3.3.0-dev.514",
|
|
6
|
+
"sourceCommit": "c08afa1e430e63dffd758478435e8d59940d36c7",
|
|
7
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.514.json",
|
|
8
|
+
"otaManifestSha256": "24a37a482d08ef1a11ff5c96280c641150601b255e336f7723dbf88cb598337e"
|
|
9
9
|
});
|
|
10
10
|
//# sourceMappingURL=releaseMetadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.514\",\n \"releaseSetId\": \"mentra-3.3.0-dev.514\",\n \"sourceCommit\": \"c08afa1e430e63dffd758478435e8d59940d36c7\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.514.json\",\n \"otaManifestSha256\": \"24a37a482d08ef1a11ff5c96280c641150601b255e336f7723dbf88cb598337e\"\n})\n"]}
|
|
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
|
|
|
16
16
|
/// silently disable it on every pair the app touches.
|
|
17
17
|
static let autoPowerOffEnabled = false
|
|
18
18
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
19
|
-
private static let swiftPackageSdkVersion = "3.3.0-dev.
|
|
19
|
+
private static let swiftPackageSdkVersion = "3.3.0-dev.514"
|
|
20
20
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
21
21
|
|
|
22
22
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.3.0"
|
|
6
|
-
static let releaseIdentity = "3.3.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.3.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.3.0-dev.514"
|
|
7
|
+
static let releaseSetId = "mentra-3.3.0-dev.514"
|
|
8
|
+
static let sourceCommit = "c08afa1e430e63dffd758478435e8d59940d36c7"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.514.json"
|
|
10
|
+
static let otaManifestSha256 = "24a37a482d08ef1a11ff5c96280c641150601b255e336f7723dbf88cb598337e"
|
|
11
11
|
}
|
|
@@ -677,145 +677,13 @@ enum K900ProtocolUtils {
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
private struct FileTransferSession {
|
|
681
|
-
let fileName: String
|
|
682
|
-
let fileSize: Int // NOTE: May be "fake" (inflated) due to BES firmware workaround
|
|
683
|
-
var actualPackSize: Int = 0 // Actual pack size from first received packet
|
|
684
|
-
var totalPackets: Int
|
|
685
|
-
var expectedNextPacket: Int = 0
|
|
686
|
-
var receivedPackets: [Int: Data] = [:]
|
|
687
|
-
let startTime: Date
|
|
688
|
-
var isComplete: Bool = false
|
|
689
|
-
var isAnnounced: Bool = false
|
|
690
|
-
|
|
691
|
-
/// BES2700 firmware hardcodes FILE_PACK_SIZE=400 when calculating totalPack.
|
|
692
|
-
/// Android glasses "lie" about fileSize to make BES expect correct packet count.
|
|
693
|
-
private static let BES_HARDCODED_PACK_SIZE = 400
|
|
694
|
-
|
|
695
|
-
init(fileName: String, fileSize: Int, announcedPackets: Int? = nil) {
|
|
696
|
-
self.fileName = fileName
|
|
697
|
-
self.fileSize = fileSize
|
|
698
|
-
let computedPackets =
|
|
699
|
-
(fileSize + K900ProtocolUtils.FILE_PACK_SIZE - 1) / K900ProtocolUtils.FILE_PACK_SIZE
|
|
700
|
-
if let announced = announcedPackets, announced > 0 {
|
|
701
|
-
totalPackets = announced
|
|
702
|
-
isAnnounced = true
|
|
703
|
-
} else {
|
|
704
|
-
totalPackets = computedPackets
|
|
705
|
-
isAnnounced = false
|
|
706
|
-
}
|
|
707
|
-
startTime = Date()
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
mutating func updateAnnouncedPackets(_ announced: Int) {
|
|
711
|
-
guard announced > 0 else { return }
|
|
712
|
-
totalPackets = announced
|
|
713
|
-
isAnnounced = true
|
|
714
|
-
if expectedNextPacket >= totalPackets {
|
|
715
|
-
expectedNextPacket = min(expectedNextPacket, max(totalPackets - 1, 0))
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/// Recalculate total packets based on actual pack size from received packet.
|
|
720
|
-
/// Detects BES lie: if fileSize is multiple of 400 but actual pack size differs.
|
|
721
|
-
mutating func recalculateTotalPackets(actualPackSize: Int) {
|
|
722
|
-
guard actualPackSize > 0, actualPackSize <= K900ProtocolUtils.FILE_PACK_SIZE else { return }
|
|
723
|
-
|
|
724
|
-
self.actualPackSize = actualPackSize
|
|
725
|
-
|
|
726
|
-
// Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
|
|
727
|
-
let isBesLie =
|
|
728
|
-
(fileSize % Self.BES_HARDCODED_PACK_SIZE == 0)
|
|
729
|
-
&& (actualPackSize != Self.BES_HARDCODED_PACK_SIZE)
|
|
730
|
-
|
|
731
|
-
let newTotalPackets: Int
|
|
732
|
-
if isBesLie {
|
|
733
|
-
// BES lie detected: totalPackets = fileSize / 400
|
|
734
|
-
newTotalPackets = fileSize / Self.BES_HARDCODED_PACK_SIZE
|
|
735
|
-
Bridge.log(
|
|
736
|
-
"📦 BES Lie detected! fakeFileSize=\(fileSize), totalPackets=\(newTotalPackets), actualPackSize=\(actualPackSize)"
|
|
737
|
-
)
|
|
738
|
-
} else {
|
|
739
|
-
// Normal case: calculate based on actual pack size
|
|
740
|
-
newTotalPackets = (fileSize + actualPackSize - 1) / actualPackSize
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if newTotalPackets != totalPackets {
|
|
744
|
-
Bridge.log(
|
|
745
|
-
"📦 Recalculating totalPackets: \(totalPackets) -> \(newTotalPackets) (packSize=\(actualPackSize), fileSize=\(fileSize))"
|
|
746
|
-
)
|
|
747
|
-
totalPackets = newTotalPackets
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
mutating func addPacket(_ index: Int, data: Data) -> Bool {
|
|
752
|
-
guard index >= 0 else { return false }
|
|
753
|
-
|
|
754
|
-
// On first packet, recalculate total packets only when we do not already
|
|
755
|
-
// have an authoritative pack size from protocol metadata.
|
|
756
|
-
if receivedPackets.isEmpty && actualPackSize == 0 && !data.isEmpty {
|
|
757
|
-
recalculateTotalPackets(actualPackSize: data.count)
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if index >= totalPackets {
|
|
761
|
-
totalPackets = index + 1
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
guard receivedPackets[index] == nil else {
|
|
765
|
-
return false
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
receivedPackets[index] = data
|
|
769
|
-
|
|
770
|
-
while receivedPackets[expectedNextPacket] != nil, expectedNextPacket < totalPackets {
|
|
771
|
-
expectedNextPacket += 1
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
isComplete = (receivedPackets.count == totalPackets)
|
|
775
|
-
return true
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
func isFinalPacket(_ index: Int) -> Bool {
|
|
779
|
-
index == totalPackets - 1
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
func missingPacketIndices() -> [Int] {
|
|
783
|
-
guard totalPackets > receivedPackets.count else { return [] }
|
|
784
|
-
return (0 ..< totalPackets).compactMap { receivedPackets[$0] == nil ? $0 : nil }
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/// Assemble file from received packets.
|
|
788
|
-
/// NOTE: Calculates actual file size from received data, NOT from header fileSize,
|
|
789
|
-
/// because fileSize may be "fake" (inflated) due to BES firmware workaround.
|
|
790
|
-
func assembleFile() -> Data? {
|
|
791
|
-
guard isComplete else { return nil }
|
|
792
|
-
|
|
793
|
-
// Calculate actual file size by summing all received packet sizes
|
|
794
|
-
let actualFileSize = receivedPackets.values.reduce(0) { $0 + $1.count }
|
|
795
|
-
|
|
796
|
-
Bridge.log(
|
|
797
|
-
"📦 Assembling file: headerFileSize=\(fileSize), actualFileSize=\(actualFileSize), totalPackets=\(totalPackets)"
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
var fileData = Data(capacity: actualFileSize)
|
|
801
|
-
|
|
802
|
-
for i in 0 ..< totalPackets {
|
|
803
|
-
if let packet = receivedPackets[i] {
|
|
804
|
-
fileData.append(packet)
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
return fileData
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
680
|
private struct BlePhotoTransfer {
|
|
813
681
|
var isThumbnail = false
|
|
814
682
|
let bleImgId: String
|
|
815
683
|
let requestId: String
|
|
816
684
|
let webhookUrl: String
|
|
817
685
|
var authToken: String?
|
|
818
|
-
var session:
|
|
686
|
+
var session: MentraLiveFileTransferSession?
|
|
819
687
|
let phoneStartTime: Date
|
|
820
688
|
var bleTransferStartTime: Date?
|
|
821
689
|
var glassesCompressionDurationMs: Int64 = 0
|
|
@@ -838,7 +706,7 @@ private final class BleIncidentLogRelayEntry {
|
|
|
838
706
|
let incidentId: String
|
|
839
707
|
let apiBaseUrl: String
|
|
840
708
|
let kind: BleIncidentLogRelayKind
|
|
841
|
-
var session:
|
|
709
|
+
var session: MentraLiveFileTransferSession?
|
|
842
710
|
|
|
843
711
|
init(
|
|
844
712
|
fileBaseKey: String, incidentId: String, apiBaseUrl: String, kind: BleIncidentLogRelayKind
|
|
@@ -1602,7 +1470,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1602
1470
|
// NEW: File transfer properties
|
|
1603
1471
|
private var fileReadCharacteristic: CBCharacteristic?
|
|
1604
1472
|
private var fileWriteCharacteristic: CBCharacteristic?
|
|
1605
|
-
private var activeFileTransfers = [String:
|
|
1473
|
+
private var activeFileTransfers = [String: MentraLiveFileTransferSession]()
|
|
1606
1474
|
private var blePhotoTransfers = [String: BlePhotoTransfer]()
|
|
1607
1475
|
private var bleIncidentLogRelays = [String: BleIncidentLogRelayEntry]()
|
|
1608
1476
|
private var l2capFileChannel: MentraLiveL2capChannel?
|
|
@@ -2639,7 +2507,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2639
2507
|
// The reader thread keeps draining the stream (and returning CoC credits)
|
|
2640
2508
|
// while the existing transfer state remains serialized on the main actor.
|
|
2641
2509
|
DispatchQueue.main.async {
|
|
2642
|
-
self
|
|
2510
|
+
guard let self, self.l2capFileChannelId == channelId else { return }
|
|
2511
|
+
self.processReceivedData(frame)
|
|
2643
2512
|
}
|
|
2644
2513
|
},
|
|
2645
2514
|
onClose: { [weak self] in
|
|
@@ -2666,7 +2535,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2666
2535
|
|
|
2667
2536
|
// MARK: - Data Processing
|
|
2668
2537
|
|
|
2669
|
-
|
|
2538
|
+
/// Internal so native integration tests can replay the actual BLE receive path.
|
|
2670
2539
|
func processReceivedData(_ data: Data) {
|
|
2671
2540
|
guard data.count > 0 else { return }
|
|
2672
2541
|
|
|
@@ -4161,7 +4030,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4161
4030
|
activeFileTransfers.removeValue(forKey: fileName)
|
|
4162
4031
|
}
|
|
4163
4032
|
|
|
4164
|
-
var session =
|
|
4033
|
+
var session = MentraLiveFileTransferSession(
|
|
4165
4034
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4166
4035
|
)
|
|
4167
4036
|
session.isAnnounced = true
|
|
@@ -4171,7 +4040,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4171
4040
|
if var bleTransfer = blePhotoTransfers[bleImgId] {
|
|
4172
4041
|
var bleSession =
|
|
4173
4042
|
bleTransfer.session
|
|
4174
|
-
??
|
|
4043
|
+
?? MentraLiveFileTransferSession(
|
|
4175
4044
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4176
4045
|
)
|
|
4177
4046
|
bleSession.updateAnnouncedPackets(totalPackets)
|
|
@@ -4264,8 +4133,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4264
4133
|
|
|
4265
4134
|
if incidentRelay.session == nil {
|
|
4266
4135
|
activeFileTransfers.removeValue(forKey: packetInfo.fileName)
|
|
4267
|
-
var session =
|
|
4268
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4136
|
+
var session = MentraLiveFileTransferSession(
|
|
4137
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4269
4138
|
)
|
|
4270
4139
|
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4271
4140
|
incidentRelay.session = session
|
|
@@ -4316,10 +4185,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4316
4185
|
|
|
4317
4186
|
// Get or create session for this transfer
|
|
4318
4187
|
if photoTransfer.session == nil {
|
|
4319
|
-
var session =
|
|
4188
|
+
var session = MentraLiveFileTransferSession(
|
|
4320
4189
|
fileName: packetInfo.fileName,
|
|
4321
|
-
fileSize: Int(packetInfo.fileSize)
|
|
4190
|
+
fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4322
4191
|
)
|
|
4192
|
+
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4323
4193
|
photoTransfer.session = session
|
|
4324
4194
|
blePhotoTransfers[bleImgId] = photoTransfer
|
|
4325
4195
|
Bridge.log(
|
|
@@ -4392,9 +4262,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4392
4262
|
var session = activeFileTransfers[packetInfo.fileName]
|
|
4393
4263
|
if session == nil {
|
|
4394
4264
|
// New file transfer
|
|
4395
|
-
session =
|
|
4396
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4265
|
+
session = MentraLiveFileTransferSession(
|
|
4266
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4397
4267
|
)
|
|
4268
|
+
session?.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4398
4269
|
activeFileTransfers[packetInfo.fileName] = session
|
|
4399
4270
|
|
|
4400
4271
|
Bridge.log(
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct MentraLiveFileTransferSession {
|
|
4
|
+
let fileName: String
|
|
5
|
+
let fileSize: Int // NOTE: May be "fake" (inflated) due to BES firmware workaround
|
|
6
|
+
let dynamicPayload: Bool
|
|
7
|
+
var actualPackSize: Int = 0 // Actual pack size from first received packet
|
|
8
|
+
var totalPackets: Int
|
|
9
|
+
var expectedNextPacket: Int = 0
|
|
10
|
+
var receivedPackets: [Int: Data] = [:]
|
|
11
|
+
let startTime: Date
|
|
12
|
+
var isComplete: Bool = false
|
|
13
|
+
var isAnnounced: Bool = false
|
|
14
|
+
|
|
15
|
+
/// BES2700 firmware hardcodes FILE_PACK_SIZE=400 when calculating totalPack.
|
|
16
|
+
/// Android glasses "lie" about fileSize to make BES expect correct packet count.
|
|
17
|
+
private static let BES_HARDCODED_PACK_SIZE = 400
|
|
18
|
+
|
|
19
|
+
init(fileName: String, fileSize: Int, announcedPackets: Int? = nil, flags: UInt16 = 0) {
|
|
20
|
+
self.fileName = fileName
|
|
21
|
+
self.fileSize = fileSize
|
|
22
|
+
dynamicPayload = (flags & 0x0002) != 0
|
|
23
|
+
let computedPackets =
|
|
24
|
+
(fileSize + K900ProtocolUtils.FILE_PACK_SIZE - 1) / K900ProtocolUtils.FILE_PACK_SIZE
|
|
25
|
+
if let announced = announcedPackets, announced > 0 {
|
|
26
|
+
totalPackets = announced
|
|
27
|
+
isAnnounced = true
|
|
28
|
+
} else {
|
|
29
|
+
totalPackets = computedPackets
|
|
30
|
+
isAnnounced = false
|
|
31
|
+
}
|
|
32
|
+
startTime = Date()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
mutating func updateAnnouncedPackets(_ announced: Int) {
|
|
36
|
+
guard announced > 0 else { return }
|
|
37
|
+
totalPackets = announced
|
|
38
|
+
isAnnounced = true
|
|
39
|
+
if expectedNextPacket >= totalPackets {
|
|
40
|
+
expectedNextPacket = min(expectedNextPacket, max(totalPackets - 1, 0))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Recalculate total packets based on actual pack size from received packet.
|
|
45
|
+
/// Dynamic headers carry the true byte count. Only legacy small packs use inflated sizes.
|
|
46
|
+
mutating func recalculateTotalPackets(actualPackSize: Int) {
|
|
47
|
+
guard actualPackSize > 0, actualPackSize <= K900ProtocolUtils.FILE_PACK_SIZE else { return }
|
|
48
|
+
|
|
49
|
+
self.actualPackSize = actualPackSize
|
|
50
|
+
|
|
51
|
+
// Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
|
|
52
|
+
let isBesLie = !dynamicPayload
|
|
53
|
+
&& (fileSize % Self.BES_HARDCODED_PACK_SIZE == 0)
|
|
54
|
+
&& (actualPackSize < Self.BES_HARDCODED_PACK_SIZE)
|
|
55
|
+
|
|
56
|
+
let newTotalPackets: Int
|
|
57
|
+
if isBesLie {
|
|
58
|
+
// BES lie detected: totalPackets = fileSize / 400
|
|
59
|
+
newTotalPackets = fileSize / Self.BES_HARDCODED_PACK_SIZE
|
|
60
|
+
Bridge.log(
|
|
61
|
+
"📦 BES Lie detected! fakeFileSize=\(fileSize), totalPackets=\(newTotalPackets), actualPackSize=\(actualPackSize)"
|
|
62
|
+
)
|
|
63
|
+
} else {
|
|
64
|
+
// Normal case: calculate based on actual pack size
|
|
65
|
+
newTotalPackets = (fileSize + actualPackSize - 1) / actualPackSize
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if newTotalPackets != totalPackets {
|
|
69
|
+
Bridge.log(
|
|
70
|
+
"📦 Recalculating totalPackets: \(totalPackets) -> \(newTotalPackets) (packSize=\(actualPackSize), fileSize=\(fileSize))"
|
|
71
|
+
)
|
|
72
|
+
totalPackets = newTotalPackets
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
mutating func addPacket(_ index: Int, data: Data) -> Bool {
|
|
77
|
+
guard index >= 0 else { return false }
|
|
78
|
+
|
|
79
|
+
// On first packet, recalculate total packets only when we do not already
|
|
80
|
+
// have an authoritative pack size from protocol metadata.
|
|
81
|
+
if receivedPackets.isEmpty && actualPackSize == 0 && !data.isEmpty {
|
|
82
|
+
recalculateTotalPackets(actualPackSize: data.count)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
guard index < totalPackets else { return false }
|
|
86
|
+
if dynamicPayload {
|
|
87
|
+
let expectedSize = min(actualPackSize, fileSize - index * actualPackSize)
|
|
88
|
+
guard expectedSize > 0, data.count == expectedSize else { return false }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
guard receivedPackets[index] == nil else {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
receivedPackets[index] = data
|
|
96
|
+
|
|
97
|
+
while receivedPackets[expectedNextPacket] != nil, expectedNextPacket < totalPackets {
|
|
98
|
+
expectedNextPacket += 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isComplete = (receivedPackets.count == totalPackets)
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func isFinalPacket(_ index: Int) -> Bool {
|
|
106
|
+
index == totalPackets - 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func missingPacketIndices() -> [Int] {
|
|
110
|
+
guard totalPackets > receivedPackets.count else { return [] }
|
|
111
|
+
return (0 ..< totalPackets).compactMap { receivedPackets[$0] == nil ? $0 : nil }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Assemble file from received packets.
|
|
115
|
+
/// NOTE: Calculates actual file size from received data, NOT from header fileSize,
|
|
116
|
+
/// because fileSize may be "fake" (inflated) due to BES firmware workaround.
|
|
117
|
+
func assembleFile() -> Data? {
|
|
118
|
+
guard isComplete else { return nil }
|
|
119
|
+
|
|
120
|
+
// Calculate actual file size by summing all received packet sizes
|
|
121
|
+
let actualFileSize = receivedPackets.values.reduce(0) { $0 + $1.count }
|
|
122
|
+
|
|
123
|
+
Bridge.log(
|
|
124
|
+
"📦 Assembling file: headerFileSize=\(fileSize), actualFileSize=\(actualFileSize), totalPackets=\(totalPackets)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
var fileData = Data(capacity: actualFileSize)
|
|
128
|
+
|
|
129
|
+
for i in 0 ..< totalPackets {
|
|
130
|
+
if let packet = receivedPackets[i] {
|
|
131
|
+
fileData.append(packet)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return fileData
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class MentraLiveFileTransferSessionTests: XCTestCase {
|
|
6
|
+
func testDynamicPayloadUsesTrueSizeEvenWhenDivisibleBy400() {
|
|
7
|
+
for pack in [244, 474, 800] {
|
|
8
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: 80000, flags: 2)
|
|
9
|
+
session.recalculateTotalPackets(actualPackSize: pack)
|
|
10
|
+
XCTAssertEqual(session.totalPackets, (80000 + pack - 1) / pack)
|
|
11
|
+
for index in 0 ..< session.totalPackets {
|
|
12
|
+
XCTAssertTrue(session.addPacket(index, data: Data(repeating: 7, count: min(pack, 80000 - index * pack))))
|
|
13
|
+
}
|
|
14
|
+
XCTAssertTrue(session.isComplete)
|
|
15
|
+
XCTAssertEqual(session.assembleFile()?.count, 80000)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testLegacyInflatedSizeAndLargeUnflaggedPayloads() {
|
|
20
|
+
// Old senders inflate the header length for small packs, including
|
|
21
|
+
// the short final pack. Flag 1 only enables batched ACKs.
|
|
22
|
+
for (pack, headerSize, actualSize, packets) in [(221, 80000, 43982, 200), (400, 80000, 79777, 200), (800, 80000, 80000, 100)] {
|
|
23
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: headerSize, flags: 1)
|
|
24
|
+
session.recalculateTotalPackets(actualPackSize: pack)
|
|
25
|
+
XCTAssertEqual(session.totalPackets, packets)
|
|
26
|
+
let source = Data((0 ..< actualSize).map { UInt8($0 % 251) })
|
|
27
|
+
for index in (0 ..< packets).reversed() {
|
|
28
|
+
let start = index * pack
|
|
29
|
+
XCTAssertTrue(session.addPacket(index, data: source.subdata(in: start ..< min(start + pack, actualSize))))
|
|
30
|
+
}
|
|
31
|
+
XCTAssertTrue(session.isComplete)
|
|
32
|
+
XCTAssertEqual(session.assembleFile(), source)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testMalformedOrDuplicatePacketsCannotCompleteFile() {
|
|
37
|
+
var session = MentraLiveFileTransferSession(fileName: "photo", fileSize: 1000, flags: 2)
|
|
38
|
+
session.recalculateTotalPackets(actualPackSize: 800)
|
|
39
|
+
XCTAssertFalse(session.addPacket(0, data: Data(count: 799)))
|
|
40
|
+
XCTAssertFalse(session.addPacket(2, data: Data(count: 200)))
|
|
41
|
+
XCTAssertTrue(session.addPacket(1, data: Data(count: 200)))
|
|
42
|
+
XCTAssertFalse(session.isComplete)
|
|
43
|
+
XCTAssertFalse(session.addPacket(1, data: Data(count: 200)))
|
|
44
|
+
XCTAssertTrue(session.addPacket(0, data: Data(count: 800)))
|
|
45
|
+
XCTAssertEqual(session.assembleFile()?.count, 1000)
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.514",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"registry": "https://registry.npmjs.org/"
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@mentra/cloud-protocol": "3.3.0-dev.
|
|
117
|
+
"@mentra/cloud-protocol": "3.3.0-dev.514"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@types/node": "^25.9.3",
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.3.0",
|
|
15
|
-
"releaseIdentity": "3.3.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.3.0-dev.514",
|
|
16
|
+
"releaseSetId": "mentra-3.3.0-dev.514",
|
|
17
|
+
"sourceCommit": "c08afa1e430e63dffd758478435e8d59940d36c7",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.514.json",
|
|
19
|
+
"otaManifestSha256": "24a37a482d08ef1a11ff5c96280c641150601b255e336f7723dbf88cb598337e"
|
|
20
20
|
})
|