@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.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,25 @@ 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 idOf = (device) => byId && roster.filter((d) => d.serialNo === device.serialNo).length > 1 ? { deviceId: device.deviceId } : {};
|
|
1405
|
+
const findDevice = (args) => args.deviceId !== void 0 ? roster.find((d) => d.deviceId === args.deviceId) : roster.find((d) => d.serialNo === args.serialNo);
|
|
1406
|
+
const statusForBundle = () => {
|
|
1407
|
+
if (alwaysReplies) return status;
|
|
1408
|
+
const { hasDevicesKnown: _omitted, ...older } = status;
|
|
1409
|
+
return older;
|
|
1410
|
+
};
|
|
1300
1411
|
const record2 = (entry) => {
|
|
1301
1412
|
const full = { at: now(), ...entry };
|
|
1302
1413
|
log.push(full);
|
|
@@ -1311,12 +1422,34 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1311
1422
|
const reply = (key, payload, delay = latency()) => {
|
|
1312
1423
|
const timer = setTimeout(() => {
|
|
1313
1424
|
timers.delete(timer);
|
|
1314
|
-
|
|
1425
|
+
if (typeof payload === "function") payload = payload();
|
|
1426
|
+
record2({ kind: "reply", key, detail: `${key} \u2192 bundle`, payload });
|
|
1315
1427
|
adapter.triggerEmit(key, payload);
|
|
1316
1428
|
}, delay);
|
|
1317
1429
|
timers.add(timer);
|
|
1318
1430
|
};
|
|
1319
1431
|
const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
|
|
1432
|
+
const answerUserDevice = () => {
|
|
1433
|
+
if (scenario.userDeviceError) {
|
|
1434
|
+
if (!alwaysReplies) {
|
|
1435
|
+
record2({
|
|
1436
|
+
kind: "dropped",
|
|
1437
|
+
command: "userDevice",
|
|
1438
|
+
detail: `userDevice unanswered: the lookup failed (${scenario.userDeviceError}) and this app version replies only on success`
|
|
1439
|
+
});
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
reply("userDevice", {
|
|
1443
|
+
lastUpdate: now(),
|
|
1444
|
+
devicesConfig: [],
|
|
1445
|
+
devicesTelemetry: [],
|
|
1446
|
+
devicesUserConfig: [],
|
|
1447
|
+
error: scenario.userDeviceError
|
|
1448
|
+
});
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
reply("userDevice", scenario.devices());
|
|
1452
|
+
};
|
|
1320
1453
|
const language = () => {
|
|
1321
1454
|
const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
|
|
1322
1455
|
const lang = deviceLocale.slice(0, 2);
|
|
@@ -1333,7 +1466,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1333
1466
|
if (command !== "handshake") {
|
|
1334
1467
|
const shown = JSON.stringify(args);
|
|
1335
1468
|
const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
|
|
1336
|
-
record2({ kind: "sent", command, detail });
|
|
1469
|
+
record2({ kind: "sent", command, detail, args });
|
|
1337
1470
|
}
|
|
1338
1471
|
switch (command) {
|
|
1339
1472
|
case "handshake":
|
|
@@ -1345,8 +1478,8 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1345
1478
|
return;
|
|
1346
1479
|
}
|
|
1347
1480
|
reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
|
|
1348
|
-
reply("systemStatus",
|
|
1349
|
-
reply("language", language
|
|
1481
|
+
reply("systemStatus", statusForBundle, latency() + 20);
|
|
1482
|
+
reply("language", language, latency() + 30);
|
|
1350
1483
|
return;
|
|
1351
1484
|
case "ping":
|
|
1352
1485
|
if (!serviceUp()) {
|
|
@@ -1361,30 +1494,33 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1361
1494
|
return;
|
|
1362
1495
|
case "userDevice":
|
|
1363
1496
|
if (!serviceUp()) {
|
|
1497
|
+
pendingUserDevice = alwaysReplies;
|
|
1364
1498
|
record2({
|
|
1365
1499
|
kind: "dropped",
|
|
1366
1500
|
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)"
|
|
1501
|
+
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
1502
|
});
|
|
1377
1503
|
return;
|
|
1378
1504
|
}
|
|
1379
|
-
|
|
1505
|
+
answerUserDevice();
|
|
1380
1506
|
return;
|
|
1381
1507
|
case "sensorLog": {
|
|
1382
1508
|
const query = args;
|
|
1383
|
-
|
|
1509
|
+
sensorLogCount += 1;
|
|
1510
|
+
if (scenario.sensorLogFailOnRequest === sensorLogCount) {
|
|
1384
1511
|
reply("sensorLog", {
|
|
1385
1512
|
requestId: query.requestId,
|
|
1386
1513
|
success: false,
|
|
1387
|
-
error:
|
|
1514
|
+
error: "Connection failed"
|
|
1515
|
+
});
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const logError = scenario.sensorLogError ?? (status.internet === false ? "Connection failed" : void 0);
|
|
1519
|
+
if (logError) {
|
|
1520
|
+
reply("sensorLog", {
|
|
1521
|
+
requestId: query.requestId,
|
|
1522
|
+
success: false,
|
|
1523
|
+
error: logError
|
|
1388
1524
|
});
|
|
1389
1525
|
return;
|
|
1390
1526
|
}
|
|
@@ -1403,12 +1539,29 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1403
1539
|
sensorLogPage(query, {
|
|
1404
1540
|
now: now(),
|
|
1405
1541
|
periodS: scenario.logPeriodS,
|
|
1406
|
-
deviceId: device?.deviceId
|
|
1542
|
+
deviceId: device?.deviceId,
|
|
1543
|
+
bodyPart: device?.deviceBodyPart ?? "wrist",
|
|
1544
|
+
history: scenario.history
|
|
1407
1545
|
})
|
|
1408
1546
|
);
|
|
1409
1547
|
return;
|
|
1410
1548
|
}
|
|
1411
|
-
case "realTime":
|
|
1549
|
+
case "realTime": {
|
|
1550
|
+
const device = findDevice(args);
|
|
1551
|
+
if (device) {
|
|
1552
|
+
if (args.value === true) realTimeOn.add(device.deviceId);
|
|
1553
|
+
else realTimeOn.delete(device.deviceId);
|
|
1554
|
+
syncStream(device.deviceId);
|
|
1555
|
+
if (args.value !== true && connected.get(device.deviceId) && !running.has(device.deviceId)) {
|
|
1556
|
+
endSession(device.deviceId, latency());
|
|
1557
|
+
}
|
|
1558
|
+
if (args.value === true && !connected.get(device.deviceId)) {
|
|
1559
|
+
record2({
|
|
1560
|
+
kind: "info",
|
|
1561
|
+
detail: `realTime on for device ${device.deviceId}, which is between BLE sessions: readings start when it connects`
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1412
1565
|
reply("realTime", {
|
|
1413
1566
|
serialNo: args.serialNo,
|
|
1414
1567
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
@@ -1416,23 +1569,37 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1416
1569
|
value: args.value === true
|
|
1417
1570
|
});
|
|
1418
1571
|
return;
|
|
1572
|
+
}
|
|
1419
1573
|
case "syncTime":
|
|
1420
1574
|
case "deleteMemory":
|
|
1575
|
+
{
|
|
1576
|
+
const target = findDevice(args);
|
|
1577
|
+
if (target) (command === "syncTime" ? pendingSync : pendingErase).add(target.deviceId);
|
|
1578
|
+
}
|
|
1579
|
+
record2({
|
|
1580
|
+
kind: "info",
|
|
1581
|
+
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"
|
|
1582
|
+
});
|
|
1421
1583
|
reply(command, {
|
|
1422
1584
|
serialNo: args.serialNo,
|
|
1423
1585
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1424
1586
|
success: true
|
|
1425
1587
|
});
|
|
1426
1588
|
return;
|
|
1427
|
-
case "deviceConnection":
|
|
1428
|
-
|
|
1589
|
+
case "deviceConnection": {
|
|
1590
|
+
const device = findDevice(args);
|
|
1591
|
+
if (!device) {
|
|
1429
1592
|
reply("deviceConnection", {
|
|
1430
1593
|
serialNo: args.serialNo,
|
|
1431
1594
|
...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
|
|
1432
|
-
isConnect:
|
|
1595
|
+
isConnect: false
|
|
1433
1596
|
});
|
|
1597
|
+
return;
|
|
1434
1598
|
}
|
|
1599
|
+
if (args.type === "add") subscribed.add(device.deviceId);
|
|
1600
|
+
else if (args.type === "remove") subscribed.delete(device.deviceId);
|
|
1435
1601
|
return;
|
|
1602
|
+
}
|
|
1436
1603
|
case "language":
|
|
1437
1604
|
reply("language", language());
|
|
1438
1605
|
return;
|
|
@@ -1443,7 +1610,7 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1443
1610
|
missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
|
|
1444
1611
|
missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
|
|
1445
1612
|
};
|
|
1446
|
-
reply("systemStatus",
|
|
1613
|
+
reply("systemStatus", statusForBundle, latency() + 400);
|
|
1447
1614
|
return;
|
|
1448
1615
|
}
|
|
1449
1616
|
case "jsError":
|
|
@@ -1467,11 +1634,157 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1467
1634
|
return;
|
|
1468
1635
|
}
|
|
1469
1636
|
};
|
|
1637
|
+
function emitConnection(deviceId) {
|
|
1638
|
+
if (!subscribed.has(deviceId)) return;
|
|
1639
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1640
|
+
if (!device) return;
|
|
1641
|
+
reply("deviceConnection", {
|
|
1642
|
+
serialNo: device.serialNo,
|
|
1643
|
+
...idOf(device),
|
|
1644
|
+
isConnect: connected.get(deviceId) === true
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
function syncStream(deviceId) {
|
|
1648
|
+
const want = realTimeOn.has(deviceId) && connected.get(deviceId) === true;
|
|
1649
|
+
const running2 = streams.get(deviceId);
|
|
1650
|
+
if (want && !running2) {
|
|
1651
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1652
|
+
if (!device) return;
|
|
1653
|
+
streams.set(
|
|
1654
|
+
deviceId,
|
|
1655
|
+
setInterval(() => {
|
|
1656
|
+
adapter.triggerEmit(
|
|
1657
|
+
"deviceLightSensors",
|
|
1658
|
+
lightSensorsReading(device, now(), "deviceId" in idOf(device))
|
|
1659
|
+
);
|
|
1660
|
+
}, 1e3)
|
|
1661
|
+
);
|
|
1662
|
+
} else if (!want && running2) {
|
|
1663
|
+
clearInterval(running2);
|
|
1664
|
+
streams.delete(deviceId);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function setConnected(deviceId, value) {
|
|
1668
|
+
if (!connected.has(deviceId)) {
|
|
1669
|
+
record2({ kind: "info", detail: `setConnected: no device ${deviceId} in this scenario` });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (connected.get(deviceId) === value) return;
|
|
1673
|
+
connected.set(deviceId, value);
|
|
1674
|
+
record2({
|
|
1675
|
+
kind: "info",
|
|
1676
|
+
detail: `device ${deviceId}: BLE session ${value ? "started" : "ended"}`
|
|
1677
|
+
});
|
|
1678
|
+
emitConnection(deviceId);
|
|
1679
|
+
syncStream(deviceId);
|
|
1680
|
+
const device = roster.find((d) => d.deviceId === deviceId);
|
|
1681
|
+
if (device) runSession(device, value);
|
|
1682
|
+
}
|
|
1683
|
+
function setSyncError(device, text, delay) {
|
|
1684
|
+
const timer = setTimeout(() => {
|
|
1685
|
+
timers.delete(timer);
|
|
1686
|
+
const byId2 = { ...status.deviceSyncErrorById ?? {} };
|
|
1687
|
+
const bySerial = { ...status.deviceSyncError };
|
|
1688
|
+
const shared = roster.filter((d) => d.serialNo === device.serialNo).length > 1;
|
|
1689
|
+
if (text === null) {
|
|
1690
|
+
if (!(device.deviceId in byId2)) return;
|
|
1691
|
+
delete byId2[device.deviceId];
|
|
1692
|
+
if (!shared) delete bySerial[device.serialNo];
|
|
1693
|
+
} else {
|
|
1694
|
+
byId2[device.deviceId] = text;
|
|
1695
|
+
if (!shared) bySerial[device.serialNo] = text;
|
|
1696
|
+
}
|
|
1697
|
+
status = { ...status, deviceSyncErrorById: byId2, deviceSyncError: bySerial };
|
|
1698
|
+
reply("systemStatus", statusForBundle, 0);
|
|
1699
|
+
}, delay);
|
|
1700
|
+
timers.add(timer);
|
|
1701
|
+
}
|
|
1702
|
+
function runSession(device, start) {
|
|
1703
|
+
const note = (message, messageCode, isError = false) => ({
|
|
1704
|
+
serialNo: device.serialNo,
|
|
1705
|
+
...idOf(device),
|
|
1706
|
+
isConnect: !isError,
|
|
1707
|
+
message,
|
|
1708
|
+
messageCode,
|
|
1709
|
+
isError
|
|
1710
|
+
});
|
|
1711
|
+
if (!start) {
|
|
1712
|
+
reply("deviceNotification", { ...note("Device disconnected", null), isConnect: false });
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const id = device.deviceId;
|
|
1716
|
+
const steps = [
|
|
1717
|
+
["[Start] Enable BLE periodic", "enable_ble_periodic_notification", "enable_ble_periodic"],
|
|
1718
|
+
["[Not needed] Enable BLE periodic", "enable_ble_periodic_not_needed_notification", ""],
|
|
1719
|
+
["[Start] Set BLE period", "set_ble_period_notification", "set_ble_period"],
|
|
1720
|
+
["[Not needed] Set BLE period", "set_ble_period_not_needed_notification", ""],
|
|
1721
|
+
["[Start] Set sync time", "set_sync_time_notification", "set_sync_time"],
|
|
1722
|
+
...pendingSync.has(id) ? [] : [["[Not needed] Set sync time", "set_sync_time_not_needed_notification", ""]],
|
|
1723
|
+
["[Start] Get data log", "get_data_log_notification", "get_data_log"],
|
|
1724
|
+
["[Start] Send data log", "send_data_log_notification", "send_data_log"],
|
|
1725
|
+
[
|
|
1726
|
+
"[Success] Set last data log sent to server",
|
|
1727
|
+
"set_last_data_log_sent_to_server_notification",
|
|
1728
|
+
""
|
|
1729
|
+
],
|
|
1730
|
+
["[Start] Get system log", "get_system_log_notification", "get_system_log"],
|
|
1731
|
+
["[Start] Send system log", "send_system_log_notification", "send_system_log"],
|
|
1732
|
+
["[Start] Get full charge log", "get_full_charge_log_notification", "get_full_charge_log"],
|
|
1733
|
+
["[Start] Send full charge log", "send_full_charge_log_notification", "send_full_charge_log"],
|
|
1734
|
+
["[Start] Set erase memory", "set_erase_memory_notification", "set_erase_memory"],
|
|
1735
|
+
...pendingErase.has(id) ? [["[Start] Erase memory", "erase_memory_notification", ""]] : [],
|
|
1736
|
+
["[Start] Get device telemetry", "get_device_telemetry_notification", "get_device_telemetry"],
|
|
1737
|
+
["[Start] Save device telemetry", "save_device_telemetry_notification", ""]
|
|
1738
|
+
];
|
|
1739
|
+
const failAt = scenario.sessionFailAt ?? (status.internet === false ? "send_data_log" : void 0);
|
|
1740
|
+
running.add(id);
|
|
1741
|
+
let delay = latency();
|
|
1742
|
+
reply("deviceNotification", note("Device connected", null), delay);
|
|
1743
|
+
for (const [message, code, stage] of steps) {
|
|
1744
|
+
delay += STEP_MS;
|
|
1745
|
+
reply("deviceNotification", note(message, code), delay);
|
|
1746
|
+
if (stage && failAt === stage) {
|
|
1747
|
+
delay += STEP_MS;
|
|
1748
|
+
const what = message.replace(/^\[\w+\] /, "");
|
|
1749
|
+
reply("deviceNotification", note(`[Error] ${what}`, `${stage}_error`, true), delay);
|
|
1750
|
+
setSyncError(device, `Sync failed (${stage}_error)`, delay + 20);
|
|
1751
|
+
endSession(id, delay + STEP_MS);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (stage === "set_sync_time") pendingSync.delete(id);
|
|
1755
|
+
}
|
|
1756
|
+
delay += STEP_MS;
|
|
1757
|
+
reply("deviceNotification", note("[Done] Pipeline", "pipeline_done_notification"), delay);
|
|
1758
|
+
setSyncError(device, null, delay + 20);
|
|
1759
|
+
pendingErase.delete(id);
|
|
1760
|
+
endSession(id, delay + STEP_MS);
|
|
1761
|
+
}
|
|
1762
|
+
function endSession(deviceId, delay) {
|
|
1763
|
+
const timer = setTimeout(() => {
|
|
1764
|
+
timers.delete(timer);
|
|
1765
|
+
running.delete(deviceId);
|
|
1766
|
+
if (!realTimeOn.has(deviceId)) setConnected(deviceId, false);
|
|
1767
|
+
}, delay);
|
|
1768
|
+
timers.add(timer);
|
|
1769
|
+
}
|
|
1470
1770
|
const unsubscribe = adapter.onCallHandler(handle);
|
|
1771
|
+
const first = roster[0];
|
|
1772
|
+
if (first && typeof scenario.firstSessionAfterMs === "number") {
|
|
1773
|
+
const timer = setTimeout(() => {
|
|
1774
|
+
timers.delete(timer);
|
|
1775
|
+
setConnected(first.deviceId, true);
|
|
1776
|
+
}, scenario.firstSessionAfterMs);
|
|
1777
|
+
timers.add(timer);
|
|
1778
|
+
}
|
|
1471
1779
|
if (scenario.serviceStartMs > 0) {
|
|
1472
1780
|
const timer = setTimeout(() => {
|
|
1473
1781
|
timers.delete(timer);
|
|
1474
1782
|
record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
|
|
1783
|
+
if (pendingUserDevice) {
|
|
1784
|
+
pendingUserDevice = false;
|
|
1785
|
+
record2({ kind: "info", detail: "the app re-sent the pending userDevice" });
|
|
1786
|
+
answerUserDevice();
|
|
1787
|
+
}
|
|
1475
1788
|
}, scenario.serviceStartMs);
|
|
1476
1789
|
timers.add(timer);
|
|
1477
1790
|
}
|
|
@@ -1487,8 +1800,15 @@ function createSimulatedNative(adapter, scenario, hooks = {}) {
|
|
|
1487
1800
|
}
|
|
1488
1801
|
reply("navigationBack", {}, 0);
|
|
1489
1802
|
},
|
|
1803
|
+
setStatus(patch) {
|
|
1804
|
+
status = { ...status, ...patch };
|
|
1805
|
+
reply("systemStatus", statusForBundle(), 0);
|
|
1806
|
+
},
|
|
1807
|
+
setConnected,
|
|
1490
1808
|
dispose() {
|
|
1491
1809
|
unsubscribe();
|
|
1810
|
+
for (const t of streams.values()) clearInterval(t);
|
|
1811
|
+
streams.clear();
|
|
1492
1812
|
for (const t of timers) clearTimeout(t);
|
|
1493
1813
|
timers.clear();
|
|
1494
1814
|
}
|
|
@@ -1548,15 +1868,18 @@ async function createDevHarness(opts) {
|
|
|
1548
1868
|
iframe.setAttribute("title", "Condor Canvas bundle");
|
|
1549
1869
|
iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
|
|
1550
1870
|
(opts.mount ?? document.body).appendChild(iframe);
|
|
1871
|
+
let confirmations = "ask";
|
|
1551
1872
|
const shell = new BridgeShell({
|
|
1552
1873
|
iframe,
|
|
1553
1874
|
policy: CANVAS_POLICY,
|
|
1554
|
-
onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
|
|
1875
|
+
onConfirmationRequest: (req) => confirmations === "ask" ? (opts.onConfirmationRequest ?? confirmInPage)(req) : confirmations === "accept"
|
|
1555
1876
|
});
|
|
1556
1877
|
if (opts.devPanel !== false) {
|
|
1557
1878
|
installDevPanel({ adapter, options: { startCollapsed: true } });
|
|
1558
1879
|
}
|
|
1559
|
-
|
|
1880
|
+
const deviceLike = "<script>try{delete Crypto.prototype.randomUUID}catch(e){}</script>";
|
|
1881
|
+
const html = /<head[^>]*>/i.test(opts.html) ? opts.html.replace(/<head[^>]*>/i, (m) => m + deviceLike) : deviceLike + opts.html;
|
|
1882
|
+
await shell.bootstrapClientIframeWithHtml(html);
|
|
1560
1883
|
try {
|
|
1561
1884
|
const result = await shell.verifySandbox();
|
|
1562
1885
|
if (!result.ok) {
|
|
@@ -1581,6 +1904,15 @@ async function createDevHarness(opts) {
|
|
|
1581
1904
|
emit(key, payload) {
|
|
1582
1905
|
Bridge.mockEmit(key, payload);
|
|
1583
1906
|
},
|
|
1907
|
+
setStatus(patch) {
|
|
1908
|
+
native.setStatus(patch);
|
|
1909
|
+
},
|
|
1910
|
+
setConnected(deviceId, value) {
|
|
1911
|
+
native.setConnected(deviceId, value);
|
|
1912
|
+
},
|
|
1913
|
+
setConfirmations(mode) {
|
|
1914
|
+
confirmations = mode;
|
|
1915
|
+
},
|
|
1584
1916
|
pressBack() {
|
|
1585
1917
|
native.pressBack();
|
|
1586
1918
|
},
|
|
@@ -1601,6 +1933,7 @@ export {
|
|
|
1601
1933
|
createDevHarness,
|
|
1602
1934
|
createSimulatedNative,
|
|
1603
1935
|
formatLocalTimestamp,
|
|
1936
|
+
lightSensorsReading,
|
|
1604
1937
|
parseLocalTimestamp,
|
|
1605
1938
|
resolveScenario,
|
|
1606
1939
|
sensorLogPage,
|