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.cjs CHANGED
@@ -30,23 +30,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ StandardTciMeterAdapter: () => StandardTciMeterAdapter,
33
34
  TCI_STREAM_HEADER_BYTES: () => TCI_STREAM_HEADER_BYTES,
34
35
  TciClient: () => TciClient,
35
36
  TciCommandQueue: () => TciCommandQueue,
36
37
  TciDialectRegistry: () => TciDialectRegistry,
37
38
  TciError: () => TciError,
38
39
  TciIqStreamSession: () => TciIqStreamSession,
40
+ TciMeterStreamSession: () => TciMeterStreamSession,
39
41
  TciSampleType: () => TciSampleType,
40
42
  TciStreamType: () => TciStreamType,
43
+ UNKNOWN_TCI_METER_CAPABILITIES: () => UNKNOWN_TCI_METER_CAPABILITIES,
41
44
  WebSocketTciTransport: () => WebSocketTciTransport,
42
45
  aetherSdrDialect: () => aetherSdrDialect,
43
46
  assertValidTciHandshake: () => assertValidTciHandshake,
44
47
  buildStreamFrame: () => buildStreamFrame,
45
48
  buildTxAudioFrame: () => buildTxAudioFrame,
46
49
  builtInDialects: () => builtInDialects,
50
+ cloneMeterCapabilities: () => cloneMeterCapabilities,
47
51
  commandKey: () => commandKey,
48
52
  compareTciVersion: () => compareTciVersion,
49
53
  createTciClient: () => createTciClient,
54
+ createUnknownTciMeterAdapter: () => createUnknownTciMeterAdapter,
50
55
  decodeInterleavedIq: () => decodeInterleavedIq,
51
56
  defaultTciDialectRegistry: () => defaultTciDialectRegistry,
52
57
  deinterleaveChannels: () => deinterleaveChannels,
@@ -100,7 +105,7 @@ function toTciError(error, fallbackCode = "protocol-error") {
100
105
  }
101
106
 
102
107
  // src/client/TciClient.ts
103
- var import_eventemitter32 = require("eventemitter3");
108
+ var import_eventemitter33 = require("eventemitter3");
104
109
  var import_ws2 = __toESM(require("ws"), 1);
105
110
 
106
111
  // src/audio/streamFrame.ts
@@ -677,6 +682,302 @@ function ensureSemicolon(command) {
677
682
  return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
678
683
  }
679
684
 
685
+ // src/meter/types.ts
686
+ var UNKNOWN_TCI_METER_CAPABILITIES = {
687
+ rxLevel: "unknown",
688
+ rxAverageLevel: "unknown",
689
+ rxPeakBin: "unknown",
690
+ txMicLevel: "unknown",
691
+ txRmsPower: "unknown",
692
+ txPeakPower: "unknown",
693
+ txSwr: "unknown",
694
+ txAlcDbfs: "unknown"
695
+ };
696
+ function cloneMeterCapabilities(capabilities) {
697
+ return { ...capabilities };
698
+ }
699
+
700
+ // src/meter/adapters.ts
701
+ var StandardTciMeterAdapter = class {
702
+ declaredCapabilities;
703
+ options;
704
+ constructor(options = {}) {
705
+ this.options = options;
706
+ this.declaredCapabilities = {
707
+ ...UNKNOWN_TCI_METER_CAPABILITIES,
708
+ rxLevel: "declared",
709
+ txMicLevel: "declared",
710
+ txRmsPower: "declared",
711
+ txPeakPower: "declared",
712
+ txSwr: "declared",
713
+ rxAverageLevel: options.supportsRxExtended ? "declared" : "unknown",
714
+ rxPeakBin: options.supportsRxExtended ? "declared" : "unknown",
715
+ txAlcDbfs: options.txAlcUnit === "dbfs" ? "declared" : "unknown",
716
+ ...options.capabilities
717
+ };
718
+ }
719
+ normalizeInterval(intervalMs) {
720
+ const requestedMs = Math.round(intervalMs);
721
+ if (this.options.interval?.fixedMs !== void 0) {
722
+ return { requestedMs, appliedMs: this.options.interval.fixedMs };
723
+ }
724
+ const minMs = this.options.interval?.minMs ?? requestedMs;
725
+ const maxMs = this.options.interval?.maxMs ?? requestedMs;
726
+ const normalized = Math.max(minMs, Math.min(maxMs, requestedMs));
727
+ return {
728
+ requestedMs,
729
+ appliedMs: this.options.interval?.reportsApplied === false ? void 0 : normalized
730
+ };
731
+ }
732
+ buildEnableCommand(kind, enabled, intervalMs) {
733
+ return {
734
+ name: kind === "rx" ? "RX_SENSORS_ENABLE" : "TX_SENSORS_ENABLE",
735
+ args: enabled ? [true, intervalMs] : [false]
736
+ };
737
+ }
738
+ decode(command, receivedAtMs) {
739
+ switch (command.name) {
740
+ case "rx_sensors":
741
+ return decodeRx(command, receivedAtMs, "rx_sensors");
742
+ case "rx_channel_sensors":
743
+ return decodeRx(command, receivedAtMs, "rx_channel_sensors");
744
+ case "rx_channel_sensors_ex":
745
+ return decodeRx(command, receivedAtMs, "rx_channel_sensors_ex");
746
+ case "tx_sensors":
747
+ return decodeTx(command, receivedAtMs, this.options.txAlcUnit);
748
+ default:
749
+ return void 0;
750
+ }
751
+ }
752
+ };
753
+ function createUnknownTciMeterAdapter() {
754
+ return new StandardTciMeterAdapter({
755
+ capabilities: cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES),
756
+ interval: { reportsApplied: false }
757
+ });
758
+ }
759
+ function decodeRx(command, receivedAtMs, source) {
760
+ const receiver = integer(command.args[0]);
761
+ const hasChannel = source !== "rx_sensors";
762
+ const channel = hasChannel ? integer(command.args[1]) : 0;
763
+ const levelIndex = hasChannel ? 2 : 1;
764
+ const levelDbm = finite(command.args[levelIndex]);
765
+ if (receiver === void 0 || receiver < 0 || channel === void 0 || channel < 0 || levelDbm === void 0) {
766
+ return { issue: `Invalid ${command.originalName} meter frame: ${command.raw}` };
767
+ }
768
+ const frame = { receiver, channel, levelDbm, source, receivedAtMs };
769
+ if (source === "rx_channel_sensors_ex") {
770
+ const averageLevelDbm = finite(command.args[3]);
771
+ const peakBinDbm = finite(command.args[4]);
772
+ if (averageLevelDbm === void 0 || peakBinDbm === void 0) {
773
+ return { issue: `Invalid ${command.originalName} extended meter frame: ${command.raw}` };
774
+ }
775
+ frame.averageLevelDbm = averageLevelDbm;
776
+ frame.peakBinDbm = peakBinDbm;
777
+ if (command.args.length > 5) frame.extraArgs = command.args.slice(5);
778
+ } else if (command.args.length > levelIndex + 1) {
779
+ frame.extraArgs = command.args.slice(levelIndex + 1);
780
+ }
781
+ return { decoded: { kind: "rx", frame } };
782
+ }
783
+ function decodeTx(command, receivedAtMs, alcUnit) {
784
+ const trx = integer(command.args[0]);
785
+ if (trx === void 0 || trx < 0) {
786
+ return { issue: `Invalid ${command.originalName} transmitter index: ${command.raw}` };
787
+ }
788
+ const micLevelDbm = optionalFinite(command.args[1]);
789
+ const rmsPowerWatts = optionalFinite(command.args[2]);
790
+ const peakPowerWatts = optionalFinite(command.args[3]);
791
+ const swr = optionalFinite(command.args[4]);
792
+ const alcValue = alcUnit ? optionalFinite(command.args[5]) : void 0;
793
+ if (micLevelDbm.invalid || rmsPowerWatts.invalid || peakPowerWatts.invalid || swr.invalid || alcValue?.invalid) {
794
+ return { issue: `Invalid ${command.originalName} numeric meter frame: ${command.raw}` };
795
+ }
796
+ if (rmsPowerWatts.value !== void 0 && rmsPowerWatts.value < 0 || peakPowerWatts.value !== void 0 && peakPowerWatts.value < 0 || swr.value !== void 0 && swr.value < 1) {
797
+ return { issue: `Out-of-range ${command.originalName} meter frame: ${command.raw}` };
798
+ }
799
+ if (micLevelDbm.value === void 0 && rmsPowerWatts.value === void 0 && peakPowerWatts.value === void 0 && swr.value === void 0 && alcValue?.value === void 0) {
800
+ return { issue: `Empty ${command.originalName} meter frame: ${command.raw}` };
801
+ }
802
+ const frame = {
803
+ trx,
804
+ micLevelDbm: micLevelDbm.value,
805
+ rmsPowerWatts: rmsPowerWatts.value,
806
+ peakPowerWatts: peakPowerWatts.value,
807
+ swr: swr.value,
808
+ receivedAtMs
809
+ };
810
+ if (alcUnit && alcValue?.value !== void 0) frame.alc = { value: alcValue.value, unit: alcUnit };
811
+ const knownArgs = alcUnit ? 6 : 5;
812
+ if (command.args.length > knownArgs) frame.extraArgs = command.args.slice(knownArgs);
813
+ return { decoded: { kind: "tx", frame } };
814
+ }
815
+ function finite(value) {
816
+ if (value === void 0 || value === "") return void 0;
817
+ const parsed = Number(value);
818
+ return Number.isFinite(parsed) ? parsed : void 0;
819
+ }
820
+ function integer(value) {
821
+ const parsed = finite(value);
822
+ return parsed !== void 0 && Number.isInteger(parsed) ? parsed : void 0;
823
+ }
824
+ function optionalFinite(value) {
825
+ if (value === void 0 || value === "") return { invalid: false };
826
+ const parsed = Number(value);
827
+ return Number.isFinite(parsed) ? { value: parsed, invalid: false } : { invalid: true };
828
+ }
829
+
830
+ // src/meter/TciMeterStreamSession.ts
831
+ var import_eventemitter3 = require("eventemitter3");
832
+ var RX_COALESCE_MS = 20;
833
+ var TciMeterStreamSession = class extends import_eventemitter3.EventEmitter {
834
+ receiver;
835
+ channel;
836
+ trx;
837
+ requestedIntervalMs;
838
+ appliedIntervalMs;
839
+ rxEnabled;
840
+ txEnabled;
841
+ adapter;
842
+ callbacks;
843
+ capabilities;
844
+ pendingRx = /* @__PURE__ */ new Map();
845
+ closed = false;
846
+ constructor(options) {
847
+ super();
848
+ this.receiver = options.receiver;
849
+ this.channel = options.channel;
850
+ this.trx = options.trx;
851
+ this.requestedIntervalMs = options.requestedIntervalMs;
852
+ this.appliedIntervalMs = options.appliedIntervalMs;
853
+ this.rxEnabled = options.rxEnabled;
854
+ this.txEnabled = options.txEnabled;
855
+ this.adapter = options.adapter;
856
+ this.callbacks = options.callbacks;
857
+ this.capabilities = cloneMeterCapabilities(options.adapter.declaredCapabilities);
858
+ }
859
+ getCapabilities() {
860
+ return cloneMeterCapabilities(this.capabilities);
861
+ }
862
+ async close() {
863
+ if (this.closed) return;
864
+ this.closed = true;
865
+ this.clearPendingRx();
866
+ try {
867
+ await this.callbacks.close();
868
+ } finally {
869
+ this.emit("closed");
870
+ this.removeAllListeners();
871
+ }
872
+ }
873
+ _acceptCommand(command, receivedAtMs) {
874
+ if (this.closed) return;
875
+ this.acceptEnableAcknowledgement(command);
876
+ const result = this.adapter.decode(command, receivedAtMs);
877
+ if (!result) return;
878
+ if (result.issue) {
879
+ this.emit("error", new TciError("protocol-error", result.issue));
880
+ return;
881
+ }
882
+ if (result.decoded?.kind === "rx") this.acceptRxFrame(result.decoded.frame);
883
+ if (result.decoded?.kind === "tx") this.acceptTxFrame(result.decoded.frame);
884
+ }
885
+ _fail(error) {
886
+ if (this.closed) return;
887
+ this.closed = true;
888
+ this.clearPendingRx();
889
+ this.emit("error", error);
890
+ this.emit("closed");
891
+ this.removeAllListeners();
892
+ }
893
+ acceptRxFrame(frame) {
894
+ if (!this.rxEnabled || frame.receiver !== this.receiver || frame.channel !== this.channel) return;
895
+ this.observe("rxLevel");
896
+ if (frame.averageLevelDbm !== void 0) this.observe("rxAverageLevel");
897
+ if (frame.peakBinDbm !== void 0) this.observe("rxPeakBin");
898
+ const key = `${frame.receiver}:${frame.channel}`;
899
+ const pending = this.pendingRx.get(key);
900
+ if (pending) {
901
+ const sameReading = Math.abs(pending.frame.levelDbm - frame.levelDbm) < 0.05;
902
+ if (sameReading && rxPriority(frame) >= rxPriority(pending.frame)) {
903
+ clearTimeout(pending.timer);
904
+ this.pendingRx.delete(key);
905
+ } else if (sameReading) {
906
+ return;
907
+ } else {
908
+ this.flushRx(key, pending);
909
+ }
910
+ }
911
+ const timer = setTimeout(() => {
912
+ const current = this.pendingRx.get(key);
913
+ if (current?.frame === frame) this.flushRx(key, current);
914
+ }, RX_COALESCE_MS);
915
+ this.pendingRx.set(key, { frame, timer });
916
+ }
917
+ acceptTxFrame(frame) {
918
+ if (!this.txEnabled || frame.trx !== this.trx) return;
919
+ if (frame.micLevelDbm !== void 0) this.observe("txMicLevel");
920
+ if (frame.rmsPowerWatts !== void 0) this.observe("txRmsPower");
921
+ if (frame.peakPowerWatts !== void 0) this.observe("txPeakPower");
922
+ if (frame.swr !== void 0) this.observe("txSwr");
923
+ if (frame.alc?.unit === "dbfs") this.observe("txAlcDbfs");
924
+ this.emit("txFrame", frame);
925
+ }
926
+ acceptEnableAcknowledgement(command) {
927
+ const enabled = command.args[0]?.toLowerCase();
928
+ if (enabled !== "true" && enabled !== "1" && enabled !== "on") return;
929
+ if (command.name === "rx_sensors_enable" && this.rxEnabled) {
930
+ this.acknowledge(["rxLevel"]);
931
+ }
932
+ if (command.name === "tx_sensors_enable" && this.txEnabled) {
933
+ this.acknowledge(["txMicLevel", "txRmsPower", "txPeakPower", "txSwr"]);
934
+ }
935
+ }
936
+ acknowledge(keys) {
937
+ let changed = false;
938
+ for (const key of keys) {
939
+ if (supportRank(this.capabilities[key]) < supportRank("acknowledged")) {
940
+ this.capabilities[key] = "acknowledged";
941
+ changed = true;
942
+ }
943
+ }
944
+ if (changed) this.emit("capabilitiesChanged", this.getCapabilities());
945
+ }
946
+ observe(key) {
947
+ if (this.capabilities[key] === "observed") return;
948
+ this.capabilities[key] = "observed";
949
+ this.emit("capabilitiesChanged", this.getCapabilities());
950
+ }
951
+ flushRx(key, pending) {
952
+ clearTimeout(pending.timer);
953
+ if (this.pendingRx.get(key) === pending) this.pendingRx.delete(key);
954
+ this.emit("rxFrame", pending.frame);
955
+ }
956
+ clearPendingRx() {
957
+ for (const pending of this.pendingRx.values()) clearTimeout(pending.timer);
958
+ this.pendingRx.clear();
959
+ }
960
+ };
961
+ function rxPriority(frame) {
962
+ if (frame.source === "rx_channel_sensors_ex") return 3;
963
+ if (frame.source === "rx_channel_sensors") return 2;
964
+ return 1;
965
+ }
966
+ function supportRank(value) {
967
+ switch (value) {
968
+ case "unsupported":
969
+ return -1;
970
+ case "unknown":
971
+ return 0;
972
+ case "declared":
973
+ return 1;
974
+ case "acknowledged":
975
+ return 2;
976
+ case "observed":
977
+ return 3;
978
+ }
979
+ }
980
+
680
981
  // src/dialect/builtins.ts
