@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bota.dev/react-native-app-sdk",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.3",
4
4
  "description": "React Native facade for the Bota App SDK",
5
5
  "packageManager": "npm@12.0.2",
6
6
  "main": "lib/commonjs/index.js",
@@ -25,29 +25,40 @@ module BotaDeviceSDKSPMWorkaround
25
25
  end
26
26
 
27
27
  rewrite_aggregate_modulemap_references(installer)
28
+ installer.pod_targets.each do |pod_target|
29
+ pod_target.build_settings.each do |configuration_name, build_settings|
30
+ path = pod_target.xcconfig_path(configuration_name)
31
+ # Expo can re-save this cached config after the SPM post-install hook.
32
+ rewrite_modulemap_config(build_settings.xcconfig, path)
33
+ end
34
+ end
28
35
  end
29
36
 
30
37
  def self.rewrite_aggregate_modulemap_references(installer)
31
- nested = "${PODS_CONFIGURATION_BUILD_DIR}/#{POD_NAME}/#{POD_NAME}.modulemap"
32
- flattened = "${PODS_CONFIGURATION_BUILD_DIR}/#{POD_NAME}.modulemap"
33
-
34
38
  installer.aggregate_targets.each do |aggregate_target|
35
39
  aggregate_target.xcconfigs.each do |configuration_name, config_file|
36
- changed = false
37
- %w[OTHER_CFLAGS OTHER_SWIFT_FLAGS].each do |key|
38
- value = config_file.attributes[key]
39
- next unless value
40
-
41
- updated = value.gsub(nested, flattened)
42
- next if updated == value
43
-
44
- config_file.attributes[key] = updated
45
- changed = true
46
- end
47
- config_file.save_as(aggregate_target.xcconfig_path(configuration_name)) if changed
40
+ rewrite_modulemap_config(config_file, aggregate_target.xcconfig_path(configuration_name))
48
41
  end
49
42
  end
50
43
  end
51
44
 
45
+ def self.rewrite_modulemap_config(config_file, path)
46
+ nested = "${PODS_CONFIGURATION_BUILD_DIR}/#{POD_NAME}/#{POD_NAME}.modulemap"
47
+ flattened = "${PODS_CONFIGURATION_BUILD_DIR}/#{POD_NAME}.modulemap"
48
+ changed = false
49
+ %w[OTHER_CFLAGS OTHER_SWIFT_FLAGS].each do |key|
50
+ value = config_file.attributes[key]
51
+ next unless value
52
+
53
+ updated = value.gsub(nested, flattened)
54
+ next if updated == value
55
+
56
+ config_file.attributes[key] = updated
57
+ changed = true
58
+ end
59
+ config_file.save_as(path) if changed
60
+ end
61
+
52
62
  private_class_method :rewrite_aggregate_modulemap_references
63
+ private_class_method :rewrite_modulemap_config
53
64
  end
package/src/BotaClient.ts CHANGED
@@ -5,6 +5,7 @@ import { getCompatibilityClient } from './compatibility/runtime';
5
5
  import { DeviceManager } from './managers/DeviceManager';
6
6
  import { OTAManager } from './managers/OTAManager';
7
7
  import { RecordingManager } from './managers/RecordingManager';
