tci-client-node 0.1.0 → 0.1.2

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/dist/index.js CHANGED
@@ -48,28 +48,37 @@ function parseStreamFrame(input) {
48
48
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
49
49
  const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
50
50
  const sampleType = normalizeSampleType(header[2]);
51
+ const streamType = normalizeStreamType(header[6]);
51
52
  let channels = header[7];
52
53
  const bytesPerSample = sampleTypeBytes(sampleType);
53
54
  const sampleCount = header[5];
54
55
  const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
55
56
  if (channels <= 0) {
56
- const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
57
- if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
58
- throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
57
+ if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
58
+ channels = 1;
59
+ } else {
60
+ const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
61
+ if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
62
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
63
+ }
64
+ channels = inferredChannels;
59
65
  }
60
- channels = inferredChannels;
61
- }
62
- const payloadLength = sampleCount * bytesPerSample * channels;
63
- const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;
64
- if (buffer.byteLength !== expectedLength) {
65
- throw new TciError(
66
- "invalid-frame",
67
- `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
68
- );
69
66
  }
70
- if (payloadLength % (bytesPerSample * channels) !== 0) {
67
+ const payloadLength = actualPayloadLength;
68
+ const alignedFrameBytes = bytesPerSample * channels;
69
+ if (payloadLength % alignedFrameBytes !== 0) {
71
70
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
72
71
  }
72
+ if (streamType !== 3 /* TX_CHRONO */) {
73
+ const expectedPerChannelPayloadLength = sampleCount * bytesPerSample * channels;
74
+ const expectedScalarPayloadLength = sampleCount * bytesPerSample;
75
+ if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {
76
+ throw new TciError(
77
+ "invalid-frame",
78
+ `TCI stream frame length mismatch: header says ${sampleCount} samples (${expectedPerChannelPayloadLength} payload bytes), got ${payloadLength}`
79
+ );
80
+ }
81
+ }
73
82
  return {
74
83
  receiver: header[0],
75
84
  sampleRate: header[1],
@@ -77,7 +86,7 @@ function parseStreamFrame(input) {
77
86
  codec: header[3],
78
87
  crc: header[4],
79
88
  payloadLength,
80
- streamType: normalizeStreamType(header[6]),
89
+ streamType,
81
90
  channels,
82
91
  reserved: header.slice(8),
83
92
  payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
@@ -95,7 +104,11 @@ function buildStreamFrame(options) {
95
104
  if (payload.byteLength % (bytesPerSample * channels) !== 0) {
96
105
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
97
106
  }
98
- const sampleCount = payload.byteLength / bytesPerSample / channels;
107
+ const derivedSampleCount = payload.byteLength / bytesPerSample / channels;
108
+ const sampleCount = options.sampleCount ?? derivedSampleCount;
109
+ if (!Number.isInteger(sampleCount) || sampleCount < 0) {
110
+ throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
111
+ }
99
112
  const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
100
113
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
101
114
  const reserved = options.reserved ?? [];
@@ -560,7 +573,11 @@ var TciClient = class extends EventEmitter {
560
573
  trx: options.trx ?? 0,
561
574
  vfo: options.vfo ?? 0,
562
575
  connectTimeoutMs: options.connectTimeoutMs ?? 5e3,
563
- commandTimeoutMs: options.commandTimeoutMs ?? 1e3
576
+ commandTimeoutMs: options.commandTimeoutMs ?? 1e3,
577
+ writeAckMode: options.writeAckMode ?? "state",
578
+ writeTimeoutMs: options.writeTimeoutMs ?? 3e3,
579
+ writeSettleMs: options.writeSettleMs ?? 0,
580
+ frequencyWriteSettleMs: options.frequencyWriteSettleMs ?? 250
564
581
  };
565
582
  this.WebSocketImpl = options.WebSocketImpl ?? WebSocket;
566
583
  this.queue = new TciCommandQueue({
@@ -646,15 +663,55 @@ var TciClient = class extends EventEmitter {
646
663
  }
647
664
  return reply;
648
665
  }
649
- async setFrequency(frequencyHz, receiver = this.options.receiver, vfo = this.options.vfo) {
650
- await this.sendCommand("VFO", [receiver, vfo, Math.round(frequencyHz)]);
666
+ async sendStateWrite(name, args, isApplied, description = formatTciCommand(name, args).replace(/;$/, ""), options = {}) {
667
+ const ackMode = options.ackMode ?? this.options.writeAckMode;
668
+ if (ackMode === "reply") {
669
+ await this.sendCommand(name, args, { timeoutMs: options.timeoutMs });
670
+ return;
671
+ }
672
+ if (isApplied(this.getState())) {
673
+ return;
674
+ }
675
+ if (ackMode === "optimistic") {
676
+ await this.sendCommand(name, args, { waitForReply: false });
677
+ return;
678
+ }
679
+ const timeoutMs = options.timeoutMs ?? this.options.writeTimeoutMs;
680
+ const settleMs = options.settleMs ?? this.options.writeSettleMs;
681
+ const waiter = this.waitForState(isApplied, timeoutMs, settleMs, description);
682
+ try {
683
+ await this.sendCommand(name, args, { waitForReply: false });
684
+ await waiter.promise;
685
+ } catch (error) {
686
+ waiter.cancel();
687
+ throw error;
688
+ }
689
+ }
690
+ async setFrequency(frequencyHz, receiver = this.options.receiver, vfo = this.options.vfo, options = {}) {
691
+ const frequency = Math.round(frequencyHz);
692
+ const key = rxVfoKey(receiver, vfo);
693
+ await this.sendStateWrite(
694
+ "VFO",
695
+ [receiver, vfo, frequency],
696
+ (state) => state.frequencies[key] === frequency,
697
+ `VFO:${receiver},${vfo},${frequency}`,
698
+ { settleMs: this.options.frequencyWriteSettleMs, ...options }
699
+ );
651
700
  }
652
701
  async getFrequency(receiver = this.options.receiver, vfo = this.options.vfo) {
653
702
  const reply = await this.request("VFO", [receiver, vfo]);
654
703
  return parseNumber(reply.args[2]) ?? this.state.frequencies[rxVfoKey(receiver, vfo)];
655
704
  }
656
- async setMode(mode, receiver = this.options.receiver) {
657
- await this.sendCommand("MODULATION", [receiver, mode.toUpperCase()]);
705
+ async setMode(mode, receiver = this.options.receiver, options = {}) {
706
+ const normalizedMode = mode.toUpperCase();
707
+ const key = rxVfoKey(receiver, this.options.vfo);
708
+ await this.sendStateWrite(
709
+ "MODULATION",
710
+ [receiver, normalizedMode],
711
+ (state) => state.modes[key]?.toLowerCase() === normalizedMode.toLowerCase(),
712
+ `MODULATION:${receiver},${normalizedMode}`,
713
+ options
714
+ );
658
715
  }
659
716
  async getMode(receiver = this.options.receiver) {
660
717
  const reply = await this.request("MODULATION", [receiver]);
@@ -664,7 +721,13 @@ var TciClient = class extends EventEmitter {
664
721
  async setPtt(enabled, options = {}) {
665
722
  const trx = options.trx ?? this.options.trx;
666
723
  const args = options.source ? [trx, enabled, options.source] : [trx, enabled];
667
- await this.sendCommand("TRX", args);
724
+ await this.sendStateWrite(
725
+ "TRX",
726
+ args,
727
+ (state) => state.ptt[String(trx)] === enabled,
728
+ `TRX:${trx},${enabled}`,
729
+ options
730
+ );
668
731
  }
669
732
  async getPtt(trx = this.options.trx) {
670
733
  const reply = await this.request("TRX", [trx]);
@@ -716,6 +779,20 @@ var TciClient = class extends EventEmitter {
716
779
  const frame = buildTxAudioFrame({ receiver: this.options.receiver, ...options });
717
780
  this.sendRawBinary(frame);
718
781
  }
782
+ sendTxAudioForChrono(request, samples) {
783
+ const channels = Math.max(1, Math.floor(request.channels || 1));
784
+ const targetSampleLength = Math.max(0, Math.floor(request.sampleCount) * channels);
785
+ const output = new Float32Array(targetSampleLength);
786
+ const source = samples instanceof Float32Array ? samples : Float32Array.from(samples);
787
+ output.set(source.subarray(0, output.length));
788
+ this.sendTxAudio({
789
+ receiver: request.receiver,
790
+ sampleRate: request.sampleRate,
791
+ sampleType: request.sampleType,
792
+ channels,
793
+ samples: output
794
+ });
795
+ }
719
796
  async setRxSensorsEnabled(enabled, intervalMs) {
720
797
  const args = intervalMs === void 0 ? [enabled] : [enabled, intervalMs];
721
798
  await this.sendCommand("RX_SENSORS_ENABLE", args, { waitForReply: false });
@@ -733,6 +810,75 @@ var TciClient = class extends EventEmitter {
733
810
  async stopCw() {
734
811
  await this.sendCommand("CW_MACROS_STOP");
735
812
  }
813
+ waitForState(predicate, timeoutMs, settleMs, description) {
814
+ let timeout;
815
+ let settleTimeout;
816
+ let resolved = false;
817
+ let resolvePromise;
818
+ let rejectPromise;
819
+ const cleanup = () => {
820
+ if (timeout) {
821
+ clearTimeout(timeout);
822
+ timeout = void 0;
823
+ }
824
+ if (settleTimeout) {
825
+ clearTimeout(settleTimeout);
826
+ settleTimeout = void 0;
827
+ }
828
+ this.off("state", onState);
829
+ this.off("disconnected", onDisconnected);
830
+ };
831
+ const resolveNow = () => {
832
+ resolved = true;
833
+ cleanup();
834
+ resolvePromise();
835
+ };
836
+ const check = () => {
837
+ if (!predicate(this.getState())) {
838
+ if (settleTimeout) {
839
+ clearTimeout(settleTimeout);
840
+ settleTimeout = void 0;
841
+ }
842
+ return;
843
+ }
844
+ if (settleMs <= 0) {
845
+ resolveNow();
846
+ return;
847
+ }
848
+ if (settleTimeout) {
849
+ return;
850
+ }
851
+ settleTimeout = setTimeout(() => {
852
+ settleTimeout = void 0;
853
+ if (predicate(this.getState())) {
854
+ resolveNow();
855
+ }
856
+ }, settleMs);
857
+ };
858
+ const onState = () => check();
859
+ const onDisconnected = () => {
860
+ if (resolved) {
861
+ return;
862
+ }
863
+ cleanup();
864
+ rejectPromise(new TciError("disconnected", `Disconnected while waiting for TCI state ${description}`));
865
+ };
866
+ const promise = new Promise((resolve, reject) => {
867
+ resolvePromise = resolve;
868
+ rejectPromise = reject;
869
+ timeout = setTimeout(() => {
870
+ cleanup();
871
+ reject(new TciError("command-timeout", `Timed out waiting for TCI state ${description}`));
872
+ }, timeoutMs);
873
+ this.on("state", onState);
874
+ this.on("disconnected", onDisconnected);
875
+ check();
876
+ });
877
+ return {
878
+ promise,
879
+ cancel: cleanup
880
+ };
881
+ }
736
882
  waitForOpen(ws) {
737
883
  return new Promise((resolve, reject) => {
738
884
  const timer = setTimeout(() => {
@@ -783,6 +929,7 @@ var TciClient = class extends EventEmitter {
783
929
  if (!ws || ws.readyState !== WebSocket.OPEN) {
784
930
  throw new TciError("not-connected", "TCI socket is not connected");
785
931
  }
932
+ this.emit("tci:tx", raw);
786
933
  await new Promise((resolve, reject) => {
787
934
  ws.send(raw, (error) => error ? reject(error) : resolve());
788
935
  });
@@ -800,7 +947,9 @@ var TciClient = class extends EventEmitter {
800
947
  this.handleBinary(data);
801
948
  return;
802
949
  }
803
- const commands = parseTciText(dataToBuffer(data));
950
+ const raw = dataToBuffer(data).toString("utf8");
951
+ const commands = parseTciText(raw);
952
+ this.emit("tci:rx", raw, commands);
804
953
  for (const command of commands) {
805
954
  this.queue.handleCommand(command);
806
955
  this.applyCommand(command);
@@ -812,6 +961,7 @@ var TciClient = class extends EventEmitter {
812
961
  }
813
962
  handleBinary(data) {
814
963
  const frame = parseStreamFrame(dataToBuffer(data));
964
+ this.emit("tci:binary", frame);
815
965
  this.emit("binary", frame);
816
966
  switch (frame.streamType) {
817
967
  case 1 /* RX_AUDIO_STREAM */: