tci-client-node 0.2.0 → 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/dist/index.d.cts CHANGED
@@ -3,11 +3,11 @@ export { a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue,
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import WebSocket from 'ws';
5
5
  import { TciSampleType, TciSampleTypeName, TciStreamFrame, BuildTxAudioFrameOptions } from './audio/index.cjs';
6
- export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.cjs';
6
+ export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.cjs';
7
7
  import { T as TciCommand } from './text-BwCWY1k1.cjs';
8
8
  export { a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from './text-BwCWY1k1.cjs';
9
- import { T as TciDialectId, a as TciHandshakeResult, b as TciDialectSelection, c as TciWriteResult } from './types-xRotf9gW.cjs';
10
- export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-xRotf9gW.cjs';
9
+ import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-1YXGwrgI.cjs';
10
+ export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-1YXGwrgI.cjs';
11
11
  import { TciDialectRegistry } from './dialect/index.cjs';
12
12
  export { aetherSdrDialect, assertValidTciHandshake, builtInDialects, compareTciVersion, defaultTciDialectRegistry, expertSdr14Dialect, expertSdrLegacyDialect, expertSdrModernDialect, genericObservedDialect, parseProtocolIdentity, parseTciVersion, thetisDialect } from './dialect/index.cjs';
13
13
  import { TciTransportFactory } from './transport/index.cjs';
@@ -59,6 +59,49 @@ interface TciTxChronoRequest {
59
59
  sampleCount: number;
60
60
  frameCount: number;
61
61
  }
62
+ interface TciIqCapabilities {
63
+ supported: boolean;
64
+ currentSampleRate?: number;
65
+ supportedSampleRates: readonly number[];
66
+ }
67
+ interface TciIqFrame {
68
+ frame: TciStreamFrame;
69
+ receiver: number;
70
+ sampleRate: number;
71
+ centerFrequency?: number;
72
+ complexSampleCount: number;
73
+ }
74
+ interface TciIqStreamOptions {
75
+ receiver?: number;
76
+ sampleRate?: number;
77
+ firstFrameTimeoutMs?: number;
78
+ }
79
+ interface IqSampleRateResult extends TciWriteResult<number> {
80
+ }
81
+ interface TciIqStreamEvents {
82
+ frame: (frame: TciIqFrame) => void;
83
+ error: (error: TciError) => void;
84
+ closed: () => void;
85
+ }
86
+ interface TciIqStreamCallbacks {
87
+ setSampleRate: (sampleRate: number) => Promise<IqSampleRateResult>;
88
+ close: () => Promise<void>;
89
+ }
90
+ declare class TciIqStreamSession extends EventEmitter<TciIqStreamEvents> {
91
+ readonly receiver: number;
92
+ private _appliedSampleRate;
93
+ private readonly callbacks;
94
+ private closed;
95
+ private firstFrame?;
96
+ constructor(receiver: number, appliedSampleRate: number, callbacks: TciIqStreamCallbacks);
97
+ get appliedSampleRate(): number;
98
+ setSampleRate(sampleRate: number): Promise<IqSampleRateResult>;
99
+ close(): Promise<void>;
100
+ waitForFirstFrame(timeoutMs: number): Promise<TciIqFrame>;
101
+ _acceptFrame(frame: TciIqFrame): void;
102
+ _fail(error: TciError): void;
103
+ private rejectFirstFrame;
104
+ }
62
105
  interface TciClientState {
63
106
  connected: boolean;
64
107
  ready: boolean;
@@ -89,6 +132,11 @@ interface TciClientState {
89
132
  txBufferingMs?: number;
90
133
  running: boolean;
91
134
  };
135
+ iq: {
136
+ sampleRate?: number;
137
+ activeReceivers: Record<string, boolean>;
138
+ };
139
+ dds: Record<string, number>;
92
140
  }
93
141
  interface TciClientEvents {
94
142
  connected: () => void;
@@ -102,6 +150,7 @@ interface TciClientEvents {
102
150
  'tci:rx': (raw: string, commands: TciCommand[]) => void;
103
151
  'tci:binary': (frame: TciStreamFrame) => void;
104
152
  rxAudioFrame: (frame: TciStreamFrame) => void;
153
+ iqFrame: (frame: TciIqFrame) => void;
105
154
  lineoutAudioFrame: (frame: TciStreamFrame) => void;
106
155
  txChrono: (request: TciTxChronoRequest) => void;
107
156
  error: (error: TciError) => void;
@@ -125,12 +174,16 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
125
174
  private handshakeError?;
126
175
  private initializationCommands;
127
176
  private handshakeWaiter?;
177
+ private activeIqSession?;
128
178
  constructor(options: TciClientOptions);
129
179
  connect(): Promise<TciHandshakeResult>;
130
180
  disconnect(code?: number, reason?: string): Promise<void>;
131
181
  isConnected(): boolean;
132
182
  getState(): TciClientState;
133
183
  getHandshakeResult(): TciHandshakeResult | undefined;
184
+ getIqCapabilities(): TciIqCapabilities;
185
+ setIqSampleRate(sampleRate: number, timeoutMs?: number): Promise<IqSampleRateResult>;
186
+ openIqStream(options?: TciIqStreamOptions): Promise<TciIqStreamSession>;
134
187
  sendCommand(name: string, args?: readonly unknown[], options?: SendCommandOptions): Promise<TciCommand | undefined>;
135
188
  request(name: string, args?: readonly unknown[], options?: QueueCommandOptions): Promise<TciCommand>;
136
189
  sendStateWrite(name: string, args: readonly unknown[], isApplied: (state: TciClientState) => boolean, description?: string, options?: TciWriteOptions): Promise<void>;
@@ -187,4 +240,4 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
187
240
  }
188
241
  declare function createTciClient(options: TciClientOptions): TciClient;
189
242
 
190
- export { BuildTxAudioFrameOptions, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
243
+ export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
package/dist/index.d.ts CHANGED
@@ -3,11 +3,11 @@ export { a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue,
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import WebSocket from 'ws';
5
5
  import { TciSampleType, TciSampleTypeName, TciStreamFrame, BuildTxAudioFrameOptions } from './audio/index.js';
6
- export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.js';
6
+ export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.js';
7
7
  import { T as TciCommand } from './text-BwCWY1k1.js';
8
8
  export { a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from './text-BwCWY1k1.js';
9
- import { T as TciDialectId, a as TciHandshakeResult, b as TciDialectSelection, c as TciWriteResult } from './types-9seY9Th-.js';
10
- export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-9seY9Th-.js';
9
+ import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-BtPh4Dbd.js';
10
+ export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-BtPh4Dbd.js';
11
11
  import { TciDialectRegistry } from './dialect/index.js';
12
12
  export { aetherSdrDialect, assertValidTciHandshake, builtInDialects, compareTciVersion, defaultTciDialectRegistry, expertSdr14Dialect, expertSdrLegacyDialect, expertSdrModernDialect, genericObservedDialect, parseProtocolIdentity, parseTciVersion, thetisDialect } from './dialect/index.js';
13
13
  import { TciTransportFactory } from './transport/index.js';
@@ -59,6 +59,49 @@ interface TciTxChronoRequest {
59
59
  sampleCount: number;
60
60
  frameCount: number;
61
61
  }
62
+ interface TciIqCapabilities {
63
+ supported: boolean;
64
+ currentSampleRate?: number;
65
+ supportedSampleRates: readonly number[];
66
+ }
67
+ interface TciIqFrame {
68
+ frame: TciStreamFrame;
69
+ receiver: number;
70
+ sampleRate: number;
71
+ centerFrequency?: number;
72
+ complexSampleCount: number;
73
+ }
74
+ interface TciIqStreamOptions {
75
+ receiver?: number;
76
+ sampleRate?: number;
77
+ firstFrameTimeoutMs?: number;
78
+ }
79
+ interface IqSampleRateResult extends TciWriteResult<number> {
80
+ }
81
+ interface TciIqStreamEvents {
82
+ frame: (frame: TciIqFrame) => void;
83
+ error: (error: TciError) => void;
84
+ closed: () => void;
85
+ }
86
+ interface TciIqStreamCallbacks {
87
+ setSampleRate: (sampleRate: number) => Promise<IqSampleRateResult>;
88
+ close: () => Promise<void>;
89
+ }
90
+ declare class TciIqStreamSession extends EventEmitter<TciIqStreamEvents> {
91
+ readonly receiver: number;
92
+ private _appliedSampleRate;
93
+ private readonly callbacks;
94
+ private closed;
95
+ private firstFrame?;
96
+ constructor(receiver: number, appliedSampleRate: number, callbacks: TciIqStreamCallbacks);
97
+ get appliedSampleRate(): number;
98
+ setSampleRate(sampleRate: number): Promise<IqSampleRateResult>;
99
+ close(): Promise<void>;
100
+ waitForFirstFrame(timeoutMs: number): Promise<TciIqFrame>;
101
+ _acceptFrame(frame: TciIqFrame): void;
102
+ _fail(error: TciError): void;
103
+ private rejectFirstFrame;
104
+ }
62
105
  interface TciClientState {
63
106
  connected: boolean;
64
107
  ready: boolean;
@@ -89,6 +132,11 @@ interface TciClientState {
89
132
  txBufferingMs?: number;
90
133
  running: boolean;
91
134
  };
135
+ iq: {
136
+ sampleRate?: number;
137
+ activeReceivers: Record<string, boolean>;
138
+ };
139
+ dds: Record<string, number>;
92
140
  }
93
141
  interface TciClientEvents {
94
142
  connected: () => void;
@@ -102,6 +150,7 @@ interface TciClientEvents {
102
150
  'tci:rx': (raw: string, commands: TciCommand[]) => void;
103
151
  'tci:binary': (frame: TciStreamFrame) => void;
104
152
  rxAudioFrame: (frame: TciStreamFrame) => void;
153
+ iqFrame: (frame: TciIqFrame) => void;
105
154
  lineoutAudioFrame: (frame: TciStreamFrame) => void;
106
155
  txChrono: (request: TciTxChronoRequest) => void;
107
156
  error: (error: TciError) => void;
@@ -125,12 +174,16 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
125
174
  private handshakeError?;
126
175
  private initializationCommands;
127
176
  private handshakeWaiter?;
177
+ private activeIqSession?;
128
178
  constructor(options: TciClientOptions);
129
179
  connect(): Promise<TciHandshakeResult>;
130
180
  disconnect(code?: number, reason?: string): Promise<void>;
131
181
  isConnected(): boolean;
132
182
  getState(): TciClientState;
133
183
  getHandshakeResult(): TciHandshakeResult | undefined;
184
+ getIqCapabilities(): TciIqCapabilities;
185
+ setIqSampleRate(sampleRate: number, timeoutMs?: number): Promise<IqSampleRateResult>;
186
+ openIqStream(options?: TciIqStreamOptions): Promise<TciIqStreamSession>;
134
187
  sendCommand(name: string, args?: readonly unknown[], options?: SendCommandOptions): Promise<TciCommand | undefined>;
135
188
  request(name: string, args?: readonly unknown[], options?: QueueCommandOptions): Promise<TciCommand>;
136
189
  sendStateWrite(name: string, args: readonly unknown[], isApplied: (state: TciClientState) => boolean, description?: string, options?: TciWriteOptions): Promise<void>;
@@ -187,4 +240,4 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
187
240
  }
188
241
  declare function createTciClient(options: TciClientOptions): TciClient;
189
242
 
190
- export { BuildTxAudioFrameOptions, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
243
+ export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
package/dist/index.js CHANGED
@@ -241,6 +241,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
241
241
  }
242
242
  return output;
243
243
  }
244
+ function decodeInterleavedIq(frame) {
245
+ if (frame.streamType !== 0 /* IQ_STREAM */) {
246
+ throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
247
+ }
248
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
249
+ throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
250
+ }
251
+ return payloadToFloat32(frame);
252
+ }
244
253
  function samplesToPayload(samples, sampleType) {
245
254
  const type = normalizeSampleType(sampleType);
246
255
  const bytes = sampleTypeBytes(type);
@@ -595,6 +604,8 @@ var StandardTciDialect = class {
595
604
  streamLengthSemantics;
596
605
  supportsStreamChannels;
597
606
  supportsTxAudioSource;
607
+ supportsIqStream;
608
+ iqSampleRates;
598
609
  driveHasTrx;
599
610
  detector;
600
611
  resolver;
@@ -604,6 +615,8 @@ var StandardTciDialect = class {
604
615
  this.streamLengthSemantics = options.streamLengthSemantics;
605
616
  this.supportsStreamChannels = options.supportsStreamChannels;
606
617
  this.supportsTxAudioSource = options.supportsTxAudioSource;
618
+ this.supportsIqStream = options.supportsIqStream;
619
+ this.iqSampleRates = [...options.iqSampleRates];
607
620
  this.driveHasTrx = options.driveHasTrx;
608
621
  this.detector = options.detect;
609
622
  this.resolver = options.resolve;
@@ -651,6 +664,8 @@ var expertSdr14Dialect = new StandardTciDialect({
651
664
  streamLengthSemantics: "per-channel",
652
665
  supportsStreamChannels: false,
653
666
  supportsTxAudioSource: false,
667
+ supportsIqStream: true,
668
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
654
669
  driveHasTrx: false,
655
670
  detect: (context) => {
656
671
  const parsed = version(context);
@@ -664,6 +679,8 @@ var expertSdrLegacyDialect = new StandardTciDialect({
664
679
  streamLengthSemantics: "per-channel",
665
680
  supportsStreamChannels: false,
666
681
  supportsTxAudioSource: false,
682
+ supportsIqStream: true,
683
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
667
684
  driveHasTrx: true,
668
685
  detect: (context) => {
669
686
  const parsed = version(context);
@@ -677,6 +694,8 @@ var expertSdrModernDialect = new StandardTciDialect({
677
694
  streamLengthSemantics: "scalar",
678
695
  supportsStreamChannels: true,
679
696
  supportsTxAudioSource: true,
697
+ supportsIqStream: true,
698
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
680
699
  driveHasTrx: true,
681
700
  detect: (context) => {
682
701
  const parsed = version(context);
@@ -695,6 +714,8 @@ var thetisDialect = new StandardTciDialect({
695
714
  streamLengthSemantics: "scalar",
696
715
  supportsStreamChannels: true,
697
716
  supportsTxAudioSource: true,
717
+ supportsIqStream: true,
718
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
698
719
  driveHasTrx: true,
699
720
  detect: (context) => {
700
721
  const evidence = [];
@@ -721,6 +742,8 @@ var aetherSdrDialect = new StandardTciDialect({
721
742
  streamLengthSemantics: "scalar",
722
743
  supportsStreamChannels: true,
723
744
  supportsTxAudioSource: true,
745
+ supportsIqStream: true,
746
+ iqSampleRates: [24e3, 48e3, 96e3, 192e3],
724
747
  driveHasTrx: true,
725
748
  detect: (context) => {
726
749
  if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
@@ -740,6 +763,8 @@ var genericObservedDialect = new StandardTciDialect({
740
763
  streamLengthSemantics: "auto",
741
764
  supportsStreamChannels: true,
742
765
  supportsTxAudioSource: true,
766
+ supportsIqStream: false,
767
+ iqSampleRates: [],
743
768
  driveHasTrx: true,
744
769
  detect: (context) => ({
745
770
  score: context.commandNames.has("ready") ? 10 : 0,
@@ -755,6 +780,8 @@ var genericObservedDialect = new StandardTciDialect({
755
780
  streamLengthSemantics: "auto",
756
781
  supportsStreamChannels: context.commandNames.has("audio_stream_channels"),
757
782
  supportsTxAudioSource: compareTciVersion(version(context) ?? [0], [2, 0]) >= 0,
783
+ supportsIqStream: context.commandNames.has("iq_samplerate"),
784
+ iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
758
785
  driveHasTrx,
759
786
  detect: genericObservedDialect.detect.bind(genericObservedDialect)
760
787
  });
@@ -966,6 +993,77 @@ function dataToBuffer(data) {
966
993
  }
967
994
 
968
995
  // src/client/TciClient.ts
996
+ var TciIqStreamSession = class extends EventEmitter2 {
997
+ receiver;
998
+ _appliedSampleRate;
999
+ callbacks;
1000
+ closed = false;
1001
+ firstFrame;
1002
+ constructor(receiver, appliedSampleRate, callbacks) {
1003
+ super();
1004
+ this.receiver = receiver;
1005
+ this._appliedSampleRate = appliedSampleRate;
1006
+ this.callbacks = callbacks;
1007
+ }
1008
+ get appliedSampleRate() {
1009
+ return this._appliedSampleRate;
1010
+ }
1011
+ async setSampleRate(sampleRate) {
1012
+ if (this.closed) throw new TciError("cancelled", "TCI IQ stream is closed");
1013
+ const result = await this.callbacks.setSampleRate(sampleRate);
1014
+ this._appliedSampleRate = result.applied;
1015
+ return result;
1016
+ }
1017
+ async close() {
1018
+ if (this.closed) return;
1019
+ this.closed = true;
1020
+ this.rejectFirstFrame(new TciError("cancelled", "TCI IQ stream closed before its first frame"));
1021
+ try {
1022
+ await this.callbacks.close();
1023
+ } finally {
1024
+ this.emit("closed");
1025
+ this.removeAllListeners();
1026
+ }
1027
+ }
1028
+ waitForFirstFrame(timeoutMs) {
1029
+ if (this.firstFrame) return this.firstFrame.promise;
1030
+ let resolvePromise;
1031
+ let rejectPromise;
1032
+ const promise = new Promise((resolve, reject) => {
1033
+ resolvePromise = resolve;
1034
+ rejectPromise = reject;
1035
+ });
1036
+ const timer = setTimeout(() => {
1037
+ this.firstFrame = void 0;
1038
+ rejectPromise(new TciError("command-timeout", `Timed out waiting for TCI IQ frame for receiver ${this.receiver}`));
1039
+ }, timeoutMs);
1040
+ this.firstFrame = { promise, resolve: resolvePromise, reject: rejectPromise, timer };
1041
+ return promise;
1042
+ }
1043
+ _acceptFrame(frame) {
1044
+ if (this.closed || frame.receiver !== this.receiver) return;
1045
+ this._appliedSampleRate = frame.sampleRate;
1046
+ const firstFrame = this.firstFrame;
1047
+ if (firstFrame) {
1048
+ clearTimeout(firstFrame.timer);
1049
+ this.firstFrame = void 0;
1050
+ firstFrame.resolve(frame);
1051
+ }
1052
+ this.emit("frame", frame);
1053
+ }
1054
+ _fail(error) {
1055
+ if (this.closed) return;
1056
+ this.rejectFirstFrame(error);
1057
+ this.emit("error", error);
1058
+ }
1059
+ rejectFirstFrame(error) {
1060
+ const firstFrame = this.firstFrame;
1061
+ if (!firstFrame) return;
1062
+ clearTimeout(firstFrame.timer);
1063
+ this.firstFrame = void 0;
1064
+ firstFrame.reject(error);
1065
+ }
1066
+ };
969
1067
  var TciClient = class extends EventEmitter2 {
970
1068
  options;
971
1069
  WebSocketImpl;
@@ -980,6 +1078,7 @@ var TciClient = class extends EventEmitter2 {
980
1078
  handshakeError;
981
1079
  initializationCommands = [];
982
1080
  handshakeWaiter;
1081
+ activeIqSession;
983
1082
  constructor(options) {
984
1083
  super();
985
1084
  this.options = {
@@ -1018,7 +1117,9 @@ var TciClient = class extends EventEmitter2 {
1018
1117
  split: {},
1019
1118
  dialectWarnings: [],
1020
1119
  rxSensors: {},
1021
- txSensors: {}
1120
+ txSensors: {},
1121
+ iq: { activeReceivers: {} },
1122
+ dds: {}
1022
1123
  };
1023
1124
  this.stateReducers = this.createStateReducers();
1024
1125
  }
@@ -1057,6 +1158,68 @@ var TciClient = class extends EventEmitter2 {
1057
1158
  getHandshakeResult() {
1058
1159
  return this.handshakeResult ? cloneHandshake(this.handshakeResult) : void 0;
1059
1160
  }
1161
+ getIqCapabilities() {
1162
+ const dialect = this.requireDialect();
1163
+ return {
1164
+ supported: dialect.supportsIqStream,
1165
+ currentSampleRate: this.state.iq.sampleRate,
1166
+ supportedSampleRates: [...dialect.iqSampleRates]
1167
+ };
1168
+ }
1169
+ async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
1170
+ const capabilities = this.getIqCapabilities();
1171
+ const requested = Math.round(sampleRate);
1172
+ if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
1173
+ if (!Number.isFinite(requested) || requested <= 0) throw new TciError("protocol-error", `Invalid TCI IQ sample rate: ${sampleRate}`);
1174
+ if (capabilities.supportedSampleRates.length > 0 && !capabilities.supportedSampleRates.includes(requested)) {
1175
+ throw new TciError("protocol-error", `TCI IQ sample rate ${requested} is not supported by dialect ${this.requireDialect().id}`);
1176
+ }
1177
+ if (this.state.iq.sampleRate === requested) return writeResult(requested, requested, "state");
1178
+ const waiter = this.waitForCommand((command) => command.name === "iq_samplerate", timeoutMs, "IQ_SAMPLERATE readback");
1179
+ try {
1180
+ await this.sendCommand("IQ_SAMPLERATE", [requested], { waitForReply: false });
1181
+ const reply = await waiter.promise;
1182
+ const applied = parseNumber(reply.args[0]);
1183
+ if (applied === void 0 || applied <= 0) throw new TciError("protocol-error", `Invalid IQ_SAMPLERATE readback: ${reply.raw}`);
1184
+ return writeResult(requested, applied, "reply");
1185
+ } finally {
1186
+ waiter.cancel();
1187
+ }
1188
+ }
1189
+ async openIqStream(options = {}) {
1190
+ if (this.activeIqSession) throw new TciError("protocol-error", "This TCI client already has an active IQ stream session");
1191
+ const capabilities = this.getIqCapabilities();
1192
+ if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
1193
+ const receiver = Math.floor(options.receiver ?? this.options.receiver);
1194
+ if (!Number.isInteger(receiver) || receiver < 0) throw new TciError("protocol-error", `Invalid TCI IQ receiver: ${receiver}`);
1195
+ const requestedRate = options.sampleRate ?? capabilities.currentSampleRate ?? capabilities.supportedSampleRates[0] ?? 48e3;
1196
+ const rateResult = await this.setIqSampleRate(requestedRate);
1197
+ let session;
1198
+ session = new TciIqStreamSession(receiver, rateResult.applied, {
1199
+ setSampleRate: (rate) => this.setIqSampleRate(rate),
1200
+ close: async () => {
1201
+ try {
1202
+ if (this.isConnected()) await this.sendCommand("IQ_STOP", [receiver], { waitForReply: false });
1203
+ } finally {
1204
+ this.state.iq.activeReceivers[String(receiver)] = false;
1205
+ if (this.activeIqSession === session) this.activeIqSession = void 0;
1206
+ this.emitState();
1207
+ }
1208
+ }
1209
+ });
1210
+ this.activeIqSession = session;
1211
+ const firstFrame = session.waitForFirstFrame(options.firstFrameTimeoutMs ?? 5e3);
1212
+ try {
1213
+ await this.sendCommand("IQ_START", [receiver], { waitForReply: false });
1214
+ this.state.iq.activeReceivers[String(receiver)] = true;
1215
+ this.emitState();
1216
+ await firstFrame;
1217
+ return session;
1218
+ } catch (error) {
1219
+ await session.close().catch(() => void 0);
1220
+ throw error;
1221
+ }
1222
+ }
1060
1223
  async sendCommand(name, args = [], options = {}) {
1061
1224
  const raw = formatTciCommand(name, args);
1062
1225
  if (options.waitForReply === false) {
@@ -1546,6 +1709,22 @@ var TciClient = class extends EventEmitter2 {
1546
1709
  this.emit("tci:binary", frame);
1547
1710
  this.emit("binary", frame);
1548
1711
  switch (frame.streamType) {
1712
+ case 0 /* IQ_STREAM */: {
1713
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
1714
+ throw new TciError("invalid-frame", "TCI IQ stream frame must contain interleaved I/Q pairs");
1715
+ }
1716
+ this.state.iq.sampleRate = frame.sampleRate;
1717
+ const iqFrame = {
1718
+ frame,
1719
+ receiver: frame.receiver,
1720
+ sampleRate: frame.sampleRate,
1721
+ centerFrequency: this.state.dds[String(frame.receiver)],
1722
+ complexSampleCount: frame.frameCount
1723
+ };
1724
+ this.emit("iqFrame", iqFrame);
1725
+ this.activeIqSession?._acceptFrame(iqFrame);
1726
+ break;
1727
+ }
1549
1728
  case 1 /* RX_AUDIO_STREAM */:
1550
1729
  this.emit("rxAudioFrame", frame);
1551
1730
  break;
@@ -1623,6 +1802,11 @@ var TciClient = class extends EventEmitter2 {
1623
1802
  this.state.modulations = args.map((mode) => mode.toLowerCase());
1624
1803
  });
1625
1804
  reducers.set("vfo", (args) => this.applyVfo(args));
1805
+ reducers.set("dds", (args) => {
1806
+ const receiver = parseNumber(args[0]);
1807
+ const frequency = parseNumber(args[1]);
1808
+ if (receiver !== void 0 && frequency !== void 0 && frequency >= 0) this.state.dds[String(receiver)] = frequency;
1809
+ });
1626
1810
  reducers.set("modulation", (args) => this.applyModulation(args));
1627
1811
  reducers.set("trx", (args) => this.applyTrx(args));
1628
1812
  reducers.set("tune", (args) => this.applyBooleanByFirstArg(this.state.tune, args));
@@ -1639,6 +1823,15 @@ var TciClient = class extends EventEmitter2 {
1639
1823
  reducers.set("tx_stream_audio_buffering", (args) => this.updateAudioState({ txBufferingMs: parseNumber(args[0]) }));
1640
1824
  reducers.set("audio_start", () => this.updateAudioState({ running: true }));
1641
1825
  reducers.set("audio_stop", () => this.updateAudioState({ running: false }));
1826
+ reducers.set("iq_samplerate", (args) => {
1827
+ this.state.iq.sampleRate = parseNumber(args[0]);
1828
+ });
1829
+ reducers.set("iq_start", (args) => {
1830
+ this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = true;
1831
+ });
1832
+ reducers.set("iq_stop", (args) => {
1833
+ this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = false;
1834
+ });
1642
1835
  return reducers;
1643
1836
  }
1644
1837
  applyVfo(args) {
@@ -1746,6 +1939,8 @@ var TciClient = class extends EventEmitter2 {
1746
1939
  this.state.ready = false;
1747
1940
  this.queue.setConnected(false);
1748
1941
  this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
1942
+ this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
1943
+ this.activeIqSession = void 0;
1749
1944
  if (wasConnected) {
1750
1945
  this.emit("disconnected", reason);
1751
1946
  this.emitState();
@@ -1826,7 +2021,9 @@ function cloneState(state) {
1826
2021
  dialectWarnings: [...state.dialectWarnings],
1827
2022
  rxSensors: cloneNested(state.rxSensors),
1828
2023
  txSensors: cloneNested(state.txSensors),
1829
- audio: state.audio ? { ...state.audio } : void 0
2024
+ audio: state.audio ? { ...state.audio } : void 0,
2025
+ iq: { ...state.iq, activeReceivers: { ...state.iq.activeReceivers } },
2026
+ dds: { ...state.dds }
1830
2027
  };
1831
2028
  }
1832
2029
  function cloneHandshake(result) {
@@ -1850,6 +2047,7 @@ export {
1850
2047
  TciCommandQueue,
1851
2048
  TciDialectRegistry,
1852
2049
  TciError,
2050
+ TciIqStreamSession,
1853
2051
  TciSampleType,
1854
2052
  TciStreamType,
1855
2053
  WebSocketTciTransport,
@@ -1861,6 +2059,7 @@ export {
1861
2059
  commandKey,
1862
2060
  compareTciVersion,
1863
2061
  createTciClient,
2062
+ decodeInterleavedIq,
1864
2063
  defaultTciDialectRegistry,
1865
2064
  deinterleaveChannels,
1866
2065
  escapeTciText,