@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/AGENTS.md +133 -34
- package/CHANGELOG.md +175 -0
- package/README.md +30 -481
- package/contract/bridge-contract.json +32 -21
- package/dist/cli.cjs +126 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +126 -29
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +388 -55
- package/dist/dev.cjs +389 -55
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +239 -9
- package/dist/dev.d.ts +239 -9
- package/dist/dev.js +388 -55
- package/dist/dev.js.map +1 -1
- package/dist/index.cjs +30 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -26
- package/dist/index.d.ts +154 -26
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +9 -265
- package/docs/bridge-contract.md +313 -26
- package/docs/client-guide.md +34 -400
- package/docs/device-testing.md +234 -0
- package/docs/pilot-onboarding.md +41 -115
- package/docs/shell-integration.md +292 -0
- package/docs/troubleshooting.md +19 -11
- package/examples/minimal-home/README.md +2 -2
- package/examples/minimal-home/package.json +1 -1
- package/examples/minimal-home/src/main.ts +23 -3
- package/llms-full.txt +764 -865
- package/llms.txt +13 -15
- package/package.json +1 -1
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: "
|
|
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:
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
318
|
+
slots = walk(
|
|
319
|
+
Math.min(last, decodeCursor(query.endingBefore) - periodMs),
|
|
320
|
+
-periodMs,
|
|
321
|
+
limit
|
|
322
|
+
).reverse();
|
|
241
323
|
} else if (query.startingAfter) {
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
to = Math.min(last, from + (limit - 1) * periodMs);
|
|
326
|
+
slots = walk(first, periodMs, limit);
|
|
247
327
|
} else {
|
|
248
|
-
|
|
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
|
|
262
|
-
hasPrevious: logs.length > 0 && from >
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
1399
|
-
reply("language", language
|
|
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
|
-
|
|
1556
|
+
answerUserDevice();
|
|
1430
1557
|
return;
|
|
1431
1558
|
case "sensorLog": {
|
|
1432
1559
|
const query = args;
|
|
1433
|
-
|
|
1560
|
+
sensorLogCount += 1;
|
|
1561
|
+
if (scenario.sensorLogFailOnRequest === sensorLogCount) {
|
|
1434
1562
|
reply("sensorLog", {
|
|
1435
1563
|
requestId: query.requestId,
|
|
1436
1564
|
success: false,
|
|
1437
|
-
error:
|
|
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
|
-
|
|
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:
|
|
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",
|
|
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
|
-
|
|
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,
|