@bota.dev/react-native-app-sdk 2.0.0-beta.0 → 2.0.0-beta.3

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 (100) hide show
  1. package/README.md +66 -2
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
  4. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
  5. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +68 -3
  9. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
  10. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
  11. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
  12. package/generated/codegen-contract.json +495 -8
  13. package/ios/BotaDeviceSDK.mm +102 -0
  14. package/ios/BotaDeviceSDKAppleBridge.swift +126 -4
  15. package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
  16. package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
  17. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
  18. package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
  19. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
  20. package/lib/commonjs/BotaClient.js +13 -2
  21. package/lib/commonjs/BotaClient.js.map +1 -1
  22. package/lib/commonjs/ble/constants.js +5 -1
  23. package/lib/commonjs/ble/constants.js.map +1 -1
  24. package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
  25. package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
  26. package/lib/commonjs/client.js +163 -28
  27. package/lib/commonjs/client.js.map +1 -1
  28. package/lib/commonjs/index.js +15 -0
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/managers/DeviceManager.js +8 -0
  31. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  32. package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
  33. package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
  34. package/lib/commonjs/managers/RecordingManager.js +83 -102
  35. package/lib/commonjs/managers/RecordingManager.js.map +1 -1
  36. package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
  37. package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
  38. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
  39. package/lib/module/BotaClient.js +13 -2
  40. package/lib/module/BotaClient.js.map +1 -1
  41. package/lib/module/ble/constants.js +4 -0
  42. package/lib/module/ble/constants.js.map +1 -1
  43. package/lib/module/ble/deviceDiagnostics.js +234 -0
  44. package/lib/module/ble/deviceDiagnostics.js.map +1 -0
  45. package/lib/module/client.js +163 -28
  46. package/lib/module/client.js.map +1 -1
  47. package/lib/module/index.js +2 -0
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/managers/DeviceManager.js +8 -0
  50. package/lib/module/managers/DeviceManager.js.map +1 -1
  51. package/lib/module/managers/NativeUploadQueue.js +380 -0
  52. package/lib/module/managers/NativeUploadQueue.js.map +1 -0
  53. package/lib/module/managers/RecordingManager.js +83 -102
  54. package/lib/module/managers/RecordingManager.js.map +1 -1
  55. package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
  56. package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
  57. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
  58. package/lib/typescript/BotaClient.d.ts.map +1 -1
  59. package/lib/typescript/ble/constants.d.ts +4 -0
  60. package/lib/typescript/ble/constants.d.ts.map +1 -1
  61. package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
  62. package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
  63. package/lib/typescript/client.d.ts +18 -2
  64. package/lib/typescript/client.d.ts.map +1 -1
  65. package/lib/typescript/index.d.ts +6 -4
  66. package/lib/typescript/index.d.ts.map +1 -1
  67. package/lib/typescript/managers/DeviceManager.d.ts +3 -1
  68. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
  69. package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
  70. package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
  71. package/lib/typescript/managers/RecordingManager.d.ts +3 -8
  72. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
  73. package/lib/typescript/managers/types.d.ts +6 -1
  74. package/lib/typescript/managers/types.d.ts.map +1 -1
  75. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
  76. package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
  77. package/lib/typescript/models/Device.d.ts +42 -0
  78. package/lib/typescript/models/Device.d.ts.map +1 -1
  79. package/lib/typescript/models/Recording.d.ts +47 -0
  80. package/lib/typescript/models/Recording.d.ts.map +1 -1
  81. package/lib/typescript/models/Status.d.ts +5 -1
  82. package/lib/typescript/models/Status.d.ts.map +1 -1
  83. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +61 -0
  84. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
  87. package/src/BotaClient.ts +13 -3
  88. package/src/ble/constants.ts +5 -0
  89. package/src/ble/deviceDiagnostics.ts +238 -0
  90. package/src/client.ts +166 -18
  91. package/src/index.ts +12 -0
  92. package/src/managers/DeviceManager.ts +11 -0
  93. package/src/managers/NativeUploadQueue.ts +330 -0
  94. package/src/managers/RecordingManager.ts +83 -116
  95. package/src/managers/types.ts +7 -1
  96. package/src/managers/uploadRecoveryMetadata.ts +59 -0
  97. package/src/models/Device.ts +43 -0
  98. package/src/models/Recording.ts +52 -0
  99. package/src/models/Status.ts +5 -1
  100. package/src/specs/NativeBotaDeviceSDK.ts +56 -0
