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/testing/index.cjs
CHANGED
|
@@ -337,6 +337,7 @@ var MockTciServer = class {
|
|
|
337
337
|
mode = "DIGU";
|
|
338
338
|
ptt = false;
|
|
339
339
|
drive = 30;
|
|
340
|
+
iqSampleRate = 48e3;
|
|
340
341
|
constructor(options = {}) {
|
|
341
342
|
this.options = {
|
|
342
343
|
port: options.port ?? 0,
|
|
@@ -408,6 +409,20 @@ var MockTciServer = class {
|
|
|
408
409
|
});
|
|
409
410
|
this.broadcastBinary(frame);
|
|
410
411
|
}
|
|
412
|
+
sendIqFrame(options = {}) {
|
|
413
|
+
const samples = options.samples ?? new Float32Array(4096);
|
|
414
|
+
const frame = buildStreamFrame({
|
|
415
|
+
receiver: options.receiver ?? 0,
|
|
416
|
+
sampleRate: options.sampleRate ?? this.iqSampleRate,
|
|
417
|
+
sampleType: options.sampleType ?? 3 /* FLOAT32 */,
|
|
418
|
+
streamType: 0 /* IQ_STREAM */,
|
|
419
|
+
channels: options.channels ?? 2,
|
|
420
|
+
samples,
|
|
421
|
+
payload: options.payload,
|
|
422
|
+
lengthSemantics: options.lengthSemantics
|
|
423
|
+
});
|
|
424
|
+
this.broadcastBinary(frame);
|
|
425
|
+
}
|
|
411
426
|
sendTxChrono(options = {}) {
|
|
412
427
|
const sampleType = options.sampleType ?? 3 /* FLOAT32 */;
|
|
413
428
|
const channels = options.channels ?? 1;
|
|
@@ -437,6 +452,8 @@ var MockTciServer = class {
|
|
|
437
452
|
"DEVICE:Mock ExpertSDR3;",
|
|
438
453
|
"MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;",
|
|
439
454
|
`VFO:0,0,${this.frequency};`,
|
|
455
|
+
`DDS:0,${this.frequency};`,
|
|
456
|
+
`IQ_SAMPLERATE:${this.iqSampleRate};`,
|
|
440
457
|
`MODULATION:0,${this.mode};`,
|
|
441
458
|
`TRX:0,${this.ptt};`,
|
|
442
459
|
`DRIVE:0,${this.drive};`,
|
|
@@ -517,6 +534,15 @@ var MockTciServer = class {
|
|
|
517
534
|
case "tx_stream_audio_buffering":
|
|
518
535
|
socket.send(formatTciCommand(command.originalName, command.args));
|
|
519
536
|
break;
|
|
537
|
+
case "iq_samplerate": {
|
|
538
|
+
if (command.args[0] !== void 0) this.iqSampleRate = Number(command.args[0]);
|
|
539
|
+
socket.send(formatTciCommand("IQ_SAMPLERATE", [this.iqSampleRate]));
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "iq_start":
|
|
543
|
+
case "iq_stop":
|
|
544
|
+
socket.send(formatTciCommand(command.originalName, command.args));
|
|
545
|
+
break;
|
|
520
546
|
default:
|
|
521
547
|
if (this.options.echoUnknown) {
|
|
522
548
|
socket.send(formatTciCommand(command.originalName, command.args));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/MockTciServer.ts","../../src/errors.ts","../../src/audio/streamFrame.ts","../../src/protocol/text.ts","../../src/testing/FakeWebSocket.ts"],"sourcesContent":["export * from './MockTciServer.js';\nexport * from './FakeWebSocket.js';\n","import { AddressInfo } from 'node:net';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n buildStreamFrame,\n parseStreamFrame,\n TciSampleType,\n TciStreamType,\n type BuildStreamFrameOptions,\n type TciStreamFrame,\n} from '../audio/index.js';\nimport { formatTciCommand, parseTciText, type TciCommand } from '../protocol/index.js';\n\nexport interface MockTciServerOptions {\n port?: number;\n host?: string;\n startupCommands?: string[];\n echoUnknown?: boolean;\n commandDelayMs?: number;\n}\n\nexport interface MockTciServerCommandContext {\n server: MockTciServer;\n socket: WebSocket;\n command: TciCommand;\n}\n\nexport type MockTciServerCommandHandler = (context: MockTciServerCommandContext) => void | boolean | Promise<void | boolean>;\n\nexport class MockTciServer {\n readonly receivedCommands: TciCommand[] = [];\n readonly receivedTxAudioFrames: TciStreamFrame[] = [];\n\n private readonly options: Required<Pick<MockTciServerOptions, 'host' | 'echoUnknown' | 'commandDelayMs'>> &\n Pick<MockTciServerOptions, 'port' | 'startupCommands'>;\n private wss?: WebSocketServer;\n private sockets = new Set<WebSocket>();\n private handler?: MockTciServerCommandHandler;\n private frequency = 14_074_000;\n private mode = 'DIGU';\n private ptt = false;\n private drive = 30;\n\n constructor(options: MockTciServerOptions = {}) {\n this.options = {\n port: options.port ?? 0,\n host: options.host ?? '127.0.0.1',\n startupCommands: options.startupCommands,\n echoUnknown: options.echoUnknown ?? true,\n commandDelayMs: options.commandDelayMs ?? 0,\n };\n }\n\n async start(): Promise<void> {\n if (this.wss) {\n return;\n }\n this.wss = new WebSocketServer({ port: this.options.port, host: this.options.host });\n this.wss.on('connection', (socket) => this.handleConnection(socket));\n await new Promise<void>((resolve, reject) => {\n this.wss?.once('listening', () => resolve());\n this.wss?.once('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const sockets = [...this.sockets];\n await Promise.all(\n sockets.map(\n (socket) =>\n new Promise<void>((resolve) => {\n socket.once('close', () => resolve());\n socket.close();\n setTimeout(() => resolve(), 200).unref?.();\n }),\n ),\n );\n if (!this.wss) {\n return;\n }\n await new Promise<void>((resolve, reject) => {\n this.wss?.close((error) => (error ? reject(error) : resolve()));\n });\n this.wss = undefined;\n }\n\n url(): string {\n if (!this.wss) {\n throw new Error('MockTciServer is not started');\n }\n const address = this.wss.address() as AddressInfo;\n return `ws://${address.address}:${address.port}`;\n }\n\n onCommand(handler: MockTciServerCommandHandler): void {\n this.handler = handler;\n }\n\n broadcast(command: string): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command.endsWith(';') ? command : `${command};`);\n }\n }\n }\n\n broadcastCommand(name: string, args: readonly unknown[] = []): void {\n this.broadcast(formatTciCommand(name, args));\n }\n\n sendRxAudioFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.RX_AUDIO_STREAM,\n channels: options.channels ?? 1,\n samples: options.samples ?? new Float32Array(512),\n payload: options.payload,\n });\n this.broadcastBinary(frame);\n }\n\n sendTxChrono(options: Partial<BuildStreamFrameOptions> & { sampleCount?: number } = {}): void {\n const sampleType = options.sampleType ?? TciSampleType.FLOAT32;\n const channels = options.channels ?? 1;\n const sampleCount = options.sampleCount ?? 512;\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType,\n streamType: TciStreamType.TX_CHRONO,\n channels,\n payload: options.payload,\n sampleCount,\n });\n this.broadcastBinary(frame);\n }\n\n closeClients(): void {\n for (const socket of this.sockets) {\n socket.close();\n }\n }\n\n private handleConnection(socket: WebSocket): void {\n this.sockets.add(socket);\n socket.on('close', () => this.sockets.delete(socket));\n socket.on('message', (data, isBinary) => void this.handleMessage(socket, data, isBinary));\n\n const startupCommands = this.options.startupCommands ?? [\n 'PROTOCOL:ExpertSDR3,2.0;',\n 'DEVICE:Mock ExpertSDR3;',\n 'MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;',\n `VFO:0,0,${this.frequency};`,\n `MODULATION:0,${this.mode};`,\n `TRX:0,${this.ptt};`,\n `DRIVE:0,${this.drive};`,\n 'READY;',\n ];\n queueMicrotask(() => {\n for (const command of startupCommands) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command);\n }\n }\n });\n }\n\n private async handleMessage(socket: WebSocket, data: WebSocket.RawData, isBinary: boolean): Promise<void> {\n if (isBinary) {\n const frame = parseStreamFrame(dataToBuffer(data));\n if (frame.streamType === TciStreamType.TX_AUDIO_STREAM) {\n this.receivedTxAudioFrames.push(frame);\n }\n return;\n }\n\n for (const command of parseTciText(dataToBuffer(data))) {\n this.receivedCommands.push(command);\n if (this.handler) {\n const handled = await this.handler({ server: this, socket, command });\n if (handled === true) {\n continue;\n }\n }\n await this.delay();\n this.defaultReply(socket, command);\n }\n }\n\n private defaultReply(socket: WebSocket, command: TciCommand): void {\n if (socket.readyState !== WebSocket.OPEN) {\n return;\n }\n switch (command.name) {\n case 'vfo': {\n const receiver = command.args[0] ?? '0';\n const vfo = command.args[1] ?? '0';\n if (command.args[2] !== undefined) {\n this.frequency = Number(command.args[2]);\n }\n socket.send(formatTciCommand('VFO', [receiver, vfo, this.frequency]));\n break;\n }\n case 'modulation': {\n const receiver = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.mode = command.args[command.args.length - 1]?.toUpperCase() ?? this.mode;\n }\n socket.send(formatTciCommand('MODULATION', [receiver, this.mode]));\n break;\n }\n case 'trx': {\n const trx = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.ptt = command.args[1]?.toLowerCase() === 'true';\n }\n socket.send(formatTciCommand('TRX', [trx, this.ptt]));\n break;\n }\n case 'drive': {\n const hasTrx = command.args.length >= 2;\n const trx = hasTrx ? command.args[0] ?? '0' : '0';\n const valueArg = command.args[hasTrx ? 1 : 0];\n if (valueArg !== undefined) this.drive = Number(valueArg);\n socket.send(formatTciCommand('DRIVE', [trx, this.drive]));\n break;\n }\n case 'tune':\n case 'split_enable':\n case 'cw_macros':\n case 'cw_msg':\n case 'cw_macros_stop':\n case 'audio_samplerate':\n case 'tx_stream_audio_buffering':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n default:\n if (this.options.echoUnknown) {\n socket.send(formatTciCommand(command.originalName, command.args));\n }\n break;\n }\n }\n\n private broadcastBinary(frame: Buffer): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(frame, { binary: true });\n }\n }\n }\n\n private async delay(): Promise<void> {\n if (this.options.commandDelayMs <= 0) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, this.options.commandDelayMs));\n }\n}\n\nfunction dataToBuffer(data: WebSocket.RawData): Buffer {\n if (Buffer.isBuffer(data)) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return Buffer.from(data);\n }\n if (Array.isArray(data)) {\n return Buffer.concat(data.map((item) => dataToBuffer(item)));\n }\n throw new Error('Unsupported WebSocket data type');\n}\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'handshake-timeout'\n | 'invalid-handshake'\n | 'unknown-dialect'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport type { TciStreamLengthSemantics } from '../dialect/types.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Raw Stream.length value from the wire header. */\n headerSampleCount: number;\n /** Canonical number of scalar sample values across all channels. */\n sampleCount: number;\n /** Number of sample frames, each containing `channels` scalar values. */\n frameCount: number;\n lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;\n}\n\nexport interface ParseStreamFrameOptions {\n lengthSemantics?: TciStreamLengthSemantics;\n negotiatedChannels?: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n lengthSemantics?: TciStreamLengthSemantics;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options: ParseStreamFrameOptions = {}): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7] || options.negotiatedChannels || 0;\n const bytesPerSample = sampleTypeBytes(sampleType);\n const headerSampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n const actualScalarCount = payloadLength / bytesPerSample;\n const requestedSemantics = options.lengthSemantics ?? 'auto';\n const lengthSemantics = resolveLengthSemantics(\n requestedSemantics,\n streamType,\n headerSampleCount,\n actualScalarCount,\n channels,\n );\n const sampleCount = lengthSemantics === 'per-channel' ? headerSampleCount * channels : headerSampleCount;\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n headerSampleCount,\n sampleCount,\n frameCount: sampleCount / channels,\n lengthSemantics,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {\n throw new TciError('invalid-frame', `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);\n }\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n const lengthSemantics = options.lengthSemantics === 'per-channel' ? 'per-channel' : 'scalar';\n const headerSampleCount = lengthSemantics === 'per-channel' ? sampleCount / channels : sampleCount;\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n headerSampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nfunction resolveLengthSemantics(\n requested: TciStreamLengthSemantics,\n streamType: TciStreamType,\n headerSampleCount: number,\n actualScalarCount: number,\n channels: number,\n): Exclude<TciStreamLengthSemantics, 'auto'> {\n if (requested !== 'auto') return requested;\n if (streamType === TciStreamType.TX_CHRONO && actualScalarCount === 0) return 'scalar';\n if (actualScalarCount === headerSampleCount) return 'scalar';\n if (actualScalarCount === headerSampleCount * channels) return 'per-channel';\n return 'scalar';\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n","export interface TciCommand {\n /** Lower-case command name for case-insensitive matching. */\n name: string;\n /** Original command name as received, without surrounding whitespace. */\n originalName: string;\n /** Unescaped argument list. Empty commands have an empty array. */\n args: string[];\n /** Raw command fragment without the trailing semicolon. */\n raw: string;\n}\n\nexport type TciCommandInput = string | TciCommand;\n\nconst ESCAPE_TO_CHAR: Record<string, string> = {\n '^': ':',\n '~': ',',\n '*': ';',\n};\n\nconst CHAR_TO_ESCAPE: Record<string, string> = {\n ':': '^',\n ',': '~',\n ';': '*',\n};\n\nexport function escapeTciText(value: unknown): string {\n return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);\n}\n\nexport function unescapeTciText(value: string): string {\n return value.replace(/[\\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);\n}\n\nexport function parseTciText(text: string | Buffer | ArrayBuffer | ArrayBufferView): TciCommand[] {\n const source = normalizeTextInput(text);\n const commands: TciCommand[] = [];\n\n for (const fragment of source.split(';')) {\n const raw = fragment.trim();\n if (!raw) {\n continue;\n }\n\n const colonIndex = raw.indexOf(':');\n const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();\n if (!originalName) {\n continue;\n }\n\n const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : undefined;\n commands.push({\n name: originalName.toLowerCase(),\n originalName,\n args: argsText === undefined ? [] : splitArgs(argsText),\n raw,\n });\n }\n\n return commands;\n}\n\nexport function parseTciCommand(input: TciCommandInput): TciCommand {\n if (typeof input !== 'string') {\n return input;\n }\n const [command] = parseTciText(input);\n if (!command) {\n throw new Error(`Invalid TCI command: ${input}`);\n }\n return command;\n}\n\nexport function formatTciCommand(name: string, args: readonly unknown[] = []): string {\n const commandName = name.trim().toUpperCase();\n if (!commandName) {\n throw new Error('TCI command name cannot be empty');\n }\n if (args.length === 0) {\n return `${commandName};`;\n }\n return `${commandName}:${args.map(escapeTciText).join(',')};`;\n}\n\nexport function normalizeCommandName(name: string): string {\n return name.trim().toLowerCase();\n}\n\nexport function isCommandReplyTo(replyInput: TciCommandInput, requestInput: TciCommandInput): boolean {\n const reply = parseTciCommand(replyInput);\n const request = parseTciCommand(requestInput);\n if (reply.name !== request.name) {\n return false;\n }\n\n if (request.args.length === 0) {\n return true;\n }\n\n if (argsHavePrefix(reply.args, request.args)) {\n return true;\n }\n\n return isKnownVariantReply(reply, request);\n}\n\nexport function commandKey(command: TciCommandInput): string {\n const parsed = parseTciCommand(command);\n return `${parsed.name}:${parsed.args.join(',')}`;\n}\n\nfunction normalizeTextInput(text: string | Buffer | ArrayBuffer | ArrayBufferView): string {\n if (typeof text === 'string') {\n return text;\n }\n if (Buffer.isBuffer(text)) {\n return text.toString('utf8');\n }\n if (text instanceof ArrayBuffer) {\n return Buffer.from(text).toString('utf8');\n }\n return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString('utf8');\n}\n\nfunction splitArgs(argsText: string): string[] {\n return argsText.split(',').map((arg) => unescapeTciText(arg.trim()));\n}\n\nfunction argsHavePrefix(args: readonly string[], prefix: readonly string[]): boolean {\n if (args.length < prefix.length) {\n return false;\n }\n return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());\n}\n\nfunction isKnownVariantReply(reply: TciCommand, request: TciCommand): boolean {\n if (reply.name === 'modulation') {\n // ExpertSDR/WSJT-X variants can use MODULATION:rx,mode and MODULATION:rx,vfo,mode.\n if (request.args.length === 2 && reply.args.length >= 3) {\n return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n if (request.args.length === 3 && reply.args.length === 2) {\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();\n }\n }\n\n if (reply.name === 'protocol') {\n return request.args.length <= 1;\n }\n\n if (reply.name === 'trx' && request.args.length >= 3 && reply.args.length >= 2) {\n // Official TRX writes may include an audio source as arg3, while replies only echo trx+state.\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n\n return false;\n}\n","import { EventEmitter } from 'eventemitter3';\nimport type WebSocket from 'ws';\n\nexport interface FakeWebSocketSentMessage {\n data: WebSocket.RawData | string;\n isBinary: boolean;\n}\n\nexport type FakeWebSocketObserver = (socket: FakeWebSocket) => void;\n\n/**\n * Minimal in-memory WebSocket implementation for deterministic TciClient tests.\n * It auto-opens on the next microtask and exposes helpers to inject server data.\n */\nexport class FakeWebSocket extends EventEmitter {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n\n readonly sentMessages: FakeWebSocketSentMessage[] = [];\n readyState = FakeWebSocket.CONNECTING;\n\n constructor(readonly url: string) {\n super();\n queueMicrotask(() => this.open());\n }\n\n send(\n data: WebSocket.RawData | string,\n optionsOrCallback?: { binary?: boolean } | ((error?: Error) => void),\n callback?: (error?: Error) => void,\n ): void {\n const done = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;\n const isBinary = typeof optionsOrCallback === 'object'\n ? Boolean(optionsOrCallback.binary)\n : typeof data !== 'string';\n\n if (this.readyState !== FakeWebSocket.OPEN) {\n done?.(new Error('FakeWebSocket is not open'));\n return;\n }\n\n this.sentMessages.push({ data, isBinary });\n this.emit('sent', data, isBinary);\n done?.();\n }\n\n receive(data: WebSocket.RawData | string, isBinary = typeof data !== 'string'): void {\n if (this.readyState === FakeWebSocket.OPEN) {\n this.emit('message', typeof data === 'string' ? Buffer.from(data) : data, isBinary);\n }\n }\n\n open(): void {\n if (this.readyState !== FakeWebSocket.CONNECTING) {\n return;\n }\n this.readyState = FakeWebSocket.OPEN;\n this.emit('open');\n }\n\n close(code = 1000, reason = 'fake close'): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSING;\n queueMicrotask(() => {\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', code, Buffer.from(reason));\n });\n }\n\n terminate(): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', 1006, Buffer.from('terminated'));\n }\n}\n\nexport function createFakeWebSocketImpl(observer?: FakeWebSocketObserver): typeof WebSocket {\n return class FakeWebSocketImpl extends FakeWebSocket {\n constructor(url: string) {\n super(url);\n observer?.(this);\n }\n } as unknown as typeof WebSocket;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,gBAA2C;;;ACWpC,IAAM,WAAN,cAAuB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,MAAoB,SAAiB,SAAmB;AAClE,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;;;ACnBO,IAAM,0BAA0B,KAAK;AAiErC,SAAS,iBAAiB,OAA+C,UAAmC,CAAC,GAAmB;AACrI,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,OAAO,aAAa,yBAAyB;AAC/C,UAAM,IAAI,SAAS,iBAAiB,oCAAoC,uBAAuB,QAAQ;AAAA,EACzG;AAEA,QAAM,OAAO,IAAI,SAAS,OAAO,QAAQ,OAAO,YAAY,OAAO,UAAU;AAC7E,QAAM,SAAS,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,GAAG,UAAU,KAAK,UAAU,QAAQ,GAAG,IAAI,CAAC;AACvF,QAAM,aAAa,oBAAoB,OAAO,CAAC,CAAC;AAChD,QAAM,aAAa,oBAAoB,OAAO,CAAC,CAAC;AAChD,MAAI,WAAW,OAAO,CAAC,KAAK,QAAQ,sBAAsB;AAC1D,QAAM,iBAAiB,gBAAgB,UAAU;AACjD,QAAM,oBAAoB,OAAO,CAAC;AAClC,QAAM,sBAAsB,OAAO,aAAa;AAChD,MAAI,YAAY,GAAG;AACjB,QAAI,eAAe,qBAA2B,wBAAwB,GAAG;AACvE,iBAAW;AAAA,IACb,OAAO;AACL,YAAM,mBAAmB,oBAAoB,IAAI,sBAAsB,oBAAoB,iBAAiB;AAC5G,UAAI,CAAC,OAAO,UAAU,gBAAgB,KAAK,oBAAoB,GAAG;AAChE,cAAM,IAAI,SAAS,iBAAiB,8BAA8B,QAAQ,EAAE;AAAA,MAC9E;AACA,iBAAW;AAAA,IACb;AAAA,EACF;AACA,QAAM,gBAAgB;AACtB,QAAM,oBAAoB,iBAAiB;AAC3C,MAAI,gBAAgB,sBAAsB,GAAG;AAC3C,UAAM,IAAI,SAAS,iBAAiB,oEAAoE;AAAA,EAC1G;AAEA,QAAM,oBAAoB,gBAAgB;AAC1C,QAAM,qBAAqB,QAAQ,mBAAmB;AACtD,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,cAAc,oBAAoB,gBAAgB,oBAAoB,WAAW;AAEvF,MAAI,eAAe,mBAAyB;AAC1C,UAAM,wBAAwB,cAAc;AAC5C,QAAI,kBAAkB,uBAAuB;AAC3C,YAAM,IAAI;AAAA,QACR;AAAA,QACA,iDAAiD,iBAAiB,kBAAkB,eAAe,eAAe,qBAAqB,wBAAwB,aAAa;AAAA,MAC9K;AAAA,IACF;AAAA,EACF;AAEA,MAAI,cAAc,aAAa,GAAG;AAChC,UAAM,IAAI,SAAS,iBAAiB,2BAA2B,WAAW,wBAAwB,QAAQ,WAAW;AAAA,EACvH;AAEA,SAAO;AAAA,IACL,UAAU,OAAO,CAAC;AAAA,IAClB,YAAY,OAAO,CAAC;AAAA,IACpB;AAAA,IACA,OAAO,OAAO,CAAC;AAAA,IACf,KAAK,OAAO,CAAC;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO,MAAM,CAAC;AAAA,IACxB,SAAS,OAAO,SAAS,uBAAuB;AAAA,IAChD;AAAA,IACA;AAAA,IACA,YAAY,cAAc;AAAA,IAC1B;AAAA,EACF;AACF;AAEO,SAAS,iBAAiB,SAA0C;AACzE,QAAM,aAAa,oBAAoB,QAAQ,UAAU;AACzD,QAAM,UAAU,QAAQ,UAAU,SAAS,QAAQ,OAAO,IAAI,iBAAiB,QAAQ,WAAW,CAAC,GAAG,UAAU;AAChH,QAAM,WAAW,QAAQ;AACzB,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI,SAAS,iBAAiB,8BAA8B,QAAQ,EAAE;AAAA,EAC9E;AACA,QAAM,iBAAiB,gBAAgB,UAAU;AACjD,MAAI,QAAQ,cAAc,iBAAiB,cAAc,GAAG;AAC1D,UAAM,IAAI,SAAS,iBAAiB,oEAAoE;AAAA,EAC1G;AACA,QAAM,qBAAqB,QAAQ,aAAa;AAChD,QAAM,cAAc,QAAQ,eAAe;AAC3C,MAAI,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,GAAG;AACrD,UAAM,IAAI,SAAS,iBAAiB,6BAA6B,WAAW,EAAE;AAAA,EAChF;AACA,MAAI,QAAQ,aAAa,KAAK,gBAAgB,oBAAoB;AAChE,UAAM,IAAI,SAAS,iBAAiB,gCAAgC,WAAW,iCAAiC,kBAAkB,EAAE;AAAA,EACtI;AACA,MAAI,cAAc,aAAa,GAAG;AAChC,UAAM,IAAI,SAAS,iBAAiB,2BAA2B,WAAW,wBAAwB,QAAQ,WAAW;AAAA,EACvH;AACA,QAAM,kBAAkB,QAAQ,oBAAoB,gBAAgB,gBAAgB;AACpF,QAAM,oBAAoB,oBAAoB,gBAAgB,cAAc,WAAW;AAEvF,QAAM,QAAQ,OAAO,MAAM,0BAA0B,QAAQ,UAAU;AACvE,QAAM,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;AAC1E,QAAM,WAAW,QAAQ,YAAY,CAAC;AACtC,QAAM,SAAS;AAAA,IACb,QAAQ,YAAY;AAAA,IACpB,QAAQ;AAAA,IACR;AAAA,IACA,QAAQ,SAAS;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,UAAU,SAAS,KAAK,KAAK,CAAC;AAAA,EACjE;AACA,SAAO,QAAQ,CAAC,OAAO,UAAU,KAAK,UAAU,QAAQ,GAAG,UAAU,GAAG,IAAI,CAAC;AAC7E,UAAQ,KAAK,OAAO,uBAAuB;AAC3C,SAAO;AACT;AAEA,SAAS,uBACP,WACA,YACA,mBACA,mBACA,UAC2C;AAC3C,MAAI,cAAc,OAAQ,QAAO;AACjC,MAAI,eAAe,qBAA2B,sBAAsB,EAAG,QAAO;AAC9E,MAAI,sBAAsB,kBAAmB,QAAO;AACpD,MAAI,sBAAsB,oBAAoB,SAAU,QAAO;AAC/D,SAAO;AACT;AAMO,SAAS,gBAAgB,YAAuD;AACrF,UAAQ,oBAAoB,UAAU,GAAG;AAAA,IACvC,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AACE,YAAM,IAAI,SAAS,iBAAiB,gCAAgC,UAAU,EAAE;AAAA,EACpF;AACF;AAiBO,SAAS,oBAAoB,YAAuE;AACzG,MAAI,OAAO,eAAe,UAAU;AAClC,YAAQ,WAAW,YAAY,GAAG;AAAA,MAChC,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,cAAM,IAAI,SAAS,iBAAiB,gCAAgC,UAAU,EAAE;AAAA,IACpF;AAAA,EACF;AACA,MAAI,cAAc,iBAAuB,cAAc,iBAAuB;AAC5E,WAAO;AAAA,EACT;AACA,QAAM,IAAI,SAAS,iBAAiB,gCAAgC,UAAU,EAAE;AAClF;AAEO,SAAS,oBAAoB,YAAmD;AACrF,MAAI,cAAc,qBAA2B,cAAc,wBAA8B;AACvF,WAAO;AAAA,EACT;AACA,QAAM,IAAI,SAAS,iBAAiB,gCAAgC,UAAU,EAAE;AAClF;AAgCO,SAAS,iBAAiB,SAA2C,YAAuD;AACjI,QAAM,OAAO,oBAAoB,UAAU;AAC3C,QAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAM,UAAU,OAAO,MAAM,QAAQ,SAAS,KAAK;AACnD,QAAM,OAAO,IAAI,SAAS,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,UAAU;AAChF,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC1C,UAAM,QAAQ,YAAY,QAAQ,CAAC,KAAK,CAAC;AACzC,UAAM,SAAS,IAAI;AACnB,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,aAAK,SAAS,QAAQ,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI;AACrD;AAAA,MACF,KAAK;AACH,mBAAW,MAAM,QAAQ,KAAK,MAAM,QAAQ,OAAO,CAAC;AACpD;AAAA,MACF,KAAK;AACH,aAAK,SAAS,QAAQ,KAAK,MAAM,QAAQ,UAAU,GAAG,IAAI;AAC1D;AAAA,MACF,KAAK;AACH,aAAK,WAAW,QAAQ,OAAO,IAAI;AACnC;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AA6CA,SAAS,SAAS,OAAoE;AACpF,MAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,iBAAiB,aAAa;AAChC,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AACA,MAAI,YAAY,OAAO,KAAK,GAAG;AAC7B,WAAO,OAAO,KAAK,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;AAAA,EACrE;AACA,SAAO,OAAO,KAAK,KAAK;AAC1B;AAMA,SAAS,YAAY,OAAuB;AAC1C,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,CAAC;AACxC;AAOA,SAAS,WAAW,MAAgB,QAAgB,OAAqB;AACvE,QAAM,UAAU,KAAK,IAAI,UAAU,KAAK,IAAI,SAAS,KAAK,CAAC;AAC3D,OAAK,SAAS,QAAQ,UAAU,GAAI;AACpC,OAAK,SAAS,SAAS,GAAI,WAAW,IAAK,GAAI;AAC/C,OAAK,SAAS,SAAS,GAAI,WAAW,KAAM,GAAI;AAClD;;;AC7XA,IAAM,iBAAyC;AAAA,EAC7C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEA,IAAM,iBAAyC;AAAA,EAC7C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEO,SAAS,cAAc,OAAwB;AACpD,SAAO,OAAO,KAAK,EAAE,QAAQ,UAAU,CAAC,SAAS,eAAe,IAAI,KAAK,IAAI;AAC/E;AAEO,SAAS,gBAAgB,OAAuB;AACrD,SAAO,MAAM,QAAQ,WAAW,CAAC,SAAS,eAAe,IAAI,KAAK,IAAI;AACxE;AAEO,SAAS,aAAa,MAAqE;AAChG,QAAM,SAAS,mBAAmB,IAAI;AACtC,QAAM,WAAyB,CAAC;AAEhC,aAAW,YAAY,OAAO,MAAM,GAAG,GAAG;AACxC,UAAM,MAAM,SAAS,KAAK;AAC1B,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,aAAa,IAAI,QAAQ,GAAG;AAClC,UAAM,gBAAgB,cAAc,IAAI,IAAI,MAAM,GAAG,UAAU,IAAI,KAAK,KAAK;AAC7E,QAAI,CAAC,cAAc;AACjB;AAAA,IACF;AAEA,UAAM,WAAW,cAAc,IAAI,IAAI,MAAM,aAAa,CAAC,IAAI;AAC/D,aAAS,KAAK;AAAA,MACZ,MAAM,aAAa,YAAY;AAAA,MAC/B;AAAA,MACA,MAAM,aAAa,SAAY,CAAC,IAAI,UAAU,QAAQ;AAAA,MACtD;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAaO,SAAS,iBAAiB,MAAc,OAA2B,CAAC,GAAW;AACpF,QAAM,cAAc,KAAK,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AACA,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO,GAAG,WAAW;AAAA,EACvB;AACA,SAAO,GAAG,WAAW,IAAI,KAAK,IAAI,aAAa,EAAE,KAAK,GAAG,CAAC;AAC5D;AA6BA,SAAS,mBAAmB,MAA+D;AACzF,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS,IAAI,GAAG;AACzB,WAAO,KAAK,SAAS,MAAM;AAAA,EAC7B;AACA,MAAI,gBAAgB,aAAa;AAC/B,WAAO,OAAO,KAAK,IAAI,EAAE,SAAS,MAAM;AAAA,EAC1C;AACA,SAAO,OAAO,KAAK,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,EAAE,SAAS,MAAM;AACnF;AAEA,SAAS,UAAU,UAA4B;AAC7C,SAAO,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,QAAQ,gBAAgB,IAAI,KAAK,CAAC,CAAC;AACrE;;;AHjGO,IAAM,gBAAN,MAAoB;AAAA,EAChB,mBAAiC,CAAC;AAAA,EAClC,wBAA0C,CAAC;AAAA,EAEnC;AAAA,EAET;AAAA,EACA,UAAU,oBAAI,IAAe;AAAA,EAC7B;AAAA,EACA,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EAEhB,YAAY,UAAgC,CAAC,GAAG;AAC9C,SAAK,UAAU;AAAA,MACb,MAAM,QAAQ,QAAQ;AAAA,MACtB,MAAM,QAAQ,QAAQ;AAAA,MACtB,iBAAiB,QAAQ;AAAA,MACzB,aAAa,QAAQ,eAAe;AAAA,MACpC,gBAAgB,QAAQ,kBAAkB;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,KAAK;AACZ;AAAA,IACF;AACA,SAAK,MAAM,IAAI,0BAAgB,EAAE,MAAM,KAAK,QAAQ,MAAM,MAAM,KAAK,QAAQ,KAAK,CAAC;AACnF,SAAK,IAAI,GAAG,cAAc,CAAC,WAAW,KAAK,iBAAiB,MAAM,CAAC;AACnE,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAK,KAAK,KAAK,aAAa,MAAM,QAAQ,CAAC;AAC3C,WAAK,KAAK,KAAK,SAAS,MAAM;AAAA,IAChC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,UAAU,CAAC,GAAG,KAAK,OAAO;AAChC,UAAM,QAAQ;AAAA,MACZ,QAAQ;AAAA,QACN,CAAC,WACC,IAAI,QAAc,CAAC,YAAY;AAC7B,iBAAO,KAAK,SAAS,MAAM,QAAQ,CAAC;AACpC,iBAAO,MAAM;AACb,qBAAW,MAAM,QAAQ,GAAG,GAAG,EAAE,QAAQ;AAAA,QAC3C,CAAC;AAAA,MACL;AAAA,IACF;AACA,QAAI,CAAC,KAAK,KAAK;AACb;AAAA,IACF;AACA,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAK,KAAK,MAAM,CAAC,UAAW,QAAQ,OAAO,KAAK,IAAI,QAAQ,CAAE;AAAA,IAChE,CAAC;AACD,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,MAAc;AACZ,QAAI,CAAC,KAAK,KAAK;AACb,YAAM,IAAI,MAAM,8BAA8B;AAAA,IAChD;AACA,UAAM,UAAU,KAAK,IAAI,QAAQ;AACjC,WAAO,QAAQ,QAAQ,OAAO,IAAI,QAAQ,IAAI;AAAA,EAChD;AAAA,EAEA,UAAU,SAA4C;AACpD,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,UAAU,SAAuB;AAC/B,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,OAAO,eAAe,UAAAA,QAAU,MAAM;AACxC,eAAO,KAAK,QAAQ,SAAS,GAAG,IAAI,UAAU,GAAG,OAAO,GAAG;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,iBAAiB,MAAc,OAA2B,CAAC,GAAS;AAClE,SAAK,UAAU,iBAAiB,MAAM,IAAI,CAAC;AAAA,EAC7C;AAAA,EAEA,iBAAiB,UAA6F,CAAC,GAAS;AACtH,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,UAAU,QAAQ,YAAY;AAAA,MAC9B,YAAY,QAAQ,cAAc;AAAA,MAClC,YAAY,QAAQ;AAAA,MACpB;AAAA,MACA,UAAU,QAAQ,YAAY;AAAA,MAC9B,SAAS,QAAQ,WAAW,IAAI,aAAa,GAAG;AAAA,MAChD,SAAS,QAAQ;AAAA,IACnB,CAAC;AACD,SAAK,gBAAgB,KAAK;AAAA,EAC5B;AAAA,EAEA,aAAa,UAAuE,CAAC,GAAS;AAC5F,UAAM,aAAa,QAAQ;AAC3B,UAAM,WAAW,QAAQ,YAAY;AACrC,UAAM,cAAc,QAAQ,eAAe;AAC3C,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,UAAU,QAAQ,YAAY;AAAA,MAC9B,YAAY,QAAQ,cAAc;AAAA,MAClC;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB;AAAA,IACF,CAAC;AACD,SAAK,gBAAgB,KAAK;AAAA,EAC5B;AAAA,EAEA,eAAqB;AACnB,eAAW,UAAU,KAAK,SAAS;AACjC,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AAAA,EAEQ,iBAAiB,QAAyB;AAChD,SAAK,QAAQ,IAAI,MAAM;AACvB,WAAO,GAAG,SAAS,MAAM,KAAK,QAAQ,OAAO,MAAM,CAAC;AACpD,WAAO,GAAG,WAAW,CAAC,MAAM,aAAa,KAAK,KAAK,cAAc,QAAQ,MAAM,QAAQ,CAAC;AAExF,UAAM,kBAAkB,KAAK,QAAQ,mBAAmB;AAAA,MACtD;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,SAAS;AAAA,MACzB,gBAAgB,KAAK,IAAI;AAAA,MACzB,SAAS,KAAK,GAAG;AAAA,MACjB,WAAW,KAAK,KAAK;AAAA,MACrB;AAAA,IACF;AACA,mBAAe,MAAM;AACnB,iBAAW,WAAW,iBAAiB;AACrC,YAAI,OAAO,eAAe,UAAAA,QAAU,MAAM;AACxC,iBAAO,KAAK,OAAO;AAAA,QACrB;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,cAAc,QAAmB,MAAyB,UAAkC;AACxG,QAAI,UAAU;AACZ,YAAM,QAAQ,iBAAiB,aAAa,IAAI,CAAC;AACjD,UAAI,MAAM,wCAA8C;AACtD,aAAK,sBAAsB,KAAK,KAAK;AAAA,MACvC;AACA;AAAA,IACF;AAEA,eAAW,WAAW,aAAa,aAAa,IAAI,CAAC,GAAG;AACtD,WAAK,iBAAiB,KAAK,OAAO;AAClC,UAAI,KAAK,SAAS;AAChB,cAAM,UAAU,MAAM,KAAK,QAAQ,EAAE,QAAQ,MAAM,QAAQ,QAAQ,CAAC;AACpE,YAAI,YAAY,MAAM;AACpB;AAAA,QACF;AAAA,MACF;AACA,YAAM,KAAK,MAAM;AACjB,WAAK,aAAa,QAAQ,OAAO;AAAA,IACnC;AAAA,EACF;AAAA,EAEQ,aAAa,QAAmB,SAA2B;AACjE,QAAI,OAAO,eAAe,UAAAA,QAAU,MAAM;AACxC;AAAA,IACF;AACA,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK,OAAO;AACV,cAAM,WAAW,QAAQ,KAAK,CAAC,KAAK;AACpC,cAAM,MAAM,QAAQ,KAAK,CAAC,KAAK;AAC/B,YAAI,QAAQ,KAAK,CAAC,MAAM,QAAW;AACjC,eAAK,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC;AAAA,QACzC;AACA,eAAO,KAAK,iBAAiB,OAAO,CAAC,UAAU,KAAK,KAAK,SAAS,CAAC,CAAC;AACpE;AAAA,MACF;AAAA,MACA,KAAK,cAAc;AACjB,cAAM,WAAW,QAAQ,KAAK,CAAC,KAAK;AACpC,YAAI,QAAQ,KAAK,CAAC,MAAM,QAAW;AACjC,eAAK,OAAO,QAAQ,KAAK,QAAQ,KAAK,SAAS,CAAC,GAAG,YAAY,KAAK,KAAK;AAAA,QAC3E;AACA,eAAO,KAAK,iBAAiB,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC;AACjE;AAAA,MACF;AAAA,MACA,KAAK,OAAO;AACV,cAAM,MAAM,QAAQ,KAAK,CAAC,KAAK;AAC/B,YAAI,QAAQ,KAAK,CAAC,MAAM,QAAW;AACjC,eAAK,MAAM,QAAQ,KAAK,CAAC,GAAG,YAAY,MAAM;AAAA,QAChD;AACA,eAAO,KAAK,iBAAiB,OAAO,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC;AACpD;AAAA,MACF;AAAA,MACA,KAAK,SAAS;AACZ,cAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,cAAM,MAAM,SAAS,QAAQ,KAAK,CAAC,KAAK,MAAM;AAC9C,cAAM,WAAW,QAAQ,KAAK,SAAS,IAAI,CAAC;AAC5C,YAAI,aAAa,OAAW,MAAK,QAAQ,OAAO,QAAQ;AACxD,eAAO,KAAK,iBAAiB,SAAS,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;AACxD;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO,KAAK,iBAAiB,QAAQ,cAAc,QAAQ,IAAI,CAAC;AAChE;AAAA,MACF;AACE,YAAI,KAAK,QAAQ,aAAa;AAC5B,iBAAO,KAAK,iBAAiB,QAAQ,cAAc,QAAQ,IAAI,CAAC;AAAA,QAClE;AACA;AAAA,IACJ;AAAA,EACF;AAAA,EAEQ,gBAAgB,OAAqB;AAC3C,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,OAAO,eAAe,UAAAA,QAAU,MAAM;AACxC,eAAO,KAAK,OAAO,EAAE,QAAQ,KAAK,CAAC;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,QAAuB;AACnC,QAAI,KAAK,QAAQ,kBAAkB,GAAG;AACpC;AAAA,IACF;AACA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,QAAQ,cAAc,CAAC;AAAA,EACjF;AACF;AAEA,SAAS,aAAa,MAAiC;AACrD,MAAI,OAAO,SAAS,IAAI,GAAG;AACzB,WAAO;AAAA,EACT;AACA,MAAI,gBAAgB,aAAa;AAC/B,WAAO,OAAO,KAAK,IAAI;AAAA,EACzB;AACA,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,OAAO,OAAO,KAAK,IAAI,CAAC,SAAS,aAAa,IAAI,CAAC,CAAC;AAAA,EAC7D;AACA,QAAM,IAAI,MAAM,iCAAiC;AACnD;;;AIhRA,2BAA6B;AActB,IAAM,gBAAN,MAAM,uBAAsB,kCAAa;AAAA,EAS9C,YAAqB,KAAa;AAChC,UAAM;AADa;AAEnB,mBAAe,MAAM,KAAK,KAAK,CAAC;AAAA,EAClC;AAAA,EAHqB;AAAA,EARrB,OAAgB,aAAa;AAAA,EAC7B,OAAgB,OAAO;AAAA,EACvB,OAAgB,UAAU;AAAA,EAC1B,OAAgB,SAAS;AAAA,EAEhB,eAA2C,CAAC;AAAA,EACrD,aAAa,eAAc;AAAA,EAO3B,KACE,MACA,mBACA,UACM;AACN,UAAM,OAAO,OAAO,sBAAsB,aAAa,oBAAoB;AAC3E,UAAM,WAAW,OAAO,sBAAsB,WAC1C,QAAQ,kBAAkB,MAAM,IAChC,OAAO,SAAS;AAEpB,QAAI,KAAK,eAAe,eAAc,MAAM;AAC1C,aAAO,IAAI,MAAM,2BAA2B,CAAC;AAC7C;AAAA,IACF;AAEA,SAAK,aAAa,KAAK,EAAE,MAAM,SAAS,CAAC;AACzC,SAAK,KAAK,QAAQ,MAAM,QAAQ;AAChC,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,MAAkC,WAAW,OAAO,SAAS,UAAgB;AACnF,QAAI,KAAK,eAAe,eAAc,MAAM;AAC1C,WAAK,KAAK,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK,IAAI,IAAI,MAAM,QAAQ;AAAA,IACpF;AAAA,EACF;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,eAAe,eAAc,YAAY;AAChD;AAAA,IACF;AACA,SAAK,aAAa,eAAc;AAChC,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,KAAM,SAAS,cAAoB;AAC9C,QAAI,KAAK,eAAe,eAAc,QAAQ;AAC5C;AAAA,IACF;AACA,SAAK,aAAa,eAAc;AAChC,mBAAe,MAAM;AACnB,WAAK,aAAa,eAAc;AAChC,WAAK,KAAK,SAAS,MAAM,OAAO,KAAK,MAAM,CAAC;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA,EAEA,YAAkB;AAChB,QAAI,KAAK,eAAe,eAAc,QAAQ;AAC5C;AAAA,IACF;AACA,SAAK,aAAa,eAAc;AAChC,SAAK,KAAK,SAAS,MAAM,OAAO,KAAK,YAAY,CAAC;AAAA,EACpD;AACF;AAEO,SAAS,wBAAwB,UAAoD;AAC1F,SAAO,MAAM,0BAA0B,cAAc;AAAA,IACnD,YAAY,KAAa;AACvB,YAAM,GAAG;AACT,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF;AACF;","names":["WebSocket"]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/MockTciServer.ts","../../src/errors.ts","../../src/audio/streamFrame.ts","../../src/protocol/text.ts","../../src/testing/FakeWebSocket.ts"],"sourcesContent":["export * from './MockTciServer.js';\nexport * from './FakeWebSocket.js';\n","import { AddressInfo } from 'node:net';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n buildStreamFrame,\n parseStreamFrame,\n TciSampleType,\n TciStreamType,\n type BuildStreamFrameOptions,\n type TciStreamFrame,\n} from '../audio/index.js';\nimport { formatTciCommand, parseTciText, type TciCommand } from '../protocol/index.js';\n\nexport interface MockTciServerOptions {\n port?: number;\n host?: string;\n startupCommands?: string[];\n echoUnknown?: boolean;\n commandDelayMs?: number;\n}\n\nexport interface MockTciServerCommandContext {\n server: MockTciServer;\n socket: WebSocket;\n command: TciCommand;\n}\n\nexport type MockTciServerCommandHandler = (context: MockTciServerCommandContext) => void | boolean | Promise<void | boolean>;\n\nexport class MockTciServer {\n readonly receivedCommands: TciCommand[] = [];\n readonly receivedTxAudioFrames: TciStreamFrame[] = [];\n\n private readonly options: Required<Pick<MockTciServerOptions, 'host' | 'echoUnknown' | 'commandDelayMs'>> &\n Pick<MockTciServerOptions, 'port' | 'startupCommands'>;\n private wss?: WebSocketServer;\n private sockets = new Set<WebSocket>();\n private handler?: MockTciServerCommandHandler;\n private frequency = 14_074_000;\n private mode = 'DIGU';\n private ptt = false;\n private drive = 30;\n private iqSampleRate = 48_000;\n\n constructor(options: MockTciServerOptions = {}) {\n this.options = {\n port: options.port ?? 0,\n host: options.host ?? '127.0.0.1',\n startupCommands: options.startupCommands,\n echoUnknown: options.echoUnknown ?? true,\n commandDelayMs: options.commandDelayMs ?? 0,\n };\n }\n\n async start(): Promise<void> {\n if (this.wss) {\n return;\n }\n this.wss = new WebSocketServer({ port: this.options.port, host: this.options.host });\n this.wss.on('connection', (socket) => this.handleConnection(socket));\n await new Promise<void>((resolve, reject) => {\n this.wss?.once('listening', () => resolve());\n this.wss?.once('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const sockets = [...this.sockets];\n await Promise.all(\n sockets.map(\n (socket) =>\n new Promise<void>((resolve) => {\n socket.once('close', () => resolve());\n socket.close();\n setTimeout(() => resolve(), 200).unref?.();\n }),\n ),\n );\n if (!this.wss) {\n return;\n }\n await new Promise<void>((resolve, reject) => {\n this.wss?.close((error) => (error ? reject(error) : resolve()));\n });\n this.wss = undefined;\n }\n\n url(): string {\n if (!this.wss) {\n throw new Error('MockTciServer is not started');\n }\n const address = this.wss.address() as AddressInfo;\n return `ws://${address.address}:${address.port}`;\n }\n\n onCommand(handler: MockTciServerCommandHandler): void {\n this.handler = handler;\n }\n\n broadcast(command: string): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command.endsWith(';') ? command : `${command};`);\n }\n }\n }\n\n broadcastCommand(name: string, args: readonly unknown[] = []): void {\n this.broadcast(formatTciCommand(name, args));\n }\n\n sendRxAudioFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.RX_AUDIO_STREAM,\n channels: options.channels ?? 1,\n samples: options.samples ?? new Float32Array(512),\n payload: options.payload,\n });\n this.broadcastBinary(frame);\n }\n\n sendIqFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const samples = options.samples ?? new Float32Array(4096);\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? this.iqSampleRate,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.IQ_STREAM,\n channels: options.channels ?? 2,\n samples,\n payload: options.payload,\n lengthSemantics: options.lengthSemantics,\n });\n this.broadcastBinary(frame);\n }\n\n sendTxChrono(options: Partial<BuildStreamFrameOptions> & { sampleCount?: number } = {}): void {\n const sampleType = options.sampleType ?? TciSampleType.FLOAT32;\n const channels = options.channels ?? 1;\n const sampleCount = options.sampleCount ?? 512;\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType,\n streamType: TciStreamType.TX_CHRONO,\n channels,\n payload: options.payload,\n sampleCount,\n });\n this.broadcastBinary(frame);\n }\n\n closeClients(): void {\n for (const socket of this.sockets) {\n socket.close();\n }\n }\n\n private handleConnection(socket: WebSocket): void {\n this.sockets.add(socket);\n socket.on('close', () => this.sockets.delete(socket));\n socket.on('message', (data, isBinary) => void this.handleMessage(socket, data, isBinary));\n\n const startupCommands = this.options.startupCommands ?? [\n 'PROTOCOL:ExpertSDR3,2.0;',\n 'DEVICE:Mock ExpertSDR3;',\n 'MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;',\n `VFO:0,0,${this.frequency};`,\n `DDS:0,${this.frequency};`,\n `IQ_SAMPLERATE:${this.iqSampleRate};`,\n `MODULATION:0,${this.mode};`,\n `TRX:0,${this.ptt};`,\n `DRIVE:0,${this.drive};`,\n 'READY;',\n ];\n queueMicrotask(() => {\n for (const command of startupCommands) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command);\n }\n }\n });\n }\n\n private async handleMessage(socket: WebSocket, data: WebSocket.RawData, isBinary: boolean): Promise<void> {\n if (isBinary) {\n const frame = parseStreamFrame(dataToBuffer(data));\n if (frame.streamType === TciStreamType.TX_AUDIO_STREAM) {\n this.receivedTxAudioFrames.push(frame);\n }\n return;\n }\n\n for (const command of parseTciText(dataToBuffer(data))) {\n this.receivedCommands.push(command);\n if (this.handler) {\n const handled = await this.handler({ server: this, socket, command });\n if (handled === true) {\n continue;\n }\n }\n await this.delay();\n this.defaultReply(socket, command);\n }\n }\n\n private defaultReply(socket: WebSocket, command: TciCommand): void {\n if (socket.readyState !== WebSocket.OPEN) {\n return;\n }\n switch (command.name) {\n case 'vfo': {\n const receiver = command.args[0] ?? '0';\n const vfo = command.args[1] ?? '0';\n if (command.args[2] !== undefined) {\n this.frequency = Number(command.args[2]);\n }\n socket.send(formatTciCommand('VFO', [receiver, vfo, this.frequency]));\n break;\n }\n case 'modulation': {\n const receiver = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.mode = command.args[command.args.length - 1]?.toUpperCase() ?? this.mode;\n }\n socket.send(formatTciCommand('MODULATION', [receiver, this.mode]));\n break;\n }\n case 'trx': {\n const trx = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.ptt = command.args[1]?.toLowerCase() === 'true';\n }\n socket.send(formatTciCommand('TRX', [trx, this.ptt]));\n break;\n }\n case 'drive': {\n const hasTrx = command.args.length >= 2;\n const trx = hasTrx ? command.args[0] ?? '0' : '0';\n const valueArg = command.args[hasTrx ? 1 : 0];\n if (valueArg !== undefined) this.drive = Number(valueArg);\n socket.send(formatTciCommand('DRIVE', [trx, this.drive]));\n break;\n }\n case 'tune':\n case 'split_enable':\n case 'cw_macros':\n case 'cw_msg':\n case 'cw_macros_stop':\n case 'audio_samplerate':\n case 'tx_stream_audio_buffering':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n case 'iq_samplerate': {\n if (command.args[0] !== undefined) this.iqSampleRate = Number(command.args[0]);\n socket.send(formatTciCommand('IQ_SAMPLERATE', [this.iqSampleRate]));\n break;\n }\n case 'iq_start':\n case 'iq_stop':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n default:\n if (this.options.echoUnknown) {\n socket.send(formatTciCommand(command.originalName, command.args));\n }\n break;\n }\n }\n\n private broadcastBinary(frame: Buffer): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(frame, { binary: true });\n }\n }\n }\n\n private async delay(): Promise<void> {\n if (this.options.commandDelayMs <= 0) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, this.options.commandDelayMs));\n }\n}\n\nfunction dataToBuffer(data: WebSocket.RawData): Buffer {\n if (Buffer.isBuffer(data)) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return Buffer.from(data);\n }\n if (Array.isArray(data)) {\n return Buffer.concat(data.map((item) => dataToBuffer(item)));\n }\n throw new Error('Unsupported WebSocket data type');\n}\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'handshake-timeout'\n | 'invalid-handshake'\n | 'unknown-dialect'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport type { TciStreamLengthSemantics } from '../dialect/types.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Raw Stream.length value from the wire header. */\n headerSampleCount: number;\n /** Canonical number of scalar sample values across all channels. */\n sampleCount: number;\n /** Number of sample frames, each containing `channels` scalar values. */\n frameCount: number;\n lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;\n}\n\nexport interface ParseStreamFrameOptions {\n lengthSemantics?: TciStreamLengthSemantics;\n negotiatedChannels?: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n lengthSemantics?: TciStreamLengthSemantics;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options: ParseStreamFrameOptions = {}): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7] || options.negotiatedChannels || 0;\n const bytesPerSample = sampleTypeBytes(sampleType);\n const headerSampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n const actualScalarCount = payloadLength / bytesPerSample;\n const requestedSemantics = options.lengthSemantics ?? 'auto';\n const lengthSemantics = resolveLengthSemantics(\n requestedSemantics,\n streamType,\n headerSampleCount,\n actualScalarCount,\n channels,\n );\n const sampleCount = lengthSemantics === 'per-channel' ? headerSampleCount * channels : headerSampleCount;\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n headerSampleCount,\n sampleCount,\n frameCount: sampleCount / channels,\n lengthSemantics,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {\n throw new TciError('invalid-frame', `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);\n }\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n const lengthSemantics = options.lengthSemantics === 'per-channel' ? 'per-channel' : 'scalar';\n const headerSampleCount = lengthSemantics === 'per-channel' ? sampleCount / channels : sampleCount;\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n headerSampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nfunction resolveLengthSemantics(\n requested: TciStreamLengthSemantics,\n streamType: TciStreamType,\n headerSampleCount: number,\n actualScalarCount: number,\n channels: number,\n): Exclude<TciStreamLengthSemantics, 'auto'> {\n if (requested !== 'auto') return requested;\n if (streamType === TciStreamType.TX_CHRONO && actualScalarCount === 0) return 'scalar';\n if (actualScalarCount === headerSampleCount) return 'scalar';\n if (actualScalarCount === headerSampleCount * channels) return 'per-channel';\n return 'scalar';\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function decodeInterleavedIq(frame: TciStreamFrame): Float32Array {\n if (frame.streamType !== TciStreamType.IQ_STREAM) {\n throw new TciError('invalid-frame', 'Expected a TCI IQ stream frame');\n }\n if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {\n throw new TciError('invalid-frame', `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);\n }\n return payloadToFloat32(frame);\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n","export interface TciCommand {\n /** Lower-case command name for case-insensitive matching. */\n name: string;\n /** Original command name as received, without surrounding whitespace. */\n originalName: string;\n /** Unescaped argument list. Empty commands have an empty array. */\n args: string[];\n /** Raw command fragment without the trailing semicolon. */\n raw: string;\n}\n\nexport type TciCommandInput = string | TciCommand;\n\nconst ESCAPE_TO_CHAR: Record<string, string> = {\n '^': ':',\n '~': ',',\n '*': ';',\n};\n\nconst CHAR_TO_ESCAPE: Record<string, string> = {\n ':': '^',\n ',': '~',\n ';': '*',\n};\n\nexport function escapeTciText(value: unknown): string {\n return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);\n}\n\nexport function unescapeTciText(value: string): string {\n return value.replace(/[\\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);\n}\n\nexport function parseTciText(text: string | Buffer | ArrayBuffer | ArrayBufferView): TciCommand[] {\n const source = normalizeTextInput(text);\n const commands: TciCommand[] = [];\n\n for (const fragment of source.split(';')) {\n const raw = fragment.trim();\n if (!raw) {\n continue;\n }\n\n const colonIndex = raw.indexOf(':');\n const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();\n if (!originalName) {\n continue;\n }\n\n const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : undefined;\n commands.push({\n name: originalName.toLowerCase(),\n originalName,\n args: argsText === undefined ? [] : splitArgs(argsText),\n raw,\n });\n }\n\n return commands;\n}\n\nexport function parseTciCommand(input: TciCommandInput): TciCommand {\n if (typeof input !== 'string') {\n return input;\n }\n const [command] = parseTciText(input);\n if (!command) {\n throw new Error(`Invalid TCI command: ${input}`);\n }\n return command;\n}\n\nexport function formatTciCommand(name: string, args: readonly unknown[] = []): string {\n const commandName = name.trim().toUpperCase();\n if (!commandName) {\n throw new Error('TCI command name cannot be empty');\n }\n if (args.length === 0) {\n return `${commandName};`;\n }\n return `${commandName}:${args.map(escapeTciText).join(',')};`;\n}\n\nexport function normalizeCommandName(name: string): string {\n return name.trim().toLowerCase();\n}\n\nexport function isCommandReplyTo(replyInput: TciCommandInput, requestInput: TciCommandInput): boolean {\n const reply = parseTciCommand(replyInput);\n const request = parseTciCommand(requestInput);\n if (reply.name !== request.name) {\n return false;\n }\n\n if (request.args.length === 0) {\n return true;\n }\n\n if (argsHavePrefix(reply.args, request.args)) {\n return true;\n }\n\n return isKnownVariantReply(reply, request);\n}\n\nexport function commandKey(command: TciCommandInput): string {\n const parsed = parseTciCommand(command);\n return `${parsed.name}:${parsed.args.join(',')}`;\n}\n\nfunction normalizeTextInput(text: string | Buffer | ArrayBuffer | ArrayBufferView): string {\n if (typeof text === 'string') {\n return text;\n }\n if (Buffer.isBuffer(text)) {\n return text.toString('utf8');\n }\n if (text instanceof ArrayBuffer) {\n return Buffer.from(text).toString('utf8');\n }\n return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString('utf8');\n}\n\nfunction splitArgs(argsText: string): string[] {\n return argsText.split(',').map((arg) => unescapeTciText(arg.trim()));\n}\n\nfunction argsHavePrefix(args: readonly string[], prefix: readonly string[]): boolean {\n if (args.length < prefix.length) {\n return false;\n }\n return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());\n}\n\nfunction isKnownVariantReply(reply: TciCommand, request: TciCommand): boolean {\n if (reply.name === 'modulation') {\n // ExpertSDR/WSJT-X variants can use MODULATION:rx,mode and MODULATION:rx,vfo,mode.\n if (request.args.length === 2 && reply.args.length >= 3) {\n return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n if (request.args.length === 3 && reply.args.length === 2) {\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();\n }\n }\n\n if (reply.name === 'protocol') {\n return request.args.length <= 1;\n }\n\n if (reply.name === 'trx' && request.args.length >= 3 && reply.args.length >= 2) {\n // Official TRX writes may include an audio source as arg3, while replies only echo trx+state.\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n\n return false;\n}\n","import { EventEmitter } from 'eventemitter3';\nimport type WebSocket from 'ws';\n\nexport interface FakeWebSocketSentMessage {\n data: WebSocket.RawData | string;\n isBinary: boolean;\n}\n\nexport type FakeWebSocketObserver = (socket: FakeWebSocket) => void;\n\n/**\n * Minimal in-memory WebSocket implementation for deterministic TciClient tests.\n * It auto-opens on the next microtask and exposes helpers to inject server data.\n */\nexport class FakeWebSocket extends EventEmitter {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n\n readonly sentMessages: FakeWebSocketSentMessage[] = [];\n readyState = FakeWebSocket.CONNECTING;\n\n constructor(readonly url: string) {\n super();\n queueMicrotask(() => this.open());\n }\n\n send(\n data: WebSocket.RawData | string,\n optionsOrCallback?: { binary?: boolean } | ((error?: Error) => void),\n callback?: (error?: Error) => void,\n ): void {\n const done = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;\n const isBinary = typeof optionsOrCallback === 'object'\n ? Boolean(optionsOrCallback.binary)\n : typeof data !== 'string';\n\n if (this.readyState !== FakeWebSocket.OPEN) {\n done?.(new Error('FakeWebSocket is not open'));\n return;\n }\n\n this.sentMessages.push({ data, isBinary });\n this.emit('sent', data, isBinary);\n done?.();\n }\n\n receive(data: WebSocket.RawData | string, isBinary = typeof data !== 'string'): void {\n if (this.readyState === FakeWebSocket.OPEN) {\n this.emit('message', typeof data === 'string' ? Buffer.from(data) : data, isBinary);\n }\n }\n\n open(): void {\n if (this.readyState !== FakeWebSocket.CONNECTING) {\n return;\n }\n this.readyState = FakeWebSocket.OPEN;\n this.emit('open');\n }\n\n close(code = 1000, reason = 'fake close'): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSING;\n queueMicrotask(() => {\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', code, Buffer.from(reason));\n });\n }\n\n terminate(): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', 1006, Buffer.from('terminated'));\n }\n}\n\nexport function createFakeWebSocketImpl(observer?: FakeWebSocketObserver): typeof WebSocket {\n return class FakeWebSocketImpl extends FakeWebSocket {\n constructor(url: string) {\n super(url);\n observer?.(this);\n }\n } as unknown as typeof WebSocket;\n}\n"],"mappings":"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","names":["WebSocket"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import WebSocket from 'ws';
|
|
|
2
2
|
import { TciStreamFrame, BuildStreamFrameOptions } from '../audio/index.cjs';
|
|
3
3
|
import { T as TciCommand } from '../text-BwCWY1k1.cjs';
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-1YXGwrgI.cjs';
|
|
6
6
|
|
|
7
7
|
interface MockTciServerOptions {
|
|
8
8
|
port?: number;
|
|
@@ -28,6 +28,7 @@ declare class MockTciServer {
|
|
|
28
28
|
private mode;
|
|
29
29
|
private ptt;
|
|
30
30
|
private drive;
|
|
31
|
+
private iqSampleRate;
|
|
31
32
|
constructor(options?: MockTciServerOptions);
|
|
32
33
|
start(): Promise<void>;
|
|
33
34
|
stop(): Promise<void>;
|
|
@@ -38,6 +39,9 @@ declare class MockTciServer {
|
|
|
38
39
|
sendRxAudioFrame(options?: Partial<BuildStreamFrameOptions> & {
|
|
39
40
|
samples?: Float32Array | readonly number[];
|
|
40
41
|
}): void;
|
|
42
|
+
sendIqFrame(options?: Partial<BuildStreamFrameOptions> & {
|
|
43
|
+
samples?: Float32Array | readonly number[];
|
|
44
|
+
}): void;
|
|
41
45
|
sendTxChrono(options?: Partial<BuildStreamFrameOptions> & {
|
|
42
46
|
sampleCount?: number;
|
|
43
47
|
}): void;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import WebSocket from 'ws';
|
|
|
2
2
|
import { TciStreamFrame, BuildStreamFrameOptions } from '../audio/index.js';
|
|
3
3
|
import { T as TciCommand } from '../text-BwCWY1k1.js';
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-BtPh4Dbd.js';
|
|
6
6
|
|
|
7
7
|
interface MockTciServerOptions {
|
|
8
8
|
port?: number;
|
|
@@ -28,6 +28,7 @@ declare class MockTciServer {
|
|
|
28
28
|
private mode;
|
|
29
29
|
private ptt;
|
|
30
30
|
private drive;
|
|
31
|
+
private iqSampleRate;
|
|
31
32
|
constructor(options?: MockTciServerOptions);
|
|
32
33
|
start(): Promise<void>;
|
|
33
34
|
stop(): Promise<void>;
|
|
@@ -38,6 +39,9 @@ declare class MockTciServer {
|
|
|
38
39
|
sendRxAudioFrame(options?: Partial<BuildStreamFrameOptions> & {
|
|
39
40
|
samples?: Float32Array | readonly number[];
|
|
40
41
|
}): void;
|
|
42
|
+
sendIqFrame(options?: Partial<BuildStreamFrameOptions> & {
|
|
43
|
+
samples?: Float32Array | readonly number[];
|
|
44
|
+
}): void;
|
|
41
45
|
sendTxChrono(options?: Partial<BuildStreamFrameOptions> & {
|
|
42
46
|
sampleCount?: number;
|
|
43
47
|
}): void;
|
package/dist/testing/index.js
CHANGED
|
@@ -299,6 +299,7 @@ var MockTciServer = class {
|
|
|
299
299
|
mode = "DIGU";
|
|
300
300
|
ptt = false;
|
|
301
301
|
drive = 30;
|
|
302
|
+
iqSampleRate = 48e3;
|
|
302
303
|
constructor(options = {}) {
|
|
303
304
|
this.options = {
|
|
304
305
|
port: options.port ?? 0,
|
|
@@ -370,6 +371,20 @@ var MockTciServer = class {
|
|
|
370
371
|
});
|
|
371
372
|
this.broadcastBinary(frame);
|
|
372
373
|
}
|
|
374
|
+
sendIqFrame(options = {}) {
|
|
375
|
+
const samples = options.samples ?? new Float32Array(4096);
|
|
376
|
+
const frame = buildStreamFrame({
|
|
377
|
+
receiver: options.receiver ?? 0,
|
|
378
|
+
sampleRate: options.sampleRate ?? this.iqSampleRate,
|
|
379
|
+
sampleType: options.sampleType ?? 3 /* FLOAT32 */,
|
|
380
|
+
streamType: 0 /* IQ_STREAM */,
|
|
381
|
+
channels: options.channels ?? 2,
|
|
382
|
+
samples,
|
|
383
|
+
payload: options.payload,
|
|
384
|
+
lengthSemantics: options.lengthSemantics
|
|
385
|
+
});
|
|
386
|
+
this.broadcastBinary(frame);
|
|
387
|
+
}
|
|
373
388
|
sendTxChrono(options = {}) {
|
|
374
389
|
const sampleType = options.sampleType ?? 3 /* FLOAT32 */;
|
|
375
390
|
const channels = options.channels ?? 1;
|
|
@@ -399,6 +414,8 @@ var MockTciServer = class {
|
|
|
399
414
|
"DEVICE:Mock ExpertSDR3;",
|
|
400
415
|
"MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;",
|
|
401
416
|
`VFO:0,0,${this.frequency};`,
|
|
417
|
+
`DDS:0,${this.frequency};`,
|
|
418
|
+
`IQ_SAMPLERATE:${this.iqSampleRate};`,
|
|
402
419
|
`MODULATION:0,${this.mode};`,
|
|
403
420
|
`TRX:0,${this.ptt};`,
|
|
404
421
|
`DRIVE:0,${this.drive};`,
|
|
@@ -479,6 +496,15 @@ var MockTciServer = class {
|
|
|
479
496
|
case "tx_stream_audio_buffering":
|
|
480
497
|
socket.send(formatTciCommand(command.originalName, command.args));
|
|
481
498
|
break;
|
|
499
|
+
case "iq_samplerate": {
|
|
500
|
+
if (command.args[0] !== void 0) this.iqSampleRate = Number(command.args[0]);
|
|
501
|
+
socket.send(formatTciCommand("IQ_SAMPLERATE", [this.iqSampleRate]));
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "iq_start":
|
|
505
|
+
case "iq_stop":
|
|
506
|
+
socket.send(formatTciCommand(command.originalName, command.args));
|
|
507
|
+
break;
|
|
482
508
|
default:
|
|
483
509
|
if (this.options.echoUnknown) {
|
|
484
510
|
socket.send(formatTciCommand(command.originalName, command.args));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/MockTciServer.ts","../../src/errors.ts","../../src/audio/streamFrame.ts","../../src/protocol/text.ts","../../src/testing/FakeWebSocket.ts"],"sourcesContent":["import { AddressInfo } from 'node:net';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n buildStreamFrame,\n parseStreamFrame,\n TciSampleType,\n TciStreamType,\n type BuildStreamFrameOptions,\n type TciStreamFrame,\n} from '../audio/index.js';\nimport { formatTciCommand, parseTciText, type TciCommand } from '../protocol/index.js';\n\nexport interface MockTciServerOptions {\n port?: number;\n host?: string;\n startupCommands?: string[];\n echoUnknown?: boolean;\n commandDelayMs?: number;\n}\n\nexport interface MockTciServerCommandContext {\n server: MockTciServer;\n socket: WebSocket;\n command: TciCommand;\n}\n\nexport type MockTciServerCommandHandler = (context: MockTciServerCommandContext) => void | boolean | Promise<void | boolean>;\n\nexport class MockTciServer {\n readonly receivedCommands: TciCommand[] = [];\n readonly receivedTxAudioFrames: TciStreamFrame[] = [];\n\n private readonly options: Required<Pick<MockTciServerOptions, 'host' | 'echoUnknown' | 'commandDelayMs'>> &\n Pick<MockTciServerOptions, 'port' | 'startupCommands'>;\n private wss?: WebSocketServer;\n private sockets = new Set<WebSocket>();\n private handler?: MockTciServerCommandHandler;\n private frequency = 14_074_000;\n private mode = 'DIGU';\n private ptt = false;\n private drive = 30;\n\n constructor(options: MockTciServerOptions = {}) {\n this.options = {\n port: options.port ?? 0,\n host: options.host ?? '127.0.0.1',\n startupCommands: options.startupCommands,\n echoUnknown: options.echoUnknown ?? true,\n commandDelayMs: options.commandDelayMs ?? 0,\n };\n }\n\n async start(): Promise<void> {\n if (this.wss) {\n return;\n }\n this.wss = new WebSocketServer({ port: this.options.port, host: this.options.host });\n this.wss.on('connection', (socket) => this.handleConnection(socket));\n await new Promise<void>((resolve, reject) => {\n this.wss?.once('listening', () => resolve());\n this.wss?.once('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const sockets = [...this.sockets];\n await Promise.all(\n sockets.map(\n (socket) =>\n new Promise<void>((resolve) => {\n socket.once('close', () => resolve());\n socket.close();\n setTimeout(() => resolve(), 200).unref?.();\n }),\n ),\n );\n if (!this.wss) {\n return;\n }\n await new Promise<void>((resolve, reject) => {\n this.wss?.close((error) => (error ? reject(error) : resolve()));\n });\n this.wss = undefined;\n }\n\n url(): string {\n if (!this.wss) {\n throw new Error('MockTciServer is not started');\n }\n const address = this.wss.address() as AddressInfo;\n return `ws://${address.address}:${address.port}`;\n }\n\n onCommand(handler: MockTciServerCommandHandler): void {\n this.handler = handler;\n }\n\n broadcast(command: string): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command.endsWith(';') ? command : `${command};`);\n }\n }\n }\n\n broadcastCommand(name: string, args: readonly unknown[] = []): void {\n this.broadcast(formatTciCommand(name, args));\n }\n\n sendRxAudioFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.RX_AUDIO_STREAM,\n channels: options.channels ?? 1,\n samples: options.samples ?? new Float32Array(512),\n payload: options.payload,\n });\n this.broadcastBinary(frame);\n }\n\n sendTxChrono(options: Partial<BuildStreamFrameOptions> & { sampleCount?: number } = {}): void {\n const sampleType = options.sampleType ?? TciSampleType.FLOAT32;\n const channels = options.channels ?? 1;\n const sampleCount = options.sampleCount ?? 512;\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType,\n streamType: TciStreamType.TX_CHRONO,\n channels,\n payload: options.payload,\n sampleCount,\n });\n this.broadcastBinary(frame);\n }\n\n closeClients(): void {\n for (const socket of this.sockets) {\n socket.close();\n }\n }\n\n private handleConnection(socket: WebSocket): void {\n this.sockets.add(socket);\n socket.on('close', () => this.sockets.delete(socket));\n socket.on('message', (data, isBinary) => void this.handleMessage(socket, data, isBinary));\n\n const startupCommands = this.options.startupCommands ?? [\n 'PROTOCOL:ExpertSDR3,2.0;',\n 'DEVICE:Mock ExpertSDR3;',\n 'MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;',\n `VFO:0,0,${this.frequency};`,\n `MODULATION:0,${this.mode};`,\n `TRX:0,${this.ptt};`,\n `DRIVE:0,${this.drive};`,\n 'READY;',\n ];\n queueMicrotask(() => {\n for (const command of startupCommands) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command);\n }\n }\n });\n }\n\n private async handleMessage(socket: WebSocket, data: WebSocket.RawData, isBinary: boolean): Promise<void> {\n if (isBinary) {\n const frame = parseStreamFrame(dataToBuffer(data));\n if (frame.streamType === TciStreamType.TX_AUDIO_STREAM) {\n this.receivedTxAudioFrames.push(frame);\n }\n return;\n }\n\n for (const command of parseTciText(dataToBuffer(data))) {\n this.receivedCommands.push(command);\n if (this.handler) {\n const handled = await this.handler({ server: this, socket, command });\n if (handled === true) {\n continue;\n }\n }\n await this.delay();\n this.defaultReply(socket, command);\n }\n }\n\n private defaultReply(socket: WebSocket, command: TciCommand): void {\n if (socket.readyState !== WebSocket.OPEN) {\n return;\n }\n switch (command.name) {\n case 'vfo': {\n const receiver = command.args[0] ?? '0';\n const vfo = command.args[1] ?? '0';\n if (command.args[2] !== undefined) {\n this.frequency = Number(command.args[2]);\n }\n socket.send(formatTciCommand('VFO', [receiver, vfo, this.frequency]));\n break;\n }\n case 'modulation': {\n const receiver = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.mode = command.args[command.args.length - 1]?.toUpperCase() ?? this.mode;\n }\n socket.send(formatTciCommand('MODULATION', [receiver, this.mode]));\n break;\n }\n case 'trx': {\n const trx = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.ptt = command.args[1]?.toLowerCase() === 'true';\n }\n socket.send(formatTciCommand('TRX', [trx, this.ptt]));\n break;\n }\n case 'drive': {\n const hasTrx = command.args.length >= 2;\n const trx = hasTrx ? command.args[0] ?? '0' : '0';\n const valueArg = command.args[hasTrx ? 1 : 0];\n if (valueArg !== undefined) this.drive = Number(valueArg);\n socket.send(formatTciCommand('DRIVE', [trx, this.drive]));\n break;\n }\n case 'tune':\n case 'split_enable':\n case 'cw_macros':\n case 'cw_msg':\n case 'cw_macros_stop':\n case 'audio_samplerate':\n case 'tx_stream_audio_buffering':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n default:\n if (this.options.echoUnknown) {\n socket.send(formatTciCommand(command.originalName, command.args));\n }\n break;\n }\n }\n\n private broadcastBinary(frame: Buffer): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(frame, { binary: true });\n }\n }\n }\n\n private async delay(): Promise<void> {\n if (this.options.commandDelayMs <= 0) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, this.options.commandDelayMs));\n }\n}\n\nfunction dataToBuffer(data: WebSocket.RawData): Buffer {\n if (Buffer.isBuffer(data)) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return Buffer.from(data);\n }\n if (Array.isArray(data)) {\n return Buffer.concat(data.map((item) => dataToBuffer(item)));\n }\n throw new Error('Unsupported WebSocket data type');\n}\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'handshake-timeout'\n | 'invalid-handshake'\n | 'unknown-dialect'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport type { TciStreamLengthSemantics } from '../dialect/types.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Raw Stream.length value from the wire header. */\n headerSampleCount: number;\n /** Canonical number of scalar sample values across all channels. */\n sampleCount: number;\n /** Number of sample frames, each containing `channels` scalar values. */\n frameCount: number;\n lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;\n}\n\nexport interface ParseStreamFrameOptions {\n lengthSemantics?: TciStreamLengthSemantics;\n negotiatedChannels?: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n lengthSemantics?: TciStreamLengthSemantics;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options: ParseStreamFrameOptions = {}): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7] || options.negotiatedChannels || 0;\n const bytesPerSample = sampleTypeBytes(sampleType);\n const headerSampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n const actualScalarCount = payloadLength / bytesPerSample;\n const requestedSemantics = options.lengthSemantics ?? 'auto';\n const lengthSemantics = resolveLengthSemantics(\n requestedSemantics,\n streamType,\n headerSampleCount,\n actualScalarCount,\n channels,\n );\n const sampleCount = lengthSemantics === 'per-channel' ? headerSampleCount * channels : headerSampleCount;\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n headerSampleCount,\n sampleCount,\n frameCount: sampleCount / channels,\n lengthSemantics,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {\n throw new TciError('invalid-frame', `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);\n }\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n const lengthSemantics = options.lengthSemantics === 'per-channel' ? 'per-channel' : 'scalar';\n const headerSampleCount = lengthSemantics === 'per-channel' ? sampleCount / channels : sampleCount;\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n headerSampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nfunction resolveLengthSemantics(\n requested: TciStreamLengthSemantics,\n streamType: TciStreamType,\n headerSampleCount: number,\n actualScalarCount: number,\n channels: number,\n): Exclude<TciStreamLengthSemantics, 'auto'> {\n if (requested !== 'auto') return requested;\n if (streamType === TciStreamType.TX_CHRONO && actualScalarCount === 0) return 'scalar';\n if (actualScalarCount === headerSampleCount) return 'scalar';\n if (actualScalarCount === headerSampleCount * channels) return 'per-channel';\n return 'scalar';\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n","export interface TciCommand {\n /** Lower-case command name for case-insensitive matching. */\n name: string;\n /** Original command name as received, without surrounding whitespace. */\n originalName: string;\n /** Unescaped argument list. Empty commands have an empty array. */\n args: string[];\n /** Raw command fragment without the trailing semicolon. */\n raw: string;\n}\n\nexport type TciCommandInput = string | TciCommand;\n\nconst ESCAPE_TO_CHAR: Record<string, string> = {\n '^': ':',\n '~': ',',\n '*': ';',\n};\n\nconst CHAR_TO_ESCAPE: Record<string, string> = {\n ':': '^',\n ',': '~',\n ';': '*',\n};\n\nexport function escapeTciText(value: unknown): string {\n return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);\n}\n\nexport function unescapeTciText(value: string): string {\n return value.replace(/[\\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);\n}\n\nexport function parseTciText(text: string | Buffer | ArrayBuffer | ArrayBufferView): TciCommand[] {\n const source = normalizeTextInput(text);\n const commands: TciCommand[] = [];\n\n for (const fragment of source.split(';')) {\n const raw = fragment.trim();\n if (!raw) {\n continue;\n }\n\n const colonIndex = raw.indexOf(':');\n const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();\n if (!originalName) {\n continue;\n }\n\n const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : undefined;\n commands.push({\n name: originalName.toLowerCase(),\n originalName,\n args: argsText === undefined ? [] : splitArgs(argsText),\n raw,\n });\n }\n\n return commands;\n}\n\nexport function parseTciCommand(input: TciCommandInput): TciCommand {\n if (typeof input !== 'string') {\n return input;\n }\n const [command] = parseTciText(input);\n if (!command) {\n throw new Error(`Invalid TCI command: ${input}`);\n }\n return command;\n}\n\nexport function formatTciCommand(name: string, args: readonly unknown[] = []): string {\n const commandName = name.trim().toUpperCase();\n if (!commandName) {\n throw new Error('TCI command name cannot be empty');\n }\n if (args.length === 0) {\n return `${commandName};`;\n }\n return `${commandName}:${args.map(escapeTciText).join(',')};`;\n}\n\nexport function normalizeCommandName(name: string): string {\n return name.trim().toLowerCase();\n}\n\nexport function isCommandReplyTo(replyInput: TciCommandInput, requestInput: TciCommandInput): boolean {\n const reply = parseTciCommand(replyInput);\n const request = parseTciCommand(requestInput);\n if (reply.name !== request.name) {\n return false;\n }\n\n if (request.args.length === 0) {\n return true;\n }\n\n if (argsHavePrefix(reply.args, request.args)) {\n return true;\n }\n\n return isKnownVariantReply(reply, request);\n}\n\nexport function commandKey(command: TciCommandInput): string {\n const parsed = parseTciCommand(command);\n return `${parsed.name}:${parsed.args.join(',')}`;\n}\n\nfunction normalizeTextInput(text: string | Buffer | ArrayBuffer | ArrayBufferView): string {\n if (typeof text === 'string') {\n return text;\n }\n if (Buffer.isBuffer(text)) {\n return text.toString('utf8');\n }\n if (text instanceof ArrayBuffer) {\n return Buffer.from(text).toString('utf8');\n }\n return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString('utf8');\n}\n\nfunction splitArgs(argsText: string): string[] {\n return argsText.split(',').map((arg) => unescapeTciText(arg.trim()));\n}\n\nfunction argsHavePrefix(args: readonly string[], prefix: readonly string[]): boolean {\n if (args.length < prefix.length) {\n return false;\n }\n return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());\n}\n\nfunction isKnownVariantReply(reply: TciCommand, request: TciCommand): boolean {\n if (reply.name === 'modulation') {\n // ExpertSDR/WSJT-X variants can use MODULATION:rx,mode and MODULATION:rx,vfo,mode.\n if (request.args.length === 2 && reply.args.length >= 3) {\n return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n if (request.args.length === 3 && reply.args.length === 2) {\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();\n }\n }\n\n if (reply.name === 'protocol') {\n return request.args.length <= 1;\n }\n\n if (reply.name === 'trx' && request.args.length >= 3 && reply.args.length >= 2) {\n // Official TRX writes may include an audio source as arg3, while replies only echo trx+state.\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n\n return false;\n}\n","import { EventEmitter } from 'eventemitter3';\nimport type WebSocket from 'ws';\n\nexport interface FakeWebSocketSentMessage {\n data: WebSocket.RawData | string;\n isBinary: boolean;\n}\n\nexport type FakeWebSocketObserver = (socket: FakeWebSocket) => void;\n\n/**\n * Minimal in-memory WebSocket implementation for deterministic TciClient tests.\n * It auto-opens on the next microtask and exposes helpers to inject server data.\n */\nexport class FakeWebSocket extends EventEmitter {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n\n readonly sentMessages: FakeWebSocketSentMessage[] = [];\n readyState = FakeWebSocket.CONNECTING;\n\n constructor(readonly url: string) {\n super();\n queueMicrotask(() => this.open());\n }\n\n send(\n data: WebSocket.RawData | string,\n optionsOrCallback?: { binary?: boolean } | ((error?: Error) => void),\n callback?: (error?: Error) => void,\n ): void {\n const done = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;\n const isBinary = typeof optionsOrCallback === 'object'\n ? Boolean(optionsOrCallback.binary)\n : typeof data !== 'string';\n\n if (this.readyState !== FakeWebSocket.OPEN) {\n done?.(new Error('FakeWebSocket is not open'));\n return;\n }\n\n this.sentMessages.push({ data, isBinary });\n this.emit('sent', data, isBinary);\n done?.();\n }\n\n receive(data: WebSocket.RawData | string, isBinary = typeof data !== 'string'): void {\n if (this.readyState === FakeWebSocket.OPEN) {\n this.emit('message', typeof data === 'string' ? Buffer.from(data) : data, isBinary);\n }\n }\n\n open(): void {\n if (this.readyState !== FakeWebSocket.CONNECTING) {\n return;\n }\n this.readyState = FakeWebSocket.OPEN;\n this.emit('open');\n }\n\n close(code = 1000, reason = 'fake close'): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSING;\n queueMicrotask(() => {\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', code, Buffer.from(reason));\n });\n }\n\n terminate(): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', 1006, Buffer.from('terminated'));\n }\n}\n\nexport function createFakeWebSocketImpl(observer?: FakeWebSocketObserver): typeof WebSocket {\n return class FakeWebSocketImpl extends FakeWebSocket {\n constructor(url: string) {\n super(url);\n observer?.(this);\n }\n } as unknown as typeof WebSocket;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/MockTciServer.ts","../../src/errors.ts","../../src/audio/streamFrame.ts","../../src/protocol/text.ts","../../src/testing/FakeWebSocket.ts"],"sourcesContent":["import { AddressInfo } from 'node:net';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n buildStreamFrame,\n parseStreamFrame,\n TciSampleType,\n TciStreamType,\n type BuildStreamFrameOptions,\n type TciStreamFrame,\n} from '../audio/index.js';\nimport { formatTciCommand, parseTciText, type TciCommand } from '../protocol/index.js';\n\nexport interface MockTciServerOptions {\n port?: number;\n host?: string;\n startupCommands?: string[];\n echoUnknown?: boolean;\n commandDelayMs?: number;\n}\n\nexport interface MockTciServerCommandContext {\n server: MockTciServer;\n socket: WebSocket;\n command: TciCommand;\n}\n\nexport type MockTciServerCommandHandler = (context: MockTciServerCommandContext) => void | boolean | Promise<void | boolean>;\n\nexport class MockTciServer {\n readonly receivedCommands: TciCommand[] = [];\n readonly receivedTxAudioFrames: TciStreamFrame[] = [];\n\n private readonly options: Required<Pick<MockTciServerOptions, 'host' | 'echoUnknown' | 'commandDelayMs'>> &\n Pick<MockTciServerOptions, 'port' | 'startupCommands'>;\n private wss?: WebSocketServer;\n private sockets = new Set<WebSocket>();\n private handler?: MockTciServerCommandHandler;\n private frequency = 14_074_000;\n private mode = 'DIGU';\n private ptt = false;\n private drive = 30;\n private iqSampleRate = 48_000;\n\n constructor(options: MockTciServerOptions = {}) {\n this.options = {\n port: options.port ?? 0,\n host: options.host ?? '127.0.0.1',\n startupCommands: options.startupCommands,\n echoUnknown: options.echoUnknown ?? true,\n commandDelayMs: options.commandDelayMs ?? 0,\n };\n }\n\n async start(): Promise<void> {\n if (this.wss) {\n return;\n }\n this.wss = new WebSocketServer({ port: this.options.port, host: this.options.host });\n this.wss.on('connection', (socket) => this.handleConnection(socket));\n await new Promise<void>((resolve, reject) => {\n this.wss?.once('listening', () => resolve());\n this.wss?.once('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const sockets = [...this.sockets];\n await Promise.all(\n sockets.map(\n (socket) =>\n new Promise<void>((resolve) => {\n socket.once('close', () => resolve());\n socket.close();\n setTimeout(() => resolve(), 200).unref?.();\n }),\n ),\n );\n if (!this.wss) {\n return;\n }\n await new Promise<void>((resolve, reject) => {\n this.wss?.close((error) => (error ? reject(error) : resolve()));\n });\n this.wss = undefined;\n }\n\n url(): string {\n if (!this.wss) {\n throw new Error('MockTciServer is not started');\n }\n const address = this.wss.address() as AddressInfo;\n return `ws://${address.address}:${address.port}`;\n }\n\n onCommand(handler: MockTciServerCommandHandler): void {\n this.handler = handler;\n }\n\n broadcast(command: string): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command.endsWith(';') ? command : `${command};`);\n }\n }\n }\n\n broadcastCommand(name: string, args: readonly unknown[] = []): void {\n this.broadcast(formatTciCommand(name, args));\n }\n\n sendRxAudioFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.RX_AUDIO_STREAM,\n channels: options.channels ?? 1,\n samples: options.samples ?? new Float32Array(512),\n payload: options.payload,\n });\n this.broadcastBinary(frame);\n }\n\n sendIqFrame(options: Partial<BuildStreamFrameOptions> & { samples?: Float32Array | readonly number[] } = {}): void {\n const samples = options.samples ?? new Float32Array(4096);\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? this.iqSampleRate,\n sampleType: options.sampleType ?? TciSampleType.FLOAT32,\n streamType: TciStreamType.IQ_STREAM,\n channels: options.channels ?? 2,\n samples,\n payload: options.payload,\n lengthSemantics: options.lengthSemantics,\n });\n this.broadcastBinary(frame);\n }\n\n sendTxChrono(options: Partial<BuildStreamFrameOptions> & { sampleCount?: number } = {}): void {\n const sampleType = options.sampleType ?? TciSampleType.FLOAT32;\n const channels = options.channels ?? 1;\n const sampleCount = options.sampleCount ?? 512;\n const frame = buildStreamFrame({\n receiver: options.receiver ?? 0,\n sampleRate: options.sampleRate ?? 12_000,\n sampleType,\n streamType: TciStreamType.TX_CHRONO,\n channels,\n payload: options.payload,\n sampleCount,\n });\n this.broadcastBinary(frame);\n }\n\n closeClients(): void {\n for (const socket of this.sockets) {\n socket.close();\n }\n }\n\n private handleConnection(socket: WebSocket): void {\n this.sockets.add(socket);\n socket.on('close', () => this.sockets.delete(socket));\n socket.on('message', (data, isBinary) => void this.handleMessage(socket, data, isBinary));\n\n const startupCommands = this.options.startupCommands ?? [\n 'PROTOCOL:ExpertSDR3,2.0;',\n 'DEVICE:Mock ExpertSDR3;',\n 'MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;',\n `VFO:0,0,${this.frequency};`,\n `DDS:0,${this.frequency};`,\n `IQ_SAMPLERATE:${this.iqSampleRate};`,\n `MODULATION:0,${this.mode};`,\n `TRX:0,${this.ptt};`,\n `DRIVE:0,${this.drive};`,\n 'READY;',\n ];\n queueMicrotask(() => {\n for (const command of startupCommands) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(command);\n }\n }\n });\n }\n\n private async handleMessage(socket: WebSocket, data: WebSocket.RawData, isBinary: boolean): Promise<void> {\n if (isBinary) {\n const frame = parseStreamFrame(dataToBuffer(data));\n if (frame.streamType === TciStreamType.TX_AUDIO_STREAM) {\n this.receivedTxAudioFrames.push(frame);\n }\n return;\n }\n\n for (const command of parseTciText(dataToBuffer(data))) {\n this.receivedCommands.push(command);\n if (this.handler) {\n const handled = await this.handler({ server: this, socket, command });\n if (handled === true) {\n continue;\n }\n }\n await this.delay();\n this.defaultReply(socket, command);\n }\n }\n\n private defaultReply(socket: WebSocket, command: TciCommand): void {\n if (socket.readyState !== WebSocket.OPEN) {\n return;\n }\n switch (command.name) {\n case 'vfo': {\n const receiver = command.args[0] ?? '0';\n const vfo = command.args[1] ?? '0';\n if (command.args[2] !== undefined) {\n this.frequency = Number(command.args[2]);\n }\n socket.send(formatTciCommand('VFO', [receiver, vfo, this.frequency]));\n break;\n }\n case 'modulation': {\n const receiver = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.mode = command.args[command.args.length - 1]?.toUpperCase() ?? this.mode;\n }\n socket.send(formatTciCommand('MODULATION', [receiver, this.mode]));\n break;\n }\n case 'trx': {\n const trx = command.args[0] ?? '0';\n if (command.args[1] !== undefined) {\n this.ptt = command.args[1]?.toLowerCase() === 'true';\n }\n socket.send(formatTciCommand('TRX', [trx, this.ptt]));\n break;\n }\n case 'drive': {\n const hasTrx = command.args.length >= 2;\n const trx = hasTrx ? command.args[0] ?? '0' : '0';\n const valueArg = command.args[hasTrx ? 1 : 0];\n if (valueArg !== undefined) this.drive = Number(valueArg);\n socket.send(formatTciCommand('DRIVE', [trx, this.drive]));\n break;\n }\n case 'tune':\n case 'split_enable':\n case 'cw_macros':\n case 'cw_msg':\n case 'cw_macros_stop':\n case 'audio_samplerate':\n case 'tx_stream_audio_buffering':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n case 'iq_samplerate': {\n if (command.args[0] !== undefined) this.iqSampleRate = Number(command.args[0]);\n socket.send(formatTciCommand('IQ_SAMPLERATE', [this.iqSampleRate]));\n break;\n }\n case 'iq_start':\n case 'iq_stop':\n socket.send(formatTciCommand(command.originalName, command.args));\n break;\n default:\n if (this.options.echoUnknown) {\n socket.send(formatTciCommand(command.originalName, command.args));\n }\n break;\n }\n }\n\n private broadcastBinary(frame: Buffer): void {\n for (const socket of this.sockets) {\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(frame, { binary: true });\n }\n }\n }\n\n private async delay(): Promise<void> {\n if (this.options.commandDelayMs <= 0) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, this.options.commandDelayMs));\n }\n}\n\nfunction dataToBuffer(data: WebSocket.RawData): Buffer {\n if (Buffer.isBuffer(data)) {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return Buffer.from(data);\n }\n if (Array.isArray(data)) {\n return Buffer.concat(data.map((item) => dataToBuffer(item)));\n }\n throw new Error('Unsupported WebSocket data type');\n}\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'handshake-timeout'\n | 'invalid-handshake'\n | 'unknown-dialect'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport type { TciStreamLengthSemantics } from '../dialect/types.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Raw Stream.length value from the wire header. */\n headerSampleCount: number;\n /** Canonical number of scalar sample values across all channels. */\n sampleCount: number;\n /** Number of sample frames, each containing `channels` scalar values. */\n frameCount: number;\n lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;\n}\n\nexport interface ParseStreamFrameOptions {\n lengthSemantics?: TciStreamLengthSemantics;\n negotiatedChannels?: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n lengthSemantics?: TciStreamLengthSemantics;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options: ParseStreamFrameOptions = {}): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7] || options.negotiatedChannels || 0;\n const bytesPerSample = sampleTypeBytes(sampleType);\n const headerSampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n const actualScalarCount = payloadLength / bytesPerSample;\n const requestedSemantics = options.lengthSemantics ?? 'auto';\n const lengthSemantics = resolveLengthSemantics(\n requestedSemantics,\n streamType,\n headerSampleCount,\n actualScalarCount,\n channels,\n );\n const sampleCount = lengthSemantics === 'per-channel' ? headerSampleCount * channels : headerSampleCount;\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n headerSampleCount,\n sampleCount,\n frameCount: sampleCount / channels,\n lengthSemantics,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {\n throw new TciError('invalid-frame', `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);\n }\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n const lengthSemantics = options.lengthSemantics === 'per-channel' ? 'per-channel' : 'scalar';\n const headerSampleCount = lengthSemantics === 'per-channel' ? sampleCount / channels : sampleCount;\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n headerSampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nfunction resolveLengthSemantics(\n requested: TciStreamLengthSemantics,\n streamType: TciStreamType,\n headerSampleCount: number,\n actualScalarCount: number,\n channels: number,\n): Exclude<TciStreamLengthSemantics, 'auto'> {\n if (requested !== 'auto') return requested;\n if (streamType === TciStreamType.TX_CHRONO && actualScalarCount === 0) return 'scalar';\n if (actualScalarCount === headerSampleCount) return 'scalar';\n if (actualScalarCount === headerSampleCount * channels) return 'per-channel';\n return 'scalar';\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function decodeInterleavedIq(frame: TciStreamFrame): Float32Array {\n if (frame.streamType !== TciStreamType.IQ_STREAM) {\n throw new TciError('invalid-frame', 'Expected a TCI IQ stream frame');\n }\n if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {\n throw new TciError('invalid-frame', `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);\n }\n return payloadToFloat32(frame);\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n","export interface TciCommand {\n /** Lower-case command name for case-insensitive matching. */\n name: string;\n /** Original command name as received, without surrounding whitespace. */\n originalName: string;\n /** Unescaped argument list. Empty commands have an empty array. */\n args: string[];\n /** Raw command fragment without the trailing semicolon. */\n raw: string;\n}\n\nexport type TciCommandInput = string | TciCommand;\n\nconst ESCAPE_TO_CHAR: Record<string, string> = {\n '^': ':',\n '~': ',',\n '*': ';',\n};\n\nconst CHAR_TO_ESCAPE: Record<string, string> = {\n ':': '^',\n ',': '~',\n ';': '*',\n};\n\nexport function escapeTciText(value: unknown): string {\n return String(value).replace(/[:;,]/g, (char) => CHAR_TO_ESCAPE[char] ?? char);\n}\n\nexport function unescapeTciText(value: string): string {\n return value.replace(/[\\^~*]/g, (char) => ESCAPE_TO_CHAR[char] ?? char);\n}\n\nexport function parseTciText(text: string | Buffer | ArrayBuffer | ArrayBufferView): TciCommand[] {\n const source = normalizeTextInput(text);\n const commands: TciCommand[] = [];\n\n for (const fragment of source.split(';')) {\n const raw = fragment.trim();\n if (!raw) {\n continue;\n }\n\n const colonIndex = raw.indexOf(':');\n const originalName = (colonIndex >= 0 ? raw.slice(0, colonIndex) : raw).trim();\n if (!originalName) {\n continue;\n }\n\n const argsText = colonIndex >= 0 ? raw.slice(colonIndex + 1) : undefined;\n commands.push({\n name: originalName.toLowerCase(),\n originalName,\n args: argsText === undefined ? [] : splitArgs(argsText),\n raw,\n });\n }\n\n return commands;\n}\n\nexport function parseTciCommand(input: TciCommandInput): TciCommand {\n if (typeof input !== 'string') {\n return input;\n }\n const [command] = parseTciText(input);\n if (!command) {\n throw new Error(`Invalid TCI command: ${input}`);\n }\n return command;\n}\n\nexport function formatTciCommand(name: string, args: readonly unknown[] = []): string {\n const commandName = name.trim().toUpperCase();\n if (!commandName) {\n throw new Error('TCI command name cannot be empty');\n }\n if (args.length === 0) {\n return `${commandName};`;\n }\n return `${commandName}:${args.map(escapeTciText).join(',')};`;\n}\n\nexport function normalizeCommandName(name: string): string {\n return name.trim().toLowerCase();\n}\n\nexport function isCommandReplyTo(replyInput: TciCommandInput, requestInput: TciCommandInput): boolean {\n const reply = parseTciCommand(replyInput);\n const request = parseTciCommand(requestInput);\n if (reply.name !== request.name) {\n return false;\n }\n\n if (request.args.length === 0) {\n return true;\n }\n\n if (argsHavePrefix(reply.args, request.args)) {\n return true;\n }\n\n return isKnownVariantReply(reply, request);\n}\n\nexport function commandKey(command: TciCommandInput): string {\n const parsed = parseTciCommand(command);\n return `${parsed.name}:${parsed.args.join(',')}`;\n}\n\nfunction normalizeTextInput(text: string | Buffer | ArrayBuffer | ArrayBufferView): string {\n if (typeof text === 'string') {\n return text;\n }\n if (Buffer.isBuffer(text)) {\n return text.toString('utf8');\n }\n if (text instanceof ArrayBuffer) {\n return Buffer.from(text).toString('utf8');\n }\n return Buffer.from(text.buffer, text.byteOffset, text.byteLength).toString('utf8');\n}\n\nfunction splitArgs(argsText: string): string[] {\n return argsText.split(',').map((arg) => unescapeTciText(arg.trim()));\n}\n\nfunction argsHavePrefix(args: readonly string[], prefix: readonly string[]): boolean {\n if (args.length < prefix.length) {\n return false;\n }\n return prefix.every((arg, index) => args[index]?.toLowerCase() === arg.toLowerCase());\n}\n\nfunction isKnownVariantReply(reply: TciCommand, request: TciCommand): boolean {\n if (reply.name === 'modulation') {\n // ExpertSDR/WSJT-X variants can use MODULATION:rx,mode and MODULATION:rx,vfo,mode.\n if (request.args.length === 2 && reply.args.length >= 3) {\n return reply.args[0] === request.args[0] && reply.args[2]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n if (request.args.length === 3 && reply.args.length === 2) {\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[2]?.toLowerCase();\n }\n }\n\n if (reply.name === 'protocol') {\n return request.args.length <= 1;\n }\n\n if (reply.name === 'trx' && request.args.length >= 3 && reply.args.length >= 2) {\n // Official TRX writes may include an audio source as arg3, while replies only echo trx+state.\n return reply.args[0] === request.args[0] && reply.args[1]?.toLowerCase() === request.args[1]?.toLowerCase();\n }\n\n return false;\n}\n","import { EventEmitter } from 'eventemitter3';\nimport type WebSocket from 'ws';\n\nexport interface FakeWebSocketSentMessage {\n data: WebSocket.RawData | string;\n isBinary: boolean;\n}\n\nexport type FakeWebSocketObserver = (socket: FakeWebSocket) => void;\n\n/**\n * Minimal in-memory WebSocket implementation for deterministic TciClient tests.\n * It auto-opens on the next microtask and exposes helpers to inject server data.\n */\nexport class FakeWebSocket extends EventEmitter {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n\n readonly sentMessages: FakeWebSocketSentMessage[] = [];\n readyState = FakeWebSocket.CONNECTING;\n\n constructor(readonly url: string) {\n super();\n queueMicrotask(() => this.open());\n }\n\n send(\n data: WebSocket.RawData | string,\n optionsOrCallback?: { binary?: boolean } | ((error?: Error) => void),\n callback?: (error?: Error) => void,\n ): void {\n const done = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;\n const isBinary = typeof optionsOrCallback === 'object'\n ? Boolean(optionsOrCallback.binary)\n : typeof data !== 'string';\n\n if (this.readyState !== FakeWebSocket.OPEN) {\n done?.(new Error('FakeWebSocket is not open'));\n return;\n }\n\n this.sentMessages.push({ data, isBinary });\n this.emit('sent', data, isBinary);\n done?.();\n }\n\n receive(data: WebSocket.RawData | string, isBinary = typeof data !== 'string'): void {\n if (this.readyState === FakeWebSocket.OPEN) {\n this.emit('message', typeof data === 'string' ? Buffer.from(data) : data, isBinary);\n }\n }\n\n open(): void {\n if (this.readyState !== FakeWebSocket.CONNECTING) {\n return;\n }\n this.readyState = FakeWebSocket.OPEN;\n this.emit('open');\n }\n\n close(code = 1000, reason = 'fake close'): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSING;\n queueMicrotask(() => {\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', code, Buffer.from(reason));\n });\n }\n\n terminate(): void {\n if (this.readyState === FakeWebSocket.CLOSED) {\n return;\n }\n this.readyState = FakeWebSocket.CLOSED;\n this.emit('close', 1006, Buffer.from('terminated'));\n }\n}\n\nexport function createFakeWebSocketImpl(observer?: FakeWebSocketObserver): typeof WebSocket {\n return class FakeWebSocketImpl extends FakeWebSocket {\n constructor(url: string) {\n super(url);\n observer?.(this);\n }\n } as unknown as typeof WebSocket;\n}\n"],"mappings":"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","names":[]}
|