tci-client-node 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +24 -0
  3. package/dist/audio/index.d.cts +2 -1
  4. package/dist/audio/index.d.ts +2 -1
  5. package/dist/dialect/index.cjs +175 -12
  6. package/dist/dialect/index.cjs.map +1 -1
  7. package/dist/dialect/index.d.cts +3 -2
  8. package/dist/dialect/index.d.ts +3 -2
  9. package/dist/dialect/index.js +175 -12
  10. package/dist/dialect/index.js.map +1 -1
  11. package/dist/errors-CT3b8LLw.d.cts +9 -0
  12. package/dist/errors-CT3b8LLw.d.ts +9 -0
  13. package/dist/index.cjs +404 -5
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +14 -5
  16. package/dist/index.d.ts +14 -5
  17. package/dist/index.js +399 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/meter/index.cjs +348 -0
  20. package/dist/meter/index.cjs.map +1 -0
  21. package/dist/meter/index.d.cts +78 -0
  22. package/dist/meter/index.d.ts +78 -0
  23. package/dist/meter/index.js +317 -0
  24. package/dist/meter/index.js.map +1 -0
  25. package/dist/protocol/index.d.cts +36 -2
  26. package/dist/protocol/index.d.ts +36 -2
  27. package/dist/testing/index.cjs +32 -0
  28. package/dist/testing/index.cjs.map +1 -1
  29. package/dist/testing/index.d.cts +19 -1
  30. package/dist/testing/index.d.ts +19 -1
  31. package/dist/testing/index.js +32 -0
  32. package/dist/testing/index.js.map +1 -1
  33. package/dist/types-C0qVJVSO.d.ts +74 -0
  34. package/dist/types-C4MtLKoy.d.cts +74 -0
  35. package/dist/{types-1YXGwrgI.d.cts → types-CYK_NOTz.d.cts} +2 -0
  36. package/dist/{types-BtPh4Dbd.d.ts → types-DVIPU-VR.d.ts} +2 -0
  37. package/package.json +11 -2
  38. package/dist/index-lbK6NGY4.d.cts +0 -42
  39. package/dist/index-paj1AOJY.d.ts +0 -42
package/dist/index.d.cts CHANGED
@@ -1,17 +1,23 @@
1
- import { Q as QueueCommandOptions, T as TciError } from './index-lbK6NGY4.cjs';
2
- export { a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions, e as TciErrorCode, t as toTciError } from './index-lbK6NGY4.cjs';
1
+ import { T as TciError } from './errors-CT3b8LLw.cjs';
2
+ export { a as TciErrorCode, t as toTciError } from './errors-CT3b8LLw.cjs';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import WebSocket from 'ws';
5
5
  import { TciSampleType, TciSampleTypeName, TciStreamFrame, BuildTxAudioFrameOptions } from './audio/index.cjs';
6
6
  export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.cjs';
7
7
  import { T as TciCommand } from './text-BwCWY1k1.cjs';