681
982
  var StandardTciDialect = class {
682
983
  id;
@@ -686,6 +987,7 @@ var StandardTciDialect = class {
686
987
  supportsTxAudioSource;
687
988
  supportsIqStream;
688
989
  iqSampleRates;
990
+ meterAdapter;
689
991
  driveHasTrx;
690
992
  detector;
691
993
  resolver;
@@ -697,6 +999,7 @@ var StandardTciDialect = class {
697
999
  this.supportsTxAudioSource = options.supportsTxAudioSource;
698
1000
  this.supportsIqStream = options.supportsIqStream;
699
1001
  this.iqSampleRates = [...options.iqSampleRates];
1002
+ this.meterAdapter = options.meterAdapter;
700
1003
  this.driveHasTrx = options.driveHasTrx;
701
1004
  this.detector = options.detect;
702
1005
  this.resolver = options.resolve;
@@ -747,6 +1050,7 @@ var expertSdr14Dialect = new StandardTciDialect({
747
1050
  supportsIqStream: true,
748
1051
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
749
1052
  driveHasTrx: false,
1053
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
750
1054
  detect: (context) => {
751
1055
  const parsed = version(context);
752
1056
  if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
@@ -762,6 +1066,7 @@ var expertSdrLegacyDialect = new StandardTciDialect({
762
1066
  supportsIqStream: true,
763
1067
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
764
1068
  driveHasTrx: true,
1069
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
765
1070
  detect: (context) => {
766
1071
  const parsed = version(context);
767
1072
  if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
@@ -777,6 +1082,7 @@ var expertSdrModernDialect = new StandardTciDialect({
777
1082
  supportsIqStream: true,
778
1083
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
779
1084
  driveHasTrx: true,
1085
+ meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
780
1086
  detect: (context) => {
781
1087
  const parsed = version(context);
782
1088
  if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
@@ -797,6 +1103,10 @@ var thetisDialect = new StandardTciDialect({
797
1103
  supportsIqStream: true,
798
1104
  iqSampleRates: [48e3, 96e3, 192e3, 384e3],
799
1105
  driveHasTrx: true,
1106
+ meterAdapter: new StandardTciMeterAdapter({
1107
+ interval: { minMs: 30, maxMs: 1e3 },
1108
+ supportsRxExtended: true
1109
+ }),
800
1110
  detect: (context) => {
801
1111
  const evidence = [];
802
1112
  let score = 0;
@@ -825,6 +1135,10 @@ var aetherSdrDialect = new StandardTciDialect({
825
1135
  supportsIqStream: true,
826
1136
  iqSampleRates: [24e3, 48e3, 96e3, 192e3],
827
1137
  driveHasTrx: true,
1138
+ meterAdapter: new StandardTciMeterAdapter({
1139
+ interval: { fixedMs: 200 },
1140
+ txAlcUnit: "dbfs"
1141
+ }),
828
1142
  detect: (context) => {
829
1143
  if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
830
1144
  const evidence = [`AetherSDR device identity: ${context.identity.device}`];
@@ -846,6 +1160,7 @@ var genericObservedDialect = new StandardTciDialect({
846
1160
  supportsIqStream: false,
847
1161
  iqSampleRates: [],
848
1162
  driveHasTrx: true,
1163
+ meterAdapter: createUnknownTciMeterAdapter(),
849
1164
  detect: (context) => ({
850
1165
  score: context.commandNames.has("ready") ? 10 : 0,
851
1166
  evidence: ["No vendor-specific match; using observed command shapes"],
@@ -863,6 +1178,7 @@ var genericObservedDialect = new StandardTciDialect({
863
1178
  supportsIqStream: context.commandNames.has("iq_samplerate"),
864
1179
  iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
865
1180
  driveHasTrx,
1181
+ meterAdapter: createUnknownTciMeterAdapter(),
866
1182
  detect: genericObservedDialect.detect.bind(genericObservedDialect)
867
1183
  });
868
1184
  }
@@ -961,9 +1277,9 @@ function assertValidTciHandshake(commands) {
961
1277
  }
962
1278
 
963
1279
  // src/transport/WebSocketTransport.ts
964
- var import_eventemitter3 = require("eventemitter3");
1280
+ var import_eventemitter32 = require("eventemitter3");
965
1281
  var import_ws = __toESM(require("ws"), 1);
966
- var WebSocketTciTransport = class extends import_eventemitter3.EventEmitter {
1282
+ var WebSocketTciTransport = class extends import_eventemitter32.EventEmitter {
967
1283
  constructor(url, WebSocketImpl = import_ws.default) {
968
1284
  super();
969
1285
  this.url = url;
@@ -1073,7 +1389,7 @@ function dataToBuffer(data) {
1073
1389
  }
1074
1390
 
1075
1391
  // src/client/TciClient.ts
1076
- var TciIqStreamSession = class extends import_eventemitter32.EventEmitter {
1392
+ var TciIqStreamSession = class extends import_eventemitter33.EventEmitter {
1077
1393
  receiver;
1078
1394
  _appliedSampleRate;
1079
1395
  callbacks;
@@ -1144,7 +1460,7 @@ var TciIqStreamSession = class extends import_eventemitter32.EventEmitter {
1144
1460
  firstFrame.reject(error);
1145
1461
  }
1146
1462
  };
1147
- var TciClient = class extends import_eventemitter32.EventEmitter {
1463
+ var TciClient = class extends import_eventemitter33.EventEmitter {
1148
1464
  options;
1149
1465
  WebSocketImpl;
1150
1466
  transportFactory;
@@ -1159,6 +1475,7 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
1159
1475
  initializationCommands = [];
1160
1476
  handshakeWaiter;
1161
1477
  activeIqSession;
1478
+ activeMeterSession;
1162
1479
  constructor(options) {
1163
1480
  super();
1164
1481
  this.options = {
@@ -1224,6 +1541,7 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
1224
1541
  }
1225
1542
  }
1226
1543
  async disconnect(code = 1e3, reason = "client disconnect") {
1544
+ await this.activeMeterSession?.close().catch(() => void 0);
1227
1545
  const transport = this.transport;
1228
1546
  if (!transport) return;
1229
1547
  await transport.disconnect(code, reason);
@@ -1246,6 +1564,71 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
1246
1564
  supportedSampleRates: [...dialect.iqSampleRates]
1247
1565
  };
1248
1566
  }
1567
+ getMeterCapabilities() {
1568
+ if (this.activeMeterSession) return this.activeMeterSession.getCapabilities();
1569
+ const adapter = this.activeDialect?.meterAdapter;
1570
+ return adapter ? cloneMeterCapabilities(adapter.declaredCapabilities) : cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES);
1571
+ }
1572
+ async openMeterStream(options = {}) {
1573
+ if (this.activeMeterSession) throw new TciError("protocol-error", "This TCI client already has an active meter stream session");
1574
+ this.requireDialect();
1575
+ const receiver = normalizeNonNegativeInteger(options.receiver ?? this.options.receiver, "meter receiver");
1576
+ const channel = normalizeNonNegativeInteger(options.channel ?? this.options.vfo, "meter channel");
1577
+ const trx = normalizeNonNegativeInteger(options.trx ?? this.options.trx, "meter transmitter");
1578
+ const rxEnabled = options.rx ?? true;
1579
+ const txEnabled = options.tx ?? true;
1580
+ if (!rxEnabled && !txEnabled) throw new TciError("protocol-error", "TCI meter stream must enable RX, TX, or both");
1581
+ const requestedIntervalMs = Math.round(options.intervalMs ?? 300);
1582
+ if (!Number.isFinite(requestedIntervalMs) || requestedIntervalMs <= 0) {
1583
+ throw new TciError("protocol-error", `Invalid TCI meter interval: ${options.intervalMs}`);
1584
+ }
1585
+ const adapter = this.activeDialect?.meterAdapter ?? createUnknownTciMeterAdapter();
1586
+ const interval = adapter.normalizeInterval(requestedIntervalMs);
1587
+ let session;
1588
+ session = new TciMeterStreamSession({
1589
+ receiver,
1590
+ channel,
1591
+ trx,
1592
+ requestedIntervalMs: interval.requestedMs,
1593
+ appliedIntervalMs: interval.appliedMs,
1594
+ rxEnabled,
1595
+ txEnabled,
1596
+ adapter,
1597
+ callbacks: {
1598
+ close: async () => {
1599
+ try {
1600
+ if (this.isConnected()) {
1601
+ if (rxEnabled) {
1602
+ const command = adapter.buildEnableCommand("rx", false, interval.appliedMs ?? interval.requestedMs);
1603
+ await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
1604
+ }
1605
+ if (txEnabled) {
1606
+ const command = adapter.buildEnableCommand("tx", false, interval.appliedMs ?? interval.requestedMs);
1607
+ await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
1608
+ }
1609
+ }
1610
+ } finally {
1611
+ if (this.activeMeterSession === session) this.activeMeterSession = void 0;
1612
+ }
1613
+ }
1614
+ }
1615
+ });
1616
+ this.activeMeterSession = session;
1617
+ try {
1618
+ if (rxEnabled) {
1619
+ const command = adapter.buildEnableCommand("rx", true, interval.appliedMs ?? interval.requestedMs);
1620
+ await this.sendCommand(command.name, command.args, { waitForReply: false });
1621
+ }
1622
+ if (txEnabled) {
1623
+ const command = adapter.buildEnableCommand("tx", true, interval.appliedMs ?? interval.requestedMs);
1624
+ await this.sendCommand(command.name, command.args, { waitForReply: false });
1625
+ }
1626
+ return session;
1627
+ } catch (error) {
1628
+ await session.close().catch(() => void 0);
1629
+ throw error;
1630
+ }
1631
+ }
1249
1632
  async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
1250
1633
  const capabilities = this.getIqCapabilities();
1251
1634
  const requested = Math.round(sampleRate);
@@ -1770,11 +2153,13 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
1770
2153
  handleText(raw) {
1771
2154
  try {
1772
2155
  const commands = parseTciText(raw);
2156
+ const receivedAtMs = Date.now();
1773
2157
  this.emit("tci:rx", raw, commands);
1774
2158
  for (const command of commands) {
1775
2159
  if (!this.handshakeResult) this.initializationCommands.push(command);
1776
2160
  this.queue.handleCommand(command);
1777
2161
  this.applyCommand(command);
2162
+ this.activeMeterSession?._acceptCommand(command, receivedAtMs);
1778
2163
  this.emit("command", command);
1779
2164
  }
1780
2165
  } catch (error) {
@@ -1894,6 +2279,7 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
1894
2279
  reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
1895
2280
  reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
1896
2281
  reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
2282
+ reducers.set("rx_channel_sensors_ex", (args) => this.applyRxChannelSensors(args));
1897
2283
  reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
1898
2284
  reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
1899
2285
  reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
@@ -2021,6 +2407,8 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
2021
2407
  this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
2022
2408
  this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
2023
2409
  this.activeIqSession = void 0;
2410
+ this.activeMeterSession?._fail(new TciError("disconnected", "TCI connection closed while meter stream was active", reason));
2411
+ this.activeMeterSession = void 0;
2024
2412
  if (wasConnected) {
2025
2413
  this.emit("disconnected", reason);
2026
2414
  this.emitState();
@@ -2047,6 +2435,12 @@ function parseNumber(value) {
2047
2435
  const number = Number(value);
2048
2436
  return Number.isFinite(number) ? number : void 0;
2049
2437
  }
2438
+ function normalizeNonNegativeInteger(value, description) {
2439
+ if (!Number.isInteger(value) || value < 0) {
2440
+ throw new TciError("protocol-error", `Invalid TCI ${description}: ${value}`);
2441
+ }
2442
+ return value;
2443
+ }
2050
2444
  function parseBoolean(value) {
2051
2445
  if (value === void 0) {
2052
2446
  return void 0;
@@ -2123,23 +2517,28 @@ function cloneNested(value) {
2123
2517
  }
2124
2518
  // Annotate the CommonJS export names for ESM import in node:
2125
2519
  0 && (module.exports = {
2520
+ StandardTciMeterAdapter,
2126
2521
  TCI_STREAM_HEADER_BYTES,
2127
2522
  TciClient,
2128
2523
  TciCommandQueue,
2129
2524
  TciDialectRegistry,
2130
2525
  TciError,
2131
2526
  TciIqStreamSession,
2527
+ TciMeterStreamSession,
2132
2528
  TciSampleType,
2133
2529
  TciStreamType,
2530
+ UNKNOWN_TCI_METER_CAPABILITIES,
2134
2531
  WebSocketTciTransport,
2135
2532
  aetherSdrDialect,
2136
2533
  assertValidTciHandshake,
2137
2534
  buildStreamFrame,
2138
2535
  buildTxAudioFrame,
2139
2536
  builtInDialects,
2537
+ cloneMeterCapabilities,
2140
2538
  commandKey,
2141
2539
  compareTciVersion,
2142
2540
  createTciClient,
2541
+ createUnknownTciMeterAdapter,
2143
2542
  decodeInterleavedIq,
2144
2543
  defaultTciDialectRegistry,
2145
2544
  deinterleaveChannels,