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/CHANGELOG.md +8 -0
- package/README.md +26 -6
- package/dist/audio/index.cjs +11 -0
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +3 -2
- package/dist/audio/index.d.ts +3 -2
- package/dist/audio/index.js +10 -0
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +18 -0
- package/dist/dialect/index.cjs.map +1 -1
- package/dist/dialect/index.d.cts +2 -2
- package/dist/dialect/index.d.ts +2 -2
- package/dist/dialect/index.js +18 -0
- package/dist/dialect/index.js.map +1 -1
- package/dist/index.cjs +203 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -4
- package/dist/index.d.ts +57 -4
- package/dist/index.js +201 -2
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +26 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +5 -1
- package/dist/testing/index.d.ts +5 -1
- package/dist/testing/index.js +26 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-xRotf9gW.d.cts → types-1YXGwrgI.d.cts} +3 -1
- package/dist/{types-9seY9Th-.d.ts → types-BtPh4Dbd.d.ts} +3 -1
- package/package.json +1 -1
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
|
|
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-
|
|
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
|
|
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-
|
|
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,
|