tci-client-node 0.1.1 → 0.2.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 +9 -0
- package/README.md +33 -13
- package/dist/audio/index.cjs +63 -20
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +18 -3
- package/dist/audio/index.d.ts +18 -3
- package/dist/audio/index.js +63 -20
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +331 -0
- package/dist/dialect/index.cjs.map +1 -0
- package/dist/dialect/index.d.cts +29 -0
- package/dist/dialect/index.d.ts +29 -0
- package/dist/dialect/index.js +292 -0
- package/dist/dialect/index.js.map +1 -0
- package/dist/{index-CK3XdXP3.d.cts → index-lbK6NGY4.d.cts} +1 -1
- package/dist/{index-Dfmrk2MR.d.ts → index-paj1AOJY.d.ts} +1 -1
- package/dist/index.cjs +890 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +876 -224
- package/dist/index.js.map +1 -1
- package/dist/protocol/index.cjs.map +1 -1
- package/dist/protocol/index.d.cts +1 -1
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/testing/index.cjs +77 -25
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +2 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +77 -25
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/index.cjs +175 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +37 -0
- package/dist/transport/index.d.ts +37 -0
- package/dist/transport/index.js +138 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/types-9seY9Th-.d.ts +63 -0
- package/dist/types-xRotf9gW.d.cts +63 -0
- package/package.json +15 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { T as TciCommand, a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from '../text-BwCWY1k1.cjs';
|
|
2
|
-
export { Q as QueueCommandOptions, a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions } from '../index-
|
|
2
|
+
export { Q as QueueCommandOptions, a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions } from '../index-lbK6NGY4.cjs';
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { T as TciCommand, a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from '../text-BwCWY1k1.js';
|
|
2
|
-
export { Q as QueueCommandOptions, a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions } from '../index-
|
|
2
|
+
export { Q as QueueCommandOptions, a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions } from '../index-paj1AOJY.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/protocol/text.ts","../../src/errors.ts","../../src/protocol/commandQueue.ts"],"sourcesContent":["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","export type TciErrorCode =\n | 'connect-timeout'\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 {\n formatTciCommand,\n isCommandReplyTo,\n parseTciCommand,\n type TciCommand,\n type TciCommandInput,\n} from './text.js';\n\nexport type TciCommandMatcher = (reply: TciCommand, request: TciCommand) => boolean;\n\nexport interface QueueCommandOptions {\n timeoutMs?: number;\n matcher?: TciCommandMatcher;\n signal?: AbortSignal;\n}\n\nexport interface QueuedCommandResult {\n request: TciCommand;\n reply: TciCommand;\n}\n\ninterface PendingCommand {\n raw: string;\n request: TciCommand;\n timeoutMs: number;\n matcher: TciCommandMatcher;\n resolve: (result: QueuedCommandResult) => void;\n reject: (error: TciError) => void;\n timer?: NodeJS.Timeout;\n abortCleanup?: () => void;\n}\n\nexport interface TciCommandQueueOptions {\n send: (raw: string) => void | Promise<void>;\n timeoutMs?: number;\n}\n\nexport class TciCommandQueue {\n private readonly send: (raw: string) => void | Promise<void>;\n private readonly defaultTimeoutMs: number;\n private queue: PendingCommand[] = [];\n private active?: PendingCommand;\n private connected = true;\n\n constructor(options: TciCommandQueueOptions) {\n this.send = options.send;\n this.defaultTimeoutMs = options.timeoutMs ?? 1_000;\n }\n\n setConnected(connected: boolean): void {\n this.connected = connected;\n if (!connected) {\n this.cancelAll(new TciError('disconnected', 'TCI connection closed'));\n }\n }\n\n enqueue(command: TciCommandInput, options: QueueCommandOptions = {}): Promise<QueuedCommandResult> {\n const request = parseTciCommand(command);\n const raw = typeof command === 'string' ? ensureSemicolon(command) : formatTciCommand(command.originalName, command.args);\n\n if (!this.connected) {\n return Promise.reject(new TciError('not-connected', 'TCI socket is not connected'));\n }\n\n return new Promise<QueuedCommandResult>((resolve, reject) => {\n const pending: PendingCommand = {\n raw,\n request,\n timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,\n matcher: options.matcher ?? ((reply, req) => isCommandReplyTo(reply, req)),\n resolve,\n reject,\n };\n\n if (options.signal) {\n if (options.signal.aborted) {\n reject(new TciError('cancelled', 'TCI command was cancelled'));\n return;\n }\n const onAbort = () => this.rejectPending(pending, new TciError('cancelled', 'TCI command was cancelled'));\n options.signal.addEventListener('abort', onAbort, { once: true });\n pending.abortCleanup = () => options.signal?.removeEventListener('abort', onAbort);\n }\n\n this.queue.push(pending);\n void this.pump();\n });\n }\n\n handleCommand(commandInput: TciCommandInput): boolean {\n const active = this.active;\n if (!active) {\n return false;\n }\n const reply = parseTciCommand(commandInput);\n if (!active.matcher(reply, active.request)) {\n return false;\n }\n this.finishActive(reply);\n return true;\n }\n\n cancelAll(error = new TciError('cancelled', 'TCI command queue cancelled')): void {\n const pending = [...this.queue];\n this.queue = [];\n if (this.active) {\n pending.unshift(this.active);\n this.active = undefined;\n }\n for (const item of pending) {\n this.rejectPending(item, error);\n }\n }\n\n get size(): number {\n return this.queue.length + (this.active ? 1 : 0);\n }\n\n private async pump(): Promise<void> {\n if (this.active || !this.connected) {\n return;\n }\n const next = this.queue.shift();\n if (!next) {\n return;\n }\n\n this.active = next;\n next.timer = setTimeout(() => {\n this.rejectPending(next, new TciError('command-timeout', `Timed out waiting for TCI reply to ${next.raw}`));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }, next.timeoutMs);\n\n try {\n await this.send(next.raw);\n } catch (error) {\n this.rejectPending(next, new TciError('disconnected', error instanceof Error ? error.message : String(error), error));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }\n }\n\n private finishActive(reply: TciCommand): void {\n const active = this.active;\n if (!active) {\n return;\n }\n this.active = undefined;\n if (active.timer) {\n clearTimeout(active.timer);\n }\n active.abortCleanup?.();\n active.resolve({ request: active.request, reply });\n void this.pump();\n }\n\n private rejectPending(pending: PendingCommand, error: TciError): void {\n if (pending.timer) {\n clearTimeout(pending.timer);\n }\n pending.abortCleanup?.();\n const wasActive = this.active === pending;\n if (wasActive) {\n this.active = undefined;\n } else {\n this.queue = this.queue.filter((item) => item !== pending);\n }\n pending.reject(error);\n if (wasActive && this.connected) {\n void this.pump();\n }\n }\n}\n\nfunction ensureSemicolon(command: string): string {\n return command.trim().endsWith(';') ? command.trim() : `${command.trim()};`;\n}\n"],"mappings":";AAaA,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;AAEO,SAAS,gBAAgB,OAAoC;AAClE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AACA,QAAM,CAAC,OAAO,IAAI,aAAa,KAAK;AACpC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,wBAAwB,KAAK,EAAE;AAAA,EACjD;AACA,SAAO;AACT;AAEO,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;AAEO,SAAS,qBAAqB,MAAsB;AACzD,SAAO,KAAK,KAAK,EAAE,YAAY;AACjC;AAEO,SAAS,iBAAiB,YAA6B,cAAwC;AACpG,QAAM,QAAQ,gBAAgB,UAAU;AACxC,QAAM,UAAU,gBAAgB,YAAY;AAC5C,MAAI,MAAM,SAAS,QAAQ,MAAM;AAC/B,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,KAAK,WAAW,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,MAAI,eAAe,MAAM,MAAM,QAAQ,IAAI,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SAAO,oBAAoB,OAAO,OAAO;AAC3C;AAEO,SAAS,WAAW,SAAkC;AAC3D,QAAM,SAAS,gBAAgB,OAAO;AACtC,SAAO,GAAG,OAAO,IAAI,IAAI,OAAO,KAAK,KAAK,GAAG,CAAC;AAChD;AAEA,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;AAEA,SAAS,eAAe,MAAyB,QAAoC;AACnF,MAAI,KAAK,SAAS,OAAO,QAAQ;AAC/B,WAAO;AAAA,EACT;AACA,SAAO,OAAO,MAAM,CAAC,KAAK,UAAU,KAAK,KAAK,GAAG,YAAY,MAAM,IAAI,YAAY,CAAC;AACtF;AAEA,SAAS,oBAAoB,OAAmB,SAA8B;AAC5E,MAAI,MAAM,SAAS,cAAc;AAE/B,QAAI,QAAQ,KAAK,WAAW,KAAK,MAAM,KAAK,UAAU,GAAG;AACvD,aAAO,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,YAAY,MAAM,QAAQ,KAAK,CAAC,GAAG,YAAY;AAAA,IAC5G;AACA,QAAI,QAAQ,KAAK,WAAW,KAAK,MAAM,KAAK,WAAW,GAAG;AACxD,aAAO,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,YAAY,MAAM,QAAQ,KAAK,CAAC,GAAG,YAAY;AAAA,IAC5G;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,YAAY;AAC7B,WAAO,QAAQ,KAAK,UAAU;AAAA,EAChC;AAEA,MAAI,MAAM,SAAS,SAAS,QAAQ,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,GAAG;AAE9E,WAAO,MAAM,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,YAAY,MAAM,QAAQ,KAAK,CAAC,GAAG,YAAY;AAAA,EAC5G;AAEA,SAAO;AACT;;;AClJO,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;;;ACmBO,IAAM,kBAAN,MAAsB;AAAA,EACV;AAAA,EACA;AAAA,EACT,QAA0B,CAAC;AAAA,EAC3B;AAAA,EACA,YAAY;AAAA,EAEpB,YAAY,SAAiC;AAC3C,SAAK,OAAO,QAAQ;AACpB,SAAK,mBAAmB,QAAQ,aAAa;AAAA,EAC/C;AAAA,EAEA,aAAa,WAA0B;AACrC,SAAK,YAAY;AACjB,QAAI,CAAC,WAAW;AACd,WAAK,UAAU,IAAI,SAAS,gBAAgB,uBAAuB,CAAC;AAAA,IACtE;AAAA,EACF;AAAA,EAEA,QAAQ,SAA0B,UAA+B,CAAC,GAAiC;AACjG,UAAM,UAAU,gBAAgB,OAAO;AACvC,UAAM,MAAM,OAAO,YAAY,WAAW,gBAAgB,OAAO,IAAI,iBAAiB,QAAQ,cAAc,QAAQ,IAAI;AAExH,QAAI,CAAC,KAAK,WAAW;AACnB,aAAO,QAAQ,OAAO,IAAI,SAAS,iBAAiB,6BAA6B,CAAC;AAAA,IACpF;AAEA,WAAO,IAAI,QAA6B,CAAC,SAAS,WAAW;AAC3D,YAAM,UAA0B;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,WAAW,QAAQ,aAAa,KAAK;AAAA,QACrC,SAAS,QAAQ,YAAY,CAAC,OAAO,QAAQ,iBAAiB,OAAO,GAAG;AAAA,QACxE;AAAA,QACA;AAAA,MACF;AAEA,UAAI,QAAQ,QAAQ;AAClB,YAAI,QAAQ,OAAO,SAAS;AAC1B,iBAAO,IAAI,SAAS,aAAa,2BAA2B,CAAC;AAC7D;AAAA,QACF;AACA,cAAM,UAAU,MAAM,KAAK,cAAc,SAAS,IAAI,SAAS,aAAa,2BAA2B,CAAC;AACxG,gBAAQ,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAChE,gBAAQ,eAAe,MAAM,QAAQ,QAAQ,oBAAoB,SAAS,OAAO;AAAA,MACnF;AAEA,WAAK,MAAM,KAAK,OAAO;AACvB,WAAK,KAAK,KAAK;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EAEA,cAAc,cAAwC;AACpD,UAAM,SAAS,KAAK;AACpB,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,gBAAgB,YAAY;AAC1C,QAAI,CAAC,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG;AAC1C,aAAO;AAAA,IACT;AACA,SAAK,aAAa,KAAK;AACvB,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,QAAQ,IAAI,SAAS,aAAa,6BAA6B,GAAS;AAChF,UAAM,UAAU,CAAC,GAAG,KAAK,KAAK;AAC9B,SAAK,QAAQ,CAAC;AACd,QAAI,KAAK,QAAQ;AACf,cAAQ,QAAQ,KAAK,MAAM;AAC3B,WAAK,SAAS;AAAA,IAChB;AACA,eAAW,QAAQ,SAAS;AAC1B,WAAK,cAAc,MAAM,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM,UAAU,KAAK,SAAS,IAAI;AAAA,EAChD;AAAA,EAEA,MAAc,OAAsB;AAClC,QAAI,KAAK,UAAU,CAAC,KAAK,WAAW;AAClC;AAAA,IACF;AACA,UAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,SAAK,SAAS;AACd,SAAK,QAAQ,WAAW,MAAM;AAC5B,WAAK,cAAc,MAAM,IAAI,SAAS,mBAAmB,sCAAsC,KAAK,GAAG,EAAE,CAAC;AAC1G,UAAI,KAAK,WAAW,MAAM;AACxB,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,KAAK,KAAK;AAAA,IACjB,GAAG,KAAK,SAAS;AAEjB,QAAI;AACF,YAAM,KAAK,KAAK,KAAK,GAAG;AAAA,IAC1B,SAAS,OAAO;AACd,WAAK,cAAc,MAAM,IAAI,SAAS,gBAAgB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG,KAAK,CAAC;AACpH,UAAI,KAAK,WAAW,MAAM;AACxB,aAAK,SAAS;AAAA,MAChB;AACA,WAAK,KAAK,KAAK;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,aAAa,OAAyB;AAC5C,UAAM,SAAS,KAAK;AACpB,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,SAAK,SAAS;AACd,QAAI,OAAO,OAAO;AAChB,mBAAa,OAAO,KAAK;AAAA,IAC3B;AACA,WAAO,eAAe;AACtB,WAAO,QAAQ,EAAE,SAAS,OAAO,SAAS,MAAM,CAAC;AACjD,SAAK,KAAK,KAAK;AAAA,EACjB;AAAA,EAEQ,cAAc,SAAyB,OAAuB;AACpE,QAAI,QAAQ,OAAO;AACjB,mBAAa,QAAQ,KAAK;AAAA,IAC5B;AACA,YAAQ,eAAe;AACvB,UAAM,YAAY,KAAK,WAAW;AAClC,QAAI,WAAW;AACb,WAAK,SAAS;AAAA,IAChB,OAAO;AACL,WAAK,QAAQ,KAAK,MAAM,OAAO,CAAC,SAAS,SAAS,OAAO;AAAA,IAC3D;AACA,YAAQ,OAAO,KAAK;AACpB,QAAI,aAAa,KAAK,WAAW;AAC/B,WAAK,KAAK,KAAK;AAAA,IACjB;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,SAAyB;AAChD,SAAO,QAAQ,KAAK,EAAE,SAAS,GAAG,IAAI,QAAQ,KAAK,IAAI,GAAG,QAAQ,KAAK,CAAC;AAC1E;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/text.ts","../../src/errors.ts","../../src/protocol/commandQueue.ts"],"sourcesContent":["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","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 {\n formatTciCommand,\n isCommandReplyTo,\n parseTciCommand,\n type TciCommand,\n type TciCommandInput,\n} from './text.js';\n\nexport type TciCommandMatcher = (reply: TciCommand, request: TciCommand) => boolean;\n\nexport interface QueueCommandOptions {\n timeoutMs?: number;\n matcher?: TciCommandMatcher;\n signal?: AbortSignal;\n}\n\nexport interface QueuedCommandResult {\n request: TciCommand;\n reply: TciCommand;\n}\n\ninterface PendingCommand {\n raw: string;\n request: TciCommand;\n timeoutMs: number;\n matcher: TciCommandMatcher;\n resolve: (result: QueuedCommandResult) => void;\n reject: (error: TciError) => void;\n timer?: NodeJS.Timeout;\n abortCleanup?: () => void;\n}\n\nexport interface TciCommandQueueOptions {\n send: (raw: string) => void | Promise<void>;\n timeoutMs?: number;\n}\n\nexport class TciCommandQueue {\n private readonly send: (raw: string) => void | Promise<void>;\n private readonly defaultTimeoutMs: number;\n private queue: PendingCommand[] = [];\n private active?: PendingCommand;\n private connected = true;\n\n constructor(options: TciCommandQueueOptions) {\n this.send = options.send;\n this.defaultTimeoutMs = options.timeoutMs ?? 1_000;\n }\n\n setConnected(connected: boolean): void {\n this.connected = connected;\n if (!connected) {\n this.cancelAll(new TciError('disconnected', 'TCI connection closed'));\n }\n }\n\n enqueue(command: TciCommandInput, options: QueueCommandOptions = {}): Promise<QueuedCommandResult> {\n const request = parseTciCommand(command);\n const raw = typeof command === 'string' ? ensureSemicolon(command) : formatTciCommand(command.originalName, command.args);\n\n if (!this.connected) {\n return Promise.reject(new TciError('not-connected', 'TCI socket is not connected'));\n }\n\n return new Promise<QueuedCommandResult>((resolve, reject) => {\n const pending: PendingCommand = {\n raw,\n request,\n timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,\n matcher: options.matcher ?? ((reply, req) => isCommandReplyTo(reply, req)),\n resolve,\n reject,\n };\n\n if (options.signal) {\n if (options.signal.aborted) {\n reject(new TciError('cancelled', 'TCI command was cancelled'));\n return;\n }\n const onAbort = () => this.rejectPending(pending, new TciError('cancelled', 'TCI command was cancelled'));\n options.signal.addEventListener('abort', onAbort, { once: true });\n pending.abortCleanup = () => options.signal?.removeEventListener('abort', onAbort);\n }\n\n this.queue.push(pending);\n void this.pump();\n });\n }\n\n handleCommand(commandInput: TciCommandInput): boolean {\n const active = this.active;\n if (!active) {\n return false;\n }\n const reply = parseTciCommand(commandInput);\n if (!active.matcher(reply, active.request)) {\n return false;\n }\n this.finishActive(reply);\n return true;\n }\n\n cancelAll(error = new TciError('cancelled', 'TCI command queue cancelled')): void {\n const pending = [...this.queue];\n this.queue = [];\n if (this.active) {\n pending.unshift(this.active);\n this.active = undefined;\n }\n for (const item of pending) {\n this.rejectPending(item, error);\n }\n }\n\n get size(): number {\n return this.queue.length + (this.active ? 1 : 0);\n }\n\n private async pump(): Promise<void> {\n if (this.active || !this.connected) {\n return;\n }\n const next = this.queue.shift();\n if (!next) {\n return;\n }\n\n this.active = next;\n next.timer = setTimeout(() => {\n this.rejectPending(next, new TciError('command-timeout', `Timed out waiting for TCI reply to ${next.raw}`));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }, next.timeoutMs);\n\n try {\n await this.send(next.raw);\n } catch (error) {\n this.rejectPending(next, new TciError('disconnected', error instanceof Error ? error.message : String(error), error));\n if (this.active === next) {\n this.active = undefined;\n }\n void this.pump();\n }\n }\n\n private finishActive(reply: TciCommand): void {\n const active = this.active;\n if (!active) {\n return;\n }\n this.active = undefined;\n if (active.timer) {\n clearTimeout(active.timer);\n }\n active.abortCleanup?.();\n active.resolve({ request: active.request, reply });\n void this.pump();\n }\n\n private rejectPending(pending: PendingCommand, error: TciError): void {\n if (pending.timer) {\n clearTimeout(pending.timer);\n }\n pending.abortCleanup?.();\n const wasActive = this.active === pending;\n if (wasActive) {\n this.active = undefined;\n } else {\n this.queue = this.queue.filter((item) => item !== pending);\n }\n pending.reject(error);\n if (wasActive && this.connected) {\n void this.pump();\n }\n }\n}\n\nfunction ensureSemicolon(command: string): string {\n return command.trim().endsWith(';') ? command.trim() : `${command.trim()};`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/testing/index.cjs
CHANGED
|
@@ -53,7 +53,7 @@ var TciError = class extends Error {
|
|
|
53
53
|
|
|
54
54
|
// src/audio/streamFrame.ts
|
|
55
55
|
var TCI_STREAM_HEADER_BYTES = 16 * 4;
|
|
56
|
-
function parseStreamFrame(input) {
|
|
56
|
+
function parseStreamFrame(input, options = {}) {
|
|
57
57
|
const buffer = toBuffer(input);
|
|
58
58
|
if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
|
|
59
59
|
throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
|
|
@@ -61,28 +61,49 @@ function parseStreamFrame(input) {
|
|
|
61
61
|
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
62
62
|
const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
|
|
63
63
|
const sampleType = normalizeSampleType(header[2]);
|
|
64
|
-
|
|
64
|
+
const streamType = normalizeStreamType(header[6]);
|
|
65
|
+
let channels = header[7] || options.negotiatedChannels || 0;
|
|
65
66
|
const bytesPerSample = sampleTypeBytes(sampleType);
|
|
66
|
-
const
|
|
67
|
+
const headerSampleCount = header[5];
|
|
67
68
|
const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
|
|
68
69
|
if (channels <= 0) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
|
|
71
|
+
channels = 1;
|
|
72
|
+
} else {
|
|
73
|
+
const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
|
|
74
|
+
if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
|
|
75
|
+
throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
|
|
76
|
+
}
|
|
77
|
+
channels = inferredChannels;
|
|
72
78
|
}
|
|
73
|
-
channels = inferredChannels;
|
|
74
|
-
}
|
|
75
|
-
const payloadLength = sampleCount * bytesPerSample * channels;
|
|
76
|
-
const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;
|
|
77
|
-
if (buffer.byteLength !== expectedLength) {
|
|
78
|
-
throw new TciError(
|
|
79
|
-
"invalid-frame",
|
|
80
|
-
`TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
|
|
81
|
-
);
|
|
82
79
|
}
|
|
83
|
-
|
|
80
|
+
const payloadLength = actualPayloadLength;
|
|
81
|
+
const alignedFrameBytes = bytesPerSample * channels;
|
|
82
|
+
if (payloadLength % alignedFrameBytes !== 0) {
|
|
84
83
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
85
84
|
}
|
|
85
|
+
const actualScalarCount = payloadLength / bytesPerSample;
|
|
86
|
+
const requestedSemantics = options.lengthSemantics ?? "auto";
|
|
87
|
+
const lengthSemantics = resolveLengthSemantics(
|
|
88
|
+
requestedSemantics,
|
|
89
|
+
streamType,
|
|
90
|
+
headerSampleCount,
|
|
91
|
+
actualScalarCount,
|
|
92
|
+
channels
|
|
93
|
+
);
|
|
94
|
+
const sampleCount = lengthSemantics === "per-channel" ? headerSampleCount * channels : headerSampleCount;
|
|
95
|
+
if (streamType !== 3 /* TX_CHRONO */) {
|
|
96
|
+
const expectedPayloadLength = sampleCount * bytesPerSample;
|
|
97
|
+
if (payloadLength !== expectedPayloadLength) {
|
|
98
|
+
throw new TciError(
|
|
99
|
+
"invalid-frame",
|
|
100
|
+
`TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (sampleCount % channels !== 0) {
|
|
105
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
106
|
+
}
|
|
86
107
|
return {
|
|
87
108
|
receiver: header[0],
|
|
88
109
|
sampleRate: header[1],
|
|
@@ -90,11 +111,14 @@ function parseStreamFrame(input) {
|
|
|
90
111
|
codec: header[3],
|
|
91
112
|
crc: header[4],
|
|
92
113
|
payloadLength,
|
|
93
|
-
streamType
|
|
114
|
+
streamType,
|
|
94
115
|
channels,
|
|
95
116
|
reserved: header.slice(8),
|
|
96
117
|
payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
|
|
97
|
-
|
|
118
|
+
headerSampleCount,
|
|
119
|
+
sampleCount,
|
|
120
|
+
frameCount: sampleCount / channels,
|
|
121
|
+
lengthSemantics
|
|
98
122
|
};
|
|
99
123
|
}
|
|
100
124
|
function buildStreamFrame(options) {
|
|
@@ -108,7 +132,19 @@ function buildStreamFrame(options) {
|
|
|
108
132
|
if (payload.byteLength % (bytesPerSample * channels) !== 0) {
|
|
109
133
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
110
134
|
}
|
|
111
|
-
const
|
|
135
|
+
const derivedSampleCount = payload.byteLength / bytesPerSample;
|
|
136
|
+
const sampleCount = options.sampleCount ?? derivedSampleCount;
|
|
137
|
+
if (!Number.isInteger(sampleCount) || sampleCount < 0) {
|
|
138
|
+
throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
|
|
139
|
+
}
|
|
140
|
+
if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {
|
|
141
|
+
throw new TciError("invalid-frame", `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);
|
|
142
|
+
}
|
|
143
|
+
if (sampleCount % channels !== 0) {
|
|
144
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
145
|
+
}
|
|
146
|
+
const lengthSemantics = options.lengthSemantics === "per-channel" ? "per-channel" : "scalar";
|
|
147
|
+
const headerSampleCount = lengthSemantics === "per-channel" ? sampleCount / channels : sampleCount;
|
|
112
148
|
const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
|
|
113
149
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
114
150
|
const reserved = options.reserved ?? [];
|
|
@@ -118,7 +154,7 @@ function buildStreamFrame(options) {
|
|
|
118
154
|
sampleType,
|
|
119
155
|
options.codec ?? 0,
|
|
120
156
|
options.crc ?? 0,
|
|
121
|
-
|
|
157
|
+
headerSampleCount,
|
|
122
158
|
options.streamType,
|
|
123
159
|
channels,
|
|
124
160
|
...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
|
|
@@ -127,6 +163,13 @@ function buildStreamFrame(options) {
|
|
|
127
163
|
payload.copy(frame, TCI_STREAM_HEADER_BYTES);
|
|
128
164
|
return frame;
|
|
129
165
|
}
|
|
166
|
+
function resolveLengthSemantics(requested, streamType, headerSampleCount, actualScalarCount, channels) {
|
|
167
|
+
if (requested !== "auto") return requested;
|
|
168
|
+
if (streamType === 3 /* TX_CHRONO */ && actualScalarCount === 0) return "scalar";
|
|
169
|
+
if (actualScalarCount === headerSampleCount) return "scalar";
|
|
170
|
+
if (actualScalarCount === headerSampleCount * channels) return "per-channel";
|
|
171
|
+
return "scalar";
|
|
172
|
+
}
|
|
130
173
|
function sampleTypeBytes(sampleType) {
|
|
131
174
|
switch (normalizeSampleType(sampleType)) {
|
|
132
175
|
case 0 /* INT16 */:
|
|
@@ -293,6 +336,7 @@ var MockTciServer = class {
|
|
|
293
336
|
frequency = 14074e3;
|
|
294
337
|
mode = "DIGU";
|
|
295
338
|
ptt = false;
|
|
339
|
+
drive = 30;
|
|
296
340
|
constructor(options = {}) {
|
|
297
341
|
this.options = {
|
|
298
342
|
port: options.port ?? 0,
|
|
@@ -368,14 +412,14 @@ var MockTciServer = class {
|
|
|
368
412
|
const sampleType = options.sampleType ?? 3 /* FLOAT32 */;
|
|
369
413
|
const channels = options.channels ?? 1;
|
|
370
414
|
const sampleCount = options.sampleCount ?? 512;
|
|
371
|
-
const payload = options.payload ?? samplesToPayload(new Float32Array(sampleCount * channels), sampleType);
|
|
372
415
|
const frame = buildStreamFrame({
|
|
373
416
|
receiver: options.receiver ?? 0,
|
|
374
417
|
sampleRate: options.sampleRate ?? 12e3,
|
|
375
418
|
sampleType,
|
|
376
419
|
streamType: 3 /* TX_CHRONO */,
|
|
377
420
|
channels,
|
|
378
|
-
payload
|
|
421
|
+
payload: options.payload,
|
|
422
|
+
sampleCount
|
|
379
423
|
});
|
|
380
424
|
this.broadcastBinary(frame);
|
|
381
425
|
}
|
|
@@ -389,13 +433,14 @@ var MockTciServer = class {
|
|
|
389
433
|
socket.on("close", () => this.sockets.delete(socket));
|
|
390
434
|
socket.on("message", (data, isBinary) => void this.handleMessage(socket, data, isBinary));
|
|
391
435
|
const startupCommands = this.options.startupCommands ?? [
|
|
392
|
-
"PROTOCOL:2.0;",
|
|
436
|
+
"PROTOCOL:ExpertSDR3,2.0;",
|
|
393
437
|
"DEVICE:Mock ExpertSDR3;",
|
|
394
438
|
"MODULATIONS_LIST:LSB,USB,CW,AM,NFM,DIGU,DIGL;",
|
|
395
439
|
`VFO:0,0,${this.frequency};`,
|
|
396
440
|
`MODULATION:0,${this.mode};`,
|
|
397
441
|
`TRX:0,${this.ptt};`,
|
|
398
|
-
|
|
442
|
+
`DRIVE:0,${this.drive};`,
|
|
443
|
+
"READY;"
|
|
399
444
|
];
|
|
400
445
|
queueMicrotask(() => {
|
|
401
446
|
for (const command of startupCommands) {
|
|
@@ -455,8 +500,15 @@ var MockTciServer = class {
|
|
|
455
500
|
socket.send(formatTciCommand("TRX", [trx, this.ptt]));
|
|
456
501
|
break;
|
|
457
502
|
}
|
|
503
|
+
case "drive": {
|
|
504
|
+
const hasTrx = command.args.length >= 2;
|
|
505
|
+
const trx = hasTrx ? command.args[0] ?? "0" : "0";
|
|
506
|
+
const valueArg = command.args[hasTrx ? 1 : 0];
|
|
507
|
+
if (valueArg !== void 0) this.drive = Number(valueArg);
|
|
508
|
+
socket.send(formatTciCommand("DRIVE", [trx, this.drive]));
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
458
511
|
case "tune":
|
|
459
|
-
case "drive":
|
|
460
512
|
case "split_enable":
|
|
461
513
|
case "cw_macros":
|
|
462
514
|
case "cw_msg":
|
|
@@ -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 samplesToPayload,\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\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 payload = options.payload ?? samplesToPayload(new Float32Array(sampleCount * channels), sampleType);\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,\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: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 'READY:true;',\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 'tune':\n case 'drive':\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 | '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';\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 /** Official Stream.length value: number of samples per channel in the payload. */\n sampleCount: 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 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): 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 let channels = header[7];\n const bytesPerSample = sampleTypeBytes(sampleType);\n const sampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / 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 const payloadLength = sampleCount * bytesPerSample * channels;\n const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;\n if (buffer.byteLength !== expectedLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`,\n );\n }\n if (payloadLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\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: normalizeStreamType(header[6]),\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n sampleCount,\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 sampleCount = payload.byteLength / bytesPerSample / channels;\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 sampleCount,\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\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":["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\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":["WebSocket"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -2,6 +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-xRotf9gW.cjs';
|
|
5
6
|
|
|
6
7
|
interface MockTciServerOptions {
|
|
7
8
|
port?: number;
|
|
@@ -26,6 +27,7 @@ declare class MockTciServer {
|
|
|
26
27
|
private frequency;
|
|
27
28
|
private mode;
|
|
28
29
|
private ptt;
|
|
30
|
+
private drive;
|
|
29
31
|
constructor(options?: MockTciServerOptions);
|
|
30
32
|
start(): Promise<void>;
|
|
31
33
|
stop(): Promise<void>;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -2,6 +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-9seY9Th-.js';
|
|
5
6
|
|
|
6
7
|
interface MockTciServerOptions {
|
|
7
8
|
port?: number;
|
|
@@ -26,6 +27,7 @@ declare class MockTciServer {
|
|
|
26
27
|
private frequency;
|
|
27
28
|
private mode;
|
|
28
29
|
private ptt;
|
|
30
|
+
private drive;
|
|
29
31
|
constructor(options?: MockTciServerOptions);
|
|
30
32
|
start(): Promise<void>;
|
|
31
33
|
stop(): Promise<void>;
|