@makeitnow/jumpitnow 0.0.0-stage → 0.1.0-alpha.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +367 -2
  3. package/dist/cjs/index.d.ts +6 -0
  4. package/dist/cjs/index.js +29 -0
  5. package/dist/cjs/internal/errors.d.ts +15 -0
  6. package/dist/cjs/internal/errors.js +44 -0
  7. package/dist/cjs/internal/gateway.d.ts +41 -0
  8. package/dist/cjs/internal/gateway.js +586 -0
  9. package/dist/cjs/internal/link.d.ts +13 -0
  10. package/dist/cjs/internal/link.js +111 -0
  11. package/dist/cjs/internal/protocol.d.ts +29 -0
  12. package/dist/cjs/internal/protocol.js +38 -0
  13. package/dist/cjs/internal/simulator.d.ts +15 -0
  14. package/dist/cjs/internal/simulator.js +117 -0
  15. package/dist/cjs/package.json +1 -0
  16. package/dist/cjs/types.d.ts +182 -0
  17. package/dist/cjs/types.js +2 -0
  18. package/dist/esm/index.d.ts +6 -0
  19. package/dist/esm/index.js +22 -0
  20. package/dist/esm/internal/errors.d.ts +15 -0
  21. package/dist/esm/internal/errors.js +37 -0
  22. package/dist/esm/internal/gateway.d.ts +41 -0
  23. package/dist/esm/internal/gateway.js +582 -0
  24. package/dist/esm/internal/link.d.ts +13 -0
  25. package/dist/esm/internal/link.js +106 -0
  26. package/dist/esm/internal/protocol.d.ts +29 -0
  27. package/dist/esm/internal/protocol.js +32 -0
  28. package/dist/esm/internal/simulator.d.ts +15 -0
  29. package/dist/esm/internal/simulator.js +113 -0
  30. package/dist/esm/types.d.ts +182 -0
  31. package/dist/esm/types.js +1 -0
  32. package/docs/API.md +106 -0
  33. package/docs/COMPATIBILITY.md +11 -0
  34. package/docs/SECURITY.md +17 -0
  35. package/docs/index.html +30 -0
  36. package/examples/browser/app.js +104 -0
  37. package/examples/browser/index.html +37 -0
  38. package/examples/react/GatewayPanel.tsx +66 -0
  39. package/package.json +55 -4
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BrowserLink = void 0;
4
+ exports.getSupport = getSupport;
5
+ const errors_js_1 = require("./errors.js");
6
+ const provider = () => globalThis.navigator?.serial;
7
+ function getSupport() {
8
+ const secureContext = globalThis.isSecureContext === true;
9
+ const webSerial = !!provider();
10
+ return { supported: secureContext && webSerial, secureContext, webSerial,
11
+ reason: !secureContext ? 'INSECURE_CONTEXT' : !webSerial ? 'UNSUPPORTED_BROWSER' : null };
12
+ }
13
+ class BrowserLink {
14
+ #port = null;
15
+ #reader = null;
16
+ #writer = null;
17
+ #reading = null;
18
+ #closing = false;
19
+ #openedByUs = false;
20
+ async open(line, lost) {
21
+ const support = getSupport();
22
+ if (!support.supported)
23
+ throw new errors_js_1.SDKError(support.reason);
24
+ this.#closing = false;
25
+ try {
26
+ this.#port = await provider().requestPort();
27
+ }
28
+ catch (e) {
29
+ throw new errors_js_1.SDKError(e?.name === 'NotFoundError' ? 'USER_CANCELLED' : 'PORT_UNAVAILABLE');
30
+ }
31
+ try {
32
+ await this.#port.open({ baudRate: 115200 });
33
+ this.#openedByUs = true;
34
+ if (!this.#port.readable || !this.#port.writable)
35
+ throw new errors_js_1.SDKError('PORT_UNAVAILABLE');
36
+ this.#reader = this.#port.readable.getReader();
37
+ this.#writer = this.#port.writable.getWriter();
38
+ this.#reading = this.#read(line, lost);
39
+ }
40
+ catch {
41
+ await this.close().catch(() => { });
42
+ throw new errors_js_1.SDKError('PORT_UNAVAILABLE');
43
+ }
44
+ }
45
+ async #read(line, lost) {
46
+ const reader = this.#reader;
47
+ const decoder = new TextDecoder();
48
+ let buffer = '';
49
+ let discarding = false;
50
+ try {
51
+ while (!this.#closing) {
52
+ const { value, done } = await reader.read();
53
+ if (done)
54
+ break;
55
+ const text = decoder.decode(value, { stream: true });
56
+ for (const ch of text) {
57
+ if (ch === '\n') {
58
+ if (!discarding)
59
+ line(buffer.replace(/\r$/, ''));
60
+ buffer = '';
61
+ discarding = false;
62
+ }
63
+ else if (!discarding) {
64
+ buffer += ch;
65
+ if (buffer.length > 4096) {
66
+ buffer = '';
67
+ discarding = true;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ catch { /* The public error deliberately omits OS and raw device details. */ }
74
+ finally {
75
+ reader.releaseLock();
76
+ this.#reader = null;
77
+ if (!this.#closing)
78
+ lost();
79
+ }
80
+ }
81
+ async write(command) {
82
+ if (!this.#writer || this.#closing)
83
+ throw new errors_js_1.SDKError('CONNECTION_LOST');
84
+ try {
85
+ await this.#writer.write(new TextEncoder().encode(`${command}\n`));
86
+ }
87
+ catch {
88
+ throw new errors_js_1.SDKError('CONNECTION_LOST');
89
+ }
90
+ }
91
+ async close() {
92
+ this.#closing = true;
93
+ try {
94
+ await this.#reader?.cancel().catch(() => { });
95
+ await this.#reading;
96
+ if (this.#writer) {
97
+ await this.#writer.abort().catch(() => { });
98
+ this.#writer.releaseLock();
99
+ this.#writer = null;
100
+ }
101
+ if (this.#port && this.#openedByUs)
102
+ await this.#port.close();
103
+ this.#port = null;
104
+ this.#openedByUs = false;
105
+ }
106
+ catch (error) {
107
+ throw (0, errors_js_1.asError)(error, 'PORT_UNAVAILABLE');
108
+ }
109
+ }
110
+ }
111
+ exports.BrowserLink = BrowserLink;
@@ -0,0 +1,29 @@
1
+ export type Frame = {
2
+ kind: 'help';
3
+ text: string;
4
+ } | {
5
+ kind: 'list';
6
+ phase: 'BEGIN' | 'END';
7
+ } | {
8
+ kind: 'device';
9
+ number: number;
10
+ endpoint: string;
11
+ battery: number;
12
+ version: string;
13
+ } | {
14
+ kind: 'tx';
15
+ target: string;
16
+ endpoint: string;
17
+ bytes: number[];
18
+ ok: boolean;
19
+ } | {
20
+ kind: 'rx';
21
+ number: number;
22
+ endpoint: string;
23
+ bytes: number[];
24
+ } | {
25
+ kind: 'error';
26
+ };
27
+ export declare function bytes(hex: string): number[] | null;
28
+ export declare function parse(line: string): Frame | null;
29
+ export declare const u16: (data: number[], offset: number) => number;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.u16 = void 0;
4
+ exports.bytes = bytes;
5
+ exports.parse = parse;
6
+ function bytes(hex) {
7
+ if (!/^(?:[0-9a-f]{2}){1,48}$/i.test(hex))
8
+ return null;
9
+ return hex.match(/../g).map(v => Number.parseInt(v, 16));
10
+ }
11
+ const number = (s, max = 255) => s !== undefined && /^\d+$/.test(s) && Number(s) <= max ? Number(s) : null;
12
+ const endpoint = (s) => !!s && /^[a-z0-9:.%-]{1,63}$/i.test(s);
13
+ function parse(line) {
14
+ const p = line.trim().split(',');
15
+ if (line.startsWith('HELP'))
16
+ return { kind: 'help', text: line };
17
+ if (p[0] === 'LIST' && (p[1] === 'BEGIN' || p[1] === 'END'))
18
+ return { kind: 'list', phase: p[1] };
19
+ if (p[0] === 'DEVICE' && p.length === 10 && number(p[1], 40) !== null && number(p[2]) !== null && endpoint(p[3]) && /^\d+\.\d+\.\d+$/.test(p[8]) && /^-?\d+$/.test(p[5])) {
20
+ return { kind: 'device', number: Number(p[2]), endpoint: p[3], battery: Number(p[5]), version: p[8] };
21
+ }
22
+ if (p[0] === 'TXB' && p.length === 5 && endpoint(p[2]) && p[1] && (p[4] === 'OK' || p[4] === 'FAIL')) {
23
+ const b = bytes(p[3]);
24
+ if (b)
25
+ return { kind: 'tx', target: p[1], endpoint: p[2], bytes: b, ok: p[4] === 'OK' };
26
+ }
27
+ if (p[0] === 'RXB' && p.length === 4 && number(p[1]) !== null && endpoint(p[2])) {
28
+ const b = bytes(p[3]);
29
+ if (b && b.length >= 2 && b[1] === Number(p[1]))
30
+ return { kind: 'rx', number: Number(p[1]), endpoint: p[2], bytes: b };
31
+ }
32
+ // Normalized measurement lines have no unambiguous endpoint. Use the addressed response above only.
33
+ if (p[0] === 'ERR' && p[1] !== 'DEVICE')
34
+ return { kind: 'error' };
35
+ return null;
36
+ }
37
+ const u16 = (data, offset) => data[offset] | (data[offset + 1] << 8);
38
+ exports.u16 = u16;
@@ -0,0 +1,15 @@
1
+ import type { SimulationOptions } from '../types.js';
2
+ import type { Link } from './link.js';
3
+ export declare class SimulationLink implements Link {
4
+ #private;
5
+ constructor(options: SimulationOptions);
6
+ open(line: (line: string) => void, lost: () => void): Promise<void>;
7
+ write(command: string): Promise<void>;
8
+ controls(): Readonly<{
9
+ disconnectDevice: (displayNumber: number) => void;
10
+ reconnectDevice: (displayNumber: number) => void;
11
+ setStartFailure: (displayNumber: number, fail: boolean) => void;
12
+ dropConnection: () => void;
13
+ }>;
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimulationLink = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const codes = { free: 16, count: 17, time: 18, start: 19, stop: 20, getData: 21, getBattery: 6, vibrate: 49, buzzer: 48, showId: 51 };
6
+ const hex = (values) => values.map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase();
7
+ const word = (n) => [n & 255, (n >> 8) & 255];
8
+ class SimulationLink {
9
+ #line = null;
10
+ #lost = null;
11
+ #units;
12
+ #delay;
13
+ #failures;
14
+ #seed;
15
+ #timers = new Set();
16
+ constructor(options) {
17
+ if (!options || typeof options !== 'object' || Array.isArray(options))
18
+ throw new errors_js_1.SDKError('INVALID_ARGUMENT');
19
+ const count = (0, errors_js_1.integer)(options.deviceCount ?? 3, 1, 40);
20
+ this.#delay = (0, errors_js_1.integer)(options.responseDelayMs ?? 10, 0, 30000);
21
+ this.#seed = (0, errors_js_1.integer)(options.seed ?? 1, 0, 0xffffffff);
22
+ if (options.startFailures && !Array.isArray(options.startFailures))
23
+ throw new errors_js_1.SDKError('INVALID_ARGUMENT');
24
+ this.#failures = new Set((options.startFailures ?? []).map(n => (0, errors_js_1.integer)(n, 1, count)));
25
+ this.#units = Array.from({ length: count }, (_, i) => ({ number: i + 1, online: true, mode: 0, running: false, count: 0, ticks: 0, target: 0 }));
26
+ }
27
+ async open(line, lost) { this.#line = line; this.#lost = lost; }
28
+ #send(lines) {
29
+ const timer = setTimeout(() => { this.#timers.delete(timer); for (const line of lines)
30
+ this.#line?.(line); }, this.#delay);
31
+ this.#timers.add(timer);
32
+ }
33
+ async write(command) {
34
+ if (!this.#line)
35
+ throw new errors_js_1.SDKError('CONNECTION_LOST');
36
+ if (command === 'help') {
37
+ this.#send(['HELP: target=all|0|id|#index|!addr', 'HELP: getData|status|fw', 'HELP: getList|clearList|scanList|refreshList']);
38
+ return;
39
+ }
40
+ if (command === 'getList') {
41
+ this.#send(['LIST,BEGIN', ...this.#units.filter(u => u.online).map(u => `DEVICE,${u.number},${u.number},unit-${u.number},-,85,0,0,1.0.0,-`), 'LIST,END']);
42
+ return;
43
+ }
44
+ if (command === 'discover all') {
45
+ this.#send(['TXB,all,group,01,OK', ...this.#units.filter(u => u.online).map(u => `RXB,${u.number},unit-${u.number},${hex([1, u.number])}`)]);
46
+ return;
47
+ }
48
+ const [name, target, parameter] = command.split(' ');
49
+ const code = codes[name];
50
+ const unit = this.#units.find(u => `!unit-${u.number}` === target);
51
+ if (code === undefined || !unit) {
52
+ this.#send(['ERR,BAD_TARGET']);
53
+ return;
54
+ }
55
+ const request = hex([code, ...(parameter === undefined ? [] : word(Number(parameter)))]);
56
+ const tx = `TXB,${target},unit-${unit.number},${request},OK`;
57
+ if (!unit.online) {
58
+ this.#send([tx]);
59
+ return;
60
+ }
61
+ if (code === 19 && this.#failures.has(unit.number)) {
62
+ this.#send([tx, `RXB,${unit.number},unit-${unit.number},${hex([127, unit.number, 1, code])}`]);
63
+ return;
64
+ }
65
+ let payload = [code, unit.number];
66
+ if ([16, 17, 18].includes(code)) {
67
+ unit.mode = code - 16;
68
+ unit.target = Number(parameter ?? 0);
69
+ }
70
+ if (code === 19) {
71
+ unit.running = true;
72
+ unit.count = 0;
73
+ unit.ticks = 0;
74
+ }
75
+ if (code === 20)
76
+ unit.running = false;
77
+ if (code === 21) {
78
+ if (unit.running) {
79
+ this.#seed = (Math.imul(this.#seed, 1664525) + 1013904223) >>> 0;
80
+ unit.count += 1 + this.#seed % 3;
81
+ unit.ticks += 5;
82
+ if (unit.mode === 1 && unit.count >= unit.target) {
83
+ unit.count = unit.target;
84
+ unit.running = false;
85
+ }
86
+ if (unit.mode === 2 && unit.ticks >= unit.target * 10) {
87
+ unit.ticks = unit.target * 10;
88
+ unit.running = false;
89
+ }
90
+ }
91
+ payload = [...payload, unit.mode, ...word(unit.count % 65536), ...word(unit.ticks % 65536), 0, 0];
92
+ }
93
+ if (code === 6)
94
+ payload.push(85);
95
+ this.#send([tx, `RXB,${unit.number},unit-${unit.number},${hex(payload)}`]);
96
+ }
97
+ #unit(displayNumber) { (0, errors_js_1.integer)(displayNumber, 1, this.#units.length); return this.#units[displayNumber - 1]; }
98
+ controls() {
99
+ return Object.freeze({
100
+ disconnectDevice: (displayNumber) => { this.#unit(displayNumber).online = false; },
101
+ reconnectDevice: (displayNumber) => { this.#unit(displayNumber).online = true; },
102
+ setStartFailure: (displayNumber, fail) => {
103
+ this.#unit(displayNumber);
104
+ if (typeof fail !== 'boolean')
105
+ throw new errors_js_1.SDKError('INVALID_ARGUMENT');
106
+ if (fail)
107
+ this.#failures.add(displayNumber);
108
+ else
109
+ this.#failures.delete(displayNumber);
110
+ },
111
+ dropConnection: () => { const lost = this.#lost; void this.close(); lost?.(); }
112
+ });
113
+ }
114
+ async close() { for (const timer of this.#timers)
115
+ clearTimeout(timer); this.#timers.clear(); this.#line = null; this.#lost = null; }
116
+ }
117
+ exports.SimulationLink = SimulationLink;
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,182 @@
1
+ export type ErrorCode = 'UNSUPPORTED_BROWSER' | 'INSECURE_CONTEXT' | 'USER_CANCELLED' | 'PORT_UNAVAILABLE' | 'NOT_CONNECTED' | 'UNSUPPORTED_GATEWAY' | 'UNSUPPORTED_FEATURE' | 'INVALID_ARGUMENT' | 'INVALID_STATE' | 'DEVICE_OFFLINE' | 'AMBIGUOUS_DEVICE' | 'BUSY' | 'TIMEOUT' | 'DEVICE_REJECTED' | 'CONNECTION_LOST' | 'PROTOCOL_ERROR';
2
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'disconnecting' | 'lost';
3
+ export type DeviceState = 'available' | 'running' | 'unknown' | 'offline';
4
+ export type Mode = {
5
+ type: 'free';
6
+ } | {
7
+ type: 'count';
8
+ targetCount: number;
9
+ } | {
10
+ type: 'time';
11
+ durationSeconds: number;
12
+ };
13
+ export type IdentifySignal = 'vibrate' | 'buzzer' | 'displayId';
14
+ export type Battery = {
15
+ kind: 'percent';
16
+ value: number;
17
+ } | {
18
+ kind: 'unknown';
19
+ };
20
+ export interface Capabilities {
21
+ modes: Array<Mode['type']>;
22
+ maxTargetCount: number;
23
+ maxDurationSeconds: number;
24
+ identifySignals: IdentifySignal[];
25
+ /** Actual hardware combinations must still pass the release acceptance checks. */
26
+ hardwareVerified: boolean;
27
+ }
28
+ export interface Device {
29
+ deviceId: string;
30
+ displayNumber: number | null;
31
+ state: DeviceState;
32
+ firmwareVersion: string | null;
33
+ battery: Battery;
34
+ capabilities: Capabilities;
35
+ simulated: boolean;
36
+ }
37
+ export interface Measurement {
38
+ deviceId: string;
39
+ displayNumber: number | null;
40
+ sessionId: string;
41
+ segmentId: string;
42
+ count: number;
43
+ elapsedMs: number;
44
+ pauseCount: number | null;
45
+ receivedAt: number;
46
+ source: 'live' | 'cache';
47
+ stale: boolean;
48
+ simulated: boolean;
49
+ }
50
+ export interface ErrorInfo {
51
+ code: ErrorCode;
52
+ message: string;
53
+ recoverable: boolean;
54
+ deviceId?: string;
55
+ requestId?: string;
56
+ }
57
+ export type ResultStatus = 'acknowledged' | 'failed' | 'unknown';
58
+ export interface DeviceResult {
59
+ deviceId: string;
60
+ status: ResultStatus;
61
+ errorCode?: ErrorCode;
62
+ recovery?: {
63
+ action: 'stop';
64
+ status: ResultStatus;
65
+ };
66
+ finalMeasurement?: {
67
+ available: boolean;
68
+ measurement: Measurement | null;
69
+ };
70
+ }
71
+ export interface CommandResult {
72
+ requestId: string;
73
+ ok: boolean;
74
+ results: DeviceResult[];
75
+ }
76
+ export interface DisconnectResult {
77
+ closed: boolean;
78
+ stopResults: CommandResult | null;
79
+ }
80
+ export interface ScanResult {
81
+ devices: Device[];
82
+ status: 'complete';
83
+ windowMs: number;
84
+ }
85
+ export interface Support {
86
+ supported: boolean;
87
+ secureContext: boolean;
88
+ webSerial: boolean;
89
+ reason: ErrorCode | null;
90
+ }
91
+ export interface GatewayInfo {
92
+ connection: ConnectionState;
93
+ sdkVersion: string;
94
+ interfaceVersion: string;
95
+ firmwareVersion: string | null;
96
+ simulated: boolean;
97
+ features: string[];
98
+ }
99
+ export interface Diagnostics {
100
+ sdkVersion: string;
101
+ interfaceVersion: string;
102
+ connection: ConnectionState;
103
+ simulated: boolean;
104
+ deviceCount: number;
105
+ blockedOperationCount: number;
106
+ recentRequests: Array<{
107
+ requestId: string;
108
+ durationMs: number;
109
+ errorCode: ErrorCode | null;
110
+ }>;
111
+ }
112
+ export interface Events {
113
+ measurement: Measurement;
114
+ deviceChanged: {
115
+ device: Device;
116
+ previousState: DeviceState | null;
117
+ };
118
+ connectionChanged: {
119
+ state: ConnectionState;
120
+ previousState: ConnectionState;
121
+ };
122
+ error: ErrorInfo;
123
+ }
124
+ export interface GatewayOptions {
125
+ requestTimeoutMs?: number;
126
+ scanWindowMs?: number;
127
+ pollIntervalMs?: number;
128
+ staleAfterMs?: number;
129
+ /** Empty by default; enable only signals validated for the actual product. */
130
+ identifySignals?: IdentifySignal[];
131
+ }
132
+ export interface Gateway {
133
+ connect(): Promise<GatewayInfo>;
134
+ disconnect(): Promise<DisconnectResult>;
135
+ getInfo(): GatewayInfo;
136
+ scan(options?: {
137
+ timeoutMs?: number;
138
+ }): Promise<ScanResult>;
139
+ getDevices(): Device[];
140
+ configure(options: {
141
+ deviceIds: string[];
142
+ mode: Mode;
143
+ }): Promise<CommandResult>;
144
+ start(options: {
145
+ deviceIds: string[];
146
+ }): Promise<CommandResult>;
147
+ stop(options: {
148
+ deviceIds: string[];
149
+ }): Promise<CommandResult>;
150
+ readMeasurement(options: {
151
+ deviceId: string;
152
+ timeoutMs?: number;
153
+ }): Promise<Measurement>;
154
+ getLastMeasurement(options: {
155
+ deviceId: string;
156
+ }): Measurement | null;
157
+ readBattery(options: {
158
+ deviceId: string;
159
+ timeoutMs?: number;
160
+ }): Promise<Battery>;
161
+ identify(options: {
162
+ deviceId: string;
163
+ signal: IdentifySignal;
164
+ }): Promise<CommandResult>;
165
+ on<K extends keyof Events>(event: K, handler: (value: Events[K]) => void): () => void;
166
+ getDiagnostics(): Diagnostics;
167
+ }
168
+ export interface SimulationOptions extends GatewayOptions {
169
+ deviceCount?: number;
170
+ seed?: number;
171
+ responseDelayMs?: number;
172
+ /** Display numbers that reject start, for testing partial failure recovery. */
173
+ startFailures?: number[];
174
+ }
175
+ export interface SimulatedGateway extends Gateway {
176
+ simulation: {
177
+ disconnectDevice(displayNumber: number): void;
178
+ reconnectDevice(displayNumber: number): void;
179
+ setStartFailure(displayNumber: number, fail: boolean): void;
180
+ dropConnection(): void;
181
+ };
182
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import type { Gateway, GatewayOptions, SimulatedGateway, SimulationOptions } from './types.js';
2
+ export { getSupport } from './internal/link.js';
3
+ export { SDKError } from './internal/errors.js';
4
+ export type * from './types.js';
5
+ export declare function createGateway(options?: GatewayOptions): Gateway;
6
+ export declare function createSimulatedGateway(options?: SimulationOptions): SimulatedGateway;
@@ -0,0 +1,22 @@
1
+ import { GatewayEngine } from './internal/gateway.js';
2
+ import { BrowserLink } from './internal/link.js';
3
+ import { SimulationLink } from './internal/simulator.js';
4
+ export { getSupport } from './internal/link.js';
5
+ export { SDKError } from './internal/errors.js';
6
+ function facade(engine) {
7
+ return {
8
+ connect: () => engine.connect(), disconnect: () => engine.disconnect(),
9
+ getInfo: () => engine.getInfo(), scan: options => engine.scan(options), getDevices: () => engine.getDevices(),
10
+ configure: options => engine.configure(options), start: options => engine.start(options), stop: options => engine.stop(options),
11
+ readMeasurement: options => engine.readMeasurement(options), getLastMeasurement: options => engine.getLastMeasurement(options),
12
+ readBattery: options => engine.readBattery(options), identify: options => engine.identify(options),
13
+ on: (event, handler) => engine.on(event, handler), getDiagnostics: () => engine.getDiagnostics()
14
+ };
15
+ }
16
+ export function createGateway(options = {}) {
17
+ return Object.freeze(facade(new GatewayEngine(new BrowserLink(), options)));
18
+ }
19
+ export function createSimulatedGateway(options = {}) {
20
+ const link = new SimulationLink(options);
21
+ return Object.freeze({ ...facade(new GatewayEngine(link, options, true)), simulation: link.controls() });
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { ErrorCode, ErrorInfo } from '../types.js';
2
+ export declare class SDKError extends Error implements ErrorInfo {
3
+ readonly code: ErrorCode;
4
+ readonly recoverable: boolean;
5
+ readonly deviceId?: string;
6
+ readonly requestId?: string;
7
+ constructor(code: ErrorCode, context?: {
8
+ deviceId?: string;
9
+ requestId?: string;
10
+ });
11
+ toJSON(): ErrorInfo;
12
+ }
13
+ export declare const fail: (code: ErrorCode) => never;
14
+ export declare const asError: (error: unknown, fallback?: ErrorCode) => SDKError;
15
+ export declare function integer(value: unknown, min: number, max: number): number;
@@ -0,0 +1,37 @@
1
+ const messages = {
2
+ UNSUPPORTED_BROWSER: '이 브라우저는 USB 장비 연결을 지원하지 않습니다.',
3
+ INSECURE_CONTEXT: 'HTTPS 또는 localhost에서 실행해주세요.',
4
+ USER_CANCELLED: '포트 선택을 취소했습니다.', PORT_UNAVAILABLE: 'USB 포트를 열거나 해제할 수 없습니다.',
5
+ NOT_CONNECTED: '게이트웨이를 먼저 연결해주세요.', UNSUPPORTED_GATEWAY: '지원하는 게이트웨이 응답을 확인하지 못했습니다.',
6
+ UNSUPPORTED_FEATURE: '이 장비에서 활성화되지 않은 기능입니다.', INVALID_ARGUMENT: '입력값을 확인해주세요.',
7
+ INVALID_STATE: '현재 상태에서 실행할 수 없습니다. 장비 상태를 먼저 확인해주세요.',
8
+ DEVICE_OFFLINE: '장비의 응답을 확인할 수 없습니다.', AMBIGUOUS_DEVICE: '장비 번호가 중복되었거나 응답 대상을 확인할 수 없습니다.',
9
+ BUSY: '다른 장비 작업이 진행 중입니다.', TIMEOUT: '제한 시간 안에 응답을 받지 못했습니다.',
10
+ DEVICE_REJECTED: '장비 또는 게이트웨이가 요청을 거부했습니다.', CONNECTION_LOST: '게이트웨이 연결이 끊어졌습니다.',
11
+ PROTOCOL_ERROR: '지원하는 응답 형식이 아닙니다.'
12
+ };
13
+ export class SDKError extends Error {
14
+ code;
15
+ recoverable;
16
+ deviceId;
17
+ requestId;
18
+ constructor(code, context = {}) {
19
+ super(messages[code]);
20
+ this.name = 'SDKError';
21
+ this.code = code;
22
+ this.recoverable = !['UNSUPPORTED_BROWSER', 'UNSUPPORTED_GATEWAY', 'UNSUPPORTED_FEATURE', 'INVALID_ARGUMENT'].includes(code);
23
+ this.deviceId = context.deviceId;
24
+ this.requestId = context.requestId;
25
+ }
26
+ toJSON() {
27
+ return { code: this.code, message: this.message, recoverable: this.recoverable,
28
+ ...(this.deviceId ? { deviceId: this.deviceId } : {}), ...(this.requestId ? { requestId: this.requestId } : {}) };
29
+ }
30
+ }
31
+ export const fail = (code) => { throw new SDKError(code); };
32
+ export const asError = (error, fallback = 'PROTOCOL_ERROR') => error instanceof SDKError ? error : new SDKError(fallback);
33
+ export function integer(value, min, max) {
34
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max)
35
+ fail('INVALID_ARGUMENT');
36
+ return value;
37
+ }
@@ -0,0 +1,41 @@
1
+ import type { Battery, CommandResult, Device, Diagnostics, DisconnectResult, Events, Gateway, GatewayInfo, GatewayOptions, IdentifySignal, Measurement, Mode, ScanResult } from '../types.js';
2
+ import type { Link } from './link.js';
3
+ export declare const VERSION = "0.1.0-alpha.3";
4
+ export declare class GatewayEngine implements Gateway {
5
+ #private;
6
+ constructor(link: Link, options?: GatewayOptions, simulated?: boolean);
7
+ on<K extends keyof Events>(name: K, handler: (value: Events[K]) => void): () => void;
8
+ connect(): Promise<GatewayInfo>;
9
+ scan(options?: {
10
+ timeoutMs?: number;
11
+ }): Promise<ScanResult>;
12
+ configure(options: {
13
+ deviceIds: string[];
14
+ mode: Mode;
15
+ }): Promise<CommandResult>;
16
+ start(options: {
17
+ deviceIds: string[];
18
+ }): Promise<CommandResult>;
19
+ stop(options: {
20
+ deviceIds: string[];
21
+ }): Promise<CommandResult>;
22
+ readMeasurement(options: {
23
+ deviceId: string;
24
+ timeoutMs?: number;
25
+ }): Promise<Measurement>;
26
+ getLastMeasurement(options: {
27
+ deviceId: string;
28
+ }): Measurement | null;
29
+ readBattery(options: {
30
+ deviceId: string;
31
+ timeoutMs?: number;
32
+ }): Promise<Battery>;
33
+ identify(options: {
34
+ deviceId: string;
35
+ signal: IdentifySignal;
36
+ }): Promise<CommandResult>;
37
+ disconnect(): Promise<DisconnectResult>;
38
+ getDevices(): Device[];
39
+ getInfo(): GatewayInfo;
40
+ getDiagnostics(): Diagnostics;
41
+ }