tci-client-node 0.2.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 (43) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +50 -6
  3. package/dist/audio/index.cjs +11 -0
  4. package/dist/audio/index.cjs.map +1 -1
  5. package/dist/audio/index.d.cts +4 -2
  6. package/dist/audio/index.d.ts +4 -2
  7. package/dist/audio/index.js +10 -0
  8. package/dist/audio/index.js.map +1 -1
  9. package/dist/dialect/index.cjs +193 -12
  10. package/dist/dialect/index.cjs.map +1 -1
  11. package/dist/dialect/index.d.cts +3 -2
  12. package/dist/dialect/index.d.ts +3 -2
  13. package/dist/dialect/index.js +193 -12
  14. package/dist/dialect/index.js.map +1 -1
  15. package/dist/errors-CT3b8LLw.d.cts +9 -0
  16. package/dist/errors-CT3b8LLw.d.ts +9 -0
  17. package/dist/index.cjs +606 -6
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +68 -6
  20. package/dist/index.d.ts +68 -6
  21. package/dist/index.js +599 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/meter/index.cjs +348 -0
  24. package/dist/meter/index.cjs.map +1 -0
  25. package/dist/meter/index.d.cts +78 -0
  26. package/dist/meter/index.d.ts +78 -0
  27. package/dist/meter/index.js +317 -0
  28. package/dist/meter/index.js.map +1 -0
  29. package/dist/protocol/index.d.cts +36 -2
  30. package/dist/protocol/index.d.ts +36 -2
  31. package/dist/testing/index.cjs +58 -0
  32. package/dist/testing/index.cjs.map +1 -1
  33. package/dist/testing/index.d.cts +23 -1
  34. package/dist/testing/index.d.ts +23 -1
  35. package/dist/testing/index.js +58 -0
  36. package/dist/testing/index.js.map +1 -1
  37. package/dist/types-C0qVJVSO.d.ts +74 -0
  38. package/dist/types-C4MtLKoy.d.cts +74 -0
  39. package/dist/{types-xRotf9gW.d.cts → types-CYK_NOTz.d.cts} +5 -1
  40. package/dist/{types-9seY9Th-.d.ts → types-DVIPU-VR.d.ts} +5 -1
  41. package/package.json +11 -2
  42. package/dist/index-lbK6NGY4.d.cts +0 -42
  43. package/dist/index-paj1AOJY.d.ts +0 -42
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
@@ -241,6 +241,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
241
241
  }
242
242
  return output;
243
243
  }