8
8
  export { a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from './text-BwCWY1k1.cjs';
9
- import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-1YXGwrgI.cjs';
10
- export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-1YXGwrgI.cjs';
9
+ import { QueueCommandOptions } from './protocol/index.cjs';
10
+ export { QueuedCommandResult, TciCommandMatcher, TciCommandQueue, TciCommandQueueOptions } from './protocol/index.cjs';
11
+ import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-CYK_NOTz.cjs';
12
+ export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-CYK_NOTz.cjs';
11
13
  import { TciDialectRegistry } from './dialect/index.cjs';
12
14
  export { aetherSdrDialect, assertValidTciHandshake, builtInDialects, compareTciVersion, defaultTciDialectRegistry, expertSdr14Dialect, expertSdrLegacyDialect, expertSdrModernDialect, genericObservedDialect, parseProtocolIdentity, parseTciVersion, thetisDialect } from './dialect/index.cjs';
13
15
  import { TciTransportFactory } from './transport/index.cjs';
14
16
  export { TciTransport, TciTransportEvents, WebSocketTciTransport } from './transport/index.cjs';
17
+ import { T as TciMeterCapabilities, a as TciMeterStreamOptions } from './types-C4MtLKoy.cjs';
18
+ export { b as TciMeterAdapter, c as TciMeterCommand, d as TciMeterDecodeResult, e as TciMeterDecodedFrame, f as TciMeterInterval, g as TciMeterSupport, h as TciRxMeterFrame, i as TciRxMeterSource, j as TciTxMeterFrame, U as UNKNOWN_TCI_METER_CAPABILITIES, k as cloneMeterCapabilities } from './types-C4MtLKoy.cjs';
19
+ import { TciMeterStreamSession } from './meter/index.cjs';
20
+ export { StandardTciMeterAdapter, TciMeterStreamEvents, createUnknownTciMeterAdapter } from './meter/index.cjs';
15
21
 
16
22
  interface TciClientOptions {
17
23
  url: string;
@@ -175,6 +181,7 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
175
181
  private initializationCommands;
176
182
  private handshakeWaiter?;
177
183
  private activeIqSession?;
184
+ private activeMeterSession?;
178
185
  constructor(options: TciClientOptions);
179
186
  connect(): Promise<TciHandshakeResult>;
180
187
  disconnect(code?: number, reason?: string): Promise<void>;
@@ -182,6 +189,8 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
182
189
  getState(): TciClientState;
183
190
  getHandshakeResult(): TciHandshakeResult | undefined;
184
191
  getIqCapabilities(): TciIqCapabilities;
192
+ getMeterCapabilities(): TciMeterCapabilities;
193
+ openMeterStream(options?: TciMeterStreamOptions): Promise<TciMeterStreamSession>;
185
194
  setIqSampleRate(sampleRate: number, timeoutMs?: number): Promise<IqSampleRateResult>;
186
195
  openIqStream(options?: TciIqStreamOptions): Promise<TciIqStreamSession>;
187
196
  sendCommand(name: string, args?: readonly unknown[], options?: SendCommandOptions): Promise<TciCommand | undefined>;
@@ -240,4 +249,4 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
240
249
  }
241
250
  declare function createTciClient(options: TciClientOptions): TciClient;
242
251
 
243
- export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
252
+ export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, TciMeterCapabilities, TciMeterStreamOptions, TciMeterStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,23 @@
1
- import { Q as QueueCommandOptions, T as TciError } from './index-paj1AOJY.js';
2
- export { a as QueuedCommandResult, b as TciCommandMatcher, c as TciCommandQueue, d as TciCommandQueueOptions, e as TciErrorCode, t as toTciError } from './index-paj1AOJY.js';
1
+ import { T as TciError } from './errors-CT3b8LLw.js';
2
+ export { a as TciErrorCode, t as toTciError } from './errors-CT3b8LLw.js';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import WebSocket from 'ws';
5
5
  import { TciSampleType, TciSampleTypeName, TciStreamFrame, BuildTxAudioFrameOptions } from './audio/index.js';
6
6
  export { BuildStreamFrameOptions, ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload } from './audio/index.js';
7
7
  import { T as TciCommand } from './text-BwCWY1k1.js';
8
8
  export { a as TciCommandInput, c as commandKey, e as escapeTciText, f as formatTciCommand, i as isCommandReplyTo, n as normalizeCommandName, p as parseTciCommand, b as parseTciText, u as unescapeTciText } from './text-BwCWY1k1.js';
9
- import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-BtPh4Dbd.js';
10
- export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-BtPh4Dbd.js';
9
+ import { QueueCommandOptions } from './protocol/index.js';
10
+ export { QueuedCommandResult, TciCommandMatcher, TciCommandQueue, TciCommandQueueOptions } from './protocol/index.js';
11
+ import { T as TciWriteResult, a as TciDialectId, b as TciHandshakeResult, c as TciDialectSelection } from './types-DVIPU-VR.js';
12
+ export { B as BuiltInTciDialectId, d as TciDialect, e as TciDialectDetection, f as TciDialectDetectionContext, g as TciDialectScore, h as TciDriveState, i as TciProtocolIdentity, j as TciStreamLengthSemantics } from './types-DVIPU-VR.js';
11
13
  import { TciDialectRegistry } from './dialect/index.js';
12
14
  export { aetherSdrDialect, assertValidTciHandshake, builtInDialects, compareTciVersion, defaultTciDialectRegistry, expertSdr14Dialect, expertSdrLegacyDialect, expertSdrModernDialect, genericObservedDialect, parseProtocolIdentity, parseTciVersion, thetisDialect } from './dialect/index.js';
13
15
  import { TciTransportFactory } from './transport/index.js';
14
16
  export { TciTransport, TciTransportEvents, WebSocketTciTransport } from './transport/index.js';
17
+ import { T as TciMeterCapabilities, a as TciMeterStreamOptions } from './types-C0qVJVSO.js';
18
+ export { b as TciMeterAdapter, c as TciMeterCommand, d as TciMeterDecodeResult, e as TciMeterDecodedFrame, f as TciMeterInterval, g as TciMeterSupport, h as TciRxMeterFrame, i as TciRxMeterSource, j as TciTxMeterFrame, U as UNKNOWN_TCI_METER_CAPABILITIES, k as cloneMeterCapabilities } from './types-C0qVJVSO.js';
19
+ import { TciMeterStreamSession } from './meter/index.js';
20
+ export { StandardTciMeterAdapter, TciMeterStreamEvents, createUnknownTciMeterAdapter } from './meter/index.js';
15
21
 
16
22
  interface TciClientOptions {
17
23
  url: string;
@@ -175,6 +181,7 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
175
181
  private initializationCommands;
176
182
  private handshakeWaiter?;
177
183
  private activeIqSession?;
184
+ private activeMeterSession?;
178
185
  constructor(options: TciClientOptions);
179
186
  connect(): Promise<TciHandshakeResult>;
180
187
  disconnect(code?: number, reason?: string): Promise<void>;
@@ -182,6 +189,8 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
182
189
  getState(): TciClientState;
183
190
  getHandshakeResult(): TciHandshakeResult | undefined;
184
191
  getIqCapabilities(): TciIqCapabilities;
192
+ getMeterCapabilities(): TciMeterCapabilities;
193
+ openMeterStream(options?: TciMeterStreamOptions): Promise<TciMeterStreamSession>;
185
194
  setIqSampleRate(sampleRate: number, timeoutMs?: number): Promise<IqSampleRateResult>;
186
195
  openIqStream(options?: TciIqStreamOptions): Promise<TciIqStreamSession>;
187
196
  sendCommand(name: string, args?: readonly unknown[], options?: SendCommandOptions): Promise<TciCommand | undefined>;
@@ -240,4 +249,4 @@ declare class TciClient extends EventEmitter<TciClientEvents> {
240
249
  }
241
250
  declare function createTciClient(options: TciClientOptions): TciClient;
242
251
 
243
- export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
252
+ export { BuildTxAudioFrameOptions, type IqSampleRateResult, QueueCommandOptions, type SendCommandOptions, type TciAudioConfig, TciClient, type TciClientEvents, type TciClientOptions, type TciClientState, TciCommand, TciDialectId, TciDialectRegistry, TciDialectSelection, TciError, TciHandshakeResult, type TciIqCapabilities, type TciIqFrame, type TciIqStreamEvents, type TciIqStreamOptions, TciIqStreamSession, TciMeterCapabilities, TciMeterStreamOptions, TciMeterStreamSession, type TciPttOptions, TciSampleType, TciSampleTypeName, TciStreamFrame, TciTransportFactory, type TciTxChronoRequest, type TciWriteAckMode, type TciWriteOptions, TciWriteResult, createTciClient };
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ function toTciError(error, fallbackCode = "protocol-error") {
20
20
  }
21
21
 
22
22
  // src/client/TciClient.ts
23
- import { EventEmitter as EventEmitter2 } from "eventemitter3";
23
+ import { EventEmitter as EventEmitter3 } from "eventemitter3";
24
24
  import WebSocket2 from "ws";
25
25
 
26
26
  // src/audio/streamFrame.ts
@@ -597,6 +597,302 @@ function ensureSemicolon(command) {
597
597
  return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
598
598
  }
599
599
 
600
+ // src/meter/types.ts
601
+ var UNKNOWN_TCI_METER_CAPABILITIES = {
602
+ rxLevel: "unknown",
603
+ rxAverageLevel: "unknown",
604
+ rxPeakBin: "unknown",
605
+ txMicLevel: "unknown",
606
+ txRmsPower: "unknown",
607
+ txPeakPower: "unknown",
608
+ txSwr: "unknown",
609
+ txAlcDbfs: "unknown"
610
+ };
611
+ function cloneMeterCapabilities(capabilities) {
612
+ return { ...capabilities };
613
+ }
614
+
615
+ // src/meter/adapters.ts
616
+ var StandardTciMeterAdapter = class {
617
+ declaredCapabilities;
618
+ options;
619
+ constructor(options = {}) {
620
+ this.options = options;
621
+ this.declaredCapabilities = {
622
+ ...UNKNOWN_TCI_METER_CAPABILITIES,
623
+ rxLevel: "declared",
624
+ txMicLevel: "declared",
625
+ txRmsPower: "declared",
626
+ txPeakPower: "declared",
627
+ txSwr: "declared",
628
+ rxAverageLevel: options.supportsRxExtended ? "declared" : "unknown",
629
+ rxPeakBin: options.supportsRxExtended ? "declared" : "unknown",
630
+ txAlcDbfs: options.txAlcUnit === "dbfs" ? "declared" : "unknown",
631
+ ...options.capabilities
632
+ };
633
+ }
634
+ normalizeInterval(intervalMs) {
635
+ const requestedMs = Math.round(intervalMs);
636
+ if (this.options.interval?.fixedMs !== void 0) {
637
+ return { requestedMs, appliedMs: this.options.interval.fixedMs };
638
+ }
639
+ const minMs = this.options.interval?.minMs ?? requestedMs;
640
+ const maxMs = this.options.interval?.maxMs ?? requestedMs;
641
+ const normalized = Math.max(minMs, Math.min(maxMs, requestedMs));
642
+ return {
643
+ requestedMs,
644
+ appliedMs: this.options.interval?.reportsApplied === false ? void 0 : normalized
645
+ };
646
+ }
647
+ buildEnableCommand(kind, enabled, intervalMs) {
648
+ return {
649
+ name: kind === "rx" ? "RX_SENSORS_ENABLE" : "TX_SENSORS_ENABLE",
650
+ args: enabled ? [true, intervalMs] : [false]
651
+ };
652
+ }
653
+ decode(command, receivedAtMs) {
654
+ switch (command.name) {
655
+ case "rx_sensors":
656
+ return decodeRx(command, receivedAtMs, "rx_sensors");
657
+ case "rx_channel_sensors":
658
+ return decodeRx(command, receivedAtMs, "rx_channel_sensors");
659
+ case "rx_channel_sensors_ex":
660
+ return decodeRx(command, receivedAtMs, "rx_channel_sensors_ex");
661
+ case "tx_sensors":
662
+ return decodeTx(command, receivedAtMs, this.options.txAlcUnit);
663
+ default:
664
+ return void 0;
665
+ }
666
+ }
667
+ };
668
+ function createUnknownTciMeterAdapter() {
669
+ return new StandardTciMeterAdapter({
670
+ capabilities: cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES),
671
+ interval: { reportsApplied: false }
672
+ });
673
+ }
674
+ function decodeRx(command, receivedAtMs, source) {
675
+ const receiver = integer(command.args[0]);
676
+ const hasChannel = source !== "rx_sensors";
677
+ const channel = hasChannel ? integer(command.args[1]) : 0;
678
+ const levelIndex = hasChannel ? 2 : 1;
679
+ const levelDbm = finite(command.args[levelIndex]);
680
+ if (receiver === void 0 || receiver < 0 || channel === void 0 || channel < 0 || levelDbm === void 0) {
681
+ return { issue: `Invalid ${command.originalName} meter frame: ${command.raw}` };
682
+ }
683
+ const frame = { receiver, channel, levelDbm, source, receivedAtMs };
684
+ if (source === "rx_channel_sensors_ex") {
685
+ const averageLevelDbm = finite(command.args[3]);
686
+ const peakBinDbm = finite(command.args[4]);
687
+ if (averageLevelDbm === void 0 || peakBinDbm === void 0) {
688
+ return { issue: `Invalid ${command.originalName} extended meter frame: ${command.raw}` };
689
+ }
690
+ frame.averageLevelDbm = averageLevelDbm;
691
+ frame.peakBinDbm = peakBinDbm;
692
+ if (command.args.length > 5) frame.extraArgs = command.args.slice(5);
693
+ } else if (command.args.length > levelIndex + 1) {
694
+ frame.extraArgs = command.args.slice(levelIndex + 1);
695
+ }
696
+ return { decoded: { kind: "rx", frame } };
697
+ }
698
+ function decodeTx(command, receivedAtMs, alcUnit) {
699
+ const trx = integer(command.args[0]);
700
+ if (trx === void 0 || trx < 0) {
701
+ return { issue: `Invalid ${command.originalName} transmitter index: ${command.raw}` };
702
+ }
703
+ const micLevelDbm = optionalFinite(command.args[1]);
704
+ const rmsPowerWatts = optionalFinite(command.args[2]);
705
+ const peakPowerWatts = optionalFinite(command.args[3]);
706
+ const swr = optionalFinite(command.args[4]);
707
+ const alcValue = alcUnit ? optionalFinite(command.args[5]) : void 0;
708
+ if (micLevelDbm.invalid || rmsPowerWatts.invalid || peakPowerWatts.invalid || swr.invalid || alcValue?.invalid) {
709
+ return { issue: `Invalid ${command.originalName} numeric meter frame: ${command.raw}` };
710
+ }
711
+ if (rmsPowerWatts.value !== void 0 && rmsPowerWatts.value < 0 || peakPowerWatts.value !== void 0 && peakPowerWatts.value < 0 || swr.value !== void 0 && swr.value < 1) {
712
+ return { issue: `Out-of-range ${command.originalName} meter frame: ${command.raw}` };
713
+ }
714
+ if (micLevelDbm.value === void 0 && rmsPowerWatts.value === void 0 && peakPowerWatts.value === void 0 && swr.value === void 0 && alcValue?.value === void 0) {
715
+ return { issue: `Empty ${command.originalName} meter frame: ${command.raw}` };
716
+ }
717
+ const frame = {
718
+ trx,
719
+ micLevelDbm: micLevelDbm.value,
720
+ rmsPowerWatts: rmsPowerWatts.value,
721
+ peakPowerWatts: peakPowerWatts.value,
722
+ swr: swr.value,
723
+ receivedAtMs
724
+ };
725
+ if (alcUnit && alcValue?.value !== void 0) frame.alc = { value: alcValue.value, unit: alcUnit };
726
+ const knownArgs = alcUnit ? 6 : 5;
727
+ if (command.args.length > knownArgs) frame.extraArgs = command.args.slice(knownArgs);
728
+ return { decoded: { kind: "tx", frame } };
729
+ }
730
+ function finite(value) {
731
+ if (value === void 0 || value === "") return void 0;
732
+ const parsed = Number(value);
733
+ return Number.isFinite(parsed) ? parsed : void 0;
734
+ }
735
+ function integer(value) {
736
+ const parsed = finite(value);
737
+ return parsed !== void 0 && Number.isInteger(parsed) ? parsed : void 0;
738
+ }
739
+ function optionalFinite(value) {
740
+ if (value === void 0 || value === "") return { invalid: false };
741
+ const parsed = Number(value);
742
+ return Number.isFinite(parsed) ? { value: parsed, invalid: false } : { invalid: true };
743
+ }
744
+
745
+ // src/meter/TciMeterStreamSession.ts
746
+ import { EventEmitter } from "eventemitter3";
747
+ var RX_COALESCE_MS = 20;
748
+ var TciMeterStreamSession = class extends EventEmitter {
749
+ receiver;
750
+ channel;
751
+ trx;
752
+ requestedIntervalMs;
753
+ appliedIntervalMs;
754
+ rxEnabled;
755
+ txEnabled;
756
+ adapter;
757
+ callbacks;
758
+ capabilities;
759
+ pendingRx = /* @__PURE__ */ new Map();
760
+ closed = false;
761
+ constructor(options) {
762
+ super();
763
+ this.receiver = options.receiver;
764
+ this.channel = options.channel;
765
+ this.trx = options.trx;
766
+ this.requestedIntervalMs = options.requestedIntervalMs;
767
+ this.appliedIntervalMs = options.appliedIntervalMs;
768
+ this.rxEnabled = options.rxEnabled;
769
+ this.txEnabled = options.txEnabled;
770
+ this.adapter = options.adapter;
771
+ this.callbacks = options.callbacks;
772
+ this.capabilities = cloneMeterCapabilities(options.adapter.declaredCapabilities);
773
+ }
774
+ getCapabilities() {
775
+ return cloneMeterCapabilities(this.capabilities);
776
+ }
777
+ async close() {
778
+ if (this.closed) return;
779
+ this.closed = true;
780
+ this.clearPendingRx();
781
+ try {
782
+ await this.callbacks.close();
783
+ } finally {
784
+ this.emit("closed");
785
+ this.removeAllListeners();
786
+ }
787
+ }
788
+ _acceptCommand(command, receivedAtMs) {
789
+ if (this.closed) return;
790
+ this.acceptEnableAcknowledgement(command);
791
+ const result = this.adapter.decode(command, receivedAtMs);
792
+ if (!result) return;
793
+ if (result.issue) {
794
+ this.emit("error", new TciError("protocol-error", result.issue));
795
+ return;
796
+ }
797
+ if (result.decoded?.kind === "rx") this.acceptRxFrame(result.decoded.frame);
798
+ if (result.decoded?.kind === "tx") this.acceptTxFrame(result.decoded.frame);
799
+ }
800
+ _fail(error) {
801
+ if (this.closed) return;
802
+ this.closed = true;
803
+ this.clearPendingRx();
804
+ this.emit("error", error);
805
+ this.emit("closed");
806
+ this.removeAllListeners();
807
+ }
808
+ acceptRxFrame(frame) {
809
+ if (!this.rxEnabled || frame.receiver !== this.receiver || frame.channel !== this.channel) return;
810
+ this.observe("rxLevel");
811
+ if (frame.averageLevelDbm !== void 0) this.observe("rxAverageLevel");
812
+ if (frame.peakBinDbm !== void 0) this.observe("rxPeakBin");
813
+ const key = `${frame.receiver}:${frame.channel}`;
814
+ const pending = this.pendingRx.get(key);
815
+ if (pending) {
816
+ const sameReading = Math.abs(pending.frame.levelDbm - frame.levelDbm) < 0.05;
817
+ if (sameReading && rxPriority(frame) >= rxPriority(pending.frame)) {
818
+ clearTimeout(pending.timer);
819
+ this.pendingRx.delete(key);
820
+ } else if (sameReading) {
821
+ return;
822
+ } else {
823
+ this.flushRx(key, pending);
824
+ }
825
+ }
826
+ const timer = setTimeout(() => {
827
+ const current = this.pendingRx.get(key);
828
+ if (current?.frame === frame) this.flushRx(key, current);
829
+ }, RX_COALESCE_MS);
830
+ this.pendingRx.set(key, { frame, timer });
831
+ }
832
+ acceptTxFrame(frame) {
833
+ if (!this.txEnabled || frame.trx !== this.trx) return;
834
+ if (frame.micLevelDbm !== void 0) this.observe("txMicLevel");
835
+ if (frame.rmsPowerWatts !== void 0) this.observe("txRmsPower");
836
+ if (frame.peakPowerWatts !== void 0) this.observe("txPeakPower");
837
+ if (frame.swr !== void 0) this.observe("txSwr");
838
+ if (frame.alc?.unit === "dbfs") this.observe("txAlcDbfs");
839
+ this.emit("txFrame", frame);
840
+ }
841
+ acceptEnableAcknowledgement(command) {
842
+ const enabled = command.args[0]?.toLowerCase();
843
+ if (enabled !== "true" && enabled !== "1" && enabled !== "on") return;
844
+ if (command.name === "rx_sensors_enable" && this.rxEnabled) {
845
+ this.acknowledge(["rxLevel"]);
846
+ }
847
+ if (command.name === "tx_sensors_enable" && this.txEnabled) {
848
+ this.acknowledge(["txMicLevel", "txRmsPower", "txPeakPower", "txSwr"]);
849
+ }
850
+ }
851
+ acknowledge(keys) {
852
+ let changed = false;
853
+ for (const key of keys) {
854
+ if (supportRank(this.capabilities[key]) < supportRank("acknowledged")) {
855
+ this.capabilities[key] = "acknowledged";
856
+ changed = true;
857
+ }
858
+ }
859
+ if (changed) this.emit("capabilitiesChanged", this.getCapabilities());
860
+ }
861
+ observe(key) {
862
+ if (this.capabilities[key] === "observed") return;
863
+ this.capabilities[key] = "observed";
864
+ this.emit("capabilitiesChanged", this.getCapabilities());
865
+ }
866
+ flushRx(key, pending) {
867
+ clearTimeout(pending.timer);
868
+ if (this.pendingRx.get(key) === pending) this.pendingRx.delete(key);
869
+ this.emit("rxFrame", pending.frame);
870
+ }
871
+ clearPendingRx() {
872
+ for (const pending of this.pendingRx.values()) clearTimeout(pending.timer);
873
+ this.pendingRx.clear();
874
+ }
875
+ };
876
+ function rxPriority(frame) {
877
+ if (frame.source === "rx_channel_sensors_ex") return 3;
878
+ if (frame.source === "rx_channel_sensors") return 2;
879
+ return 1;
880
+ }
881
+ function supportRank(value) {
882
+ switch (value) {
883
+ case "unsupported":
884
+ return -1;
885
+ case "unknown":
886
+ return 0;
887
+ case "declared":
888
+ return 1;
889
+ case "acknowledged":
890
+ return 2;
891
+ case "observed":
892
+ return 3;
893
+ }
894
+ }
895
+
600
896
  // src/dialect/builtins.ts
601
897
  var StandardTciDialect = class {
602
898
  id;
@@ -606,6 +902,7 @@ var StandardTciDialect = class {
606
902
  supportsTxAudioSource;
607
903
  supportsIqStream;
608
904
  iqSampleRates;
905
+ meterAdapter;
609
906
  driveHasTrx;
610
907
  detector;
611
908
  resolver;
@@ -617,6 +914,7 @@ var StandardTciDialect = class {
617
914
  this.supportsTxAudioSource = options.supportsTxAudioSource;
618
915
  this.supportsIqStream = options.supportsIqStream;
619
916
  this.iqSampleRates = [...options.iqSampleRates];
917
+ this.meterAdapter = options.meterAdapter;
620
918
  this.driveHasTrx = options.driveHasTrx;
621
919
  this.detector = options.detect;
622
920
  this.resolver = options.resolve;
@@ -667,6 +965,7 @@ var expertSdr14Dialect = new StandardTciDialect({
667
965
  supportsIqStream: true,
668
966
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
669
967
  driveHasTrx: false,
968
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
670
969
  detect: (context) => {
671
970
  const parsed = version(context);
672
971
  if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
@@ -682,6 +981,7 @@ var expertSdrLegacyDialect = new StandardTciDialect({
682
981
  supportsIqStream: true,
683
982
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
684
983
  driveHasTrx: true,
984
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
685
985
  detect: (context) => {
686
986
  const parsed = version(context);
687
987
  if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
@@ -697,6 +997,7 @@ var expertSdrModernDialect = new StandardTciDialect({
697
997
  supportsIqStream: true,
698
998
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
699
999
  driveHasTrx: true,
1000
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
700
1001
  detect: (context) => {
701
1002
  const parsed = version(context);
702
1003
  if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
@@ -717,6 +1018,10 @@ var thetisDialect = new StandardTciDialect({
717
1018
  supportsIqStream: true,
718
1019
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
719
1020
  driveHasTrx: true,
1021
+ meterAdapter: new StandardTciMeterAdapter({
1022
+ interval: { minMs: 30, maxMs: 1e3 },
1023
+ supportsRxExtended: true
1024
+ }),
720
1025
  detect: (context) => {
721
1026
  const evidence = [];
722
1027
  let score = 0;
@@ -745,6 +1050,10 @@ var aetherSdrDialect = new StandardTciDialect({
745
1050
  supportsIqStream: true,
746
1051
  iqSampleRates: [24e3, 48e3, 96e3, 192e3],
747
1052
  driveHasTrx: true,
1053
+ meterAdapter: new StandardTciMeterAdapter({
1054
+ interval: { fixedMs: 200 },
1055
+ txAlcUnit: "dbfs"
1056
+ }),
748
1057
  detect: (context) => {
749
1058
  if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
750
1059
  const evidence = [`AetherSDR device identity: ${context.identity.device}`];
@@ -766,6 +1075,7 @@ var genericObservedDialect = new StandardTciDialect({
766
1075
  supportsIqStream: false,
767
1076
  iqSampleRates: [],
768
1077
  driveHasTrx: true,
1078
+ meterAdapter: createUnknownTciMeterAdapter(),
769
1079
  detect: (context) => ({
770
1080
  score: context.commandNames.has("ready") ? 10 : 0,
771
1081
  evidence: ["No vendor-specific match; using observed command shapes"],
@@ -783,6 +1093,7 @@ var genericObservedDialect = new StandardTciDialect({
783
1093
  supportsIqStream: context.commandNames.has("iq_samplerate"),
784
1094
  iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
785
1095
  driveHasTrx,
1096
+ meterAdapter: createUnknownTciMeterAdapter(),
786
1097
  detect: genericObservedDialect.detect.bind(genericObservedDialect)
787
1098
  });
788
1099
  }
@@ -881,9 +1192,9 @@ function assertValidTciHandshake(commands) {
881
1192
  }
882
1193
 
883
1194
  // src/transport/WebSocketTransport.ts
884
- import { EventEmitter } from "eventemitter3";
1195
+ import { EventEmitter as EventEmitter2 } from "eventemitter3";
885
1196
  import WebSocket from "ws";
886
- var WebSocketTciTransport = class extends EventEmitter {
1197
+ var WebSocketTciTransport = class extends EventEmitter2 {
887
1198
  constructor(url, WebSocketImpl = WebSocket) {
888
1199
  super();
889
1200
  this.url = url;
@@ -993,7 +1304,7 @@ function dataToBuffer(data) {
993
1304
  }
994
1305
 
995
1306
  // src/client/TciClient.ts
996
- var TciIqStreamSession = class extends EventEmitter2 {
1307
+ var TciIqStreamSession = class extends EventEmitter3 {
997
1308
  receiver;
998
1309
  _appliedSampleRate;
999
1310
  callbacks;
@@ -1064,7 +1375,7 @@ var TciIqStreamSession = class extends EventEmitter2 {
1064
1375
  firstFrame.reject(error);
1065
1376
  }
1066
1377
  };
1067
- var TciClient = class extends EventEmitter2 {
1378
+ var TciClient = class extends EventEmitter3 {
1068
1379
  options;
1069
1380
  WebSocketImpl;
1070
1381
  transportFactory;
@@ -1079,6 +1390,7 @@ var TciClient = class extends EventEmitter2 {
1079
1390
  initializationCommands = [];
1080
1391
  handshakeWaiter;
1081
1392
  activeIqSession;
1393
+ activeMeterSession;
1082
1394
  constructor(options) {
1083
1395
  super();
1084
1396
  this.options = {
@@ -1144,6 +1456,7 @@ var TciClient = class extends EventEmitter2 {
1144
1456
  }
1145
1457
  }
1146
1458
  async disconnect(code = 1e3, reason = "client disconnect") {
1459
+ await this.activeMeterSession?.close().catch(() => void 0);
1147
1460
  const transport = this.transport;
1148
1461
  if (!transport) return;
1149
1462
  await transport.disconnect(code, reason);
@@ -1166,6 +1479,71 @@ var TciClient = class extends EventEmitter2 {
1166
1479
  supportedSampleRates: [...dialect.iqSampleRates]
1167
1480
  };
1168
1481
  }
1482
+ getMeterCapabilities() {
1483
+ if (this.activeMeterSession) return this.activeMeterSession.getCapabilities();
1484
+ const adapter = this.activeDialect?.meterAdapter;
1485
+ return adapter ? cloneMeterCapabilities(adapter.declaredCapabilities) : cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES);
1486
+ }
1487
+ async openMeterStream(options = {}) {
1488
+ if (this.activeMeterSession) throw new TciError("protocol-error", "This TCI client already has an active meter stream session");
1489
+ this.requireDialect();
1490
+ const receiver = normalizeNonNegativeInteger(options.receiver ?? this.options.receiver, "meter receiver");
1491
+ const channel = normalizeNonNegativeInteger(options.channel ?? this.options.vfo, "meter channel");
1492
+ const trx = normalizeNonNegativeInteger(options.trx ?? this.options.trx, "meter transmitter");
1493
+ const rxEnabled = options.rx ?? true;
1494
+ const txEnabled = options.tx ?? true;
1495
+ if (!rxEnabled && !txEnabled) throw new TciError("protocol-error", "TCI meter stream must enable RX, TX, or both");
1496
+ const requestedIntervalMs = Math.round(options.intervalMs ?? 300);
1497
+ if (!Number.isFinite(requestedIntervalMs) || requestedIntervalMs <= 0) {
1498
+ throw new TciError("protocol-error", `Invalid TCI meter interval: ${options.intervalMs}`);
1499
+ }
1500
+ const adapter = this.activeDialect?.meterAdapter ?? createUnknownTciMeterAdapter();
1501
+ const interval = adapter.normalizeInterval(requestedIntervalMs);
1502
+ let session;
1503
+ session = new TciMeterStreamSession({
1504
+ receiver,
1505
+ channel,
1506
+ trx,
1507
+ requestedIntervalMs: interval.requestedMs,
1508
+ appliedIntervalMs: interval.appliedMs,
1509
+ rxEnabled,
1510
+ txEnabled,
1511
+ adapter,
1512
+ callbacks: {
1513
+ close: async () => {
1514
+ try {
1515
+ if (this.isConnected()) {
1516
+ if (rxEnabled) {
1517
+ const command = adapter.buildEnableCommand("rx", false, interval.appliedMs ?? interval.requestedMs);
1518
+ await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
1519
+ }
1520
+ if (txEnabled) {
1521
+ const command = adapter.buildEnableCommand("tx", false, interval.appliedMs ?? interval.requestedMs);
1522
+ await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
1523
+ }
1524
+ }
1525
+ } finally {
1526
+ if (this.activeMeterSession === session) this.activeMeterSession = void 0;
1527
+ }
1528
+ }
1529
+ }
1530
+ });
1531
+ this.activeMeterSession = session;
1532
+ try {
1533
+ if (rxEnabled) {
1534
+ const command = adapter.buildEnableCommand("rx", true, interval.appliedMs ?? interval.requestedMs);
1535
+ await this.sendCommand(command.name, command.args, { waitForReply: false });
1536
+ }
1537
+ if (txEnabled) {
1538
+ const command = adapter.buildEnableCommand("tx", true, interval.appliedMs ?? interval.requestedMs);
1539
+ await this.sendCommand(command.name, command.args, { waitForReply: false });
1540
+ }
1541
+ return session;
1542
+ } catch (error) {
1543
+ await session.close().catch(() => void 0);
1544
+ throw error;
1545
+ }
1546
+ }
1169
1547
  async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
1170
1548
  const capabilities = this.getIqCapabilities();
1171
1549
  const requested = Math.round(sampleRate);
@@ -1690,11 +2068,13 @@ var TciClient = class extends EventEmitter2 {
1690
2068
  handleText(raw) {
1691
2069
  try {
1692
2070
  const commands = parseTciText(raw);
2071
+ const receivedAtMs = Date.now();
1693
2072
  this.emit("tci:rx", raw, commands);
1694
2073
  for (const command of commands) {
1695
2074
  if (!this.handshakeResult) this.initializationCommands.push(command);
1696
2075
  this.queue.handleCommand(command);
1697
2076
  this.applyCommand(command);
2077
+ this.activeMeterSession?._acceptCommand(command, receivedAtMs);
1698
2078
  this.emit("command", command);
1699
2079
  }
1700
2080
  } catch (error) {
@@ -1814,6 +2194,7 @@ var TciClient = class extends EventEmitter2 {
1814
2194
  reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
1815
2195
  reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
1816
2196
  reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
2197
+ reducers.set("rx_channel_sensors_ex", (args) => this.applyRxChannelSensors(args));
1817
2198
  reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
1818
2199
  reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
1819
2200
  reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
@@ -1941,6 +2322,8 @@ var TciClient = class extends EventEmitter2 {
1941
2322
  this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
1942
2323
  this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
1943
2324
  this.activeIqSession = void 0;
2325
+ this.activeMeterSession?._fail(new TciError("disconnected", "TCI connection closed while meter stream was active", reason));
2326
+ this.activeMeterSession = void 0;
1944
2327
  if (wasConnected) {
1945
2328
  this.emit("disconnected", reason);
1946
2329
  this.emitState();
@@ -1967,6 +2350,12 @@ function parseNumber(value) {
1967
2350
  const number = Number(value);
1968
2351
  return Number.isFinite(number) ? number : void 0;
1969
2352
  }
2353
+ function normalizeNonNegativeInteger(value, description) {
2354
+ if (!Number.isInteger(value) || value < 0) {
2355
+ throw new TciError("protocol-error", `Invalid TCI ${description}: ${value}`);
2356
+ }
2357
+ return value;
2358
+ }
1970
2359
  function parseBoolean(value) {
1971
2360
  if (value === void 0) {
1972
2361
  return void 0;
@@ -2042,23 +2431,28 @@ function cloneNested(value) {
2042
2431
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, { ...item }]));
2043
2432
  }
2044
2433
  export {
2434
+ StandardTciMeterAdapter,
2045
2435
  TCI_STREAM_HEADER_BYTES,
2046
2436
  TciClient,
2047
2437
  TciCommandQueue,
2048
2438
  TciDialectRegistry,
2049
2439
  TciError,
2050
2440
  TciIqStreamSession,
2441
+ TciMeterStreamSession,
2051
2442
  TciSampleType,
2052
2443
  TciStreamType,
2444
+ UNKNOWN_TCI_METER_CAPABILITIES,
2053
2445
  WebSocketTciTransport,
2054
2446
  aetherSdrDialect,
2055
2447
  assertValidTciHandshake,
2056
2448
  buildStreamFrame,
2057
2449
  buildTxAudioFrame,
2058
2450
  builtInDialects,
2451
+ cloneMeterCapabilities,
2059
2452
  commandKey,
2060
2453
  compareTciVersion,
2061
2454
  createTciClient,
2455
+ createUnknownTciMeterAdapter,
2062
2456
  decodeInterleavedIq,
2063
2457
  defaultTciDialectRegistry,
2064
2458
  deinterleaveChannels,