@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.js
CHANGED
|
@@ -48,7 +48,7 @@ function telemetry(spec, now) {
|
|
|
48
48
|
serialNo: spec.serialNo,
|
|
49
49
|
attemptDate: now - 10 * 6e4,
|
|
50
50
|
message: spec.syncError,
|
|
51
|
-
messageCode: "
|
|
51
|
+
messageCode: "send_data_log_error"
|
|
52
52
|
}
|
|
53
53
|
] : []
|
|
54
54
|
};
|
|
@@ -132,6 +132,7 @@ function systemStatus(overrides = {}) {
|
|
|
132
132
|
internet: true,
|
|
133
133
|
permission: true,
|
|
134
134
|
hasDevices: true,
|
|
135
|
+
hasDevicesKnown: true,
|
|
135
136
|
missingOptionalPermissions: [],
|
|
136
137
|
missingEssentialPermissions: [],
|
|
137
138
|
deviceSyncError: {},
|
|
@@ -147,13 +148,15 @@ function lightAt(epochMs, serialNo) {
|
|
|
147
148
|
const photopic = Math.round((daylight * 1800 + noise * 40) * 10) / 10;
|
|
148
149
|
return { photopic, melanopic: Math.round(photopic * 0.62 * 10) / 10 };
|
|
149
150
|
}
|
|
150
|
-
function logAt(epochMs, serialNo, deviceId) {
|
|
151
|
-
const { photopic, melanopic } = lightAt(epochMs, serialNo);
|
|
151
|
+
function logAt(epochMs, serialNo, deviceId, bodyPart, docked) {
|
|
152
|
+
const { photopic, melanopic } = docked ? { photopic: 0, melanopic: 0 } : lightAt(epochMs, serialNo);
|
|
152
153
|
return {
|
|
153
154
|
id: Math.floor(epochMs / 1e3) * 10 + serialNo % 10,
|
|
154
155
|
log_time: formatLocalTimestamp(new Date(epochMs)),
|
|
155
156
|
log_type: 1,
|
|
156
|
-
body_part:
|
|
157
|
+
body_part: bodyPart,
|
|
158
|
+
// The device's raw wear flag; the name misleads: 1 = worn, 0 = on the dock.
|
|
159
|
+
off_wrist: docked ? 0 : 1,
|
|
157
160
|
temperature: 31.5,
|
|
158
161
|
lux_photopic: photopic,
|
|
159
162
|
lux_melanopic: melanopic,
|
|
@@ -161,6 +164,67 @@ function logAt(epochMs, serialNo, deviceId) {
|
|
|
161
164
|
act_data_pim: 120
|
|
162
165
|
};
|
|
163
166
|
}
|
|
167
|
+
function spectrum(nm, photopic) {
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const w of nm) {
|
|
170
|
+
const weight = Math.exp(-(((w - 555) / 120) ** 2));
|
|
171
|
+
out[`wavelength${w}`] = Math.round(photopic * weight * 0.01 * 1e4) / 1e4;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
var GEN1_NM = [390, 415, 445, 480, 515, 555, 590, 630, 680, 750];
|
|
176
|
+
var GEN2_NM = [395, 426, 457, 488, 520, 551, 582, 613, 644, 675, 707, 738, 769, 800];
|
|
177
|
+
function lightSensorsReading(device, epochMs = Date.now(), withDeviceId = true) {
|
|
178
|
+
const { photopic, melanopic } = lightAt(epochMs, device.serialNo);
|
|
179
|
+
const raw = {
|
|
180
|
+
light_sensor_f1: 0.01,
|
|
181
|
+
light_sensor_f2: 0.02,
|
|
182
|
+
light_sensor_f3: 0.03,
|
|
183
|
+
light_sensor_f4: 0.04,
|
|
184
|
+
light_sensor_f5: 0.05,
|
|
185
|
+
light_sensor_f6: 0.04,
|
|
186
|
+
light_sensor_f7: 0.03,
|
|
187
|
+
light_sensor_f8: 0.02,
|
|
188
|
+
light_sensor_clear: Math.round(photopic * 2e-4 * 1e4) / 1e4,
|
|
189
|
+
light_sensor_nir: 0.01
|
|
190
|
+
};
|
|
191
|
+
const address = {
|
|
192
|
+
serialNo: device.serialNo,
|
|
193
|
+
...withDeviceId ? { deviceId: device.deviceId } : {}
|
|
194
|
+
};
|
|
195
|
+
if (device.modelId === "actlumus_plus") {
|
|
196
|
+
return {
|
|
197
|
+
...address,
|
|
198
|
+
type: "actlumus2",
|
|
199
|
+
data: {
|
|
200
|
+
...raw,
|
|
201
|
+
...spectrum(GEN2_NM, photopic),
|
|
202
|
+
time: epochMs,
|
|
203
|
+
photopic,
|
|
204
|
+
melanopic,
|
|
205
|
+
light_sensor_z: 9e-3,
|
|
206
|
+
light_sensor_y: 0.038,
|
|
207
|
+
light_sensor_x: 0.044,
|
|
208
|
+
light_sensor_fd: 0
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
...address,
|
|
214
|
+
type: "actlumus",
|
|
215
|
+
data: {
|
|
216
|
+
...raw,
|
|
217
|
+
...spectrum(GEN1_NM, photopic),
|
|
218
|
+
time: epochMs,
|
|
219
|
+
photopic,
|
|
220
|
+
melanopic,
|
|
221
|
+
light_sensor_z: null,
|
|
222
|
+
light_sensor_y: null,
|
|
223
|
+
light_sensor_x: null,
|
|
224
|
+
light_sensor_fd: null
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
164
228
|
var HISTORY_DAYS = 14;
|
|
165
229
|
var encodeCursor = (epochMs) => `mock:${epochMs}`;
|
|
166
230
|
var decodeCursor = (cursor) => Number(cursor.slice("mock:".length));
|
|
@@ -175,7 +239,15 @@ function sensorLogPage(query, opts = {}) {
|
|
|
175
239
|
return { requestId: query.requestId, success: false, error: "CONFLICTING_CURSORS" };
|
|
176
240
|
}
|
|
177
241
|
const newest = Math.floor(now / periodMs) * periodMs;
|
|
178
|
-
const
|
|
242
|
+
const history = opts.history ?? {};
|
|
243
|
+
const oldest = newest - (history.days ?? HISTORY_DAYS) * 864e5;
|
|
244
|
+
const dock = new Set(history.dockHours ?? [7]);
|
|
245
|
+
const gapHours = new Set(history.gapHours ?? []);
|
|
246
|
+
const gapDates = new Set(history.gapDates ?? []);
|
|
247
|
+
const present = (t) => {
|
|
248
|
+
const d = new Date(t);
|
|
249
|
+
return !gapHours.has(d.getHours()) && !gapDates.has(formatLocalTimestamp(d).slice(0, 10));
|
|
250
|
+
};
|
|
179
251
|
const rangeStart = query.startTime ? parseLocalTimestamp(query.startTime) : oldest;
|
|
180
252
|
const rangeEnd = query.endTime ? parseLocalTimestamp(query.endTime) - 1 : newest;
|
|
181
253
|
if (Number.isNaN(rangeStart) || Number.isNaN(rangeEnd) || rangeStart > rangeEnd + 1) {
|
|
@@ -183,33 +255,41 @@ function sensorLogPage(query, opts = {}) {
|
|
|
183
255
|
}
|
|
184
256
|
const first = Math.max(oldest, Math.ceil(rangeStart / periodMs) * periodMs);
|
|
185
257
|
const last = Math.min(newest, Math.floor(rangeEnd / periodMs) * periodMs);
|
|
186
|
-
|
|
187
|
-
|
|
258
|
+
const walk = (start, step, max) => {
|
|
259
|
+
const out = [];
|
|
260
|
+
for (let t = start; t >= first && t <= last && out.length < max; t += step) {
|
|
261
|
+
if (present(t)) out.push(t);
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
};
|
|
265
|
+
let slots;
|
|
188
266
|
if (query.endingBefore) {
|
|
189
|
-
|
|
190
|
-
|
|
267
|
+
slots = walk(
|
|
268
|
+
Math.min(last, decodeCursor(query.endingBefore) - periodMs),
|
|
269
|
+
-periodMs,
|
|
270
|
+
limit
|
|
271
|
+
).reverse();
|
|
191
272
|
} else if (query.startingAfter) {
|
|
192
|
-
|
|
193
|
-
to = Math.min(last, from + (limit - 1) * periodMs);
|
|
273
|
+
slots = walk(Math.max(first, decodeCursor(query.startingAfter) + periodMs), periodMs, limit);
|
|
194
274
|
} else if (query.startTime) {
|
|
195
|
-
|
|
196
|
-
to = Math.min(last, from + (limit - 1) * periodMs);
|
|
275
|
+
slots = walk(first, periodMs, limit);
|
|
197
276
|
} else {
|
|
198
|
-
|
|
199
|
-
from = Math.max(first, to - (limit - 1) * periodMs);
|
|
200
|
-
}
|
|
201
|
-
const logs = [];
|
|
202
|
-
for (let t = from; t <= to; t += periodMs) {
|
|
203
|
-
logs.push(logAt(t, query.serialNo, opts.deviceId ?? 0));
|
|
277
|
+
slots = walk(last, -periodMs, limit).reverse();
|
|
204
278
|
}
|
|
279
|
+
const bodyPart = opts.bodyPart ?? "wrist";
|
|
280
|
+
const logs = slots.map(
|
|
281
|
+
(t) => logAt(t, query.serialNo, opts.deviceId ?? 0, bodyPart, dock.has(new Date(t).getHours()))
|
|
282
|
+
);
|
|
283
|
+
const from = slots[0] ?? 0;
|
|
284
|
+
const to = slots[slots.length - 1] ?? 0;
|
|
205
285
|
return {
|
|
206
286
|
requestId: query.requestId,
|
|
207
287
|
success: true,
|
|
208
288
|
logs,
|
|
209
289
|
startingAfter: logs.length ? encodeCursor(to) : null,
|
|
210
290
|
endingBefore: logs.length ? encodeCursor(from) : null,
|
|
211
|
-
hasMore: logs.length > 0 && to
|
|
212
|
-
hasPrevious: logs.length > 0 && from >
|
|
291
|
+
hasMore: logs.length > 0 && walk(to + periodMs, periodMs, 1).length > 0,
|
|
292
|
+
hasPrevious: logs.length > 0 && walk(from - periodMs, -periodMs, 1).length > 0
|
|
213
293
|
};
|
|
214
294
|
}
|
|
215
295
|
|
|
@@ -1203,6 +1283,19 @@ var BridgeShell = class {
|
|
|
1203
1283
|
}
|
|
1204
1284
|
};
|
|
1205
1285
|
|
|
1286
|
+
// src/version.ts
|
|
1287
|
+
function versionAtLeast(version, min) {
|
|
1288
|
+
if (!version) return false;
|
|
1289
|
+
const a = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1290
|
+
const b = min.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1291
|
+
for (let i = 0; i < 3; i++) {
|
|
1292
|
+
const x = a[i] ?? 0;
|
|
1293
|
+
const y = b[i] ?? 0;
|
|
1294
|
+
if (x !== y) return x > y;
|
|
1295
|
+
}
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1206
1299
|
// src/dev/policy.ts
|
|
1207
1300
|
var CANVAS_POLICY = {
|
|
1208
1301
|
readOnlyKeys: [
|
|
@@ -1238,28 +1331,27 @@ var CANVAS_POLICY = {
|
|
|
1238
1331
|
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';";
|
|
1239
1332
|
|
|
1240
1333
|
// src/dev/scenarios.ts
|
|
1241
|
-
var HARNESS_NATIVE_VERSION = "0.
|
|
1334
|
+
var HARNESS_NATIVE_VERSION = "0.18.0";
|
|
1242
1335
|
var base = {
|
|
1243
|
-
description: "Two devices, background service already running, realistic latency.",
|
|
1336
|
+
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.",
|
|
1244
1337
|
nativeVersion: HARNESS_NATIVE_VERSION,
|
|
1245
1338
|
latencyMs: [80, 250],
|
|
1246
1339
|
serviceStartMs: 0,
|
|
1247
|
-
userDeviceSilent: false,
|
|
1248
1340
|
logPeriodS: 60,
|
|
1249
1341
|
systemStatus: systemStatus(),
|
|
1250
1342
|
devices: () => twoDevices()
|
|
1251
1343
|
};
|
|
1252
1344
|
var SCENARIOS = {
|
|
1253
|
-
default: base,
|
|
1345
|
+
default: { ...base, firstSessionAfterMs: 3e3 },
|
|
1254
1346
|
"cold-start": {
|
|
1255
1347
|
...base,
|
|
1256
|
-
description: "App launched from cold: the background service takes 4 s to start, and every ping/userDevice sent before that is lost.
|
|
1348
|
+
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.",
|
|
1257
1349
|
serviceStartMs: 4e3
|
|
1258
1350
|
},
|
|
1259
1351
|
"no-devices": {
|
|
1260
1352
|
...base,
|
|
1261
|
-
description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
|
|
1262
|
-
|
|
1353
|
+
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.",
|
|
1354
|
+
userDeviceError: "NO_DEVICES",
|
|
1263
1355
|
systemStatus: systemStatus({ hasDevices: false })
|
|
1264
1356
|
},
|
|
1265
1357
|
"shared-serial": {
|
|
@@ -1269,9 +1361,8 @@ var SCENARIOS = {
|
|
|
1269
1361
|
},
|
|
1270
1362
|
offline: {
|
|
1271
1363
|
...base,
|
|
1272
|
-
description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
|
|
1273
|
-
|
|
1274
|
-
systemStatus: systemStatus({ internet: false })
|
|
1364
|
+
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 }).",
|
|
1365
|
+
systemStatus: systemStatus({ internet: false, hasDevicesKnown: false })
|
|
1275
1366
|
}
|
|
1276
1367
|
};
|
|
1277
1368
|
function resolveScenario(scenario) {
|
|
@@ -1290,6 +1381,7 @@ function resolveScenario(scenario) {
|
|
|
1290
1381
|
|
|
1291
1382
|
// src/dev/harness.ts
|
|
1292
1383
|
var DEV_HARNESS_MARKER = "__condorDevHarness__";
|
|
1384
|
+
var STEP_MS = 200;
|
|
1293
1385
|
function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
1294
1386
|
const now = hooks.now ?? Date.now;
|
|
1295
1387
|
const startedAt = now();
|
|
@@ -1297,6 +1389,26 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1297
1389
|
const timers = /* @__PURE__ */ new Set();
|
|
1298
1390
|
let status = { ...scenario.systemStatus };
|
|
1299
1391
|
let canGoBack = false;
|
|
1392
|
+
const alwaysReplies = versionAtLeast(scenario.nativeVersion, "0.18.0");
|
|
1393
|
+
let pendingUserDevice = false;
|
|
1394
|
+
let sensorLogCount = 0;
|
|
1395
|
+
const roster = scenario.devices().devicesConfig;
|
|
1396
|
+
const connected = new Map(roster.map((d) => [d.deviceId, false]));
|
|
1397
|
+
const subscribed = /* @__PURE__ */ new Set();
|
|
1398
|
+
const realTimeOn = /* @__PURE__ */ new Set();
|
|
1399
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1400
|
+
const byId = versionAtLeast(scenario.nativeVersion, "0.17.0");
|
|
1401
|
+
const pendingSync = /* @__PURE__ */ new Set();
|
|
1402
|
+
const pendingErase = /* @__PURE__ */ new Set();
|
|
1403
|
+
const running = /* @__PURE__ */ new Set();
|
|
1404
|
+
const sessionStart = /* @__PURE__ */ new Map();
|
|
1405
|
+
const idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
|
|
1406
|
+
const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
|
|
1407
|
+
const statusForBundle = () => {
|
|
1408
|
+
if (alwaysReplies) return status;
|
|
1409
|
+
const { hasDevicesKnown: _omitted, ...older } = status;
|
|
1410
|
+
return older;
|
|
1411
|
+
};
|
|
1300
1412
|
const record2 = (entry) => {
|
|
1301
1413
|
const full = { at: now(), ...entry };
|
|
1302
1414
|
log.push(full);
|
|
@@ -1311,12 +1423,34 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1311
1423
|
const reply = (key, payload, delay = latency()) => {
|
|
1312
1424
|
const timer = setTimeout(() => {
|
|
1313
1425
|
timers.delete(timer);
|
|
1314
|
-
|
|
1426
|
+
if (typeof payload === "function") payload = payload();
|
|
1427
|
+
record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
|
|
1315
1428
|
adapter.triggerEmit(key, payload);
|
|
1316
1429
|
}, delay);
|
|
1317
1430
|
timers.add(timer);
|
|
1318
1431
|
};
|
|
1319
1432
|
const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
|
|
1433
|
+
const answerUserDevice = () => {
|
|
1434
|
+
if (scenario.userDeviceError) {
|
|
1435
|
+
if (!alwaysReplies) {
|
|
1436
|
+
record2({
|
|
1437
|
+
kind: "dropped",
|
|
1438
|
+
command: "userDevice",
|
|
1439
|
+
detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
|
|
1440
|
+
});
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
reply("userDevice", {
|
|
1444
|
+
lastUpdate: now(),
|
|
1445
|
+
devicesConfig: [],
|
|
1446
|
+
devicesTelemetry: [],
|
|
1447
|
+
devicesUserConfig: [],
|
|
1448
|
+
error: scenario.userDeviceError
|
|
1449
|
+
});
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
reply("userDevice", scenario.devices());
|
|
1453
|
+
};
|
|
1320
1454
|
const language = () => {
|
|
1321
1455
|
const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
|
|
1322
1456
|
const lang = deviceLocale.slice(0, 2);
|
|
@@ -1333,7 +1467,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1333
1467
|
if (command !== "handshake") {
|
|
1334
1468
|
const shown = JSON.stringify(args);
|
|
1335
1469
|
const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
|
|
1336
|
-
record2({ kind: "sent", command, detail });
|
|
1470
|
+
record2({ kind: "sent", command, detail, args });
|
|
1337
1471
|
}
|
|
1338
1472
|
switch (command) {
|
|
1339
1473
|
case "handshake":
|
|
@@ -1345,8 +1479,8 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1345
1479
|
return;
|
|
1346
1480
|
}
|
|
1347
1481
|
reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
|
|
1348
|
-
reply("systemStatus",
|
|
1349
|
-
reply("language", language
|
|
1482
|
+
reply("systemStatus", statusForBundle, latency() + 20);
|
|
1483
|
+
reply("language", language, latency() + 30);
|
|
1350
1484
|
return;
|
|
1351
1485
|
case "ping":
|
|
1352
1486
|
if (!serviceUp()) {
|
|
@@ -1361,30 +1495,33 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1361
1495
|
return;
|
|
1362
1496
|
case "userDevice":
|
|
1363
1497
|
if (!serviceUp()) {
|
|
1498
|
+
pendingUserDevice = alwaysReplies;
|
|
1364
1499
|
record2({
|
|
1365
1500
|
kind: "dropped",
|
|
1366
1501
|
command,
|
|
1367
|
-
detail: "userDevice lost: the background service is not running yet. Ask again."
|
|
1368
|
-
});
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
if (scenario.userDeviceSilent) {
|
|
1372
|
-
record2({
|
|
1373
|
-
kind: "dropped",
|
|
1374
|
-
command,
|
|
1375
|
-
detail: "userDevice unanswered: the account has no linked device (the app replies only on success)"
|
|
1502
|
+
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."
|
|
1376
1503
|
});
|
|
1377
1504
|
return;
|
|
1378
1505
|
}
|
|
1379
|
-
|
|
1506
|
+
answerUserDevice();
|
|
1380
1507
|
return;
|
|
1381
1508
|
case "sensorLog": {
|
|
1382
1509
|
const query = args;
|
|
1383
|
-
|
|
1510
|
+
sensorLogCount += 1;
|
|
1511
|
+
if (scenario.sensorLogFailOnRequest === sensorLogCount) {
|
|
1384
1512
|
reply("sensorLog", {
|
|
1385
1513
|
requestId: query.requestId,
|
|
1386
1514
|
success: false,
|
|
1387
|
-
error:
|
|
1515
|
+
error: "Connection failed"
|
|
1516
|
+
});
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
|
|
1520
|
+
if (logError) {
|
|
1521
|
+
reply("sensorLog", {
|
|
1522
|
+
requestId: query.requestId,
|
|
1523
|
+
success: false,
|
|
1524
|
+
error: logError
|
|
1388
1525
|
});
|
|
1389
1526
|
return;
|
|
1390
1527
|
}
|
|
@@ -1403,12 +1540,29 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1403
1540
|
sensorLogPage(query, {
|
|
1404
1541
|
now: now(),
|
|
1405
1542
|
periodS: scenario.logPeriodS,
|
|
1406
|
-
deviceId: device?.deviceId
|
|
1543
|
+
deviceId: device?.deviceId,
|
|
1544
|
+
bodyPart: device?.deviceBodyPart ?? "wrist",
|
|
1545
|
+
history: scenario.history
|
|
1407
1546
|
})
|
|
1408
1547
|
);
|
|
1409
1548
|
return;
|
|
1410
1549
|
}
|
|
1411
|
-
case "realTime":
|
|
1550
|
+
case "realTime": {
|
|
1551
|
+
const device = findDevice(args);
|
|
1552
|
+
if (device) {
|
|
1553
|
+
if (args.value === true) realTimeOn.add(device.deviceId);
|
|
1554
|
+
else realTimeOn.delete(device.deviceId);
|
|
1555
|
+
syncStream(device.deviceId);
|
|
1556
|
+
if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
|
|
1557
|
+
endSession(device.deviceId, latency());
|
|
1558
|
+
}
|
|
1559
|
+
if (args.value === true && !connected.get(device.deviceId)) {
|
|
1560
|
+
record2({
|
|
1561
|
+
kind: "info",
|
|
1562
|
+
detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1412
1566
|
reply("realTime", {
|
|
1413
1567
|
serialNo: args.serialNo,
|
|
1414
1568
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
@@ -1416,23 +1570,37 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1416
1570
|
value: args.value === true
|
|
1417
1571
|
});
|
|
1418
1572
|
return;
|
|
1573
|
+
}
|
|
1419
1574
|
case "syncTime":
|
|
1420
1575
|
case "deleteMemory":
|
|
1576
|
+
{
|
|
1577
|
+
const target = findDevice(args);
|
|
1578
|
+
if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
|
|
1579
|
+
}
|
|
1580
|
+
record2({
|
|
1581
|
+
kind: "info",
|
|
1582
|
+
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"
|
|
1583
|
+
});
|
|
1421
1584
|
reply(command, {
|
|
1422
1585
|
serialNo: args.serialNo,
|
|
1423
1586
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1424
1587
|
success: true
|
|
1425
1588
|
});
|
|
1426
1589
|
return;
|
|
1427
|
-
case "deviceConnection":
|
|
1428
|
-
|
|
1590
|
+
case "deviceConnection": {
|
|
1591
|
+
const device = findDevice(args);
|
|
1592
|
+
if (!device) {
|
|
1429
1593
|
reply("deviceConnection", {
|
|
1430
1594
|
serialNo: args.serialNo,
|
|
1431
1595
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1432
|
-
isConnect:
|
|
1596
|
+
isConnect: false
|
|
1433
1597
|
});
|
|
1598
|
+
return;
|
|
1434
1599
|
}
|
|
1600
|
+
if (args.type === "add") subscribed.add(device.deviceId);
|
|
1601
|
+
else if (args.type === "remove") subscribed.delete(device.deviceId);
|
|
1435
1602
|
return;
|
|
1603
|
+
}
|
|
1436
1604
|
case "language":
|
|
1437
1605
|
reply("language", language());
|
|
1438
1606
|
return;
|
|
@@ -1443,7 +1611,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1443
1611
|
missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
|
|
1444
1612
|
missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
|
|
1445
1613
|
};
|
|
1446
|
-
reply("systemStatus",
|
|
1614
|
+
reply("systemStatus", statusForBundle, latency() + 400);
|
|
1447
1615
|
return;
|
|
1448
1616
|
}
|
|
1449
1617
|
case "jsError":
|
|
@@ -1467,11 +1635,170 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1467
1635
|
return;
|
|
1468
1636
|
}
|
|
1469
1637
|
};
|
|
1638
|
+
function emitConnection(deviceId) {
|
|
1639
|
+
if (!subscribed.has(deviceId)) return;
|
|
1640
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1641
|
+
if (!device) return;
|
|
1642
|
+
reply("deviceConnection", {
|
|
1643
|
+
serialNo: device.serialNo,
|
|
1644
|
+
...idOf(device),
|
|
1645
|
+
isConnect: connected.get(deviceId) === true
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
function syncStream(deviceId) {
|
|
1649
|
+
const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
|
|
1650
|
+
const running2 = streams.get(deviceId);
|
|
1651
|
+
if (want && !running2) {
|
|
1652
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1653
|
+
if (!device) return;
|
|
1654
|
+
streams.set(
|
|
1655
|
+
deviceId,
|
|
1656
|
+
setInterval(() => {
|
|
1657
|
+
adapter.triggerEmit(
|
|
1658
|
+
"deviceLightSensors",
|
|
1659
|
+
lightSensorsReading(device, now(), "deviceId" in idOf(device))
|
|
1660
|
+
);
|
|
1661
|
+
}, 1e3)
|
|
1662
|
+
);
|
|
1663
|
+
} else if (!want && running2) {
|
|
1664
|
+
clearInterval(running2);
|
|
1665
|
+
streams.delete(deviceId);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function setConnected(deviceId, value) {
|
|
1669
|
+
if (!connected.has(deviceId)) {
|
|
1670
|
+
record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (connected.get(deviceId) === value) return;
|
|
1674
|
+
connected.set(deviceId, value);
|
|
1675
|
+
record2({
|
|
1676
|
+
kind: "info",
|
|
1677
|
+
detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
|
|
1678
|
+
});
|
|
1679
|
+
emitConnection(deviceId);
|
|
1680
|
+
syncStream(deviceId);
|
|
1681
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1682
|
+
if (device) runSession(device, value);
|
|
1683
|
+
}
|
|
1684
|
+
function setSyncError(device, text, delay) {
|
|
1685
|
+
const timer = setTimeout(() => {
|
|
1686
|
+
timers.delete(timer);
|
|
1687
|
+
const byId2 = { ...status.deviceSyncErrorById ?? {} };
|
|
1688
|
+
const bySerial = { ...status.deviceSyncError };
|
|
1689
|
+
const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
|
|
1690
|
+
if (text === null) {
|
|
1691
|
+
if (!(device.deviceId in byId2)) return;
|
|
1692
|
+
delete byId2[device.deviceId];
|
|
1693
|
+
if (!shared) delete bySerial[device.serialNo];
|
|
1694
|
+
} else {
|
|
1695
|
+
byId2[device.deviceId] = text;
|
|
1696
|
+
if (!shared) bySerial[device.serialNo] = text;
|
|
1697
|
+
}
|
|
1698
|
+
status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
|
|
1699
|
+
reply("systemStatus", statusForBundle, 0);
|
|
1700
|
+
}, delay);
|
|
1701
|
+
timers.add(timer);
|
|
1702
|
+
}
|
|
1703
|
+
function runSession(device, start) {
|
|
1704
|
+
const note = (message, messageCode, isError = false) => ({
|
|
1705
|
+
serialNo: device.serialNo,
|
|
1706
|
+
...idOf(device),
|
|
1707
|
+
isConnect: !isError,
|
|
1708
|
+
message,
|
|
1709
|
+
messageCode,
|
|
1710
|
+
isError
|
|
1711
|
+
});
|
|
1712
|
+
if (!start) {
|
|
1713
|
+
reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const id = device.deviceId;
|
|
1717
|
+
const steps = [
|
|
1718
|
+
["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
|
|
1719
|
+
["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
|
|
1720
|
+
["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
|
|
1721
|
+
["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
|
|
1722
|
+
["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
|
|
1723
|
+
...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
|
|
1724
|
+
["[Start] Get data log", "get_data_log_notification", "get_data_log"],
|
|
1725
|
+
["[Start] Send data log", "send_data_log_notification", "send_data_log"],
|
|
1726
|
+
[
|
|
1727
|
+
"[Success] Set last data log sent to server",
|
|
1728
|
+
"set_last_data_log_sent_to_server_notification",
|
|
1729
|
+
""
|
|
1730
|
+
],
|
|
1731
|
+
["[Start] Get system log", "get_system_log_notification", "get_system_log"],
|
|
1732
|
+
["[Start] Send system log", "send_system_log_notification", "send_system_log"],
|
|
1733
|
+
["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
|
|
1734
|
+
["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
|
|
1735
|
+
["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
|
|
1736
|
+
...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
|
|
1737
|
+
["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
|
|
1738
|
+
["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
|
|
1739
|
+
];
|
|
1740
|
+
const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
|
|
1741
|
+
running.add(id);
|
|
1742
|
+
sessionStart.set(id, now());
|
|
1743
|
+
let delay = latency();
|
|
1744
|
+
reply("deviceNotification", note("Device connected", null), delay);
|
|
1745
|
+
for (const [message, code, stage] of steps) {
|
|
1746
|
+
delay += STEP_MS;
|
|
1747
|
+
reply("deviceNotification", note(message, code), delay);
|
|
1748
|
+
if (stage && failAt === stage) {
|
|
1749
|
+
delay += STEP_MS;
|
|
1750
|
+
const what = message.replace(/^\[\w+\] /, "");
|
|
1751
|
+
reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
|
|
1752
|
+
setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
|
|
1753
|
+
endSession(id, delay + STEP_MS);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (stage === "set_sync_time") pendingSync.delete(id);
|
|
1757
|
+
}
|
|
1758
|
+
delay += STEP_MS;
|
|
1759
|
+
reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
|
|
1760
|
+
setSyncError(device, null, delay + 20);
|
|
1761
|
+
pendingErase.delete(id);
|
|
1762
|
+
endSession(id, delay + STEP_MS);
|
|
1763
|
+
}
|
|
1764
|
+
function endSession(deviceId, delay) {
|
|
1765
|
+
const timer = setTimeout(() => {
|
|
1766
|
+
timers.delete(timer);
|
|
1767
|
+
running.delete(deviceId);
|
|
1768
|
+
if (!realTimeOn.has(deviceId)) {
|
|
1769
|
+
setConnected(deviceId, false);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const startedAt2 = sessionStart.get(deviceId) ?? now();
|
|
1773
|
+
const hold = setTimeout(
|
|
1774
|
+
() => {
|
|
1775
|
+
timers.delete(hold);
|
|
1776
|
+
if (connected.get(deviceId) && !running.has(deviceId)) setConnected(deviceId, false);
|
|
1777
|
+
},
|
|
1778
|
+
Math.max(0, startedAt2 + (scenario.realTimeSessionMs ?? 18e4) - now())
|
|
1779
|
+
);
|
|
1780
|
+
timers.add(hold);
|
|
1781
|
+
}, delay);
|
|
1782
|
+
timers.add(timer);
|
|
1783
|
+
}
|
|
1470
1784
|
const unsubscribe = adapter.onCallHandler(handle);
|
|
1785
|
+
const first = roster[0];
|
|
1786
|
+
if (first && typeof scenario.firstSessionAfterMs === "number") {
|
|
1787
|
+
const timer = setTimeout(() => {
|
|
1788
|
+
timers.delete(timer);
|
|
1789
|
+
setConnected(first.deviceId, true);
|
|
1790
|
+
}, scenario.firstSessionAfterMs);
|
|
1791
|
+
timers.add(timer);
|
|
1792
|
+
}
|
|
1471
1793
|
if (scenario.serviceStartMs > 0) {
|
|
1472
1794
|
const timer = setTimeout(() => {
|
|
1473
1795
|
timers.delete(timer);
|
|
1474
1796
|
record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
|
|
1797
|
+
if (pendingUserDevice) {
|
|
1798
|
+
pendingUserDevice = false;
|
|
1799
|
+
record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
|
|
1800
|
+
answerUserDevice();
|
|
1801
|
+
}
|
|
1475
1802
|
}, scenario.serviceStartMs);
|
|
1476
1803
|
timers.add(timer);
|
|
1477
1804
|
}
|
|
@@ -1487,8 +1814,15 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1487
1814
|
}
|
|
1488
1815
|
reply("navigationBack", {}, 0);
|
|
1489
1816
|
},
|
|
1817
|
+
setStatus(patch) {
|
|
1818
|
+
status = { ...status, ...patch };
|
|
1819
|
+
reply("systemStatus", statusForBundle(), 0);
|
|
1820
|
+
},
|
|
1821
|
+
setConnected,
|
|
1490
1822
|
dispose() {
|
|
1491
1823
|
unsubscribe();
|
|
1824
|
+
for (const t of streams.values()) clearInterval(t);
|
|
1825
|
+
streams.clear();
|
|
1492
1826
|
for (const t of timers) clearTimeout(t);
|
|
1493
1827
|
timers.clear();
|
|
1494
1828
|
}
|
|
@@ -1548,15 +1882,18 @@ async function createDevHarness(opts) {
|
|
|
1548
1882
|
iframe.setAttribute("title", "Condor Canvas bundle");
|
|
1549
1883
|
iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
|
|
1550
1884
|
(opts.mount ?? document.body).appendChild(iframe);
|
|
1885
|
+
let confirmations = "ask";
|
|
1551
1886
|
const shell = new BridgeShell({
|
|
1552
1887
|
iframe,
|
|
1553
1888
|
policy: CANVAS_POLICY,
|
|
1554
|
-
onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
|
|
1889
|
+
onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
|
|
1555
1890
|
});
|
|
1556
1891
|
if (opts.devPanel !== false) {
|
|
1557
1892
|
installDevPanel({ adapter, options: { startCollapsed: true } });
|
|
1558
1893
|
}
|
|
1559
|
-
|
|
1894
|
+
const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
|
|
1895
|
+
const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
|
|
1896
|
+
await shell.bootstrapClientIframeWithHtml(html);
|
|
1560
1897
|
try {
|
|
1561
1898
|
const result = await shell.verifySandbox();
|
|
1562
1899
|
if (!result.ok) {
|
|
@@ -1581,6 +1918,15 @@ async function createDevHarness(opts) {
|
|
|
1581
1918
|
emit(key, payload) {
|
|
1582
1919
|
Bridge.mockEmit(key, payload);
|
|
1583
1920
|
},
|
|
1921
|
+
setStatus(patch) {
|
|
1922
|
+
native.setStatus(patch);
|
|
1923
|
+
},
|
|
1924
|
+
setConnected(deviceId, value) {
|
|
1925
|
+
native.setConnected(deviceId, value);
|
|
1926
|
+
},
|
|
1927
|
+
setConfirmations(mode) {
|
|
1928
|
+
confirmations = mode;
|
|
1929
|
+
},
|
|
1584
1930
|
pressBack() {
|
|
1585
1931
|
native.pressBack();
|
|
1586
1932
|
},
|
|
@@ -1601,6 +1947,7 @@ export {
|
|
|
1601
1947
|
createDevHarness,
|
|
1602
1948
|
createSimulatedNative,
|
|
1603
1949
|
formatLocalTimestamp,
|
|
1950
|
+
lightSensorsReading,
|
|
1604
1951
|
parseLocalTimestamp,
|
|
1605
1952
|
resolveScenario,
|
|
1606
1953
|
sensorLogPage,
|