244
+ function decodeInterleavedIq(frame) {
245
+ if (frame.streamType !== 0 /* IQ_STREAM */) {
246
+ throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
247
+ }
248
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
249
+ throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
250
+ }
251
+ return payloadToFloat32(frame);
252
+ }
244
253
  function samplesToPayload(samples, sampleType) {
245
254
  const type = normalizeSampleType(sampleType);
246
255
  const bytes = sampleTypeBytes(type);
@@ -588,6 +597,302 @@ function ensureSemicolon(command) {
588
597
  return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
589
598
  }
590
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
+
591
896
  // src/dialect/builtins.ts
592
897
  var StandardTciDialect = class {
593
898
  id;
@@ -595,6 +900,9 @@ var StandardTciDialect = class {
595
900
  streamLengthSemantics;
596
901
  supportsStreamChannels;
597
902
  supportsTxAudioSource;
903
+ supportsIqStream;
904
+ iqSampleRates;
905
+ meterAdapter;
598
906
  driveHasTrx;
599
907
  detector;
600
908
  resolver;
@@ -604,6 +912,9 @@ var StandardTciDialect = class {
604
912
  this.streamLengthSemantics = options.streamLengthSemantics;
605
913
  this.supportsStreamChannels = options.supportsStreamChannels;
606
914
  this.supportsTxAudioSource = options.supportsTxAudioSource;
915
+ this.supportsIqStream = options.supportsIqStream;
916
+ this.iqSampleRates = [...options.iqSampleRates];
917
+ this.meterAdapter = options.meterAdapter;
607
918
  this.driveHasTrx = options.driveHasTrx;
608
919
  this.detector = options.detect;
609
920
  this.resolver = options.resolve;
@@ -651,7 +962,10 @@ var expertSdr14Dialect = new StandardTciDialect({
651
962
  streamLengthSemantics: "per-channel",
652
963
  supportsStreamChannels: false,
653
964
  supportsTxAudioSource: false,
965
+ supportsIqStream: true,
966
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
654
967
  driveHasTrx: false,
968
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
655
969
  detect: (context) => {
656
970
  const parsed = version(context);
657
971
  if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
@@ -664,7 +978,10 @@ var expertSdrLegacyDialect = new StandardTciDialect({
664
978
  streamLengthSemantics: "per-channel",
665
979
  supportsStreamChannels: false,
666
980
  supportsTxAudioSource: false,
981
+ supportsIqStream: true,
982
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
667
983
  driveHasTrx: true,
984
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
668
985
  detect: (context) => {
669
986
  const parsed = version(context);
670
987
  if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
@@ -677,7 +994,10 @@ var expertSdrModernDialect = new StandardTciDialect({
677
994
  streamLengthSemantics: "scalar",
678
995
  supportsStreamChannels: true,
679
996
  supportsTxAudioSource: true,
997
+ supportsIqStream: true,
998
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
680
999
  driveHasTrx: true,
1000
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
681
1001
  detect: (context) => {
682
1002
  const parsed = version(context);
683
1003
  if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
@@ -695,7 +1015,13 @@ var thetisDialect = new StandardTciDialect({
695
1015
  streamLengthSemantics: "scalar",
696
1016
  supportsStreamChannels: true,
697
1017
  supportsTxAudioSource: true,
1018
+ supportsIqStream: true,
1019
+ iqSampleRates: [48e3, 96e3, 192e3, 384e3],
698
1020
  driveHasTrx: true,
1021
+ meterAdapter: new StandardTciMeterAdapter({
1022
+ interval: { minMs: 30, maxMs: 1e3 },
1023
+ supportsRxExtended: true
1024
+ }),
699
1025
  detect: (context) => {
700
1026
  const evidence = [];
701
1027
  let score = 0;
@@ -721,7 +1047,13 @@ var aetherSdrDialect = new StandardTciDialect({
721
1047
  streamLengthSemantics: "scalar",
722
1048
  supportsStreamChannels: true,
723
1049
  supportsTxAudioSource: true,
1050
+ supportsIqStream: true,
1051
+ iqSampleRates: [24e3, 48e3, 96e3, 192e3],
724
1052
  driveHasTrx: true,
1053
+ meterAdapter: new StandardTciMeterAdapter({
1054
+ interval: { fixedMs: 200 },
1055
+ txAlcUnit: "dbfs"
1056
+ }),
725
1057
  detect: (context) => {
726
1058
  if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
727
1059
  const evidence = [`AetherSDR device identity: ${context.identity.device}`];
@@ -740,7 +1072,10 @@ var genericObservedDialect = new StandardTciDialect({
740
1072
  streamLengthSemantics: "auto",
741
1073
  supportsStreamChannels: true,
742
1074
  supportsTxAudioSource: true,
1075
+ supportsIqStream: false,
1076
+ iqSampleRates: [],
743
1077
  driveHasTrx: true,
1078
+ meterAdapter: createUnknownTciMeterAdapter(),
744
1079
  detect: (context) => ({
745
1080
  score: context.commandNames.has("ready") ? 10 : 0,
746
1081
  evidence: ["No vendor-specific match; using observed command shapes"],
@@ -755,7 +1090,10 @@ var genericObservedDialect = new StandardTciDialect({
755
1090
  streamLengthSemantics: "auto",
756
1091
  supportsStreamChannels: context.commandNames.has("audio_stream_channels"),
757
1092
  supportsTxAudioSource: compareTciVersion(version(context) ?? [0], [2, 0]) >= 0,
1093
+ supportsIqStream: context.commandNames.has("iq_samplerate"),
1094
+ iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
758
1095
  driveHasTrx,
1096
+ meterAdapter: createUnknownTciMeterAdapter(),
759
1097
  detect: genericObservedDialect.detect.bind(genericObservedDialect)
760
1098
  });
761
1099
  }
@@ -854,9 +1192,9 @@ function assertValidTciHandshake(commands) {
854
1192
  }
855
1193
 
856
1194
  // src/transport/WebSocketTransport.ts
857
- import { EventEmitter } from "eventemitter3";
1195
+ import { EventEmitter as EventEmitter2 } from "eventemitter3";
858
1196
  import WebSocket from "ws";
859
- var WebSocketTciTransport = class extends EventEmitter {
1197
+ var WebSocketTciTransport = class extends EventEmitter2 {
860
1198
  constructor(url, WebSocketImpl = WebSocket) {
861
1199
  super();
862
1200
  this.url = url;
@@ -966,7 +1304,78 @@ function dataToBuffer(data) {
966
1304
  }
967
1305
 
968
1306
  // src/client/TciClient.ts
969
- var TciClient = class extends EventEmitter2 {
1307
+ var TciIqStreamSession = class extends EventEmitter3 {
1308
+ receiver;
1309
+ _appliedSampleRate;
1310
+ callbacks;
1311
+ closed = false;
1312
+ firstFrame;
1313
+ constructor(receiver, appliedSampleRate, callbacks) {
1314
+ super();
1315
+ this.receiver = receiver;
1316
+ this._appliedSampleRate = appliedSampleRate;
1317
+ this.callbacks = callbacks;
1318
+ }
1319
+ get appliedSampleRate() {
1320
+ return this._appliedSampleRate;
1321
+ }
1322
+ async setSampleRate(sampleRate) {
1323
+ if (this.closed) throw new TciError("cancelled", "TCI IQ stream is closed");
1324
+ const result = await this.callbacks.setSampleRate(sampleRate);
1325
+ this._appliedSampleRate = result.applied;
1326
+ return result;
1327
+ }
1328
+ async close() {
1329
+ if (this.closed) return;
1330
+ this.closed = true;
1331
+ this.rejectFirstFrame(new TciError("cancelled", "TCI IQ stream closed before its first frame"));
1332
+ try {
1333
+ await this.callbacks.close();
1334
+ } finally {
1335
+ this.emit("closed");
1336
+ this.removeAllListeners();
1337
+ }
1338
+ }
1339
+ waitForFirstFrame(timeoutMs) {
1340
+ if (this.firstFrame) return this.firstFrame.promise;
1341
+ let resolvePromise;
1342
+ let rejectPromise;
1343
+ const promise = new Promise((resolve, reject) => {
1344
+ resolvePromise = resolve;
1345
+ rejectPromise = reject;
1346
+ });
1347
+ const timer = setTimeout(() => {
1348
+ this.firstFrame = void 0;
1349
+ rejectPromise(new TciError("command-timeout", `Timed out waiting for TCI IQ frame for receiver ${this.receiver}`));
1350
+ }, timeoutMs);
1351
+ this.firstFrame = { promise, resolve: resolvePromise, reject: rejectPromise, timer };
1352
+ return promise;
1353
+ }
1354
+ _acceptFrame(frame) {
1355
+ if (this.closed || frame.receiver !== this.receiver) return;
1356
+ this._appliedSampleRate = frame.sampleRate;
1357
+ const firstFrame = this.firstFrame;
1358
+ if (firstFrame) {
1359
+ clearTimeout(firstFrame.timer);
1360
+ this.firstFrame = void 0;
1361
+ firstFrame.resolve(frame);
1362
+ }
1363
+ this.emit("frame", frame);
1364
+ }
1365
+ _fail(error) {
1366
+ if (this.closed) return;
1367
+ this.rejectFirstFrame(error);
1368
+ this.emit("error", error);
1369
+ }
1370
+ rejectFirstFrame(error) {
1371
+ const firstFrame = this.firstFrame;
1372
+ if (!firstFrame) return;
1373
+ clearTimeout(firstFrame.timer);
1374
+ this.firstFrame = void 0;
1375
+ firstFrame.reject(error);
1376
+ }
1377
+ };
1378
+ var TciClient = class extends EventEmitter3 {
970
1379
  options;
971
1380
  WebSocketImpl;
972
1381
  transportFactory;
@@ -980,6 +1389,8 @@ var TciClient = class extends EventEmitter2 {
980
1389
  handshakeError;
981
1390
  initializationCommands = [];
982
1391
  handshakeWaiter;
1392
+ activeIqSession;
1393
+ activeMeterSession;
983
1394
  constructor(options) {
984
1395
  super();
985
1396
  this.options = {
@@ -1018,7 +1429,9 @@ var TciClient = class extends EventEmitter2 {
1018
1429
  split: {},
1019
1430
  dialectWarnings: [],
1020
1431
  rxSensors: {},
1021
- txSensors: {}
1432
+ txSensors: {},
1433
+ iq: { activeReceivers: {} },
1434
+ dds: {}
1022
1435
  };
1023
1436
  this.stateReducers = this.createStateReducers();
1024
1437
  }
@@ -1043,6 +1456,7 @@ var TciClient = class extends EventEmitter2 {
1043
1456
  }
1044
1457
  }
1045
1458
  async disconnect(code = 1e3, reason = "client disconnect") {
1459
+ await this.activeMeterSession?.close().catch(() => void 0);
1046
1460
  const transport = this.transport;
1047
1461
  if (!transport) return;
1048
1462
  await transport.disconnect(code, reason);
@@ -1057,6 +1471,133 @@ var TciClient = class extends EventEmitter2 {
1057
1471
  getHandshakeResult() {
1058
1472
  return this.handshakeResult ? cloneHandshake(this.handshakeResult) : void 0;
1059
1473
  }
1474
+ getIqCapabilities() {
1475
+ const dialect = this.requireDialect();
1476
+ return {
1477
+ supported: dialect.supportsIqStream,
1478
+ currentSampleRate: this.state.iq.sampleRate,
1479
+ supportedSampleRates: [...dialect.iqSampleRates]
1480
+ };
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
+ }
1547
+ async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
1548
+ const capabilities = this.getIqCapabilities();
1549
+ const requested = Math.round(sampleRate);
1550
+ if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
1551
+ if (!Number.isFinite(requested) || requested <= 0) throw new TciError("protocol-error", `Invalid TCI IQ sample rate: ${sampleRate}`);
1552
+ if (capabilities.supportedSampleRates.length > 0 && !capabilities.supportedSampleRates.includes(requested)) {
1553
+ throw new TciError("protocol-error", `TCI IQ sample rate ${requested} is not supported by dialect ${this.requireDialect().id}`);
1554
+ }
1555
+ if (this.state.iq.sampleRate === requested) return writeResult(requested, requested, "state");
1556
+ const waiter = this.waitForCommand((command) => command.name === "iq_samplerate", timeoutMs, "IQ_SAMPLERATE readback");
1557
+ try {
1558
+ await this.sendCommand("IQ_SAMPLERATE", [requested], { waitForReply: false });
1559
+ const reply = await waiter.promise;
1560
+ const applied = parseNumber(reply.args[0]);
1561
+ if (applied === void 0 || applied <= 0) throw new TciError("protocol-error", `Invalid IQ_SAMPLERATE readback: ${reply.raw}`);
1562
+ return writeResult(requested, applied, "reply");
1563
+ } finally {
1564
+ waiter.cancel();
1565
+ }
1566
+ }
1567
+ async openIqStream(options = {}) {
1568
+ if (this.activeIqSession) throw new TciError("protocol-error", "This TCI client already has an active IQ stream session");
1569
+ const capabilities = this.getIqCapabilities();
1570
+ if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
1571
+ const receiver = Math.floor(options.receiver ?? this.options.receiver);
1572
+ if (!Number.isInteger(receiver) || receiver < 0) throw new TciError("protocol-error", `Invalid TCI IQ receiver: ${receiver}`);
1573
+ const requestedRate = options.sampleRate ?? capabilities.currentSampleRate ?? capabilities.supportedSampleRates[0] ?? 48e3;
1574
+ const rateResult = await this.setIqSampleRate(requestedRate);
1575
+ let session;
1576
+ session = new TciIqStreamSession(receiver, rateResult.applied, {
1577
+ setSampleRate: (rate) => this.setIqSampleRate(rate),
1578
+ close: async () => {
1579
+ try {
1580
+ if (this.isConnected()) await this.sendCommand("IQ_STOP", [receiver], { waitForReply: false });
1581
+ } finally {
1582
+ this.state.iq.activeReceivers[String(receiver)] = false;
1583
+ if (this.activeIqSession === session) this.activeIqSession = void 0;
1584
+ this.emitState();
1585
+ }
1586
+ }
1587
+ });
1588
+ this.activeIqSession = session;
1589
+ const firstFrame = session.waitForFirstFrame(options.firstFrameTimeoutMs ?? 5e3);
1590
+ try {
1591
+ await this.sendCommand("IQ_START", [receiver], { waitForReply: false });
1592
+ this.state.iq.activeReceivers[String(receiver)] = true;
1593
+ this.emitState();
1594
+ await firstFrame;
1595
+ return session;
1596
+ } catch (error) {
1597
+ await session.close().catch(() => void 0);
1598
+ throw error;
1599
+ }
1600
+ }
1060
1601
  async sendCommand(name, args = [], options = {}) {
1061
1602
  const raw = formatTciCommand(name, args);
1062
1603
  if (options.waitForReply === false) {
@@ -1527,11 +2068,13 @@ var TciClient = class extends EventEmitter2 {
1527
2068
  handleText(raw) {
1528
2069
  try {
1529
2070
  const commands = parseTciText(raw);
2071
+ const receivedAtMs = Date.now();
1530
2072
  this.emit("tci:rx", raw, commands);
1531
2073
  for (const command of commands) {
1532
2074
  if (!this.handshakeResult) this.initializationCommands.push(command);
1533
2075
  this.queue.handleCommand(command);
1534
2076
  this.applyCommand(command);
2077
+ this.activeMeterSession?._acceptCommand(command, receivedAtMs);
1535
2078
  this.emit("command", command);
1536
2079
  }
1537
2080
  } catch (error) {
@@ -1546,6 +2089,22 @@ var TciClient = class extends EventEmitter2 {
1546
2089
  this.emit("tci:binary", frame);
1547
2090
  this.emit("binary", frame);
1548
2091
  switch (frame.streamType) {
2092
+ case 0 /* IQ_STREAM */: {
2093
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
2094
+ throw new TciError("invalid-frame", "TCI IQ stream frame must contain interleaved I/Q pairs");
2095
+ }
2096
+ this.state.iq.sampleRate = frame.sampleRate;
2097
+ const iqFrame = {
2098
+ frame,
2099
+ receiver: frame.receiver,
2100
+ sampleRate: frame.sampleRate,
2101
+ centerFrequency: this.state.dds[String(frame.receiver)],
2102
+ complexSampleCount: frame.frameCount
2103
+ };
2104
+ this.emit("iqFrame", iqFrame);
2105
+ this.activeIqSession?._acceptFrame(iqFrame);
2106
+ break;
2107
+ }
1549
2108
  case 1 /* RX_AUDIO_STREAM */:
1550
2109
  this.emit("rxAudioFrame", frame);
1551
2110
  break;
@@ -1623,6 +2182,11 @@ var TciClient = class extends EventEmitter2 {
1623
2182
  this.state.modulations = args.map((mode) => mode.toLowerCase());
1624
2183
  });
1625
2184
  reducers.set("vfo", (args) => this.applyVfo(args));
2185
+ reducers.set("dds", (args) => {
2186
+ const receiver = parseNumber(args[0]);
2187
+ const frequency = parseNumber(args[1]);
2188
+ if (receiver !== void 0 && frequency !== void 0 && frequency >= 0) this.state.dds[String(receiver)] = frequency;
2189
+ });
1626
2190
  reducers.set("modulation", (args) => this.applyModulation(args));
1627
2191
  reducers.set("trx", (args) => this.applyTrx(args));
1628
2192
  reducers.set("tune", (args) => this.applyBooleanByFirstArg(this.state.tune, args));
@@ -1630,6 +2194,7 @@ var TciClient = class extends EventEmitter2 {
1630
2194
  reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
1631
2195
  reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
1632
2196
  reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
2197
+ reducers.set("rx_channel_sensors_ex", (args) => this.applyRxChannelSensors(args));
1633
2198
  reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
1634
2199
  reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
1635
2200
  reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
@@ -1639,6 +2204,15 @@ var TciClient = class extends EventEmitter2 {
1639
2204
  reducers.set("tx_stream_audio_buffering", (args) => this.updateAudioState({ txBufferingMs: parseNumber(args[0]) }));
1640
2205
  reducers.set("audio_start", () => this.updateAudioState({ running: true }));
1641
2206
  reducers.set("audio_stop", () => this.updateAudioState({ running: false }));
2207
+ reducers.set("iq_samplerate", (args) => {
2208
+ this.state.iq.sampleRate = parseNumber(args[0]);
2209
+ });
2210
+ reducers.set("iq_start", (args) => {
2211
+ this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = true;
2212
+ });
2213
+ reducers.set("iq_stop", (args) => {
2214
+ this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = false;
2215
+ });
1642
2216
  return reducers;
1643
2217
  }
1644
2218
  applyVfo(args) {
@@ -1746,6 +2320,10 @@ var TciClient = class extends EventEmitter2 {
1746
2320
  this.state.ready = false;
1747
2321
  this.queue.setConnected(false);
1748
2322
  this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
2323
+ this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
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;
1749
2327
  if (wasConnected) {
1750
2328
  this.emit("disconnected", reason);
1751
2329
  this.emitState();
@@ -1772,6 +2350,12 @@ function parseNumber(value) {
1772
2350
  const number = Number(value);
1773
2351
  return Number.isFinite(number) ? number : void 0;
1774
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
+ }
1775
2359
  function parseBoolean(value) {
1776
2360
  if (value === void 0) {
1777
2361
  return void 0;
@@ -1826,7 +2410,9 @@ function cloneState(state) {
1826
2410
  dialectWarnings: [...state.dialectWarnings],
1827
2411
  rxSensors: cloneNested(state.rxSensors),
1828
2412
  txSensors: cloneNested(state.txSensors),
1829
- audio: state.audio ? { ...state.audio } : void 0
2413
+ audio: state.audio ? { ...state.audio } : void 0,
2414
+ iq: { ...state.iq, activeReceivers: { ...state.iq.activeReceivers } },
2415
+ dds: { ...state.dds }
1830
2416
  };
1831
2417
  }
1832
2418
  function cloneHandshake(result) {
@@ -1845,22 +2431,29 @@ function cloneNested(value) {
1845
2431
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, { ...item }]));
1846
2432
  }
1847
2433
  export {
2434
+ StandardTciMeterAdapter,
1848
2435
  TCI_STREAM_HEADER_BYTES,
1849
2436
  TciClient,
1850
2437
  TciCommandQueue,
1851
2438
  TciDialectRegistry,
1852
2439
  TciError,
2440
+ TciIqStreamSession,
2441
+ TciMeterStreamSession,
1853
2442
  TciSampleType,
1854
2443
  TciStreamType,
2444
+ UNKNOWN_TCI_METER_CAPABILITIES,
1855
2445
  WebSocketTciTransport,
1856
2446
  aetherSdrDialect,
1857
2447
  assertValidTciHandshake,
1858
2448
  buildStreamFrame,
1859
2449
  buildTxAudioFrame,
1860
2450
  builtInDialects,
2451
+ cloneMeterCapabilities,
1861
2452
  commandKey,
1862
2453
  compareTciVersion,
1863
2454
  createTciClient,
2455
+ createUnknownTciMeterAdapter,
2456
+ decodeInterleavedIq,
1864
2457
  defaultTciDialectRegistry,
1865
2458
  deinterleaveChannels,
1866
2459
  escapeTciText,