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.
- package/CHANGELOG.md +17 -0
- package/README.md +50 -6
- package/dist/audio/index.cjs +11 -0
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +4 -2
- package/dist/audio/index.d.ts +4 -2
- package/dist/audio/index.js +10 -0
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +193 -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 +193 -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 +606 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -6
- package/dist/index.d.ts +68 -6
- package/dist/index.js +599 -6
- 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 +58 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +23 -1
- package/dist/testing/index.d.ts +23 -1
- package/dist/testing/index.js +58 -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-xRotf9gW.d.cts → types-CYK_NOTz.d.cts} +5 -1
- package/dist/{types-9seY9Th-.d.ts → types-DVIPU-VR.d.ts} +5 -1
- 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,22 +30,29 @@ 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,
|
|
39
|
+
TciIqStreamSession: () => TciIqStreamSession,
|
|
40
|
+
TciMeterStreamSession: () => TciMeterStreamSession,
|
|
38
41
|
TciSampleType: () => TciSampleType,
|
|
39
42
|
TciStreamType: () => TciStreamType,
|
|
43
|
+
UNKNOWN_TCI_METER_CAPABILITIES: () => UNKNOWN_TCI_METER_CAPABILITIES,
|
|
40
44
|
WebSocketTciTransport: () => WebSocketTciTransport,
|
|
41
45
|
aetherSdrDialect: () => aetherSdrDialect,
|
|
42
46
|
assertValidTciHandshake: () => assertValidTciHandshake,
|
|
43
47
|
buildStreamFrame: () => buildStreamFrame,
|
|
44
48
|
buildTxAudioFrame: () => buildTxAudioFrame,
|
|
45
49
|
builtInDialects: () => builtInDialects,
|
|
50
|
+
cloneMeterCapabilities: () => cloneMeterCapabilities,
|
|
46
51
|
commandKey: () => commandKey,
|
|
47
52
|
compareTciVersion: () => compareTciVersion,
|
|
48
53
|
createTciClient: () => createTciClient,
|
|
54
|
+
createUnknownTciMeterAdapter: () => createUnknownTciMeterAdapter,
|
|
55
|
+
decodeInterleavedIq: () => decodeInterleavedIq,
|
|
49
56
|
defaultTciDialectRegistry: () => defaultTciDialectRegistry,
|
|
50
57
|
deinterleaveChannels: () => deinterleaveChannels,
|
|
51
58
|
escapeTciText: () => escapeTciText,
|
|
@@ -98,7 +105,7 @@ function toTciError(error, fallbackCode = "protocol-error") {
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
// src/client/TciClient.ts
|
|
101
|
-
var
|
|
108
|
+
var import_eventemitter33 = require("eventemitter3");
|
|
102
109
|
var import_ws2 = __toESM(require("ws"), 1);
|
|
103
110
|
|
|
104
111
|
// src/audio/streamFrame.ts
|
|
@@ -319,6 +326,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
|
|
|
319
326
|
}
|
|
320
327
|
return output;
|
|
321
328
|
}
|
|
329
|
+
function decodeInterleavedIq(frame) {
|
|
330
|
+
if (frame.streamType !== 0 /* IQ_STREAM */) {
|
|
331
|
+
throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
|
|
332
|
+
}
|
|
333
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
334
|
+
throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
|
|
335
|
+
}
|
|
336
|
+
return payloadToFloat32(frame);
|
|
337
|
+
}
|
|
322
338
|
function samplesToPayload(samples, sampleType) {
|
|
323
339
|
const type = normalizeSampleType(sampleType);
|
|
324
340
|
const bytes = sampleTypeBytes(type);
|
|
@@ -666,6 +682,302 @@ function ensureSemicolon(command) {
|
|
|
666
682
|
return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
|
|
667
683
|
}
|
|
668
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
|
+
|
|
669
981
|
// src/dialect/builtins.ts
|
|
670
982
|
var StandardTciDialect = class {
|
|
671
983
|
id;
|
|
@@ -673,6 +985,9 @@ var StandardTciDialect = class {
|
|
|
673
985
|
streamLengthSemantics;
|
|
674
986
|
supportsStreamChannels;
|
|
675
987
|
supportsTxAudioSource;
|
|
988
|
+
supportsIqStream;
|
|
989
|
+
iqSampleRates;
|
|
990
|
+
meterAdapter;
|
|
676
991
|
driveHasTrx;
|
|
677
992
|
detector;
|
|
678
993
|
resolver;
|
|
@@ -682,6 +997,9 @@ var StandardTciDialect = class {
|
|
|
682
997
|
this.streamLengthSemantics = options.streamLengthSemantics;
|
|
683
998
|
this.supportsStreamChannels = options.supportsStreamChannels;
|
|
684
999
|
this.supportsTxAudioSource = options.supportsTxAudioSource;
|
|
1000
|
+
this.supportsIqStream = options.supportsIqStream;
|
|
1001
|
+
this.iqSampleRates = [...options.iqSampleRates];
|
|
1002
|
+
this.meterAdapter = options.meterAdapter;
|
|
685
1003
|
this.driveHasTrx = options.driveHasTrx;
|
|
686
1004
|
this.detector = options.detect;
|
|
687
1005
|
this.resolver = options.resolve;
|
|
@@ -729,7 +1047,10 @@ var expertSdr14Dialect = new StandardTciDialect({
|
|
|
729
1047
|
streamLengthSemantics: "per-channel",
|
|
730
1048
|
supportsStreamChannels: false,
|
|
731
1049
|
supportsTxAudioSource: false,
|
|
1050
|
+
supportsIqStream: true,
|
|
1051
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
732
1052
|
driveHasTrx: false,
|
|
1053
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
733
1054
|
detect: (context) => {
|
|
734
1055
|
const parsed = version(context);
|
|
735
1056
|
if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
|
|
@@ -742,7 +1063,10 @@ var expertSdrLegacyDialect = new StandardTciDialect({
|
|
|
742
1063
|
streamLengthSemantics: "per-channel",
|
|
743
1064
|
supportsStreamChannels: false,
|
|
744
1065
|
supportsTxAudioSource: false,
|
|
1066
|
+
supportsIqStream: true,
|
|
1067
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
745
1068
|
driveHasTrx: true,
|
|
1069
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
746
1070
|
detect: (context) => {
|
|
747
1071
|
const parsed = version(context);
|
|
748
1072
|
if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
|
|
@@ -755,7 +1079,10 @@ var expertSdrModernDialect = new StandardTciDialect({
|
|
|
755
1079
|
streamLengthSemantics: "scalar",
|
|
756
1080
|
supportsStreamChannels: true,
|
|
757
1081
|
supportsTxAudioSource: true,
|
|
1082
|
+
supportsIqStream: true,
|
|
1083
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
758
1084
|
driveHasTrx: true,
|
|
1085
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
759
1086
|
detect: (context) => {
|
|
760
1087
|
const parsed = version(context);
|
|
761
1088
|
if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
|
|
@@ -773,7 +1100,13 @@ var thetisDialect = new StandardTciDialect({
|
|
|
773
1100
|
streamLengthSemantics: "scalar",
|
|
774
1101
|
supportsStreamChannels: true,
|
|
775
1102
|
supportsTxAudioSource: true,
|
|
1103
|
+
supportsIqStream: true,
|
|
1104
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
776
1105
|
driveHasTrx: true,
|
|
1106
|
+
meterAdapter: new StandardTciMeterAdapter({
|
|
1107
|
+
interval: { minMs: 30, maxMs: 1e3 },
|
|
1108
|
+
supportsRxExtended: true
|
|
1109
|
+
}),
|
|
777
1110
|
detect: (context) => {
|
|
778
1111
|
const evidence = [];
|
|
779
1112
|
let score = 0;
|
|
@@ -799,7 +1132,13 @@ var aetherSdrDialect = new StandardTciDialect({
|
|
|
799
1132
|
streamLengthSemantics: "scalar",
|
|
800
1133
|
supportsStreamChannels: true,
|
|
801
1134
|
supportsTxAudioSource: true,
|
|
1135
|
+
supportsIqStream: true,
|
|
1136
|
+
iqSampleRates: [24e3, 48e3, 96e3, 192e3],
|
|
802
1137
|
driveHasTrx: true,
|
|
1138
|
+
meterAdapter: new StandardTciMeterAdapter({
|
|
1139
|
+
interval: { fixedMs: 200 },
|
|
1140
|
+
txAlcUnit: "dbfs"
|
|
1141
|
+
}),
|
|
803
1142
|
detect: (context) => {
|
|
804
1143
|
if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
|
|
805
1144
|
const evidence = [`AetherSDR device identity: ${context.identity.device}`];
|
|
@@ -818,7 +1157,10 @@ var genericObservedDialect = new StandardTciDialect({
|
|
|
818
1157
|
streamLengthSemantics: "auto",
|
|
819
1158
|
supportsStreamChannels: true,
|
|
820
1159
|
supportsTxAudioSource: true,
|
|
1160
|
+
supportsIqStream: false,
|
|
1161
|
+
iqSampleRates: [],
|
|
821
1162
|
driveHasTrx: true,
|
|
1163
|
+
meterAdapter: createUnknownTciMeterAdapter(),
|
|
822
1164
|
detect: (context) => ({
|
|
823
1165
|
score: context.commandNames.has("ready") ? 10 : 0,
|
|
824
1166
|
evidence: ["No vendor-specific match; using observed command shapes"],
|
|
@@ -833,7 +1175,10 @@ var genericObservedDialect = new StandardTciDialect({
|
|
|
833
1175
|
streamLengthSemantics: "auto",
|
|
834
1176
|
supportsStreamChannels: context.commandNames.has("audio_stream_channels"),
|
|
835
1177
|
supportsTxAudioSource: compareTciVersion(version(context) ?? [0], [2, 0]) >= 0,
|
|
1178
|
+
supportsIqStream: context.commandNames.has("iq_samplerate"),
|
|
1179
|
+
iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
|
|
836
1180
|
driveHasTrx,
|
|
1181
|
+
meterAdapter: createUnknownTciMeterAdapter(),
|
|
837
1182
|
detect: genericObservedDialect.detect.bind(genericObservedDialect)
|
|
838
1183
|
});
|
|
839
1184
|
}
|
|
@@ -932,9 +1277,9 @@ function assertValidTciHandshake(commands) {
|
|
|
932
1277
|
}
|
|
933
1278
|
|
|
934
1279
|
// src/transport/WebSocketTransport.ts
|
|
935
|
-
var
|
|
1280
|
+
var import_eventemitter32 = require("eventemitter3");
|
|
936
1281
|
var import_ws = __toESM(require("ws"), 1);
|
|
937
|
-
var WebSocketTciTransport = class extends
|
|
1282
|
+
var WebSocketTciTransport = class extends import_eventemitter32.EventEmitter {
|
|
938
1283
|
constructor(url, WebSocketImpl = import_ws.default) {
|
|
939
1284
|
super();
|
|
940
1285
|
this.url = url;
|
|
@@ -1044,7 +1389,78 @@ function dataToBuffer(data) {
|
|
|
1044
1389
|
}
|
|
1045
1390
|
|
|
1046
1391
|
// src/client/TciClient.ts
|
|
1047
|
-
var
|
|
1392
|
+
var TciIqStreamSession = class extends import_eventemitter33.EventEmitter {
|
|
1393
|
+
receiver;
|
|
1394
|
+
_appliedSampleRate;
|
|
1395
|
+
callbacks;
|
|
1396
|
+
closed = false;
|
|
1397
|
+
firstFrame;
|
|
1398
|
+
constructor(receiver, appliedSampleRate, callbacks) {
|
|
1399
|
+
super();
|
|
1400
|
+
this.receiver = receiver;
|
|
1401
|
+
this._appliedSampleRate = appliedSampleRate;
|
|
1402
|
+
this.callbacks = callbacks;
|
|
1403
|
+
}
|
|
1404
|
+
get appliedSampleRate() {
|
|
1405
|
+
return this._appliedSampleRate;
|
|
1406
|
+
}
|
|
1407
|
+
async setSampleRate(sampleRate) {
|
|
1408
|
+
if (this.closed) throw new TciError("cancelled", "TCI IQ stream is closed");
|
|
1409
|
+
const result = await this.callbacks.setSampleRate(sampleRate);
|
|
1410
|
+
this._appliedSampleRate = result.applied;
|
|
1411
|
+
return result;
|
|
1412
|
+
}
|
|
1413
|
+
async close() {
|
|
1414
|
+
if (this.closed) return;
|
|
1415
|
+
this.closed = true;
|
|
1416
|
+
this.rejectFirstFrame(new TciError("cancelled", "TCI IQ stream closed before its first frame"));
|
|
1417
|
+
try {
|
|
1418
|
+
await this.callbacks.close();
|
|
1419
|
+
} finally {
|
|
1420
|
+
this.emit("closed");
|
|
1421
|
+
this.removeAllListeners();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
waitForFirstFrame(timeoutMs) {
|
|
1425
|
+
if (this.firstFrame) return this.firstFrame.promise;
|
|
1426
|
+
let resolvePromise;
|
|
1427
|
+
let rejectPromise;
|
|
1428
|
+
const promise = new Promise((resolve, reject) => {
|
|
1429
|
+
resolvePromise = resolve;
|
|
1430
|
+
rejectPromise = reject;
|
|
1431
|
+
});
|
|
1432
|
+
const timer = setTimeout(() => {
|
|
1433
|
+
this.firstFrame = void 0;
|
|
1434
|
+
rejectPromise(new TciError("command-timeout", `Timed out waiting for TCI IQ frame for receiver ${this.receiver}`));
|
|
1435
|
+
}, timeoutMs);
|
|
1436
|
+
this.firstFrame = { promise, resolve: resolvePromise, reject: rejectPromise, timer };
|
|
1437
|
+
return promise;
|
|
1438
|
+
}
|
|
1439
|
+
_acceptFrame(frame) {
|
|
1440
|
+
if (this.closed || frame.receiver !== this.receiver) return;
|
|
1441
|
+
this._appliedSampleRate = frame.sampleRate;
|
|
1442
|
+
const firstFrame = this.firstFrame;
|
|
1443
|
+
if (firstFrame) {
|
|
1444
|
+
clearTimeout(firstFrame.timer);
|
|
1445
|
+
this.firstFrame = void 0;
|
|
1446
|
+
firstFrame.resolve(frame);
|
|
1447
|
+
}
|
|
1448
|
+
this.emit("frame", frame);
|
|
1449
|
+
}
|
|
1450
|
+
_fail(error) {
|
|
1451
|
+
if (this.closed) return;
|
|
1452
|
+
this.rejectFirstFrame(error);
|
|
1453
|
+
this.emit("error", error);
|
|
1454
|
+
}
|
|
1455
|
+
rejectFirstFrame(error) {
|
|
1456
|
+
const firstFrame = this.firstFrame;
|
|
1457
|
+
if (!firstFrame) return;
|
|
1458
|
+
clearTimeout(firstFrame.timer);
|
|
1459
|
+
this.firstFrame = void 0;
|
|
1460
|
+
firstFrame.reject(error);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
var TciClient = class extends import_eventemitter33.EventEmitter {
|
|
1048
1464
|
options;
|
|
1049
1465
|
WebSocketImpl;
|
|
1050
1466
|
transportFactory;
|
|
@@ -1058,6 +1474,8 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1058
1474
|
handshakeError;
|
|
1059
1475
|
initializationCommands = [];
|
|
1060
1476
|
handshakeWaiter;
|
|
1477
|
+
activeIqSession;
|
|
1478
|
+
activeMeterSession;
|
|
1061
1479
|
constructor(options) {
|
|
1062
1480
|
super();
|
|
1063
1481
|
this.options = {
|
|
@@ -1096,7 +1514,9 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1096
1514
|
split: {},
|
|
1097
1515
|
dialectWarnings: [],
|
|
1098
1516
|
rxSensors: {},
|
|
1099
|
-
txSensors: {}
|
|
1517
|
+
txSensors: {},
|
|
1518
|
+
iq: { activeReceivers: {} },
|
|
1519
|
+
dds: {}
|
|
1100
1520
|
};
|
|
1101
1521
|
this.stateReducers = this.createStateReducers();
|
|
1102
1522
|
}
|
|
@@ -1121,6 +1541,7 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1121
1541
|
}
|
|
1122
1542
|
}
|
|
1123
1543
|
async disconnect(code = 1e3, reason = "client disconnect") {
|
|
1544
|
+
await this.activeMeterSession?.close().catch(() => void 0);
|
|
1124
1545
|
const transport = this.transport;
|
|
1125
1546
|
if (!transport) return;
|
|
1126
1547
|
await transport.disconnect(code, reason);
|
|
@@ -1135,6 +1556,133 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1135
1556
|
getHandshakeResult() {
|
|
1136
1557
|
return this.handshakeResult ? cloneHandshake(this.handshakeResult) : void 0;
|
|
1137
1558
|
}
|
|
1559
|
+
getIqCapabilities() {
|
|
1560
|
+
const dialect = this.requireDialect();
|
|
1561
|
+
return {
|
|
1562
|
+
supported: dialect.supportsIqStream,
|
|
1563
|
+
currentSampleRate: this.state.iq.sampleRate,
|
|
1564
|
+
supportedSampleRates: [...dialect.iqSampleRates]
|
|
1565
|
+
};
|
|
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
|
+
}
|
|
1632
|
+
async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
|
|
1633
|
+
const capabilities = this.getIqCapabilities();
|
|
1634
|
+
const requested = Math.round(sampleRate);
|
|
1635
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1636
|
+
if (!Number.isFinite(requested) || requested <= 0) throw new TciError("protocol-error", `Invalid TCI IQ sample rate: ${sampleRate}`);
|
|
1637
|
+
if (capabilities.supportedSampleRates.length > 0 && !capabilities.supportedSampleRates.includes(requested)) {
|
|
1638
|
+
throw new TciError("protocol-error", `TCI IQ sample rate ${requested} is not supported by dialect ${this.requireDialect().id}`);
|
|
1639
|
+
}
|
|
1640
|
+
if (this.state.iq.sampleRate === requested) return writeResult(requested, requested, "state");
|
|
1641
|
+
const waiter = this.waitForCommand((command) => command.name === "iq_samplerate", timeoutMs, "IQ_SAMPLERATE readback");
|
|
1642
|
+
try {
|
|
1643
|
+
await this.sendCommand("IQ_SAMPLERATE", [requested], { waitForReply: false });
|
|
1644
|
+
const reply = await waiter.promise;
|
|
1645
|
+
const applied = parseNumber(reply.args[0]);
|
|
1646
|
+
if (applied === void 0 || applied <= 0) throw new TciError("protocol-error", `Invalid IQ_SAMPLERATE readback: ${reply.raw}`);
|
|
1647
|
+
return writeResult(requested, applied, "reply");
|
|
1648
|
+
} finally {
|
|
1649
|
+
waiter.cancel();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async openIqStream(options = {}) {
|
|
1653
|
+
if (this.activeIqSession) throw new TciError("protocol-error", "This TCI client already has an active IQ stream session");
|
|
1654
|
+
const capabilities = this.getIqCapabilities();
|
|
1655
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1656
|
+
const receiver = Math.floor(options.receiver ?? this.options.receiver);
|
|
1657
|
+
if (!Number.isInteger(receiver) || receiver < 0) throw new TciError("protocol-error", `Invalid TCI IQ receiver: ${receiver}`);
|
|
1658
|
+
const requestedRate = options.sampleRate ?? capabilities.currentSampleRate ?? capabilities.supportedSampleRates[0] ?? 48e3;
|
|
1659
|
+
const rateResult = await this.setIqSampleRate(requestedRate);
|
|
1660
|
+
let session;
|
|
1661
|
+
session = new TciIqStreamSession(receiver, rateResult.applied, {
|
|
1662
|
+
setSampleRate: (rate) => this.setIqSampleRate(rate),
|
|
1663
|
+
close: async () => {
|
|
1664
|
+
try {
|
|
1665
|
+
if (this.isConnected()) await this.sendCommand("IQ_STOP", [receiver], { waitForReply: false });
|
|
1666
|
+
} finally {
|
|
1667
|
+
this.state.iq.activeReceivers[String(receiver)] = false;
|
|
1668
|
+
if (this.activeIqSession === session) this.activeIqSession = void 0;
|
|
1669
|
+
this.emitState();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
this.activeIqSession = session;
|
|
1674
|
+
const firstFrame = session.waitForFirstFrame(options.firstFrameTimeoutMs ?? 5e3);
|
|
1675
|
+
try {
|
|
1676
|
+
await this.sendCommand("IQ_START", [receiver], { waitForReply: false });
|
|
1677
|
+
this.state.iq.activeReceivers[String(receiver)] = true;
|
|
1678
|
+
this.emitState();
|
|
1679
|
+
await firstFrame;
|
|
1680
|
+
return session;
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
await session.close().catch(() => void 0);
|
|
1683
|
+
throw error;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1138
1686
|
async sendCommand(name, args = [], options = {}) {
|
|
1139
1687
|
const raw = formatTciCommand(name, args);
|
|
1140
1688
|
if (options.waitForReply === false) {
|
|
@@ -1605,11 +2153,13 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1605
2153
|
handleText(raw) {
|
|
1606
2154
|
try {
|
|
1607
2155
|
const commands = parseTciText(raw);
|
|
2156
|
+
const receivedAtMs = Date.now();
|
|
1608
2157
|
this.emit("tci:rx", raw, commands);
|
|
1609
2158
|
for (const command of commands) {
|
|
1610
2159
|
if (!this.handshakeResult) this.initializationCommands.push(command);
|
|
1611
2160
|
this.queue.handleCommand(command);
|
|
1612
2161
|
this.applyCommand(command);
|
|
2162
|
+
this.activeMeterSession?._acceptCommand(command, receivedAtMs);
|
|
1613
2163
|
this.emit("command", command);
|
|
1614
2164
|
}
|
|
1615
2165
|
} catch (error) {
|
|
@@ -1624,6 +2174,22 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1624
2174
|
this.emit("tci:binary", frame);
|
|
1625
2175
|
this.emit("binary", frame);
|
|
1626
2176
|
switch (frame.streamType) {
|
|
2177
|
+
case 0 /* IQ_STREAM */: {
|
|
2178
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
2179
|
+
throw new TciError("invalid-frame", "TCI IQ stream frame must contain interleaved I/Q pairs");
|
|
2180
|
+
}
|
|
2181
|
+
this.state.iq.sampleRate = frame.sampleRate;
|
|
2182
|
+
const iqFrame = {
|
|
2183
|
+
frame,
|
|
2184
|
+
receiver: frame.receiver,
|
|
2185
|
+
sampleRate: frame.sampleRate,
|
|
2186
|
+
centerFrequency: this.state.dds[String(frame.receiver)],
|
|
2187
|
+
complexSampleCount: frame.frameCount
|
|
2188
|
+
};
|
|
2189
|
+
this.emit("iqFrame", iqFrame);
|
|
2190
|
+
this.activeIqSession?._acceptFrame(iqFrame);
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
1627
2193
|
case 1 /* RX_AUDIO_STREAM */:
|
|
1628
2194
|
this.emit("rxAudioFrame", frame);
|
|
1629
2195
|
break;
|
|
@@ -1701,6 +2267,11 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1701
2267
|
this.state.modulations = args.map((mode) => mode.toLowerCase());
|
|
1702
2268
|
});
|
|
1703
2269
|
reducers.set("vfo", (args) => this.applyVfo(args));
|
|
2270
|
+
reducers.set("dds", (args) => {
|
|
2271
|
+
const receiver = parseNumber(args[0]);
|
|
2272
|
+
const frequency = parseNumber(args[1]);
|
|
2273
|
+
if (receiver !== void 0 && frequency !== void 0 && frequency >= 0) this.state.dds[String(receiver)] = frequency;
|
|
2274
|
+
});
|
|
1704
2275
|
reducers.set("modulation", (args) => this.applyModulation(args));
|
|
1705
2276
|
reducers.set("trx", (args) => this.applyTrx(args));
|
|
1706
2277
|
reducers.set("tune", (args) => this.applyBooleanByFirstArg(this.state.tune, args));
|
|
@@ -1708,6 +2279,7 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1708
2279
|
reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
|
|
1709
2280
|
reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
|
|
1710
2281
|
reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
|
|
2282
|
+
reducers.set("rx_channel_sensors_ex", (args) => this.applyRxChannelSensors(args));
|
|
1711
2283
|
reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
|
|
1712
2284
|
reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
|
|
1713
2285
|
reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
|
|
@@ -1717,6 +2289,15 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1717
2289
|
reducers.set("tx_stream_audio_buffering", (args) => this.updateAudioState({ txBufferingMs: parseNumber(args[0]) }));
|
|
1718
2290
|
reducers.set("audio_start", () => this.updateAudioState({ running: true }));
|
|
1719
2291
|
reducers.set("audio_stop", () => this.updateAudioState({ running: false }));
|
|
2292
|
+
reducers.set("iq_samplerate", (args) => {
|
|
2293
|
+
this.state.iq.sampleRate = parseNumber(args[0]);
|
|
2294
|
+
});
|
|
2295
|
+
reducers.set("iq_start", (args) => {
|
|
2296
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = true;
|
|
2297
|
+
});
|
|
2298
|
+
reducers.set("iq_stop", (args) => {
|
|
2299
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = false;
|
|
2300
|
+
});
|
|
1720
2301
|
return reducers;
|
|
1721
2302
|
}
|
|
1722
2303
|
applyVfo(args) {
|
|
@@ -1824,6 +2405,10 @@ var TciClient = class extends import_eventemitter32.EventEmitter {
|
|
|
1824
2405
|
this.state.ready = false;
|
|
1825
2406
|
this.queue.setConnected(false);
|
|
1826
2407
|
this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
|
|
2408
|
+
this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
|
|
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;
|
|
1827
2412
|
if (wasConnected) {
|
|
1828
2413
|
this.emit("disconnected", reason);
|
|
1829
2414
|
this.emitState();
|
|
@@ -1850,6 +2435,12 @@ function parseNumber(value) {
|
|
|
1850
2435
|
const number = Number(value);
|
|
1851
2436
|
return Number.isFinite(number) ? number : void 0;
|
|
1852
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
|
+
}
|
|
1853
2444
|
function parseBoolean(value) {
|
|
1854
2445
|
if (value === void 0) {
|
|
1855
2446
|
return void 0;
|
|
@@ -1904,7 +2495,9 @@ function cloneState(state) {
|
|
|
1904
2495
|
dialectWarnings: [...state.dialectWarnings],
|
|
1905
2496
|
rxSensors: cloneNested(state.rxSensors),
|
|
1906
2497
|
txSensors: cloneNested(state.txSensors),
|
|
1907
|
-
audio: state.audio ? { ...state.audio } : void 0
|
|
2498
|
+
audio: state.audio ? { ...state.audio } : void 0,
|
|
2499
|
+
iq: { ...state.iq, activeReceivers: { ...state.iq.activeReceivers } },
|
|
2500
|
+
dds: { ...state.dds }
|
|
1908
2501
|
};
|
|
1909
2502
|
}
|
|
1910
2503
|
function cloneHandshake(result) {
|
|
@@ -1924,22 +2517,29 @@ function cloneNested(value) {
|
|
|
1924
2517
|
}
|
|
1925
2518
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1926
2519
|
0 && (module.exports = {
|
|
2520
|
+
StandardTciMeterAdapter,
|
|
1927
2521
|
TCI_STREAM_HEADER_BYTES,
|
|
1928
2522
|
TciClient,
|
|
1929
2523
|
TciCommandQueue,
|
|
1930
2524
|
TciDialectRegistry,
|
|
1931
2525
|
TciError,
|
|
2526
|
+
TciIqStreamSession,
|
|
2527
|
+
TciMeterStreamSession,
|
|
1932
2528
|
TciSampleType,
|
|
1933
2529
|
TciStreamType,
|
|
2530
|
+
UNKNOWN_TCI_METER_CAPABILITIES,
|
|
1934
2531
|
WebSocketTciTransport,
|
|
1935
2532
|
aetherSdrDialect,
|
|
1936
2533
|
assertValidTciHandshake,
|
|
1937
2534
|
buildStreamFrame,
|
|
1938
2535
|
buildTxAudioFrame,
|
|
1939
2536
|
builtInDialects,
|
|
2537
|
+
cloneMeterCapabilities,
|
|
1940
2538
|
commandKey,
|
|
1941
2539
|
compareTciVersion,
|
|
1942
2540
|
createTciClient,
|
|
2541
|
+
createUnknownTciMeterAdapter,
|
|
2542
|
+
decodeInterleavedIq,
|
|
1943
2543
|
defaultTciDialectRegistry,
|
|
1944
2544
|
deinterleaveChannels,
|
|
1945
2545
|
escapeTciText,
|