@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.
@@ -30,6 +30,7 @@ var CondorDevHarness = (() => {
30
30
  createDevHarness: () => createDevHarness,
31
31
  createSimulatedNative: () => createSimulatedNative,
32
32
  formatLocalTimestamp: () => formatLocalTimestamp,
33
+ lightSensorsReading: () => lightSensorsReading,
33
34
  parseLocalTimestamp: () => parseLocalTimestamp,
34
35
  resolveScenario: () => resolveScenario,
35
36
  sensorLogPage: () => sensorLogPage,
@@ -88,7 +89,7 @@ var CondorDevHarness = (() => {
88
89
  serialNo: spec.serialNo,
89
90
  attemptDate: now - 10 * 6e4,
90
91
  message: spec.syncError,
91
- messageCode: "SYNC_FAILED"
92
+ messageCode: "send_data_log_error"
92
93
  }
93
94
  ] : []
94
95
  };
@@ -172,6 +173,7 @@ var CondorDevHarness = (() => {
172
173
  internet: true,
173
174
  permission: true,
174
175
  hasDevices: true,
176
+ hasDevicesKnown: true,
175
177
  missingOptionalPermissions: [],
176
178
  missingEssentialPermissions: [],
177
179
  deviceSyncError: {},
@@ -187,13 +189,15 @@ var CondorDevHarness = (() => {
187
189
  const photopic = Math.round((daylight * 1800 + noise * 40) * 10) / 10;
188
190
  return { photopic, melanopic: Math.round(photopic * 0.62 * 10) / 10 };
189
191
  }
190
- function logAt(epochMs, serialNo, deviceId) {
191
- const { photopic, melanopic } = lightAt(epochMs, serialNo);
192
+ function logAt(epochMs, serialNo, deviceId, bodyPart, docked) {
193
+ const { photopic, melanopic } = docked ? { photopic: 0, melanopic: 0 } : lightAt(epochMs, serialNo);
192
194
  return {
193
195
  id: Math.floor(epochMs / 1e3) * 10 + serialNo % 10,
194
196
  log_time: formatLocalTimestamp(new Date(epochMs)),
195
197
  log_type: 1,
196
- body_part: "wrist",
198
+ body_part: bodyPart,
199
+ // The device's raw wear flag; the name misleads: 1 = worn, 0 = on the dock.
200
+ off_wrist: docked ? 0 : 1,
197
201
  temperature: 31.5,
198
202
  lux_photopic: photopic,
199
203
  lux_melanopic: melanopic,
@@ -201,6 +205,67 @@ var CondorDevHarness = (() => {
201
205
  act_data_pim: 120
202
206
  };
203
207
  }
208
+ function spectrum(nm, photopic) {
209
+ const out = {};
210
+ for (const w of nm) {
211
+ const weight = Math.exp(-(((w - 555) / 120) ** 2));
212
+ out[`wavelength${w}`] = Math.round(photopic * weight * 0.01 * 1e4) / 1e4;
213
+ }
214
+ return out;
215
+ }
216
+ var GEN1_NM = [390, 415, 445, 480, 515, 555, 590, 630, 680, 750];
217
+ var GEN2_NM = [395, 426, 457, 488, 520, 551, 582, 613, 644, 675, 707, 738, 769, 800];
218
+ function lightSensorsReading(device, epochMs = Date.now(), withDeviceId = true) {
219
+ const { photopic, melanopic } = lightAt(epochMs, device.serialNo);
220
+ const raw = {
221
+ light_sensor_f1: 0.01,
222
+ light_sensor_f2: 0.02,
223
+ light_sensor_f3: 0.03,
224
+ light_sensor_f4: 0.04,
225
+ light_sensor_f5: 0.05,
226
+ light_sensor_f6: 0.04,
227
+ light_sensor_f7: 0.03,
228
+ light_sensor_f8: 0.02,
229
+ light_sensor_clear: Math.round(photopic * 2e-4 * 1e4) / 1e4,
230
+ light_sensor_nir: 0.01
231
+ };
232
+ const address = {
233
+ serialNo: device.serialNo,
234
+ ...withDeviceId ? { deviceId: device.deviceId } : {}
235
+ };
236
+ if (device.modelId === "actlumus_plus") {
237
+ return {
238
+ ...address,
239
+ type: "actlumus2",
240
+ data: {
241
+ ...raw,
242
+ ...spectrum(GEN2_NM, photopic),
243
+ time: epochMs,
244
+ photopic,
245
+ melanopic,
246
+ light_sensor_z: 9e-3,
247
+ light_sensor_y: 0.038,
248
+ light_sensor_x: 0.044,
249
+ light_sensor_fd: 0
250
+ }
251
+ };
252
+ }
253
+ return {
254
+ ...address,
255
+ type: "actlumus",
256
+ data: {
257
+ ...raw,
258
+ ...spectrum(GEN1_NM, photopic),
259
+ time: epochMs,
260
+ photopic,
261
+ melanopic,
262
+ light_sensor_z: null,
263
+ light_sensor_y: null,
264
+ light_sensor_x: null,
265
+ light_sensor_fd: null
266
+ }
267
+ };
268
+ }
204
269
  var HISTORY_DAYS = 14;
205
270
  var encodeCursor = (epochMs) => `mock:${epochMs}`;
206
271
  var decodeCursor = (cursor) => Number(cursor.slice("mock:".length));
@@ -215,7 +280,15 @@ var CondorDevHarness = (() => {
215
280
  return { requestId: query.requestId, success: false, error: "CONFLICTING_CURSORS" };
216
281
  }
217
282
  const newest = Math.floor(now / periodMs) * periodMs;
218
- const oldest = newest - HISTORY_DAYS * 864e5;
283
+ const history = opts.history ?? {};
284
+ const oldest = newest - (history.days ?? HISTORY_DAYS) * 864e5;
285
+ const dock = new Set(history.dockHours ?? [7]);
286
+ const gapHours = new Set(history.gapHours ?? []);
287
+ const gapDates = new Set(history.gapDates ?? []);
288
+ const present = (t) => {
289
+ const d = new Date(t);
290
+ return !gapHours.has(d.getHours()) && !gapDates.has(formatLocalTimestamp(d).slice(0, 10));
291
+ };
219
292
  const rangeStart = query.startTime ? parseLocalTimestamp(query.startTime) : oldest;
220
293
  const rangeEnd = query.endTime ? parseLocalTimestamp(query.endTime) - 1 : newest;
221
294
  if (Number.isNaN(rangeStart) || Number.isNaN(rangeEnd) || rangeStart > rangeEnd + 1) {
@@ -223,33 +296,41 @@ var CondorDevHarness = (() => {
223
296
  }
224
297
  const first = Math.max(oldest, Math.ceil(rangeStart / periodMs) * periodMs);
225
298
  const last = Math.min(newest, Math.floor(rangeEnd / periodMs) * periodMs);
226
- let from;
227
- let to;
299
+ const walk = (start, step, max) => {
300
+ const out = [];
301
+ for (let t = start; t >= first && t <= last && out.length < max; t += step) {
302
+ if (present(t)) out.push(t);
303
+ }
304
+ return out;
305
+ };
306
+ let slots;
228
307
  if (query.endingBefore) {
229
- to = Math.min(last, decodeCursor(query.endingBefore) - periodMs);
230
- from = Math.max(first, to - (limit - 1) * periodMs);
308
+ slots = walk(
309
+ Math.min(last, decodeCursor(query.endingBefore) - periodMs),
310
+ -periodMs,
311
+ limit
312
+ ).reverse();
231
313
  } else if (query.startingAfter) {
232
- from = Math.max(first, decodeCursor(query.startingAfter) + periodMs);
233
- to = Math.min(last, from + (limit - 1) * periodMs);
314
+ slots = walk(Math.max(first, decodeCursor(query.startingAfter) + periodMs), periodMs, limit);
234
315
  } else if (query.startTime) {
235
- from = first;
236
- to = Math.min(last, from + (limit - 1) * periodMs);
316
+ slots = walk(first, periodMs, limit);
237
317
  } else {
238
- to = last;
239
- from = Math.max(first, to - (limit - 1) * periodMs);
240
- }
241
- const logs = [];
242
- for (let t = from; t <= to; t += periodMs) {
243
- logs.push(logAt(t, query.serialNo, opts.deviceId ?? 0));
318
+ slots = walk(last, -periodMs, limit).reverse();
244
319
  }
320
+ const bodyPart = opts.bodyPart ?? "wrist";
321
+ const logs = slots.map(
322
+ (t) => logAt(t, query.serialNo, opts.deviceId ?? 0, bodyPart, dock.has(new Date(t).getHours()))
323
+ );
324
+ const from = slots[0] ?? 0;
325
+ const to = slots[slots.length - 1] ?? 0;
245
326
  return {
246
327
  requestId: query.requestId,
247
328
  success: true,
248
329
  logs,
249
330
  startingAfter: logs.length ? encodeCursor(to) : null,
250
331
  endingBefore: logs.length ? encodeCursor(from) : null,
251
- hasMore: logs.length > 0 && to < last,
252
- hasPrevious: logs.length > 0 && from > first
332
+ hasMore: logs.length > 0 && walk(to + periodMs, periodMs, 1).length > 0,
333
+ hasPrevious: logs.length > 0 && walk(from - periodMs, -periodMs, 1).length > 0
253
334
  };
254
335
  }
255
336
 
@@ -1718,6 +1799,19 @@ var CondorDevHarness = (() => {
1718
1799
  }
1719
1800
  };
1720
1801
 
1802
+ // src/version.ts
1803
+ function versionAtLeast(version, min) {
1804
+ if (!version) return false;
1805
+ const a = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
1806
+ const b = min.split(".").map((n) => Number.parseInt(n, 10) || 0);
1807
+ for (let i = 0; i < 3; i++) {
1808
+ const x = a[i] ?? 0;
1809
+ const y = b[i] ?? 0;
1810
+ if (x !== y) return x > y;
1811
+ }
1812
+ return true;
1813
+ }
1814
+
1721
1815
  // src/dev/policy.ts
1722
1816
  var CANVAS_POLICY = {
1723
1817
  readOnlyKeys: [
@@ -1753,28 +1847,27 @@ var CondorDevHarness = (() => {
1753
1847
  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';";
1754
1848
 
1755
1849
  // src/dev/scenarios.ts
1756
- var HARNESS_NATIVE_VERSION = "0.17.0";
1850
+ var HARNESS_NATIVE_VERSION = "0.18.0";
1757
1851
  var base = {
1758
- description: "Two devices, background service already running, realistic latency.",
1852
+ 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.",
1759
1853
  nativeVersion: HARNESS_NATIVE_VERSION,
1760
1854
  latencyMs: [80, 250],
1761
1855
  serviceStartMs: 0,
1762
- userDeviceSilent: false,
1763
1856
  logPeriodS: 60,
1764
1857
  systemStatus: systemStatus(),
1765
1858
  devices: () => twoDevices()
1766
1859
  };
1767
1860
  var SCENARIOS = {
1768
- default: base,
1861
+ default: { ...base, firstSessionAfterMs: 3e3 },
1769
1862
  "cold-start": {
1770
1863
  ...base,
1771
- 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.",
1864
+ 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.",
1772
1865
  serviceStartMs: 4e3
1773
1866
  },
1774
1867
  "no-devices": {
1775
1868
  ...base,
1776
- description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
1777
- userDeviceSilent: true,
1869
+ 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.",
1870
+ userDeviceError: "NO_DEVICES",
1778
1871
  systemStatus: systemStatus({ hasDevices: false })
1779
1872
  },
1780
1873
  "shared-serial": {
@@ -1784,9 +1877,8 @@ var CondorDevHarness = (() => {
1784
1877
  },
1785
1878
  offline: {
1786
1879
  ...base,
1787
- description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
1788
- sensorLogError: "Connection failed",
1789
- systemStatus: systemStatus({ internet: false })
1880
+ 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 }).",
1881
+ systemStatus: systemStatus({ internet: false, hasDevicesKnown: false })
1790
1882
  }
1791
1883
  };
1792
1884
  function resolveScenario(scenario) {
@@ -1805,6 +1897,7 @@ var CondorDevHarness = (() => {
1805
1897
 
1806
1898
  // src/dev/harness.ts
1807
1899
  var DEV_HARNESS_MARKER = "__condorDevHarness__";
1900
+ var STEP_MS = 200;
1808
1901
  function createSimulatedNative(adapter, scenario, hooks = {}) {
1809
1902
  const now = hooks.now ?? Date.now;
1810
1903
  const startedAt = now();
@@ -1812,6 +1905,25 @@ var CondorDevHarness = (() => {
1812
1905
  const timers = /* @__PURE__ */ new Set();
1813
1906
  let status = { ...scenario.systemStatus };
1814
1907
  let canGoBack = false;
1908
+ const alwaysReplies = versionAtLeast(scenario.nativeVersion, "0.18.0");
1909
+ let pendingUserDevice = false;
1910
+ let sensorLogCount = 0;
1911
+ const roster = scenario.devices().devicesConfig;
1912
+ const connected = new Map(roster.map((d) => [d.deviceId, false]));
1913
+ const subscribed = /* @__PURE__ */ new Set();
1914
+ const realTimeOn = /* @__PURE__ */ new Set();
1915
+ const streams = /* @__PURE__ */ new Map();
1916
+ const byId = versionAtLeast(scenario.nativeVersion, "0.17.0");
1917
+ const pendingSync = /* @__PURE__ */ new Set();
1918
+ const pendingErase = /* @__PURE__ */ new Set();
1919
+ const running = /* @__PURE__ */ new Set();
1920
+ const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
1921
+ const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
1922
+ const statusForBundle = () => {
1923
+ if (alwaysReplies) return status;
1924
+ const { hasDevicesKnown: _omitted, ...older } = status;
1925
+ return older;
1926
+ };
1815
1927
  const record2 = (entry) => {
1816
1928
  const full = { at: now(), ...entry };
1817
1929
  log.push(full);
@@ -1826,12 +1938,34 @@ var CondorDevHarness = (() => {
1826
1938
  const reply = (key, payload, delay = latency()) => {
1827
1939
  const timer = setTimeout(() => {
1828
1940
  timers.delete(timer);
1829
- record2({ kind: "reply", key, detail: `${key} \u2192 bundle` });
1941
+ if (typeof payload === "function") payload = payload();
1942
+ record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
1830
1943
  adapter.triggerEmit(key, payload);
1831
1944
  }, delay);
1832
1945
  timers.add(timer);
1833
1946
  };
1834
1947
  const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
1948
+ const answerUserDevice = () => {
1949
+ if (scenario.userDeviceError) {
1950
+ if (!alwaysReplies) {
1951
+ record2({
1952
+ kind: "dropped",
1953
+ command: "userDevice",
1954
+ detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
1955
+ });
1956
+ return;
1957
+ }
1958
+ reply("userDevice", {
1959
+ lastUpdate: now(),
1960
+ devicesConfig: [],
1961
+ devicesTelemetry: [],
1962
+ devicesUserConfig: [],
1963
+ error: scenario.userDeviceError
1964
+ });
1965
+ return;
1966
+ }
1967
+ reply("userDevice", scenario.devices());
1968
+ };
1835
1969
  const language = () => {
1836
1970
  const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
1837
1971
  const lang = deviceLocale.slice(0, 2);
@@ -1848,7 +1982,7 @@ var CondorDevHarness = (() => {
1848
1982
  if (command !== "handshake") {
1849
1983
  const shown = JSON.stringify(args);
1850
1984
  const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
1851
- record2({ kind: "sent", command, detail });
1985
+ record2({ kind: "sent", command, detail, args });
1852
1986
  }
1853
1987
  switch (command) {
1854
1988
  case "handshake":
@@ -1860,8 +1994,8 @@ var CondorDevHarness = (() => {
1860
1994
  return;
1861
1995
  }
1862
1996
  reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
1863
- reply("systemStatus", status, latency() + 20);
1864
- reply("language", language(), latency() + 30);
1997
+ reply("systemStatus", statusForBundle, latency() + 20);
1998
+ reply("language", language, latency() + 30);
1865
1999
  return;
1866
2000
  case "ping":
1867
2001
  if (!serviceUp()) {
@@ -1876,30 +2010,33 @@ var CondorDevHarness = (() => {
1876
2010
  return;
1877
2011
  case "userDevice":
1878
2012
  if (!serviceUp()) {
2013
+ pendingUserDevice = alwaysReplies;
1879
2014
  record2({
1880
2015
  kind: "dropped",
1881
2016
  command,
1882
- detail: "userDevice lost: the background service is not running yet. Ask again."
1883
- });
1884
- return;
1885
- }
1886
- if (scenario.userDeviceSilent) {
1887
- record2({
1888
- kind: "dropped",
1889
- command,
1890
- detail: "userDevice unanswered: the account has no linked device (the app replies only on success)"
2017
+ 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."
1891
2018
  });
1892
2019
  return;
1893
2020
  }
1894
- reply("userDevice", scenario.devices());
2021
+ answerUserDevice();
1895
2022
  return;
1896
2023
  case "sensorLog": {
1897
2024
  const query = args;
1898
- if (scenario.sensorLogError) {
2025
+ sensorLogCount += 1;
2026
+ if (scenario.sensorLogFailOnRequest === sensorLogCount) {
1899
2027
  reply("sensorLog", {
1900
2028
  requestId: query.requestId,
1901
2029
  success: false,
1902
- error: scenario.sensorLogError
2030
+ error: "Connection failed"
2031
+ });
2032
+ return;
2033
+ }
2034
+ const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
2035
+ if (logError) {
2036
+ reply("sensorLog", {
2037
+ requestId: query.requestId,
2038
+ success: false,
2039
+ error: logError
1903
2040
  });
1904
2041
  return;
1905
2042
  }
@@ -1918,12 +2055,29 @@ var CondorDevHarness = (() => {
1918
2055
  sensorLogPage(query, {
1919
2056
  now: now(),
1920
2057
  periodS: scenario.logPeriodS,
1921
- deviceId: device?.deviceId
2058
+ deviceId: device?.deviceId,
2059
+ bodyPart: device?.deviceBodyPart ?? "wrist",
2060
+ history: scenario.history
1922
2061
  })
1923
2062
  );
1924
2063
  return;
1925
2064
  }
1926
- case "realTime":
2065
+ case "realTime": {
2066
+ const device = findDevice(args);
2067
+ if (device) {
2068
+ if (args.value === true) realTimeOn.add(device.deviceId);
2069
+ else realTimeOn.delete(device.deviceId);
2070
+ syncStream(device.deviceId);
2071
+ if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
2072
+ endSession(device.deviceId, latency());
2073
+ }
2074
+ if (args.value === true && !connected.get(device.deviceId)) {
2075
+ record2({
2076
+ kind: "info",
2077
+ detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
2078
+ });
2079
+ }
2080
+ }
1927
2081
  reply("realTime", {
1928
2082
  serialNo: args.serialNo,
1929
2083
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
@@ -1931,23 +2085,37 @@ var CondorDevHarness = (() => {
1931
2085
  value: args.value === true
1932
2086
  });
1933
2087
  return;
2088
+ }
1934
2089
  case "syncTime":
1935
2090
  case "deleteMemory":
2091
+ {
2092
+ const target = findDevice(args);
2093
+ if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
2094
+ }
2095
+ record2({
2096
+ kind: "info",
2097
+ 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"
2098
+ });
1936
2099
  reply(command, {
1937
2100
  serialNo: args.serialNo,
1938
2101
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1939
2102
  success: true
1940
2103
  });
1941
2104
  return;
1942
- case "deviceConnection":
1943
- if (args.type === "add") {
2105
+ case "deviceConnection": {
2106
+ const device = findDevice(args);
2107
+ if (!device) {
1944
2108
  reply("deviceConnection", {
1945
2109
  serialNo: args.serialNo,
1946
2110
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1947
- isConnect: true
2111
+ isConnect: false
1948
2112
  });
2113
+ return;
1949
2114
  }
2115
+ if (args.type === "add") subscribed.add(device.deviceId);
2116
+ else if (args.type === "remove") subscribed.delete(device.deviceId);
1950
2117
  return;
2118
+ }
1951
2119
  case "language":
1952
2120
  reply("language", language());
1953
2121
  return;
@@ -1958,7 +2126,7 @@ var CondorDevHarness = (() => {
1958
2126
  missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
1959
2127
  missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
1960
2128
  };
1961
- reply("systemStatus", status, latency() + 400);
2129
+ reply("systemStatus", statusForBundle, latency() + 400);
1962
2130
  return;
1963
2131
  }
1964
2132
  case "jsError":
@@ -1982,11 +2150,157 @@ var CondorDevHarness = (() => {
1982
2150
  return;
1983
2151
  }
1984
2152
  };
2153
+ function emitConnection(deviceId) {
2154
+ if (!subscribed.has(deviceId)) return;
2155
+ const device = roster.find((d) => d.deviceId === deviceId);
2156
+ if (!device) return;
2157
+ reply("deviceConnection", {
2158
+ serialNo: device.serialNo,
2159
+ ...idOf(device),
2160
+ isConnect: connected.get(deviceId) === true
2161
+ });
2162
+ }
2163
+ function syncStream(deviceId) {
2164
+ const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
2165
+ const running2 = streams.get(deviceId);
2166
+ if (want && !running2) {
2167
+ const device = roster.find((d) => d.deviceId === deviceId);
2168
+ if (!device) return;
2169
+ streams.set(
2170
+ deviceId,
2171
+ setInterval(() => {
2172
+ adapter.triggerEmit(
2173
+ "deviceLightSensors",
2174
+ lightSensorsReading(device, now(), "deviceId" in idOf(device))
2175
+ );
2176
+ }, 1e3)
2177
+ );
2178
+ } else if (!want && running2) {
2179
+ clearInterval(running2);
2180
+ streams.delete(deviceId);
2181
+ }
2182
+ }
2183
+ function setConnected(deviceId, value) {
2184
+ if (!connected.has(deviceId)) {
2185
+ record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
2186
+ return;
2187
+ }
2188
+ if (connected.get(deviceId) === value) return;
2189
+ connected.set(deviceId, value);
2190
+ record2({
2191
+ kind: "info",
2192
+ detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
2193
+ });
2194
+ emitConnection(deviceId);
2195
+ syncStream(deviceId);
2196
+ const device = roster.find((d) => d.deviceId === deviceId);
2197
+ if (device) runSession(device, value);
2198
+ }
2199
+ function setSyncError(device, text, delay) {
2200
+ const timer = setTimeout(() => {
2201
+ timers.delete(timer);
2202
+ const byId2 = { ...status.deviceSyncErrorById ?? {} };
2203
+ const bySerial = { ...status.deviceSyncError };
2204
+ const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
2205
+ if (text === null) {
2206
+ if (!(device.deviceId in byId2)) return;
2207
+ delete byId2[device.deviceId];
2208
+ if (!shared) delete bySerial[device.serialNo];
2209
+ } else {
2210
+ byId2[device.deviceId] = text;
2211
+ if (!shared) bySerial[device.serialNo] = text;
2212
+ }
2213
+ status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
2214
+ reply("systemStatus", statusForBundle, 0);
2215
+ }, delay);
2216
+ timers.add(timer);
2217
+ }
2218
+ function runSession(device, start) {
2219
+ const note = (message, messageCode, isError = false) => ({
2220
+ serialNo: device.serialNo,
2221
+ ...idOf(device),
2222
+ isConnect: !isError,
2223
+ message,
2224
+ messageCode,
2225
+ isError
2226
+ });
2227
+ if (!start) {
2228
+ reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
2229
+ return;
2230
+ }
2231
+ const id = device.deviceId;
2232
+ const steps = [
2233
+ ["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
2234
+ ["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
2235
+ ["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
2236
+ ["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
2237
+ ["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
2238
+ ...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
2239
+ ["[Start] Get data log", "get_data_log_notification", "get_data_log"],
2240
+ ["[Start] Send data log", "send_data_log_notification", "send_data_log"],
2241
+ [
2242
+ "[Success] Set last data log sent to server",
2243
+ "set_last_data_log_sent_to_server_notification",
2244
+ ""
2245
+ ],
2246
+ ["[Start] Get system log", "get_system_log_notification", "get_system_log"],
2247
+ ["[Start] Send system log", "send_system_log_notification", "send_system_log"],
2248
+ ["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
2249
+ ["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
2250
+ ["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
2251
+ ...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
2252
+ ["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
2253
+ ["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
2254
+ ];
2255
+ const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
2256
+ running.add(id);
2257
+ let delay = latency();
2258
+ reply("deviceNotification", note("Device connected", null), delay);
2259
+ for (const [message, code, stage] of steps) {
2260
+ delay += STEP_MS;
2261
+ reply("deviceNotification", note(message, code), delay);
2262
+ if (stage && failAt === stage) {
2263
+ delay += STEP_MS;
2264
+ const what = message.replace(/^\[\w+\] /, "");
2265
+ reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
2266
+ setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
2267
+ endSession(id, delay + STEP_MS);
2268
+ return;
2269
+ }
2270
+ if (stage === "set_sync_time") pendingSync.delete(id);
2271
+ }
2272
+ delay += STEP_MS;
2273
+ reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
2274
+ setSyncError(device, null, delay + 20);
2275
+ pendingErase.delete(id);
2276
+ endSession(id, delay + STEP_MS);
2277
+ }
2278
+ function endSession(deviceId, delay) {
2279
+ const timer = setTimeout(() => {
2280
+ timers.delete(timer);
2281
+ running.delete(deviceId);
2282
+ if (!realTimeOn.has(deviceId)) setConnected(deviceId, false);
2283
+ }, delay);
2284
+ timers.add(timer);
2285
+ }
1985
2286
  const unsubscribe = adapter.onCallHandler(handle);
2287
+ const first = roster[0];
2288
+ if (first && typeof scenario.firstSessionAfterMs === "number") {
2289
+ const timer = setTimeout(() => {
2290
+ timers.delete(timer);
2291
+ setConnected(first.deviceId, true);
2292
+ }, scenario.firstSessionAfterMs);
2293
+ timers.add(timer);
2294
+ }
1986
2295
  if (scenario.serviceStartMs > 0) {
1987
2296
  const timer = setTimeout(() => {
1988
2297
  timers.delete(timer);
1989
2298
  record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
2299
+ if (pendingUserDevice) {
2300
+ pendingUserDevice = false;
2301
+ record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
2302
+ answerUserDevice();
2303
+ }
1990
2304
  }, scenario.serviceStartMs);
1991
2305
  timers.add(timer);
1992
2306
  }
@@ -2002,8 +2316,15 @@ var CondorDevHarness = (() => {
2002
2316
  }
2003
2317
  reply("navigationBack", {}, 0);
2004
2318
  },
2319
+ setStatus(patch) {
2320
+ status = { ...status, ...patch };
2321
+ reply("systemStatus", statusForBundle(), 0);
2322
+ },
2323
+ setConnected,
2005
2324
  dispose() {
2006
2325
  unsubscribe();
2326
+ for (const t of streams.values()) clearInterval(t);
2327
+ streams.clear();
2007
2328
  for (const t of timers) clearTimeout(t);
2008
2329
  timers.clear();
2009
2330
  }
@@ -2063,15 +2384,18 @@ var CondorDevHarness = (() => {
2063
2384
  iframe.setAttribute("title", "Condor Canvas bundle");
2064
2385
  iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
2065
2386
  (opts.mount ?? document.body).appendChild(iframe);
2387
+ let confirmations = "ask";
2066
2388
  const shell = new BridgeShell({
2067
2389
  iframe,
2068
2390
  policy: CANVAS_POLICY,
2069
- onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
2391
+ onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
2070
2392
  });
2071
2393
  if (opts.devPanel !== false) {
2072
2394
  installDevPanel({ adapter, options: { startCollapsed: true } });
2073
2395
  }
2074
- await shell.bootstrapClientIframeWithHtml(opts.html);
2396
+ const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
2397
+ const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
2398
+ await shell.bootstrapClientIframeWithHtml(html);
2075
2399
  try {
2076
2400
  const result = await shell.verifySandbox();
2077
2401
  if (!result.ok) {
@@ -2096,6 +2420,15 @@ var CondorDevHarness = (() => {
2096
2420
  emit(key, payload) {
2097
2421
  Bridge.mockEmit(key, payload);
2098
2422
  },
2423
+ setStatus(patch) {
2424
+ native.setStatus(patch);
2425
+ },
2426
+ setConnected(deviceId, value) {
2427
+ native.setConnected(deviceId, value);
2428
+ },
2429
+ setConfirmations(mode) {
2430
+ confirmations = mode;
2431
+ },
2099
2432
  pressBack() {
2100
2433
  native.pressBack();
2101
2434
  },