tci-client-node 0.1.2 → 0.3.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/CHANGELOG.md +17 -0
- package/README.md +57 -11
- package/dist/audio/index.cjs +52 -11
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +17 -3
- package/dist/audio/index.d.ts +17 -3
- package/dist/audio/index.js +51 -11
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +349 -0
- package/dist/dialect/index.cjs.map +1 -0
- package/dist/dialect/index.d.cts +29 -0
- package/dist/dialect/index.d.ts +29 -0
- package/dist/dialect/index.js +310 -0
- package/dist/dialect/index.js.map +1 -0
- package/dist/{index-CK3XdXP3.d.cts → index-lbK6NGY4.d.cts} +1 -1
- package/dist/{index-Dfmrk2MR.d.ts → index-paj1AOJY.d.ts} +1 -1
- package/dist/index.cjs +1057 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -12
- package/dist/index.d.ts +106 -12
- package/dist/index.js +1041 -217
- package/dist/index.js.map +1 -1
- package/dist/protocol/index.cjs.map +1 -1
- package/dist/protocol/index.d.cts +1 -1
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/testing/index.cjs +79 -14
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +79 -14
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/index.cjs +175 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +37 -0
- package/dist/transport/index.d.ts +37 -0
- package/dist/transport/index.js +138 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/types-1YXGwrgI.d.cts +65 -0
- package/dist/types-BtPh4Dbd.d.ts +65 -0
- package/package.json +15 -1
package/dist/index.cjs
CHANGED
|
@@ -33,30 +33,46 @@ __export(src_exports, {
|
|
|
33
33
|
TCI_STREAM_HEADER_BYTES: () => TCI_STREAM_HEADER_BYTES,
|
|
34
34
|
TciClient: () => TciClient,
|
|
35
35
|
TciCommandQueue: () => TciCommandQueue,
|
|
36
|
+
TciDialectRegistry: () => TciDialectRegistry,
|
|
36
37
|
TciError: () => TciError,
|
|
38
|
+
TciIqStreamSession: () => TciIqStreamSession,
|
|
37
39
|
TciSampleType: () => TciSampleType,
|
|
38
40
|
TciStreamType: () => TciStreamType,
|
|
41
|
+
WebSocketTciTransport: () => WebSocketTciTransport,
|
|
42
|
+
aetherSdrDialect: () => aetherSdrDialect,
|
|
43
|
+
assertValidTciHandshake: () => assertValidTciHandshake,
|
|
39
44
|
buildStreamFrame: () => buildStreamFrame,
|
|
40
45
|
buildTxAudioFrame: () => buildTxAudioFrame,
|
|
46
|
+
builtInDialects: () => builtInDialects,
|
|
41
47
|
commandKey: () => commandKey,
|
|
48
|
+
compareTciVersion: () => compareTciVersion,
|
|
42
49
|
createTciClient: () => createTciClient,
|
|
50
|
+
decodeInterleavedIq: () => decodeInterleavedIq,
|
|
51
|
+
defaultTciDialectRegistry: () => defaultTciDialectRegistry,
|
|
43
52
|
deinterleaveChannels: () => deinterleaveChannels,
|
|
44
53
|
escapeTciText: () => escapeTciText,
|
|
54
|
+
expertSdr14Dialect: () => expertSdr14Dialect,
|
|
55
|
+
expertSdrLegacyDialect: () => expertSdrLegacyDialect,
|
|
56
|
+
expertSdrModernDialect: () => expertSdrModernDialect,
|
|
45
57
|
float32ToPcm16: () => float32ToPcm16,
|
|
46
58
|
formatTciCommand: () => formatTciCommand,
|
|
59
|
+
genericObservedDialect: () => genericObservedDialect,
|
|
47
60
|
isCommandReplyTo: () => isCommandReplyTo,
|
|
48
61
|
mixToMono: () => mixToMono,
|
|
49
62
|
normalizeCommandName: () => normalizeCommandName,
|
|
50
63
|
normalizeSampleType: () => normalizeSampleType,
|
|
51
64
|
normalizeStreamType: () => normalizeStreamType,
|
|
65
|
+
parseProtocolIdentity: () => parseProtocolIdentity,
|
|
52
66
|
parseStreamFrame: () => parseStreamFrame,
|
|
53
67
|
parseTciCommand: () => parseTciCommand,
|
|
54
68
|
parseTciText: () => parseTciText,
|
|
69
|
+
parseTciVersion: () => parseTciVersion,
|
|
55
70
|
payloadToFloat32: () => payloadToFloat32,
|
|
56
71
|
pcm16ToFloat32: () => pcm16ToFloat32,
|
|
57
72
|
sampleTypeBytes: () => sampleTypeBytes,
|
|
58
73
|
sampleTypeName: () => sampleTypeName,
|
|
59
74
|
samplesToPayload: () => samplesToPayload,
|
|
75
|
+
thetisDialect: () => thetisDialect,
|
|
60
76
|
toTciError: () => toTciError,
|
|
61
77
|
unescapeTciText: () => unescapeTciText
|
|
62
78
|
});
|
|
@@ -84,8 +100,8 @@ function toTciError(error, fallbackCode = "protocol-error") {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
// src/client/TciClient.ts
|
|
87
|
-
var
|
|
88
|
-
var
|
|
103
|
+
var import_eventemitter32 = require("eventemitter3");
|
|
104
|
+
var import_ws2 = __toESM(require("ws"), 1);
|
|
89
105
|
|
|
90
106
|
// src/audio/streamFrame.ts
|
|
91
107
|
var TCI_STREAM_HEADER_BYTES = 16 * 4;
|
|
@@ -104,7 +120,7 @@ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
|
|
|
104
120
|
TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
|
|
105
121
|
return TciSampleType2;
|
|
106
122
|
})(TciSampleType || {});
|
|
107
|
-
function parseStreamFrame(input) {
|
|
123
|
+
function parseStreamFrame(input, options = {}) {
|
|
108
124
|
const buffer = toBuffer(input);
|
|
109
125
|
if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
|
|
110
126
|
throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
|
|
@@ -113,15 +129,15 @@ function parseStreamFrame(input) {
|
|
|
113
129
|
const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
|
|
114
130
|
const sampleType = normalizeSampleType(header[2]);
|
|
115
131
|
const streamType = normalizeStreamType(header[6]);
|
|
116
|
-
let channels = header[7];
|
|
132
|
+
let channels = header[7] || options.negotiatedChannels || 0;
|
|
117
133
|
const bytesPerSample = sampleTypeBytes(sampleType);
|
|
118
|
-
const
|
|
134
|
+
const headerSampleCount = header[5];
|
|
119
135
|
const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
|
|
120
136
|
if (channels <= 0) {
|
|
121
137
|
if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
|
|
122
138
|
channels = 1;
|
|
123
139
|
} else {
|
|
124
|
-
const inferredChannels =
|
|
140
|
+
const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
|
|
125
141
|
if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
|
|
126
142
|
throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
|
|
127
143
|
}
|
|
@@ -133,16 +149,28 @@ function parseStreamFrame(input) {
|
|
|
133
149
|
if (payloadLength % alignedFrameBytes !== 0) {
|
|
134
150
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
135
151
|
}
|
|
152
|
+
const actualScalarCount = payloadLength / bytesPerSample;
|
|
153
|
+
const requestedSemantics = options.lengthSemantics ?? "auto";
|
|
154
|
+
const lengthSemantics = resolveLengthSemantics(
|
|
155
|
+
requestedSemantics,
|
|
156
|
+
streamType,
|
|
157
|
+
headerSampleCount,
|
|
158
|
+
actualScalarCount,
|
|
159
|
+
channels
|
|
160
|
+
);
|
|
161
|
+
const sampleCount = lengthSemantics === "per-channel" ? headerSampleCount * channels : headerSampleCount;
|
|
136
162
|
if (streamType !== 3 /* TX_CHRONO */) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {
|
|
163
|
+
const expectedPayloadLength = sampleCount * bytesPerSample;
|
|
164
|
+
if (payloadLength !== expectedPayloadLength) {
|
|
140
165
|
throw new TciError(
|
|
141
166
|
"invalid-frame",
|
|
142
|
-
`TCI stream frame length mismatch: header says ${
|
|
167
|
+
`TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
|
|
143
168
|
);
|
|
144
169
|
}
|
|
145
170
|
}
|
|
171
|
+
if (sampleCount % channels !== 0) {
|
|
172
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
173
|
+
}
|
|
146
174
|
return {
|
|
147
175
|
receiver: header[0],
|
|
148
176
|
sampleRate: header[1],
|
|
@@ -154,7 +182,10 @@ function parseStreamFrame(input) {
|
|
|
154
182
|
channels,
|
|
155
183
|
reserved: header.slice(8),
|
|
156
184
|
payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
|
|
157
|
-
|
|
185
|
+
headerSampleCount,
|
|
186
|
+
sampleCount,
|
|
187
|
+
frameCount: sampleCount / channels,
|
|
188
|
+
lengthSemantics
|
|
158
189
|
};
|
|
159
190
|
}
|
|
160
191
|
function buildStreamFrame(options) {
|
|
@@ -168,11 +199,19 @@ function buildStreamFrame(options) {
|
|
|
168
199
|
if (payload.byteLength % (bytesPerSample * channels) !== 0) {
|
|
169
200
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
170
201
|
}
|
|
171
|
-
const derivedSampleCount = payload.byteLength / bytesPerSample
|
|
202
|
+
const derivedSampleCount = payload.byteLength / bytesPerSample;
|
|
172
203
|
const sampleCount = options.sampleCount ?? derivedSampleCount;
|
|
173
204
|
if (!Number.isInteger(sampleCount) || sampleCount < 0) {
|
|
174
205
|
throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
|
|
175
206
|
}
|
|
207
|
+
if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {
|
|
208
|
+
throw new TciError("invalid-frame", `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);
|
|
209
|
+
}
|
|
210
|
+
if (sampleCount % channels !== 0) {
|
|
211
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
212
|
+
}
|
|
213
|
+
const lengthSemantics = options.lengthSemantics === "per-channel" ? "per-channel" : "scalar";
|
|
214
|
+
const headerSampleCount = lengthSemantics === "per-channel" ? sampleCount / channels : sampleCount;
|
|
176
215
|
const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
|
|
177
216
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
178
217
|
const reserved = options.reserved ?? [];
|
|
@@ -182,7 +221,7 @@ function buildStreamFrame(options) {
|
|
|
182
221
|
sampleType,
|
|
183
222
|
options.codec ?? 0,
|
|
184
223
|
options.crc ?? 0,
|
|
185
|
-
|
|
224
|
+
headerSampleCount,
|
|
186
225
|
options.streamType,
|
|
187
226
|
channels,
|
|
188
227
|
...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
|
|
@@ -191,6 +230,13 @@ function buildStreamFrame(options) {
|
|
|
191
230
|
payload.copy(frame, TCI_STREAM_HEADER_BYTES);
|
|
192
231
|
return frame;
|
|
193
232
|
}
|
|
233
|
+
function resolveLengthSemantics(requested, streamType, headerSampleCount, actualScalarCount, channels) {
|
|
234
|
+
if (requested !== "auto") return requested;
|
|
235
|
+
if (streamType === 3 /* TX_CHRONO */ && actualScalarCount === 0) return "scalar";
|
|
236
|
+
if (actualScalarCount === headerSampleCount) return "scalar";
|
|
237
|
+
if (actualScalarCount === headerSampleCount * channels) return "per-channel";
|
|
238
|
+
return "scalar";
|
|
239
|
+
}
|
|
194
240
|
function buildTxAudioFrame(options) {
|
|
195
241
|
return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
|
|
196
242
|
}
|
|
@@ -275,6 +321,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
|
|
|
275
321
|
}
|
|
276
322
|
return output;
|
|
277
323
|
}
|
|
324
|
+
function decodeInterleavedIq(frame) {
|
|
325
|
+
if (frame.streamType !== 0 /* IQ_STREAM */) {
|
|
326
|
+
throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
|
|
327
|
+
}
|
|
328
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
329
|
+
throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
|
|
330
|
+
}
|
|
331
|
+
return payloadToFloat32(frame);
|
|
332
|
+
}
|
|
278
333
|
function samplesToPayload(samples, sampleType) {
|
|
279
334
|
const type = normalizeSampleType(sampleType);
|
|
280
335
|
const bytes = sampleTypeBytes(type);
|
|
@@ -622,13 +677,488 @@ function ensureSemicolon(command) {
|
|
|
622
677
|
return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
|
|
623
678
|
}
|
|
624
679
|
|
|
680
|
+
// src/dialect/builtins.ts
|
|
681
|
+
var StandardTciDialect = class {
|
|
682
|
+
id;
|
|
683
|
+
label;
|
|
684
|
+
streamLengthSemantics;
|
|
685
|
+
supportsStreamChannels;
|
|
686
|
+
supportsTxAudioSource;
|
|
687
|
+
supportsIqStream;
|
|
688
|
+
iqSampleRates;
|
|
689
|
+
driveHasTrx;
|
|
690
|
+
detector;
|
|
691
|
+
resolver;
|
|
692
|
+
constructor(options) {
|
|
693
|
+
this.id = options.id;
|
|
694
|
+
this.label = options.label;
|
|
695
|
+
this.streamLengthSemantics = options.streamLengthSemantics;
|
|
696
|
+
this.supportsStreamChannels = options.supportsStreamChannels;
|
|
697
|
+
this.supportsTxAudioSource = options.supportsTxAudioSource;
|
|
698
|
+
this.supportsIqStream = options.supportsIqStream;
|
|
699
|
+
this.iqSampleRates = [...options.iqSampleRates];
|
|
700
|
+
this.driveHasTrx = options.driveHasTrx;
|
|
701
|
+
this.detector = options.detect;
|
|
702
|
+
this.resolver = options.resolve;
|
|
703
|
+
}
|
|
704
|
+
detect(context) {
|
|
705
|
+
return this.detector(context);
|
|
706
|
+
}
|
|
707
|
+
resolve(context) {
|
|
708
|
+
return this.resolver?.(context) ?? this;
|
|
709
|
+
}
|
|
710
|
+
buildDriveSetArgs(trx, value) {
|
|
711
|
+
return this.driveHasTrx ? [trx, value] : [value];
|
|
712
|
+
}
|
|
713
|
+
buildDriveReadArgs(trx) {
|
|
714
|
+
return this.driveHasTrx ? [trx] : [];
|
|
715
|
+
}
|
|
716
|
+
parseDrive(args, defaultTrx) {
|
|
717
|
+
return parseDriveState(args, defaultTrx, this.driveHasTrx);
|
|
718
|
+
}
|
|
719
|
+
buildTuneDriveSetArgs(trx, value) {
|
|
720
|
+
return this.driveHasTrx ? [trx, value] : [value];
|
|
721
|
+
}
|
|
722
|
+
buildTuneDriveReadArgs(trx) {
|
|
723
|
+
return this.driveHasTrx ? [trx] : [];
|
|
724
|
+
}
|
|
725
|
+
parseTuneDrive(args, defaultTrx) {
|
|
726
|
+
return parseDriveState(args, defaultTrx, this.driveHasTrx);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
function parseDriveState(args, defaultTrx, hasTrx) {
|
|
730
|
+
const trx = hasTrx ? Number(args[0]) : defaultTrx;
|
|
731
|
+
const value = Number(args[hasTrx ? 1 : 0]);
|
|
732
|
+
if (!Number.isInteger(trx) || !Number.isFinite(value)) return void 0;
|
|
733
|
+
return { trx, value };
|
|
734
|
+
}
|
|
735
|
+
function version(context) {
|
|
736
|
+
return parseTciVersion(context.identity.protocolVersion);
|
|
737
|
+
}
|
|
738
|
+
function programIncludes(context, value) {
|
|
739
|
+
return context.identity.programName?.toLowerCase().includes(value) ?? false;
|
|
740
|
+
}
|
|
741
|
+
var expertSdr14Dialect = new StandardTciDialect({
|
|
742
|
+
id: "expertsdr-1.4",
|
|
743
|
+
label: "ExpertSDR / TCI 1.4",
|
|
744
|
+
streamLengthSemantics: "per-channel",
|
|
745
|
+
supportsStreamChannels: false,
|
|
746
|
+
supportsTxAudioSource: false,
|
|
747
|
+
supportsIqStream: true,
|
|
748
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
749
|
+
driveHasTrx: false,
|
|
750
|
+
detect: (context) => {
|
|
751
|
+
const parsed = version(context);
|
|
752
|
+
if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
|
|
753
|
+
return { score: 80 + (programIncludes(context, "expert") ? 10 : 0), evidence: [`protocol ${formatVersion(parsed)} <= 1.4`] };
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
var expertSdrLegacyDialect = new StandardTciDialect({
|
|
757
|
+
id: "expertsdr-1.5-1.8",
|
|
758
|
+
label: "ExpertSDR / TCI 1.5-1.8",
|
|
759
|
+
streamLengthSemantics: "per-channel",
|
|
760
|
+
supportsStreamChannels: false,
|
|
761
|
+
supportsTxAudioSource: false,
|
|
762
|
+
supportsIqStream: true,
|
|
763
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
764
|
+
driveHasTrx: true,
|
|
765
|
+
detect: (context) => {
|
|
766
|
+
const parsed = version(context);
|
|
767
|
+
if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
|
|
768
|
+
return { score: 80 + (programIncludes(context, "expert") ? 10 : 0), evidence: [`protocol ${formatVersion(parsed)} is in 1.5-1.8`] };
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
var expertSdrModernDialect = new StandardTciDialect({
|
|
772
|
+
id: "expertsdr-1.9-2.0",
|
|
773
|
+
label: "ExpertSDR / TCI 1.9-2.0",
|
|
774
|
+
streamLengthSemantics: "scalar",
|
|
775
|
+
supportsStreamChannels: true,
|
|
776
|
+
supportsTxAudioSource: true,
|
|
777
|
+
supportsIqStream: true,
|
|
778
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
779
|
+
driveHasTrx: true,
|
|
780
|
+
detect: (context) => {
|
|
781
|
+
const parsed = version(context);
|
|
782
|
+
if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
|
|
783
|
+
const future = compareTciVersion(parsed, [2, 0]) > 0;
|
|
784
|
+
return {
|
|
785
|
+
score: 70 + (programIncludes(context, "expert") ? 15 : 0),
|
|
786
|
+
evidence: [`protocol ${formatVersion(parsed)} uses modern stream negotiation`],
|
|
787
|
+
warnings: future ? [`Unknown future TCI version ${formatVersion(parsed)}; using the modern dialect`] : []
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
var thetisDialect = new StandardTciDialect({
|
|
792
|
+
id: "thetis-2.0",
|
|
793
|
+
label: "Thetis / TCI 2.0",
|
|
794
|
+
streamLengthSemantics: "scalar",
|
|
795
|
+
supportsStreamChannels: true,
|
|
796
|
+
supportsTxAudioSource: true,
|
|
797
|
+
supportsIqStream: true,
|
|
798
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
799
|
+
driveHasTrx: true,
|
|
800
|
+
detect: (context) => {
|
|
801
|
+
const evidence = [];
|
|
802
|
+
let score = 0;
|
|
803
|
+
if (programIncludes(context, "thetis")) {
|
|
804
|
+
score += 120;
|
|
805
|
+
evidence.push("PROTOCOL program is Thetis");
|
|
806
|
+
}
|
|
807
|
+
const observed = ["tx_frequency_ex", "tx_profiles_ex", "tx_profile_ex", "calibration_ex"].filter((name) => context.commandNames.has(name));
|
|
808
|
+
if (observed.length > 0) {
|
|
809
|
+
score += 100;
|
|
810
|
+
evidence.push(`Thetis extension commands: ${observed.join(", ")}`);
|
|
811
|
+
}
|
|
812
|
+
if (/anan|hermes|orion|saturn/i.test(context.identity.device ?? "")) {
|
|
813
|
+
score += 30;
|
|
814
|
+
evidence.push(`Thetis-family device: ${context.identity.device}`);
|
|
815
|
+
}
|
|
816
|
+
return { score, evidence };
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
var aetherSdrDialect = new StandardTciDialect({
|
|
820
|
+
id: "aethersdr-1.5",
|
|
821
|
+
label: "AetherSDR / TCI 1.5 hybrid",
|
|
822
|
+
streamLengthSemantics: "scalar",
|
|
823
|
+
supportsStreamChannels: true,
|
|
824
|
+
supportsTxAudioSource: true,
|
|
825
|
+
supportsIqStream: true,
|
|
826
|
+
iqSampleRates: [24e3, 48e3, 96e3, 192e3],
|
|
827
|
+
driveHasTrx: true,
|
|
828
|
+
detect: (context) => {
|
|
829
|
+
if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
|
|
830
|
+
const evidence = [`AetherSDR device identity: ${context.identity.device}`];
|
|
831
|
+
const modernAudioCommands = ["audio_stream_sample_type", "audio_stream_channels", "audio_stream_samples"].filter((name) => context.commandNames.has(name));
|
|
832
|
+
if (modernAudioCommands.length > 0) evidence.push(`Modern audio negotiation: ${modernAudioCommands.join(", ")}`);
|
|
833
|
+
return {
|
|
834
|
+
score: 150,
|
|
835
|
+
evidence,
|
|
836
|
+
warnings: context.identity.protocolVersion === "1.5" ? ["AetherSDR reports TCI 1.5 but uses modern scalar audio stream semantics"] : []
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
var genericObservedDialect = new StandardTciDialect({
|
|
841
|
+
id: "generic-observed",
|
|
842
|
+
label: "Generic observed TCI",
|
|
843
|
+
streamLengthSemantics: "auto",
|
|
844
|
+
supportsStreamChannels: true,
|
|
845
|
+
supportsTxAudioSource: true,
|
|
846
|
+
supportsIqStream: false,
|
|
847
|
+
iqSampleRates: [],
|
|
848
|
+
driveHasTrx: true,
|
|
849
|
+
detect: (context) => ({
|
|
850
|
+
score: context.commandNames.has("ready") ? 10 : 0,
|
|
851
|
+
evidence: ["No vendor-specific match; using observed command shapes"],
|
|
852
|
+
warnings: ["Dialect identity is uncertain"]
|
|
853
|
+
}),
|
|
854
|
+
resolve: (context) => {
|
|
855
|
+
const drive = [...context.commands].reverse().find((command) => command.name === "drive");
|
|
856
|
+
const driveHasTrx = (drive?.args.length ?? 0) >= 2;
|
|
857
|
+
return new StandardTciDialect({
|
|
858
|
+
id: "generic-observed",
|
|
859
|
+
label: "Generic observed TCI",
|
|
860
|
+
streamLengthSemantics: "auto",
|
|
861
|
+
supportsStreamChannels: context.commandNames.has("audio_stream_channels"),
|
|
862
|
+
supportsTxAudioSource: compareTciVersion(version(context) ?? [0], [2, 0]) >= 0,
|
|
863
|
+
supportsIqStream: context.commandNames.has("iq_samplerate"),
|
|
864
|
+
iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
|
|
865
|
+
driveHasTrx,
|
|
866
|
+
detect: genericObservedDialect.detect.bind(genericObservedDialect)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
var builtInDialects = [
|
|
871
|
+
aetherSdrDialect,
|
|
872
|
+
thetisDialect,
|
|
873
|
+
expertSdr14Dialect,
|
|
874
|
+
expertSdrLegacyDialect,
|
|
875
|
+
expertSdrModernDialect,
|
|
876
|
+
genericObservedDialect
|
|
877
|
+
];
|
|
878
|
+
function parseTciVersion(value) {
|
|
879
|
+
if (!value) return void 0;
|
|
880
|
+
const match = value.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
881
|
+
if (!match) return void 0;
|
|
882
|
+
return match.slice(1).filter((part) => part !== void 0).map(Number);
|
|
883
|
+
}
|
|
884
|
+
function compareTciVersion(left, right) {
|
|
885
|
+
const length = Math.max(left.length, right.length);
|
|
886
|
+
for (let index = 0; index < length; index += 1) {
|
|
887
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
888
|
+
if (difference !== 0) return difference;
|
|
889
|
+
}
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
function formatVersion(value) {
|
|
893
|
+
return value.join(".");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/dialect/registry.ts
|
|
897
|
+
var TciDialectRegistry = class {
|
|
898
|
+
dialects = /* @__PURE__ */ new Map();
|
|
899
|
+
constructor(dialects = builtInDialects) {
|
|
900
|
+
for (const dialect of dialects) this.register(dialect);
|
|
901
|
+
}
|
|
902
|
+
register(dialect) {
|
|
903
|
+
this.dialects.set(dialect.id, dialect);
|
|
904
|
+
}
|
|
905
|
+
get(id) {
|
|
906
|
+
return this.dialects.get(id);
|
|
907
|
+
}
|
|
908
|
+
list() {
|
|
909
|
+
return [...this.dialects.values()];
|
|
910
|
+
}
|
|
911
|
+
select(context, selection = "auto") {
|
|
912
|
+
if (typeof selection === "object") {
|
|
913
|
+
return { dialect: selection, confidence: "manual", evidence: ["Custom dialect supplied by caller"], warnings: [] };
|
|
914
|
+
}
|
|
915
|
+
if (selection !== "auto") {
|
|
916
|
+
const dialect = this.get(selection);
|
|
917
|
+
if (!dialect) throw new TciError("unknown-dialect", `Unknown TCI dialect: ${selection}`);
|
|
918
|
+
return { dialect: dialect.resolve?.(context) ?? dialect, confidence: "manual", evidence: [`Dialect ${selection} selected by caller`], warnings: [] };
|
|
919
|
+
}
|
|
920
|
+
const candidates = this.list().map((dialect) => ({ dialect, result: dialect.detect(context) })).sort((left, right) => right.result.score - left.result.score);
|
|
921
|
+
const selected = candidates[0];
|
|
922
|
+
if (!selected || selected.result.score <= 0) {
|
|
923
|
+
throw new TciError("unknown-dialect", "Unable to identify the TCI server dialect");
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
dialect: selected.dialect.resolve?.(context) ?? selected.dialect,
|
|
927
|
+
confidence: selected.result.score >= 100 ? "high" : selected.result.score >= 70 ? "medium" : "low",
|
|
928
|
+
evidence: selected.result.evidence,
|
|
929
|
+
warnings: selected.result.warnings ?? []
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
var defaultTciDialectRegistry = new TciDialectRegistry();
|
|
934
|
+
|
|
935
|
+
// src/dialect/handshake.ts
|
|
936
|
+
var IDENTITY_COMMANDS = /* @__PURE__ */ new Set(["protocol", "device", "trx_count", "channels_count", "channel_count"]);
|
|
937
|
+
var STATE_COMMANDS = /* @__PURE__ */ new Set(["vfo", "modulation", "modulations_list", "trx", "drive"]);
|
|
938
|
+
function parseProtocolIdentity(commands) {
|
|
939
|
+
const protocol = [...commands].reverse().find((command) => command.name === "protocol");
|
|
940
|
+
const device = [...commands].reverse().find((command) => command.name === "device");
|
|
941
|
+
const rawProtocolArgs = protocol?.args ?? [];
|
|
942
|
+
const firstLooksLikeVersion = /^\d+(?:\.\d+){0,2}/.test(rawProtocolArgs[0] ?? "");
|
|
943
|
+
return {
|
|
944
|
+
programName: firstLooksLikeVersion ? void 0 : rawProtocolArgs[0],
|
|
945
|
+
protocolVersion: firstLooksLikeVersion ? rawProtocolArgs[0] : rawProtocolArgs[1],
|
|
946
|
+
rawProtocolArgs: [...rawProtocolArgs],
|
|
947
|
+
device: device?.args.join(",")
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function assertValidTciHandshake(commands) {
|
|
951
|
+
const names = new Set(commands.map((command) => command.name));
|
|
952
|
+
if (!names.has("ready")) throw new TciError("handshake-timeout", "TCI READY was not received");
|
|
953
|
+
const categories = [
|
|
954
|
+
[...IDENTITY_COMMANDS].some((name) => names.has(name)),
|
|
955
|
+
[...STATE_COMMANDS].some((name) => names.has(name))
|
|
956
|
+
].filter(Boolean).length;
|
|
957
|
+
const identitySignals = [...IDENTITY_COMMANDS].filter((name) => names.has(name)).length;
|
|
958
|
+
if (categories < 2 && identitySignals < 2) {
|
|
959
|
+
throw new TciError("invalid-handshake", "WebSocket opened but did not provide enough TCI initialization evidence");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/transport/WebSocketTransport.ts
|
|
964
|
+
var import_eventemitter3 = require("eventemitter3");
|
|
965
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
966
|
+
var WebSocketTciTransport = class extends import_eventemitter3.EventEmitter {
|
|
967
|
+
constructor(url, WebSocketImpl = import_ws.default) {
|
|
968
|
+
super();
|
|
969
|
+
this.url = url;
|
|
970
|
+
this.WebSocketImpl = WebSocketImpl;
|
|
971
|
+
}
|
|
972
|
+
url;
|
|
973
|
+
WebSocketImpl;
|
|
974
|
+
socket;
|
|
975
|
+
async connect(timeoutMs) {
|
|
976
|
+
if (this.socket?.readyState === import_ws.default.OPEN) return;
|
|
977
|
+
const socket = new this.WebSocketImpl(this.url);
|
|
978
|
+
this.socket = socket;
|
|
979
|
+
await new Promise((resolve, reject) => {
|
|
980
|
+
const timer = setTimeout(() => {
|
|
981
|
+
cleanup();
|
|
982
|
+
this.terminate();
|
|
983
|
+
reject(new TciError("connect-timeout", `Timed out connecting to ${this.url}`));
|
|
984
|
+
}, timeoutMs);
|
|
985
|
+
const cleanup = () => {
|
|
986
|
+
clearTimeout(timer);
|
|
987
|
+
socket.off("open", onOpen);
|
|
988
|
+
socket.off("close", onCloseBeforeOpen);
|
|
989
|
+
socket.off("error", onErrorBeforeOpen);
|
|
990
|
+
};
|
|
991
|
+
const onOpen = () => {
|
|
992
|
+
cleanup();
|
|
993
|
+
this.attach(socket);
|
|
994
|
+
this.emit("connected");
|
|
995
|
+
resolve();
|
|
996
|
+
};
|
|
997
|
+
const onCloseBeforeOpen = () => {
|
|
998
|
+
cleanup();
|
|
999
|
+
reject(new TciError("disconnected", `Disconnected while connecting to ${this.url}`));
|
|
1000
|
+
};
|
|
1001
|
+
const onErrorBeforeOpen = (error) => {
|
|
1002
|
+
cleanup();
|
|
1003
|
+
reject(toTciError(error, "disconnected"));
|
|
1004
|
+
};
|
|
1005
|
+
socket.once("open", onOpen);
|
|
1006
|
+
socket.once("close", onCloseBeforeOpen);
|
|
1007
|
+
socket.once("error", onErrorBeforeOpen);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
async disconnect(code = 1e3, reason = "client disconnect") {
|
|
1011
|
+
const socket = this.socket;
|
|
1012
|
+
if (!socket || socket.readyState === import_ws.default.CLOSED) return;
|
|
1013
|
+
await new Promise((resolve) => {
|
|
1014
|
+
const timer = setTimeout(resolve, 1e3);
|
|
1015
|
+
timer.unref?.();
|
|
1016
|
+
socket.once("close", () => {
|
|
1017
|
+
clearTimeout(timer);
|
|
1018
|
+
resolve();
|
|
1019
|
+
});
|
|
1020
|
+
socket.once("error", () => {
|
|
1021
|
+
clearTimeout(timer);
|
|
1022
|
+
resolve();
|
|
1023
|
+
});
|
|
1024
|
+
socket.close(code, reason);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
isConnected() {
|
|
1028
|
+
return this.socket?.readyState === import_ws.default.OPEN;
|
|
1029
|
+
}
|
|
1030
|
+
async sendText(raw) {
|
|
1031
|
+
await this.send(raw);
|
|
1032
|
+
}
|
|
1033
|
+
async sendBinary(raw) {
|
|
1034
|
+
await this.send(raw, true);
|
|
1035
|
+
}
|
|
1036
|
+
terminate() {
|
|
1037
|
+
try {
|
|
1038
|
+
this.socket?.terminate();
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
attach(socket) {
|
|
1043
|
+
socket.on("message", (data, isBinary) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const buffer = dataToBuffer(data);
|
|
1046
|
+
if (isBinary) this.emit("binary", buffer);
|
|
1047
|
+
else this.emit("text", buffer.toString("utf8"));
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
socket.on("close", (code, reason) => {
|
|
1053
|
+
if (this.socket === socket) this.socket = void 0;
|
|
1054
|
+
this.emit("disconnected", { code, reason: reason.toString("utf8") });
|
|
1055
|
+
});
|
|
1056
|
+
socket.on("error", (error) => this.emit("error", error));
|
|
1057
|
+
}
|
|
1058
|
+
async send(data, binary = false) {
|
|
1059
|
+
const socket = this.socket;
|
|
1060
|
+
if (!socket || socket.readyState !== import_ws.default.OPEN) {
|
|
1061
|
+
throw new TciError("not-connected", "TCI socket is not connected");
|
|
1062
|
+
}
|
|
1063
|
+
await new Promise((resolve, reject) => {
|
|
1064
|
+
socket.send(data, { binary }, (error) => error ? reject(error) : resolve());
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
function dataToBuffer(data) {
|
|
1069
|
+
if (Buffer.isBuffer(data)) return data;
|
|
1070
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
1071
|
+
if (Array.isArray(data)) return Buffer.concat(data.map((item) => dataToBuffer(item)));
|
|
1072
|
+
throw new TciError("protocol-error", "Unsupported WebSocket data type");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
625
1075
|
// src/client/TciClient.ts
|
|
626
|
-
var
|
|
1076
|
+
var TciIqStreamSession = class extends import_eventemitter32.EventEmitter {
|
|
1077
|
+
receiver;
|
|
1078
|
+
_appliedSampleRate;
|
|
1079
|
+
callbacks;
|
|
1080
|
+
closed = false;
|
|
1081
|
+
firstFrame;
|
|
1082
|
+
constructor(receiver, appliedSampleRate, callbacks) {
|
|
1083
|
+
super();
|
|
1084
|
+
this.receiver = receiver;
|
|
1085
|
+
this._appliedSampleRate = appliedSampleRate;
|
|
1086
|
+
this.callbacks = callbacks;
|
|
1087
|
+
}
|
|
1088
|
+
get appliedSampleRate() {
|
|
1089
|
+
return this._appliedSampleRate;
|
|
1090
|
+
}
|
|
1091
|
+
async setSampleRate(sampleRate) {
|
|
1092
|
+
if (this.closed) throw new TciError("cancelled", "TCI IQ stream is closed");
|
|
1093
|
+
const result = await this.callbacks.setSampleRate(sampleRate);
|
|
1094
|
+
this._appliedSampleRate = result.applied;
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
async close() {
|
|
1098
|
+
if (this.closed) return;
|
|
1099
|
+
this.closed = true;
|
|
1100
|
+
this.rejectFirstFrame(new TciError("cancelled", "TCI IQ stream closed before its first frame"));
|
|
1101
|
+
try {
|
|
1102
|
+
await this.callbacks.close();
|
|
1103
|
+
} finally {
|
|
1104
|
+
this.emit("closed");
|
|
1105
|
+
this.removeAllListeners();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
waitForFirstFrame(timeoutMs) {
|
|
1109
|
+
if (this.firstFrame) return this.firstFrame.promise;
|
|
1110
|
+
let resolvePromise;
|
|
1111
|
+
let rejectPromise;
|
|
1112
|
+
const promise = new Promise((resolve, reject) => {
|
|
1113
|
+
resolvePromise = resolve;
|
|
1114
|
+
rejectPromise = reject;
|
|
1115
|
+
});
|
|
1116
|
+
const timer = setTimeout(() => {
|
|
1117
|
+
this.firstFrame = void 0;
|
|
1118
|
+
rejectPromise(new TciError("command-timeout", `Timed out waiting for TCI IQ frame for receiver ${this.receiver}`));
|
|
1119
|
+
}, timeoutMs);
|
|
1120
|
+
this.firstFrame = { promise, resolve: resolvePromise, reject: rejectPromise, timer };
|
|
1121
|
+
return promise;
|
|
1122
|
+
}
|
|
1123
|
+
_acceptFrame(frame) {
|
|
1124
|
+
if (this.closed || frame.receiver !== this.receiver) return;
|
|
1125
|
+
this._appliedSampleRate = frame.sampleRate;
|
|
1126
|
+
const firstFrame = this.firstFrame;
|
|
1127
|
+
if (firstFrame) {
|
|
1128
|
+
clearTimeout(firstFrame.timer);
|
|
1129
|
+
this.firstFrame = void 0;
|
|
1130
|
+
firstFrame.resolve(frame);
|
|
1131
|
+
}
|
|
1132
|
+
this.emit("frame", frame);
|
|
1133
|
+
}
|
|
1134
|
+
_fail(error) {
|
|
1135
|
+
if (this.closed) return;
|
|
1136
|
+
this.rejectFirstFrame(error);
|
|
1137
|
+
this.emit("error", error);
|
|
1138
|
+
}
|
|
1139
|
+
rejectFirstFrame(error) {
|
|
1140
|
+
const firstFrame = this.firstFrame;
|
|
1141
|
+
if (!firstFrame) return;
|
|
1142
|
+
clearTimeout(firstFrame.timer);
|
|
1143
|
+
this.firstFrame = void 0;
|
|
1144
|
+
firstFrame.reject(error);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
627
1148
|
options;
|
|
628
1149
|
WebSocketImpl;
|
|
629
|
-
|
|
1150
|
+
transportFactory;
|
|
1151
|
+
transport;
|
|
630
1152
|
queue;
|
|
631
1153
|
state;
|
|
1154
|
+
stateReducers;
|
|
1155
|
+
dialectRegistry;
|
|
1156
|
+
activeDialect;
|
|
1157
|
+
handshakeResult;
|
|
1158
|
+
handshakeError;
|
|
1159
|
+
initializationCommands = [];
|
|
1160
|
+
handshakeWaiter;
|
|
1161
|
+
activeIqSession;
|
|
632
1162
|
constructor(options) {
|
|
633
1163
|
super();
|
|
634
1164
|
this.options = {
|
|
@@ -637,13 +1167,17 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
637
1167
|
trx: options.trx ?? 0,
|
|
638
1168
|
vfo: options.vfo ?? 0,
|
|
639
1169
|
connectTimeoutMs: options.connectTimeoutMs ?? 5e3,
|
|
1170
|
+
handshakeTimeoutMs: options.handshakeTimeoutMs ?? 1e4,
|
|
640
1171
|
commandTimeoutMs: options.commandTimeoutMs ?? 1e3,
|
|
641
1172
|
writeAckMode: options.writeAckMode ?? "state",
|
|
642
1173
|
writeTimeoutMs: options.writeTimeoutMs ?? 3e3,
|
|
643
1174
|
writeSettleMs: options.writeSettleMs ?? 0,
|
|
644
|
-
frequencyWriteSettleMs: options.frequencyWriteSettleMs ?? 250
|
|
1175
|
+
frequencyWriteSettleMs: options.frequencyWriteSettleMs ?? 250,
|
|
1176
|
+
dialect: options.dialect ?? "auto"
|
|
645
1177
|
};
|
|
646
|
-
this.
|
|
1178
|
+
this.dialectRegistry = options.dialectRegistry ?? defaultTciDialectRegistry;
|
|
1179
|
+
this.WebSocketImpl = options.WebSocketImpl ?? import_ws2.default;
|
|
1180
|
+
this.transportFactory = options.transportFactory ?? ((url) => new WebSocketTciTransport(url, this.WebSocketImpl));
|
|
647
1181
|
this.queue = new TciCommandQueue({
|
|
648
1182
|
timeoutMs: this.options.commandTimeoutMs,
|
|
649
1183
|
send: (raw) => this.sendRaw(raw)
|
|
@@ -659,58 +1193,113 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
659
1193
|
pttSource: {},
|
|
660
1194
|
tune: {},
|
|
661
1195
|
drive: {},
|
|
1196
|
+
tuneDrive: {},
|
|
662
1197
|
split: {},
|
|
1198
|
+
dialectWarnings: [],
|
|
663
1199
|
rxSensors: {},
|
|
664
|
-
txSensors: {}
|
|
1200
|
+
txSensors: {},
|
|
1201
|
+
iq: { activeReceivers: {} },
|
|
1202
|
+
dds: {}
|
|
665
1203
|
};
|
|
1204
|
+
this.stateReducers = this.createStateReducers();
|
|
666
1205
|
}
|
|
667
1206
|
async connect() {
|
|
668
|
-
if (this.
|
|
669
|
-
return;
|
|
1207
|
+
if (this.transport?.isConnected()) {
|
|
1208
|
+
return this.handshakeResult ?? this.waitForHandshake();
|
|
670
1209
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1210
|
+
this.resetHandshake();
|
|
1211
|
+
const transport = this.transportFactory(this.options.url);
|
|
1212
|
+
this.transport = transport;
|
|
1213
|
+
this.attachTransport(transport);
|
|
1214
|
+
await transport.connect(this.options.connectTimeoutMs);
|
|
1215
|
+
this.state.connected = true;
|
|
1216
|
+
this.queue.setConnected(true);
|
|
1217
|
+
this.emit("connected");
|
|
1218
|
+
this.emitState();
|
|
1219
|
+
try {
|
|
1220
|
+
return await this.waitForHandshake();
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
transport.terminate();
|
|
1223
|
+
throw error;
|
|
674
1224
|
}
|
|
675
|
-
const ws = new this.WebSocketImpl(this.options.url);
|
|
676
|
-
this.ws = ws;
|
|
677
|
-
await this.waitForOpen(ws);
|
|
678
1225
|
}
|
|
679
1226
|
async disconnect(code = 1e3, reason = "client disconnect") {
|
|
680
|
-
const
|
|
681
|
-
if (!
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
if (ws.readyState === import_ws.default.CLOSED) {
|
|
685
|
-
this.handleClose();
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
await new Promise((resolve) => {
|
|
689
|
-
const cleanup = () => {
|
|
690
|
-
ws.off("close", onClose);
|
|
691
|
-
ws.off("error", onError);
|
|
692
|
-
};
|
|
693
|
-
const onClose = () => {
|
|
694
|
-
cleanup();
|
|
695
|
-
resolve();
|
|
696
|
-
};
|
|
697
|
-
const onError = () => {
|
|
698
|
-
cleanup();
|
|
699
|
-
resolve();
|
|
700
|
-
};
|
|
701
|
-
ws.once("close", onClose);
|
|
702
|
-
ws.once("error", onError);
|
|
703
|
-
ws.close(code, reason);
|
|
704
|
-
setTimeout(() => resolve(), 1e3).unref?.();
|
|
705
|
-
});
|
|
1227
|
+
const transport = this.transport;
|
|
1228
|
+
if (!transport) return;
|
|
1229
|
+
await transport.disconnect(code, reason);
|
|
706
1230
|
this.handleClose();
|
|
707
1231
|
}
|
|
708
1232
|
isConnected() {
|
|
709
|
-
return this.
|
|
1233
|
+
return this.transport?.isConnected() ?? false;
|
|
710
1234
|
}
|
|
711
1235
|
getState() {
|
|
712
1236
|
return cloneState(this.state);
|
|
713
1237
|
}
|
|
1238
|
+
getHandshakeResult() {
|
|
1239
|
+
return this.handshakeResult ? cloneHandshake(this.handshakeResult) : void 0;
|
|
1240
|
+
}
|
|
1241
|
+
getIqCapabilities() {
|
|
1242
|
+
const dialect = this.requireDialect();
|
|
1243
|
+
return {
|
|
1244
|
+
supported: dialect.supportsIqStream,
|
|
1245
|
+
currentSampleRate: this.state.iq.sampleRate,
|
|
1246
|
+
supportedSampleRates: [...dialect.iqSampleRates]
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
|
|
1250
|
+
const capabilities = this.getIqCapabilities();
|
|
1251
|
+
const requested = Math.round(sampleRate);
|
|
1252
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1253
|
+
if (!Number.isFinite(requested) || requested <= 0) throw new TciError("protocol-error", `Invalid TCI IQ sample rate: ${sampleRate}`);
|
|
1254
|
+
if (capabilities.supportedSampleRates.length > 0 && !capabilities.supportedSampleRates.includes(requested)) {
|
|
1255
|
+
throw new TciError("protocol-error", `TCI IQ sample rate ${requested} is not supported by dialect ${this.requireDialect().id}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (this.state.iq.sampleRate === requested) return writeResult(requested, requested, "state");
|
|
1258
|
+
const waiter = this.waitForCommand((command) => command.name === "iq_samplerate", timeoutMs, "IQ_SAMPLERATE readback");
|
|
1259
|
+
try {
|
|
1260
|
+
await this.sendCommand("IQ_SAMPLERATE", [requested], { waitForReply: false });
|
|
1261
|
+
const reply = await waiter.promise;
|
|
1262
|
+
const applied = parseNumber(reply.args[0]);
|
|
1263
|
+
if (applied === void 0 || applied <= 0) throw new TciError("protocol-error", `Invalid IQ_SAMPLERATE readback: ${reply.raw}`);
|
|
1264
|
+
return writeResult(requested, applied, "reply");
|
|
1265
|
+
} finally {
|
|
1266
|
+
waiter.cancel();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
async openIqStream(options = {}) {
|
|
1270
|
+
if (this.activeIqSession) throw new TciError("protocol-error", "This TCI client already has an active IQ stream session");
|
|
1271
|
+
const capabilities = this.getIqCapabilities();
|
|
1272
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1273
|
+
const receiver = Math.floor(options.receiver ?? this.options.receiver);
|
|
1274
|
+
if (!Number.isInteger(receiver) || receiver < 0) throw new TciError("protocol-error", `Invalid TCI IQ receiver: ${receiver}`);
|
|
1275
|
+
const requestedRate = options.sampleRate ?? capabilities.currentSampleRate ?? capabilities.supportedSampleRates[0] ?? 48e3;
|
|
1276
|
+
const rateResult = await this.setIqSampleRate(requestedRate);
|
|
1277
|
+
let session;
|
|
1278
|
+
session = new TciIqStreamSession(receiver, rateResult.applied, {
|
|
1279
|
+
setSampleRate: (rate) => this.setIqSampleRate(rate),
|
|
1280
|
+
close: async () => {
|
|
1281
|
+
try {
|
|
1282
|
+
if (this.isConnected()) await this.sendCommand("IQ_STOP", [receiver], { waitForReply: false });
|
|
1283
|
+
} finally {
|
|
1284
|
+
this.state.iq.activeReceivers[String(receiver)] = false;
|
|
1285
|
+
if (this.activeIqSession === session) this.activeIqSession = void 0;
|
|
1286
|
+
this.emitState();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
this.activeIqSession = session;
|
|
1291
|
+
const firstFrame = session.waitForFirstFrame(options.firstFrameTimeoutMs ?? 5e3);
|
|
1292
|
+
try {
|
|
1293
|
+
await this.sendCommand("IQ_START", [receiver], { waitForReply: false });
|
|
1294
|
+
this.state.iq.activeReceivers[String(receiver)] = true;
|
|
1295
|
+
this.emitState();
|
|
1296
|
+
await firstFrame;
|
|
1297
|
+
return session;
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
await session.close().catch(() => void 0);
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
714
1303
|
async sendCommand(name, args = [], options = {}) {
|
|
715
1304
|
const raw = formatTciCommand(name, args);
|
|
716
1305
|
if (options.waitForReply === false) {
|
|
@@ -784,6 +1373,9 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
784
1373
|
}
|
|
785
1374
|
async setPtt(enabled, options = {}) {
|
|
786
1375
|
const trx = options.trx ?? this.options.trx;
|
|
1376
|
+
if (options.source && !this.requireDialect().supportsTxAudioSource) {
|
|
1377
|
+
throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support a TRX audio source argument`);
|
|
1378
|
+
}
|
|
787
1379
|
const args = options.source ? [trx, enabled, options.source] : [trx, enabled];
|
|
788
1380
|
await this.sendStateWrite(
|
|
789
1381
|
"TRX",
|
|
@@ -797,16 +1389,107 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
797
1389
|
const reply = await this.request("TRX", [trx]);
|
|
798
1390
|
return parseBoolean(reply.args[1]) ?? this.state.ptt[String(trx)];
|
|
799
1391
|
}
|
|
800
|
-
async setTune(enabled, trx = this.options.trx) {
|
|
801
|
-
|
|
1392
|
+
async setTune(enabled, trx = this.options.trx, options = {}) {
|
|
1393
|
+
try {
|
|
1394
|
+
await this.sendStateWrite(
|
|
1395
|
+
"TUNE",
|
|
1396
|
+
[trx, enabled],
|
|
1397
|
+
(state) => state.tune[String(trx)] === enabled,
|
|
1398
|
+
`TUNE:${trx},${enabled}`,
|
|
1399
|
+
options
|
|
1400
|
+
);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
if (!(error instanceof TciError) || error.code !== "command-timeout") throw error;
|
|
1403
|
+
await this.request("TUNE", [trx], { timeoutMs: options.timeoutMs });
|
|
1404
|
+
if (this.state.tune[String(trx)] !== enabled) throw error;
|
|
1405
|
+
}
|
|
802
1406
|
}
|
|
803
1407
|
async setDrive(value, trx = this.options.trx) {
|
|
804
|
-
await this.
|
|
1408
|
+
await this.setDriveWithResult(value, trx);
|
|
1409
|
+
}
|
|
1410
|
+
async setDriveWithResult(value, trx = this.options.trx, options = {}) {
|
|
1411
|
+
const requested = normalizePercent(value);
|
|
1412
|
+
const dialect = this.requireDialect();
|
|
1413
|
+
if (this.state.drive[String(trx)] === requested) {
|
|
1414
|
+
return writeResult(requested, requested, "state");
|
|
1415
|
+
}
|
|
1416
|
+
const timeoutMs = options.timeoutMs ?? this.options.writeTimeoutMs;
|
|
1417
|
+
const waiter = this.waitForCommand(
|
|
1418
|
+
(command) => command.name === "drive" && dialect.parseDrive(command.args, trx)?.trx === trx,
|
|
1419
|
+
Math.min(500, timeoutMs),
|
|
1420
|
+
`DRIVE state for TRX ${trx}`
|
|
1421
|
+
);
|
|
1422
|
+
await this.sendCommand("DRIVE", dialect.buildDriveSetArgs(trx, requested), { waitForReply: false });
|
|
1423
|
+
try {
|
|
1424
|
+
const command = await waiter.promise;
|
|
1425
|
+
const applied2 = dialect.parseDrive(command.args, trx)?.value;
|
|
1426
|
+
if (applied2 !== void 0) return writeResult(requested, applied2, "state");
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
if (!(error instanceof TciError) || error.code !== "command-timeout") throw error;
|
|
1429
|
+
} finally {
|
|
1430
|
+
waiter.cancel();
|
|
1431
|
+
}
|
|
1432
|
+
const reply = await this.request("DRIVE", dialect.buildDriveReadArgs(trx), {
|
|
1433
|
+
timeoutMs: Math.max(1, timeoutMs - Math.min(500, timeoutMs))
|
|
1434
|
+
});
|
|
1435
|
+
const applied = dialect.parseDrive(reply.args, trx)?.value;
|
|
1436
|
+
if (applied === void 0) throw new TciError("protocol-error", `Invalid DRIVE readback: ${reply.raw}`);
|
|
1437
|
+
return writeResult(requested, applied, "readback");
|
|
1438
|
+
}
|
|
1439
|
+
async getDrive(trx = this.options.trx) {
|
|
1440
|
+
const dialect = this.requireDialect();
|
|
1441
|
+
const reply = await this.request("DRIVE", dialect.buildDriveReadArgs(trx));
|
|
1442
|
+
return dialect.parseDrive(reply.args, trx)?.value ?? this.state.drive[String(trx)];
|
|
1443
|
+
}
|
|
1444
|
+
async setTuneDrive(value, trx = this.options.trx, options = {}) {
|
|
1445
|
+
const requested = normalizePercent(value);
|
|
1446
|
+
const dialect = this.requireDialect();
|
|
1447
|
+
if (this.state.tuneDrive[String(trx)] === requested) return writeResult(requested, requested, "state");
|
|
1448
|
+
const timeoutMs = options.timeoutMs ?? this.options.writeTimeoutMs;
|
|
1449
|
+
const waiter = this.waitForCommand(
|
|
1450
|
+
(command) => command.name === "tune_drive" && dialect.parseTuneDrive(command.args, trx)?.trx === trx,
|
|
1451
|
+
Math.min(500, timeoutMs),
|
|
1452
|
+
`TUNE_DRIVE state for TRX ${trx}`
|
|
1453
|
+
);
|
|
1454
|
+
await this.sendCommand("TUNE_DRIVE", dialect.buildTuneDriveSetArgs(trx, requested), { waitForReply: false });
|
|
1455
|
+
try {
|
|
1456
|
+
const command = await waiter.promise;
|
|
1457
|
+
const applied2 = dialect.parseTuneDrive(command.args, trx)?.value;
|
|
1458
|
+
if (applied2 !== void 0) return writeResult(requested, applied2, "state");
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (!(error instanceof TciError) || error.code !== "command-timeout") throw error;
|
|
1461
|
+
} finally {
|
|
1462
|
+
waiter.cancel();
|
|
1463
|
+
}
|
|
1464
|
+
const reply = await this.request("TUNE_DRIVE", dialect.buildTuneDriveReadArgs(trx), {
|
|
1465
|
+
timeoutMs: Math.max(1, timeoutMs - Math.min(500, timeoutMs))
|
|
1466
|
+
});
|
|
1467
|
+
const applied = dialect.parseTuneDrive(reply.args, trx)?.value;
|
|
1468
|
+
if (applied === void 0) throw new TciError("protocol-error", `Invalid TUNE_DRIVE readback: ${reply.raw}`);
|
|
1469
|
+
return writeResult(requested, applied, "readback");
|
|
805
1470
|
}
|
|
806
|
-
async
|
|
807
|
-
|
|
1471
|
+
async getTuneDrive(trx = this.options.trx) {
|
|
1472
|
+
const dialect = this.requireDialect();
|
|
1473
|
+
const reply = await this.request("TUNE_DRIVE", dialect.buildTuneDriveReadArgs(trx));
|
|
1474
|
+
return dialect.parseTuneDrive(reply.args, trx)?.value ?? this.state.tuneDrive[String(trx)];
|
|
1475
|
+
}
|
|
1476
|
+
async setSplit(enabled, trx = this.options.trx, options = {}) {
|
|
1477
|
+
try {
|
|
1478
|
+
await this.sendStateWrite(
|
|
1479
|
+
"SPLIT_ENABLE",
|
|
1480
|
+
[trx, enabled],
|
|
1481
|
+
(state) => state.split[String(trx)] === enabled,
|
|
1482
|
+
`SPLIT_ENABLE:${trx},${enabled}`,
|
|
1483
|
+
options
|
|
1484
|
+
);
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
if (!(error instanceof TciError) || error.code !== "command-timeout") throw error;
|
|
1487
|
+
await this.request("SPLIT_ENABLE", [trx], { timeoutMs: options.timeoutMs });
|
|
1488
|
+
if (this.state.split[String(trx)] !== enabled) throw error;
|
|
1489
|
+
}
|
|
808
1490
|
}
|
|
809
1491
|
async configureAudio(config) {
|
|
1492
|
+
const dialect = this.requireDialect();
|
|
810
1493
|
const audio = {
|
|
811
1494
|
sampleRate: config.sampleRate,
|
|
812
1495
|
sampleType: normalizeSampleType(config.sampleType ?? 3 /* FLOAT32 */),
|
|
@@ -817,11 +1500,13 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
817
1500
|
};
|
|
818
1501
|
this.state.audio = audio;
|
|
819
1502
|
await this.sendCommand("AUDIO_SAMPLERATE", [audio.sampleRate], { waitForReply: false });
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1503
|
+
if (dialect.supportsStreamChannels) {
|
|
1504
|
+
await this.sendCommand("AUDIO_STREAM_SAMPLE_TYPE", [sampleTypeName(audio.sampleType)], { waitForReply: false });
|
|
1505
|
+
await this.sendCommand("AUDIO_STREAM_CHANNELS", [audio.channels], { waitForReply: false });
|
|
1506
|
+
await this.sendCommand("AUDIO_STREAM_SAMPLES", [audio.samplesPerFrame], { waitForReply: false });
|
|
1507
|
+
if (audio.txBufferingMs !== void 0) {
|
|
1508
|
+
await this.sendCommand("TX_STREAM_AUDIO_BUFFERING", [audio.txBufferingMs], { waitForReply: false });
|
|
1509
|
+
}
|
|
825
1510
|
}
|
|
826
1511
|
this.emitState();
|
|
827
1512
|
}
|
|
@@ -840,12 +1525,16 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
840
1525
|
}
|
|
841
1526
|
}
|
|
842
1527
|
sendTxAudio(options) {
|
|
843
|
-
const frame = buildTxAudioFrame({
|
|
1528
|
+
const frame = buildTxAudioFrame({
|
|
1529
|
+
receiver: this.options.receiver,
|
|
1530
|
+
lengthSemantics: this.requireDialect().streamLengthSemantics,
|
|
1531
|
+
...options
|
|
1532
|
+
});
|
|
844
1533
|
this.sendRawBinary(frame);
|
|
845
1534
|
}
|
|
846
1535
|
sendTxAudioForChrono(request, samples) {
|
|
847
1536
|
const channels = Math.max(1, Math.floor(request.channels || 1));
|
|
848
|
-
const targetSampleLength = Math.max(0, Math.floor(request.sampleCount)
|
|
1537
|
+
const targetSampleLength = Math.max(0, Math.floor(request.sampleCount));
|
|
849
1538
|
const output = new Float32Array(targetSampleLength);
|
|
850
1539
|
const source = samples instanceof Float32Array ? samples : Float32Array.from(samples);
|
|
851
1540
|
output.set(source.subarray(0, output.length));
|
|
@@ -943,78 +1632,147 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
943
1632
|
cancel: cleanup
|
|
944
1633
|
};
|
|
945
1634
|
}
|
|
946
|
-
|
|
1635
|
+
waitForCommand(predicate, timeoutMs, description) {
|
|
1636
|
+
let timer;
|
|
1637
|
+
let resolvePromise;
|
|
1638
|
+
let rejectPromise;
|
|
1639
|
+
const cleanup = () => {
|
|
1640
|
+
if (timer) clearTimeout(timer);
|
|
1641
|
+
timer = void 0;
|
|
1642
|
+
this.off("command", onCommand);
|
|
1643
|
+
this.off("disconnected", onDisconnected);
|
|
1644
|
+
};
|
|
1645
|
+
const onCommand = (command) => {
|
|
1646
|
+
if (!predicate(command)) return;
|
|
1647
|
+
cleanup();
|
|
1648
|
+
resolvePromise(command);
|
|
1649
|
+
};
|
|
1650
|
+
const onDisconnected = () => {
|
|
1651
|
+
cleanup();
|
|
1652
|
+
rejectPromise(new TciError("disconnected", `Disconnected while waiting for ${description}`));
|
|
1653
|
+
};
|
|
1654
|
+
const promise = new Promise((resolve, reject) => {
|
|
1655
|
+
resolvePromise = resolve;
|
|
1656
|
+
rejectPromise = reject;
|
|
1657
|
+
timer = setTimeout(() => {
|
|
1658
|
+
cleanup();
|
|
1659
|
+
reject(new TciError("command-timeout", `Timed out waiting for ${description}`));
|
|
1660
|
+
}, timeoutMs);
|
|
1661
|
+
this.on("command", onCommand);
|
|
1662
|
+
this.on("disconnected", onDisconnected);
|
|
1663
|
+
});
|
|
1664
|
+
return { promise, cancel: cleanup };
|
|
1665
|
+
}
|
|
1666
|
+
resetHandshake() {
|
|
1667
|
+
this.rejectHandshake(new TciError("cancelled", "TCI handshake replaced by a new connection"));
|
|
1668
|
+
this.handshakeResult = void 0;
|
|
1669
|
+
this.handshakeError = void 0;
|
|
1670
|
+
this.activeDialect = void 0;
|
|
1671
|
+
this.initializationCommands = [];
|
|
1672
|
+
this.state.ready = false;
|
|
1673
|
+
this.state.protocol = void 0;
|
|
1674
|
+
this.state.protocolName = void 0;
|
|
1675
|
+
this.state.protocolVersion = void 0;
|
|
1676
|
+
this.state.dialectId = void 0;
|
|
1677
|
+
this.state.dialectConfidence = void 0;
|
|
1678
|
+
this.state.dialectWarnings = [];
|
|
1679
|
+
}
|
|
1680
|
+
waitForHandshake() {
|
|
1681
|
+
if (this.handshakeResult) return Promise.resolve(cloneHandshake(this.handshakeResult));
|
|
1682
|
+
if (this.handshakeError) return Promise.reject(this.handshakeError);
|
|
1683
|
+
if (this.handshakeWaiter) {
|
|
1684
|
+
return new Promise((resolve, reject) => {
|
|
1685
|
+
const onHandshake = (result) => {
|
|
1686
|
+
cleanup();
|
|
1687
|
+
resolve(result);
|
|
1688
|
+
};
|
|
1689
|
+
const onError = (error) => {
|
|
1690
|
+
cleanup();
|
|
1691
|
+
reject(error);
|
|
1692
|
+
};
|
|
1693
|
+
const cleanup = () => {
|
|
1694
|
+
this.off("handshake", onHandshake);
|
|
1695
|
+
this.off("error", onError);
|
|
1696
|
+
};
|
|
1697
|
+
this.once("handshake", onHandshake);
|
|
1698
|
+
this.once("error", onError);
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
947
1701
|
return new Promise((resolve, reject) => {
|
|
948
1702
|
const timer = setTimeout(() => {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
reject(new TciError("connect-timeout", `Timed out connecting to ${this.options.url}`));
|
|
955
|
-
}, this.options.connectTimeoutMs);
|
|
956
|
-
const cleanup = () => {
|
|
957
|
-
clearTimeout(timer);
|
|
958
|
-
ws.off("open", onOpen);
|
|
959
|
-
ws.off("close", onClose);
|
|
960
|
-
ws.off("error", onError);
|
|
961
|
-
};
|
|
962
|
-
const onOpen = () => {
|
|
963
|
-
cleanup();
|
|
964
|
-
this.attachSocket(ws);
|
|
965
|
-
this.state.connected = true;
|
|
966
|
-
this.queue.setConnected(true);
|
|
967
|
-
this.emit("connected");
|
|
968
|
-
this.emitState();
|
|
969
|
-
resolve();
|
|
970
|
-
};
|
|
971
|
-
const onClose = () => {
|
|
972
|
-
cleanup();
|
|
973
|
-
this.handleClose();
|
|
974
|
-
reject(new TciError("disconnected", `Disconnected while connecting to ${this.options.url}`));
|
|
975
|
-
};
|
|
976
|
-
const onError = (error) => {
|
|
977
|
-
cleanup();
|
|
978
|
-
this.handleError(error);
|
|
979
|
-
reject(toTciError(error, "disconnected"));
|
|
980
|
-
};
|
|
981
|
-
ws.once("open", onOpen);
|
|
982
|
-
ws.once("close", onClose);
|
|
983
|
-
ws.once("error", onError);
|
|
1703
|
+
const error = new TciError("handshake-timeout", `Timed out waiting for TCI READY from ${this.options.url}`);
|
|
1704
|
+
this.handshakeWaiter = void 0;
|
|
1705
|
+
reject(error);
|
|
1706
|
+
}, this.options.handshakeTimeoutMs);
|
|
1707
|
+
this.handshakeWaiter = { resolve, reject, timer };
|
|
984
1708
|
});
|
|
985
1709
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1710
|
+
finalizeHandshake() {
|
|
1711
|
+
if (this.handshakeResult) return this.handshakeResult;
|
|
1712
|
+
assertValidTciHandshake(this.initializationCommands);
|
|
1713
|
+
const identity = parseProtocolIdentity(this.initializationCommands);
|
|
1714
|
+
const commandNames = [...new Set(this.initializationCommands.map((command) => command.name))];
|
|
1715
|
+
const dialect = this.dialectRegistry.select(
|
|
1716
|
+
{ identity, commands: this.initializationCommands, commandNames: new Set(commandNames) },
|
|
1717
|
+
this.options.dialect
|
|
1718
|
+
);
|
|
1719
|
+
const result = { identity, dialect, ready: true, commandNames };
|
|
1720
|
+
this.handshakeResult = result;
|
|
1721
|
+
this.activeDialect = dialect.dialect;
|
|
1722
|
+
this.state.protocolName = identity.programName;
|
|
1723
|
+
this.state.protocolVersion = identity.protocolVersion;
|
|
1724
|
+
this.state.protocol = identity.protocolVersion ?? identity.programName;
|
|
1725
|
+
this.state.device = identity.device ?? this.state.device;
|
|
1726
|
+
this.state.dialectId = dialect.dialect.id;
|
|
1727
|
+
this.state.dialectConfidence = dialect.confidence;
|
|
1728
|
+
this.state.dialectWarnings = [...dialect.warnings];
|
|
1729
|
+
const waiter = this.handshakeWaiter;
|
|
1730
|
+
this.handshakeWaiter = void 0;
|
|
1731
|
+
if (waiter) {
|
|
1732
|
+
clearTimeout(waiter.timer);
|
|
1733
|
+
waiter.resolve(cloneHandshake(result));
|
|
1734
|
+
}
|
|
1735
|
+
this.emit("handshake", cloneHandshake(result));
|
|
1736
|
+
return result;
|
|
1737
|
+
}
|
|
1738
|
+
rejectHandshake(error) {
|
|
1739
|
+
const waiter = this.handshakeWaiter;
|
|
1740
|
+
this.handshakeWaiter = void 0;
|
|
1741
|
+
if (!waiter) return;
|
|
1742
|
+
clearTimeout(waiter.timer);
|
|
1743
|
+
waiter.reject(error);
|
|
1744
|
+
}
|
|
1745
|
+
requireDialect() {
|
|
1746
|
+
if (!this.activeDialect) throw new TciError("invalid-handshake", "TCI dialect is not available before READY");
|
|
1747
|
+
return this.activeDialect;
|
|
1748
|
+
}
|
|
1749
|
+
attachTransport(transport) {
|
|
1750
|
+
transport.on("text", (raw) => this.handleText(raw));
|
|
1751
|
+
transport.on("binary", (raw) => this.handleBinary(raw));
|
|
1752
|
+
transport.on("disconnected", (reason) => this.handleClose(reason));
|
|
1753
|
+
transport.on("error", (error) => this.handleError(error));
|
|
990
1754
|
}
|
|
991
1755
|
async sendRaw(raw) {
|
|
992
|
-
const
|
|
993
|
-
if (!
|
|
1756
|
+
const transport = this.transport;
|
|
1757
|
+
if (!transport?.isConnected()) {
|
|
994
1758
|
throw new TciError("not-connected", "TCI socket is not connected");
|
|
995
1759
|
}
|
|
996
1760
|
this.emit("tci:tx", raw);
|
|
997
|
-
await
|
|
998
|
-
ws.send(raw, (error) => error ? reject(error) : resolve());
|
|
999
|
-
});
|
|
1761
|
+
await transport.sendText(raw);
|
|
1000
1762
|
}
|
|
1001
1763
|
sendRawBinary(raw) {
|
|
1002
|
-
const
|
|
1003
|
-
if (!
|
|
1764
|
+
const transport = this.transport;
|
|
1765
|
+
if (!transport?.isConnected()) {
|
|
1004
1766
|
throw new TciError("not-connected", "TCI socket is not connected");
|
|
1005
1767
|
}
|
|
1006
|
-
|
|
1768
|
+
void transport.sendBinary(raw).catch((error) => this.handleError(error));
|
|
1007
1769
|
}
|
|
1008
|
-
|
|
1770
|
+
handleText(raw) {
|
|
1009
1771
|
try {
|
|
1010
|
-
if (isBinary) {
|
|
1011
|
-
this.handleBinary(data);
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
const raw = dataToBuffer(data).toString("utf8");
|
|
1015
1772
|
const commands = parseTciText(raw);
|
|
1016
1773
|
this.emit("tci:rx", raw, commands);
|
|
1017
1774
|
for (const command of commands) {
|
|
1775
|
+
if (!this.handshakeResult) this.initializationCommands.push(command);
|
|
1018
1776
|
this.queue.handleCommand(command);
|
|
1019
1777
|
this.applyCommand(command);
|
|
1020
1778
|
this.emit("command", command);
|
|
@@ -1024,10 +1782,29 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1024
1782
|
}
|
|
1025
1783
|
}
|
|
1026
1784
|
handleBinary(data) {
|
|
1027
|
-
const frame = parseStreamFrame(
|
|
1785
|
+
const frame = parseStreamFrame(data, {
|
|
1786
|
+
lengthSemantics: this.requireDialect().streamLengthSemantics,
|
|
1787
|
+
negotiatedChannels: this.state.audio?.channels
|
|
1788
|
+
});
|
|
1028
1789
|
this.emit("tci:binary", frame);
|
|
1029
1790
|
this.emit("binary", frame);
|
|
1030
1791
|
switch (frame.streamType) {
|
|
1792
|
+
case 0 /* IQ_STREAM */: {
|
|
1793
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
1794
|
+
throw new TciError("invalid-frame", "TCI IQ stream frame must contain interleaved I/Q pairs");
|
|
1795
|
+
}
|
|
1796
|
+
this.state.iq.sampleRate = frame.sampleRate;
|
|
1797
|
+
const iqFrame = {
|
|
1798
|
+
frame,
|
|
1799
|
+
receiver: frame.receiver,
|
|
1800
|
+
sampleRate: frame.sampleRate,
|
|
1801
|
+
centerFrequency: this.state.dds[String(frame.receiver)],
|
|
1802
|
+
complexSampleCount: frame.frameCount
|
|
1803
|
+
};
|
|
1804
|
+
this.emit("iqFrame", iqFrame);
|
|
1805
|
+
this.activeIqSession?._acceptFrame(iqFrame);
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1031
1808
|
case 1 /* RX_AUDIO_STREAM */:
|
|
1032
1809
|
this.emit("rxAudioFrame", frame);
|
|
1033
1810
|
break;
|
|
@@ -1038,7 +1815,8 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1038
1815
|
sampleRate: frame.sampleRate,
|
|
1039
1816
|
channels: frame.channels,
|
|
1040
1817
|
sampleType: frame.sampleType,
|
|
1041
|
-
sampleCount: frame.sampleCount
|
|
1818
|
+
sampleCount: frame.sampleCount,
|
|
1819
|
+
frameCount: frame.frameCount
|
|
1042
1820
|
});
|
|
1043
1821
|
break;
|
|
1044
1822
|
case 4 /* LINEOUT_STREAM */:
|
|
@@ -1050,80 +1828,92 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1050
1828
|
}
|
|
1051
1829
|
applyCommand(command) {
|
|
1052
1830
|
const readyBefore = this.state.ready;
|
|
1053
|
-
|
|
1054
|
-
case "ready":
|
|
1055
|
-
this.state.ready = command.args.length === 0 ? true : parseBoolean(command.args[0]) ?? true;
|
|
1056
|
-
break;
|
|
1057
|
-
case "protocol":
|
|
1058
|
-
this.state.protocol = command.args[0];
|
|
1059
|
-
break;
|
|
1060
|
-
case "device":
|
|
1061
|
-
this.state.device = command.args.join(",");
|
|
1062
|
-
break;
|
|
1063
|
-
case "receive_only":
|
|
1064
|
-
this.state.receiveOnly = parseBoolean(command.args[0]);
|
|
1065
|
-
break;
|
|
1066
|
-
case "trx_count":
|
|
1067
|
-
this.state.trxCount = parseNumber(command.args[0]);
|
|
1068
|
-
break;
|
|
1069
|
-
case "channels_count":
|
|
1070
|
-
case "channel_count":
|
|
1071
|
-
this.state.channelCount = parseNumber(command.args[0]);
|
|
1072
|
-
break;
|
|
1073
|
-
case "vfo_limits":
|
|
1074
|
-
this.state.vfoLimits = parseNumberPair(command.args);
|
|
1075
|
-
break;
|
|
1076
|
-
case "if_limits":
|
|
1077
|
-
this.state.ifLimits = parseNumberPair(command.args);
|
|
1078
|
-
break;
|
|
1079
|
-
case "modulations_list":
|
|
1080
|
-
this.state.modulations = command.args.map((mode) => mode.toLowerCase());
|
|
1081
|
-
break;
|
|
1082
|
-
case "vfo":
|
|
1083
|
-
this.applyVfo(command.args);
|
|
1084
|
-
break;
|
|
1085
|
-
case "modulation":
|
|
1086
|
-
this.applyModulation(command.args);
|
|
1087
|
-
break;
|
|
1088
|
-
case "trx":
|
|
1089
|
-
this.applyTrx(command.args);
|
|
1090
|
-
break;
|
|
1091
|
-
case "tune":
|
|
1092
|
-
this.applyBooleanByFirstArg(this.state.tune, command.args);
|
|
1093
|
-
break;
|
|
1094
|
-
case "drive":
|
|
1095
|
-
this.applyDrive(command.args);
|
|
1096
|
-
break;
|
|
1097
|
-
case "split_enable":
|
|
1098
|
-
this.applyBooleanByFirstArg(this.state.split, command.args);
|
|
1099
|
-
break;
|
|
1100
|
-
case "rx_channel_sensors":
|
|
1101
|
-
this.applyRxChannelSensors(command.args);
|
|
1102
|
-
break;
|
|
1103
|
-
case "rx_sensors":
|
|
1104
|
-
this.applyRxSensors(command.args);
|
|
1105
|
-
break;
|
|
1106
|
-
case "tx_sensors":
|
|
1107
|
-
this.applyTxSensors(command.args);
|
|
1108
|
-
break;
|
|
1109
|
-
case "audio_samplerate":
|
|
1110
|
-
this.state.audio = {
|
|
1111
|
-
sampleRate: parseNumber(command.args[0]) ?? this.state.audio?.sampleRate ?? 12e3,
|
|
1112
|
-
sampleType: this.state.audio?.sampleType ?? 3 /* FLOAT32 */,
|
|
1113
|
-
channels: this.state.audio?.channels ?? 1,
|
|
1114
|
-
samplesPerFrame: this.state.audio?.samplesPerFrame ?? 512,
|
|
1115
|
-
txBufferingMs: this.state.audio?.txBufferingMs,
|
|
1116
|
-
running: this.state.audio?.running ?? false
|
|
1117
|
-
};
|
|
1118
|
-
break;
|
|
1119
|
-
default:
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1831
|
+
this.stateReducers.get(command.name)?.(command.args);
|
|
1122
1832
|
if (!readyBefore && this.state.ready) {
|
|
1123
|
-
|
|
1833
|
+
try {
|
|
1834
|
+
this.finalizeHandshake();
|
|
1835
|
+
this.emit("ready", this.getState());
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
const tciError = toTciError(error, "invalid-handshake");
|
|
1838
|
+
this.handshakeError = tciError;
|
|
1839
|
+
this.state.ready = false;
|
|
1840
|
+
this.rejectHandshake(tciError);
|
|
1841
|
+
this.handleError(tciError);
|
|
1842
|
+
}
|
|
1124
1843
|
}
|
|
1125
1844
|
this.emitState();
|
|
1126
1845
|
}
|
|
1846
|
+
createStateReducers() {
|
|
1847
|
+
const reducers = /* @__PURE__ */ new Map();
|
|
1848
|
+
reducers.set("ready", (args) => {
|
|
1849
|
+
this.state.ready = args.length === 0 ? true : parseBoolean(args[0]) ?? true;
|
|
1850
|
+
});
|
|
1851
|
+
reducers.set("protocol", (args) => {
|
|
1852
|
+
if (/^\d+(?:\.\d+){0,2}/.test(args[0] ?? "")) {
|
|
1853
|
+
this.state.protocol = args[0];
|
|
1854
|
+
this.state.protocolVersion = args[0];
|
|
1855
|
+
} else {
|
|
1856
|
+
this.state.protocolName = args[0];
|
|
1857
|
+
this.state.protocolVersion = args[1];
|
|
1858
|
+
this.state.protocol = args[1] ?? args[0];
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
reducers.set("device", (args) => {
|
|
1862
|
+
this.state.device = args.join(",");
|
|
1863
|
+
});
|
|
1864
|
+
reducers.set("receive_only", (args) => {
|
|
1865
|
+
this.state.receiveOnly = parseBoolean(args[0]);
|
|
1866
|
+
});
|
|
1867
|
+
reducers.set("trx_count", (args) => {
|
|
1868
|
+
this.state.trxCount = parseNumber(args[0]);
|
|
1869
|
+
});
|
|
1870
|
+
const channelCount = (args) => {
|
|
1871
|
+
this.state.channelCount = parseNumber(args[0]);
|
|
1872
|
+
};
|
|
1873
|
+
reducers.set("channels_count", channelCount);
|
|
1874
|
+
reducers.set("channel_count", channelCount);
|
|
1875
|
+
reducers.set("vfo_limits", (args) => {
|
|
1876
|
+
this.state.vfoLimits = parseNumberPair(args);
|
|
1877
|
+
});
|
|
1878
|
+
reducers.set("if_limits", (args) => {
|
|
1879
|
+
this.state.ifLimits = parseNumberPair(args);
|
|
1880
|
+
});
|
|
1881
|
+
reducers.set("modulations_list", (args) => {
|
|
1882
|
+
this.state.modulations = args.map((mode) => mode.toLowerCase());
|
|
1883
|
+
});
|
|
1884
|
+
reducers.set("vfo", (args) => this.applyVfo(args));
|
|
1885
|
+
reducers.set("dds", (args) => {
|
|
1886
|
+
const receiver = parseNumber(args[0]);
|
|
1887
|
+
const frequency = parseNumber(args[1]);
|
|
1888
|
+
if (receiver !== void 0 && frequency !== void 0 && frequency >= 0) this.state.dds[String(receiver)] = frequency;
|
|
1889
|
+
});
|
|
1890
|
+
reducers.set("modulation", (args) => this.applyModulation(args));
|
|
1891
|
+
reducers.set("trx", (args) => this.applyTrx(args));
|
|
1892
|
+
reducers.set("tune", (args) => this.applyBooleanByFirstArg(this.state.tune, args));
|
|
1893
|
+
reducers.set("drive", (args) => this.applyDrive(args));
|
|
1894
|
+
reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
|
|
1895
|
+
reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
|
|
1896
|
+
reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
|
|
1897
|
+
reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
|
|
1898
|
+
reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
|
|
1899
|
+
reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
|
|
1900
|
+
reducers.set("audio_stream_sample_type", (args) => this.updateAudioState({ sampleType: parseSampleType(args[0]) }));
|
|
1901
|
+
reducers.set("audio_stream_channels", (args) => this.updateAudioState({ channels: parseNumber(args[0]) }));
|
|
1902
|
+
reducers.set("audio_stream_samples", (args) => this.updateAudioState({ samplesPerFrame: parseNumber(args[0]) }));
|
|
1903
|
+
reducers.set("tx_stream_audio_buffering", (args) => this.updateAudioState({ txBufferingMs: parseNumber(args[0]) }));
|
|
1904
|
+
reducers.set("audio_start", () => this.updateAudioState({ running: true }));
|
|
1905
|
+
reducers.set("audio_stop", () => this.updateAudioState({ running: false }));
|
|
1906
|
+
reducers.set("iq_samplerate", (args) => {
|
|
1907
|
+
this.state.iq.sampleRate = parseNumber(args[0]);
|
|
1908
|
+
});
|
|
1909
|
+
reducers.set("iq_start", (args) => {
|
|
1910
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = true;
|
|
1911
|
+
});
|
|
1912
|
+
reducers.set("iq_stop", (args) => {
|
|
1913
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = false;
|
|
1914
|
+
});
|
|
1915
|
+
return reducers;
|
|
1916
|
+
}
|
|
1127
1917
|
applyVfo(args) {
|
|
1128
1918
|
if (args.length < 3) {
|
|
1129
1919
|
return;
|
|
@@ -1170,18 +1960,22 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1170
1960
|
}
|
|
1171
1961
|
}
|
|
1172
1962
|
applyDrive(args) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
this.state.
|
|
1184
|
-
|
|
1963
|
+
const parsed = this.activeDialect?.parseDrive(args, this.options.trx) ?? parseObservedDrive(args, this.options.trx);
|
|
1964
|
+
if (parsed) this.state.drive[String(parsed.trx)] = parsed.value;
|
|
1965
|
+
}
|
|
1966
|
+
applyTuneDrive(args) {
|
|
1967
|
+
const parsed = this.activeDialect?.parseTuneDrive(args, this.options.trx) ?? parseObservedDrive(args, this.options.trx);
|
|
1968
|
+
if (parsed) this.state.tuneDrive[String(parsed.trx)] = parsed.value;
|
|
1969
|
+
}
|
|
1970
|
+
updateAudioState(update) {
|
|
1971
|
+
this.state.audio = {
|
|
1972
|
+
sampleRate: update.sampleRate ?? this.state.audio?.sampleRate ?? 12e3,
|
|
1973
|
+
sampleType: update.sampleType ?? this.state.audio?.sampleType ?? 3 /* FLOAT32 */,
|
|
1974
|
+
channels: update.channels ?? this.state.audio?.channels ?? 1,
|
|
1975
|
+
samplesPerFrame: update.samplesPerFrame ?? this.state.audio?.samplesPerFrame ?? 512,
|
|
1976
|
+
txBufferingMs: update.txBufferingMs ?? this.state.audio?.txBufferingMs,
|
|
1977
|
+
running: update.running ?? this.state.audio?.running ?? false
|
|
1978
|
+
};
|
|
1185
1979
|
}
|
|
1186
1980
|
applyRxChannelSensors(args) {
|
|
1187
1981
|
if (args.length < 3) {
|
|
@@ -1217,11 +2011,16 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1217
2011
|
};
|
|
1218
2012
|
}
|
|
1219
2013
|
handleClose(reason) {
|
|
1220
|
-
|
|
2014
|
+
const transport = this.transport;
|
|
2015
|
+
this.transport = void 0;
|
|
2016
|
+
transport?.removeAllListeners();
|
|
1221
2017
|
const wasConnected = this.state.connected;
|
|
1222
2018
|
this.state.connected = false;
|
|
1223
2019
|
this.state.ready = false;
|
|
1224
2020
|
this.queue.setConnected(false);
|
|
2021
|
+
this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
|
|
2022
|
+
this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
|
|
2023
|
+
this.activeIqSession = void 0;
|
|
1225
2024
|
if (wasConnected) {
|
|
1226
2025
|
this.emit("disconnected", reason);
|
|
1227
2026
|
this.emitState();
|
|
@@ -1238,18 +2037,6 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
|
|
|
1238
2037
|
function createTciClient(options) {
|
|
1239
2038
|
return new TciClient(options);
|
|
1240
2039
|
}
|
|
1241
|
-
function dataToBuffer(data) {
|
|
1242
|
-
if (Buffer.isBuffer(data)) {
|
|
1243
|
-
return data;
|
|
1244
|
-
}
|
|
1245
|
-
if (data instanceof ArrayBuffer) {
|
|
1246
|
-
return Buffer.from(data);
|
|
1247
|
-
}
|
|
1248
|
-
if (Array.isArray(data)) {
|
|
1249
|
-
return Buffer.concat(data.map((item) => dataToBuffer(item)));
|
|
1250
|
-
}
|
|
1251
|
-
throw new TciError("protocol-error", "Unsupported WebSocket data type");
|
|
1252
|
-
}
|
|
1253
2040
|
function rxVfoKey(receiver, vfo) {
|
|
1254
2041
|
return `${receiver}:${vfo}`;
|
|
1255
2042
|
}
|
|
@@ -1273,6 +2060,27 @@ function parseBoolean(value) {
|
|
|
1273
2060
|
}
|
|
1274
2061
|
return void 0;
|
|
1275
2062
|
}
|
|
2063
|
+
function parseSampleType(value) {
|
|
2064
|
+
if (!value) return void 0;
|
|
2065
|
+
try {
|
|
2066
|
+
return normalizeSampleType(value.toLowerCase());
|
|
2067
|
+
} catch {
|
|
2068
|
+
return void 0;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function parseObservedDrive(args, defaultTrx) {
|
|
2072
|
+
const hasTrx = args.length >= 2;
|
|
2073
|
+
const trx = hasTrx ? parseNumber(args[0]) : defaultTrx;
|
|
2074
|
+
const value = parseNumber(args[hasTrx ? 1 : 0]);
|
|
2075
|
+
return trx === void 0 || value === void 0 ? void 0 : { trx, value };
|
|
2076
|
+
}
|
|
2077
|
+
function normalizePercent(value) {
|
|
2078
|
+
if (!Number.isFinite(value)) throw new TciError("protocol-error", `Invalid TCI percentage: ${value}`);
|
|
2079
|
+
return Math.round(Math.max(0, Math.min(100, value)));
|
|
2080
|
+
}
|
|
2081
|
+
function writeResult(requested, applied, acknowledgement) {
|
|
2082
|
+
return { requested, applied, outcome: requested === applied ? "applied" : "clamped", acknowledgement };
|
|
2083
|
+
}
|
|
1276
2084
|
function parseNumberPair(args) {
|
|
1277
2085
|
const first = parseNumber(args[0]);
|
|
1278
2086
|
const second = parseNumber(args[1]);
|
|
@@ -1288,10 +2096,26 @@ function cloneState(state) {
|
|
|
1288
2096
|
pttSource: { ...state.pttSource },
|
|
1289
2097
|
tune: { ...state.tune },
|
|
1290
2098
|
drive: { ...state.drive },
|
|
2099
|
+
tuneDrive: { ...state.tuneDrive },
|
|
1291
2100
|
split: { ...state.split },
|
|
2101
|
+
dialectWarnings: [...state.dialectWarnings],
|
|
1292
2102
|
rxSensors: cloneNested(state.rxSensors),
|
|
1293
2103
|
txSensors: cloneNested(state.txSensors),
|
|
1294
|
-
audio: state.audio ? { ...state.audio } : void 0
|
|
2104
|
+
audio: state.audio ? { ...state.audio } : void 0,
|
|
2105
|
+
iq: { ...state.iq, activeReceivers: { ...state.iq.activeReceivers } },
|
|
2106
|
+
dds: { ...state.dds }
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function cloneHandshake(result) {
|
|
2110
|
+
return {
|
|
2111
|
+
identity: { ...result.identity, rawProtocolArgs: [...result.identity.rawProtocolArgs] },
|
|
2112
|
+
dialect: {
|
|
2113
|
+
...result.dialect,
|
|
2114
|
+
evidence: [...result.dialect.evidence],
|
|
2115
|
+
warnings: [...result.dialect.warnings]
|
|
2116
|
+
},
|
|
2117
|
+
ready: true,
|
|
2118
|
+
commandNames: [...result.commandNames]
|
|
1295
2119
|
};
|
|
1296
2120
|
}
|
|
1297
2121
|
function cloneNested(value) {
|
|
@@ -1302,30 +2126,46 @@ function cloneNested(value) {
|
|
|
1302
2126
|
TCI_STREAM_HEADER_BYTES,
|
|
1303
2127
|
TciClient,
|
|
1304
2128
|
TciCommandQueue,
|
|
2129
|
+
TciDialectRegistry,
|
|
1305
2130
|
TciError,
|
|
2131
|
+
TciIqStreamSession,
|
|
1306
2132
|
TciSampleType,
|
|
1307
2133
|
TciStreamType,
|
|
2134
|
+
WebSocketTciTransport,
|
|
2135
|
+
aetherSdrDialect,
|
|
2136
|
+
assertValidTciHandshake,
|
|
1308
2137
|
buildStreamFrame,
|
|
1309
2138
|
buildTxAudioFrame,
|
|
2139
|
+
builtInDialects,
|
|
1310
2140
|
commandKey,
|
|
2141
|
+
compareTciVersion,
|
|
1311
2142
|
createTciClient,
|
|
2143
|
+
decodeInterleavedIq,
|
|
2144
|
+
defaultTciDialectRegistry,
|
|
1312
2145
|
deinterleaveChannels,
|
|
1313
2146
|
escapeTciText,
|
|
2147
|
+
expertSdr14Dialect,
|
|
2148
|
+
expertSdrLegacyDialect,
|
|
2149
|
+
expertSdrModernDialect,
|
|
1314
2150
|
float32ToPcm16,
|
|
1315
2151
|
formatTciCommand,
|
|
2152
|
+
genericObservedDialect,
|
|
1316
2153
|
isCommandReplyTo,
|
|
1317
2154
|
mixToMono,
|
|
1318
2155
|
normalizeCommandName,
|
|
1319
2156
|
normalizeSampleType,
|
|
1320
2157
|
normalizeStreamType,
|
|
2158
|
+
parseProtocolIdentity,
|
|
1321
2159
|
parseStreamFrame,
|
|
1322
2160
|
parseTciCommand,
|
|
1323
2161
|
parseTciText,
|
|
2162
|
+
parseTciVersion,
|
|
1324
2163
|
payloadToFloat32,
|
|
1325
2164
|
pcm16ToFloat32,
|
|
1326
2165
|
sampleTypeBytes,
|
|
1327
2166
|
sampleTypeName,
|
|
1328
2167
|
samplesToPayload,
|
|
2168
|
+
thetisDialect,
|
|
1329
2169
|
toTciError,
|
|
1330
2170
|
unescapeTciText
|
|
1331
2171
|
});
|