@condorinst/mobile-bridge 0.22.0 → 0.23.0

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/dist/dev.js CHANGED
@@ -48,7 +48,7 @@ function telemetry(spec, now) {
48
48
  serialNo: spec.serialNo,
49
49
  attemptDate: now - 10 * 6e4,
50
50
  message: spec.syncError,
51
- messageCode: "SYNC_FAILED"
51
+ messageCode: "send_data_log_error"
52
52
  }
53
53
  ] : []
54
54
  };
@@ -132,6 +132,7 @@ function systemStatus(overrides = {}) {
132
132
  internet: true,
133
133
  permission: true,
134
134
  hasDevices: true,
135
+ hasDevicesKnown: true,
135
136
  missingOptionalPermissions: [],
136
137
  missingEssentialPermissions: [],
137
138
  deviceSyncError: {},
@@ -147,13 +148,15 @@ function lightAt(epochMs, serialNo) {
147
148
  const photopic = Math.round((daylight * 1800 + noise * 40) * 10) / 10;
148
149
  return { photopic, melanopic: Math.round(photopic * 0.62 * 10) / 10 };
149
150
  }
150
- function logAt(epochMs, serialNo, deviceId) {
151
- const { photopic, melanopic } = lightAt(epochMs, serialNo);
151
+ function logAt(epochMs, serialNo, deviceId, bodyPart, docked) {
152
+ const { photopic, melanopic } = docked ? { photopic: 0, melanopic: 0 } : lightAt(epochMs, serialNo);
152
153
  return {
153
154
  id: Math.floor(epochMs / 1e3) * 10 + serialNo % 10,
154
155
  log_time: formatLocalTimestamp(new Date(epochMs)),
155
156
  log_type: 1,
156
- body_part: "wrist",
157
+ body_part: bodyPart,
158
+ // The device's raw wear flag; the name misleads: 1 = worn, 0 = on the dock.
159
+ off_wrist: docked ? 0 : 1,
157
160
  temperature: 31.5,
158
161
  lux_photopic: photopic,
159
162
  lux_melanopic: melanopic,
@@ -161,6 +164,67 @@ function logAt(epochMs, serialNo, deviceId) {
161
164
  act_data_pim: 120
162
165
  };
163
166
  }
167
+ function spectrum(nm, photopic) {
168
+ const out = {};
169
+ for (const w of nm) {
170
+ const weight = Math.exp(-(((w - 555) / 120) ** 2));
171
+ out[`wavelength${w}`] = Math.round(photopic * weight * 0.01 * 1e4) / 1e4;
172
+ }
173
+ return out;
174
+ }
175
+ var GEN1_NM = [390, 415, 445, 480, 515, 555, 590, 630, 680, 750];
176
+ var GEN2_NM = [395, 426, 457, 488, 520, 551, 582, 613, 644, 675, 707, 738, 769, 800];
177
+ function lightSensorsReading(device, epochMs = Date.now(), withDeviceId = true) {
178
+ const { photopic, melanopic } = lightAt(epochMs, device.serialNo);
179
+ const raw = {
180
+ light_sensor_f1: 0.01,
181
+ light_sensor_f2: 0.02,
182
+ light_sensor_f3: 0.03,
183
+ light_sensor_f4: 0.04,
184
+ light_sensor_f5: 0.05,
185
+ light_sensor_f6: 0.04,
186
+ light_sensor_f7: 0.03,
187
+ light_sensor_f8: 0.02,
188
+ light_sensor_clear: Math.round(photopic * 2e-4 * 1e4) / 1e4,
189
+ light_sensor_nir: 0.01
190
+ };
191
+ const address = {
192
+ serialNo: device.serialNo,
193
+ ...withDeviceId ? { deviceId: device.deviceId } : {}
194
+ };
195
+ if (device.modelId === "actlumus_plus") {
196
+ return {
197
+ ...address,
198
+ type: "actlumus2",
199
+ data: {
200
+ ...raw,
201
+ ...spectrum(GEN2_NM, photopic),
202
+ time: epochMs,
203
+ photopic,
204
+ melanopic,
205
+ light_sensor_z: 9e-3,
206
+ light_sensor_y: 0.038,
207
+ light_sensor_x: 0.044,
208
+ light_sensor_fd: 0
209
+ }
210
+ };
211
+ }
212
+ return {
213
+ ...address,
214
+ type: "actlumus",
215
+ data: {
216
+ ...raw,
217
+ ...spectrum(GEN1_NM, photopic),
218
+ time: epochMs,
219
+ photopic,
220
+ melanopic,
221
+ light_sensor_z: null,
222
+ light_sensor_y: null,
223
+ light_sensor_x: null,
224
+ light_sensor_fd: null
225
+ }
226
+ };
227
+ }
164
228
  var HISTORY_DAYS = 14;
165
229
  var encodeCursor = (epochMs) => `mock:${epochMs}`;
166
230
  var decodeCursor = (cursor) => Number(cursor.slice("mock:".length));
@@ -175,7 +239,15 @@ function sensorLogPage(query, opts = {}) {
175
239
  return { requestId: query.requestId, success: false, error: "CONFLICTING_CURSORS" };
176
240
  }
177
241
  const newest = Math.floor(now / periodMs) * periodMs;
178
- const oldest = newest - HISTORY_DAYS * 864e5;
242
+ const history = opts.history ?? {};
243
+ const oldest = newest - (history.days ?? HISTORY_DAYS) * 864e5;
244
+ const dock = new Set(history.dockHours ?? [7]);
245
+ const gapHours = new Set(history.gapHours ?? []);
246
+ const gapDates = new Set(history.gapDates ?? []);
247
+ const present = (t) => {
248
+ const d = new Date(t);
249
+ return !gapHours.has(d.getHours()) && !gapDates.has(formatLocalTimestamp(d).slice(0, 10));
250
+ };
179
251
  const rangeStart = query.startTime ? parseLocalTimestamp(query.startTime) : oldest;
180
252
  const rangeEnd = query.endTime ? parseLocalTimestamp(query.endTime) - 1 : newest;
181
253
  if (Number.isNaN(rangeStart) || Number.isNaN(rangeEnd) || rangeStart > rangeEnd + 1) {
@@ -183,33 +255,41 @@ function sensorLogPage(query, opts = {}) {
183
255
  }
184
256
  const first = Math.max(oldest, Math.ceil(rangeStart / periodMs) * periodMs);
185
257
  const last = Math.min(newest, Math.floor(rangeEnd / periodMs) * periodMs);
186
- let from;
187
- let to;
258
+ const walk = (start, step, max) => {
259
+ const out = [];
260
+ for (let t = start; t >= first && t <= last && out.length < max; t += step) {
261
+ if (present(t)) out.push(t);
262
+ }
263
+ return out;
264
+ };
265
+ let slots;
188
266
  if (query.endingBefore) {
189
- to = Math.min(last, decodeCursor(query.endingBefore) - periodMs);
190
- from = Math.max(first, to - (limit - 1) * periodMs);
267
+ slots = walk(
268
+ Math.min(last, decodeCursor(query.endingBefore) - periodMs),
269
+ -periodMs,
270
+ limit
271
+ ).reverse();
191
272
  } else if (query.startingAfter) {
192
- from = Math.max(first, decodeCursor(query.startingAfter) + periodMs);
193
- to = Math.min(last, from + (limit - 1) * periodMs);
273
+ slots = walk(Math.max(first, decodeCursor(query.startingAfter) + periodMs), periodMs, limit);
194
274
  } else if (query.startTime) {
195
- from = first;
196
- to = Math.min(last, from + (limit - 1) * periodMs);
275
+ slots = walk(first, periodMs, limit);
197
276
  } else {
198
- to = last;
199
- from = Math.max(first, to - (limit - 1) * periodMs);
200
- }
201
- const logs = [];
202
- for (let t = from; t <= to; t += periodMs) {
203
- logs.push(logAt(t, query.serialNo, opts.deviceId ?? 0));
277
+ slots = walk(last, -periodMs, limit).reverse();
204
278
  }
279
+ const bodyPart = opts.bodyPart ?? "wrist";
280
+ const logs = slots.map(
281
+ (t) => logAt(t, query.serialNo, opts.deviceId ?? 0, bodyPart, dock.has(new Date(t).getHours()))
282
+ );
283
+ const from = slots[0] ?? 0;
284
+ const to = slots[slots.length - 1] ?? 0;
205
285
  return {
206
286
  requestId: query.requestId,
207
287
  success: true,
208
288
  logs,
209
289
  startingAfter: logs.length ? encodeCursor(to) : null,
210
290
  endingBefore: logs.length ? encodeCursor(from) : null,
211
- hasMore: logs.length > 0 && to < last,
212
- hasPrevious: logs.length > 0 && from > first
291
+ hasMore: logs.length > 0 && walk(to + periodMs, periodMs, 1).length > 0,
292
+ hasPrevious: logs.length > 0 && walk(from - periodMs, -periodMs, 1).length > 0
213
293
  };
214
294
  }
215
295
 
@@ -1203,6 +1283,19 @@ var BridgeShell = class {
1203
1283
  }
1204
1284
  };
1205
1285
 
1286
+ // src/version.ts
1287
+ function versionAtLeast(version, min) {
1288
+ if (!version) return false;
1289
+ const a = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
1290
+ const b = min.split(".").map((n) => Number.parseInt(n, 10) || 0);
1291
+ for (let i = 0; i < 3; i++) {
1292
+ const x = a[i] ?? 0;
1293
+ const y = b[i] ?? 0;
1294
+ if (x !== y) return x > y;
1295
+ }
1296
+ return true;
1297
+ }
1298
+
1206
1299
  // src/dev/policy.ts
1207
1300
  var CANVAS_POLICY = {
1208
1301
  readOnlyKeys: [
@@ -1238,28 +1331,27 @@ var CANVAS_POLICY = {
1238
1331
  var CANVAS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none';";
1239
1332
 
1240
1333
  // src/dev/scenarios.ts
1241
- var HARNESS_NATIVE_VERSION = "0.17.0";
1334
+ var HARNESS_NATIVE_VERSION = "0.18.0";
1242
1335
  var base = {
1243
- description: "Two devices, background service already running, realistic latency.",
1336
+ description: "Two devices, background service already running, realistic latency. The first device starts a BLE session 3 s after load; the second stays between sessions.",
1244
1337
  nativeVersion: HARNESS_NATIVE_VERSION,
1245
1338
  latencyMs: [80, 250],
1246
1339
  serviceStartMs: 0,
1247
- userDeviceSilent: false,
1248
1340
  logPeriodS: 60,
1249
1341
  systemStatus: systemStatus(),
1250
1342
  devices: () => twoDevices()
1251
1343
  };
1252
1344
  var SCENARIOS = {
1253
- default: base,
1345
+ default: { ...base, firstSessionAfterMs: 3e3 },
1254
1346
  "cold-start": {
1255
1347
  ...base,
1256
- description: "App launched from cold: the background service takes 4 s to start, and every ping/userDevice sent before that is lost. A bundle that asks once stays on its loading screen forever.",
1348
+ description: "App launched from cold: the background service takes 4 s to start, and every ping/userDevice sent before that is lost. From nativeVersion 0.18.0 the app re-sends the lost userDevice itself; below it, a bundle that asks once stays on its loading screen forever.",
1257
1349
  serviceStartMs: 4e3
1258
1350
  },
1259
1351
  "no-devices": {
1260
1352
  ...base,
1261
- description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
1262
- userDeviceSilent: true,
1353
+ description: "Account with no linked device: userDevice answers with error NO_DEVICES (below nativeVersion 0.18.0: never answers) and systemStatus.hasDevices is false. Your bundle must show an empty state.",
1354
+ userDeviceError: "NO_DEVICES",
1263
1355
  systemStatus: systemStatus({ hasDevices: false })
1264
1356
  },
1265
1357
  "shared-serial": {
@@ -1269,9 +1361,8 @@ var SCENARIOS = {
1269
1361
  },
1270
1362
  offline: {
1271
1363
  ...base,
1272
- description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
1273
- sensorLogError: "Connection failed",
1274
- systemStatus: systemStatus({ internet: false })
1364
+ description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache. The internet comes back when a test calls window.condorDev.setStatus({ internet: true }).",
1365
+ systemStatus: systemStatus({ internet: false, hasDevicesKnown: false })
1275
1366
  }
1276
1367
  };
1277
1368
  function resolveScenario(scenario) {
@@ -1290,6 +1381,7 @@ function resolveScenario(scenario) {
1290
1381
 
1291
1382
  // src/dev/harness.ts
1292
1383
  var DEV_HARNESS_MARKER = "__condorDevHarness__";
1384
+ var STEP_MS = 200;
1293
1385
  function createSimulatedNative(adapter, scenario, hooks = {}) {
1294
1386
  const now = hooks.now ?? Date.now;
1295
1387
  const startedAt = now();
@@ -1297,6 +1389,25 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1297
1389
  const timers = /* @__PURE__ */ new Set();
1298
1390
  let status = { ...scenario.systemStatus };
1299
1391
  let canGoBack = false;
1392
+ const alwaysReplies = versionAtLeast(scenario.nativeVersion, "0.18.0");
1393
+ let pendingUserDevice = false;
1394
+ let sensorLogCount = 0;
1395
+ const roster = scenario.devices().devicesConfig;
1396
+ const connected = new Map(roster.map((d) => [d.deviceId, false]));
1397
+ const subscribed = /* @__PURE__ */ new Set();
1398
+ const realTimeOn = /* @__PURE__ */ new Set();
1399
+ const streams = /* @__PURE__ */ new Map();
1400
+ const byId = versionAtLeast(scenario.nativeVersion, "0.17.0");
1401
+ const pendingSync = /* @__PURE__ */ new Set();
1402
+ const pendingErase = /* @__PURE__ */ new Set();
1403
+ const running = /* @__PURE__ */ new Set();
1404
+ const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
1405
+ const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
1406
+ const statusForBundle = () => {
1407
+ if (alwaysReplies) return status;
1408
+ const { hasDevicesKnown: _omitted, ...older } = status;
1409
+ return older;
1410
+ };
1300
1411
  const record2 = (entry) => {
1301
1412
  const full = { at: now(), ...entry };
1302
1413
  log.push(full);
@@ -1311,12 +1422,34 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1311
1422
  const reply = (key, payload, delay = latency()) => {
1312
1423
  const timer = setTimeout(() => {
1313
1424
  timers.delete(timer);
1314
- record2({ kind: "reply", key, detail: `${key} \u2192 bundle` });
1425
+ if (typeof payload === "function") payload = payload();
1426
+ record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
1315
1427
  adapter.triggerEmit(key, payload);
1316
1428
  }, delay);
1317
1429
  timers.add(timer);
1318
1430
  };
1319
1431
  const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
1432
+ const answerUserDevice = () => {
1433
+ if (scenario.userDeviceError) {
1434
+ if (!alwaysReplies) {
1435
+ record2({
1436
+ kind: "dropped",
1437
+ command: "userDevice",
1438
+ detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
1439
+ });
1440
+ return;
1441
+ }
1442
+ reply("userDevice", {
1443
+ lastUpdate: now(),
1444
+ devicesConfig: [],
1445
+ devicesTelemetry: [],
1446
+ devicesUserConfig: [],
1447
+ error: scenario.userDeviceError
1448
+ });
1449
+ return;
1450
+ }
1451
+ reply("userDevice", scenario.devices());
1452
+ };
1320
1453
  const language = () => {
1321
1454
  const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
1322
1455
  const lang = deviceLocale.slice(0, 2);
@@ -1333,7 +1466,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1333
1466
  if (command !== "handshake") {
1334
1467
  const shown = JSON.stringify(args);
1335
1468
  const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
1336
- record2({ kind: "sent", command, detail });
1469
+ record2({ kind: "sent", command, detail, args });
1337
1470
  }
1338
1471
  switch (command) {
1339
1472
  case "handshake":
@@ -1345,8 +1478,8 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1345
1478
  return;
1346
1479
  }
1347
1480
  reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
1348
- reply("systemStatus", status, latency() + 20);
1349
- reply("language", language(), latency() + 30);
1481
+ reply("systemStatus", statusForBundle, latency() + 20);
1482
+ reply("language", language, latency() + 30);
1350
1483
  return;
1351
1484
  case "ping":
1352
1485
  if (!serviceUp()) {
@@ -1361,30 +1494,33 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1361
1494
  return;
1362
1495
  case "userDevice":
1363
1496
  if (!serviceUp()) {
1497
+ pendingUserDevice = alwaysReplies;
1364
1498
  record2({
1365
1499
  kind: "dropped",
1366
1500
  command,
1367
- detail: "userDevice lost: the background service is not running yet. Ask again."
1368
- });
1369
- return;
1370
- }
1371
- if (scenario.userDeviceSilent) {
1372
- record2({
1373
- kind: "dropped",
1374
- command,
1375
- detail: "userDevice unanswered: the account has no linked device (the app replies only on success)"
1501
+ detail: alwaysReplies ? "userDevice lost: the background service is not running yet; the app re-sends it when the service is up" : "userDevice lost: the background service is not running yet. Ask again."
1376
1502
  });
1377
1503
  return;
1378
1504
  }
1379
- reply("userDevice", scenario.devices());
1505
+ answerUserDevice();
1380
1506
  return;
1381
1507
  case "sensorLog": {
1382
1508
  const query = args;
1383
- if (scenario.sensorLogError) {
1509
+ sensorLogCount += 1;
1510
+ if (scenario.sensorLogFailOnRequest === sensorLogCount) {
1384
1511
  reply("sensorLog", {
1385
1512
  requestId: query.requestId,
1386
1513
  success: false,
1387
- error: scenario.sensorLogError
1514
+ error: "Connection failed"
1515
+ });
1516
+ return;
1517
+ }
1518
+ const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
1519
+ if (logError) {
1520
+ reply("sensorLog", {
1521
+ requestId: query.requestId,
1522
+ success: false,
1523
+ error: logError
1388
1524
  });
1389
1525
  return;
1390
1526
  }
@@ -1403,12 +1539,29 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1403
1539
  sensorLogPage(query, {
1404
1540
  now: now(),
1405
1541
  periodS: scenario.logPeriodS,
1406
- deviceId: device?.deviceId
1542
+ deviceId: device?.deviceId,
1543
+ bodyPart: device?.deviceBodyPart ?? "wrist",
1544
+ history: scenario.history
1407
1545
  })
1408
1546
  );
1409
1547
  return;
1410
1548
  }
1411
- case "realTime":
1549
+ case "realTime": {
1550
+ const device = findDevice(args);
1551
+ if (device) {
1552
+ if (args.value === true) realTimeOn.add(device.deviceId);
1553
+ else realTimeOn.delete(device.deviceId);
1554
+ syncStream(device.deviceId);
1555
+ if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
1556
+ endSession(device.deviceId, latency());
1557
+ }
1558
+ if (args.value === true && !connected.get(device.deviceId)) {
1559
+ record2({
1560
+ kind: "info",
1561
+ detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
1562
+ });
1563
+ }
1564
+ }
1412
1565
  reply("realTime", {
1413
1566
  serialNo: args.serialNo,
1414
1567
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
@@ -1416,23 +1569,37 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1416
1569
  value: args.value === true
1417
1570
  });
1418
1571
  return;
1572
+ }
1419
1573
  case "syncTime":
1420
1574
  case "deleteMemory":
1575
+ {
1576
+ const target = findDevice(args);
1577
+ if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
1578
+ }
1579
+ record2({
1580
+ kind: "info",
1581
+ detail: command === "deleteMemory" ? "deleteMemory recorded: the app erases the memory in the next sync, after the data is uploaded" : "syncTime recorded: the app sets the clock in the next sync"
1582
+ });
1421
1583
  reply(command, {
1422
1584
  serialNo: args.serialNo,
1423
1585
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1424
1586
  success: true
1425
1587
  });
1426
1588
  return;
1427
- case "deviceConnection":
1428
- if (args.type === "add") {
1589
+ case "deviceConnection": {
1590
+ const device = findDevice(args);
1591
+ if (!device) {
1429
1592
  reply("deviceConnection", {
1430
1593
  serialNo: args.serialNo,
1431
1594
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1432
- isConnect: true
1595
+ isConnect: false
1433
1596
  });
1597
+ return;
1434
1598
  }
1599
+ if (args.type === "add") subscribed.add(device.deviceId);
1600
+ else if (args.type === "remove") subscribed.delete(device.deviceId);
1435
1601
  return;
1602
+ }
1436
1603
  case "language":
1437
1604
  reply("language", language());
1438
1605
  return;
@@ -1443,7 +1610,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1443
1610
  missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
1444
1611
  missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
1445
1612
  };
1446
- reply("systemStatus", status, latency() + 400);
1613
+ reply("systemStatus", statusForBundle, latency() + 400);
1447
1614
  return;
1448
1615
  }
1449
1616
  case "jsError":
@@ -1467,11 +1634,157 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1467
1634
  return;
1468
1635
  }
1469
1636
  };
1637
+ function emitConnection(deviceId) {
1638
+ if (!subscribed.has(deviceId)) return;
1639
+ const device = roster.find((d) => d.deviceId === deviceId);
1640
+ if (!device) return;
1641
+ reply("deviceConnection", {
1642
+ serialNo: device.serialNo,
1643
+ ...idOf(device),
1644
+ isConnect: connected.get(deviceId) === true
1645
+ });
1646
+ }
1647
+ function syncStream(deviceId) {
1648
+ const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
1649
+ const running2 = streams.get(deviceId);
1650
+ if (want && !running2) {
1651
+ const device = roster.find((d) => d.deviceId === deviceId);
1652
+ if (!device) return;
1653
+ streams.set(
1654
+ deviceId,
1655
+ setInterval(() => {
1656
+ adapter.triggerEmit(
1657
+ "deviceLightSensors",
1658
+ lightSensorsReading(device, now(), "deviceId" in idOf(device))
1659
+ );
1660
+ }, 1e3)
1661
+ );
1662
+ } else if (!want && running2) {
1663
+ clearInterval(running2);
1664
+ streams.delete(deviceId);
1665
+ }
1666
+ }
1667
+ function setConnected(deviceId, value) {
1668
+ if (!connected.has(deviceId)) {
1669
+ record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
1670
+ return;
1671
+ }
1672
+ if (connected.get(deviceId) === value) return;
1673
+ connected.set(deviceId, value);
1674
+ record2({
1675
+ kind: "info",
1676
+ detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
1677
+ });
1678
+ emitConnection(deviceId);
1679
+ syncStream(deviceId);
1680
+ const device = roster.find((d) => d.deviceId === deviceId);
1681
+ if (device) runSession(device, value);
1682
+ }
1683
+ function setSyncError(device, text, delay) {
1684
+ const timer = setTimeout(() => {
1685
+ timers.delete(timer);
1686
+ const byId2 = { ...status.deviceSyncErrorById ?? {} };
1687
+ const bySerial = { ...status.deviceSyncError };
1688
+ const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
1689
+ if (text === null) {
1690
+ if (!(device.deviceId in byId2)) return;
1691
+ delete byId2[device.deviceId];
1692
+ if (!shared) delete bySerial[device.serialNo];
1693
+ } else {
1694
+ byId2[device.deviceId] = text;
1695
+ if (!shared) bySerial[device.serialNo] = text;
1696
+ }
1697
+ status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
1698
+ reply("systemStatus", statusForBundle, 0);
1699
+ }, delay);
1700
+ timers.add(timer);
1701
+ }
1702
+ function runSession(device, start) {
1703
+ const note = (message, messageCode, isError = false) => ({
1704
+ serialNo: device.serialNo,
1705
+ ...idOf(device),
1706
+ isConnect: !isError,
1707
+ message,
1708
+ messageCode,
1709
+ isError
1710
+ });
1711
+ if (!start) {
1712
+ reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
1713
+ return;
1714
+ }
1715
+ const id = device.deviceId;
1716
+ const steps = [
1717
+ ["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
1718
+ ["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
1719
+ ["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
1720
+ ["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
1721
+ ["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
1722
+ ...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
1723
+ ["[Start] Get data log", "get_data_log_notification", "get_data_log"],
1724
+ ["[Start] Send data log", "send_data_log_notification", "send_data_log"],
1725
+ [
1726
+ "[Success] Set last data log sent to server",
1727
+ "set_last_data_log_sent_to_server_notification",
1728
+ ""
1729
+ ],
1730
+ ["[Start] Get system log", "get_system_log_notification", "get_system_log"],
1731
+ ["[Start] Send system log", "send_system_log_notification", "send_system_log"],
1732
+ ["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
1733
+ ["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
1734
+ ["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
1735
+ ...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
1736
+ ["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
1737
+ ["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
1738
+ ];
1739
+ const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
1740
+ running.add(id);
1741
+ let delay = latency();
1742
+ reply("deviceNotification", note("Device connected", null), delay);
1743
+ for (const [message, code, stage] of steps) {
1744
+ delay += STEP_MS;
1745
+ reply("deviceNotification", note(message, code), delay);
1746
+ if (stage && failAt === stage) {
1747
+ delay += STEP_MS;
1748
+ const what = message.replace(/^\[\w+\] /, "");
1749
+ reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
1750
+ setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
1751
+ endSession(id, delay + STEP_MS);
1752
+ return;
1753
+ }
1754
+ if (stage === "set_sync_time") pendingSync.delete(id);
1755
+ }
1756
+ delay += STEP_MS;
1757
+ reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
1758
+ setSyncError(device, null, delay + 20);
1759
+ pendingErase.delete(id);
1760
+ endSession(id, delay + STEP_MS);
1761
+ }
1762
+ function endSession(deviceId, delay) {
1763
+ const timer = setTimeout(() => {
1764
+ timers.delete(timer);
1765
+ running.delete(deviceId);
1766
+ if (!realTimeOn.has(deviceId)) setConnected(deviceId, false);
1767
+ }, delay);
1768
+ timers.add(timer);
1769
+ }
1470
1770
  const unsubscribe = adapter.onCallHandler(handle);
1771
+ const first = roster[0];
1772
+ if (first && typeof scenario.firstSessionAfterMs === "number") {
1773
+ const timer = setTimeout(() => {
1774
+ timers.delete(timer);
1775
+ setConnected(first.deviceId, true);
1776
+ }, scenario.firstSessionAfterMs);
1777
+ timers.add(timer);
1778
+ }
1471
1779
  if (scenario.serviceStartMs > 0) {
1472
1780
  const timer = setTimeout(() => {
1473
1781
  timers.delete(timer);
1474
1782
  record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
1783
+ if (pendingUserDevice) {
1784
+ pendingUserDevice = false;
1785
+ record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
1786
+ answerUserDevice();
1787
+ }
1475
1788
  }, scenario.serviceStartMs);
1476
1789
  timers.add(timer);
1477
1790
  }
@@ -1487,8 +1800,15 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1487
1800
  }
1488
1801
  reply("navigationBack", {}, 0);
1489
1802
  },
1803
+ setStatus(patch) {
1804
+ status = { ...status, ...patch };
1805
+ reply("systemStatus", statusForBundle(), 0);
1806
+ },
1807
+ setConnected,
1490
1808
  dispose() {
1491
1809
  unsubscribe();
1810
+ for (const t of streams.values()) clearInterval(t);
1811
+ streams.clear();
1492
1812
  for (const t of timers) clearTimeout(t);
1493
1813
  timers.clear();
1494
1814
  }
@@ -1548,15 +1868,18 @@ async function createDevHarness(opts) {
1548
1868
  iframe.setAttribute("title", "Condor Canvas bundle");
1549
1869
  iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
1550
1870
  (opts.mount ?? document.body).appendChild(iframe);
1871
+ let confirmations = "ask";
1551
1872
  const shell = new BridgeShell({
1552
1873
  iframe,
1553
1874
  policy: CANVAS_POLICY,
1554
- onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
1875
+ onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
1555
1876
  });
1556
1877
  if (opts.devPanel !== false) {
1557
1878
  installDevPanel({ adapter, options: { startCollapsed: true } });
1558
1879
  }
1559
- await shell.bootstrapClientIframeWithHtml(opts.html);
1880
+ const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
1881
+ const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
1882
+ await shell.bootstrapClientIframeWithHtml(html);
1560
1883
  try {
1561
1884
  const result = await shell.verifySandbox();
1562
1885
  if (!result.ok) {
@@ -1581,6 +1904,15 @@ async function createDevHarness(opts) {
1581
1904
  emit(key, payload) {
1582
1905
  Bridge.mockEmit(key, payload);
1583
1906
  },
1907
+ setStatus(patch) {
1908
+ native.setStatus(patch);
1909
+ },
1910
+ setConnected(deviceId, value) {
1911
+ native.setConnected(deviceId, value);
1912
+ },
1913
+ setConfirmations(mode) {
1914
+ confirmations = mode;
1915
+ },
1584
1916
  pressBack() {
1585
1917
  native.pressBack();
1586
1918
  },
@@ -1601,6 +1933,7 @@ export {
1601
1933
  createDevHarness,
1602
1934
  createSimulatedNative,
1603
1935
  formatLocalTimestamp,
1936
+ lightSensorsReading,
1604
1937
  parseLocalTimestamp,
1605
1938
  resolveScenario,
1606
1939
  sensorLogPage,