@condorinst/mobile-bridge 0.22.0 → 0.23.1

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,26 @@ 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 sessionStart = /* @__PURE__ */ new Map();
1921
+ const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
1922
+ const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
1923
+ const statusForBundle = () => {
1924
+ if (alwaysReplies) return status;
1925
+ const { hasDevicesKnown: _omitted, ...older } = status;
1926
+ return older;
1927
+ };
1815
1928
  const record2 = (entry) => {
1816
1929
  const full = { at: now(), ...entry };
1817
1930
  log.push(full);
@@ -1826,12 +1939,34 @@ var CondorDevHarness = (() => {
1826
1939
  const reply = (key, payload, delay = latency()) => {
1827
1940
  const timer = setTimeout(() => {
1828
1941
  timers.delete(timer);
1829
- record2({ kind: "reply", key, detail: `${key} \u2192 bundle` });
1942
+ if (typeof payload === "function") payload = payload();
1943
+ record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
1830
1944
  adapter.triggerEmit(key, payload);
1831
1945
  }, delay);
1832
1946
  timers.add(timer);
1833
1947
  };
1834
1948
  const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
1949
+ const answerUserDevice = () => {
1950
+ if (scenario.userDeviceError) {
1951
+ if (!alwaysReplies) {
1952
+ record2({
1953
+ kind: "dropped",
1954
+ command: "userDevice",
1955
+ detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
1956
+ });
1957
+ return;
1958
+ }
1959
+ reply("userDevice", {
1960
+ lastUpdate: now(),
1961
+ devicesConfig: [],
1962
+ devicesTelemetry: [],
1963
+ devicesUserConfig: [],
1964
+ error: scenario.userDeviceError
1965
+ });
1966
+ return;
1967
+ }
1968
+ reply("userDevice", scenario.devices());
1969
+ };
1835
1970
  const language = () => {
1836
1971
  const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
1837
1972
  const lang = deviceLocale.slice(0, 2);
@@ -1848,7 +1983,7 @@ var CondorDevHarness = (() => {
1848
1983
  if (command !== "handshake") {
1849
1984
  const shown = JSON.stringify(args);
1850
1985
  const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
1851
- record2({ kind: "sent", command, detail });
1986
+ record2({ kind: "sent", command, detail, args });
1852
1987
  }
1853
1988
  switch (command) {
1854
1989
  case "handshake":
@@ -1860,8 +1995,8 @@ var CondorDevHarness = (() => {
1860
1995
  return;
1861
1996
  }
1862
1997
  reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
1863
- reply("systemStatus", status, latency() + 20);
1864
- reply("language", language(), latency() + 30);
1998
+ reply("systemStatus", statusForBundle, latency() + 20);
1999
+ reply("language", language, latency() + 30);
1865
2000
  return;
1866
2001
  case "ping":
1867
2002
  if (!serviceUp()) {
@@ -1876,30 +2011,33 @@ var CondorDevHarness = (() => {
1876
2011
  return;
1877
2012
  case "userDevice":
1878
2013
  if (!serviceUp()) {
2014
+ pendingUserDevice = alwaysReplies;
1879
2015
  record2({
1880
2016
  kind: "dropped",
1881
2017
  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)"
2018
+ 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
2019
  });
1892
2020
  return;
1893
2021
  }
1894
- reply("userDevice", scenario.devices());
2022
+ answerUserDevice();
1895
2023
  return;
1896
2024
  case "sensorLog": {
1897
2025
  const query = args;
1898
- if (scenario.sensorLogError) {
2026
+ sensorLogCount += 1;
2027
+ if (scenario.sensorLogFailOnRequest === sensorLogCount) {
1899
2028
  reply("sensorLog", {
1900
2029
  requestId: query.requestId,
1901
2030
  success: false,
1902
- error: scenario.sensorLogError
2031
+ error: "Connection failed"
2032
+ });
2033
+ return;
2034
+ }
2035
+ const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
2036
+ if (logError) {
2037
+ reply("sensorLog", {
2038
+ requestId: query.requestId,
2039
+ success: false,
2040
+ error: logError
1903
2041
  });
1904
2042
  return;
1905
2043
  }
@@ -1918,12 +2056,29 @@ var CondorDevHarness = (() => {
1918
2056
  sensorLogPage(query, {
1919
2057
  now: now(),
1920
2058
  periodS: scenario.logPeriodS,
1921
- deviceId: device?.deviceId
2059
+ deviceId: device?.deviceId,
2060
+ bodyPart: device?.deviceBodyPart ?? "wrist",
2061
+ history: scenario.history
1922
2062
  })
1923
2063
  );
1924
2064
  return;
1925
2065
  }
1926
- case "realTime":
2066
+ case "realTime": {
2067
+ const device = findDevice(args);
2068
+ if (device) {
2069
+ if (args.value === true) realTimeOn.add(device.deviceId);
2070
+ else realTimeOn.delete(device.deviceId);
2071
+ syncStream(device.deviceId);
2072
+ if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
2073
+ endSession(device.deviceId, latency());
2074
+ }
2075
+ if (args.value === true && !connected.get(device.deviceId)) {
2076
+ record2({
2077
+ kind: "info",
2078
+ detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
2079
+ });
2080
+ }
2081
+ }
1927
2082
  reply("realTime", {
1928
2083
  serialNo: args.serialNo,
1929
2084
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
@@ -1931,23 +2086,37 @@ var CondorDevHarness = (() => {
1931
2086
  value: args.value === true
1932
2087
  });
1933
2088
  return;
2089
+ }
1934
2090
  case "syncTime":
1935
2091
  case "deleteMemory":
2092
+ {
2093
+ const target = findDevice(args);
2094
+ if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
2095
+ }
2096
+ record2({
2097
+ kind: "info",
2098
+ 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"
2099
+ });
1936
2100
  reply(command, {
1937
2101
  serialNo: args.serialNo,
1938
2102
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1939
2103
  success: true
1940
2104
  });
1941
2105
  return;
1942
- case "deviceConnection":
1943
- if (args.type === "add") {
2106
+ case "deviceConnection": {
2107
+ const device = findDevice(args);
2108
+ if (!device) {
1944
2109
  reply("deviceConnection", {
1945
2110
  serialNo: args.serialNo,
1946
2111
  ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1947
- isConnect: true
2112
+ isConnect: false
1948
2113
  });
2114
+ return;
1949
2115
  }
2116
+ if (args.type === "add") subscribed.add(device.deviceId);
2117
+ else if (args.type === "remove") subscribed.delete(device.deviceId);
1950
2118
  return;
2119
+ }
1951
2120
  case "language":
1952
2121
  reply("language", language());
1953
2122
  return;
@@ -1958,7 +2127,7 @@ var CondorDevHarness = (() => {
1958
2127
  missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
1959
2128
  missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
1960
2129
  };
1961
- reply("systemStatus", status, latency() + 400);
2130
+ reply("systemStatus", statusForBundle, latency() + 400);
1962
2131
  return;
1963
2132
  }
1964
2133
  case "jsError":
@@ -1982,11 +2151,170 @@ var CondorDevHarness = (() => {
1982
2151
  return;
1983
2152
  }
1984
2153
  };
2154
+ function emitConnection(deviceId) {
2155
+ if (!subscribed.has(deviceId)) return;
2156
+ const device = roster.find((d) => d.deviceId === deviceId);
2157
+ if (!device) return;
2158
+ reply("deviceConnection", {
2159
+ serialNo: device.serialNo,
2160
+ ...idOf(device),
2161
+ isConnect: connected.get(deviceId) === true
2162
+ });
2163
+ }
2164
+ function syncStream(deviceId) {
2165
+ const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
2166
+ const running2 = streams.get(deviceId);
2167
+ if (want && !running2) {
2168
+ const device = roster.find((d) => d.deviceId === deviceId);
2169
+ if (!device) return;
2170
+ streams.set(
2171
+ deviceId,
2172
+ setInterval(() => {
2173
+ adapter.triggerEmit(
2174
+ "deviceLightSensors",
2175
+ lightSensorsReading(device, now(), "deviceId" in idOf(device))
2176
+ );
2177
+ }, 1e3)
2178
+ );
2179
+ } else if (!want && running2) {
2180
+ clearInterval(running2);
2181
+ streams.delete(deviceId);
2182
+ }
2183
+ }
2184
+ function setConnected(deviceId, value) {
2185
+ if (!connected.has(deviceId)) {
2186
+ record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
2187
+ return;
2188
+ }
2189
+ if (connected.get(deviceId) === value) return;
2190
+ connected.set(deviceId, value);
2191
+ record2({
2192
+ kind: "info",
2193
+ detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
2194
+ });
2195
+ emitConnection(deviceId);
2196
+ syncStream(deviceId);
2197
+ const device = roster.find((d) => d.deviceId === deviceId);
2198
+ if (device) runSession(device, value);
2199
+ }
2200
+ function setSyncError(device, text, delay) {
2201
+ const timer = setTimeout(() => {
2202
+ timers.delete(timer);
2203
+ const byId2 = { ...status.deviceSyncErrorById ?? {} };
2204
+ const bySerial = { ...status.deviceSyncError };
2205
+ const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
2206
+ if (text === null) {
2207
+ if (!(device.deviceId in byId2)) return;
2208
+ delete byId2[device.deviceId];
2209
+ if (!shared) delete bySerial[device.serialNo];
2210
+ } else {
2211
+ byId2[device.deviceId] = text;
2212
+ if (!shared) bySerial[device.serialNo] = text;
2213
+ }
2214
+ status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
2215
+ reply("systemStatus", statusForBundle, 0);
2216
+ }, delay);
2217
+ timers.add(timer);
2218
+ }
2219
+ function runSession(device, start) {
2220
+ const note = (message, messageCode, isError = false) => ({
2221
+ serialNo: device.serialNo,
2222
+ ...idOf(device),
2223
+ isConnect: !isError,
2224
+ message,
2225
+ messageCode,
2226
+ isError
2227
+ });
2228
+ if (!start) {
2229
+ reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
2230
+ return;
2231
+ }
2232
+ const id = device.deviceId;
2233
+ const steps = [
2234
+ ["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
2235
+ ["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
2236
+ ["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
2237
+ ["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
2238
+ ["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
2239
+ ...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
2240
+ ["[Start] Get data log", "get_data_log_notification", "get_data_log"],
2241
+ ["[Start] Send data log", "send_data_log_notification", "send_data_log"],
2242
+ [
2243
+ "[Success] Set last data log sent to server",
2244
+ "set_last_data_log_sent_to_server_notification",
2245
+ ""
2246
+ ],
2247
+ ["[Start] Get system log", "get_system_log_notification", "get_system_log"],
2248
+ ["[Start] Send system log", "send_system_log_notification", "send_system_log"],
2249
+ ["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
2250
+ ["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
2251
+ ["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
2252
+ ...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
2253
+ ["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
2254
+ ["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
2255
+ ];
2256
+ const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
2257
+ running.add(id);
2258
+ sessionStart.set(id, now());
2259
+ let delay = latency();
2260
+ reply("deviceNotification", note("Device connected", null), delay);
2261
+ for (const [message, code, stage] of steps) {
2262
+ delay += STEP_MS;
2263
+ reply("deviceNotification", note(message, code), delay);
2264
+ if (stage && failAt === stage) {
2265
+ delay += STEP_MS;
2266
+ const what = message.replace(/^\[\w+\] /, "");
2267
+ reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
2268
+ setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
2269
+ endSession(id, delay + STEP_MS);
2270
+ return;
2271
+ }
2272
+ if (stage === "set_sync_time") pendingSync.delete(id);
2273
+ }
2274
+ delay += STEP_MS;
2275
+ reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
2276
+ setSyncError(device, null, delay + 20);
2277
+ pendingErase.delete(id);
2278
+ endSession(id, delay + STEP_MS);
2279
+ }
2280
+ function endSession(deviceId, delay) {
2281
+ const timer = setTimeout(() => {
2282
+ timers.delete(timer);
2283
+ running.delete(deviceId);
2284
+ if (!realTimeOn.has(deviceId)) {
2285
+ setConnected(deviceId, false);
2286
+ return;
2287
+ }
2288
+ const startedAt2 = sessionStart.get(deviceId) ?? now();
2289
+ const hold = setTimeout(
2290
+ () => {
2291
+ timers.delete(hold);
2292
+ if (connected.get(deviceId) && !running.has(deviceId)) setConnected(deviceId, false);
2293
+ },
2294
+ Math.max(0, startedAt2 + (scenario.realTimeSessionMs ?? 18e4) - now())
2295
+ );
2296
+ timers.add(hold);
2297
+ }, delay);
2298
+ timers.add(timer);
2299
+ }
1985
2300
  const unsubscribe = adapter.onCallHandler(handle);
2301
+ const first = roster[0];
2302
+ if (first && typeof scenario.firstSessionAfterMs === "number") {
2303
+ const timer = setTimeout(() => {
2304
+ timers.delete(timer);
2305
+ setConnected(first.deviceId, true);
2306
+ }, scenario.firstSessionAfterMs);
2307
+ timers.add(timer);
2308
+ }
1986
2309
  if (scenario.serviceStartMs > 0) {
1987
2310
  const timer = setTimeout(() => {
1988
2311
  timers.delete(timer);
1989
2312
  record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
2313
+ if (pendingUserDevice) {
2314
+ pendingUserDevice = false;
2315
+ record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
2316
+ answerUserDevice();
2317
+ }
1990
2318
  }, scenario.serviceStartMs);
1991
2319
  timers.add(timer);
1992
2320
  }
@@ -2002,8 +2330,15 @@ var CondorDevHarness = (() => {
2002
2330
  }
2003
2331
  reply("navigationBack", {}, 0);
2004
2332
  },
2333
+ setStatus(patch) {
2334
+ status = { ...status, ...patch };
2335
+ reply("systemStatus", statusForBundle(), 0);
2336
+ },
2337
+ setConnected,
2005
2338
  dispose() {
2006
2339
  unsubscribe();
2340
+ for (const t of streams.values()) clearInterval(t);
2341
+ streams.clear();
2007
2342
  for (const t of timers) clearTimeout(t);
2008
2343
  timers.clear();
2009
2344
  }
@@ -2063,15 +2398,18 @@ var CondorDevHarness = (() => {
2063
2398
  iframe.setAttribute("title", "Condor Canvas bundle");
2064
2399
  iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
2065
2400
  (opts.mount ?? document.body).appendChild(iframe);
2401
+ let confirmations = "ask";
2066
2402
  const shell = new BridgeShell({
2067
2403
  iframe,
2068
2404
  policy: CANVAS_POLICY,
2069
- onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
2405
+ onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
2070
2406
  });
2071
2407
  if (opts.devPanel !== false) {
2072
2408
  installDevPanel({ adapter, options: { startCollapsed: true } });
2073
2409
  }
2074
- await shell.bootstrapClientIframeWithHtml(opts.html);
2410
+ const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
2411
+ const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
2412
+ await shell.bootstrapClientIframeWithHtml(html);
2075
2413
  try {
2076
2414
  const result = await shell.verifySandbox();
2077
2415
  if (!result.ok) {
@@ -2096,6 +2434,15 @@ var CondorDevHarness = (() => {
2096
2434
  emit(key, payload) {
2097
2435
  Bridge.mockEmit(key, payload);
2098
2436
  },
2437
+ setStatus(patch) {
2438
+ native.setStatus(patch);
2439
+ },
2440
+ setConnected(deviceId, value) {
2441
+ native.setConnected(deviceId, value);
2442
+ },
2443
+ setConfirmations(mode) {
2444
+ confirmations = mode;
2445
+ },
2099
2446
  pressBack() {
2100
2447
  native.pressBack();
2101
2448
  },