package/src/client.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  NativeEncryptedUploadV2Checkpoint,
11
11
  NativeEncryptedUploadV2Progress,
12
12
  NativeEncryptedUploadV2Recording,
13
+ NativePendingRecording,
13
14
  NativeFactoryResetCompletion,
14
15
  NativeFactoryResetGrantRequest,
15
16
  NativeFirmwareUpdateProgress,
@@ -37,6 +38,9 @@ import type {
37
38
  DeviceStatus,
38
39
  DeviceType,
39
40
  DeviceLogEvent,
41
+ DeviceDiagnosticEventType,
42
+ DeviceDiagnosticReasonCode,
43
+ DeviceDiagnosticsBatch,
40
44
  DiscoveredDevice,
41
45
  PairingState,
42
46
  ReconnectOptions,
@@ -195,11 +199,24 @@ export type BotaRecordingTransferProgress = {
195
199
 
196
200
  export type BotaEncryptedUploadV2Recording = NativeEncryptedUploadV2Recording;
197
201
 
202
+ export type BotaEncryptedUploadV2PendingRecording = Omit<BotaEncryptedUploadV2Recording, 'durationMs'> & {
203
+ storageFormat: 3;
204
+ startedAt: Date;
205
+ durationMs: number;
206
+ plaintextLength: string;
207
+ };
208
+
209
+ export type BotaEncryptedUploadV2SyncOptions = {
210
+ signal?: AbortSignal;
211
+ operationId?: string;
212
+ };
213
+
198
214
  export type BotaEncryptedUploadV2Capability = NativeEncryptedUploadV2Capability;
199
215
 
200
216
  export type BotaEncryptedUploadV2Checkpoint = NativeEncryptedUploadV2Checkpoint;
201
217
 
202
218
  export type BotaEncryptedUploadV2ProviderContext = {
219
+ operationId: string;
203
220
  recording: BotaEncryptedUploadV2Recording;
204
221
  capability: BotaEncryptedUploadV2Capability;
205
222
  checkpoint?: BotaEncryptedUploadV2Checkpoint;
@@ -287,6 +304,8 @@ export type BotaDeviceSDKOTAClient = {
287
304
  };
288
305
 
289
306
  export type BotaDeviceSDKLogClient = {
307
+ readDiagnosticEvents(device: ConnectedDevice): Promise<DeviceDiagnosticsBatch>;
308
+ acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: string[]): Promise<void>;
290
309
  subscribe(
291
310
  device: ConnectedDevice,
292
311
  onLine: (line: DeviceLogEvent) => void
@@ -310,6 +329,7 @@ export type BotaDeviceSDKWiFiClient = {
310
329
 
311
330
  export type BotaDeviceSDKRecordingClient = {
312
331
  listRecordings(device: ConnectedDevice): Promise<DeviceRecording[]>;
332
+ listPendingRecordings(device: ConnectedDevice): Promise<Array<DeviceRecording | BotaEncryptedUploadV2PendingRecording>>;
313
333
  syncRecording(
314
334
  device: ConnectedDevice,
315
335
  recording: DeviceRecording,
@@ -319,12 +339,14 @@ export type BotaDeviceSDKRecordingClient = {
319
339
  localPath: string;
320
340
  e2eEncrypted: boolean;
321
341
  contentSha256?: string;
342
+ fileSizeBytes: number;
322
343
  }>;
323
344
  syncEncryptedRecordingV2(
324
345
  device: ConnectedDevice,
325
346
  recording: BotaEncryptedUploadV2Recording,
326
347
  provider: BotaEncryptedUploadV2ProfileProvider,
327
- onProgress?: (progress: BotaEncryptedUploadV2Progress) => void
348
+ onProgress?: (progress: BotaEncryptedUploadV2Progress) => void,
349
+ options?: BotaEncryptedUploadV2SyncOptions
328
350
  ): Promise<void>;
329
351
  confirmRecording(
330
352
  device: ConnectedDevice,
@@ -335,6 +357,7 @@ export type BotaDeviceSDKRecordingClient = {
335
357
  onProgress?: (progress: BotaRecordingTransferProgress) => void
336
358
  ): Promise<void>;
337
359
  cancelRecordingUpload(taskId: string): Promise<void>;
360
+ releaseRecordingFile(taskId: string, localPath: string): Promise<void>;
338
361
  loadUploadQueue(): Promise<UploadTask[]>;
339
362
  saveUploadQueue(tasks: UploadTask[]): Promise<void>;
340
363
  destroyCompatibilityOperations(): Promise<void>;
@@ -425,6 +448,13 @@ export type BotaDeviceSDKClient = {
425
448
  getState(): Promise<BotaDeviceSDKState>;
426
449
  };
427
450
 
451
+ const formatAdvertisedMac = (value: string | undefined): string | null => {
452
+ if (!value || !/^(?:[0-9a-f]{12}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})$/i.test(value)) {
453
+ return null;
454
+ }
455
+ return value.replace(/:/g, '').toUpperCase().match(/.{2}/g)!.join(':');
456
+ };
457
+
428
458
  const mapDiscoveredDevice = (
429
459
  device: NativeDiscoveredDevice
430
460
  ): DiscoveredDevice => ({
@@ -432,7 +462,7 @@ const mapDiscoveredDevice = (
432
462
  name: device.name ?? '',
433
463
  deviceType: (device.deviceType ?? 'bota_pin') as DeviceType,
434
464
  firmwareVersion: device.firmwareVersion ?? '',
435
- macAddress: device.macAddress ?? null,
465
+ macAddress: formatAdvertisedMac(device.macAddress),
436
466
  pairingState: (device.pairingState ?? 'unpaired') as PairingState,
437
467
  rssi: device.rssi,
438
468
  discoveredAt: new Date(device.discoveredAtMs),
@@ -484,6 +514,48 @@ const createOpaqueId = (): string =>
484
514
  return nibble.toString(16);
485
515
  });
486
516
 
517
+ const catalogInteger = (value: string | undefined, maximum = BigInt(Number.MAX_SAFE_INTEGER)): number => {
518
+ if (typeof value !== 'string' || !/^(0|[1-9][0-9]{0,19})$/.test(value) || BigInt(value) > maximum) {
519
+ throw new Error('invalid native recording catalog metadata');
520
+ }
521
+ return Number(value);
522
+ };
523
+
524
+ const mapPendingRecording = (value: NativePendingRecording): DeviceRecording | BotaEncryptedUploadV2PendingRecording => {
525
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
526
+ if (value.profile === 'legacy' && value.legacy && !value.encrypted) {
527
+ const recording = value.legacy;
528
+ if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) ||
529
+ !Number.isSafeInteger(recording.startedAtMs) || recording.startedAtMs < 0 || recording.startedAtMs > 8640000000000000 ||
530
+ !Number.isSafeInteger(recording.durationMs) || recording.durationMs < 0 ||
531
+ !Number.isSafeInteger(recording.fileSize) || recording.fileSize < 0 ||
532
+ typeof recording.codec !== 'string' || typeof recording.isEncrypted !== 'boolean') {
533
+ throw new Error('invalid native recording catalog metadata');
534
+ }
535
+ return mapRecording(recording);
536
+ }
537
+ if (value.profile !== 'encrypted_upload_v2' || !value.encrypted || value.legacy) {
538
+ throw new Error('invalid native recording catalog profile');
539
+ }
540
+ const recording = value.encrypted;
541
+ const plaintextLength = recording.plaintextLength;
542
+ if (typeof recording.uuid !== 'string' || !uuidPattern.test(recording.uuid) ||
543
+ recording.storageFormat !== 3 || typeof plaintextLength !== 'string' ||
544
+ !/^(0|[1-9][0-9]{0,19})$/.test(plaintextLength) || BigInt(plaintextLength) > 0xffffffffffffffffn ||
545
+ typeof recording.ciphertextLength !== 'string' || !/^[1-9][0-9]{0,19}$/.test(recording.ciphertextLength) || BigInt(recording.ciphertextLength) > 0xffffffffffffffffn ||
546
+ typeof recording.ciphertextSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(recording.ciphertextSha256) ||
547
+ !Number.isInteger(recording.generation) || recording.generation < 0 || recording.generation > 0xffffffff) {
548
+ throw new Error('invalid native recording catalog identity');
549
+ }
550
+ return {
551
+ uuid: recording.uuid, generation: recording.generation,
552
+ ciphertextLength: recording.ciphertextLength, ciphertextSha256: recording.ciphertextSha256,
553
+ storageFormat: 3, plaintextLength, startedAtMs: recording.startedAtMs,
554
+ startedAt: new Date(catalogInteger(recording.startedAtMs, 8640000000000000n)),
555
+ durationMs: catalogInteger(recording.durationMs),
556
+ };
557
+ };
558
+
487
559
  const bytesToHex = (value: Uint8Array): string =>
488
560
  Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
489
561
 
@@ -710,6 +782,7 @@ const toNativeRecordingUploadRequest = (
710
782
  recordingId: task.recordingId,
711
783
  deviceId: task.deviceId,
712
784
  localPath: task.localPath,
785
+ ...(task.fileSizeBytes === undefined ? {} : { fileSizeBytes: task.fileSizeBytes }),
713
786
  uploadUrl: task.uploadUrl,
714
787
  ...(task.uploadToken ? { uploadToken: task.uploadToken } : {}),
715
788
  ...(task.completeUrl ? { completeUrl: task.completeUrl } : {}),
@@ -724,9 +797,13 @@ const toNativeRecordingUploadRequest = (
724
797
  });
725
798
 
726
799
  const parseUploadQueue = (serialized: string): UploadTask[] => {
727
- const value: unknown = JSON.parse(serialized || '[]');
728
- if (!Array.isArray(value)) return [];
800
+ const value: unknown = JSON.parse(serialized);
801
+ if (!Array.isArray(value)) throw new Error('Invalid upload recovery journal');
729
802
  return value.map((task) => {
803
+ if (!task || typeof task !== 'object' || Array.isArray(task) ||
804
+ typeof task.createdAt !== 'string' || typeof task.updatedAt !== 'string') {
805
+ throw new Error('Invalid upload recovery journal');
806
+ }
730
807
  const candidate = task as UploadTask & {
731
808
  createdAt: string | Date;
732
809
  updatedAt: string | Date;
@@ -1056,6 +1133,11 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1056
1133
  return values.map(mapRecording);
1057
1134
  },
1058
1135
 
1136
+ async listPendingRecordings(device) {
1137
+ const values = await requireNativeModule().listPendingRecordings(toNativeConnectedDevice(device));
1138
+ return values.map(mapPendingRecording);
1139
+ },
1140
+
1059
1141
  async syncRecording(device, recording, onProgress, sinkId) {
1060
1142
  const module = requireNativeModule();
1061
1143
  const subscription = module.onRecordingTransferProgress((progress) => {
@@ -1072,21 +1154,48 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1072
1154
  }
1073
1155
  },
1074
1156
 
1075
- async syncEncryptedRecordingV2(device, recording, provider, onProgress) {
1157
+ async syncEncryptedRecordingV2(device, recording, provider, onProgress, options = {}) {
1158
+ if (options.signal?.aborted) throw new Error('encrypted upload v2 cancelled');
1076
1159
  const module = requireNativeModule();
1077
- const operationId = createOpaqueId();
1078
- const profileSubscription = module.onEncryptedUploadV2ProfileRequested(
1160
+ const operationId = options.operationId ?? createOpaqueId();
1161
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(operationId)) {
1162
+ throw new Error('invalid encrypted upload v2 operation ID');
1163
+ }
1164
+ let active = true;
1165
+ let nativeStarted = false;
1166
+ let cancellation: Promise<void> | undefined;
1167
+ let cancellationFailed = false;
1168
+ const cancel = () => {
1169
+ if (nativeStarted && !cancellation) {
1170
+ try {
1171
+ cancellation = module.cancelEncryptedRecordingV2(operationId).catch(() => { cancellationFailed = true; });
1172
+ } catch {
1173
+ cancellationFailed = true;
1174
+ }
1175
+ }
1176
+ };
1177
+ let profileSubscription: BotaEventSubscription | undefined;
1178
+ let progressSubscription: BotaEventSubscription | undefined;
1179
+ try {
1180
+ options.signal?.addEventListener('abort', cancel, { once: true });
1181
+ profileSubscription = module.onEncryptedUploadV2ProfileRequested(
1079
1182
  (request) => {
1080
- if (request.operationId !== operationId) return;
1183
+ if (!active || options.signal?.aborted || request.operationId !== operationId) return;
1081
1184
  void (async () => {
1185
+ let decision: BotaEncryptedUploadV2ProfileDecision | undefined;
1082
1186
  try {
1083
- const decision = await provider({
1187
+ decision = await provider({
1188
+ operationId,
1084
1189
  recording: request.recording,
1085
1190
  capability: request.capability,
1086
1191
  ...(request.checkpoint === undefined
1087
1192
  ? {}
1088
1193
  : { checkpoint: request.checkpoint }),
1089
1194
  });
1195
+ if (!active || options.signal?.aborted) {
1196
+ await module.releaseEncryptedUploadV2Material(decision.materialRegistrationId);
1197
+ return;
1198
+ }
1090
1199
  if (decision.profile !== 'encrypted_upload_v2') {
1091
1200
  throw new Error('encrypted upload v2 requires an explicit v2 profile');
1092
1201
  }
@@ -1095,29 +1204,40 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1095
1204
  decision
1096
1205
  );
1097
1206
  } catch (error) {
1098
- await module.rejectEncryptedUploadV2Profile(
1099
- request.requestId,
1100
- 'application_material_rejected'
1101
- );
1207
+ if (decision) await module.releaseEncryptedUploadV2Material(decision.materialRegistrationId).catch(() => {});
1208
+ if (active && !options.signal?.aborted) {
1209
+ await module.rejectEncryptedUploadV2Profile(request.requestId, 'application_material_rejected');
1210
+ }
1102
1211
  }
1103
1212
  })().catch(() => {});
1104
1213
  }
1105
1214
  );
1106
- const progressSubscription = module.onEncryptedUploadV2Progress(
1215
+ progressSubscription = module.onEncryptedUploadV2Progress(
1107
1216
  (progress) => {
1108
- if (progress.operationId !== operationId) return;
1217
+ if (!active || options.signal?.aborted || progress.operationId !== operationId) return;
1109
1218
  onProgress?.(mapEncryptedUploadV2Progress(progress));
1110
1219
  }
1111
1220
  );
1112
- try {
1221
+ if (options.signal?.aborted) throw new Error('encrypted upload v2 cancelled');
1222
+ nativeStarted = true;
1113
1223
  await module.syncEncryptedRecordingV2(
1114
1224
  toNativeConnectedDevice(device),
1115
1225
  recording,
1116
1226
  operationId
1117
1227
  );
1118
1228
  } finally {
1119
- profileSubscription.remove();
1120
- progressSubscription.remove();
1229
+ active = false;
1230
+ let cleanupFailed = false;
1231
+ for (const remove of [
1232
+ () => options.signal?.removeEventListener('abort', cancel),
1233
+ () => profileSubscription?.remove(),
1234
+ () => progressSubscription?.remove(),
1235
+ ]) {
1236
+ try { remove(); } catch { cleanupFailed = true; }
1237
+ }
1238
+ await cancellation;
1239
+ if (cancellationFailed) throw new Error('encrypted upload v2 cancellation failed');
1240
+ if (cleanupFailed) throw new Error('encrypted upload v2 listener cleanup failed');
1121
1241
  }
1122
1242
  },
1123
1243
 
@@ -1148,6 +1268,10 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1148
1268
  await requireNativeModule().cancelRecordingUpload(taskId);
1149
1269
  },
1150
1270
 
1271
+ async releaseRecordingFile(taskId, localPath) {
1272
+ await requireNativeModule().releaseRecordingFile(taskId, localPath);
1273
+ },
1274
+
1151
1275
  async loadUploadQueue() {
1152
1276
  return parseUploadQueue(
1153
1277
  await requireNativeModule().loadCompatibilityUploadQueue()
@@ -1278,6 +1402,30 @@ export const createBotaDeviceSDK = (nativeModule: Spec | null): BotaDeviceSDKCli
1278
1402
  };
1279
1403
 
1280
1404
  const logs: BotaDeviceSDKLogClient = {
1405
+ async readDiagnosticEvents(device) {
1406
+ const batch = await requireNativeModule().readDiagnosticEvents(toNativeConnectedDevice(device));
1407
+ if (batch.schema_version !== 1) throw new Error('Unsupported diagnostic batch schema');
1408
+ return {
1409
+ schema_version: 1,
1410
+ events: batch.events.map(({ report, ...event }) => ({
1411
+ ...event,
1412
+ event_type: event.event_type as DeviceDiagnosticEventType,
1413
+ reason_code: event.reason_code as DeviceDiagnosticReasonCode,
1414
+ ...(report ? { report: {
1415
+ fault: report.fault,
1416
+ execution: { ...report.execution, pc_trace: [...report.execution.pc_trace] },
1417
+ ...(report.runtime ? { runtime: report.runtime } : {}),
1418
+ ...(report.breadcrumbs ? { breadcrumbs: [...report.breadcrumbs] } : {}),
1419
+ } } : {}),
1420
+ })),
1421
+ };
1422
+ },
1423
+ async acknowledgeDiagnosticEvents(device, acceptedEventIds) {
1424
+ if (acceptedEventIds.some((id) => !/^[0-9a-f]{16}$/.test(id))) {
1425
+ throw new Error('Diagnostic event_id must be 16 lowercase hex characters');
1426
+ }
1427
+ await requireNativeModule().acknowledgeDiagnosticEvents(toNativeConnectedDevice(device), [...acceptedEventIds]);
1428
+ },
1281
1429
  async subscribe(device, onLine) {
1282
1430
  const module = requireNativeModule();
1283
1431
  const eventSubscription = module.onDeviceLog((line) => {
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export type {
19
19
  BotaEncryptedUploadV2ProgressPhase,
20
20
  BotaEncryptedUploadV2ProviderContext,
21
21
  BotaEncryptedUploadV2Recording,
22
+ BotaEncryptedUploadV2PendingRecording,
23
+ BotaEncryptedUploadV2SyncOptions,
22
24
  BotaEncryptedUploadV2SecurityPolicy,
23
25
  BotaDeprovisionResult,
24
26
  BotaDeviceSDKDeviceClient,
@@ -62,6 +64,10 @@ export type {
62
64
  DeviceConnectionSettings,
63
65
  DeviceFlags,
64
66
  DeviceLogEvent,
67
+ DeviceDiagnosticEvent,
68
+ DeviceDiagnosticEventType,
69
+ DeviceDiagnosticReasonCode,
70
+ DeviceDiagnosticsBatch,
65
71
  DeviceState,
66
72
  DeviceStatus,
67
73
  DeviceType,
@@ -87,6 +93,9 @@ export type {
87
93
  export type {
88
94
  AudioCodec,
89
95
  DeviceRecording,
96
+ RecordingDataStore,
97
+ UploadRecoveryContext,
98
+ UploadRecoveryProvider,
90
99
  StreamingSessionEvents,
91
100
  StreamingState,
92
101
  StreamingSyncOptions,
@@ -118,13 +127,16 @@ export type {
118
127
  FirmwareInfo,
119
128
  OtaProgress,
120
129
  OtaStage,
130
+ RecordingManagerOptions,
121
131
  UploadInfoProvider,
122
132
  } from './managers/types';
123
133
 
124
134
  export { DeviceLogDecoder } from './ble/deviceLogs';
135
+ export { DeviceDiagnosticsDecoder, diagnosticEventIdCommand } from './ble/deviceDiagnostics';
125
136
  export { DeviceManager } from './managers/DeviceManager';
126
137
  export { OTAManager } from './managers/OTAManager';
127
138
  export { RecordingManager } from './managers/RecordingManager';
139
+ export const UPLOAD_RECOVERY_VERSION = 1;
128
140
  export { StreamingSession } from './managers/StreamingSession';
129
141
  export {
130
142
  BotaError,
@@ -16,6 +16,7 @@ import type {
16
16
  ConnectedDevice,
17
17
  DeviceConnectionSettings,
18
18
  DeviceLogEvent,
19
+ DeviceDiagnosticsBatch,
19
20
  DeviceStatus,
20
21
  DeviceWiFiScanResult,
21
22
  DiscoveredDevice,
@@ -485,6 +486,16 @@ export class DeviceManager extends EventEmitter<DeviceManagerEvents> {
485
486
  });
486
487
  }
487
488
 
489
+ async readDiagnosticEvents(device: ConnectedDevice): Promise<DeviceDiagnosticsBatch> {
490
+ this.requireConnected(device.id);
491
+ return this.client.logs.readDiagnosticEvents(device);
492
+ }
493
+
494
+ async acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: string[]): Promise<void> {
495
+ this.requireConnected(device.id);
496
+ await this.client.logs.acknowledgeDiagnosticEvents(device, acceptedEventIds);
497
+ }
498
+
488
499
  async readConnectionSettings(
489
500
  device: ConnectedDevice
490
501
  ): Promise<DeviceConnectionSettings> {