@smartspectra/node-sdk 3.5.0-rc.3 → 3.5.0-rc.5
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/NOTICE +2 -1
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.txt +3 -2
- package/docs/device-readiness.md +61 -0
- package/js/constants.js +4 -8
- package/js/{device-compatibility.js → device-readiness.js} +83 -24
- package/js/index.d.ts +25 -15
- package/js/main/index.d.ts +1 -1
- package/js/messages/generated.d.ts +16 -10
- package/js/messages/generated.js +33 -10
- package/js/renderer/index.d.ts +3 -3
- package/js/smartspectra.js +41 -34
- package/package.json +9 -9
- package/samples/{device_compatibility → device_readiness}/main.cjs +1 -4
- package/samples/{device_compatibility → device_readiness}/presentation.cjs +1 -2
- package/samples/{device_compatibility → device_readiness}/simple.cjs +3 -3
- package/docs/device-compatibility.md +0 -31
package/NOTICE
CHANGED
|
@@ -15,9 +15,10 @@ The following Apache-licensed components are included in this shipped artifact:
|
|
|
15
15
|
- HighwayHash
|
|
16
16
|
- KleidiAI
|
|
17
17
|
- Kompute
|
|
18
|
+
- LiteRT (TensorFlow Lite runtime)
|
|
18
19
|
- MediaPipe
|
|
19
20
|
- ml_dtypes
|
|
20
21
|
- OpenCV
|
|
21
22
|
- protoc-gen-validate
|
|
22
23
|
- ruy
|
|
23
|
-
- TensorFlow
|
|
24
|
+
- TensorFlow
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ no native addon, no `binding.gyp`, no `electron-rebuild`, no `node-gyp`.
|
|
|
14
14
|
|
|
15
15
|
- Electron sample: [electron-quickstart](https://github.com/Presage-Security/SmartSpectra/tree/main/nodejs/samples/electron-quickstart)
|
|
16
16
|
- API reference: <https://smartspectra.presagetech.com/docs/nodejs/api-reference>
|
|
17
|
-
- Device
|
|
17
|
+
- Device Readiness: [check readiness and handle results](docs/device-readiness.md)
|
|
18
18
|
- Metrics guide: <https://smartspectra.presagetech.com/docs/nodejs/metrics>
|
|
19
19
|
|
|
20
20
|
## Architecture
|
package/THIRD_PARTY_NOTICES.txt
CHANGED
|
@@ -19,12 +19,13 @@ Apache-2.0
|
|
|
19
19
|
- HighwayHash
|
|
20
20
|
- KleidiAI
|
|
21
21
|
- Kompute
|
|
22
|
+
- LiteRT (TensorFlow Lite runtime)
|
|
22
23
|
- MediaPipe
|
|
23
24
|
- ml_dtypes
|
|
24
25
|
- OpenCV
|
|
25
26
|
- protoc-gen-validate
|
|
26
27
|
- ruy
|
|
27
|
-
- TensorFlow
|
|
28
|
+
- TensorFlow
|
|
28
29
|
|
|
29
30
|
BSD-2-Clause
|
|
30
31
|
- cpuinfo
|
|
@@ -294,7 +295,7 @@ Full license texts included in this file
|
|
|
294
295
|
See the License for the specific language governing permissions and
|
|
295
296
|
limitations under the License.
|
|
296
297
|
|
|
297
|
-
Applies to: Abseil, CNCF xDS API, Envoy data-plane-api, FlatBuffers, gemmlowp, Google APIs, gRPC, HighwayHash, KleidiAI, Kompute, MediaPipe, ml_dtypes, OpenCV, protoc-gen-validate, ruy, TensorFlow
|
|
298
|
+
Applies to: Abseil, CNCF xDS API, Envoy data-plane-api, FlatBuffers, gemmlowp, Google APIs, gRPC, HighwayHash, KleidiAI, Kompute, LiteRT (TensorFlow Lite runtime), MediaPipe, ml_dtypes, OpenCV, protoc-gen-validate, ruy, TensorFlow
|
|
298
299
|
|
|
299
300
|
===== BSD-2-Clause =====
|
|
300
301
|
Copyright (c) <year> <owner>. All rights reserved.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Node.js Device Readiness
|
|
3
|
+
description: Check device readiness for the configured metrics and camera, and handle the result.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Node.js Device Readiness
|
|
7
|
+
|
|
8
|
+
> **Important:** SDK metrics are offered for general wellness and informational purposes only. SDK metrics have not been cleared by the FDA and may not be used for medical diagnosis or treatment.
|
|
9
|
+
|
|
10
|
+
Configure the SDK's authentication, metrics, and camera, then check readiness
|
|
11
|
+
before starting a measurement:
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const result = await sdk.checkDeviceReadiness();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `result.verdict` for application decisions and `result.reason` for the
|
|
18
|
+
explanation and recommended next step. Keep the returned result if you need it later.
|
|
19
|
+
|
|
20
|
+
Compare the verdict explicitly, for example `result.verdict === 'ready'`.
|
|
21
|
+
Every verdict string is truthy, including `'blocked'`.
|
|
22
|
+
|
|
23
|
+
See [Device Readiness](../../docs/device-readiness.md) for
|
|
24
|
+
verdict meanings, supported inputs, and result handling.
|
|
25
|
+
|
|
26
|
+
## Telemetry
|
|
27
|
+
|
|
28
|
+
Telemetry is on by default. When it is enabled, each check sends its full
|
|
29
|
+
diagnostic report to Presage: device and SDK details, requested metrics and camera
|
|
30
|
+
settings, performance measurements, and free-text diagnostic messages. The report
|
|
31
|
+
never includes video frames or vital signs.
|
|
32
|
+
|
|
33
|
+
To turn it off, pass `enableTelemetry: false` when creating the SDK:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
const sdk = new SmartSpectraSDK({
|
|
37
|
+
// ...authentication and metrics...
|
|
38
|
+
enableTelemetry: false,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See [SDK Telemetry & Privacy](../../docs/telemetry-and-privacy.md) for the full
|
|
43
|
+
report contents, storage, and retention.
|
|
44
|
+
|
|
45
|
+
## When a measurement stops for low frame rate
|
|
46
|
+
|
|
47
|
+
When the frame rate stays too low to measure reliably during a measurement, the
|
|
48
|
+
session ends and the `'error'` event receives
|
|
49
|
+
`SmartSpectraErrorCode.kSustainedLowFrameRate` (14) instead of
|
|
50
|
+
`SmartSpectraErrorCode.kProcessingFailed`. The message is "SmartSpectra stopped
|
|
51
|
+
because the frame rate stayed too low to measure reliably." A frame submitted as
|
|
52
|
+
the session ends is rejected with the same code. `retryable` is `false` when the
|
|
53
|
+
SDK plays a video file, because the same file stops the same way; it is `true` for
|
|
54
|
+
cameras and custom input:
|
|
55
|
+
the host may offer a user-initiated fresh start; do not restart automatically.
|
|
56
|
+
To check whether a device can sustain measurement before starting, call
|
|
57
|
+
`checkDeviceReadiness()`.
|
|
58
|
+
|
|
59
|
+
The SDK measures frame rate from frame timestamps: the timestamp file when one is
|
|
60
|
+
given, otherwise the video's playback position. The interframe delay
|
|
61
|
+
(`interframeDelayMs`) changes playback speed, not the measured frame rate.
|
package/js/constants.js
CHANGED
|
@@ -81,6 +81,7 @@ const SmartSpectraErrorCode = Object.freeze({
|
|
|
81
81
|
kNonMonotonicTimestamp: 10,
|
|
82
82
|
kTimestampGap: 11,
|
|
83
83
|
kMetricNotAuthorized: 13,
|
|
84
|
+
kSustainedLowFrameRate: 14,
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
// Spatial transform applied to each pushed frame before it enters the
|
|
@@ -102,16 +103,11 @@ const MetricType = Object.freeze(Object.fromEntries(
|
|
|
102
103
|
|
|
103
104
|
// MetricType integer codes grouped by measurement type. Spread into
|
|
104
105
|
// `requestedMetrics`; omitting `requestedMetrics` is equivalent to passing
|
|
105
|
-
// `breathingMetrics
|
|
106
|
+
// `breathingMetrics` (chest breathing, abdomen breathing, breathing rate).
|
|
106
107
|
const breathingMetrics = Object.freeze([
|
|
107
108
|
0, // CHEST_BREATHING
|
|
108
109
|
1, // ABDOMEN_BREATHING
|
|
109
110
|
2, // BREATHING_RATE
|
|
110
|
-
3, // BREATHING_AMPLITUDE
|
|
111
|
-
4, // APNEA
|
|
112
|
-
5, // RESPIRATORY_LINE_LENGTH
|
|
113
|
-
6, // BASELINE
|
|
114
|
-
7, // INHALE_EXHALE_RATIO
|
|
115
111
|
]);
|
|
116
112
|
|
|
117
113
|
const cardioMetrics = Object.freeze([
|
|
@@ -136,8 +132,8 @@ const edaMetrics = Object.freeze([
|
|
|
136
132
|
10, // EDA_TRACE
|
|
137
133
|
]);
|
|
138
134
|
|
|
139
|
-
// The default metric bundle used when `requestedMetrics` is omitted;
|
|
140
|
-
//
|
|
135
|
+
// The default metric bundle used when `requestedMetrics` is omitted; equal to
|
|
136
|
+
// `breathingMetrics`.
|
|
141
137
|
const defaultSupportedMetrics = breathingMetrics;
|
|
142
138
|
|
|
143
139
|
module.exports = {
|
|
@@ -6,7 +6,7 @@ const { readinessLibrary, ensurePreconfigured } = require('./ffi');
|
|
|
6
6
|
let native;
|
|
7
7
|
let typesRegistered = false;
|
|
8
8
|
function unsupported() {
|
|
9
|
-
const error = new Error('This native SDK does not support device readiness ABI
|
|
9
|
+
const error = new Error('This native SDK does not support device readiness ABI v9.');
|
|
10
10
|
error.code = 'unsupported_capability';
|
|
11
11
|
return error;
|
|
12
12
|
}
|
|
@@ -14,7 +14,7 @@ function getNative() {
|
|
|
14
14
|
if (native) return native;
|
|
15
15
|
const lib = readinessLibrary();
|
|
16
16
|
try {
|
|
17
|
-
if (lib.func('int smartspectra_readiness_abi_version(void)')() !==
|
|
17
|
+
if (lib.func('int smartspectra_readiness_abi_version(void)')() !== 9) throw unsupported();
|
|
18
18
|
} catch { throw unsupported(); }
|
|
19
19
|
if (!typesRegistered) {
|
|
20
20
|
koffi.struct('smartspectra_readiness_requirement_t', {
|
|
@@ -79,6 +79,7 @@ function getNative() {
|
|
|
79
79
|
create: lib.func('void *smartspectra_readiness_create(void)'),
|
|
80
80
|
reserve: lib.func('int smartspectra_readiness_reserve(void *service)'),
|
|
81
81
|
destroy: lib.func('void smartspectra_readiness_destroy(void *service)'),
|
|
82
|
+
finish: lib.func('int smartspectra_readiness_finish_check(void *service)'),
|
|
82
83
|
start: lib.func('void *smartspectra_readiness_start(void *service, const smartspectra_config_t *config, const smartspectra_readiness_request_t *request)'),
|
|
83
84
|
cancel: lib.func('void smartspectra_readiness_cancel(void *check)'),
|
|
84
85
|
background: lib.func('void smartspectra_readiness_background(void *check)'),
|
|
@@ -94,7 +95,8 @@ const reasons = ['qualified', 'performance_below_policy', 'invalid_configuration
|
|
|
94
95
|
'run_incomplete', 'insufficient_evidence', 'policy_unavailable', 'backend_unknown',
|
|
95
96
|
'configuration_changed', 'cancelled', 'deadline_exceeded', 'application_backgrounded',
|
|
96
97
|
'busy', 'camera_interrupted', 'permission_required', 'permission_denied',
|
|
97
|
-
'borderline_performance', 'experimental_opt_in_required', 'unsupported_capability'
|
|
98
|
+
'borderline_performance', 'experimental_opt_in_required', 'unsupported_capability',
|
|
99
|
+
'network_unavailable'];
|
|
98
100
|
const actions = ['proceed', 'retry', 'authenticate', 'obtain_authorization', 'correct_configuration',
|
|
99
101
|
'supply_policy', 'reduce_workload', 'collect_evidence', 'inspect_backend', 'release_session',
|
|
100
102
|
'restore_input', 'grant_permission', 'return_to_foreground', 'resolve_setup',
|
|
@@ -220,7 +222,7 @@ function progressFromNative(n, handle) {
|
|
|
220
222
|
if (n.progress(handle, progress) !== 0) throw new Error('Unable to read native readiness progress.');
|
|
221
223
|
return freeze({ ...fields(progress), phase: phases[progress.phase] });
|
|
222
224
|
}
|
|
223
|
-
class
|
|
225
|
+
class DeviceReadinessCheck {
|
|
224
226
|
#handle; #native; #progress; #value; #promise;
|
|
225
227
|
constructor(n, handle, signal, finished) {
|
|
226
228
|
this.#native = n;
|
|
@@ -272,38 +274,61 @@ class DeviceCompatibilityCheck {
|
|
|
272
274
|
cancel() { if (this.#handle) this.#native.cancel(this.#handle); }
|
|
273
275
|
applicationBackgrounded() { if (this.#handle) this.#native.background(this.#handle); }
|
|
274
276
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
// Native threads do not keep Node's event loop alive. Drain surviving owners
|
|
278
|
+
// before normal process exit, while native singleton dependencies still exist.
|
|
279
|
+
// This does not promise delivery after process.exit(), SIGKILL, or a crash.
|
|
280
|
+
const liveServices = new Set();
|
|
281
|
+
function destroyNative(state) {
|
|
282
|
+
if (state.closing) return state.closing;
|
|
283
|
+
state.closing = new Promise((resolve, reject) => {
|
|
284
|
+
state.n.destroy.async(state.handle, error => {
|
|
285
|
+
if (error) reject(error);
|
|
286
|
+
else { state.destroyed = true; resolve(); }
|
|
287
|
+
});
|
|
288
|
+
}).finally(() => liveServices.delete(state));
|
|
289
|
+
return state.closing;
|
|
290
|
+
}
|
|
291
|
+
let exitDrainInstalled = false;
|
|
292
|
+
function installExitDrain() {
|
|
293
|
+
if (exitDrainInstalled) return;
|
|
294
|
+
exitDrainInstalled = true;
|
|
295
|
+
process.on('beforeExit', () => {
|
|
296
|
+
for (const state of liveServices) void destroyNative(state).catch(() => {});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const services = new FinalizationRegistry(state => { void destroyNative(state).catch(() => {}); });
|
|
300
|
+
class DeviceReadinessService {
|
|
280
301
|
static async run(configuration = {}) {
|
|
281
|
-
const service = new
|
|
302
|
+
const service = new DeviceReadinessService();
|
|
282
303
|
try { return await service.check(configuration).result; }
|
|
283
304
|
finally { await service.close(); }
|
|
284
305
|
}
|
|
285
|
-
#native; #handle; #checks = new Set(); #closing;
|
|
306
|
+
#native; #handle; #checks = new Set(); #closing; #lifetime; #finishing;
|
|
286
307
|
constructor() {
|
|
287
308
|
this.#native = getNative();
|
|
288
309
|
ensurePreconfigured();
|
|
289
310
|
this.#handle = this.#native.create();
|
|
290
311
|
if (!this.#handle) throw new Error('Unable to create native readiness service.');
|
|
291
|
-
|
|
312
|
+
this.#lifetime = { n: this.#native, handle: this.#handle };
|
|
313
|
+
liveServices.add(this.#lifetime);
|
|
314
|
+
installExitDrain();
|
|
315
|
+
services.register(this, this.#lifetime, this);
|
|
292
316
|
}
|
|
317
|
+
get closed() { return !!this.#lifetime.closing; }
|
|
293
318
|
reserve() {
|
|
294
|
-
if (!this.#handle || this.#closing) throw new Error('
|
|
319
|
+
if (!this.#handle || this.#closing || this.closed || this.#finishing) throw new Error('Readiness service is closed.');
|
|
295
320
|
const result = this.#native.reserve(this.#handle);
|
|
296
321
|
if (result < 0) throw new Error('Unable to reserve SDK state.');
|
|
297
322
|
return result === 1;
|
|
298
323
|
}
|
|
299
324
|
check(configuration, referenceBgrFrame = undefined, workload = 'full', scope = 'camera_paced_reference_frame') {
|
|
300
|
-
const policy =
|
|
325
|
+
const policy = DeviceReadinessPolicies.reference720p30(workload, scope);
|
|
301
326
|
return this.start({ ...configuration, requestedMetrics: policy.metrics.map(m => m.metricId) }, {
|
|
302
327
|
referenceBgrFrame, policy, scope, authenticationPrepared: !!configuration?.apiKey,
|
|
303
328
|
});
|
|
304
329
|
}
|
|
305
330
|
start(configuration = {}, request = {}) {
|
|
306
|
-
if (!this.#handle || this.#closing) throw new Error('Readiness service is closed.');
|
|
331
|
+
if (!this.#handle || this.#closing || this.closed || this.#finishing) throw new Error('Readiness service is closed.');
|
|
307
332
|
const signal = request.signal;
|
|
308
333
|
if (signal !== undefined && !(signal instanceof AbortSignal))
|
|
309
334
|
throw new TypeError('signal must be an AbortSignal.');
|
|
@@ -311,12 +336,12 @@ class DeviceCompatibilityService {
|
|
|
311
336
|
const config = configurationToNative(configuration);
|
|
312
337
|
const input = requestToNative(request);
|
|
313
338
|
// Configuration/request getters can call close() during conversion.
|
|
314
|
-
if (!this.#handle || this.#closing) throw new Error('Readiness service is closed.');
|
|
339
|
+
if (!this.#handle || this.#closing || this.closed || this.#finishing) throw new Error('Readiness service is closed.');
|
|
315
340
|
const handle = this.#native.start(this.#handle, config, input);
|
|
316
341
|
if (!handle) throw new Error('Unable to submit native readiness request.');
|
|
317
342
|
let check;
|
|
318
343
|
try {
|
|
319
|
-
check = new
|
|
344
|
+
check = new DeviceReadinessCheck(this.#native, handle, signal, value => this.#checks.delete(value));
|
|
320
345
|
} catch (error) {
|
|
321
346
|
try { this.#native.cancel(handle); }
|
|
322
347
|
finally { this.#native.free(handle); }
|
|
@@ -325,20 +350,54 @@ class DeviceCompatibilityService {
|
|
|
325
350
|
this.#checks.add(check);
|
|
326
351
|
return check;
|
|
327
352
|
}
|
|
353
|
+
finishCheck() {
|
|
354
|
+
if (this.closed) return this.#lifetime.closing;
|
|
355
|
+
if (this.#closing) return this.#closing;
|
|
356
|
+
if (this.#finishing) return this.#finishing;
|
|
357
|
+
const checks = [...this.#checks];
|
|
358
|
+
checks.forEach(check => check.cancel());
|
|
359
|
+
this.#finishing = Promise.allSettled(checks.map(check => check.result)).then(() => new Promise((resolve, reject) => {
|
|
360
|
+
this.#native.finish.async(this.#handle, (error, status) => {
|
|
361
|
+
if (error) reject(error);
|
|
362
|
+
else if (status !== 0) reject(new Error('Unable to finish readiness check.'));
|
|
363
|
+
else resolve();
|
|
364
|
+
});
|
|
365
|
+
})).finally(() => { this.#finishing = null; });
|
|
366
|
+
return this.#finishing;
|
|
367
|
+
}
|
|
368
|
+
// SDK reset is an existing synchronous teardown API. Do not overlap an
|
|
369
|
+
// asynchronous finish/close; ordinary check returns have already joined it.
|
|
370
|
+
closeSync() {
|
|
371
|
+
// beforeExit may have finished draining this owner before another
|
|
372
|
+
// listener resumes application work. Its native handle is already gone.
|
|
373
|
+
if (this.#lifetime.destroyed) {
|
|
374
|
+
this.#handle = null;
|
|
375
|
+
services.unregister(this);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (this.#closing || this.#finishing || this.closed)
|
|
379
|
+
throw new Error('Readiness teardown is already in progress; await it before reset.');
|
|
380
|
+
this.#native.destroy(this.#handle);
|
|
381
|
+
this.#lifetime.destroyed = true;
|
|
382
|
+
this.#handle = null;
|
|
383
|
+
this.#lifetime.closing = Promise.resolve();
|
|
384
|
+
liveServices.delete(this.#lifetime);
|
|
385
|
+
services.unregister(this);
|
|
386
|
+
}
|
|
328
387
|
close() {
|
|
388
|
+
if (this.closed) return this.#lifetime.closing;
|
|
329
389
|
if (this.#closing) return this.#closing;
|
|
330
390
|
const checks = [...this.#checks];
|
|
331
391
|
checks.forEach(check => check.cancel());
|
|
332
|
-
this.#closing = Promise.allSettled(checks.map(check => check.result)).then(() =>
|
|
333
|
-
const handle = this.#handle;
|
|
392
|
+
this.#closing = Promise.allSettled([...checks.map(check => check.result), this.#finishing]).then(() => {
|
|
334
393
|
this.#handle = null;
|
|
335
394
|
services.unregister(this);
|
|
336
|
-
this.#
|
|
337
|
-
})
|
|
395
|
+
return destroyNative(this.#lifetime);
|
|
396
|
+
});
|
|
338
397
|
return this.#closing;
|
|
339
398
|
}
|
|
340
399
|
}
|
|
341
|
-
const
|
|
400
|
+
const DeviceReadinessPolicies = Object.freeze({
|
|
342
401
|
reference720p30(workload = 'full', scope = 'camera_paced_reference_frame') {
|
|
343
402
|
const tier = choice(workload, 'full', ['breathing_pulse_eda', 'breathing_pulse', 'breathing', 'full'], 'workload');
|
|
344
403
|
const policy = {};
|
|
@@ -360,8 +419,8 @@ const DeviceCompatibilityPolicies = Object.freeze({
|
|
|
360
419
|
return freeze(policyFromNative(policy));
|
|
361
420
|
},
|
|
362
421
|
});
|
|
363
|
-
function
|
|
364
|
-
module.exports = {
|
|
422
|
+
function deviceReadinessAvailable() { try { getNative(); return true; } catch { return false; } }
|
|
423
|
+
module.exports = { DeviceReadinessService, DeviceReadinessPolicies, deviceReadinessAvailable, busyResult };
|
|
365
424
|
|
|
366
425
|
// A rejected busy check has no measurement evidence. Keep the full result shape.
|
|
367
426
|
function busyResult() {
|
package/js/index.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export declare const SmartSpectraErrorCode: {
|
|
|
78
78
|
readonly kTimestampGap: 11;
|
|
79
79
|
/** Requested metric is not authorized for this API key. */
|
|
80
80
|
readonly kMetricNotAuthorized: 13;
|
|
81
|
+
/** The frame rate stayed too low for a reliable measurement, so the session ended. */
|
|
82
|
+
readonly kSustainedLowFrameRate: 14;
|
|
81
83
|
};
|
|
82
84
|
export type SmartSpectraErrorCodeValue = typeof SmartSpectraErrorCode[keyof typeof SmartSpectraErrorCode];
|
|
83
85
|
|
|
@@ -123,7 +125,11 @@ export interface SmartSpectraOptions {
|
|
|
123
125
|
apiKey?: string;
|
|
124
126
|
|
|
125
127
|
/**
|
|
126
|
-
* MetricType integer codes. Defaults to `breathingMetrics`
|
|
128
|
+
* MetricType integer codes. Defaults to `breathingMetrics` (`MetricType.CHEST_BREATHING`,
|
|
129
|
+
* `MetricType.ABDOMEN_BREATHING`, `MetricType.BREATHING_RATE`) when omitted. Requesting an
|
|
130
|
+
* Unavailable metric (`MetricType.BREATHING_AMPLITUDE`, `MetricType.APNEA`,
|
|
131
|
+
* `MetricType.RESPIRATORY_LINE_LENGTH`, `MetricType.BASELINE`,
|
|
132
|
+
* `MetricType.INHALE_EXHALE_RATIO`) makes `start()` throw `kConfigurationFailed`.
|
|
127
133
|
*/
|
|
128
134
|
requestedMetrics?: number[];
|
|
129
135
|
|
|
@@ -136,7 +142,10 @@ export interface SmartSpectraOptions {
|
|
|
136
142
|
*/
|
|
137
143
|
logLevel?: SmartSpectraLogLevelValue;
|
|
138
144
|
|
|
139
|
-
/**
|
|
145
|
+
/** Session summaries and full readiness reports. Defaults to `true`; set `false`
|
|
146
|
+
* to opt out. Reports can contain free text, paths and platform context, and
|
|
147
|
+
* authenticated requests can correlate them with an account/device. No raw frames
|
|
148
|
+
* or measured vitals are sent. See SDK Telemetry & Privacy. */
|
|
140
149
|
enableTelemetry?: boolean;
|
|
141
150
|
}
|
|
142
151
|
|
|
@@ -144,7 +153,12 @@ export interface SmartSpectraOptions {
|
|
|
144
153
|
export interface VideoFileOptions {
|
|
145
154
|
/** Path to a per-frame timestamps file (one timestamp per line). */
|
|
146
155
|
timestampsPath?: string | null;
|
|
147
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Throttle between frames in ms; omit/0 = as fast as possible. The SDK measures
|
|
158
|
+
* frame rate from frame timestamps: the timestamp file when one is given, otherwise
|
|
159
|
+
* the video's playback position. The interframe delay changes playback speed, not
|
|
160
|
+
* the measured frame rate.
|
|
161
|
+
*/
|
|
148
162
|
interframeDelayMs?: number;
|
|
149
163
|
/** Seek this far into the file before playback (ms); omit/0 = start. */
|
|
150
164
|
startOffsetMs?: number;
|
|
@@ -214,10 +228,8 @@ export interface CameraOptions {
|
|
|
214
228
|
* ```
|
|
215
229
|
*/
|
|
216
230
|
export declare class SmartSpectraSDK {
|
|
217
|
-
/** Device
|
|
218
|
-
|
|
219
|
-
/** Latest completed result; undefined before/during a check or after an error. */
|
|
220
|
-
getDeviceCompatibilityResult(): DeviceCompatibilityResult | undefined;
|
|
231
|
+
/** Device readiness uses this SDK's authentication. */
|
|
232
|
+
checkDeviceReadiness(): Promise<DeviceReadinessResult>;
|
|
221
233
|
/** SDK package version. */
|
|
222
234
|
static readonly version: string;
|
|
223
235
|
|
|
@@ -350,14 +362,14 @@ export declare class SmartSpectraSDK {
|
|
|
350
362
|
/**
|
|
351
363
|
* MetricType integer codes grouped by measurement type. Spread into
|
|
352
364
|
* `requestedMetrics`; omitting it is equivalent to passing
|
|
353
|
-
* `breathingMetrics
|
|
365
|
+
* `breathingMetrics` (chest breathing, abdomen breathing, breathing rate).
|
|
354
366
|
*/
|
|
355
367
|
export declare const breathingMetrics: readonly number[];
|
|
356
368
|
export declare const cardioMetrics: readonly number[];
|
|
357
369
|
export declare const faceMetrics: readonly number[];
|
|
358
370
|
export declare const micromotionMetrics: readonly number[];
|
|
359
371
|
export declare const edaMetrics: readonly number[];
|
|
360
|
-
/** Default metric bundle used when `requestedMetrics` is omitted
|
|
372
|
+
/** Default metric bundle used when `requestedMetrics` is omitted; equal to `breathingMetrics`. */
|
|
361
373
|
export declare const defaultSupportedMetrics: readonly number[];
|
|
362
374
|
|
|
363
375
|
/**
|
|
@@ -374,12 +386,10 @@ export declare function setMetricsClass(cls: unknown): void;
|
|
|
374
386
|
*/
|
|
375
387
|
export declare function decodeMetrics(buf: Buffer): unknown;
|
|
376
388
|
|
|
377
|
-
/** Device
|
|
389
|
+
/** Device readiness. Pin the JS and native SDK versions together. */
|
|
378
390
|
/** Outcome for the configured SDK workload. */
|
|
379
|
-
export type
|
|
380
|
-
export interface
|
|
381
|
-
readonly verdict:
|
|
391
|
+
export type DeviceReadinessVerdict = 'ready' | 'limited' | 'blocked' | 'inconclusive';
|
|
392
|
+
export interface DeviceReadinessResult {
|
|
393
|
+
readonly verdict: DeviceReadinessVerdict;
|
|
382
394
|
readonly reason: string;
|
|
383
|
-
/** Native JSON support report; detailed fields are not a stable typed API. */
|
|
384
|
-
readonly diagnostics: string;
|
|
385
395
|
}
|
package/js/main/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { BrowserWindow } from 'electron';
|
|
7
7
|
|
|
8
|
-
export { SmartSpectraSDK, ProcessingStatus } from '
|
|
8
|
+
export { SmartSpectraSDK, ProcessingStatus } from '../index';
|
|
9
9
|
|
|
10
10
|
/** Options for `bindSmartSpectraIpc`. */
|
|
11
11
|
export interface BindSmartSpectraIpcOptions {
|
|
@@ -725,6 +725,9 @@ export namespace presage {
|
|
|
725
725
|
|
|
726
726
|
/** End of the completed measurement window, in microseconds on the session input timeline */
|
|
727
727
|
timestamp?: (number|Long|null);
|
|
728
|
+
|
|
729
|
+
/** True when the service returned a valid strict value; false means retry the measurement. */
|
|
730
|
+
stable?: (boolean|null);
|
|
728
731
|
}
|
|
729
732
|
|
|
730
733
|
/**
|
|
@@ -745,6 +748,9 @@ export namespace presage {
|
|
|
745
748
|
/** End of the completed measurement window, in microseconds on the session input timeline */
|
|
746
749
|
public timestamp: (number|Long);
|
|
747
750
|
|
|
751
|
+
/** True when the service returned a valid strict value; false means retry the measurement. */
|
|
752
|
+
public stable: boolean;
|
|
753
|
+
|
|
748
754
|
/**
|
|
749
755
|
* Creates a new Strict instance using the specified properties.
|
|
750
756
|
* @param [properties] Properties to set
|
|
@@ -973,19 +979,19 @@ export namespace presage {
|
|
|
973
979
|
*/
|
|
974
980
|
lowerTrace?: (presage.smartspectra.IMeasurement[]|null);
|
|
975
981
|
|
|
976
|
-
/**
|
|
982
|
+
/** Unavailable: breathing amplitude. This field stays empty; requesting BREATHING_AMPLITUDE makes start fail with ConfigurationFailed. */
|
|
977
983
|
amplitude?: (presage.smartspectra.IMeasurement[]|null);
|
|
978
984
|
|
|
979
|
-
/**
|
|
985
|
+
/** Unavailable: apnea (breathing cessation) detection. This field stays empty; requesting APNEA makes start fail with ConfigurationFailed. */
|
|
980
986
|
apnea?: (presage.smartspectra.IDetectionStatus[]|null);
|
|
981
987
|
|
|
982
|
-
/**
|
|
988
|
+
/** Unavailable: respiratory line length. This field stays empty; requesting RESPIRATORY_LINE_LENGTH makes start fail with ConfigurationFailed. */
|
|
983
989
|
respiratoryLineLength?: (presage.smartspectra.IMeasurement[]|null);
|
|
984
990
|
|
|
985
|
-
/**
|
|
991
|
+
/** Unavailable: breathing baseline. This field stays empty; requesting BASELINE makes start fail with ConfigurationFailed. */
|
|
986
992
|
baseline?: (presage.smartspectra.IMeasurement[]|null);
|
|
987
993
|
|
|
988
|
-
/**
|
|
994
|
+
/** Unavailable: inhale/exhale duration ratio. This field stays empty; requesting INHALE_EXHALE_RATIO makes start fail with ConfigurationFailed. */
|
|
989
995
|
inhaleExhaleRatio?: (presage.smartspectra.IMeasurement[]|null);
|
|
990
996
|
|
|
991
997
|
/**
|
|
@@ -1031,19 +1037,19 @@ export namespace presage {
|
|
|
1031
1037
|
*/
|
|
1032
1038
|
public lowerTrace: presage.smartspectra.IMeasurement[];
|
|
1033
1039
|
|
|
1034
|
-
/**
|
|
1040
|
+
/** Unavailable: breathing amplitude. This field stays empty; requesting BREATHING_AMPLITUDE makes start fail with ConfigurationFailed. */
|
|
1035
1041
|
public amplitude: presage.smartspectra.IMeasurement[];
|
|
1036
1042
|
|
|
1037
|
-
/**
|
|
1043
|
+
/** Unavailable: apnea (breathing cessation) detection. This field stays empty; requesting APNEA makes start fail with ConfigurationFailed. */
|
|
1038
1044
|
public apnea: presage.smartspectra.IDetectionStatus[];
|
|
1039
1045
|
|
|
1040
|
-
/**
|
|
1046
|
+
/** Unavailable: respiratory line length. This field stays empty; requesting RESPIRATORY_LINE_LENGTH makes start fail with ConfigurationFailed. */
|
|
1041
1047
|
public respiratoryLineLength: presage.smartspectra.IMeasurement[];
|
|
1042
1048
|
|
|
1043
|
-
/**
|
|
1049
|
+
/** Unavailable: breathing baseline. This field stays empty; requesting BASELINE makes start fail with ConfigurationFailed. */
|
|
1044
1050
|
public baseline: presage.smartspectra.IMeasurement[];
|
|
1045
1051
|
|
|
1046
|
-
/**
|
|
1052
|
+
/** Unavailable: inhale/exhale duration ratio. This field stays empty; requesting INHALE_EXHALE_RATIO makes start fail with ConfigurationFailed. */
|
|
1047
1053
|
public inhaleExhaleRatio: presage.smartspectra.IMeasurement[];
|
|
1048
1054
|
|
|
1049
1055
|
/**
|
package/js/messages/generated.js
CHANGED
|
@@ -2053,6 +2053,7 @@ $root.presage = (function() {
|
|
|
2053
2053
|
* @interface IStrict
|
|
2054
2054
|
* @property {number|null} [value] The strict value requiring high precision
|
|
2055
2055
|
* @property {number|Long|null} [timestamp] End of the completed measurement window, in microseconds on the session input timeline
|
|
2056
|
+
* @property {boolean|null} [stable] True when the service returned a valid strict value; false means retry the measurement.
|
|
2056
2057
|
*/
|
|
2057
2058
|
|
|
2058
2059
|
/**
|
|
@@ -2087,6 +2088,14 @@ $root.presage = (function() {
|
|
|
2087
2088
|
*/
|
|
2088
2089
|
Strict.prototype.timestamp = $util.Long ? $util.Long.fromBits(0,0,false) : 0;
|
|
2089
2090
|
|
|
2091
|
+
/**
|
|
2092
|
+
* True when the service returned a valid strict value; false means retry the measurement.
|
|
2093
|
+
* @member {boolean} stable
|
|
2094
|
+
* @memberof presage.smartspectra.Strict
|
|
2095
|
+
* @instance
|
|
2096
|
+
*/
|
|
2097
|
+
Strict.prototype.stable = false;
|
|
2098
|
+
|
|
2090
2099
|
/**
|
|
2091
2100
|
* Creates a new Strict instance using the specified properties.
|
|
2092
2101
|
* @function create
|
|
@@ -2119,6 +2128,8 @@ $root.presage = (function() {
|
|
|
2119
2128
|
writer.uint32(/* id 1, wireType 5 =*/13).float(message.value);
|
|
2120
2129
|
if (message.timestamp != null && Object.hasOwnProperty.call(message, "timestamp"))
|
|
2121
2130
|
writer.uint32(/* id 2, wireType 0 =*/16).int64(message.timestamp);
|
|
2131
|
+
if (message.stable != null && Object.hasOwnProperty.call(message, "stable"))
|
|
2132
|
+
writer.uint32(/* id 3, wireType 0 =*/24).bool(message.stable);
|
|
2122
2133
|
return writer;
|
|
2123
2134
|
};
|
|
2124
2135
|
|
|
@@ -2177,6 +2188,10 @@ $root.presage = (function() {
|
|
|
2177
2188
|
message.timestamp = reader.int64();
|
|
2178
2189
|
break;
|
|
2179
2190
|
}
|
|
2191
|
+
case 3: {
|
|
2192
|
+
message.stable = reader.bool();
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2180
2195
|
default:
|
|
2181
2196
|
reader.skipType(tag & 7, long);
|
|
2182
2197
|
break;
|
|
@@ -2227,6 +2242,9 @@ $root.presage = (function() {
|
|
|
2227
2242
|
if (message.timestamp != null && Object.hasOwnProperty.call(message, "timestamp"))
|
|
2228
2243
|
if (!$util.isInteger(message.timestamp) && !(message.timestamp && $util.isInteger(message.timestamp.low) && $util.isInteger(message.timestamp.high)))
|
|
2229
2244
|
return "timestamp: integer|Long expected";
|
|
2245
|
+
if (message.stable != null && Object.hasOwnProperty.call(message, "stable"))
|
|
2246
|
+
if (typeof message.stable !== "boolean")
|
|
2247
|
+
return "stable: boolean expected";
|
|
2230
2248
|
return null;
|
|
2231
2249
|
};
|
|
2232
2250
|
|
|
@@ -2259,6 +2277,8 @@ $root.presage = (function() {
|
|
|
2259
2277
|
message.timestamp = object.timestamp;
|
|
2260
2278
|
else if (typeof object.timestamp === "object")
|
|
2261
2279
|
message.timestamp = new $util.LongBits(object.timestamp.low >>> 0, object.timestamp.high >>> 0).toNumber();
|
|
2280
|
+
if (object.stable != null)
|
|
2281
|
+
message.stable = Boolean(object.stable);
|
|
2262
2282
|
return message;
|
|
2263
2283
|
};
|
|
2264
2284
|
|
|
@@ -2286,6 +2306,7 @@ $root.presage = (function() {
|
|
|
2286
2306
|
object.timestamp = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : typeof BigInt !== "undefined" && options.longs === BigInt ? long.toBigInt() : long;
|
|
2287
2307
|
} else
|
|
2288
2308
|
object.timestamp = options.longs === String ? "0" : typeof BigInt !== "undefined" && options.longs === BigInt ? BigInt("0") : 0;
|
|
2309
|
+
object.stable = false;
|
|
2289
2310
|
}
|
|
2290
2311
|
if (message.value != null && Object.hasOwnProperty.call(message, "value"))
|
|
2291
2312
|
object.value = options.json && !isFinite(message.value) ? String(message.value) : message.value;
|
|
@@ -2296,6 +2317,8 @@ $root.presage = (function() {
|
|
|
2296
2317
|
object.timestamp = options.longs === String ? String(message.timestamp) : message.timestamp;
|
|
2297
2318
|
else
|
|
2298
2319
|
object.timestamp = options.longs === String ? $util.Long.prototype.toString.call(message.timestamp) : options.longs === Number ? new $util.LongBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0).toNumber() : message.timestamp;
|
|
2320
|
+
if (message.stable != null && Object.hasOwnProperty.call(message, "stable"))
|
|
2321
|
+
object.stable = message.stable;
|
|
2299
2322
|
return object;
|
|
2300
2323
|
};
|
|
2301
2324
|
|
|
@@ -2729,11 +2752,11 @@ $root.presage = (function() {
|
|
|
2729
2752
|
* @property {Array.<presage.smartspectra.IMeasurement>|null} [lowerTrace] Abdominal breathing movement trace measurements. The trace has no confidence
|
|
2730
2753
|
* of its own; `stable` is inherited from the breathing rate verdict (>= 45); see
|
|
2731
2754
|
* the [breathing model card](https://vandv.presagetech.com/breathing_model_card.html).
|
|
2732
|
-
* @property {Array.<presage.smartspectra.IMeasurement>|null} [amplitude]
|
|
2733
|
-
* @property {Array.<presage.smartspectra.IDetectionStatus>|null} [apnea]
|
|
2734
|
-
* @property {Array.<presage.smartspectra.IMeasurement>|null} [respiratoryLineLength]
|
|
2735
|
-
* @property {Array.<presage.smartspectra.IMeasurement>|null} [baseline]
|
|
2736
|
-
* @property {Array.<presage.smartspectra.IMeasurement>|null} [inhaleExhaleRatio]
|
|
2755
|
+
* @property {Array.<presage.smartspectra.IMeasurement>|null} [amplitude] Unavailable: breathing amplitude. This field stays empty; requesting BREATHING_AMPLITUDE makes start fail with ConfigurationFailed.
|
|
2756
|
+
* @property {Array.<presage.smartspectra.IDetectionStatus>|null} [apnea] Unavailable: apnea (breathing cessation) detection. This field stays empty; requesting APNEA makes start fail with ConfigurationFailed.
|
|
2757
|
+
* @property {Array.<presage.smartspectra.IMeasurement>|null} [respiratoryLineLength] Unavailable: respiratory line length. This field stays empty; requesting RESPIRATORY_LINE_LENGTH makes start fail with ConfigurationFailed.
|
|
2758
|
+
* @property {Array.<presage.smartspectra.IMeasurement>|null} [baseline] Unavailable: breathing baseline. This field stays empty; requesting BASELINE makes start fail with ConfigurationFailed.
|
|
2759
|
+
* @property {Array.<presage.smartspectra.IMeasurement>|null} [inhaleExhaleRatio] Unavailable: inhale/exhale duration ratio. This field stays empty; requesting INHALE_EXHALE_RATIO makes start fail with ConfigurationFailed.
|
|
2737
2760
|
* @property {presage.smartspectra.IStrict|null} [breathingRateStrict] Strict/high-precision breathing measurements over a fixed time interval.
|
|
2738
2761
|
* Populated only when BREATHING_RATE_STRICT is requested and a value is
|
|
2739
2762
|
* available.
|
|
@@ -2796,7 +2819,7 @@ $root.presage = (function() {
|
|
|
2796
2819
|
Breathing.prototype.lowerTrace = $util.emptyArray;
|
|
2797
2820
|
|
|
2798
2821
|
/**
|
|
2799
|
-
*
|
|
2822
|
+
* Unavailable: breathing amplitude. This field stays empty; requesting BREATHING_AMPLITUDE makes start fail with ConfigurationFailed.
|
|
2800
2823
|
* @member {Array.<presage.smartspectra.IMeasurement>} amplitude
|
|
2801
2824
|
* @memberof presage.smartspectra.Breathing
|
|
2802
2825
|
* @instance
|
|
@@ -2804,7 +2827,7 @@ $root.presage = (function() {
|
|
|
2804
2827
|
Breathing.prototype.amplitude = $util.emptyArray;
|
|
2805
2828
|
|
|
2806
2829
|
/**
|
|
2807
|
-
*
|
|
2830
|
+
* Unavailable: apnea (breathing cessation) detection. This field stays empty; requesting APNEA makes start fail with ConfigurationFailed.
|
|
2808
2831
|
* @member {Array.<presage.smartspectra.IDetectionStatus>} apnea
|
|
2809
2832
|
* @memberof presage.smartspectra.Breathing
|
|
2810
2833
|
* @instance
|
|
@@ -2812,7 +2835,7 @@ $root.presage = (function() {
|
|
|
2812
2835
|
Breathing.prototype.apnea = $util.emptyArray;
|
|
2813
2836
|
|
|
2814
2837
|
/**
|
|
2815
|
-
*
|
|
2838
|
+
* Unavailable: respiratory line length. This field stays empty; requesting RESPIRATORY_LINE_LENGTH makes start fail with ConfigurationFailed.
|
|
2816
2839
|
* @member {Array.<presage.smartspectra.IMeasurement>} respiratoryLineLength
|
|
2817
2840
|
* @memberof presage.smartspectra.Breathing
|
|
2818
2841
|
* @instance
|
|
@@ -2820,7 +2843,7 @@ $root.presage = (function() {
|
|
|
2820
2843
|
Breathing.prototype.respiratoryLineLength = $util.emptyArray;
|
|
2821
2844
|
|
|
2822
2845
|
/**
|
|
2823
|
-
*
|
|
2846
|
+
* Unavailable: breathing baseline. This field stays empty; requesting BASELINE makes start fail with ConfigurationFailed.
|
|
2824
2847
|
* @member {Array.<presage.smartspectra.IMeasurement>} baseline
|
|
2825
2848
|
* @memberof presage.smartspectra.Breathing
|
|
2826
2849
|
* @instance
|
|
@@ -2828,7 +2851,7 @@ $root.presage = (function() {
|
|
|
2828
2851
|
Breathing.prototype.baseline = $util.emptyArray;
|
|
2829
2852
|
|
|
2830
2853
|
/**
|
|
2831
|
-
*
|
|
2854
|
+
* Unavailable: inhale/exhale duration ratio. This field stays empty; requesting INHALE_EXHALE_RATIO makes start fail with ConfigurationFailed.
|
|
2832
2855
|
* @member {Array.<presage.smartspectra.IMeasurement>} inhaleExhaleRatio
|
|
2833
2856
|
* @memberof presage.smartspectra.Breathing
|
|
2834
2857
|
* @instance
|
package/js/renderer/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ValidationCodeValue,
|
|
10
10
|
SmartSpectraErrorCodeValue,
|
|
11
11
|
SmartSpectraOptions,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../index';
|
|
13
13
|
|
|
14
14
|
// Re-export the public value constants and matching type aliases so the
|
|
15
15
|
// renderer-side consumer can import everything from one entry point.
|
|
@@ -26,7 +26,7 @@ export {
|
|
|
26
26
|
micromotionMetrics,
|
|
27
27
|
edaMetrics,
|
|
28
28
|
defaultSupportedMetrics,
|
|
29
|
-
} from '
|
|
29
|
+
} from '../index';
|
|
30
30
|
export type {
|
|
31
31
|
ProcessingStatusValue,
|
|
32
32
|
PixelFormatValue,
|
|
@@ -35,7 +35,7 @@ export type {
|
|
|
35
35
|
FrameTransformValue,
|
|
36
36
|
MetricTypeValue,
|
|
37
37
|
SmartSpectraOptions,
|
|
38
|
-
} from '
|
|
38
|
+
} from '../index';
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Renderer constructor options: the shared SDK options plus a renderer-only
|
package/js/smartspectra.js
CHANGED
|
@@ -39,7 +39,7 @@ const SUPPORTED_EVENTS = new Set([
|
|
|
39
39
|
// FrameTransform object declared later.
|
|
40
40
|
const DEFAULT_FRAME_TRANSFORM = 0;
|
|
41
41
|
|
|
42
|
-
let
|
|
42
|
+
let readinessInFlight = false;
|
|
43
43
|
|
|
44
44
|
class SmartSpectraSDK {
|
|
45
45
|
/** SDK package version (matches `package.json#version`). */
|
|
@@ -73,7 +73,7 @@ class SmartSpectraSDK {
|
|
|
73
73
|
this._source = null;
|
|
74
74
|
this._explicitCameraSelection = false;
|
|
75
75
|
this._session = null; // lazy: created on start()
|
|
76
|
-
this.
|
|
76
|
+
this._readinessInFlight = false;
|
|
77
77
|
|
|
78
78
|
this._destroyPromise = null;
|
|
79
79
|
}
|
|
@@ -103,7 +103,7 @@ class SmartSpectraSDK {
|
|
|
103
103
|
* Spatial transform applied to every pushed frame.
|
|
104
104
|
*/
|
|
105
105
|
useCustomInput(frameTransform = DEFAULT_FRAME_TRANSFORM) {
|
|
106
|
-
this.
|
|
106
|
+
this._requireReadinessIdle();
|
|
107
107
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
108
108
|
this._source = { kind: 'custom', frameTransform: frameTransform | 0 };
|
|
109
109
|
return this;
|
|
@@ -124,7 +124,7 @@ class SmartSpectraSDK {
|
|
|
124
124
|
* getUserMedia — use the renderer SDK's useMediaStream() there instead.
|
|
125
125
|
*/
|
|
126
126
|
useCamera(selection = null, options = {}) {
|
|
127
|
-
this.
|
|
127
|
+
this._requireReadinessIdle();
|
|
128
128
|
const isSelection = selection !== null && typeof selection === 'object' && 'kind' in selection;
|
|
129
129
|
if (!isSelection && arguments.length < 2) {
|
|
130
130
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
@@ -151,7 +151,7 @@ class SmartSpectraSDK {
|
|
|
151
151
|
* { timestampsPath?, interframeDelayMs?, startOffsetMs?, maxDurationMs?, frameTransform? }
|
|
152
152
|
*/
|
|
153
153
|
useFile(videoPath, options = null) {
|
|
154
|
-
this.
|
|
154
|
+
this._requireReadinessIdle();
|
|
155
155
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
156
156
|
if (typeof videoPath !== 'string' || !videoPath) {
|
|
157
157
|
throw new TypeError('useFile: videoPath must be a non-empty string');
|
|
@@ -167,7 +167,7 @@ class SmartSpectraSDK {
|
|
|
167
167
|
* useCustomInput(), useCamera(), or useFile() first.
|
|
168
168
|
*/
|
|
169
169
|
start() {
|
|
170
|
-
this.
|
|
170
|
+
this._requireReadinessIdle();
|
|
171
171
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
172
172
|
const source = this._source;
|
|
173
173
|
if (!source) {
|
|
@@ -215,7 +215,11 @@ class SmartSpectraSDK {
|
|
|
215
215
|
* Typed selection requires any pending stopAsync() to settle before reset.
|
|
216
216
|
*/
|
|
217
217
|
reset() {
|
|
218
|
-
this.
|
|
218
|
+
this._requireReadinessIdle();
|
|
219
|
+
if (this._readinessService) {
|
|
220
|
+
this._readinessService.closeSync();
|
|
221
|
+
this._readinessService = null;
|
|
222
|
+
}
|
|
219
223
|
if (!this._session) return;
|
|
220
224
|
this._session.reset(this._explicitCameraSelection);
|
|
221
225
|
if (!this._explicitCameraSelection) this._source = null;
|
|
@@ -276,9 +280,13 @@ class SmartSpectraSDK {
|
|
|
276
280
|
try {
|
|
277
281
|
if (this._destroyPromise) return this._destroyPromise;
|
|
278
282
|
// An unrelated, unused SDK owns no native state to tear down.
|
|
279
|
-
if (!this._session && !this.
|
|
280
|
-
this.
|
|
281
|
-
const
|
|
283
|
+
if (!this._session && !this._readinessService && !this._readinessInFlight) return Promise.resolve();
|
|
284
|
+
this._requireReadinessIdle();
|
|
285
|
+
const service = this._readinessService;
|
|
286
|
+
this._readinessService = null;
|
|
287
|
+
const session = this._session;
|
|
288
|
+
// Destroy the session synchronously unless a readiness service must drain first.
|
|
289
|
+
const teardown = service ? service.close().then(() => session?.destroy()) : session?.destroy();
|
|
282
290
|
this._session = null;
|
|
283
291
|
this._source = null;
|
|
284
292
|
this._destroyPromise = Promise.resolve(teardown).finally(() => { this._destroyPromise = null; });
|
|
@@ -288,21 +296,18 @@ class SmartSpectraSDK {
|
|
|
288
296
|
}
|
|
289
297
|
}
|
|
290
298
|
|
|
291
|
-
async
|
|
292
|
-
if (this.
|
|
293
|
-
|
|
294
|
-
this.
|
|
299
|
+
async checkDeviceReadiness() {
|
|
300
|
+
if (this._readinessResultPending) return Object.freeze({ verdict: 'inconclusive',
|
|
301
|
+
reason: 'The SDK is busy. Wait for the current SDK operation to finish.' });
|
|
302
|
+
this._readinessResultPending = true;
|
|
295
303
|
try {
|
|
296
|
-
const result = await this.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} finally { this._compatibilityResultPending = false; }
|
|
304
|
+
const result = await this._runDeviceReadiness();
|
|
305
|
+
return Object.freeze({ verdict: result.verdict, reason: result.reason });
|
|
306
|
+
} finally { this._readinessResultPending = false; }
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
async _runDeviceCompatibility() {
|
|
305
|
-
const { DeviceCompatibilityService, DeviceCompatibilityPolicies } = require('./device-compatibility');
|
|
309
|
+
async _runDeviceReadiness() {
|
|
310
|
+
const { DeviceReadinessService, DeviceReadinessPolicies } = require('./device-readiness');
|
|
306
311
|
const selected = { ...this._options,
|
|
307
312
|
requestedMetrics: [...(this._options.requestedMetrics?.length
|
|
308
313
|
? this._options.requestedMetrics : defaultSupportedMetrics)] };
|
|
@@ -314,16 +319,16 @@ class SmartSpectraSDK {
|
|
|
314
319
|
workloadCompleted: false, observationUs: 0, framesAttempted: 0,
|
|
315
320
|
performanceChecks: [], metricOutputs: [], timing: { available: false } }) });
|
|
316
321
|
const busy = () => preflight('busy', 'The SDK is busy. Wait for the current SDK operation to finish.');
|
|
317
|
-
if (
|
|
322
|
+
if (readinessInFlight || this._readinessInFlight || this._destroyPromise)
|
|
318
323
|
return busy();
|
|
319
324
|
const status = this.processingStatus;
|
|
320
325
|
if (status === ProcessingStatus.kError) return preflight('reset_required',
|
|
321
|
-
'The SDK is in an error state. Resolve the previous error, reset the SDK, and retry the
|
|
326
|
+
'The SDK is in an error state. Resolve the previous error, reset the SDK, and retry the readiness check.');
|
|
322
327
|
if (![ProcessingStatus.kUninitialized, ProcessingStatus.kIdle].includes(status)) return busy();
|
|
323
328
|
if (source?.kind !== 'camera') return preflight('unsupported_input',
|
|
324
|
-
'Select a camera before checking
|
|
325
|
-
this.
|
|
326
|
-
|
|
329
|
+
'Select a camera before checking readiness. File and custom-frame inputs are not yet supported; no device capacity verdict was established.');
|
|
330
|
+
this._readinessInFlight = true;
|
|
331
|
+
readinessInFlight = true;
|
|
327
332
|
let service;
|
|
328
333
|
try {
|
|
329
334
|
const capture = source.options ?? {};
|
|
@@ -333,7 +338,7 @@ class SmartSpectraSDK {
|
|
|
333
338
|
preferredWidth: capture.width || 1280, preferredHeight: capture.height || 720,
|
|
334
339
|
preferredFps: capture.fps || 30 },
|
|
335
340
|
frameWidth: capture.width || 1280, frameHeight: capture.height || 720, targetFps: capture.fps || 30 };
|
|
336
|
-
const policy =
|
|
341
|
+
const policy = DeviceReadinessPolicies.reference720p30('full');
|
|
337
342
|
const metrics = new Set(selected.requestedMetrics);
|
|
338
343
|
request.policy = { ...policy, frameWidth: request.frameWidth, frameHeight: request.frameHeight,
|
|
339
344
|
targetFps: request.targetFps, metrics: policy.metrics.filter(metric => metrics.has(metric.metricId)) };
|
|
@@ -341,17 +346,19 @@ class SmartSpectraSDK {
|
|
|
341
346
|
await this._session.destroy();
|
|
342
347
|
this._session = null;
|
|
343
348
|
}
|
|
344
|
-
|
|
349
|
+
if (!this._readinessService || this._readinessService.closed)
|
|
350
|
+
this._readinessService = new DeviceReadinessService();
|
|
351
|
+
service = this._readinessService;
|
|
345
352
|
if (!service.reserve()) return busy();
|
|
346
353
|
return await service.start(selected, request).result;
|
|
347
354
|
} finally {
|
|
348
|
-
try { if (service) await service.
|
|
349
|
-
finally { this.
|
|
355
|
+
try { if (service) await service.finishCheck(); }
|
|
356
|
+
finally { this._readinessInFlight = false; readinessInFlight = false; }
|
|
350
357
|
}
|
|
351
358
|
}
|
|
352
359
|
|
|
353
|
-
|
|
354
|
-
if (
|
|
360
|
+
_requireReadinessIdle() {
|
|
361
|
+
if (readinessInFlight || this._readinessInFlight) throw Object.assign(new Error('Device readiness is running'), {
|
|
355
362
|
code: SmartSpectraErrorCode.kInvalidState, retryable: true,
|
|
356
363
|
});
|
|
357
364
|
}
|
|
@@ -359,7 +366,7 @@ class SmartSpectraSDK {
|
|
|
359
366
|
// ---------- internal ----------------------------------------------------
|
|
360
367
|
|
|
361
368
|
_requireSourceSelectionAllowed() {
|
|
362
|
-
this.
|
|
369
|
+
this._requireReadinessIdle();
|
|
363
370
|
if (this._destroyPromise) {
|
|
364
371
|
throw Object.assign(new Error('SmartSpectra is shutting down'), {
|
|
365
372
|
code: SmartSpectraErrorCode.kInvalidState, retryable: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartspectra/node-sdk",
|
|
3
|
-
"version": "3.5.0-rc.
|
|
3
|
+
"version": "3.5.0-rc.5",
|
|
4
4
|
"description": "Node.js (Electron) FFI binding for SmartSpectra vitals measurement",
|
|
5
5
|
"author": "Presage Technologies, Inc.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"THIRD_PARTY_FFMPEG.md",
|
|
36
36
|
"THIRD_PARTY_NOTICES.txt",
|
|
37
37
|
"NOTICE",
|
|
38
|
-
"docs/device-
|
|
39
|
-
"samples/
|
|
38
|
+
"docs/device-readiness.md",
|
|
39
|
+
"samples/device_readiness/*.cjs",
|
|
40
40
|
"README.md"
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"prepare": "node scripts/gen-proto.cjs",
|
|
44
44
|
"build:proto": "node scripts/gen-proto.cjs",
|
|
45
|
-
"test": "npm run test:types && node --test test/log-level-pin.js test/renderer-timestamp.js test/renderer-frame-transform.js test/camera-selection.js test/reset-lifecycle.js test/resolve-native.js test/device-
|
|
46
|
-
"test:types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext test/camera-selection-types.ts test/renderer-types.ts test/device-
|
|
45
|
+
"test": "npm run test:types && node --test test/strict-results.js test/log-level-pin.js test/renderer-timestamp.js test/renderer-frame-transform.js test/camera-selection.js test/reset-lifecycle.js test/resolve-native.js test/device-readiness.js test/device-readiness-presentation.js test/smoke.js",
|
|
46
|
+
"test:types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext test/camera-selection-types.ts test/renderer-types.ts test/device-readiness-types.ts",
|
|
47
47
|
"test:models-allowlist": "node test/models-allowlist.js"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"koffi": "^2.10.0",
|
|
75
75
|
"protobufjs": "^7.6.5",
|
|
76
|
-
"@smartspectra/node-sdk-linux-x64": "3.5.0-rc.
|
|
77
|
-
"@smartspectra/node-sdk-linux-arm64": "3.5.0-rc.
|
|
78
|
-
"@smartspectra/node-sdk-darwin-arm64": "3.5.0-rc.
|
|
79
|
-
"@smartspectra/node-sdk-win32-x64": "3.5.0-rc.
|
|
76
|
+
"@smartspectra/node-sdk-linux-x64": "3.5.0-rc.5",
|
|
77
|
+
"@smartspectra/node-sdk-linux-arm64": "3.5.0-rc.5",
|
|
78
|
+
"@smartspectra/node-sdk-darwin-arm64": "3.5.0-rc.5",
|
|
79
|
+
"@smartspectra/node-sdk-win32-x64": "3.5.0-rc.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/node": "^24.0.0",
|
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
3
|
'use strict';
|
|
4
4
|
const { SmartSpectraSDK } = require('@smartspectra/node-sdk');
|
|
5
|
-
const { writeFileSync } = require('node:fs');
|
|
6
5
|
(async () => {
|
|
7
6
|
const sdk = new SmartSpectraSDK({ apiKey: process.env.PHYSIOLOGY_API_KEY });
|
|
8
7
|
sdk.useCamera();
|
|
9
8
|
try {
|
|
10
|
-
await sdk.
|
|
11
|
-
const result = sdk.getDeviceCompatibilityResult();
|
|
9
|
+
const result = await sdk.checkDeviceReadiness();
|
|
12
10
|
console.log(result.verdict, result.reason);
|
|
13
|
-
if (process.argv[2]) writeFileSync(process.argv[2], result.diagnostics, { flag: 'wx', mode: 0o600 });
|
|
14
11
|
process.exitCode = result.verdict === 'ready' ? 0 : 2;
|
|
15
12
|
} finally { await sdk.destroy(); }
|
|
16
13
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -5,5 +5,4 @@ function present(result) {
|
|
|
5
5
|
return { title: result.verdict, reason: result.reason, action: result.reason,
|
|
6
6
|
canProceed: result.verdict === 'ready' };
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
module.exports = { present, supportReport };
|
|
8
|
+
module.exports = { present };
|
|
@@ -6,9 +6,9 @@ async function main() {
|
|
|
6
6
|
const sdk = new SmartSpectraSDK({ apiKey: process.env.PHYSIOLOGY_API_KEY });
|
|
7
7
|
sdk.useCamera();
|
|
8
8
|
try {
|
|
9
|
-
const
|
|
10
|
-
console.log(verdict);
|
|
11
|
-
process.exitCode = verdict === 'ready' ? 0 : 2;
|
|
9
|
+
const result = await sdk.checkDeviceReadiness();
|
|
10
|
+
console.log(result.verdict);
|
|
11
|
+
process.exitCode = result.verdict === 'ready' ? 0 : 2;
|
|
12
12
|
} finally { await sdk.destroy(); }
|
|
13
13
|
}
|
|
14
14
|
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Node.js Device Compatibility
|
|
3
|
-
description: Check the configured SDK and return a verdict, actionable reason, and diagnostic JSON.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Node.js Device Compatibility
|
|
7
|
-
|
|
8
|
-
> **Important:** SDK metrics are offered for general wellness and informational purposes only. SDK metrics have not been cleared by the FDA and may not be used for medical diagnosis or treatment.
|
|
9
|
-
|
|
10
|
-
After configuring the SDK's authentication, metrics, and camera, call:
|
|
11
|
-
|
|
12
|
-
```javascript
|
|
13
|
-
const verdict = await sdk.isDeviceCompatible();
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Compare the returned verdict explicitly, for example `verdict === 'ready'`.
|
|
17
|
-
Every verdict string is truthy, including `'blocked'`; do not use it as a Boolean. Call `sdk.getDeviceCompatibilityResult()`
|
|
18
|
-
for the saved explanation and diagnostic JSON. The getter is empty before or during
|
|
19
|
-
a check and after an exception; starting a new check clears the previous result. There is no separate configuration input
|
|
20
|
-
or public policy/timing override. The check uses the SDK's configured workload,
|
|
21
|
-
keeps its settings unchanged, and rejects competing measurement operations.
|
|
22
|
-
|
|
23
|
-
The selected camera supplies capture pacing; bundled reference imagery replaces
|
|
24
|
-
captured pixels. Stable checks may finish after 20 seconds of observation;
|
|
25
|
-
otherwise observation continues to approximately 30 seconds. Setup and cleanup
|
|
26
|
-
add time. File/custom-frame inputs are currently Inconclusive with an explanation.
|
|
27
|
-
|
|
28
|
-
Await the result; the check does not block the JavaScript event loop.
|
|
29
|
-
|
|
30
|
-
See [Device Compatibility Results and Recovery](../../docs/device-compatibility-experience.md)
|
|
31
|
-
for complete setup, verdict meanings, execution safety, and JSON export examples.
|