@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.cjs CHANGED
@@ -39,6 +39,7 @@ __export(dev_exports, {
39
39
  createDevHarness: () => createDevHarness,
40
40
  createSimulatedNative: () => createSimulatedNative,
41
41
  formatLocalTimestamp: () => formatLocalTimestamp,
42
+ lightSensorsReading: () => lightSensorsReading,
42
43
  parseLocalTimestamp: () => parseLocalTimestamp,
43
44
  resolveScenario: () => resolveScenario,
44
45
  sensorLogPage: () => sensorLogPage,
@@ -98,7 +99,7 @@ function telemetry(spec, now) {
98
99
  serialNo: spec.serialNo,
99
100
  attemptDate: now - 10 * 6e4,
100
101
  message: spec.syncError,
101
- messageCode: "SYNC_FAILED"
102
+ messageCode: "send_data_log_error"
102
103
  }
103
104
  ] : []
104
105
  };
@@ -182,6 +183,7 @@ function systemStatus(overrides = {}) {
182
183
  internet: true,
183
184
  permission: true,
184
185
  hasDevices: true,
186
+ hasDevicesKnown: true,
185
187
  missingOptionalPermissions: [],
186
188
  missingEssentialPermissions: [],
187
189
  deviceSyncError: {},
@@ -197,13 +199,15 @@ function lightAt(epochMs, serialNo) {
197
199
  const photopic = Math.round((daylight * 1800 + noise * 40) * 10) / 10;
198
200
  return { photopic, melanopic: Math.round(photopic * 0.62 * 10) / 10 };
199
201
  }
200
- function logAt(epochMs, serialNo, deviceId) {
201
- const { photopic, melanopic } = lightAt(epochMs, serialNo);
202
+ function logAt(epochMs, serialNo, deviceId, bodyPart, docked) {
203
+ const { photopic, melanopic } = docked ? { photopic: 0, melanopic: 0 } : lightAt(epochMs, serialNo);
202
204
  return {
203
205
  id: Math.floor(epochMs / 1e3) * 10 + serialNo % 10,
204
206
  log_time: formatLocalTimestamp(new Date(epochMs)),
205
207
  log_type: 1,
206
- body_part: "wrist",
208
+ body_part: bodyPart,
209
+ // The device's raw wear flag; the name misleads: 1 = worn, 0 = on the dock.
210
+ off_wrist: docked ? 0 : 1,
207
211
  temperature: 31.5,
208
212
  lux_photopic: photopic,
209
213
  lux_melanopic: melanopic,
@@ -211,6 +215,67 @@ function logAt(epochMs, serialNo, deviceId) {
211
215
  act_data_pim: 120
212
216
  };
213
217
  }
218
+ function spectrum(nm, photopic) {
219
+ const out = {};
220
+ for (const w of nm) {
221
+ const weight = Math.exp(-(((w - 555) / 120) ** 2));
222
+ out[`wavelength${w}`] = Math.round(photopic * weight * 0.01 * 1e4) / 1e4;
223
+ }
224
+ return out;
225
+ }
226
+ var GEN1_NM = [390, 415, 445, 480, 515, 555, 590, 630, 680, 750];
227
+ var GEN2_NM = [395, 426, 457, 488, 520, 551, 582, 613, 644, 675, 707, 738, 769, 800];
228
+ function lightSensorsReading(device, epochMs = Date.now(), withDeviceId = true) {
229
+ const { photopic, melanopic } = lightAt(epochMs, device.serialNo);
230
+ const raw = {
231
+ light_sensor_f1: 0.01,
232
+ light_sensor_f2: 0.02,
233
+ light_sensor_f3: 0.03,
234
+ light_sensor_f4: 0.04,
235
+ light_sensor_f5: 0.05,
236
+ light_sensor_f6: 0.04,
237
+ light_sensor_f7: 0.03,
238
+ light_sensor_f8: 0.02,
239
+ light_sensor_clear: Math.round(photopic * 2e-4 * 1e4) / 1e4,
240
+ light_sensor_nir: 0.01
241
+ };
242
+ const address = {
243
+ serialNo: device.serialNo,
244
+ ...withDeviceId ? { deviceId: device.deviceId } : {}
245
+ };
246
+ if (device.modelId === "actlumus_plus") {
247
+ return {
248
+ ...address,
249
+ type: "actlumus2",
250
+ data: {
251
+ ...raw,
252
+ ...spectrum(GEN2_NM, photopic),
253
+ time: epochMs,
254
+ photopic,
255
+ melanopic,
256
+ light_sensor_z: 9e-3,
257
+ light_sensor_y: 0.038,
258
+ light_sensor_x: 0.044,
259
+ light_sensor_fd: 0
260
+ }
261
+ };
262
+ }
263
+ return {
264
+ ...address,
265
+ type: "actlumus",
266
+ data: {
267
+ ...raw,
268
+ ...spectrum(GEN1_NM, photopic),
269
+ time: epochMs,
270
+ photopic,
271
+ melanopic,
272
+ light_sensor_z: null,
273
+ light_sensor_y: null,
274
+ light_sensor_x: null,
275
+ light_sensor_fd: null
276
+ }
277
+ };
278
+ }
214
279
  var HISTORY_DAYS = 14;
215
280
  var encodeCursor = (epochMs) => `mock:${epochMs}`;
216
281
  var decodeCursor = (cursor) => Number(cursor.slice("mock:".length));
@@ -225,7 +290,15 @@ function sensorLogPage(query, opts = {}) {
225
290
  return { requestId: query.requestId, success: false, error: "CONFLICTING_CURSORS" };
226
291
  }
227
292
  const newest = Math.floor(now / periodMs) * periodMs;
228
- const oldest = newest - HISTORY_DAYS * 864e5;
293
+ const history = opts.history ?? {};
294
+ const oldest = newest - (history.days ?? HISTORY_DAYS) * 864e5;
295
+ const dock = new Set(history.dockHours ?? [7]);
296
+ const gapHours = new Set(history.gapHours ?? []);
297
+ const gapDates = new Set(history.gapDates ?? []);
298
+ const present = (t) => {
299
+ const d = new Date(t);
300
+ return !gapHours.has(d.getHours()) && !gapDates.has(formatLocalTimestamp(d).slice(0, 10));
301
+ };
229
302
  const rangeStart = query.startTime ? parseLocalTimestamp(query.startTime) : oldest;
230
303
  const rangeEnd = query.endTime ? parseLocalTimestamp(query.endTime) - 1 : newest;
231
304
  if (Number.isNaN(rangeStart) || Number.isNaN(rangeEnd) || rangeStart > rangeEnd + 1) {
@@ -233,33 +306,41 @@ function sensorLogPage(query, opts = {}) {
233
306
  }
234
307
  const first = Math.max(oldest, Math.ceil(rangeStart / periodMs) * periodMs);
235
308
  const last = Math.min(newest, Math.floor(rangeEnd / periodMs) * periodMs);
236
- let from;
237
- let to;
309
+ const walk = (start, step, max) => {
310
+ const out = [];
311
+ for (let t = start; t >= first && t <= last && out.length < max; t += step) {
312
+ if (present(t)) out.push(t);
313
+ }
314
+ return out;
315
+ };
316
+ let slots;
238
317
  if (query.endingBefore) {
239
- to = Math.min(last, decodeCursor(query.endingBefore) - periodMs);
240
- from = Math.max(first, to - (limit - 1) * periodMs);
318
+ slots = walk(
319
+ Math.min(last, decodeCursor(query.endingBefore) - periodMs),
320
+ -periodMs,
321
+ limit
322
+ ).reverse();
241
323
  } else if (query.startingAfter) {
242
- from = Math.max(first, decodeCursor(query.startingAfter) + periodMs);
243
- to = Math.min(last, from + (limit - 1) * periodMs);
324
+ slots = walk(Math.max(first, decodeCursor(query.startingAfter) + periodMs), periodMs, limit);
244
325
  } else if (query.startTime) {
245
- from = first;
246
- to = Math.min(last, from + (limit - 1) * periodMs);
326
+ slots = walk(first, periodMs, limit);
247
327
  } else {
248
- to = last;
249
- from = Math.max(first, to - (limit - 1) * periodMs);
250
- }
251
- const logs = [];
252
- for (let t = from; t <= to; t += periodMs) {
253
- logs.push(logAt(t, query.serialNo, opts.deviceId ?? 0));
328
+ slots = walk(last, -periodMs, limit).reverse();
254
329
  }
330
+ const bodyPart = opts.bodyPart ?? "wrist";
331
+ const logs = slots.map(
332
+ (t) => logAt(t, query.serialNo, opts.deviceId ?? 0, bodyPart, dock.has(new Date(t).getHours()))
333
+ );
334
+ const from = slots[0] ?? 0;
335
+ const to = slots[slots.length - 1] ?? 0;
255
336
  return {
256
337
  requestId: query.requestId,
257
338
  success: true,
258
339
  logs,
259
340
  startingAfter: logs.length ? encodeCursor(to) : null,
260
341
  endingBefore: logs.length ? encodeCursor(from) : null,
261
- hasMore: logs.length > 0 && to < last,
262
- hasPrevious: logs.length > 0 && from > first
342
+ hasMore: logs.length > 0 && walk(to + periodMs, periodMs, 1).length > 0,
343
+ hasPrevious: logs.length > 0 && walk(from - periodMs, -periodMs, 1).length > 0
263
344
  };
264
345
  }
265
346
 
@@ -1253,6 +1334,19 @@ var BridgeShell = class {
1253
1334
  }
1254
1335
  };
1255
1336
 
1337
+ // src/version.ts
1338
+ function versionAtLeast(version, min) {
1339
+ if (!version) return false;
1340
+ const a = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
1341
+ const b = min.split(".").map((n) => Number.parseInt(n, 10) || 0);
1342
+ for (let i = 0; i < 3; i++) {
1343
+ const x = a[i] ?? 0;
1344
+ const y = b[i] ?? 0;
1345
+ if (x !== y) return x > y;
1346
+ }
1347
+ return true;
1348
+ }
1349
+
1256
1350
  // src/dev/policy.ts
1257
1351
  var CANVAS_POLICY = {
1258
1352
  readOnlyKeys: [
@@ -1288,28 +1382,27 @@ var CANVAS_POLICY = {
1288
1382
  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';";
1289
1383
 
1290
1384
  // src/dev/scenarios.ts
1291
- var HARNESS_NATIVE_VERSION = "0.17.0";
1385
+ var HARNESS_NATIVE_VERSION = "0.18.0";
1292
1386
  var base = {
1293
- description: "Two devices, background service already running, realistic latency.",
1387
+ 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.",
1294
1388
  nativeVersion: HARNESS_NATIVE_VERSION,
1295
1389
  latencyMs: [80, 250],
1296
1390
  serviceStartMs: 0,
1297
- userDeviceSilent: false,
1298
1391
  logPeriodS: 60,
1299
1392
  systemStatus: systemStatus(),
1300
1393
  devices: () => twoDevices()
1301
1394
  };
1302
1395
  var SCENARIOS = {
1303
- default: base,
1396
+ default: { ...base, firstSessionAfterMs: 3e3 },
1304
1397
  "cold-start": {
1305
1398
  ...base,
1306
- 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.",
1399
+ 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.",
1307
1400
  serviceStartMs: 4e3
1308
1401
  },
1309
1402
  "no-devices": {
1310
1403
  ...base,
1311
- description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
1312
- userDeviceSilent: true,
1404
+ 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.",
1405
+ userDeviceError: "NO_DEVICES",
1313
1406
  systemStatus: systemStatus({ hasDevices: false })
1314
1407
  },
1315
1408
  "shared-serial": {
@@ -1319,9 +1412,8 @@ var SCENARIOS = {
1319
1412
  },
1320
1413
  offline: {
1321
1414
  ...base,
1322
- description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
1323
- sensorLogError: "Connection failed",
1324
- systemStatus: systemStatus({ internet: false })
1415
+ 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 }).",
1416
+ systemStatus: systemStatus({ internet: false, hasDevicesKnown: false })
1325
1417
  }
1326
1418
  };
1327
1419
  function resolveScenario(scenario) {
@@ -1340,6 +1432,7 @@ function resolveScenario(scenario) {
1340
1432
 
1341
1433
  // src/dev/harness.ts
1342
1434
  var DEV_HARNESS_MARKER = "__condorDevHarness__";
1435
+ var STEP_MS = 200;
1343
1436
  function createSimulatedNative(adapter, scenario, hooks = {}) {
1344
1437
  const now = hooks.now ?? Date.now;
1345
1438
  const startedAt = now();
@@ -1347,6 +1440,25 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1347
1440
  const timers = /* @__PURE__ */ new Set();
1348
1441
  let status = { ...scenario.systemStatus };
1349
1442
  let canGoBack = false;
1443
+ const alwaysReplies = versionAtLeast(scenario.nativeVersion, "0.18.0");
1444
+ let pendingUserDevice = false;
1445
+ let sensorLogCount = 0;
1446
+ const roster = scenario.devices().devicesConfig;
1447
+ const connected = new Map(roster.map((d) => [d.deviceId, false]));
1448
+ const subscribed = /* @__PURE__ */ new Set();
1449
+ const realTimeOn = /* @__PURE__ */ new Set();
1450
+ const streams = /* @__PURE__ */ new Map();
1451
+ const byId = versionAtLeast(scenario.nativeVersion, "0.17.0");
1452
+ const pendingSync = /* @__PURE__ */ new Set();
1453
+ const pendingErase = /* @__PURE__ */ new Set();
1454
+ const running = /* @__PURE__ */ new Set();
1455
+ const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
1456
+ const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
1457
+ const statusForBundle = () => {
1458
+ if (alwaysReplies) return status;
1459
+ const { hasDevicesKnown: _omitted, ...older } = status;
1460
+ return older;
1461
+ };
1350
1462
  const record2 = (entry) => {
1351
1463
  const full = { at: now(), ...entry };
1352
1464
  log.push(full);
@@ -1361,12 +1473,34 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1361
1473
  const reply = (key, payload, delay = latency()) => {
1362
1474
  const timer = setTimeout(() => {
1363
1475
  timers.delete(timer);
1364
- record2({ kind: "reply", key, detail: `${key} \u2192 bundle` });
1476
+ if (typeof payload === "function") payload = payload();
1477
+ record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
1365
1478
  adapter.triggerEmit(key, payload);
1366
1479
  }, delay);
1367
1480
  timers.add(timer);
1368
1481
  };
1369
1482
  const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
1483
+ const answerUserDevice = () => {
1484
+ if (scenario.userDeviceError) {
1485
+ if (!alwaysReplies) {
1486
+ record2({
1487
+ kind: "dropped",
1488
+ command: "userDevice",
1489
+ detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
1490
+ });
1491
+ return;
1492
+ }
1493
+ reply("userDevice", {
1494
+ lastUpdate: now(),
1495
+ devicesConfig: [],
1496
+ devicesTelemetry: [],
1497
+ devicesUserConfig: [],
1498
+ error: scenario.userDeviceError
1499
+ });
1500
+ return;
1501
+ }
1502
+ reply("userDevice", scenario.devices());
1503
+ };
1370
1504
  const language = () => {
1371
1505
  const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
1372
1506
  const lang = deviceLocale.slice(0, 2);
@@ -1383,7 +1517,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1383
1517
  if (command !== "handshake") {
1384
1518
  const shown = JSON.stringify(args);
1385
1519
  const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
1386
- record2({ kind: "sent", command, detail });
1520
+ record2({ kind: "sent", command, detail, args });
1387
1521
  }
1388
1522
  switch (command) {
1389
1523
  case "handshake":
@@ -1395,8 +1529,8 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1395
1529
  return;
1396
1530
  }
1397
1531
  reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
1398
- reply("systemStatus", status, latency() + 20);
1399
- reply("language", language(), latency() + 30);
1532
+ reply("systemStatus", statusForBundle, latency() + 20);
1533
+ reply("language", language, latency() + 30);
1400
1534
  return;
1401
1535
  case "ping":
1402
1536
  if (!serviceUp()) {
@@ -1411,30 +1545,33 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1411
1545
  return;
1412
1546
  case "userDevice":
1413
1547
  if (!serviceUp()) {
1548
+ pendingUserDevice = alwaysReplies;
1414
1549
  record2({
1415
1550
  kind: "dropped",
1416
1551
  command,
1417
- detail: "userDevice lost: the background service is not running yet. Ask again."
1418
- });
1419
- return;
1420
- }
1421
- if (scenario.userDeviceSilent) {
1422
- record2({
1423
- kind: "dropped",
1424
- command,
1425
- detail: "userDevice unanswered: the account has no linked device (the app replies only on success)"
1552
+ 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."
1426
1553
  });
1427
1554
  return;
1428
1555
  }
1429
- reply("userDevice", scenario.devices());
1556
+ answerUserDevice();
1430
1557
  return;
1431
1558
  case "sensorLog": {
1432
1559
  const query = args;
1433
- if (scenario.sensorLogError) {
1560
+ sensorLogCount += 1;
1561
+ if (scenario.sensorLogFailOnRequest === sensorLogCount) {
1434
1562
  reply("sensorLog", {
1435
1563
  requestId: query.requestId,
1436
1564
  success: false,
1437
- error: scenario.sensorLogError
1565
+ error: "Connection failed"
1566
+ });
1567
+ return;
1568
+ }
1569
+ const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
1570
+ if (logError) {
1571
+ reply("sensorLog", {
1572
+ requestId: query.requestId,
1573
+ success: false,
1574
+ error: logError
1438
1575
  });
1439
1576
  return;
1440
1577
  }
@@ -1453,12 +1590,29 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1453
1590
  sensorLogPage(query, {
1454
1591
  now: now(),
1455
1592
  periodS: scenario.logPeriodS,
1456
- deviceId: device?.deviceId
1593
+ deviceId: device?.deviceId,
1594
+ bodyPart: device?.deviceBodyPart ?? "wrist",
1595
+ history: scenario.history
1457
1596
  })
1458
1597
  );
1459
1598
  return;
1460
1599
  }
1461
- case "realTime":
1600
+ case "realTime": {
1601
+ const device = findDevice(args);
1602
+ if (device) {
1603
+ if (args.value === true) realTimeOn.add(device.deviceId);
1604
+ else realTimeOn.delete(device.deviceId);
1605
+ syncStream(device.deviceId);
1606
+ if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
1607
+ endSession(device.deviceId, latency());
1608
+ }
1609
+ if (args.value === true && !connected.get(device.deviceId)) {
1610
+ record2({
1611
+ kind: "info",
1612
+ detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
1613
+ });
1614
+ }
1615
+ }
1462
1616
  reply("realTime", {
1463
1617
  serialNo: args.serialNo,
1464
1618
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
@@ -1466,23 +1620,37 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1466
1620
  value: args.value === true
1467
1621
  });
1468
1622
  return;
1623
+ }
1469
1624
  case "syncTime":
1470
1625
  case "deleteMemory":
1626
+ {
1627
+ const target = findDevice(args);
1628
+ if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
1629
+ }
1630
+ record2({
1631
+ kind: "info",
1632
+ 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"
1633
+ });
1471
1634
  reply(command, {
1472
1635
  serialNo: args.serialNo,
1473
1636
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1474
1637
  success: true
1475
1638
  });
1476
1639
  return;
1477
- case "deviceConnection":
1478
- if (args.type === "add") {
1640
+ case "deviceConnection": {
1641
+ const device = findDevice(args);
1642
+ if (!device) {
1479
1643
  reply("deviceConnection", {
1480
1644
  serialNo: args.serialNo,
1481
1645
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1482
- isConnect: true
1646
+ isConnect: false
1483
1647
  });
1648
+ return;
1484
1649
  }
1650
+ if (args.type === "add") subscribed.add(device.deviceId);
1651
+ else if (args.type === "remove") subscribed.delete(device.deviceId);
1485
1652
  return;
1653
+ }
1486
1654
  case "language":
1487
1655
  reply("language", language());
1488
1656
  return;
@@ -1493,7 +1661,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1493
1661
  missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
1494
1662
  missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
1495
1663
  };
1496
- reply("systemStatus", status, latency() + 400);
1664
+ reply("systemStatus", statusForBundle, latency() + 400);
1497
1665
  return;
1498
1666
  }
1499
1667
  case "jsError":
@@ -1517,11 +1685,157 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1517
1685
  return;
1518
1686
  }
1519
1687
  };
1688
+ function emitConnection(deviceId) {
1689
+ if (!subscribed.has(deviceId)) return;
1690
+ const device = roster.find((d) => d.deviceId === deviceId);
1691
+ if (!device) return;
1692
+ reply("deviceConnection", {
1693
+ serialNo: device.serialNo,
1694
+ ...idOf(device),
1695
+ isConnect: connected.get(deviceId) === true
1696
+ });
1697
+ }
1698
+ function syncStream(deviceId) {
1699
+ const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
1700
+ const running2 = streams.get(deviceId);
1701
+ if (want && !running2) {
1702
+ const device = roster.find((d) => d.deviceId === deviceId);
1703
+ if (!device) return;
1704
+ streams.set(
1705
+ deviceId,
1706
+ setInterval(() => {
1707
+ adapter.triggerEmit(
1708
+ "deviceLightSensors",
1709
+ lightSensorsReading(device, now(), "deviceId" in idOf(device))
1710
+ );
1711
+ }, 1e3)
1712
+ );
1713
+ } else if (!want && running2) {
1714
+ clearInterval(running2);
1715
+ streams.delete(deviceId);
1716
+ }
1717
+ }
1718
+ function setConnected(deviceId, value) {
1719
+ if (!connected.has(deviceId)) {
1720
+ record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
1721
+ return;
1722
+ }
1723
+ if (connected.get(deviceId) === value) return;
1724
+ connected.set(deviceId, value);
1725
+ record2({
1726
+ kind: "info",
1727
+ detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
1728
+ });
1729
+ emitConnection(deviceId);
1730
+ syncStream(deviceId);
1731
+ const device = roster.find((d) => d.deviceId === deviceId);
1732
+ if (device) runSession(device, value);
1733
+ }
1734
+ function setSyncError(device, text, delay) {
1735
+ const timer = setTimeout(() => {
1736
+ timers.delete(timer);
1737
+ const byId2 = { ...status.deviceSyncErrorById ?? {} };
1738
+ const bySerial = { ...status.deviceSyncError };
1739
+ const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
1740
+ if (text === null) {
1741
+ if (!(device.deviceId in byId2)) return;
1742
+ delete byId2[device.deviceId];
1743
+ if (!shared) delete bySerial[device.serialNo];
1744
+ } else {
1745
+ byId2[device.deviceId] = text;
1746
+ if (!shared) bySerial[device.serialNo] = text;
1747
+ }
1748
+ status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
1749
+ reply("systemStatus", statusForBundle, 0);
1750
+ }, delay);
1751
+ timers.add(timer);
1752
+ }
1753
+ function runSession(device, start) {
1754
+ const note = (message, messageCode, isError = false) => ({
1755
+ serialNo: device.serialNo,
1756
+ ...idOf(device),
1757
+ isConnect: !isError,
1758
+ message,
1759
+ messageCode,
1760
+ isError
1761
+ });
1762
+ if (!start) {
1763
+ reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
1764
+ return;
1765
+ }
1766
+ const id = device.deviceId;
1767
+ const steps = [
1768
+ ["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
1769
+ ["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
1770
+ ["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
1771
+ ["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
1772
+ ["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
1773
+ ...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
1774
+ ["[Start] Get data log", "get_data_log_notification", "get_data_log"],
1775
+ ["[Start] Send data log", "send_data_log_notification", "send_data_log"],
1776
+ [
1777
+ "[Success] Set last data log sent to server",
1778
+ "set_last_data_log_sent_to_server_notification",
1779
+ ""
1780
+ ],
1781
+ ["[Start] Get system log", "get_system_log_notification", "get_system_log"],
1782
+ ["[Start] Send system log", "send_system_log_notification", "send_system_log"],
1783
+ ["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
1784
+ ["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
1785
+ ["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
1786
+ ...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
1787
+ ["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
1788
+ ["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
1789
+ ];
1790
+ const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
1791
+ running.add(id);
1792
+ let delay = latency();
1793
+ reply("deviceNotification", note("Device connected", null), delay);
1794
+ for (const [message, code, stage] of steps) {
1795
+ delay += STEP_MS;
1796
+ reply("deviceNotification", note(message, code), delay);
1797
+ if (stage && failAt === stage) {
1798
+ delay += STEP_MS;
1799
+ const what = message.replace(/^\[\w+\] /, "");
1800
+ reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
1801
+ setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
1802
+ endSession(id, delay + STEP_MS);
1803
+ return;
1804
+ }
1805
+ if (stage === "set_sync_time") pendingSync.delete(id);
1806
+ }
1807
+ delay += STEP_MS;
1808
+ reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
1809
+ setSyncError(device, null, delay + 20);
1810
+ pendingErase.delete(id);
1811
+ endSession(id, delay + STEP_MS);
1812
+ }
1813
+ function endSession(deviceId, delay) {
1814
+ const timer = setTimeout(() => {
1815
+ timers.delete(timer);
1816
+ running.delete(deviceId);
1817
+ if (!realTimeOn.has(deviceId)) setConnected(deviceId, false);
1818
+ }, delay);
1819
+ timers.add(timer);
1820
+ }
1520
1821
  const unsubscribe = adapter.onCallHandler(handle);
1822
+ const first = roster[0];
1823
+ if (first && typeof scenario.firstSessionAfterMs === "number") {
1824
+ const timer = setTimeout(() => {
1825
+ timers.delete(timer);
1826
+ setConnected(first.deviceId, true);
1827
+ }, scenario.firstSessionAfterMs);
1828
+ timers.add(timer);
1829
+ }
1521
1830
  if (scenario.serviceStartMs > 0) {
1522
1831
  const timer = setTimeout(() => {
1523
1832
  timers.delete(timer);
1524
1833
  record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
1834
+ if (pendingUserDevice) {
1835
+ pendingUserDevice = false;
1836
+ record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
1837
+ answerUserDevice();
1838
+ }
1525
1839
  }, scenario.serviceStartMs);
1526
1840
  timers.add(timer);
1527
1841
  }
@@ -1537,8 +1851,15 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
1537
1851
  }
1538
1852
  reply("navigationBack", {}, 0);
1539
1853
  },
1854
+ setStatus(patch) {
1855
+ status = { ...status, ...patch };
1856
+ reply("systemStatus", statusForBundle(), 0);
1857
+ },
1858
+ setConnected,
1540
1859
  dispose() {
1541
1860
  unsubscribe();
1861
+ for (const t of streams.values()) clearInterval(t);
1862
+ streams.clear();
1542
1863
  for (const t of timers) clearTimeout(t);
1543
1864
  timers.clear();
1544
1865
  }
@@ -1598,15 +1919,18 @@ async function createDevHarness(opts) {
1598
1919
  iframe.setAttribute("title", "Condor Canvas bundle");
1599
1920
  iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
1600
1921
  (opts.mount ?? document.body).appendChild(iframe);
1922
+ let confirmations = "ask";
1601
1923
  const shell = new BridgeShell({
1602
1924
  iframe,
1603
1925
  policy: CANVAS_POLICY,
1604
- onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
1926
+ onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
1605
1927
  });
1606
1928
  if (opts.devPanel !== false) {
1607
1929
  installDevPanel({ adapter, options: { startCollapsed: true } });
1608
1930
  }
1609
- await shell.bootstrapClientIframeWithHtml(opts.html);
1931
+ const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
1932
+ const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
1933
+ await shell.bootstrapClientIframeWithHtml(html);
1610
1934
  try {
1611
1935
  const result = await shell.verifySandbox();
1612
1936
  if (!result.ok) {
@@ -1631,6 +1955,15 @@ async function createDevHarness(opts) {
1631
1955
  emit(key, payload) {
1632
1956
  Bridge.mockEmit(key, payload);
1633
1957
  },
1958
+ setStatus(patch) {
1959
+ native.setStatus(patch);
1960
+ },
1961
+ setConnected(deviceId, value) {
1962
+ native.setConnected(deviceId, value);
1963
+ },
1964
+ setConfirmations(mode) {
1965
+ confirmations = mode;
1966
+ },
1634
1967
  pressBack() {
1635
1968
  native.pressBack();
1636
1969
  },
@@ -1652,6 +1985,7 @@ async function createDevHarness(opts) {
1652
1985
  createDevHarness,
1653
1986
  createSimulatedNative,
1654
1987
  formatLocalTimestamp,
1988
+ lightSensorsReading,
1655
1989
  parseLocalTimestamp,
1656
1990
  resolveScenario,
1657
1991
  sensorLogPage,