@smartspectra/node-sdk 3.5.0-rc.3 → 3.5.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/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 (TensorFlow Lite)
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 compatibility: [configured checks and diagnostic results](docs/device-compatibility.md)
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
@@ -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 (TensorFlow Lite)
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 (TensorFlow Lite)
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; currently
140
- // equals the breathing bundle.
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 v8.');
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)')() !== 8) throw unsupported();
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 DeviceCompatibilityCheck {
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
- const services = new FinalizationRegistry(({ n, handle }) => {
276
- // Best effort for abandoned services; explicit close() is deterministic.
277
- n.destroy.async(handle, () => {});
278
- });
279
- class DeviceCompatibilityService {
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 DeviceCompatibilityService();
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
- services.register(this, { n: this.#native, handle: this.#handle }, this);
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('Compatibility service is closed.');
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 = DeviceCompatibilityPolicies.reference720p30(workload, scope);
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 DeviceCompatibilityCheck(this.#native, handle, signal, value => this.#checks.delete(value));
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(() => new Promise((resolve, reject) => {
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.#native.destroy.async(handle, error => error ? reject(error) : resolve());
337
- }));
395
+ return destroyNative(this.#lifetime);
396
+ });
338
397
  return this.#closing;
339
398
  }
340
399
  }
341
- const DeviceCompatibilityPolicies = Object.freeze({
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 deviceCompatibilityAvailable() { try { getNative(); return true; } catch { return false; } }
364
- module.exports = { DeviceCompatibilityService, DeviceCompatibilityPolicies, deviceCompatibilityAvailable, busyResult };
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` when omitted.
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
- /** Aggregate SDK telemetry. Defaults to `true`; set `false` to opt out. */
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
- /** Throttle between frames in ms; omit/0 = as fast as possible. */
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 Compatibility uses this SDK's authentication. */
218
- isDeviceCompatible(): Promise<DeviceCompatibilityVerdict>;
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 Compatibility. Pin the JS and native SDK versions together. */
389
+ /** Device readiness. Pin the JS and native SDK versions together. */
378
390
  /** Outcome for the configured SDK workload. */
379
- export type DeviceCompatibilityVerdict = 'ready' | 'limited' | 'blocked' | 'inconclusive';
380
- export interface DeviceCompatibilityResult {
381
- readonly verdict: DeviceCompatibilityVerdict;
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
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { BrowserWindow } from 'electron';
7
7
 
8
- export { SmartSpectraSDK, ProcessingStatus } from '../../js/index';
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
- /** Breathing amplitude measurements */
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
- /** Apnea (breathing cessation) detection status */
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
- /** Respiratory line length measurements for breathing pattern analysis */
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
- /** Baseline breathing measurements */
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
- /** Inhale to exhale duration ratio measurements */
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
- /** Breathing amplitude measurements */
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
- /** Apnea (breathing cessation) detection status */
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
- /** Respiratory line length measurements for breathing pattern analysis */
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
- /** Baseline breathing measurements */
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
- /** Inhale to exhale duration ratio measurements */
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
  /**
@@ -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] Breathing amplitude measurements
2733
- * @property {Array.<presage.smartspectra.IDetectionStatus>|null} [apnea] Apnea (breathing cessation) detection status
2734
- * @property {Array.<presage.smartspectra.IMeasurement>|null} [respiratoryLineLength] Respiratory line length measurements for breathing pattern analysis
2735
- * @property {Array.<presage.smartspectra.IMeasurement>|null} [baseline] Baseline breathing measurements
2736
- * @property {Array.<presage.smartspectra.IMeasurement>|null} [inhaleExhaleRatio] Inhale to exhale duration ratio measurements
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
- * Breathing amplitude measurements
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
- * Apnea (breathing cessation) detection status
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
- * Respiratory line length measurements for breathing pattern analysis
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
- * Baseline breathing measurements
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
- * Inhale to exhale duration ratio measurements
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
@@ -9,7 +9,7 @@ import type {
9
9
  ValidationCodeValue,
10
10
  SmartSpectraErrorCodeValue,
11
11
  SmartSpectraOptions,
12
- } from '../../js/index';
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 '../../js/index';
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 '../../js/index';
38
+ } from '../index';
39
39
 
40
40
  /**
41
41
  * Renderer constructor options: the shared SDK options plus a renderer-only
@@ -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 compatibilityInFlight = false;
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._compatibilityInFlight = false;
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._requireCompatibilityIdle();
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._requireCompatibilityIdle();
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._requireCompatibilityIdle();
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._requireCompatibilityIdle();
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._requireCompatibilityIdle();
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._compatibilityInFlight) return Promise.resolve();
280
- this._requireCompatibilityIdle();
281
- const teardown = this._session.destroy();
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 isDeviceCompatible() {
292
- if (this._compatibilityResultPending) return 'inconclusive';
293
- this._compatibilityResult = undefined;
294
- this._compatibilityResultPending = true;
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._runDeviceCompatibility();
297
- this._compatibilityResult = Object.freeze({ ...result });
298
- return result.verdict;
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
- getDeviceCompatibilityResult() { return this._compatibilityResult; }
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 (compatibilityInFlight || this._compatibilityInFlight || this._destroyPromise)
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 compatibility check.');
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 compatibility. File and custom-frame inputs are not yet supported; no device capacity verdict was established.');
325
- this._compatibilityInFlight = true;
326
- compatibilityInFlight = true;
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 = DeviceCompatibilityPolicies.reference720p30('full');
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
- service = new DeviceCompatibilityService();
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.close(); }
349
- finally { this._compatibilityInFlight = false; compatibilityInFlight = false; }
355
+ try { if (service) await service.finishCheck(); }
356
+ finally { this._readinessInFlight = false; readinessInFlight = false; }
350
357
  }
351
358
  }
352
359
 
353
- _requireCompatibilityIdle() {
354
- if (compatibilityInFlight || this._compatibilityInFlight) throw Object.assign(new Error('Device compatibility is running'), {
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._requireCompatibilityIdle();
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",
3
+ "version": "3.5.0",
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-compatibility.md",
39
- "samples/device_compatibility/*.cjs",
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-compatibility.js test/device-compatibility-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-compatibility-types.ts",
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.3",
77
- "@smartspectra/node-sdk-linux-arm64": "3.5.0-rc.3",
78
- "@smartspectra/node-sdk-darwin-arm64": "3.5.0-rc.3",
79
- "@smartspectra/node-sdk-win32-x64": "3.5.0-rc.3"
76
+ "@smartspectra/node-sdk-linux-x64": "3.5.0",
77
+ "@smartspectra/node-sdk-linux-arm64": "3.5.0",
78
+ "@smartspectra/node-sdk-darwin-arm64": "3.5.0",
79
+ "@smartspectra/node-sdk-win32-x64": "3.5.0"
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.isDeviceCompatible();
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
- function supportReport(result) { return result.diagnostics; }
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 verdict = await sdk.isDeviceCompatible();
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.