@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.
- package/AGENTS.md +133 -34
- package/CHANGELOG.md +187 -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 +402 -55
- package/dist/dev.cjs +403 -55
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +245 -9
- package/dist/dev.d.ts +245 -9
- package/dist/dev.js +402 -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 +317 -26
- package/docs/client-guide.md +34 -400
- package/docs/device-testing.md +235 -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 +769 -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,26 @@ 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 sessionStart = /* @__PURE__ */ new Map();
|
|
1456
|
+
const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
|
|
1457
|
+
const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
|
|
1458
|
+
const statusForBundle = () => {
|
|
1459
|
+
if (alwaysReplies) return status;
|
|
1460
|
+
const { hasDevicesKnown: _omitted, ...older } = status;
|
|
1461
|
+
return older;
|
|
1462
|
+
};
|
|
1350
1463
|
const record2 = (entry) => {
|
|
1351
1464
|
const full = { at: now(), ...entry };
|
|
1352
1465
|
log.push(full);
|
|
@@ -1361,12 +1474,34 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1361
1474
|
const reply = (key, payload, delay = latency()) => {
|
|
1362
1475
|
const timer = setTimeout(() => {
|
|
1363
1476
|
timers.delete(timer);
|
|
1364
|
-
|
|
1477
|
+
if (typeof payload === "function") payload = payload();
|
|
1478
|
+
record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
|
|
1365
1479
|
adapter.triggerEmit(key, payload);
|
|
1366
1480
|
}, delay);
|
|
1367
1481
|
timers.add(timer);
|
|
1368
1482
|
};
|
|
1369
1483
|
const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
|
|
1484
|
+
const answerUserDevice = () => {
|
|
1485
|
+
if (scenario.userDeviceError) {
|
|
1486
|
+
if (!alwaysReplies) {
|
|
1487
|
+
record2({
|
|
1488
|
+
kind: "dropped",
|
|
1489
|
+
command: "userDevice",
|
|
1490
|
+
detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
|
|
1491
|
+
});
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
reply("userDevice", {
|
|
1495
|
+
lastUpdate: now(),
|
|
1496
|
+
devicesConfig: [],
|
|
1497
|
+
devicesTelemetry: [],
|
|
1498
|
+
devicesUserConfig: [],
|
|
1499
|
+
error: scenario.userDeviceError
|
|
1500
|
+
});
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
reply("userDevice", scenario.devices());
|
|
1504
|
+
};
|
|
1370
1505
|
const language = () => {
|
|
1371
1506
|
const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
|
|
1372
1507
|
const lang = deviceLocale.slice(0, 2);
|
|
@@ -1383,7 +1518,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1383
1518
|
if (command !== "handshake") {
|
|
1384
1519
|
const shown = JSON.stringify(args);
|
|
1385
1520
|
const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
|
|
1386
|
-
record2({ kind: "sent", command, detail });
|
|
1521
|
+
record2({ kind: "sent", command, detail, args });
|
|
1387
1522
|
}
|
|
1388
1523
|
switch (command) {
|
|
1389
1524
|
case "handshake":
|
|
@@ -1395,8 +1530,8 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1395
1530
|
return;
|
|
1396
1531
|
}
|
|
1397
1532
|
reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
|
|
1398
|
-
reply("systemStatus",
|
|
1399
|
-
reply("language", language
|
|
1533
|
+
reply("systemStatus", statusForBundle, latency() + 20);
|
|
1534
|
+
reply("language", language, latency() + 30);
|
|
1400
1535
|
return;
|
|
1401
1536
|
case "ping":
|
|
1402
1537
|
if (!serviceUp()) {
|
|
@@ -1411,30 +1546,33 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1411
1546
|
return;
|
|
1412
1547
|
case "userDevice":
|
|
1413
1548
|
if (!serviceUp()) {
|
|
1549
|
+
pendingUserDevice = alwaysReplies;
|
|
1414
1550
|
record2({
|
|
1415
1551
|
kind: "dropped",
|
|
1416
1552
|
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)"
|
|
1553
|
+
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
1554
|
});
|
|
1427
1555
|
return;
|
|
1428
1556
|
}
|
|
1429
|
-
|
|
1557
|
+
answerUserDevice();
|
|
1430
1558
|
return;
|
|
1431
1559
|
case "sensorLog": {
|
|
1432
1560
|
const query = args;
|
|
1433
|
-
|
|
1561
|
+
sensorLogCount += 1;
|
|
1562
|
+
if (scenario.sensorLogFailOnRequest === sensorLogCount) {
|
|
1434
1563
|
reply("sensorLog", {
|
|
1435
1564
|
requestId: query.requestId,
|
|
1436
1565
|
success: false,
|
|
1437
|
-
error:
|
|
1566
|
+
error: "Connection failed"
|
|
1567
|
+
});
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
|
|
1571
|
+
if (logError) {
|
|
1572
|
+
reply("sensorLog", {
|
|
1573
|
+
requestId: query.requestId,
|
|
1574
|
+
success: false,
|
|
1575
|
+
error: logError
|
|
1438
1576
|
});
|
|
1439
1577
|
return;
|
|
1440
1578
|
}
|
|
@@ -1453,12 +1591,29 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1453
1591
|
sensorLogPage(query, {
|
|
1454
1592
|
now: now(),
|
|
1455
1593
|
periodS: scenario.logPeriodS,
|
|
1456
|
-
deviceId: device?.deviceId
|
|
1594
|
+
deviceId: device?.deviceId,
|
|
1595
|
+
bodyPart: device?.deviceBodyPart ?? "wrist",
|
|
1596
|
+
history: scenario.history
|
|
1457
1597
|
})
|
|
1458
1598
|
);
|
|
1459
1599
|
return;
|
|
1460
1600
|
}
|
|
1461
|
-
case "realTime":
|
|
1601
|
+
case "realTime": {
|
|
1602
|
+
const device = findDevice(args);
|
|
1603
|
+
if (device) {
|
|
1604
|
+
if (args.value === true) realTimeOn.add(device.deviceId);
|
|
1605
|
+
else realTimeOn.delete(device.deviceId);
|
|
1606
|
+
syncStream(device.deviceId);
|
|
1607
|
+
if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
|
|
1608
|
+
endSession(device.deviceId, latency());
|
|
1609
|
+
}
|
|
1610
|
+
if (args.value === true && !connected.get(device.deviceId)) {
|
|
1611
|
+
record2({
|
|
1612
|
+
kind: "info",
|
|
1613
|
+
detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1462
1617
|
reply("realTime", {
|
|
1463
1618
|
serialNo: args.serialNo,
|
|
1464
1619
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
@@ -1466,23 +1621,37 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1466
1621
|
value: args.value === true
|
|
1467
1622
|
});
|
|
1468
1623
|
return;
|
|
1624
|
+
}
|
|
1469
1625
|
case "syncTime":
|
|
1470
1626
|
case "deleteMemory":
|
|
1627
|
+
{
|
|
1628
|
+
const target = findDevice(args);
|
|
1629
|
+
if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
|
|
1630
|
+
}
|
|
1631
|
+
record2({
|
|
1632
|
+
kind: "info",
|
|
1633
|
+
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"
|
|
1634
|
+
});
|
|
1471
1635
|
reply(command, {
|
|
1472
1636
|
serialNo: args.serialNo,
|
|
1473
1637
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1474
1638
|
success: true
|
|
1475
1639
|
});
|
|
1476
1640
|
return;
|
|
1477
|
-
case "deviceConnection":
|
|
1478
|
-
|
|
1641
|
+
case "deviceConnection": {
|
|
1642
|
+
const device = findDevice(args);
|
|
1643
|
+
if (!device) {
|
|
1479
1644
|
reply("deviceConnection", {
|
|
1480
1645
|
serialNo: args.serialNo,
|
|
1481
1646
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1482
|
-
isConnect:
|
|
1647
|
+
isConnect: false
|
|
1483
1648
|
});
|
|
1649
|
+
return;
|
|
1484
1650
|
}
|
|
1651
|
+
if (args.type === "add") subscribed.add(device.deviceId);
|
|
1652
|
+
else if (args.type === "remove") subscribed.delete(device.deviceId);
|
|
1485
1653
|
return;
|
|
1654
|
+
}
|
|
1486
1655
|
case "language":
|
|
1487
1656
|
reply("language", language());
|
|
1488
1657
|
return;
|
|
@@ -1493,7 +1662,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1493
1662
|
missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
|
|
1494
1663
|
missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
|
|
1495
1664
|
};
|
|
1496
|
-
reply("systemStatus",
|
|
1665
|
+
reply("systemStatus", statusForBundle, latency() + 400);
|
|
1497
1666
|
return;
|
|
1498
1667
|
}
|
|
1499
1668
|
case "jsError":
|
|
@@ -1517,11 +1686,170 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1517
1686
|
return;
|
|
1518
1687
|
}
|
|
1519
1688
|
};
|
|
1689
|
+
function emitConnection(deviceId) {
|
|
1690
|
+
if (!subscribed.has(deviceId)) return;
|
|
1691
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1692
|
+
if (!device) return;
|
|
1693
|
+
reply("deviceConnection", {
|
|
1694
|
+
serialNo: device.serialNo,
|
|
1695
|
+
...idOf(device),
|
|
1696
|
+
isConnect: connected.get(deviceId) === true
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
function syncStream(deviceId) {
|
|
1700
|
+
const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
|
|
1701
|
+
const running2 = streams.get(deviceId);
|
|
1702
|
+
if (want && !running2) {
|
|
1703
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1704
|
+
if (!device) return;
|
|
1705
|
+
streams.set(
|
|
1706
|
+
deviceId,
|
|
1707
|
+
setInterval(() => {
|
|
1708
|
+
adapter.triggerEmit(
|
|
1709
|
+
"deviceLightSensors",
|
|
1710
|
+
lightSensorsReading(device, now(), "deviceId" in idOf(device))
|
|
1711
|
+
);
|
|
1712
|
+
}, 1e3)
|
|
1713
|
+
);
|
|
1714
|
+
} else if (!want && running2) {
|
|
1715
|
+
clearInterval(running2);
|
|
1716
|
+
streams.delete(deviceId);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function setConnected(deviceId, value) {
|
|
1720
|
+
if (!connected.has(deviceId)) {
|
|
1721
|
+
record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (connected.get(deviceId) === value) return;
|
|
1725
|
+
connected.set(deviceId, value);
|
|
1726
|
+
record2({
|
|
1727
|
+
kind: "info",
|
|
1728
|
+
detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
|
|
1729
|
+
});
|
|
1730
|
+
emitConnection(deviceId);
|
|
1731
|
+
syncStream(deviceId);
|
|
1732
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1733
|
+
if (device) runSession(device, value);
|
|
1734
|
+
}
|
|
1735
|
+
function setSyncError(device, text, delay) {
|
|
1736
|
+
const timer = setTimeout(() => {
|
|
1737
|
+
timers.delete(timer);
|
|
1738
|
+
const byId2 = { ...status.deviceSyncErrorById ?? {} };
|
|
1739
|
+
const bySerial = { ...status.deviceSyncError };
|
|
1740
|
+
const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
|
|
1741
|
+
if (text === null) {
|
|
1742
|
+
if (!(device.deviceId in byId2)) return;
|
|
1743
|
+
delete byId2[device.deviceId];
|
|
1744
|
+
if (!shared) delete bySerial[device.serialNo];
|
|
1745
|
+
} else {
|
|
1746
|
+
byId2[device.deviceId] = text;
|
|
1747
|
+
if (!shared) bySerial[device.serialNo] = text;
|
|
1748
|
+
}
|
|
1749
|
+
status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
|
|
1750
|
+
reply("systemStatus", statusForBundle, 0);
|
|
1751
|
+
}, delay);
|
|
1752
|
+
timers.add(timer);
|
|
1753
|
+
}
|
|
1754
|
+
function runSession(device, start) {
|
|
1755
|
+
const note = (message, messageCode, isError = false) => ({
|
|
1756
|
+
serialNo: device.serialNo,
|
|
1757
|
+
...idOf(device),
|
|
1758
|
+
isConnect: !isError,
|
|
1759
|
+
message,
|
|
1760
|
+
messageCode,
|
|
1761
|
+
isError
|
|
1762
|
+
});
|
|
1763
|
+
if (!start) {
|
|
1764
|
+
reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const id = device.deviceId;
|
|
1768
|
+
const steps = [
|
|
1769
|
+
["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
|
|
1770
|
+
["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
|
|
1771
|
+
["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
|
|
1772
|
+
["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
|
|
1773
|
+
["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
|
|
1774
|
+
...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
|
|
1775
|
+
["[Start] Get data log", "get_data_log_notification", "get_data_log"],
|
|
1776
|
+
["[Start] Send data log", "send_data_log_notification", "send_data_log"],
|
|
1777
|
+
[
|
|
1778
|
+
"[Success] Set last data log sent to server",
|
|
1779
|
+
"set_last_data_log_sent_to_server_notification",
|
|
1780
|
+
""
|
|
1781
|
+
],
|
|
1782
|
+
["[Start] Get system log", "get_system_log_notification", "get_system_log"],
|
|
1783
|
+
["[Start] Send system log", "send_system_log_notification", "send_system_log"],
|
|
1784
|
+
["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
|
|
1785
|
+
["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
|
|
1786
|
+
["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
|
|
1787
|
+
...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
|
|
1788
|
+
["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
|
|
1789
|
+
["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
|
|
1790
|
+
];
|
|
1791
|
+
const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
|
|
1792
|
+
running.add(id);
|
|
1793
|
+
sessionStart.set(id, now());
|
|
1794
|
+
let delay = latency();
|
|
1795
|
+
reply("deviceNotification", note("Device connected", null), delay);
|
|
1796
|
+
for (const [message, code, stage] of steps) {
|
|
1797
|
+
delay += STEP_MS;
|
|
1798
|
+
reply("deviceNotification", note(message, code), delay);
|
|
1799
|
+
if (stage && failAt === stage) {
|
|
1800
|
+
delay += STEP_MS;
|
|
1801
|
+
const what = message.replace(/^\[\w+\] /, "");
|
|
1802
|
+
reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
|
|
1803
|
+
setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
|
|
1804
|
+
endSession(id, delay + STEP_MS);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (stage === "set_sync_time") pendingSync.delete(id);
|
|
1808
|
+
}
|
|
1809
|
+
delay += STEP_MS;
|
|
1810
|
+
reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
|
|
1811
|
+
setSyncError(device, null, delay + 20);
|
|
1812
|
+
pendingErase.delete(id);
|
|
1813
|
+
endSession(id, delay + STEP_MS);
|
|
1814
|
+
}
|
|
1815
|
+
function endSession(deviceId, delay) {
|
|
1816
|
+
const timer = setTimeout(() => {
|
|
1817
|
+
timers.delete(timer);
|
|
1818
|
+
running.delete(deviceId);
|
|
1819
|
+
if (!realTimeOn.has(deviceId)) {
|
|
1820
|
+
setConnected(deviceId, false);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const startedAt2 = sessionStart.get(deviceId) ?? now();
|
|
1824
|
+
const hold = setTimeout(
|
|
1825
|
+
() => {
|
|
1826
|
+
timers.delete(hold);
|
|
1827
|
+
if (connected.get(deviceId) && !running.has(deviceId)) setConnected(deviceId, false);
|
|
1828
|
+
},
|
|
1829
|
+
Math.max(0, startedAt2 + (scenario.realTimeSessionMs ?? 18e4) - now())
|
|
1830
|
+
);
|
|
1831
|
+
timers.add(hold);
|
|
1832
|
+
}, delay);
|
|
1833
|
+
timers.add(timer);
|
|
1834
|
+
}
|
|
1520
1835
|
const unsubscribe = adapter.onCallHandler(handle);
|
|
1836
|
+
const first = roster[0];
|
|
1837
|
+
if (first && typeof scenario.firstSessionAfterMs === "number") {
|
|
1838
|
+
const timer = setTimeout(() => {
|
|
1839
|
+
timers.delete(timer);
|
|
1840
|
+
setConnected(first.deviceId, true);
|
|
1841
|
+
}, scenario.firstSessionAfterMs);
|
|
1842
|
+
timers.add(timer);
|
|
1843
|
+
}
|
|
1521
1844
|
if (scenario.serviceStartMs > 0) {
|
|
1522
1845
|
const timer = setTimeout(() => {
|
|
1523
1846
|
timers.delete(timer);
|
|
1524
1847
|
record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
|
|
1848
|
+
if (pendingUserDevice) {
|
|
1849
|
+
pendingUserDevice = false;
|
|
1850
|
+
record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
|
|
1851
|
+
answerUserDevice();
|
|
1852
|
+
}
|
|
1525
1853
|
}, scenario.serviceStartMs);
|
|
1526
1854
|
timers.add(timer);
|
|
1527
1855
|
}
|
|
@@ -1537,8 +1865,15 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1537
1865
|
}
|
|
1538
1866
|
reply("navigationBack", {}, 0);
|
|
1539
1867
|
},
|
|
1868
|
+
setStatus(patch) {
|
|
1869
|
+
status = { ...status, ...patch };
|
|
1870
|
+
reply("systemStatus", statusForBundle(), 0);
|
|
1871
|
+
},
|
|
1872
|
+
setConnected,
|
|
1540
1873
|
dispose() {
|
|
1541
1874
|
unsubscribe();
|
|
1875
|
+
for (const t of streams.values()) clearInterval(t);
|
|
1876
|
+
streams.clear();
|
|
1542
1877
|
for (const t of timers) clearTimeout(t);
|
|
1543
1878
|
timers.clear();
|
|
1544
1879
|
}
|
|
@@ -1598,15 +1933,18 @@ async function createDevHarness(opts) {
|
|
|
1598
1933
|
iframe.setAttribute("title", "Condor Canvas bundle");
|
|
1599
1934
|
iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
|
|
1600
1935
|
(opts.mount ?? document.body).appendChild(iframe);
|
|
1936
|
+
let confirmations = "ask";
|
|
1601
1937
|
const shell = new BridgeShell({
|
|
1602
1938
|
iframe,
|
|
1603
1939
|
policy: CANVAS_POLICY,
|
|
1604
|
-
onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
|
|
1940
|
+
onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
|
|
1605
1941
|
});
|
|
1606
1942
|
if (opts.devPanel !== false) {
|
|
1607
1943
|
installDevPanel({ adapter, options: { startCollapsed: true } });
|
|
1608
1944
|
}
|
|
1609
|
-
|
|
1945
|
+
const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
|
|
1946
|
+
const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
|
|
1947
|
+
await shell.bootstrapClientIframeWithHtml(html);
|
|
1610
1948
|
try {
|
|
1611
1949
|
const result = await shell.verifySandbox();
|
|
1612
1950
|
if (!result.ok) {
|
|
@@ -1631,6 +1969,15 @@ async function createDevHarness(opts) {
|
|
|
1631
1969
|
emit(key, payload) {
|
|
1632
1970
|
Bridge.mockEmit(key, payload);
|
|
1633
1971
|
},
|
|
1972
|
+
setStatus(patch) {
|
|
1973
|
+
native.setStatus(patch);
|
|
1974
|
+
},
|
|
1975
|
+
setConnected(deviceId, value) {
|
|
1976
|
+
native.setConnected(deviceId, value);
|
|
1977
|
+
},
|
|
1978
|
+
setConfirmations(mode) {
|
|
1979
|
+
confirmations = mode;
|
|
1980
|
+
},
|
|
1634
1981
|
pressBack() {
|
|
1635
1982
|
native.pressBack();
|
|
1636
1983
|
},
|
|
@@ -1652,6 +1999,7 @@ async function createDevHarness(opts) {
|
|
|
1652
1999
|
createDevHarness,
|
|
1653
2000
|
createSimulatedNative,
|
|
1654
2001
|
formatLocalTimestamp,
|
|
2002
|
+
lightSensorsReading,
|
|
1655
2003
|
parseLocalTimestamp,
|
|
1656
2004
|
resolveScenario,
|
|
1657
2005
|
sensorLogPage,
|