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.
- package/CHANGELOG.md +9 -0
- package/README.md +24 -0
- package/dist/audio/index.d.cts +2 -1
- package/dist/audio/index.d.ts +2 -1
- package/dist/dialect/index.cjs +175 -12
- package/dist/dialect/index.cjs.map +1 -1
- package/dist/dialect/index.d.cts +3 -2
- package/dist/dialect/index.d.ts +3 -2
- package/dist/dialect/index.js +175 -12
- package/dist/dialect/index.js.map +1 -1
- package/dist/errors-CT3b8LLw.d.cts +9 -0
- package/dist/errors-CT3b8LLw.d.ts +9 -0
- package/dist/index.cjs +404 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +399 -5
- package/dist/index.js.map +1 -1
- package/dist/meter/index.cjs +348 -0
- package/dist/meter/index.cjs.map +1 -0
- package/dist/meter/index.d.cts +78 -0
- package/dist/meter/index.d.ts +78 -0
- package/dist/meter/index.js +317 -0
- package/dist/meter/index.js.map +1 -0
- package/dist/protocol/index.d.cts +36 -2
- package/dist/protocol/index.d.ts +36 -2
- package/dist/testing/index.cjs +32 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +19 -1
- package/dist/testing/index.d.ts +19 -1
- package/dist/testing/index.js +32 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/types-C0qVJVSO.d.ts +74 -0
- package/dist/types-C4MtLKoy.d.cts +74 -0
- package/dist/{types-1YXGwrgI.d.cts → types-CYK_NOTz.d.cts} +2 -0
- package/dist/{types-BtPh4Dbd.d.ts → types-DVIPU-VR.d.ts} +2 -0
- package/package.json +11 -2
- package/dist/index-lbK6NGY4.d.cts +0 -42
- 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
|
|
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
|
|
1280
|
+
var import_eventemitter32 = require("eventemitter3");
|
|
965
1281
|
var import_ws = __toESM(require("ws"), 1);
|
|
966
|
-
var WebSocketTciTransport = class extends
|
|
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
|
|
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
|
|
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,
|