8
+ import { rejectRecordingDataStore } from './managers/uploadRecoveryMetadata';
8
9
  import type {
9
10
  BluetoothState,
10
11
  BotaClientEvents,
@@ -17,12 +18,16 @@ import { logger, type LogHandler } from './utils/logger';
17
18
 
18
19
  const log = logger.tag('BotaClient');
19
20
 
20
- const normalizeConfig = (config: BotaConfig): Required<BotaConfig> => ({
21
+ type NormalizedConfig = Required<Omit<BotaConfig, 'uploadRecoveryProvider' | 'recordingDataStore'>> &
22
+ Pick<BotaConfig, 'uploadRecoveryProvider'>;
23
+
24
+ const normalizeConfig = (config: BotaConfig): NormalizedConfig => ({
21
25
  environment: config.environment ?? 'production',
22
26
  backgroundSyncEnabled: config.backgroundSyncEnabled ?? true,
23
27
  wifiOnlyUpload: config.wifiOnlyUpload ?? false,
24
28
  logLevel: config.logLevel ?? 'warn',
25
29
  debug: config.debug ?? false,
30
+ ...(config.uploadRecoveryProvider ? { uploadRecoveryProvider: config.uploadRecoveryProvider } : {}),
26
31
  });
27
32
 
28
33
  class BotaClientImpl extends EventEmitter<BotaClientEvents> {
@@ -74,6 +79,11 @@ class BotaClientImpl extends EventEmitter<BotaClientEvents> {
74
79
  }
75
80
 
76
81
  configure(config: BotaConfig = {}): Promise<void> {
82
+ try {
83
+ rejectRecordingDataStore(config.recordingDataStore);
84
+ } catch (error) {
85
+ return Promise.reject(error);
86
+ }
77
87
  if (this._configurePromise && !this._destroyPromise) {
78
88
  return this._configurePromise;
79
89
  }
@@ -141,7 +151,7 @@ class BotaClientImpl extends EventEmitter<BotaClientEvents> {
141
151
  return queued;
142
152
  }
143
153
 
144
- private async performConfigure(config: Required<BotaConfig>): Promise<void> {
154
+ private async performConfigure(config: NormalizedConfig): Promise<void> {
145
155
  if (this._state !== 'uninitialized' || this._nativeOwned) {
146
156
  log.warn('SDK already configured, reconfiguring');
147
157
  await this.releaseOwnerGraph();
@@ -170,7 +180,7 @@ class BotaClientImpl extends EventEmitter<BotaClientEvents> {
170
180
 
171
181
  deviceManager = new DeviceManager();
172
182
  await deviceManager.initialize();
173
- recordingManager = new RecordingManager();
183
+ recordingManager = new RecordingManager({ uploadRecoveryProvider: config.uploadRecoveryProvider });
174
184
  await recordingManager.initialize();
175
185
  otaManager = new OTAManager(deviceManager);
176
186
 
@@ -1,3 +1,8 @@
1
1
  export const DEVICE_LOG_FLAG_BACKLOG = 0x01;
2
2
  export const DEVICE_LOG_FLAG_DROPPED = 0x02;
3
3
  export const BLE_ERROR_STORAGE_KEY_UNAVAILABLE = 0x14;
4
+
5
+ export const DEVICE_DIAGNOSTICS_EVT_META = 0x90;
6
+ export const DEVICE_DIAGNOSTICS_EVT_SIGNATURE = 0x91;
7
+ export const DEVICE_DIAGNOSTICS_EVT_END = 0x92;
8
+ export const DEVICE_DIAGNOSTICS_EVT_DETAIL = 0x95;
@@ -0,0 +1,238 @@
1
+ import { Buffer } from 'buffer';
2
+
3
+ import {
4
+ DEVICE_DIAGNOSTICS_EVT_END,
5
+ DEVICE_DIAGNOSTICS_EVT_DETAIL,
6
+ DEVICE_DIAGNOSTICS_EVT_META,
7
+ DEVICE_DIAGNOSTICS_EVT_SIGNATURE,
8
+ } from './constants';
9
+ import type {
10
+ DeviceDiagnosticEvent,
11
+ DeviceDiagnosticEventType,
12
+ DeviceDiagnosticReasonCode,
13
+ DeviceDiagnosticsBatch,
14
+ } from '../models/Device';
15
+
16
+ const EVENT_TYPES: Record<number, DeviceDiagnosticEventType> = {
17
+ 1: 'reset', 2: 'watchdog', 3: 'hard_fault', 4: 'storage',
18
+ 5: 'power', 6: 'radio', 7: 'security',
19
+ };
20
+ const REASONS: Record<number, DeviceDiagnosticReasonCode> = {
21
+ 1: 'watchdog_timeout', 2: 'cpu_illegal_instruction',
22
+ 3: 'cpu_misaligned_access', 4: 'cpu_stack_overflow',
23
+ 5: 'cpu_usage_fault', 6: 'memory_protection_fault',
24
+ 7: 'invalid_register_read', 8: 'invalid_register_write',
25
+ 9: 'audio_subsystem_fault', 10: 'wireless_subsystem_fault',
26
+ 11: 'flash_mmu_fault', 12: 'low_voltage_reset',
27
+ 255: 'unknown_cpu_exception',
28
+ };
29
+ const SUBSYSTEMS: Record<number, string> = {
30
+ 0: 'system', 1: 'audio', 2: 'wireless', 3: 'storage',
31
+ 4: 'power', 5: 'security',
32
+ };
33
+ const STATES: Record<number, string> = {
34
+ 0: 'boot', 1: 'deep_standby', 2: 'recording', 3: 'upload_preparing',
35
+ 4: 'uploading', 5: 'error_recovery', 6: 'full_sleep',
36
+ };
37
+ const BREADCRUMBS: Record<number, string> = {
38
+ 1: 'boot', 2: 'standby_entered', 3: 'recording_started',
39
+ 4: 'upload_preparing', 5: 'upload_started', 6: 'error_recovery',
40
+ 7: 'full_sleep',
41
+ };
42
+ const DETAIL_WIRE_SIZE = 176;
43
+
44
+ function u64LeHex(bytes: Uint8Array): string {
45
+ return Buffer.from(bytes).reverse().toString('hex').padStart(16, '0');
46
+ }
47
+
48
+ export function diagnosticEventIdCommand(eventId: string): Buffer {
49
+ if (!/^[0-9a-f]{16}$/.test(eventId)) {
50
+ throw new Error('Diagnostic event_id must be 16 lowercase hex characters');
51
+ }
52
+ return Buffer.concat([
53
+ Buffer.from([0x11]),
54
+ Buffer.from(eventId, 'hex').reverse(),
55
+ ]);
56
+ }
57
+
58
+ type PartialEvent = Omit<DeviceDiagnosticEvent, 'signature' | 'firmware_build_id' |
59
+ 'subsystem' | 'state_before_event'> & {
60
+ signature?: string;
61
+ firmware_build_id?: string;
62
+ subsystem?: string;
63
+ state_before_event?: string;
64
+ };
65
+
66
+ interface DetailAssembly {
67
+ buffer: Buffer;
68
+ received: number;
69
+ }
70
+
71
+ function normalizedAddress(value: number): string | undefined {
72
+ if (value === 0) return undefined;
73
+ const region = (value & 0x80000000) !== 0
74
+ ? 'sdram'
75
+ : (value & 0x40000000) !== 0 ? 'ram' : 'rom';
76
+ const offset = (value & 0x3fffffff).toString(16).padStart(8, '0');
77
+ return `${region}:${offset}`;
78
+ }
79
+
80
+ function fixedHex32(value: number): string {
81
+ return value.toString(16).padStart(8, '0');
82
+ }
83
+
84
+ function decodeDetail(bytes: Buffer): Pick<DeviceDiagnosticEvent,
85
+ 'firmware_build_id' | 'subsystem' | 'state_before_event' | 'report'> {
86
+ if (bytes.length !== DETAIL_WIRE_SIZE || bytes[0] !== 1) {
87
+ throw new Error('Unsupported diagnostic detail payload');
88
+ }
89
+ const hasReport = (bytes[1] & 1) !== 0;
90
+ const firmwareBuildId = Buffer.from(bytes.subarray(2, 14)).toString('ascii');
91
+ if (!/^[0-9a-f]{12}$/.test(firmwareBuildId)) {
92
+ throw new Error('Invalid diagnostic firmware build id');
93
+ }
94
+ const common = {
95
+ firmware_build_id: firmwareBuildId,
96
+ subsystem: SUBSYSTEMS[bytes[14]] ?? 'system',
97
+ state_before_event: STATES[bytes[15]] ?? 'unknown',
98
+ };
99
+ if (!hasReport) return common;
100
+
101
+ const pcCount = Math.min(bytes[17], 6);
102
+ const breadcrumbCount = Math.min(bytes[18], 8);
103
+ const runtimeMask = bytes[19];
104
+ const pcTrace: string[] = [];
105
+ for (let index = 0; index < pcCount; index++) {
106
+ const address = normalizedAddress(bytes.readUInt32LE(48 + index * 4));
107
+ if (address) pcTrace.push(address);
108
+ }
109
+ const task = Buffer.from(bytes.subarray(80, 96)).toString('ascii').replace(/\0.*$/, '');
110
+ const breadcrumbs = [];
111
+ for (let index = 0; index < breadcrumbCount; index++) {
112
+ const offset = 96 + index * 10;
113
+ breadcrumbs.push({
114
+ delta_ms: bytes.readInt32LE(offset),
115
+ code: BREADCRUMBS[bytes.readUInt16LE(offset + 4)] ?? 'state_changed',
116
+ arg0: bytes.readInt32LE(offset + 6),
117
+ });
118
+ }
119
+ const reti = normalizedAddress(bytes.readUInt32LE(40));
120
+ const rets = normalizedAddress(bytes.readUInt32LE(44));
121
+ return {
122
+ ...common,
123
+ report: {
124
+ fault: {
125
+ cpu_id: bytes[16],
126
+ cpu_emu: fixedHex32(bytes.readUInt32LE(20)),
127
+ core_emu: fixedHex32(bytes.readUInt32LE(24)),
128
+ hsb_emu: fixedHex32(bytes.readUInt32LE(28)),
129
+ audio_emu: fixedHex32(bytes.readUInt32LE(32)),
130
+ wireless_emu: fixedHex32(bytes.readUInt32LE(36)),
131
+ },
132
+ execution: {
133
+ ...(task ? { task } : {}),
134
+ ...(reti ? { reti } : {}),
135
+ ...(rets ? { rets } : {}),
136
+ pc_trace: pcTrace,
137
+ },
138
+ ...((runtimeMask & 3) !== 0 ? {
139
+ runtime: {
140
+ ...((runtimeMask & 1) !== 0
141
+ ? { heap_free_bytes: bytes.readUInt32LE(72) } : {}),
142
+ ...((runtimeMask & 2) !== 0
143
+ ? { task_stack_remaining_bytes: bytes.readUInt32LE(76) } : {}),
144
+ },
145
+ } : {}),
146
+ ...(breadcrumbs.length ? { breadcrumbs } : {}),
147
+ },
148
+ };
149
+ }
150
+
151
+ export class DeviceDiagnosticsDecoder {
152
+ private events = new Map<number, PartialEvent>();
153
+ private details = new Map<number, DetailAssembly>();
154
+
155
+ push(packet: Buffer): DeviceDiagnosticsBatch | null {
156
+ if (packet.length === 17 && packet[0] === DEVICE_DIAGNOSTICS_EVT_META) {
157
+ const type = EVENT_TYPES[packet[2]];
158
+ if (!type) return null;
159
+ this.events.set(packet[1], {
160
+ event_id: u64LeHex(packet.subarray(9, 17)),
161
+ event_type: type,
162
+ reason_code: REASONS[packet.readUInt16LE(3)] ?? 'unknown_cpu_exception',
163
+ uptime_ms: packet.readUInt32LE(5),
164
+ });
165
+ return null;
166
+ }
167
+ if (packet.length === 10 && packet[0] === DEVICE_DIAGNOSTICS_EVT_SIGNATURE) {
168
+ const event = this.events.get(packet[1]);
169
+ if (event) event.signature = u64LeHex(packet.subarray(2, 10));
170
+ return null;
171
+ }
172
+ if (packet.length >= 6 && packet.length <= 20 &&
173
+ packet[0] === DEVICE_DIAGNOSTICS_EVT_DETAIL) {
174
+ const index = packet[1];
175
+ const offset = packet.readUInt16LE(2);
176
+ const total = packet.readUInt16LE(4);
177
+ if (total !== DETAIL_WIRE_SIZE || offset + packet.length - 6 > total) {
178
+ this.reset();
179
+ throw new Error('Invalid diagnostic detail chunk');
180
+ }
181
+ let detail = this.details.get(index);
182
+ if (offset === 0) {
183
+ if (detail) {
184
+ this.reset();
185
+ throw new Error(`Duplicate diagnostic detail at index ${index}`);
186
+ }
187
+ detail = { buffer: Buffer.alloc(total), received: 0 };
188
+ this.details.set(index, detail);
189
+ }
190
+ if (!detail || detail.received !== offset) {
191
+ this.reset();
192
+ throw new Error(`Out-of-order diagnostic detail at index ${index}`);
193
+ }
194
+ packet.copy(detail.buffer, offset, 6);
195
+ detail.received += packet.length - 6;
196
+ if (detail.received === total) {
197
+ const event = this.events.get(index);
198
+ if (!event) {
199
+ this.reset();
200
+ throw new Error(`Diagnostic detail without metadata at index ${index}`);
201
+ }
202
+ try {
203
+ Object.assign(event, decodeDetail(detail.buffer));
204
+ } catch (error) {
205
+ this.reset();
206
+ throw error;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ if (packet.length === 2 && packet[0] === DEVICE_DIAGNOSTICS_EVT_END) {
212
+ const expected = packet[1];
213
+ const events: DeviceDiagnosticEvent[] = [];
214
+ for (let index = 0; index < expected; index++) {
215
+ const event = this.events.get(index);
216
+ if (!event?.signature || !event.firmware_build_id ||
217
+ !event.subsystem || !event.state_before_event ||
218
+ this.details.get(index)?.received !== DETAIL_WIRE_SIZE) {
219
+ this.reset();
220
+ throw new Error(`Incomplete diagnostic event at index ${index}`);
221
+ }
222
+ events.push(event as DeviceDiagnosticEvent);
223
+ }
224
+ const batch: DeviceDiagnosticsBatch = {
225
+ schema_version: 1,
226
+ events,
227
+ };
228
+ this.reset();
229
+ return batch;
230
+ }
231
+ return null;
232
+ }
233
+
234
+ reset(): void {
235
+ this.events.clear();
236
+ this.details.clear();
237
+ }
238
+ }