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.js
CHANGED
|
@@ -20,7 +20,7 @@ function toTciError(error, fallbackCode = "protocol-error") {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// src/client/TciClient.ts
|
|
23
|
-
import { EventEmitter as
|
|
23
|
+
import { EventEmitter as EventEmitter3 } from "eventemitter3";
|
|
24
24
|
import WebSocket2 from "ws";
|
|
25
25
|
|
|
26
26
|
// src/audio/streamFrame.ts
|
|
@@ -241,6 +241,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
|
|
|
241
241
|
}
|
|
242
242
|
return output;
|
|
243
243
|
}
|
|
244
|
+
function decodeInterleavedIq(frame) {
|
|
245
|
+
if (frame.streamType !== 0 /* IQ_STREAM */) {
|
|
246
|
+
throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
|
|
247
|
+
}
|
|
248
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
249
|
+
throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
|
|
250
|
+
}
|
|
251
|
+
return payloadToFloat32(frame);
|
|
252
|
+
}
|
|
244
253
|
function samplesToPayload(samples, sampleType) {
|
|
245
254
|
const type = normalizeSampleType(sampleType);
|
|
246
255
|
const bytes = sampleTypeBytes(type);
|
|
@@ -588,6 +597,302 @@ function ensureSemicolon(command) {
|
|
|
588
597
|
return command.trim().endsWith(";") ? command.trim() : `${command.trim()};`;
|
|
589
598
|
}
|
|
590
599
|
|
|
600
|
+
// src/meter/types.ts
|
|
601
|
+
var UNKNOWN_TCI_METER_CAPABILITIES = {
|
|
602
|
+
rxLevel: "unknown",
|
|
603
|
+
rxAverageLevel: "unknown",
|
|
604
|
+
rxPeakBin: "unknown",
|
|
605
|
+
txMicLevel: "unknown",
|
|
606
|
+
txRmsPower: "unknown",
|
|
607
|
+
txPeakPower: "unknown",
|
|
608
|
+
txSwr: "unknown",
|
|
609
|
+
txAlcDbfs: "unknown"
|
|
610
|
+
};
|
|
611
|
+
function cloneMeterCapabilities(capabilities) {
|
|
612
|
+
return { ...capabilities };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/meter/adapters.ts
|
|
616
|
+
var StandardTciMeterAdapter = class {
|
|
617
|
+
declaredCapabilities;
|
|
618
|
+
options;
|
|
619
|
+
constructor(options = {}) {
|
|
620
|
+
this.options = options;
|
|
621
|
+
this.declaredCapabilities = {
|
|
622
|
+
...UNKNOWN_TCI_METER_CAPABILITIES,
|
|
623
|
+
rxLevel: "declared",
|
|
624
|
+
txMicLevel: "declared",
|
|
625
|
+
txRmsPower: "declared",
|
|
626
|
+
txPeakPower: "declared",
|
|
627
|
+
txSwr: "declared",
|
|
628
|
+
rxAverageLevel: options.supportsRxExtended ? "declared" : "unknown",
|
|
629
|
+
rxPeakBin: options.supportsRxExtended ? "declared" : "unknown",
|
|
630
|
+
txAlcDbfs: options.txAlcUnit === "dbfs" ? "declared" : "unknown",
|
|
631
|
+
...options.capabilities
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
normalizeInterval(intervalMs) {
|
|
635
|
+
const requestedMs = Math.round(intervalMs);
|
|
636
|
+
if (this.options.interval?.fixedMs !== void 0) {
|
|
637
|
+
return { requestedMs, appliedMs: this.options.interval.fixedMs };
|
|
638
|
+
}
|
|
639
|
+
const minMs = this.options.interval?.minMs ?? requestedMs;
|
|
640
|
+
const maxMs = this.options.interval?.maxMs ?? requestedMs;
|
|
641
|
+
const normalized = Math.max(minMs, Math.min(maxMs, requestedMs));
|
|
642
|
+
return {
|
|
643
|
+
requestedMs,
|
|
644
|
+
appliedMs: this.options.interval?.reportsApplied === false ? void 0 : normalized
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
buildEnableCommand(kind, enabled, intervalMs) {
|
|
648
|
+
return {
|
|
649
|
+
name: kind === "rx" ? "RX_SENSORS_ENABLE" : "TX_SENSORS_ENABLE",
|
|
650
|
+
args: enabled ? [true, intervalMs] : [false]
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
decode(command, receivedAtMs) {
|
|
654
|
+
switch (command.name) {
|
|
655
|
+
case "rx_sensors":
|
|
656
|
+
return decodeRx(command, receivedAtMs, "rx_sensors");
|
|
657
|
+
case "rx_channel_sensors":
|
|
658
|
+
return decodeRx(command, receivedAtMs, "rx_channel_sensors");
|
|
659
|
+
case "rx_channel_sensors_ex":
|
|
660
|
+
return decodeRx(command, receivedAtMs, "rx_channel_sensors_ex");
|
|
661
|
+
case "tx_sensors":
|
|
662
|
+
return decodeTx(command, receivedAtMs, this.options.txAlcUnit);
|
|
663
|
+
default:
|
|
664
|
+
return void 0;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
function createUnknownTciMeterAdapter() {
|
|
669
|
+
return new StandardTciMeterAdapter({
|
|
670
|
+
capabilities: cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES),
|
|
671
|
+
interval: { reportsApplied: false }
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function decodeRx(command, receivedAtMs, source) {
|
|
675
|
+
const receiver = integer(command.args[0]);
|
|
676
|
+
const hasChannel = source !== "rx_sensors";
|
|
677
|
+
const channel = hasChannel ? integer(command.args[1]) : 0;
|
|
678
|
+
const levelIndex = hasChannel ? 2 : 1;
|
|
679
|
+
const levelDbm = finite(command.args[levelIndex]);
|
|
680
|
+
if (receiver === void 0 || receiver < 0 || channel === void 0 || channel < 0 || levelDbm === void 0) {
|
|
681
|
+
return { issue: `Invalid ${command.originalName} meter frame: ${command.raw}` };
|
|
682
|
+
}
|
|
683
|
+
const frame = { receiver, channel, levelDbm, source, receivedAtMs };
|
|
684
|
+
if (source === "rx_channel_sensors_ex") {
|
|
685
|
+
const averageLevelDbm = finite(command.args[3]);
|
|
686
|
+
const peakBinDbm = finite(command.args[4]);
|
|
687
|
+
if (averageLevelDbm === void 0 || peakBinDbm === void 0) {
|
|
688
|
+
return { issue: `Invalid ${command.originalName} extended meter frame: ${command.raw}` };
|
|
689
|
+
}
|
|
690
|
+
frame.averageLevelDbm = averageLevelDbm;
|
|
691
|
+
frame.peakBinDbm = peakBinDbm;
|
|
692
|
+
if (command.args.length > 5) frame.extraArgs = command.args.slice(5);
|
|
693
|
+
} else if (command.args.length > levelIndex + 1) {
|
|
694
|
+
frame.extraArgs = command.args.slice(levelIndex + 1);
|
|
695
|
+
}
|
|
696
|
+
return { decoded: { kind: "rx", frame } };
|
|
697
|
+
}
|
|
698
|
+
function decodeTx(command, receivedAtMs, alcUnit) {
|
|
699
|
+
const trx = integer(command.args[0]);
|
|
700
|
+
if (trx === void 0 || trx < 0) {
|
|
701
|
+
return { issue: `Invalid ${command.originalName} transmitter index: ${command.raw}` };
|
|
702
|
+
}
|
|
703
|
+
const micLevelDbm = optionalFinite(command.args[1]);
|
|
704
|
+
const rmsPowerWatts = optionalFinite(command.args[2]);
|
|
705
|
+
const peakPowerWatts = optionalFinite(command.args[3]);
|
|
706
|
+
const swr = optionalFinite(command.args[4]);
|
|
707
|
+
const alcValue = alcUnit ? optionalFinite(command.args[5]) : void 0;
|
|
708
|
+
if (micLevelDbm.invalid || rmsPowerWatts.invalid || peakPowerWatts.invalid || swr.invalid || alcValue?.invalid) {
|
|
709
|
+
return { issue: `Invalid ${command.originalName} numeric meter frame: ${command.raw}` };
|
|
710
|
+
}
|
|
711
|
+
if (rmsPowerWatts.value !== void 0 && rmsPowerWatts.value < 0 || peakPowerWatts.value !== void 0 && peakPowerWatts.value < 0 || swr.value !== void 0 && swr.value < 1) {
|
|
712
|
+
return { issue: `Out-of-range ${command.originalName} meter frame: ${command.raw}` };
|
|
713
|
+
}
|
|
714
|
+
if (micLevelDbm.value === void 0 && rmsPowerWatts.value === void 0 && peakPowerWatts.value === void 0 && swr.value === void 0 && alcValue?.value === void 0) {
|
|
715
|
+
return { issue: `Empty ${command.originalName} meter frame: ${command.raw}` };
|
|
716
|
+
}
|
|
717
|
+
const frame = {
|
|
718
|
+
trx,
|
|
719
|
+
micLevelDbm: micLevelDbm.value,
|
|
720
|
+
rmsPowerWatts: rmsPowerWatts.value,
|
|
721
|
+
peakPowerWatts: peakPowerWatts.value,
|
|
722
|
+
swr: swr.value,
|
|
723
|
+
receivedAtMs
|
|
724
|
+
};
|
|
725
|
+
if (alcUnit && alcValue?.value !== void 0) frame.alc = { value: alcValue.value, unit: alcUnit };
|
|
726
|
+
const knownArgs = alcUnit ? 6 : 5;
|
|
727
|
+
if (command.args.length > knownArgs) frame.extraArgs = command.args.slice(knownArgs);
|
|
728
|
+
return { decoded: { kind: "tx", frame } };
|
|
729
|
+
}
|
|
730
|
+
function finite(value) {
|
|
731
|
+
if (value === void 0 || value === "") return void 0;
|
|
732
|
+
const parsed = Number(value);
|
|
733
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
734
|
+
}
|
|
735
|
+
function integer(value) {
|
|
736
|
+
const parsed = finite(value);
|
|
737
|
+
return parsed !== void 0 && Number.isInteger(parsed) ? parsed : void 0;
|
|
738
|
+
}
|
|
739
|
+
function optionalFinite(value) {
|
|
740
|
+
if (value === void 0 || value === "") return { invalid: false };
|
|
741
|
+
const parsed = Number(value);
|
|
742
|
+
return Number.isFinite(parsed) ? { value: parsed, invalid: false } : { invalid: true };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/meter/TciMeterStreamSession.ts
|
|
746
|
+
import { EventEmitter } from "eventemitter3";
|
|
747
|
+
var RX_COALESCE_MS = 20;
|
|
748
|
+
var TciMeterStreamSession = class extends EventEmitter {
|
|
749
|
+
receiver;
|
|
750
|
+
channel;
|
|
751
|
+
trx;
|
|
752
|
+
requestedIntervalMs;
|
|
753
|
+
appliedIntervalMs;
|
|
754
|
+
rxEnabled;
|
|
755
|
+
txEnabled;
|
|
756
|
+
adapter;
|
|
757
|
+
callbacks;
|
|
758
|
+
capabilities;
|
|
759
|
+
pendingRx = /* @__PURE__ */ new Map();
|
|
760
|
+
closed = false;
|
|
761
|
+
constructor(options) {
|
|
762
|
+
super();
|
|
763
|
+
this.receiver = options.receiver;
|
|
764
|
+
this.channel = options.channel;
|
|
765
|
+
this.trx = options.trx;
|
|
766
|
+
this.requestedIntervalMs = options.requestedIntervalMs;
|
|
767
|
+
this.appliedIntervalMs = options.appliedIntervalMs;
|
|
768
|
+
this.rxEnabled = options.rxEnabled;
|
|
769
|
+
this.txEnabled = options.txEnabled;
|
|
770
|
+
this.adapter = options.adapter;
|
|
771
|
+
this.callbacks = options.callbacks;
|
|
772
|
+
this.capabilities = cloneMeterCapabilities(options.adapter.declaredCapabilities);
|
|
773
|
+
}
|
|
774
|
+
getCapabilities() {
|
|
775
|
+
return cloneMeterCapabilities(this.capabilities);
|
|
776
|
+
}
|
|
777
|
+
async close() {
|
|
778
|
+
if (this.closed) return;
|
|
779
|
+
this.closed = true;
|
|
780
|
+
this.clearPendingRx();
|
|
781
|
+
try {
|
|
782
|
+
await this.callbacks.close();
|
|
783
|
+
} finally {
|
|
784
|
+
this.emit("closed");
|
|
785
|
+
this.removeAllListeners();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
_acceptCommand(command, receivedAtMs) {
|
|
789
|
+
if (this.closed) return;
|
|
790
|
+
this.acceptEnableAcknowledgement(command);
|
|
791
|
+
const result = this.adapter.decode(command, receivedAtMs);
|
|
792
|
+
if (!result) return;
|
|
793
|
+
if (result.issue) {
|
|
794
|
+
this.emit("error", new TciError("protocol-error", result.issue));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (result.decoded?.kind === "rx") this.acceptRxFrame(result.decoded.frame);
|
|
798
|
+
if (result.decoded?.kind === "tx") this.acceptTxFrame(result.decoded.frame);
|
|
799
|
+
}
|
|
800
|
+
_fail(error) {
|
|
801
|
+
if (this.closed) return;
|
|
802
|
+
this.closed = true;
|
|
803
|
+
this.clearPendingRx();
|
|
804
|
+
this.emit("error", error);
|
|
805
|
+
this.emit("closed");
|
|
806
|
+
this.removeAllListeners();
|
|
807
|
+
}
|
|
808
|
+
acceptRxFrame(frame) {
|
|
809
|
+
if (!this.rxEnabled || frame.receiver !== this.receiver || frame.channel !== this.channel) return;
|
|
810
|
+
this.observe("rxLevel");
|
|
811
|
+
if (frame.averageLevelDbm !== void 0) this.observe("rxAverageLevel");
|
|
812
|
+
if (frame.peakBinDbm !== void 0) this.observe("rxPeakBin");
|
|
813
|
+
const key = `${frame.receiver}:${frame.channel}`;
|
|
814
|
+
const pending = this.pendingRx.get(key);
|
|
815
|
+
if (pending) {
|
|
816
|
+
const sameReading = Math.abs(pending.frame.levelDbm - frame.levelDbm) < 0.05;
|
|
817
|
+
if (sameReading && rxPriority(frame) >= rxPriority(pending.frame)) {
|
|
818
|
+
clearTimeout(pending.timer);
|
|
819
|
+
this.pendingRx.delete(key);
|
|
820
|
+
} else if (sameReading) {
|
|
821
|
+
return;
|
|
822
|
+
} else {
|
|
823
|
+
this.flushRx(key, pending);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const timer = setTimeout(() => {
|
|
827
|
+
const current = this.pendingRx.get(key);
|
|
828
|
+
if (current?.frame === frame) this.flushRx(key, current);
|
|
829
|
+
}, RX_COALESCE_MS);
|
|
830
|
+
this.pendingRx.set(key, { frame, timer });
|
|
831
|
+
}
|
|
832
|
+
acceptTxFrame(frame) {
|
|
833
|
+
if (!this.txEnabled || frame.trx !== this.trx) return;
|
|
834
|
+
if (frame.micLevelDbm !== void 0) this.observe("txMicLevel");
|
|
835
|
+
if (frame.rmsPowerWatts !== void 0) this.observe("txRmsPower");
|
|
836
|
+
if (frame.peakPowerWatts !== void 0) this.observe("txPeakPower");
|
|
837
|
+
if (frame.swr !== void 0) this.observe("txSwr");
|
|
838
|
+
if (frame.alc?.unit === "dbfs") this.observe("txAlcDbfs");
|
|
839
|
+
this.emit("txFrame", frame);
|
|
840
|
+
}
|
|
841
|
+
acceptEnableAcknowledgement(command) {
|
|
842
|
+
const enabled = command.args[0]?.toLowerCase();
|
|
843
|
+
if (enabled !== "true" && enabled !== "1" && enabled !== "on") return;
|
|
844
|
+
if (command.name === "rx_sensors_enable" && this.rxEnabled) {
|
|
845
|
+
this.acknowledge(["rxLevel"]);
|
|
846
|
+
}
|
|
847
|
+
if (command.name === "tx_sensors_enable" && this.txEnabled) {
|
|
848
|
+
this.acknowledge(["txMicLevel", "txRmsPower", "txPeakPower", "txSwr"]);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
acknowledge(keys) {
|
|
852
|
+
let changed = false;
|
|
853
|
+
for (const key of keys) {
|
|
854
|
+
if (supportRank(this.capabilities[key]) < supportRank("acknowledged")) {
|
|
855
|
+
this.capabilities[key] = "acknowledged";
|
|
856
|
+
changed = true;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (changed) this.emit("capabilitiesChanged", this.getCapabilities());
|
|
860
|
+
}
|
|
861
|
+
observe(key) {
|
|
862
|
+
if (this.capabilities[key] === "observed") return;
|
|
863
|
+
this.capabilities[key] = "observed";
|
|
864
|
+
this.emit("capabilitiesChanged", this.getCapabilities());
|
|
865
|
+
}
|
|
866
|
+
flushRx(key, pending) {
|
|
867
|
+
clearTimeout(pending.timer);
|
|
868
|
+
if (this.pendingRx.get(key) === pending) this.pendingRx.delete(key);
|
|
869
|
+
this.emit("rxFrame", pending.frame);
|
|
870
|
+
}
|
|
871
|
+
clearPendingRx() {
|
|
872
|
+
for (const pending of this.pendingRx.values()) clearTimeout(pending.timer);
|
|
873
|
+
this.pendingRx.clear();
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
function rxPriority(frame) {
|
|
877
|
+
if (frame.source === "rx_channel_sensors_ex") return 3;
|
|
878
|
+
if (frame.source === "rx_channel_sensors") return 2;
|
|
879
|
+
return 1;
|
|
880
|
+
}
|
|
881
|
+
function supportRank(value) {
|
|
882
|
+
switch (value) {
|
|
883
|
+
case "unsupported":
|
|
884
|
+
return -1;
|
|
885
|
+
case "unknown":
|
|
886
|
+
return 0;
|
|
887
|
+
case "declared":
|
|
888
|
+
return 1;
|
|
889
|
+
case "acknowledged":
|
|
890
|
+
return 2;
|
|
891
|
+
case "observed":
|
|
892
|
+
return 3;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
591
896
|
// src/dialect/builtins.ts
|
|
592
897
|
var StandardTciDialect = class {
|
|
593
898
|
id;
|
|
@@ -595,6 +900,9 @@ var StandardTciDialect = class {
|
|
|
595
900
|
streamLengthSemantics;
|
|
596
901
|
supportsStreamChannels;
|
|
597
902
|
supportsTxAudioSource;
|
|
903
|
+
supportsIqStream;
|
|
904
|
+
iqSampleRates;
|
|
905
|
+
meterAdapter;
|
|
598
906
|
driveHasTrx;
|
|
599
907
|
detector;
|
|
600
908
|
resolver;
|
|
@@ -604,6 +912,9 @@ var StandardTciDialect = class {
|
|
|
604
912
|
this.streamLengthSemantics = options.streamLengthSemantics;
|
|
605
913
|
this.supportsStreamChannels = options.supportsStreamChannels;
|
|
606
914
|
this.supportsTxAudioSource = options.supportsTxAudioSource;
|
|
915
|
+
this.supportsIqStream = options.supportsIqStream;
|
|
916
|
+
this.iqSampleRates = [...options.iqSampleRates];
|
|
917
|
+
this.meterAdapter = options.meterAdapter;
|
|
607
918
|
this.driveHasTrx = options.driveHasTrx;
|
|
608
919
|
this.detector = options.detect;
|
|
609
920
|
this.resolver = options.resolve;
|
|
@@ -651,7 +962,10 @@ var expertSdr14Dialect = new StandardTciDialect({
|
|
|
651
962
|
streamLengthSemantics: "per-channel",
|
|
652
963
|
supportsStreamChannels: false,
|
|
653
964
|
supportsTxAudioSource: false,
|
|
965
|
+
supportsIqStream: true,
|
|
966
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
654
967
|
driveHasTrx: false,
|
|
968
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
655
969
|
detect: (context) => {
|
|
656
970
|
const parsed = version(context);
|
|
657
971
|
if (!parsed || compareTciVersion(parsed, [1, 4]) > 0) return { score: 0, evidence: [] };
|
|
@@ -664,7 +978,10 @@ var expertSdrLegacyDialect = new StandardTciDialect({
|
|
|
664
978
|
streamLengthSemantics: "per-channel",
|
|
665
979
|
supportsStreamChannels: false,
|
|
666
980
|
supportsTxAudioSource: false,
|
|
981
|
+
supportsIqStream: true,
|
|
982
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
667
983
|
driveHasTrx: true,
|
|
984
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
668
985
|
detect: (context) => {
|
|
669
986
|
const parsed = version(context);
|
|
670
987
|
if (!parsed || compareTciVersion(parsed, [1, 5]) < 0 || compareTciVersion(parsed, [1, 9]) >= 0) return { score: 0, evidence: [] };
|
|
@@ -677,7 +994,10 @@ var expertSdrModernDialect = new StandardTciDialect({
|
|
|
677
994
|
streamLengthSemantics: "scalar",
|
|
678
995
|
supportsStreamChannels: true,
|
|
679
996
|
supportsTxAudioSource: true,
|
|
997
|
+
supportsIqStream: true,
|
|
998
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
680
999
|
driveHasTrx: true,
|
|
1000
|
+
meterAdapter: new StandardTciMeterAdapter({ interval: { reportsApplied: false } }),
|
|
681
1001
|
detect: (context) => {
|
|
682
1002
|
const parsed = version(context);
|
|
683
1003
|
if (!parsed || compareTciVersion(parsed, [1, 9]) < 0) return { score: 0, evidence: [] };
|
|
@@ -695,7 +1015,13 @@ var thetisDialect = new StandardTciDialect({
|
|
|
695
1015
|
streamLengthSemantics: "scalar",
|
|
696
1016
|
supportsStreamChannels: true,
|
|
697
1017
|
supportsTxAudioSource: true,
|
|
1018
|
+
supportsIqStream: true,
|
|
1019
|
+
iqSampleRates: [48e3, 96e3, 192e3, 384e3],
|
|
698
1020
|
driveHasTrx: true,
|
|
1021
|
+
meterAdapter: new StandardTciMeterAdapter({
|
|
1022
|
+
interval: { minMs: 30, maxMs: 1e3 },
|
|
1023
|
+
supportsRxExtended: true
|
|
1024
|
+
}),
|
|
699
1025
|
detect: (context) => {
|
|
700
1026
|
const evidence = [];
|
|
701
1027
|
let score = 0;
|
|
@@ -721,7 +1047,13 @@ var aetherSdrDialect = new StandardTciDialect({
|
|
|
721
1047
|
streamLengthSemantics: "scalar",
|
|
722
1048
|
supportsStreamChannels: true,
|
|
723
1049
|
supportsTxAudioSource: true,
|
|
1050
|
+
supportsIqStream: true,
|
|
1051
|
+
iqSampleRates: [24e3, 48e3, 96e3, 192e3],
|
|
724
1052
|
driveHasTrx: true,
|
|
1053
|
+
meterAdapter: new StandardTciMeterAdapter({
|
|
1054
|
+
interval: { fixedMs: 200 },
|
|
1055
|
+
txAlcUnit: "dbfs"
|
|
1056
|
+
}),
|
|
725
1057
|
detect: (context) => {
|
|
726
1058
|
if (!/^aethersdr$/i.test(context.identity.device ?? "")) return { score: 0, evidence: [] };
|
|
727
1059
|
const evidence = [`AetherSDR device identity: ${context.identity.device}`];
|
|
@@ -740,7 +1072,10 @@ var genericObservedDialect = new StandardTciDialect({
|
|
|
740
1072
|
streamLengthSemantics: "auto",
|
|
741
1073
|
supportsStreamChannels: true,
|
|
742
1074
|
supportsTxAudioSource: true,
|
|
1075
|
+
supportsIqStream: false,
|
|
1076
|
+
iqSampleRates: [],
|
|
743
1077
|
driveHasTrx: true,
|
|
1078
|
+
meterAdapter: createUnknownTciMeterAdapter(),
|
|
744
1079
|
detect: (context) => ({
|
|
745
1080
|
score: context.commandNames.has("ready") ? 10 : 0,
|
|
746
1081
|
evidence: ["No vendor-specific match; using observed command shapes"],
|
|
@@ -755,7 +1090,10 @@ var genericObservedDialect = new StandardTciDialect({
|
|
|
755
1090
|
streamLengthSemantics: "auto",
|
|
756
1091
|
supportsStreamChannels: context.commandNames.has("audio_stream_channels"),
|
|
757
1092
|
supportsTxAudioSource: compareTciVersion(version(context) ?? [0], [2, 0]) >= 0,
|
|
1093
|
+
supportsIqStream: context.commandNames.has("iq_samplerate"),
|
|
1094
|
+
iqSampleRates: context.commandNames.has("iq_samplerate") ? [48e3] : [],
|
|
758
1095
|
driveHasTrx,
|
|
1096
|
+
meterAdapter: createUnknownTciMeterAdapter(),
|
|
759
1097
|
detect: genericObservedDialect.detect.bind(genericObservedDialect)
|
|
760
1098
|
});
|
|
761
1099
|
}
|
|
@@ -854,9 +1192,9 @@ function assertValidTciHandshake(commands) {
|
|
|
854
1192
|
}
|
|
855
1193
|
|
|
856
1194
|
// src/transport/WebSocketTransport.ts
|
|
857
|
-
import { EventEmitter } from "eventemitter3";
|
|
1195
|
+
import { EventEmitter as EventEmitter2 } from "eventemitter3";
|
|
858
1196
|
import WebSocket from "ws";
|
|
859
|
-
var WebSocketTciTransport = class extends
|
|
1197
|
+
var WebSocketTciTransport = class extends EventEmitter2 {
|
|
860
1198
|
constructor(url, WebSocketImpl = WebSocket) {
|
|
861
1199
|
super();
|
|
862
1200
|
this.url = url;
|
|
@@ -966,7 +1304,78 @@ function dataToBuffer(data) {
|
|
|
966
1304
|
}
|
|
967
1305
|
|
|
968
1306
|
// src/client/TciClient.ts
|
|
969
|
-
var
|
|
1307
|
+
var TciIqStreamSession = class extends EventEmitter3 {
|
|
1308
|
+
receiver;
|
|
1309
|
+
_appliedSampleRate;
|
|
1310
|
+
callbacks;
|
|
1311
|
+
closed = false;
|
|
1312
|
+
firstFrame;
|
|
1313
|
+
constructor(receiver, appliedSampleRate, callbacks) {
|
|
1314
|
+
super();
|
|
1315
|
+
this.receiver = receiver;
|
|
1316
|
+
this._appliedSampleRate = appliedSampleRate;
|
|
1317
|
+
this.callbacks = callbacks;
|
|
1318
|
+
}
|
|
1319
|
+
get appliedSampleRate() {
|
|
1320
|
+
return this._appliedSampleRate;
|
|
1321
|
+
}
|
|
1322
|
+
async setSampleRate(sampleRate) {
|
|
1323
|
+
if (this.closed) throw new TciError("cancelled", "TCI IQ stream is closed");
|
|
1324
|
+
const result = await this.callbacks.setSampleRate(sampleRate);
|
|
1325
|
+
this._appliedSampleRate = result.applied;
|
|
1326
|
+
return result;
|
|
1327
|
+
}
|
|
1328
|
+
async close() {
|
|
1329
|
+
if (this.closed) return;
|
|
1330
|
+
this.closed = true;
|
|
1331
|
+
this.rejectFirstFrame(new TciError("cancelled", "TCI IQ stream closed before its first frame"));
|
|
1332
|
+
try {
|
|
1333
|
+
await this.callbacks.close();
|
|
1334
|
+
} finally {
|
|
1335
|
+
this.emit("closed");
|
|
1336
|
+
this.removeAllListeners();
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
waitForFirstFrame(timeoutMs) {
|
|
1340
|
+
if (this.firstFrame) return this.firstFrame.promise;
|
|
1341
|
+
let resolvePromise;
|
|
1342
|
+
let rejectPromise;
|
|
1343
|
+
const promise = new Promise((resolve, reject) => {
|
|
1344
|
+
resolvePromise = resolve;
|
|
1345
|
+
rejectPromise = reject;
|
|
1346
|
+
});
|
|
1347
|
+
const timer = setTimeout(() => {
|
|
1348
|
+
this.firstFrame = void 0;
|
|
1349
|
+
rejectPromise(new TciError("command-timeout", `Timed out waiting for TCI IQ frame for receiver ${this.receiver}`));
|
|
1350
|
+
}, timeoutMs);
|
|
1351
|
+
this.firstFrame = { promise, resolve: resolvePromise, reject: rejectPromise, timer };
|
|
1352
|
+
return promise;
|
|
1353
|
+
}
|
|
1354
|
+
_acceptFrame(frame) {
|
|
1355
|
+
if (this.closed || frame.receiver !== this.receiver) return;
|
|
1356
|
+
this._appliedSampleRate = frame.sampleRate;
|
|
1357
|
+
const firstFrame = this.firstFrame;
|
|
1358
|
+
if (firstFrame) {
|
|
1359
|
+
clearTimeout(firstFrame.timer);
|
|
1360
|
+
this.firstFrame = void 0;
|
|
1361
|
+
firstFrame.resolve(frame);
|
|
1362
|
+
}
|
|
1363
|
+
this.emit("frame", frame);
|
|
1364
|
+
}
|
|
1365
|
+
_fail(error) {
|
|
1366
|
+
if (this.closed) return;
|
|
1367
|
+
this.rejectFirstFrame(error);
|
|
1368
|
+
this.emit("error", error);
|
|
1369
|
+
}
|
|
1370
|
+
rejectFirstFrame(error) {
|
|
1371
|
+
const firstFrame = this.firstFrame;
|
|
1372
|
+
if (!firstFrame) return;
|
|
1373
|
+
clearTimeout(firstFrame.timer);
|
|
1374
|
+
this.firstFrame = void 0;
|
|
1375
|
+
firstFrame.reject(error);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var TciClient = class extends EventEmitter3 {
|
|
970
1379
|
options;
|
|
971
1380
|
WebSocketImpl;
|
|
972
1381
|
transportFactory;
|
|
@@ -980,6 +1389,8 @@ var TciClient = class extends EventEmitter2 {
|
|
|
980
1389
|
handshakeError;
|
|
981
1390
|
initializationCommands = [];
|
|
982
1391
|
handshakeWaiter;
|
|
1392
|
+
activeIqSession;
|
|
1393
|
+
activeMeterSession;
|
|
983
1394
|
constructor(options) {
|
|
984
1395
|
super();
|
|
985
1396
|
this.options = {
|
|
@@ -1018,7 +1429,9 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1018
1429
|
split: {},
|
|
1019
1430
|
dialectWarnings: [],
|
|
1020
1431
|
rxSensors: {},
|
|
1021
|
-
txSensors: {}
|
|
1432
|
+
txSensors: {},
|
|
1433
|
+
iq: { activeReceivers: {} },
|
|
1434
|
+
dds: {}
|
|
1022
1435
|
};
|
|
1023
1436
|
this.stateReducers = this.createStateReducers();
|
|
1024
1437
|
}
|
|
@@ -1043,6 +1456,7 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1043
1456
|
}
|
|
1044
1457
|
}
|
|
1045
1458
|
async disconnect(code = 1e3, reason = "client disconnect") {
|
|
1459
|
+
await this.activeMeterSession?.close().catch(() => void 0);
|
|
1046
1460
|
const transport = this.transport;
|
|
1047
1461
|
if (!transport) return;
|
|
1048
1462
|
await transport.disconnect(code, reason);
|
|
@@ -1057,6 +1471,133 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1057
1471
|
getHandshakeResult() {
|
|
1058
1472
|
return this.handshakeResult ? cloneHandshake(this.handshakeResult) : void 0;
|
|
1059
1473
|
}
|
|
1474
|
+
getIqCapabilities() {
|
|
1475
|
+
const dialect = this.requireDialect();
|
|
1476
|
+
return {
|
|
1477
|
+
supported: dialect.supportsIqStream,
|
|
1478
|
+
currentSampleRate: this.state.iq.sampleRate,
|
|
1479
|
+
supportedSampleRates: [...dialect.iqSampleRates]
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
getMeterCapabilities() {
|
|
1483
|
+
if (this.activeMeterSession) return this.activeMeterSession.getCapabilities();
|
|
1484
|
+
const adapter = this.activeDialect?.meterAdapter;
|
|
1485
|
+
return adapter ? cloneMeterCapabilities(adapter.declaredCapabilities) : cloneMeterCapabilities(UNKNOWN_TCI_METER_CAPABILITIES);
|
|
1486
|
+
}
|
|
1487
|
+
async openMeterStream(options = {}) {
|
|
1488
|
+
if (this.activeMeterSession) throw new TciError("protocol-error", "This TCI client already has an active meter stream session");
|
|
1489
|
+
this.requireDialect();
|
|
1490
|
+
const receiver = normalizeNonNegativeInteger(options.receiver ?? this.options.receiver, "meter receiver");
|
|
1491
|
+
const channel = normalizeNonNegativeInteger(options.channel ?? this.options.vfo, "meter channel");
|
|
1492
|
+
const trx = normalizeNonNegativeInteger(options.trx ?? this.options.trx, "meter transmitter");
|
|
1493
|
+
const rxEnabled = options.rx ?? true;
|
|
1494
|
+
const txEnabled = options.tx ?? true;
|
|
1495
|
+
if (!rxEnabled && !txEnabled) throw new TciError("protocol-error", "TCI meter stream must enable RX, TX, or both");
|
|
1496
|
+
const requestedIntervalMs = Math.round(options.intervalMs ?? 300);
|
|
1497
|
+
if (!Number.isFinite(requestedIntervalMs) || requestedIntervalMs <= 0) {
|
|
1498
|
+
throw new TciError("protocol-error", `Invalid TCI meter interval: ${options.intervalMs}`);
|
|
1499
|
+
}
|
|
1500
|
+
const adapter = this.activeDialect?.meterAdapter ?? createUnknownTciMeterAdapter();
|
|
1501
|
+
const interval = adapter.normalizeInterval(requestedIntervalMs);
|
|
1502
|
+
let session;
|
|
1503
|
+
session = new TciMeterStreamSession({
|
|
1504
|
+
receiver,
|
|
1505
|
+
channel,
|
|
1506
|
+
trx,
|
|
1507
|
+
requestedIntervalMs: interval.requestedMs,
|
|
1508
|
+
appliedIntervalMs: interval.appliedMs,
|
|
1509
|
+
rxEnabled,
|
|
1510
|
+
txEnabled,
|
|
1511
|
+
adapter,
|
|
1512
|
+
callbacks: {
|
|
1513
|
+
close: async () => {
|
|
1514
|
+
try {
|
|
1515
|
+
if (this.isConnected()) {
|
|
1516
|
+
if (rxEnabled) {
|
|
1517
|
+
const command = adapter.buildEnableCommand("rx", false, interval.appliedMs ?? interval.requestedMs);
|
|
1518
|
+
await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
|
|
1519
|
+
}
|
|
1520
|
+
if (txEnabled) {
|
|
1521
|
+
const command = adapter.buildEnableCommand("tx", false, interval.appliedMs ?? interval.requestedMs);
|
|
1522
|
+
await this.sendCommand(command.name, command.args, { waitForReply: false }).catch(() => void 0);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
} finally {
|
|
1526
|
+
if (this.activeMeterSession === session) this.activeMeterSession = void 0;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
this.activeMeterSession = session;
|
|
1532
|
+
try {
|
|
1533
|
+
if (rxEnabled) {
|
|
1534
|
+
const command = adapter.buildEnableCommand("rx", true, interval.appliedMs ?? interval.requestedMs);
|
|
1535
|
+
await this.sendCommand(command.name, command.args, { waitForReply: false });
|
|
1536
|
+
}
|
|
1537
|
+
if (txEnabled) {
|
|
1538
|
+
const command = adapter.buildEnableCommand("tx", true, interval.appliedMs ?? interval.requestedMs);
|
|
1539
|
+
await this.sendCommand(command.name, command.args, { waitForReply: false });
|
|
1540
|
+
}
|
|
1541
|
+
return session;
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
await session.close().catch(() => void 0);
|
|
1544
|
+
throw error;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async setIqSampleRate(sampleRate, timeoutMs = this.options.commandTimeoutMs) {
|
|
1548
|
+
const capabilities = this.getIqCapabilities();
|
|
1549
|
+
const requested = Math.round(sampleRate);
|
|
1550
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1551
|
+
if (!Number.isFinite(requested) || requested <= 0) throw new TciError("protocol-error", `Invalid TCI IQ sample rate: ${sampleRate}`);
|
|
1552
|
+
if (capabilities.supportedSampleRates.length > 0 && !capabilities.supportedSampleRates.includes(requested)) {
|
|
1553
|
+
throw new TciError("protocol-error", `TCI IQ sample rate ${requested} is not supported by dialect ${this.requireDialect().id}`);
|
|
1554
|
+
}
|
|
1555
|
+
if (this.state.iq.sampleRate === requested) return writeResult(requested, requested, "state");
|
|
1556
|
+
const waiter = this.waitForCommand((command) => command.name === "iq_samplerate", timeoutMs, "IQ_SAMPLERATE readback");
|
|
1557
|
+
try {
|
|
1558
|
+
await this.sendCommand("IQ_SAMPLERATE", [requested], { waitForReply: false });
|
|
1559
|
+
const reply = await waiter.promise;
|
|
1560
|
+
const applied = parseNumber(reply.args[0]);
|
|
1561
|
+
if (applied === void 0 || applied <= 0) throw new TciError("protocol-error", `Invalid IQ_SAMPLERATE readback: ${reply.raw}`);
|
|
1562
|
+
return writeResult(requested, applied, "reply");
|
|
1563
|
+
} finally {
|
|
1564
|
+
waiter.cancel();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async openIqStream(options = {}) {
|
|
1568
|
+
if (this.activeIqSession) throw new TciError("protocol-error", "This TCI client already has an active IQ stream session");
|
|
1569
|
+
const capabilities = this.getIqCapabilities();
|
|
1570
|
+
if (!capabilities.supported) throw new TciError("protocol-error", `TCI dialect ${this.requireDialect().id} does not support IQ streaming`);
|
|
1571
|
+
const receiver = Math.floor(options.receiver ?? this.options.receiver);
|
|
1572
|
+
if (!Number.isInteger(receiver) || receiver < 0) throw new TciError("protocol-error", `Invalid TCI IQ receiver: ${receiver}`);
|
|
1573
|
+
const requestedRate = options.sampleRate ?? capabilities.currentSampleRate ?? capabilities.supportedSampleRates[0] ?? 48e3;
|
|
1574
|
+
const rateResult = await this.setIqSampleRate(requestedRate);
|
|
1575
|
+
let session;
|
|
1576
|
+
session = new TciIqStreamSession(receiver, rateResult.applied, {
|
|
1577
|
+
setSampleRate: (rate) => this.setIqSampleRate(rate),
|
|
1578
|
+
close: async () => {
|
|
1579
|
+
try {
|
|
1580
|
+
if (this.isConnected()) await this.sendCommand("IQ_STOP", [receiver], { waitForReply: false });
|
|
1581
|
+
} finally {
|
|
1582
|
+
this.state.iq.activeReceivers[String(receiver)] = false;
|
|
1583
|
+
if (this.activeIqSession === session) this.activeIqSession = void 0;
|
|
1584
|
+
this.emitState();
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
this.activeIqSession = session;
|
|
1589
|
+
const firstFrame = session.waitForFirstFrame(options.firstFrameTimeoutMs ?? 5e3);
|
|
1590
|
+
try {
|
|
1591
|
+
await this.sendCommand("IQ_START", [receiver], { waitForReply: false });
|
|
1592
|
+
this.state.iq.activeReceivers[String(receiver)] = true;
|
|
1593
|
+
this.emitState();
|
|
1594
|
+
await firstFrame;
|
|
1595
|
+
return session;
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
await session.close().catch(() => void 0);
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1060
1601
|
async sendCommand(name, args = [], options = {}) {
|
|
1061
1602
|
const raw = formatTciCommand(name, args);
|
|
1062
1603
|
if (options.waitForReply === false) {
|
|
@@ -1527,11 +2068,13 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1527
2068
|
handleText(raw) {
|
|
1528
2069
|
try {
|
|
1529
2070
|
const commands = parseTciText(raw);
|
|
2071
|
+
const receivedAtMs = Date.now();
|
|
1530
2072
|
this.emit("tci:rx", raw, commands);
|
|
1531
2073
|
for (const command of commands) {
|
|
1532
2074
|
if (!this.handshakeResult) this.initializationCommands.push(command);
|
|
1533
2075
|
this.queue.handleCommand(command);
|
|
1534
2076
|
this.applyCommand(command);
|
|
2077
|
+
this.activeMeterSession?._acceptCommand(command, receivedAtMs);
|
|
1535
2078
|
this.emit("command", command);
|
|
1536
2079
|
}
|
|
1537
2080
|
} catch (error) {
|
|
@@ -1546,6 +2089,22 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1546
2089
|
this.emit("tci:binary", frame);
|
|
1547
2090
|
this.emit("binary", frame);
|
|
1548
2091
|
switch (frame.streamType) {
|
|
2092
|
+
case 0 /* IQ_STREAM */: {
|
|
2093
|
+
if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
|
|
2094
|
+
throw new TciError("invalid-frame", "TCI IQ stream frame must contain interleaved I/Q pairs");
|
|
2095
|
+
}
|
|
2096
|
+
this.state.iq.sampleRate = frame.sampleRate;
|
|
2097
|
+
const iqFrame = {
|
|
2098
|
+
frame,
|
|
2099
|
+
receiver: frame.receiver,
|
|
2100
|
+
sampleRate: frame.sampleRate,
|
|
2101
|
+
centerFrequency: this.state.dds[String(frame.receiver)],
|
|
2102
|
+
complexSampleCount: frame.frameCount
|
|
2103
|
+
};
|
|
2104
|
+
this.emit("iqFrame", iqFrame);
|
|
2105
|
+
this.activeIqSession?._acceptFrame(iqFrame);
|
|
2106
|
+
break;
|
|
2107
|
+
}
|
|
1549
2108
|
case 1 /* RX_AUDIO_STREAM */:
|
|
1550
2109
|
this.emit("rxAudioFrame", frame);
|
|
1551
2110
|
break;
|
|
@@ -1623,6 +2182,11 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1623
2182
|
this.state.modulations = args.map((mode) => mode.toLowerCase());
|
|
1624
2183
|
});
|
|
1625
2184
|
reducers.set("vfo", (args) => this.applyVfo(args));
|
|
2185
|
+
reducers.set("dds", (args) => {
|
|
2186
|
+
const receiver = parseNumber(args[0]);
|
|
2187
|
+
const frequency = parseNumber(args[1]);
|
|
2188
|
+
if (receiver !== void 0 && frequency !== void 0 && frequency >= 0) this.state.dds[String(receiver)] = frequency;
|
|
2189
|
+
});
|
|
1626
2190
|
reducers.set("modulation", (args) => this.applyModulation(args));
|
|
1627
2191
|
reducers.set("trx", (args) => this.applyTrx(args));
|
|
1628
2192
|
reducers.set("tune", (args) => this.applyBooleanByFirstArg(this.state.tune, args));
|
|
@@ -1630,6 +2194,7 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1630
2194
|
reducers.set("tune_drive", (args) => this.applyTuneDrive(args));
|
|
1631
2195
|
reducers.set("split_enable", (args) => this.applyBooleanByFirstArg(this.state.split, args));
|
|
1632
2196
|
reducers.set("rx_channel_sensors", (args) => this.applyRxChannelSensors(args));
|
|
2197
|
+
reducers.set("rx_channel_sensors_ex", (args) => this.applyRxChannelSensors(args));
|
|
1633
2198
|
reducers.set("rx_sensors", (args) => this.applyRxSensors(args));
|
|
1634
2199
|
reducers.set("tx_sensors", (args) => this.applyTxSensors(args));
|
|
1635
2200
|
reducers.set("audio_samplerate", (args) => this.updateAudioState({ sampleRate: parseNumber(args[0]) }));
|
|
@@ -1639,6 +2204,15 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1639
2204
|
reducers.set("tx_stream_audio_buffering", (args) => this.updateAudioState({ txBufferingMs: parseNumber(args[0]) }));
|
|
1640
2205
|
reducers.set("audio_start", () => this.updateAudioState({ running: true }));
|
|
1641
2206
|
reducers.set("audio_stop", () => this.updateAudioState({ running: false }));
|
|
2207
|
+
reducers.set("iq_samplerate", (args) => {
|
|
2208
|
+
this.state.iq.sampleRate = parseNumber(args[0]);
|
|
2209
|
+
});
|
|
2210
|
+
reducers.set("iq_start", (args) => {
|
|
2211
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = true;
|
|
2212
|
+
});
|
|
2213
|
+
reducers.set("iq_stop", (args) => {
|
|
2214
|
+
this.state.iq.activeReceivers[String(args[0] ?? this.options.receiver)] = false;
|
|
2215
|
+
});
|
|
1642
2216
|
return reducers;
|
|
1643
2217
|
}
|
|
1644
2218
|
applyVfo(args) {
|
|
@@ -1746,6 +2320,10 @@ var TciClient = class extends EventEmitter2 {
|
|
|
1746
2320
|
this.state.ready = false;
|
|
1747
2321
|
this.queue.setConnected(false);
|
|
1748
2322
|
this.rejectHandshake(new TciError("disconnected", "TCI connection closed during handshake", reason));
|
|
2323
|
+
this.activeIqSession?._fail(new TciError("disconnected", "TCI connection closed while IQ stream was active", reason));
|
|
2324
|
+
this.activeIqSession = void 0;
|
|
2325
|
+
this.activeMeterSession?._fail(new TciError("disconnected", "TCI connection closed while meter stream was active", reason));
|
|
2326
|
+
this.activeMeterSession = void 0;
|
|
1749
2327
|
if (wasConnected) {
|
|
1750
2328
|
this.emit("disconnected", reason);
|
|
1751
2329
|
this.emitState();
|
|
@@ -1772,6 +2350,12 @@ function parseNumber(value) {
|
|
|
1772
2350
|
const number = Number(value);
|
|
1773
2351
|
return Number.isFinite(number) ? number : void 0;
|
|
1774
2352
|
}
|
|
2353
|
+
function normalizeNonNegativeInteger(value, description) {
|
|
2354
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2355
|
+
throw new TciError("protocol-error", `Invalid TCI ${description}: ${value}`);
|
|
2356
|
+
}
|
|
2357
|
+
return value;
|
|
2358
|
+
}
|
|
1775
2359
|
function parseBoolean(value) {
|
|
1776
2360
|
if (value === void 0) {
|
|
1777
2361
|
return void 0;
|
|
@@ -1826,7 +2410,9 @@ function cloneState(state) {
|
|
|
1826
2410
|
dialectWarnings: [...state.dialectWarnings],
|
|
1827
2411
|
rxSensors: cloneNested(state.rxSensors),
|
|
1828
2412
|
txSensors: cloneNested(state.txSensors),
|
|
1829
|
-
audio: state.audio ? { ...state.audio } : void 0
|
|
2413
|
+
audio: state.audio ? { ...state.audio } : void 0,
|
|
2414
|
+
iq: { ...state.iq, activeReceivers: { ...state.iq.activeReceivers } },
|
|
2415
|
+
dds: { ...state.dds }
|
|
1830
2416
|
};
|
|
1831
2417
|
}
|
|
1832
2418
|
function cloneHandshake(result) {
|
|
@@ -1845,22 +2431,29 @@ function cloneNested(value) {
|
|
|
1845
2431
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, { ...item }]));
|
|
1846
2432
|
}
|
|
1847
2433
|
export {
|
|
2434
|
+
StandardTciMeterAdapter,
|
|
1848
2435
|
TCI_STREAM_HEADER_BYTES,
|
|
1849
2436
|
TciClient,
|
|
1850
2437
|
TciCommandQueue,
|
|
1851
2438
|
TciDialectRegistry,
|
|
1852
2439
|
TciError,
|
|
2440
|
+
TciIqStreamSession,
|
|
2441
|
+
TciMeterStreamSession,
|
|
1853
2442
|
TciSampleType,
|
|
1854
2443
|
TciStreamType,
|
|
2444
|
+
UNKNOWN_TCI_METER_CAPABILITIES,
|
|
1855
2445
|
WebSocketTciTransport,
|
|
1856
2446
|
aetherSdrDialect,
|
|
1857
2447
|
assertValidTciHandshake,
|
|
1858
2448
|
buildStreamFrame,
|
|
1859
2449
|
buildTxAudioFrame,
|
|
1860
2450
|
builtInDialects,
|
|
2451
|
+
cloneMeterCapabilities,
|
|
1861
2452
|
commandKey,
|
|
1862
2453
|
compareTciVersion,
|
|
1863
2454
|
createTciClient,
|
|
2455
|
+
createUnknownTciMeterAdapter,
|
|
2456
|
+
decodeInterleavedIq,
|
|
1864
2457
|
defaultTciDialectRegistry,
|
|
1865
2458
|
deinterleaveChannels,
|
|
1866
2459
|
escapeTciText,
|