@smartspectra/node-sdk 3.5.0-rc.2 → 3.5.0-rc.3
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/README.md +1 -0
- package/docs/device-compatibility.md +31 -0
- package/js/constants.js +1 -0
- package/js/device-compatibility.js +371 -0
- package/js/ffi.js +3 -2
- package/js/index.d.ts +16 -0
- package/js/resolve-native.js +30 -0
- package/js/smartspectra.js +91 -7
- package/package.json +9 -7
- package/samples/device_compatibility/main.cjs +16 -0
- package/samples/device_compatibility/presentation.cjs +9 -0
- package/samples/device_compatibility/simple.cjs +14 -0
package/README.md
CHANGED
|
@@ -14,6 +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
18
|
- Metrics guide: <https://smartspectra.presagetech.com/docs/nodejs/metrics>
|
|
18
19
|
|
|
19
20
|
## Architecture
|
|
@@ -0,0 +1,31 @@
|
|
|
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.
|
package/js/constants.js
CHANGED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
|
+
'use strict';
|
|
4
|
+
const koffi = require('koffi');
|
|
5
|
+
const { readinessLibrary, ensurePreconfigured } = require('./ffi');
|
|
6
|
+
let native;
|
|
7
|
+
let typesRegistered = false;
|
|
8
|
+
function unsupported() {
|
|
9
|
+
const error = new Error('This native SDK does not support device readiness ABI v8.');
|
|
10
|
+
error.code = 'unsupported_capability';
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
function getNative() {
|
|
14
|
+
if (native) return native;
|
|
15
|
+
const lib = readinessLibrary();
|
|
16
|
+
try {
|
|
17
|
+
if (lib.func('int smartspectra_readiness_abi_version(void)')() !== 8) throw unsupported();
|
|
18
|
+
} catch { throw unsupported(); }
|
|
19
|
+
if (!typesRegistered) {
|
|
20
|
+
koffi.struct('smartspectra_readiness_requirement_t', {
|
|
21
|
+
metric_id: 'int',
|
|
22
|
+
minimum_samples: 'uint64_t',
|
|
23
|
+
minimum_output_hz: 'double',
|
|
24
|
+
output_required: 'int',
|
|
25
|
+
});
|
|
26
|
+
koffi.struct('smartspectra_readiness_policy_t', {
|
|
27
|
+
version: 'const char*',
|
|
28
|
+
rationale: 'const char*',
|
|
29
|
+
input_mode: 'const char*',
|
|
30
|
+
frame_width: 'int',
|
|
31
|
+
frame_height: 'int',
|
|
32
|
+
target_fps: 'double',
|
|
33
|
+
minimum_throughput_fps: 'double',
|
|
34
|
+
minimum_duration_us: 'uint64_t',
|
|
35
|
+
metrics: 'const smartspectra_readiness_requirement_t*',
|
|
36
|
+
metrics_len: 'size_t',
|
|
37
|
+
early_window_us: 'uint64_t',
|
|
38
|
+
early_stable_windows: 'uint32_t',
|
|
39
|
+
early_minimum_headroom: 'double',
|
|
40
|
+
uncertainty_fraction: 'double',
|
|
41
|
+
});
|
|
42
|
+
koffi.struct('smartspectra_readiness_request_t', {
|
|
43
|
+
struct_size: 'uint32_t',
|
|
44
|
+
reserved_opt_in: 'int',
|
|
45
|
+
authentication_prepared: 'int',
|
|
46
|
+
scope: 'int',
|
|
47
|
+
camera_permission: 'int',
|
|
48
|
+
camera_index: 'int',
|
|
49
|
+
camera_width: 'int',
|
|
50
|
+
camera_height: 'int',
|
|
51
|
+
camera_fps: 'int',
|
|
52
|
+
frame_width: 'int',
|
|
53
|
+
frame_height: 'int',
|
|
54
|
+
target_fps: 'double',
|
|
55
|
+
duration_us: 'uint64_t',
|
|
56
|
+
maximum_wait_us: 'uint64_t',
|
|
57
|
+
cleanup_reserve_us: 'uint64_t',
|
|
58
|
+
reference_bgr_frame: 'const uint8_t*',
|
|
59
|
+
reference_bgr_frame_len: 'size_t',
|
|
60
|
+
policy: 'smartspectra_readiness_policy_t',
|
|
61
|
+
camera_selection_kind: 'int', camera_selection_id: 'const char*',
|
|
62
|
+
});
|
|
63
|
+
koffi.struct('smartspectra_readiness_progress_t', {
|
|
64
|
+
phase: 'int',
|
|
65
|
+
timing_available: 'int',
|
|
66
|
+
elapsed_us: 'uint64_t',
|
|
67
|
+
has_deadline: 'int',
|
|
68
|
+
remaining_us: 'uint64_t',
|
|
69
|
+
});
|
|
70
|
+
koffi.struct('smartspectra_readiness_result_t', {
|
|
71
|
+
verdict: 'int', reason: 'const char*', diagnostics: 'const char*',
|
|
72
|
+
});
|
|
73
|
+
typesRegistered = true;
|
|
74
|
+
}
|
|
75
|
+
native = {
|
|
76
|
+
referencePolicy: lib.func('int smartspectra_readiness_reference_policy(int workload, int scope, _Out_ smartspectra_readiness_policy_t *out)'),
|
|
77
|
+
fallbackPolicy: lib.func('int smartspectra_readiness_fallback_policy(const smartspectra_readiness_policy_t *policy, int verdict, int reason, _Out_ smartspectra_readiness_policy_t *out)'),
|
|
78
|
+
policy: lib.func('int smartspectra_readiness_chest_policy(_Out_ smartspectra_readiness_policy_t *out)'),
|
|
79
|
+
create: lib.func('void *smartspectra_readiness_create(void)'),
|
|
80
|
+
reserve: lib.func('int smartspectra_readiness_reserve(void *service)'),
|
|
81
|
+
destroy: lib.func('void smartspectra_readiness_destroy(void *service)'),
|
|
82
|
+
start: lib.func('void *smartspectra_readiness_start(void *service, const smartspectra_config_t *config, const smartspectra_readiness_request_t *request)'),
|
|
83
|
+
cancel: lib.func('void smartspectra_readiness_cancel(void *check)'),
|
|
84
|
+
background: lib.func('void smartspectra_readiness_background(void *check)'),
|
|
85
|
+
progress: lib.func('int smartspectra_readiness_progress(void *check, _Out_ smartspectra_readiness_progress_t *out)'),
|
|
86
|
+
poll: lib.func('int smartspectra_readiness_poll(void *check, _Out_ smartspectra_readiness_result_t *out)'),
|
|
87
|
+
free: lib.func('void smartspectra_readiness_check_free(void *check)'),
|
|
88
|
+
};
|
|
89
|
+
return native;
|
|
90
|
+
}
|
|
91
|
+
const verdicts = ['ready', 'limited', 'blocked', 'inconclusive'];
|
|
92
|
+
const reasons = ['qualified', 'performance_below_policy', 'invalid_configuration',
|
|
93
|
+
'authentication_required', 'authorization_denied', 'input_unavailable', 'setup_failed',
|
|
94
|
+
'run_incomplete', 'insufficient_evidence', 'policy_unavailable', 'backend_unknown',
|
|
95
|
+
'configuration_changed', 'cancelled', 'deadline_exceeded', 'application_backgrounded',
|
|
96
|
+
'busy', 'camera_interrupted', 'permission_required', 'permission_denied',
|
|
97
|
+
'borderline_performance', 'experimental_opt_in_required', 'unsupported_capability'];
|
|
98
|
+
const actions = ['proceed', 'retry', 'authenticate', 'obtain_authorization', 'correct_configuration',
|
|
99
|
+
'supply_policy', 'reduce_workload', 'collect_evidence', 'inspect_backend', 'release_session',
|
|
100
|
+
'restore_input', 'grant_permission', 'return_to_foreground', 'resolve_setup',
|
|
101
|
+
'enable_experimental', 'choose_supported_workload'];
|
|
102
|
+
const phases = ['preparing', 'running', 'cleaning_up', 'finalizing'];
|
|
103
|
+
const scopes = ['reference_frame', 'camera_paced_reference_frame'];
|
|
104
|
+
const permissions = ['unknown', 'granted', 'denied'];
|
|
105
|
+
const booleanFields = new Set(['output_required', 'retryable', 'message_truncated', 'available', 'has_deadline', 'phases_available', 'timing_available',
|
|
106
|
+
'authorization_complete', 'backend_coverage_complete', 'input_validated',
|
|
107
|
+
'camera_geometry_changed', 'workload_completed', 'early_completion_requested']);
|
|
108
|
+
function fields(object) {
|
|
109
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [
|
|
110
|
+
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
|
|
111
|
+
booleanFields.has(key) ? !!value : typeof value === 'bigint' ? Number(value) : value,
|
|
112
|
+
]));
|
|
113
|
+
}
|
|
114
|
+
function array(pointer, type, length) {
|
|
115
|
+
return Number(length) ? Array.from(koffi.decode(pointer, koffi.array(type, Number(length)))) : [];
|
|
116
|
+
}
|
|
117
|
+
function policyFromNative(p) {
|
|
118
|
+
const { metrics, metrics_len, early_window_us, early_stable_windows,
|
|
119
|
+
early_minimum_headroom, ...rest } = p;
|
|
120
|
+
return { ...fields(rest),
|
|
121
|
+
metrics: array(metrics, 'smartspectra_readiness_requirement_t', metrics_len).map(fields),
|
|
122
|
+
earlyCompletion: { windowUs: Number(early_window_us), stableWindows: early_stable_windows,
|
|
123
|
+
minimumHeadroom: early_minimum_headroom } };
|
|
124
|
+
}
|
|
125
|
+
function freeze(value) {
|
|
126
|
+
if (value && typeof value === 'object') {
|
|
127
|
+
Object.values(value).forEach(freeze);
|
|
128
|
+
Object.freeze(value);
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function resultFromNative(r) {
|
|
133
|
+
return freeze({ verdict: verdicts[r.verdict], reason: r.reason, diagnostics: r.diagnostics });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function number(value, fallback, name, integer = false, min = -Infinity, max = Infinity) {
|
|
137
|
+
const result = value === undefined ? fallback : value;
|
|
138
|
+
if (typeof result !== 'number' || !Number.isFinite(result) ||
|
|
139
|
+
(integer && !Number.isSafeInteger(result)) || result < min || result > max)
|
|
140
|
+
throw new TypeError(`${name} must be a finite ${integer ? 'integer' : 'number'} in range.`);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
const int = (v, d, n) => number(v, d, n, true, -2147483648, 2147483647);
|
|
144
|
+
const us = (v, d, n) => number(v, d, n, true, 0, Number.MAX_SAFE_INTEGER);
|
|
145
|
+
function string(value, fallback, name) {
|
|
146
|
+
const result = value === undefined ? fallback : value;
|
|
147
|
+
if (typeof result !== 'string' || result.includes('\0')) throw new TypeError(`${name} must be a string without NUL.`);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
function flag(value, fallback, name) {
|
|
151
|
+
if (value === undefined) return +fallback;
|
|
152
|
+
if (typeof value !== 'boolean') throw new TypeError(`${name} must be boolean.`);
|
|
153
|
+
return +value;
|
|
154
|
+
}
|
|
155
|
+
function choice(value, fallback, choices, name) {
|
|
156
|
+
const index = choices.indexOf(value ?? fallback);
|
|
157
|
+
if (index < 0) throw new TypeError(`Unsupported ${name}.`);
|
|
158
|
+
return index;
|
|
159
|
+
}
|
|
160
|
+
function policyToNative(p = {}) {
|
|
161
|
+
const early = p.earlyCompletion ?? {};
|
|
162
|
+
if (!Array.isArray(p.metrics ?? [])) throw new TypeError('policy.metrics must be an array.');
|
|
163
|
+
return {
|
|
164
|
+
version: string(p.version, '', 'policy.version'), rationale: string(p.rationale, '', 'policy.rationale'),
|
|
165
|
+
input_mode: string(p.inputMode, '', 'policy.inputMode'),
|
|
166
|
+
frame_width: int(p.frameWidth, 0, 'policy.frameWidth'), frame_height: int(p.frameHeight, 0, 'policy.frameHeight'),
|
|
167
|
+
target_fps: number(p.targetFps, 0, 'policy.targetFps'),
|
|
168
|
+
minimum_throughput_fps: number(p.minimumThroughputFps, 0, 'policy.minimumThroughputFps'),
|
|
169
|
+
minimum_duration_us: us(p.minimumDurationUs, 0, 'policy.minimumDurationUs'),
|
|
170
|
+
metrics: (p.metrics ?? []).map(m => ({ metric_id: int(m.metricId, 0, 'metricId'),
|
|
171
|
+
minimum_samples: us(m.minimumSamples, 2, 'minimumSamples'),
|
|
172
|
+
minimum_output_hz: number(m.minimumOutputHz, 0, 'minimumOutputHz'),
|
|
173
|
+
output_required: flag(m.outputRequired, true, 'outputRequired') })),
|
|
174
|
+
metrics_len: (p.metrics ?? []).length,
|
|
175
|
+
early_window_us: us(early.windowUs, 0, 'earlyCompletion.windowUs'),
|
|
176
|
+
early_stable_windows: number(early.stableWindows, 0, 'earlyCompletion.stableWindows', true, 0, 4294967295),
|
|
177
|
+
early_minimum_headroom: number(early.minimumHeadroom, 0, 'earlyCompletion.minimumHeadroom'),
|
|
178
|
+
uncertainty_fraction: number(p.uncertaintyFraction, 0, 'policy.uncertaintyFraction'),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function requestToNative(request) {
|
|
182
|
+
const frame = request.referenceBgrFrame ?? Buffer.alloc(0);
|
|
183
|
+
if (!(frame instanceof Uint8Array)) throw new TypeError('referenceBgrFrame must be a Buffer or Uint8Array.');
|
|
184
|
+
const camera = request.camera ?? {};
|
|
185
|
+
return {
|
|
186
|
+
struct_size: koffi.sizeof('smartspectra_readiness_request_t'),
|
|
187
|
+
reserved_opt_in: 0,
|
|
188
|
+
authentication_prepared: flag(request.authenticationPrepared, false, 'authenticationPrepared'),
|
|
189
|
+
scope: choice(request.scope, 'camera_paced_reference_frame', scopes, 'scope'),
|
|
190
|
+
camera_permission: choice(request.cameraPermission, 'unknown', permissions, 'cameraPermission'),
|
|
191
|
+
camera_index: int(camera.deviceIndex, 0, 'camera.deviceIndex'),
|
|
192
|
+
camera_width: int(camera.preferredWidth, 1280, 'camera.preferredWidth'),
|
|
193
|
+
camera_height: int(camera.preferredHeight, 720, 'camera.preferredHeight'),
|
|
194
|
+
camera_fps: int(camera.preferredFps, 30, 'camera.preferredFps'),
|
|
195
|
+
frame_width: int(request.frameWidth, 1280, 'frameWidth'),
|
|
196
|
+
frame_height: int(request.frameHeight, 720, 'frameHeight'),
|
|
197
|
+
target_fps: number(request.targetFps, 30, 'targetFps'),
|
|
198
|
+
duration_us: us(request.durationUs, 30000000, 'durationUs'),
|
|
199
|
+
maximum_wait_us: us(request.maximumWaitUs, 40000000, 'maximumWaitUs'),
|
|
200
|
+
cleanup_reserve_us: us(request.cleanupReserveUs, 5000000, 'cleanupReserveUs'),
|
|
201
|
+
reference_bgr_frame: frame, reference_bgr_frame_len: frame.byteLength,
|
|
202
|
+
policy: policyToNative(request.policy),
|
|
203
|
+
camera_selection_kind: request.cameraSelection ? choice(request.cameraSelection.kind, '', ['default', 'front', 'back', 'byId'], 'cameraSelection.kind') : -1,
|
|
204
|
+
camera_selection_id: request.cameraSelection?.id ?? null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function configurationToNative(config) {
|
|
208
|
+
const metrics = config.requestedMetrics ?? [];
|
|
209
|
+
if (!Array.isArray(metrics)) throw new TypeError('requestedMetrics must be an array.');
|
|
210
|
+
const requested = Int32Array.from(metrics.map(m => int(m, 0, 'requested metric')));
|
|
211
|
+
return { api_key: string(config.apiKey, '', 'apiKey'), requested_metrics: requested,
|
|
212
|
+
requested_metrics_len: requested.length,
|
|
213
|
+
enable_accumulated_output: flag(config.enableAccumulatedOutput, false, 'enableAccumulatedOutput'),
|
|
214
|
+
log_level: config.logLevel === undefined ? 0 : number(config.logLevel, 0, 'logLevel', true, 0, 4) + 1,
|
|
215
|
+
struct_size: koffi.sizeof('smartspectra_config_t'),
|
|
216
|
+
disable_telemetry: 1 - flag(config.enableTelemetry, true, 'enableTelemetry'), reserved0: 0 };
|
|
217
|
+
}
|
|
218
|
+
function progressFromNative(n, handle) {
|
|
219
|
+
const progress = {};
|
|
220
|
+
if (n.progress(handle, progress) !== 0) throw new Error('Unable to read native readiness progress.');
|
|
221
|
+
return freeze({ ...fields(progress), phase: phases[progress.phase] });
|
|
222
|
+
}
|
|
223
|
+
class DeviceCompatibilityCheck {
|
|
224
|
+
#handle; #native; #progress; #value; #promise;
|
|
225
|
+
constructor(n, handle, signal, finished) {
|
|
226
|
+
this.#native = n;
|
|
227
|
+
this.#handle = handle;
|
|
228
|
+
this.#progress = freeze({ phase: 'preparing', timingAvailable: false, elapsedUs: 0,
|
|
229
|
+
hasDeadline: false, remainingUs: 0 });
|
|
230
|
+
const abort = () => this.cancel();
|
|
231
|
+
let timer;
|
|
232
|
+
this.#promise = new Promise((resolve, reject) => {
|
|
233
|
+
const tick = () => {
|
|
234
|
+
try {
|
|
235
|
+
this.#progress = progressFromNative(n, handle);
|
|
236
|
+
const result = {};
|
|
237
|
+
const status = n.poll(handle, result);
|
|
238
|
+
if (status === 0) return;
|
|
239
|
+
if (status !== 1) throw new Error('Unable to read native readiness result.');
|
|
240
|
+
this.#value = resultFromNative(result);
|
|
241
|
+
resolve(this.#value);
|
|
242
|
+
} catch (error) { reject(error); }
|
|
243
|
+
clearInterval(timer);
|
|
244
|
+
// A user-overridden listener method must not prevent native
|
|
245
|
+
// handle cleanup or leave close() waiting on an abandoned check.
|
|
246
|
+
try { signal?.removeEventListener('abort', abort); } catch {}
|
|
247
|
+
try { n.free(handle); }
|
|
248
|
+
finally { this.#handle = null; finished(this); }
|
|
249
|
+
};
|
|
250
|
+
// Referenced timer retains the event loop until native cleanup and
|
|
251
|
+
// result copying finish, even if the caller only awaits result.
|
|
252
|
+
timer = setInterval(tick, 25);
|
|
253
|
+
});
|
|
254
|
+
// Keep ignored result promises from becoming unhandled rejections.
|
|
255
|
+
this.#promise.catch(() => {});
|
|
256
|
+
try {
|
|
257
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
258
|
+
if (signal?.aborted) this.cancel();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
clearInterval(timer);
|
|
261
|
+
try { signal?.removeEventListener('abort', abort); } catch {}
|
|
262
|
+
this.#handle = null;
|
|
263
|
+
throw error; // start() still owns the native handle on this path.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
get result() { return this.#promise; }
|
|
267
|
+
poll() { return this.#value; }
|
|
268
|
+
progress() {
|
|
269
|
+
if (this.#handle) this.#progress = progressFromNative(this.#native, this.#handle);
|
|
270
|
+
return this.#progress;
|
|
271
|
+
}
|
|
272
|
+
cancel() { if (this.#handle) this.#native.cancel(this.#handle); }
|
|
273
|
+
applicationBackgrounded() { if (this.#handle) this.#native.background(this.#handle); }
|
|
274
|
+
}
|
|
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 {
|
|
280
|
+
static async run(configuration = {}) {
|
|
281
|
+
const service = new DeviceCompatibilityService();
|
|
282
|
+
try { return await service.check(configuration).result; }
|
|
283
|
+
finally { await service.close(); }
|
|
284
|
+
}
|
|
285
|
+
#native; #handle; #checks = new Set(); #closing;
|
|
286
|
+
constructor() {
|
|
287
|
+
this.#native = getNative();
|
|
288
|
+
ensurePreconfigured();
|
|
289
|
+
this.#handle = this.#native.create();
|
|
290
|
+
if (!this.#handle) throw new Error('Unable to create native readiness service.');
|
|
291
|
+
services.register(this, { n: this.#native, handle: this.#handle }, this);
|
|
292
|
+
}
|
|
293
|
+
reserve() {
|
|
294
|
+
if (!this.#handle || this.#closing) throw new Error('Compatibility service is closed.');
|
|
295
|
+
const result = this.#native.reserve(this.#handle);
|
|
296
|
+
if (result < 0) throw new Error('Unable to reserve SDK state.');
|
|
297
|
+
return result === 1;
|
|
298
|
+
}
|
|
299
|
+
check(configuration, referenceBgrFrame = undefined, workload = 'full', scope = 'camera_paced_reference_frame') {
|
|
300
|
+
const policy = DeviceCompatibilityPolicies.reference720p30(workload, scope);
|
|
301
|
+
return this.start({ ...configuration, requestedMetrics: policy.metrics.map(m => m.metricId) }, {
|
|
302
|
+
referenceBgrFrame, policy, scope, authenticationPrepared: !!configuration?.apiKey,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
start(configuration = {}, request = {}) {
|
|
306
|
+
if (!this.#handle || this.#closing) throw new Error('Readiness service is closed.');
|
|
307
|
+
const signal = request.signal;
|
|
308
|
+
if (signal !== undefined && !(signal instanceof AbortSignal))
|
|
309
|
+
throw new TypeError('signal must be an AbortSignal.');
|
|
310
|
+
if (signal?.aborted) throw new DOMException('Readiness was aborted before submission.', 'AbortError');
|
|
311
|
+
const config = configurationToNative(configuration);
|
|
312
|
+
const input = requestToNative(request);
|
|
313
|
+
// Configuration/request getters can call close() during conversion.
|
|
314
|
+
if (!this.#handle || this.#closing) throw new Error('Readiness service is closed.');
|
|
315
|
+
const handle = this.#native.start(this.#handle, config, input);
|
|
316
|
+
if (!handle) throw new Error('Unable to submit native readiness request.');
|
|
317
|
+
let check;
|
|
318
|
+
try {
|
|
319
|
+
check = new DeviceCompatibilityCheck(this.#native, handle, signal, value => this.#checks.delete(value));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
try { this.#native.cancel(handle); }
|
|
322
|
+
finally { this.#native.free(handle); }
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
this.#checks.add(check);
|
|
326
|
+
return check;
|
|
327
|
+
}
|
|
328
|
+
close() {
|
|
329
|
+
if (this.#closing) return this.#closing;
|
|
330
|
+
const checks = [...this.#checks];
|
|
331
|
+
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;
|
|
334
|
+
this.#handle = null;
|
|
335
|
+
services.unregister(this);
|
|
336
|
+
this.#native.destroy.async(handle, error => error ? reject(error) : resolve());
|
|
337
|
+
}));
|
|
338
|
+
return this.#closing;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const DeviceCompatibilityPolicies = Object.freeze({
|
|
342
|
+
reference720p30(workload = 'full', scope = 'camera_paced_reference_frame') {
|
|
343
|
+
const tier = choice(workload, 'full', ['breathing_pulse_eda', 'breathing_pulse', 'breathing', 'full'], 'workload');
|
|
344
|
+
const policy = {};
|
|
345
|
+
if (getNative().referencePolicy(tier, choice(scope, 'camera_paced_reference_frame', scopes, 'scope'), policy) !== 0) throw new Error('Unable to read native readiness policy.');
|
|
346
|
+
return freeze(policyFromNative(policy));
|
|
347
|
+
},
|
|
348
|
+
fallback(result) {
|
|
349
|
+
const policy = {};
|
|
350
|
+
const status = getNative().fallbackPolicy(policyToNative(result.policy),
|
|
351
|
+
choice(result.qualification.verdict, '', verdicts, 'verdict'),
|
|
352
|
+
choice(result.qualification.reason, '', reasons, 'reason'), policy);
|
|
353
|
+
if (status === 1) return undefined;
|
|
354
|
+
if (status !== 0) throw new Error('Unable to read native fallback policy.');
|
|
355
|
+
return freeze(policyFromNative(policy));
|
|
356
|
+
},
|
|
357
|
+
chestTraceReference720p30() {
|
|
358
|
+
const policy = {};
|
|
359
|
+
if (getNative().policy(policy) !== 0) throw new Error('Unable to read native readiness policy.');
|
|
360
|
+
return freeze(policyFromNative(policy));
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
function deviceCompatibilityAvailable() { try { getNative(); return true; } catch { return false; } }
|
|
364
|
+
module.exports = { DeviceCompatibilityService, DeviceCompatibilityPolicies, deviceCompatibilityAvailable, busyResult };
|
|
365
|
+
|
|
366
|
+
// A rejected busy check has no measurement evidence. Keep the full result shape.
|
|
367
|
+
function busyResult() {
|
|
368
|
+
return freeze({ verdict: 'inconclusive',
|
|
369
|
+
reason: 'The SDK is busy. Wait for the current SDK operation to finish.',
|
|
370
|
+
diagnostics: JSON.stringify({ reason: 'busy', workloadCompleted: false }) });
|
|
371
|
+
}
|
package/js/ffi.js
CHANGED
|
@@ -17,9 +17,9 @@ const fs = require('fs');
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const koffi = require('koffi');
|
|
20
|
-
const {
|
|
20
|
+
const { loadNativeLibrary } = require('./resolve-native');
|
|
21
21
|
|
|
22
|
-
const lib = koffi
|
|
22
|
+
const lib = loadNativeLibrary(koffi);
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Type declarations — mirror smartspectra_capi.h
|
|
@@ -648,4 +648,5 @@ module.exports = {
|
|
|
648
648
|
availableCameras,
|
|
649
649
|
preconfigure,
|
|
650
650
|
ensurePreconfigured,
|
|
651
|
+
readinessLibrary: () => lib,
|
|
651
652
|
};
|
package/js/index.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export declare const SmartSpectraErrorCode: {
|
|
|
76
76
|
readonly kFrameConversionFailed: 9;
|
|
77
77
|
readonly kNonMonotonicTimestamp: 10;
|
|
78
78
|
readonly kTimestampGap: 11;
|
|
79
|
+
/** Requested metric is not authorized for this API key. */
|
|
80
|
+
readonly kMetricNotAuthorized: 13;
|
|
79
81
|
};
|
|
80
82
|
export type SmartSpectraErrorCodeValue = typeof SmartSpectraErrorCode[keyof typeof SmartSpectraErrorCode];
|
|
81
83
|
|
|
@@ -212,6 +214,10 @@ export interface CameraOptions {
|
|
|
212
214
|
* ```
|
|
213
215
|
*/
|
|
214
216
|
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;
|
|
215
221
|
/** SDK package version. */
|
|
216
222
|
static readonly version: string;
|
|
217
223
|
|
|
@@ -367,3 +373,13 @@ export declare function setMetricsClass(cls: unknown): void;
|
|
|
367
373
|
* been registered via `setMetricsClass`, otherwise the raw Buffer.
|
|
368
374
|
*/
|
|
369
375
|
export declare function decodeMetrics(buf: Buffer): unknown;
|
|
376
|
+
|
|
377
|
+
/** Device Compatibility. Pin the JS and native SDK versions together. */
|
|
378
|
+
/** Outcome for the configured SDK workload. */
|
|
379
|
+
export type DeviceCompatibilityVerdict = 'ready' | 'limited' | 'blocked' | 'inconclusive';
|
|
380
|
+
export interface DeviceCompatibilityResult {
|
|
381
|
+
readonly verdict: DeviceCompatibilityVerdict;
|
|
382
|
+
readonly reason: string;
|
|
383
|
+
/** Native JSON support report; detailed fields are not a stable typed API. */
|
|
384
|
+
readonly diagnostics: string;
|
|
385
|
+
}
|
package/js/resolve-native.js
CHANGED
|
@@ -113,7 +113,37 @@ function resolveNativeLibrary() {
|
|
|
113
113
|
return libPath;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Koffi tries LoadLibraryW before its adjacent-DLL fallback. On Windows that
|
|
117
|
+
// can successfully bind a packaged shim to an unrelated SDK DLL on PATH.
|
|
118
|
+
// Preload the package with a scoped dependency search; do not change the
|
|
119
|
+
// process-wide DLL directory or the explicit development override's behavior.
|
|
120
|
+
function loadNativeLibrary(koffi) {
|
|
121
|
+
const filename = resolveNativeLibrary();
|
|
122
|
+
if (process.platform !== 'win32' || process.env.SMARTSPECTRA_CAPI_PATH) {
|
|
123
|
+
return koffi.load(filename);
|
|
124
|
+
}
|
|
125
|
+
const kernel32 = koffi.load('kernel32.dll');
|
|
126
|
+
try {
|
|
127
|
+
const load = kernel32.func('__stdcall', 'LoadLibraryExW', 'void *', ['str16', 'void *', 'uint32_t']);
|
|
128
|
+
const free = kernel32.func('__stdcall', 'FreeLibrary', 'int', ['void *']);
|
|
129
|
+
// LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
|
|
130
|
+
const handle = load(path.resolve(filename), null, 0x00000100 | 0x00001000);
|
|
131
|
+
if (!handle) {
|
|
132
|
+
throw new Error(`@smartspectra/node-sdk: could not load packaged runtime ${filename} and its adjacent DLLs`);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Koffi takes its own module reference and owns its lifetime from here.
|
|
136
|
+
return koffi.load(filename);
|
|
137
|
+
} finally {
|
|
138
|
+
free(handle);
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
kernel32.unload();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
module.exports = {
|
|
146
|
+
loadNativeLibrary,
|
|
117
147
|
resolveNativeLibrary,
|
|
118
148
|
// Exposed for diagnostics and tests.
|
|
119
149
|
LIB_BASENAME,
|
package/js/smartspectra.js
CHANGED
|
@@ -39,6 +39,8 @@ const SUPPORTED_EVENTS = new Set([
|
|
|
39
39
|
// FrameTransform object declared later.
|
|
40
40
|
const DEFAULT_FRAME_TRANSFORM = 0;
|
|
41
41
|
|
|
42
|
+
let compatibilityInFlight = false;
|
|
43
|
+
|
|
42
44
|
class SmartSpectraSDK {
|
|
43
45
|
/** SDK package version (matches `package.json#version`). */
|
|
44
46
|
static get version() { return PACKAGE_VERSION; }
|
|
@@ -71,6 +73,8 @@ class SmartSpectraSDK {
|
|
|
71
73
|
this._source = null;
|
|
72
74
|
this._explicitCameraSelection = false;
|
|
73
75
|
this._session = null; // lazy: created on start()
|
|
76
|
+
this._compatibilityInFlight = false;
|
|
77
|
+
|
|
74
78
|
this._destroyPromise = null;
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -99,6 +103,7 @@ class SmartSpectraSDK {
|
|
|
99
103
|
* Spatial transform applied to every pushed frame.
|
|
100
104
|
*/
|
|
101
105
|
useCustomInput(frameTransform = DEFAULT_FRAME_TRANSFORM) {
|
|
106
|
+
this._requireCompatibilityIdle();
|
|
102
107
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
103
108
|
this._source = { kind: 'custom', frameTransform: frameTransform | 0 };
|
|
104
109
|
return this;
|
|
@@ -119,6 +124,7 @@ class SmartSpectraSDK {
|
|
|
119
124
|
* getUserMedia — use the renderer SDK's useMediaStream() there instead.
|
|
120
125
|
*/
|
|
121
126
|
useCamera(selection = null, options = {}) {
|
|
127
|
+
this._requireCompatibilityIdle();
|
|
122
128
|
const isSelection = selection !== null && typeof selection === 'object' && 'kind' in selection;
|
|
123
129
|
if (!isSelection && arguments.length < 2) {
|
|
124
130
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
@@ -145,6 +151,7 @@ class SmartSpectraSDK {
|
|
|
145
151
|
* { timestampsPath?, interframeDelayMs?, startOffsetMs?, maxDurationMs?, frameTransform? }
|
|
146
152
|
*/
|
|
147
153
|
useFile(videoPath, options = null) {
|
|
154
|
+
this._requireCompatibilityIdle();
|
|
148
155
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
149
156
|
if (typeof videoPath !== 'string' || !videoPath) {
|
|
150
157
|
throw new TypeError('useFile: videoPath must be a non-empty string');
|
|
@@ -160,6 +167,7 @@ class SmartSpectraSDK {
|
|
|
160
167
|
* useCustomInput(), useCamera(), or useFile() first.
|
|
161
168
|
*/
|
|
162
169
|
start() {
|
|
170
|
+
this._requireCompatibilityIdle();
|
|
163
171
|
if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
|
|
164
172
|
const source = this._source;
|
|
165
173
|
if (!source) {
|
|
@@ -207,6 +215,7 @@ class SmartSpectraSDK {
|
|
|
207
215
|
* Typed selection requires any pending stopAsync() to settle before reset.
|
|
208
216
|
*/
|
|
209
217
|
reset() {
|
|
218
|
+
this._requireCompatibilityIdle();
|
|
210
219
|
if (!this._session) return;
|
|
211
220
|
this._session.reset(this._explicitCameraSelection);
|
|
212
221
|
if (!this._explicitCameraSelection) this._source = null;
|
|
@@ -264,18 +273,93 @@ class SmartSpectraSDK {
|
|
|
264
273
|
* replacement session, since SDK state is process-global.
|
|
265
274
|
*/
|
|
266
275
|
destroy() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
try {
|
|
277
|
+
if (this._destroyPromise) return this._destroyPromise;
|
|
278
|
+
// 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();
|
|
282
|
+
this._session = null;
|
|
283
|
+
this._source = null;
|
|
284
|
+
this._destroyPromise = Promise.resolve(teardown).finally(() => { this._destroyPromise = null; });
|
|
285
|
+
return this._destroyPromise;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return Promise.reject(error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async isDeviceCompatible() {
|
|
292
|
+
if (this._compatibilityResultPending) return 'inconclusive';
|
|
293
|
+
this._compatibilityResult = undefined;
|
|
294
|
+
this._compatibilityResultPending = true;
|
|
295
|
+
try {
|
|
296
|
+
const result = await this._runDeviceCompatibility();
|
|
297
|
+
this._compatibilityResult = Object.freeze({ ...result });
|
|
298
|
+
return result.verdict;
|
|
299
|
+
} finally { this._compatibilityResultPending = false; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getDeviceCompatibilityResult() { return this._compatibilityResult; }
|
|
303
|
+
|
|
304
|
+
async _runDeviceCompatibility() {
|
|
305
|
+
const { DeviceCompatibilityService, DeviceCompatibilityPolicies } = require('./device-compatibility');
|
|
306
|
+
const selected = { ...this._options,
|
|
307
|
+
requestedMetrics: [...(this._options.requestedMetrics?.length
|
|
308
|
+
? this._options.requestedMetrics : defaultSupportedMetrics)] };
|
|
309
|
+
const source = this._source;
|
|
310
|
+
const preflight = (code, reason) => Object.freeze({ verdict: 'inconclusive', reason,
|
|
311
|
+
diagnostics: JSON.stringify({ sdkVersion: PACKAGE_VERSION, schemaVersion: 1, reason: code,
|
|
312
|
+
reasonName: code, verdictName: 'inconclusive',
|
|
313
|
+
requestedMetrics: selected.requestedMetrics, input: source?.kind ?? 'unconfigured',
|
|
314
|
+
workloadCompleted: false, observationUs: 0, framesAttempted: 0,
|
|
315
|
+
performanceChecks: [], metricOutputs: [], timing: { available: false } }) });
|
|
316
|
+
const busy = () => preflight('busy', 'The SDK is busy. Wait for the current SDK operation to finish.');
|
|
317
|
+
if (compatibilityInFlight || this._compatibilityInFlight || this._destroyPromise)
|
|
318
|
+
return busy();
|
|
319
|
+
const status = this.processingStatus;
|
|
320
|
+
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.');
|
|
322
|
+
if (![ProcessingStatus.kUninitialized, ProcessingStatus.kIdle].includes(status)) return busy();
|
|
323
|
+
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;
|
|
327
|
+
let service;
|
|
328
|
+
try {
|
|
329
|
+
const capture = source.options ?? {};
|
|
330
|
+
const request = { authenticationPrepared: !!selected.apiKey,
|
|
331
|
+
cameraSelection: source.selection && { ...source.selection },
|
|
332
|
+
camera: { deviceIndex: capture.deviceIndex ?? 0,
|
|
333
|
+
preferredWidth: capture.width || 1280, preferredHeight: capture.height || 720,
|
|
334
|
+
preferredFps: capture.fps || 30 },
|
|
335
|
+
frameWidth: capture.width || 1280, frameHeight: capture.height || 720, targetFps: capture.fps || 30 };
|
|
336
|
+
const policy = DeviceCompatibilityPolicies.reference720p30('full');
|
|
337
|
+
const metrics = new Set(selected.requestedMetrics);
|
|
338
|
+
request.policy = { ...policy, frameWidth: request.frameWidth, frameHeight: request.frameHeight,
|
|
339
|
+
targetFps: request.targetFps, metrics: policy.metrics.filter(metric => metrics.has(metric.metricId)) };
|
|
340
|
+
if (this._session) {
|
|
341
|
+
await this._session.destroy();
|
|
342
|
+
this._session = null;
|
|
343
|
+
}
|
|
344
|
+
service = new DeviceCompatibilityService();
|
|
345
|
+
if (!service.reserve()) return busy();
|
|
346
|
+
return await service.start(selected, request).result;
|
|
347
|
+
} finally {
|
|
348
|
+
try { if (service) await service.close(); }
|
|
349
|
+
finally { this._compatibilityInFlight = false; compatibilityInFlight = false; }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_requireCompatibilityIdle() {
|
|
354
|
+
if (compatibilityInFlight || this._compatibilityInFlight) throw Object.assign(new Error('Device compatibility is running'), {
|
|
355
|
+
code: SmartSpectraErrorCode.kInvalidState, retryable: true,
|
|
356
|
+
});
|
|
274
357
|
}
|
|
275
358
|
|
|
276
359
|
// ---------- internal ----------------------------------------------------
|
|
277
360
|
|
|
278
361
|
_requireSourceSelectionAllowed() {
|
|
362
|
+
this._requireCompatibilityIdle();
|
|
279
363
|
if (this._destroyPromise) {
|
|
280
364
|
throw Object.assign(new Error('SmartSpectra is shutting down'), {
|
|
281
365
|
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.3",
|
|
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,13 +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
40
|
"README.md"
|
|
39
41
|
],
|
|
40
42
|
"scripts": {
|
|
41
43
|
"prepare": "node scripts/gen-proto.cjs",
|
|
42
44
|
"build:proto": "node scripts/gen-proto.cjs",
|
|
43
|
-
"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/smoke.js",
|
|
44
|
-
"test:types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext test/camera-selection-types.ts test/renderer-types.ts",
|
|
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
47
|
"test:models-allowlist": "node test/models-allowlist.js"
|
|
46
48
|
},
|
|
47
49
|
"keywords": [
|
|
@@ -71,10 +73,10 @@
|
|
|
71
73
|
"dependencies": {
|
|
72
74
|
"koffi": "^2.10.0",
|
|
73
75
|
"protobufjs": "^7.6.5",
|
|
74
|
-
"@smartspectra/node-sdk-linux-x64": "3.5.0-rc.
|
|
75
|
-
"@smartspectra/node-sdk-linux-arm64": "3.5.0-rc.
|
|
76
|
-
"@smartspectra/node-sdk-darwin-arm64": "3.5.0-rc.
|
|
77
|
-
"@smartspectra/node-sdk-win32-x64": "3.5.0-rc.
|
|
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"
|
|
78
80
|
},
|
|
79
81
|
"devDependencies": {
|
|
80
82
|
"@types/node": "^24.0.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
|
+
'use strict';
|
|
4
|
+
const { SmartSpectraSDK } = require('@smartspectra/node-sdk');
|
|
5
|
+
const { writeFileSync } = require('node:fs');
|
|
6
|
+
(async () => {
|
|
7
|
+
const sdk = new SmartSpectraSDK({ apiKey: process.env.PHYSIOLOGY_API_KEY });
|
|
8
|
+
sdk.useCamera();
|
|
9
|
+
try {
|
|
10
|
+
await sdk.isDeviceCompatible();
|
|
11
|
+
const result = sdk.getDeviceCompatibilityResult();
|
|
12
|
+
console.log(result.verdict, result.reason);
|
|
13
|
+
if (process.argv[2]) writeFileSync(process.argv[2], result.diagnostics, { flag: 'wx', mode: 0o600 });
|
|
14
|
+
process.exitCode = result.verdict === 'ready' ? 0 : 2;
|
|
15
|
+
} finally { await sdk.destroy(); }
|
|
16
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
|
+
'use strict';
|
|
4
|
+
function present(result) {
|
|
5
|
+
return { title: result.verdict, reason: result.reason, action: result.reason,
|
|
6
|
+
canProceed: result.verdict === 'ready' };
|
|
7
|
+
}
|
|
8
|
+
function supportReport(result) { return result.diagnostics; }
|
|
9
|
+
module.exports = { present, supportReport };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
3
|
+
'use strict';
|
|
4
|
+
const { SmartSpectraSDK } = require('@smartspectra/node-sdk');
|
|
5
|
+
async function main() {
|
|
6
|
+
const sdk = new SmartSpectraSDK({ apiKey: process.env.PHYSIOLOGY_API_KEY });
|
|
7
|
+
sdk.useCamera();
|
|
8
|
+
try {
|
|
9
|
+
const verdict = await sdk.isDeviceCompatible();
|
|
10
|
+
console.log(verdict);
|
|
11
|
+
process.exitCode = verdict === 'ready' ? 0 : 2;
|
|
12
|
+
} finally { await sdk.destroy(); }
|
|
13
|
+
}
|
|
14
|
+
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|