tci-client-node 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,1182 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ TCI_STREAM_HEADER_BYTES: () => TCI_STREAM_HEADER_BYTES,
34
+ TciClient: () => TciClient,
35
+ TciCommandQueue: () => TciCommandQueue,
36
+ TciError: () => TciError,
37
+ TciSampleType: () => TciSampleType,
38
+ TciStreamType: () => TciStreamType,
39
+ buildStreamFrame: () => buildStreamFrame,
40
+ buildTxAudioFrame: () => buildTxAudioFrame,
41
+ commandKey: () => commandKey,
42
+ createTciClient: () => createTciClient,
43
+ deinterleaveChannels: () => deinterleaveChannels,
44
+ escapeTciText: () => escapeTciText,
45
+ float32ToPcm16: () => float32ToPcm16,
46
+ formatTciCommand: () => formatTciCommand,
47
+ isCommandReplyTo: () => isCommandReplyTo,
48
+ mixToMono: () => mixToMono,
49
+ normalizeCommandName: () => normalizeCommandName,
50
+ normalizeSampleType: () => normalizeSampleType,
51
+ normalizeStreamType: () => normalizeStreamType,
52
+ parseStreamFrame: () => parseStreamFrame,
53
+ parseTciCommand: () => parseTciCommand,
54
+ parseTciText: () => parseTciText,
55
+ payloadToFloat32: () => payloadToFloat32,
56
+ pcm16ToFloat32: () => pcm16ToFloat32,
57
+ sampleTypeBytes: () => sampleTypeBytes,
58
+ sampleTypeName: () => sampleTypeName,
59
+ samplesToPayload: () => samplesToPayload,
60
+ toTciError: () => toTciError,
61
+ unescapeTciText: () => unescapeTciText
62
+ });
63
+ module.exports = __toCommonJS(src_exports);
64
+
65
+ // src/errors.ts
66
+ var TciError = class extends Error {
67
+ code;
68
+ details;
69
+ constructor(code, message, details) {
70
+ super(message);
71
+ this.name = "TciError";
72
+ this.code = code;
73
+ this.details = details;
74
+ }
75
+ };
76
+ function toTciError(error, fallbackCode = "protocol-error") {
77
+ if (error instanceof TciError) {
78
+ return error;
79
+ }
80
+ if (error instanceof Error) {
81
+ return new TciError(fallbackCode, error.message, error);
82
+ }
83
+ return new TciError(fallbackCode, String(error), error);
84
+ }
85
+
86
+ // src/client/TciClient.ts
87
+ var import_eventemitter3 = require("eventemitter3");
88
+ var import_ws = __toESM(require("ws"), 1);
89
+
90
+ // src/audio/streamFrame.ts
91
+ var TCI_STREAM_HEADER_BYTES = 16 * 4;
92
+ var TciStreamType = /* @__PURE__ */ ((TciStreamType2) => {
93
+ TciStreamType2[TciStreamType2["IQ_STREAM"] = 0] = "IQ_STREAM";
94
+ TciStreamType2[TciStreamType2["RX_AUDIO_STREAM"] = 1] = "RX_AUDIO_STREAM";
95
+ TciStreamType2[TciStreamType2["TX_AUDIO_STREAM"] = 2] = "TX_AUDIO_STREAM";
96
+ TciStreamType2[TciStreamType2["TX_CHRONO"] = 3] = "TX_CHRONO";
97
+ TciStreamType2[TciStreamType2["LINEOUT_STREAM"] = 4] = "LINEOUT_STREAM";
98
+ return TciStreamType2;
99
+ })(TciStreamType || {});
100
+ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
101
+ TciSampleType2[TciSampleType2["INT16"] = 0] = "INT16";
102
+ TciSampleType2[TciSampleType2["INT24"] = 1] = "INT24";
103
+ TciSampleType2[TciSampleType2["INT32"] = 2] = "INT32";
104
+ TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
105
+ return TciSampleType2;
106
+ })(TciSampleType || {});
107
+ function parseStreamFrame(input) {
108
+ const buffer = toBuffer(input);
109
+ if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
110
+ throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
111
+ }
112
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
113
+ const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
114
+ const sampleType = normalizeSampleType(header[2]);
115
+ let channels = header[7];
116
+ const bytesPerSample = sampleTypeBytes(sampleType);
117
+ const sampleCount = header[5];
118
+ const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
119
+ if (channels <= 0) {
120
+ const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
121
+ if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
122
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
123
+ }
124
+ channels = inferredChannels;
125
+ }
126
+ const payloadLength = sampleCount * bytesPerSample * channels;
127
+ const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;
128
+ if (buffer.byteLength !== expectedLength) {
129
+ throw new TciError(
130
+ "invalid-frame",
131
+ `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
132
+ );
133
+ }
134
+ if (payloadLength % (bytesPerSample * channels) !== 0) {
135
+ throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
136
+ }
137
+ return {
138
+ receiver: header[0],
139
+ sampleRate: header[1],
140
+ sampleType,
141
+ codec: header[3],
142
+ crc: header[4],
143
+ payloadLength,
144
+ streamType: normalizeStreamType(header[6]),
145
+ channels,
146
+ reserved: header.slice(8),
147
+ payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
148
+ sampleCount
149
+ };
150
+ }
151
+ function buildStreamFrame(options) {
152
+ const sampleType = normalizeSampleType(options.sampleType);
153
+ const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);
154
+ const channels = options.channels;
155
+ if (channels <= 0) {
156
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
157
+ }
158
+ const bytesPerSample = sampleTypeBytes(sampleType);
159
+ if (payload.byteLength % (bytesPerSample * channels) !== 0) {
160
+ throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
161
+ }
162
+ const sampleCount = payload.byteLength / bytesPerSample / channels;
163
+ const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
164
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
165
+ const reserved = options.reserved ?? [];
166
+ const header = [
167
+ options.receiver ?? 0,
168
+ options.sampleRate,
169
+ sampleType,
170
+ options.codec ?? 0,
171
+ options.crc ?? 0,
172
+ sampleCount,
173
+ options.streamType,
174
+ channels,
175
+ ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
176
+ ];
177
+ header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));
178
+ payload.copy(frame, TCI_STREAM_HEADER_BYTES);
179
+ return frame;
180
+ }
181
+ function buildTxAudioFrame(options) {
182
+ return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
183
+ }
184
+ function sampleTypeBytes(sampleType) {
185
+ switch (normalizeSampleType(sampleType)) {
186
+ case 0 /* INT16 */:
187
+ return 2;
188
+ case 1 /* INT24 */:
189
+ return 3;
190
+ case 2 /* INT32 */:
191
+ case 3 /* FLOAT32 */:
192
+ return 4;
193
+ default:
194
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
195
+ }
196
+ }
197
+ function sampleTypeName(sampleType) {
198
+ switch (sampleType) {
199
+ case 0 /* INT16 */:
200
+ return "int16";
201
+ case 1 /* INT24 */:
202
+ return "int24";
203
+ case 2 /* INT32 */:
204
+ return "int32";
205
+ case 3 /* FLOAT32 */:
206
+ return "float32";
207
+ default:
208
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
209
+ }
210
+ }
211
+ function normalizeSampleType(sampleType) {
212
+ if (typeof sampleType === "string") {
213
+ switch (sampleType.toLowerCase()) {
214
+ case "int16":
215
+ return 0 /* INT16 */;
216
+ case "int24":
217
+ return 1 /* INT24 */;
218
+ case "int32":
219
+ return 2 /* INT32 */;
220
+ case "float32":
221
+ return 3 /* FLOAT32 */;
222
+ default:
223
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
224
+ }
225
+ }
226
+ if (sampleType >= 0 /* INT16 */ && sampleType <= 3 /* FLOAT32 */) {
227
+ return sampleType;
228
+ }
229
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
230
+ }
231
+ function normalizeStreamType(streamType) {
232
+ if (streamType >= 0 /* IQ_STREAM */ && streamType <= 4 /* LINEOUT_STREAM */) {
233
+ return streamType;
234
+ }
235
+ throw new TciError("invalid-frame", `Unsupported TCI stream type: ${streamType}`);
236
+ }
237
+ function payloadToFloat32(frameOrPayload, sampleType) {
238
+ const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);
239
+ const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? 3 /* FLOAT32 */);
240
+ const bytes = sampleTypeBytes(type);
241
+ if (payload.byteLength % bytes !== 0) {
242
+ throw new TciError("invalid-frame", "Payload length is not aligned to sample type");
243
+ }
244
+ const output = new Float32Array(payload.byteLength / bytes);
245
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
246
+ for (let i = 0; i < output.length; i += 1) {
247
+ const offset = i * bytes;
248
+ switch (type) {
249
+ case 0 /* INT16 */:
250
+ output[i] = view.getInt16(offset, true) / 32768;
251
+ break;
252
+ case 1 /* INT24 */:
253
+ output[i] = readInt24(view, offset) / 8388608;
254
+ break;
255
+ case 2 /* INT32 */:
256
+ output[i] = view.getInt32(offset, true) / 2147483648;
257
+ break;
258
+ case 3 /* FLOAT32 */:
259
+ output[i] = view.getFloat32(offset, true);
260
+ break;
261
+ }
262
+ }
263
+ return output;
264
+ }
265
+ function samplesToPayload(samples, sampleType) {
266
+ const type = normalizeSampleType(sampleType);
267
+ const bytes = sampleTypeBytes(type);
268
+ const payload = Buffer.alloc(samples.length * bytes);
269
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
270
+ for (let i = 0; i < samples.length; i += 1) {
271
+ const value = clampSample(samples[i] ?? 0);
272
+ const offset = i * bytes;
273
+ switch (type) {
274
+ case 0 /* INT16 */:
275
+ view.setInt16(offset, Math.round(value * 32767), true);
276
+ break;
277
+ case 1 /* INT24 */:
278
+ writeInt24(view, offset, Math.round(value * 8388607));
279
+ break;
280
+ case 2 /* INT32 */:
281
+ view.setInt32(offset, Math.round(value * 2147483647), true);
282
+ break;
283
+ case 3 /* FLOAT32 */:
284
+ view.setFloat32(offset, value, true);
285
+ break;
286
+ }
287
+ }
288
+ return payload;
289
+ }
290
+ function pcm16ToFloat32(input) {
291
+ if (input instanceof Int16Array) {
292
+ const output = new Float32Array(input.length);
293
+ for (let i = 0; i < input.length; i += 1) {
294
+ output[i] = input[i] / 32768;
295
+ }
296
+ return output;
297
+ }
298
+ return payloadToFloat32(toBuffer(input), 0 /* INT16 */);
299
+ }
300
+ function float32ToPcm16(samples) {
301
+ return samplesToPayload(samples, 0 /* INT16 */);
302
+ }
303
+ function deinterleaveChannels(samples, channels) {
304
+ if (channels <= 0 || samples.length % channels !== 0) {
305
+ throw new TciError("invalid-frame", "Cannot deinterleave samples with invalid channel count");
306
+ }
307
+ const frames = samples.length / channels;
308
+ const outputs = Array.from({ length: channels }, () => new Float32Array(frames));
309
+ for (let frame = 0; frame < frames; frame += 1) {
310
+ for (let channel = 0; channel < channels; channel += 1) {
311
+ outputs[channel][frame] = samples[frame * channels + channel];
312
+ }
313
+ }
314
+ return outputs;
315
+ }
316
+ function mixToMono(samples, channels) {
317
+ if (channels === 1) {
318
+ return samples;
319
+ }
320
+ const separated = deinterleaveChannels(samples, channels);
321
+ const mono = new Float32Array(separated[0]?.length ?? 0);
322
+ for (const channel of separated) {
323
+ for (let i = 0; i < mono.length; i += 1) {
324
+ mono[i] += channel[i] / channels;
325
+ }
326
+ }
327
+ return mono;
328
+ }
329
+ function toBuffer(input) {
330
+ if (Buffer.isBuffer(input)) {
331
+ return input;
332
+ }
333
+ if (input instanceof ArrayBuffer) {
334
+ return Buffer.from(input);
335
+ }
336
+ if (ArrayBuffer.isView(input)) {
337
+ return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
338
+ }
339
+ return Buffer.from(input);
340
+ }
341
+ function isFrame(value) {
342
+ return Boolean(value && typeof value === "object" && "payload" in value && "sampleType" in value);
343
+ }
344
+ function clampSample(value) {
345
+ if (!Number.isFinite(value)) {
346
+ return 0;
347
+ }
348
+ return Math.max(-1, Math.min(1, value));
349
+ }
350
+ function readInt24(view, offset) {
351
+ const value = view.getUint8(offset) | view.getUint8(offset + 1) << 8 | view.getUint8(offset + 2) << 16;
352
+ return value & 8388608 ? value | 4278190080 : value;
353
+ }
354
+ function writeInt24(view, offset, value) {
355
+ const clamped = Math.max(-8388608, Math.min(8388607, value));
356
+ view.setUint8(offset, clamped & 255);
357
+ view.setUint8(offset + 1, clamped >> 8 & 255);
358
+ view.setUint8(offset + 2, clamped >> 16 & 255);
359
+ }
360
+
361
+ // src/protocol/text.ts
362
+ var ESCAPE_TO_CHAR = {
363
+ "^": ":",
364
+ "~": ",",
365
+ "*": ";"
366
+ };
367
+ var CHAR_TO_ESCAPE = {
368
+ ":": "^",
369
+ ",": "~",
370
+ ";": "*"
371
+ };
372
+ function escapeTciText(value) {
373
+ return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);
374
+ }
375
+ function unescapeTciText(value) {
376
+ return value.replace(/[\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);
377
+ }
378
+ function parseTciText(text) {
379
+ const source = normalizeTextInput(text);
380
+ const commands = [];
381
+ for (const fragment of source.split(";")) {
382
+ const raw = fragment.trim();
383
+ if (!raw) {
384
+ continue;
385
+ }
386
+ const colonIndex = raw.indexOf(":");
387
+ const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();
388
+ if (!originalName) {
389
+ continue;
390
+ }
391
+ const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : void 0;
392
+ commands.push({
393
+ name: originalName.toLowerCase(),
394
+ originalName,
395
+ args: argsText === void 0 ? [] : splitArgs(argsText),
396
+ raw
397
+ });
398
+ }
399
+ return commands;
400
+ }
401
+ function parseTciCommand(input) {
402
+ if (typeof input !== "string") {
403
+ return input;
404
+ }
405
+ const [command] = parseTciText(input);
406
+ if (!command) {
407
+ throw new Error(`Invalid TCI command: ${input}`);
408
+ }
409
+ return command;
410
+ }
411
+ function formatTciCommand(name, args = []) {
412
+ const commandName = name.trim().toUpperCase();
413
+ if (!commandName) {
414
+ throw new Error("TCI command name cannot be empty");
415
+ }
416
+ if (args.length === 0) {
417
+ return `${commandName};`;
418
+ }
419
+ return `${commandName}:${args.map(escapeTciText).join(",")};`;
420
+ }
421
+ function normalizeCommandName(name) {
422
+ return name.trim().toLowerCase();
423
+ }
424
+ function isCommandReplyTo(replyInput, requestInput) {
425
+ const reply = parseTciCommand(replyInput);
426
+ const request = parseTciCommand(requestInput);
427
+ if (reply.name !== request.name) {
428
+ return false;
429
+ }
430
+ if (request.args.length === 0) {
431
+ return true;
432
+ }
433
+ if (argsHavePrefix(reply.args, request.args)) {
434
+ return true;
435
+ }
436
+ return isKnownVariantReply(reply, request);
437
+ }
438
+ function commandKey(command) {
439
+ const parsed = parseTciCommand(command);
440
+ return `${parsed.name}:${parsed.args.join(",")}`;
441
+ }
442
+ function normalizeTextInput(text) {
443
+ if (typeof text === "string") {
444
+ return text;
445
+ }
446
+ if (Buffer.isBuffer(text)) {
447
+ return text.toString("utf8");
448
+ }
449
+ if (text instanceof ArrayBuffer) {
450
+ return Buffer.from(text).toString("utf8");
451
+ }
452
+ return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString("utf8");
453
+ }
454
+ function splitArgs(argsText) {
455
+ return argsText.split(",").map((arg) => unescapeTciText(arg.trim()));
456
+ }
457
+ function argsHavePrefix(args, prefix) {
458
+ if (args.length < prefix.length) {
459
+ return false;
460
+ }
461
+ return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());
462
+ }
463
+ function isKnownVariantReply(reply, request) {
464
+ if (reply.name === "modulation") {
465
+ if (request.args.length === 2 && reply.args.length >= 3) {
466
+ return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();
467
+ }
468
+ if (request.args.length === 3 && reply.args.length === 2) {
469
+ return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();
470
+ }
471
+ }
472
+ if (reply.name === "protocol") {
473
+ return request.args.length <= 1;
474
+ }
475
+ if (reply.name === "trx" && request.args.length >= 3 && reply.args.length >= 2) {
476
+ return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();
477
+ }
478
+ return false;
479
+ }
480
+
481
+ // src/protocol/commandQueue.ts
482
+ var TciCommandQueue = class {
483
+ send;
484
+ defaultTimeoutMs;
485
+ queue = [];
486
+ active;
487
+ connected = true;
488
+ constructor(options) {
489
+ this.send = options.send;
490
+ this.defaultTimeoutMs = options.timeoutMs ?? 1e3;
491
+ }
492
+ setConnected(connected) {
493
+ this.connected = connected;
494
+ if (!connected) {
495
+ this.cancelAll(new TciError("disconnected", "TCI connection closed"));
496
+ }
497
+ }
498
+ enqueue(command, options = {}) {
499
+ const request = parseTciCommand(command);
500
+ const raw = typeof command === "string" ? ensureSemicolon(command) : formatTciCommand(command.originalName, command.args);
501
+ if (!this.connected) {
502
+ return Promise.reject(new TciError("not-connected", "TCI socket is not connected"));
503
+ }
504
+ return new Promise((resolve, reject) => {
505
+ const pending = {
506
+ raw,
507
+ request,
508
+ timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
509
+ matcher: options.matcher ?? ((reply, req) => isCommandReplyTo(reply, req)),
510
+ resolve,
511
+ reject
512
+ };
513
+ if (options.signal) {
514
+ if (options.signal.aborted) {
515
+ reject(new TciError("cancelled", "TCI command was cancelled"));
516
+ return;
517
+ }
518
+ const onAbort = () => this.rejectPending(pending, new TciError("cancelled", "TCI command was cancelled"));
519
+ options.signal.addEventListener("abort", onAbort, { once: true });
520
+ pending.abortCleanup = () => options.signal?.removeEventListener("abort", onAbort);
521
+ }
522
+ this.queue.push(pending);
523
+ void this.pump();
524
+ });
525
+ }
526
+ handleCommand(commandInput) {
527
+ const active = this.active;
528
+ if (!active) {
529
+ return false;
530
+ }
531
+ const reply = parseTciCommand(commandInput);
532
+ if (!active.matcher(reply, active.request)) {
533
+ return false;
534
+ }
535
+ this.finishActive(reply);
536
+ return true;
537
+ }
538
+ cancelAll(error = new TciError("cancelled", "TCI command queue cancelled")) {
539
+ const pending = [...this.queue];
540
+ this.queue = [];
541
+ if (this.active) {
542
+ pending.unshift(this.active);
543
+ this.active = void 0;
544
+ }
545
+ for (const item of pending) {
546
+ this.rejectPending(item, error);
547
+ }
548
+ }
549
+ get size() {
550
+ return this.queue.length + (this.active ? 1 : 0);
551
+ }
552
+ async pump() {
553
+ if (this.active || !this.connected) {
554
+ return;
555
+ }
556
+ const next = this.queue.shift();
557
+ if (!next) {
558
+ return;
559
+ }
560
+ this.active = next;
561
+ next.timer = setTimeout(() => {
562
+ this.rejectPending(next, new TciError("command-timeout", `Timed out waiting for TCI reply to ${next.raw}`));
563
+ if (this.active === next) {
564
+ this.active = void 0;
565
+ }
566
+ void this.pump();
567
+ }, next.timeoutMs);
568
+ try {
569
+ await this.send(next.raw);
570
+ } catch (error) {
571
+ this.rejectPending(next, new TciError("disconnected", error instanceof Error ? error.message : String(error), error));
572
+ if (this.active === next) {
573
+ this.active = void 0;
574
+ }
575
+ void this.pump();
576
+ }
577
+ }
578
+ finishActive(reply) {
579
+ const active = this.active;
580
+ if (!active) {
581
+ return;
582
+ }
583
+ this.active = void 0;
584
+ if (active.timer) {
585
+ clearTimeout(active.timer);
586
+ }
587
+ active.abortCleanup?.();
588
+ active.resolve({ request: active.request, reply });
589
+ void this.pump();
590
+ }
591
+ rejectPending(pending, error) {
592
+ if (pending.timer) {
593
+ clearTimeout(pending.timer);
594
+ }
595
+ pending.abortCleanup?.();
596
+ const wasActive = this.active === pending;
597
+ if (wasActive) {
598
+ this.active = void 0;
599
+ } else {
600
+ this.queue = this.queue.filter((item) => item !== pending);
601
+ }
602
+ pending.reject(error);
603
+ if (wasActive && this.connected) {
604
+ void this.pump();
605
+ }
606
+ }
607
+ };
608
+ function ensureSemicolon(command) {
609
+ return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
610
+ }
611
+
612
+ // src/client/TciClient.ts
613
+ var TciClient = class extends import_eventemitter3.EventEmitter {
614
+ options;
615
+ WebSocketImpl;
616
+ ws;
617
+ queue;
618
+ state;
619
+ constructor(options) {
620
+ super();
621
+ this.options = {
622
+ url: options.url,
623
+ receiver: options.receiver ?? 0,
624
+ trx: options.trx ?? 0,
625
+ vfo: options.vfo ?? 0,
626
+ connectTimeoutMs: options.connectTimeoutMs ?? 5e3,
627
+ commandTimeoutMs: options.commandTimeoutMs ?? 1e3
628
+ };
629
+ this.WebSocketImpl = options.WebSocketImpl ?? import_ws.default;
630
+ this.queue = new TciCommandQueue({
631
+ timeoutMs: this.options.commandTimeoutMs,
632
+ send: (raw) => this.sendRaw(raw)
633
+ });
634
+ this.queue.setConnected(false);
635
+ this.state = {
636
+ connected: false,
637
+ ready: false,
638
+ modulations: [],
639
+ frequencies: {},
640
+ modes: {},
641
+ ptt: {},
642
+ pttSource: {},
643
+ tune: {},
644
+ drive: {},
645
+ split: {},
646
+ rxSensors: {},
647
+ txSensors: {}
648
+ };
649
+ }
650
+ async connect() {
651
+ if (this.ws?.readyState === import_ws.default.OPEN) {
652
+ return;
653
+ }
654
+ if (this.ws && this.ws.readyState === import_ws.default.CONNECTING) {
655
+ await this.waitForOpen(this.ws);
656
+ return;
657
+ }
658
+ const ws = new this.WebSocketImpl(this.options.url);
659
+ this.ws = ws;
660
+ await this.waitForOpen(ws);
661
+ }
662
+ async disconnect(code = 1e3, reason = "client disconnect") {
663
+ const ws = this.ws;
664
+ if (!ws) {
665
+ return;
666
+ }
667
+ if (ws.readyState === import_ws.default.CLOSED) {
668
+ this.handleClose();
669
+ return;
670
+ }
671
+ await new Promise((resolve) => {
672
+ const cleanup = () => {
673
+ ws.off("close", onClose);
674
+ ws.off("error", onError);
675
+ };
676
+ const onClose = () => {
677
+ cleanup();
678
+ resolve();
679
+ };
680
+ const onError = () => {
681
+ cleanup();
682
+ resolve();
683
+ };
684
+ ws.once("close", onClose);
685
+ ws.once("error", onError);
686
+ ws.close(code, reason);
687
+ setTimeout(() => resolve(), 1e3).unref?.();
688
+ });
689
+ this.handleClose();
690
+ }
691
+ isConnected() {
692
+ return this.ws?.readyState === import_ws.default.OPEN;
693
+ }
694
+ getState() {
695
+ return cloneState(this.state);
696
+ }
697
+ async sendCommand(name, args = [], options = {}) {
698
+ const raw = formatTciCommand(name, args);
699
+ if (options.waitForReply === false) {
700
+ await this.sendRaw(raw);
701
+ return void 0;
702
+ }
703
+ const result = await this.queue.enqueue(raw, options);
704
+ return result.reply;
705
+ }
706
+ async request(name, args = [], options = {}) {
707
+ const reply = await this.sendCommand(name, args, { ...options, waitForReply: true });
708
+ if (!reply) {
709
+ throw new TciError("protocol-error", `No reply for ${name}`);
710
+ }
711
+ return reply;
712
+ }
713
+ async setFrequency(frequencyHz, receiver = this.options.receiver, vfo = this.options.vfo) {
714
+ await this.sendCommand("VFO", [receiver, vfo, Math.round(frequencyHz)]);
715
+ }
716
+ async getFrequency(receiver = this.options.receiver, vfo = this.options.vfo) {
717
+ const reply = await this.request("VFO", [receiver, vfo]);
718
+ return parseNumber(reply.args[2]) ?? this.state.frequencies[rxVfoKey(receiver, vfo)];
719
+ }
720
+ async setMode(mode, receiver = this.options.receiver) {
721
+ await this.sendCommand("MODULATION", [receiver, mode.toUpperCase()]);
722
+ }
723
+ async getMode(receiver = this.options.receiver) {
724
+ const reply = await this.request("MODULATION", [receiver]);
725
+ const mode = reply.args.length >= 3 ? reply.args[2] : reply.args[1];
726
+ return (mode ?? this.state.modes[rxVfoKey(receiver, this.options.vfo)])?.toLowerCase();
727
+ }
728
+ async setPtt(enabled, options = {}) {
729
+ const trx = options.trx ?? this.options.trx;
730
+ const args = options.source ? [trx, enabled, options.source] : [trx, enabled];
731
+ await this.sendCommand("TRX", args);
732
+ }
733
+ async getPtt(trx = this.options.trx) {
734
+ const reply = await this.request("TRX", [trx]);
735
+ return parseBoolean(reply.args[1]) ?? this.state.ptt[String(trx)];
736
+ }
737
+ async setTune(enabled, trx = this.options.trx) {
738
+ await this.sendCommand("TUNE", [trx, enabled]);
739
+ }
740
+ async setDrive(value, trx = this.options.trx) {
741
+ await this.sendCommand("DRIVE", [trx, value]);
742
+ }
743
+ async setSplit(enabled, trx = this.options.trx) {
744
+ await this.sendCommand("SPLIT_ENABLE", [trx, enabled]);
745
+ }
746
+ async configureAudio(config) {
747
+ const audio = {
748
+ sampleRate: config.sampleRate,
749
+ sampleType: normalizeSampleType(config.sampleType ?? 3 /* FLOAT32 */),
750
+ channels: config.channels ?? 1,
751
+ samplesPerFrame: config.samplesPerFrame ?? 512,
752
+ txBufferingMs: config.txBufferingMs,
753
+ running: this.state.audio?.running ?? false
754
+ };
755
+ this.state.audio = audio;
756
+ await this.sendCommand("AUDIO_SAMPLERATE", [audio.sampleRate], { waitForReply: false });
757
+ await this.sendCommand("AUDIO_STREAM_SAMPLE_TYPE", [sampleTypeName(audio.sampleType)], { waitForReply: false });
758
+ await this.sendCommand("AUDIO_STREAM_CHANNELS", [audio.channels], { waitForReply: false });
759
+ await this.sendCommand("AUDIO_STREAM_SAMPLES", [audio.samplesPerFrame], { waitForReply: false });
760
+ if (audio.txBufferingMs !== void 0) {
761
+ await this.sendCommand("TX_STREAM_AUDIO_BUFFERING", [audio.txBufferingMs], { waitForReply: false });
762
+ }
763
+ this.emitState();
764
+ }
765
+ async startAudio(receiver = this.options.receiver) {
766
+ await this.sendCommand("AUDIO_START", [receiver], { waitForReply: false });
767
+ if (this.state.audio) {
768
+ this.state.audio.running = true;
769
+ this.emitState();
770
+ }
771
+ }
772
+ async stopAudio(receiver = this.options.receiver) {
773
+ await this.sendCommand("AUDIO_STOP", [receiver], { waitForReply: false });
774
+ if (this.state.audio) {
775
+ this.state.audio.running = false;
776
+ this.emitState();
777
+ }
778
+ }
779
+ sendTxAudio(options) {
780
+ const frame = buildTxAudioFrame({ receiver: this.options.receiver, ...options });
781
+ this.sendRawBinary(frame);
782
+ }
783
+ async setRxSensorsEnabled(enabled, intervalMs) {
784
+ const args = intervalMs === void 0 ? [enabled] : [enabled, intervalMs];
785
+ await this.sendCommand("RX_SENSORS_ENABLE", args, { waitForReply: false });
786
+ }
787
+ async setTxSensorsEnabled(enabled, intervalMs) {
788
+ const args = intervalMs === void 0 ? [enabled] : [enabled, intervalMs];
789
+ await this.sendCommand("TX_SENSORS_ENABLE", args, { waitForReply: false });
790
+ }
791
+ async sendCwMacro(index) {
792
+ await this.sendCommand("CW_MACROS", [index]);
793
+ }
794
+ async sendCwMessage(message) {
795
+ await this.sendCommand("CW_MSG", [message]);
796
+ }
797
+ async stopCw() {
798
+ await this.sendCommand("CW_MACROS_STOP");
799
+ }
800
+ waitForOpen(ws) {
801
+ return new Promise((resolve, reject) => {
802
+ const timer = setTimeout(() => {
803
+ cleanup();
804
+ try {
805
+ ws.terminate();
806
+ } catch {
807
+ }
808
+ reject(new TciError("connect-timeout", `Timed out connecting to ${this.options.url}`));
809
+ }, this.options.connectTimeoutMs);
810
+ const cleanup = () => {
811
+ clearTimeout(timer);
812
+ ws.off("open", onOpen);
813
+ ws.off("close", onClose);
814
+ ws.off("error", onError);
815
+ };
816
+ const onOpen = () => {
817
+ cleanup();
818
+ this.attachSocket(ws);
819
+ this.state.connected = true;
820
+ this.queue.setConnected(true);
821
+ this.emit("connected");
822
+ this.emitState();
823
+ resolve();
824
+ };
825
+ const onClose = () => {
826
+ cleanup();
827
+ this.handleClose();
828
+ reject(new TciError("disconnected", `Disconnected while connecting to ${this.options.url}`));
829
+ };
830
+ const onError = (error) => {
831
+ cleanup();
832
+ this.handleError(error);
833
+ reject(toTciError(error, "disconnected"));
834
+ };
835
+ ws.once("open", onOpen);
836
+ ws.once("close", onClose);
837
+ ws.once("error", onError);
838
+ });
839
+ }
840
+ attachSocket(ws) {
841
+ ws.on("message", (data, isBinary) => this.handleMessage(data, isBinary));
842
+ ws.on("close", () => this.handleClose());
843
+ ws.on("error", (error) => this.handleError(error));
844
+ }
845
+ async sendRaw(raw) {
846
+ const ws = this.ws;
847
+ if (!ws || ws.readyState !== import_ws.default.OPEN) {
848
+ throw new TciError("not-connected", "TCI socket is not connected");
849
+ }
850
+ await new Promise((resolve, reject) => {
851
+ ws.send(raw, (error) => error ? reject(error) : resolve());
852
+ });
853
+ }
854
+ sendRawBinary(raw) {
855
+ const ws = this.ws;
856
+ if (!ws || ws.readyState !== import_ws.default.OPEN) {
857
+ throw new TciError("not-connected", "TCI socket is not connected");
858
+ }
859
+ ws.send(raw, { binary: true });
860
+ }
861
+ handleMessage(data, isBinary) {
862
+ try {
863
+ if (isBinary) {
864
+ this.handleBinary(data);
865
+ return;
866
+ }
867
+ const commands = parseTciText(dataToBuffer(data));
868
+ for (const command of commands) {
869
+ this.queue.handleCommand(command);
870
+ this.applyCommand(command);
871
+ this.emit("command", command);
872
+ }
873
+ } catch (error) {
874
+ this.handleError(error);
875
+ }
876
+ }
877
+ handleBinary(data) {
878
+ const frame = parseStreamFrame(dataToBuffer(data));
879
+ this.emit("binary", frame);
880
+ switch (frame.streamType) {
881
+ case 1 /* RX_AUDIO_STREAM */:
882
+ this.emit("rxAudioFrame", frame);
883
+ break;
884
+ case 3 /* TX_CHRONO */:
885
+ this.emit("txChrono", {
886
+ frame,
887
+ receiver: frame.receiver,
888
+ sampleRate: frame.sampleRate,
889
+ channels: frame.channels,
890
+ sampleType: frame.sampleType,
891
+ sampleCount: frame.sampleCount
892
+ });
893
+ break;
894
+ case 4 /* LINEOUT_STREAM */:
895
+ this.emit("lineoutAudioFrame", frame);
896
+ break;
897
+ default:
898
+ break;
899
+ }
900
+ }
901
+ applyCommand(command) {
902
+ const readyBefore = this.state.ready;
903
+ switch (command.name) {
904
+ case "ready":
905
+ this.state.ready = command.args.length === 0 ? true : parseBoolean(command.args[0]) ?? true;
906
+ break;
907
+ case "protocol":
908
+ this.state.protocol = command.args[0];
909
+ break;
910
+ case "device":
911
+ this.state.device = command.args.join(",");
912
+ break;
913
+ case "receive_only":
914
+ this.state.receiveOnly = parseBoolean(command.args[0]);
915
+ break;
916
+ case "trx_count":
917
+ this.state.trxCount = parseNumber(command.args[0]);
918
+ break;
919
+ case "channels_count":
920
+ case "channel_count":
921
+ this.state.channelCount = parseNumber(command.args[0]);
922
+ break;
923
+ case "vfo_limits":
924
+ this.state.vfoLimits = parseNumberPair(command.args);
925
+ break;
926
+ case "if_limits":
927
+ this.state.ifLimits = parseNumberPair(command.args);
928
+ break;
929
+ case "modulations_list":
930
+ this.state.modulations = command.args.map((mode) => mode.toLowerCase());
931
+ break;
932
+ case "vfo":
933
+ this.applyVfo(command.args);
934
+ break;
935
+ case "modulation":
936
+ this.applyModulation(command.args);
937
+ break;
938
+ case "trx":
939
+ this.applyTrx(command.args);
940
+ break;
941
+ case "tune":
942
+ this.applyBooleanByFirstArg(this.state.tune, command.args);
943
+ break;
944
+ case "drive":
945
+ this.applyDrive(command.args);
946
+ break;
947
+ case "split_enable":
948
+ this.applyBooleanByFirstArg(this.state.split, command.args);
949
+ break;
950
+ case "rx_channel_sensors":
951
+ this.applyRxChannelSensors(command.args);
952
+ break;
953
+ case "rx_sensors":
954
+ this.applyRxSensors(command.args);
955
+ break;
956
+ case "tx_sensors":
957
+ this.applyTxSensors(command.args);
958
+ break;
959
+ case "audio_samplerate":
960
+ this.state.audio = {
961
+ sampleRate: parseNumber(command.args[0]) ?? this.state.audio?.sampleRate ?? 12e3,
962
+ sampleType: this.state.audio?.sampleType ?? 3 /* FLOAT32 */,
963
+ channels: this.state.audio?.channels ?? 1,
964
+ samplesPerFrame: this.state.audio?.samplesPerFrame ?? 512,
965
+ txBufferingMs: this.state.audio?.txBufferingMs,
966
+ running: this.state.audio?.running ?? false
967
+ };
968
+ break;
969
+ default:
970
+ break;
971
+ }
972
+ if (!readyBefore && this.state.ready) {
973
+ this.emit("ready", this.getState());
974
+ }
975
+ this.emitState();
976
+ }
977
+ applyVfo(args) {
978
+ if (args.length < 3) {
979
+ return;
980
+ }
981
+ const receiver = parseNumber(args[0]);
982
+ const vfo = parseNumber(args[1]);
983
+ const frequency = parseNumber(args[2]);
984
+ if (receiver === void 0 || vfo === void 0 || frequency === void 0 || frequency < 0) {
985
+ return;
986
+ }
987
+ this.state.frequencies[rxVfoKey(receiver, vfo)] = frequency;
988
+ }
989
+ applyModulation(args) {
990
+ if (args.length < 2) {
991
+ return;
992
+ }
993
+ const receiver = parseNumber(args[0]);
994
+ if (receiver === void 0) {
995
+ return;
996
+ }
997
+ const vfo = args.length >= 3 ? parseNumber(args[1]) ?? this.options.vfo : this.options.vfo;
998
+ const mode = args.length >= 3 ? args[2] : args[1];
999
+ if (!mode) {
1000
+ return;
1001
+ }
1002
+ this.state.modes[rxVfoKey(receiver, vfo)] = mode.toLowerCase();
1003
+ }
1004
+ applyTrx(args) {
1005
+ if (args.length < 2) {
1006
+ return;
1007
+ }
1008
+ const trx = args[0] ?? String(this.options.trx);
1009
+ this.state.ptt[trx] = parseBoolean(args[1]) ?? false;
1010
+ this.state.pttSource[trx] = args[2]?.toLowerCase();
1011
+ }
1012
+ applyBooleanByFirstArg(target, args) {
1013
+ if (args.length < 2) {
1014
+ return;
1015
+ }
1016
+ const key = args[0] ?? "0";
1017
+ const value = parseBoolean(args[1]);
1018
+ if (value !== void 0) {
1019
+ target[key] = value;
1020
+ }
1021
+ }
1022
+ applyDrive(args) {
1023
+ if (args.length === 1) {
1024
+ const value2 = parseNumber(args[0]);
1025
+ if (value2 !== void 0) {
1026
+ this.state.drive[String(this.options.trx)] = value2;
1027
+ }
1028
+ return;
1029
+ }
1030
+ const trx = args[0] ?? String(this.options.trx);
1031
+ const value = parseNumber(args[1]);
1032
+ if (value !== void 0) {
1033
+ this.state.drive[trx] = value;
1034
+ }
1035
+ }
1036
+ applyRxChannelSensors(args) {
1037
+ if (args.length < 3) {
1038
+ return;
1039
+ }
1040
+ const key = rxVfoKey(args[0], args[1]);
1041
+ this.state.rxSensors[key] = {
1042
+ receiver: args[0],
1043
+ channel: args[1],
1044
+ levelDbm: parseNumber(args[2]) ?? args[2]
1045
+ };
1046
+ }
1047
+ applyRxSensors(args) {
1048
+ if (args.length < 2) {
1049
+ return;
1050
+ }
1051
+ this.state.rxSensors[String(args[0])] = {
1052
+ receiver: args[0],
1053
+ levelDbm: parseNumber(args[1]) ?? args[1],
1054
+ deprecated: true
1055
+ };
1056
+ }
1057
+ applyTxSensors(args) {
1058
+ if (args.length < 2) {
1059
+ return;
1060
+ }
1061
+ this.state.txSensors[String(args[0])] = {
1062
+ trx: args[0],
1063
+ micDbm: parseNumber(args[1]) ?? args[1],
1064
+ rmsPowerW: parseNumber(args[2]) ?? args[2],
1065
+ peakPowerW: parseNumber(args[3]) ?? args[3],
1066
+ swr: parseNumber(args[4]) ?? args[4]
1067
+ };
1068
+ }
1069
+ handleClose(reason) {
1070
+ this.ws = void 0;
1071
+ const wasConnected = this.state.connected;
1072
+ this.state.connected = false;
1073
+ this.state.ready = false;
1074
+ this.queue.setConnected(false);
1075
+ if (wasConnected) {
1076
+ this.emit("disconnected", reason);
1077
+ this.emitState();
1078
+ }
1079
+ }
1080
+ handleError(error) {
1081
+ const tciError = toTciError(error);
1082
+ this.emit("error", tciError);
1083
+ }
1084
+ emitState() {
1085
+ this.emit("state", this.getState());
1086
+ }
1087
+ };
1088
+ function createTciClient(options) {
1089
+ return new TciClient(options);
1090
+ }
1091
+ function dataToBuffer(data) {
1092
+ if (Buffer.isBuffer(data)) {
1093
+ return data;
1094
+ }
1095
+ if (data instanceof ArrayBuffer) {
1096
+ return Buffer.from(data);
1097
+ }
1098
+ if (Array.isArray(data)) {
1099
+ return Buffer.concat(data.map((item) => dataToBuffer(item)));
1100
+ }
1101
+ throw new TciError("protocol-error", "Unsupported WebSocket data type");
1102
+ }
1103
+ function rxVfoKey(receiver, vfo) {
1104
+ return `${receiver}:${vfo}`;
1105
+ }
1106
+ function parseNumber(value) {
1107
+ if (value === void 0 || value === "") {
1108
+ return void 0;
1109
+ }
1110
+ const number = Number(value);
1111
+ return Number.isFinite(number) ? number : void 0;
1112
+ }
1113
+ function parseBoolean(value) {
1114
+ if (value === void 0) {
1115
+ return void 0;
1116
+ }
1117
+ const normalized = value.toLowerCase();
1118
+ if (normalized === "true" || normalized === "1" || normalized === "on") {
1119
+ return true;
1120
+ }
1121
+ if (normalized === "false" || normalized === "0" || normalized === "off") {
1122
+ return false;
1123
+ }
1124
+ return void 0;
1125
+ }
1126
+ function parseNumberPair(args) {
1127
+ const first = parseNumber(args[0]);
1128
+ const second = parseNumber(args[1]);
1129
+ return first === void 0 || second === void 0 ? void 0 : [first, second];
1130
+ }
1131
+ function cloneState(state) {
1132
+ return {
1133
+ ...state,
1134
+ modulations: [...state.modulations],
1135
+ frequencies: { ...state.frequencies },
1136
+ modes: { ...state.modes },
1137
+ ptt: { ...state.ptt },
1138
+ pttSource: { ...state.pttSource },
1139
+ tune: { ...state.tune },
1140
+ drive: { ...state.drive },
1141
+ split: { ...state.split },
1142
+ rxSensors: cloneNested(state.rxSensors),
1143
+ txSensors: cloneNested(state.txSensors),
1144
+ audio: state.audio ? { ...state.audio } : void 0
1145
+ };
1146
+ }
1147
+ function cloneNested(value) {
1148
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, { ...item }]));
1149
+ }
1150
+ // Annotate the CommonJS export names for ESM import in node:
1151
+ 0 && (module.exports = {
1152
+ TCI_STREAM_HEADER_BYTES,
1153
+ TciClient,
1154
+ TciCommandQueue,
1155
+ TciError,
1156
+ TciSampleType,
1157
+ TciStreamType,
1158
+ buildStreamFrame,
1159
+ buildTxAudioFrame,
1160
+ commandKey,
1161
+ createTciClient,
1162
+ deinterleaveChannels,
1163
+ escapeTciText,
1164
+ float32ToPcm16,
1165
+ formatTciCommand,
1166
+ isCommandReplyTo,
1167
+ mixToMono,
1168
+ normalizeCommandName,
1169
+ normalizeSampleType,
1170
+ normalizeStreamType,
1171
+ parseStreamFrame,
1172
+ parseTciCommand,
1173
+ parseTciText,
1174
+ payloadToFloat32,
1175
+ pcm16ToFloat32,
1176
+ sampleTypeBytes,
1177
+ sampleTypeName,
1178
+ samplesToPayload,
1179
+ toTciError,
1180
+ unescapeTciText
1181
+ });
1182
+ //# sourceMappingURL=index.cjs.map