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.cjs CHANGED
@@ -112,28 +112,37 @@ function parseStreamFrame(input) {
112
112
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
113
113
  const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
114
114
  const sampleType = normalizeSampleType(header[2]);
115
+ const streamType = normalizeStreamType(header[6]);
115
116
  let channels = header[7];
116
117
  const bytesPerSample = sampleTypeBytes(sampleType);
117
118
  const sampleCount = header[5];
118
119
  const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
119
120
  if (channels <= 0) {
120
- const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
121
- if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
122
- throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
121
+ if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
122
+ channels = 1;
123
+ } else {
124
+ const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
125
+ if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
126
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
127
+ }
128
+ channels = inferredChannels;
123
129
  }
124
- channels = inferredChannels;
125
- }
126
- const payloadLength = sampleCount * bytesPerSample * channels;
127
- const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;
128
- if (buffer.byteLength !== expectedLength) {
129
- throw new TciError(
130
- "invalid-frame",
131
- `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
132
- );
133
130
  }
134
- if (payloadLength % (bytesPerSample * channels) !== 0) {
131
+ const payloadLength = actualPayloadLength;
132
+ const alignedFrameBytes = bytesPerSample * channels;
133
+ if (payloadLength % alignedFrameBytes !== 0) {
135
134
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
136
135
  }
136
+ if (streamType !== 3 /* TX_CHRONO */) {
137
+ const expectedPerChannelPayloadLength = sampleCount * bytesPerSample * channels;
138
+ const expectedScalarPayloadLength = sampleCount * bytesPerSample;
139
+ if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {
140
+ throw new TciError(
141
+ "invalid-frame",
142
+ `TCI stream frame length mismatch: header says ${sampleCount} samples (${expectedPerChannelPayloadLength} payload bytes), got ${payloadLength}`
143
+ );
144
+ }
145
+ }
137
146
  return {
138
147
  receiver: header[0],
139
148
  sampleRate: header[1],
@@ -141,7 +150,7 @@ function parseStreamFrame(input) {
141
150
  codec: header[3],
142
151
  crc: header[4],
143
152
  payloadLength,
144
- streamType: normalizeStreamType(header[6]),
153
+ streamType,
145
154
  channels,
146
155
  reserved: header.slice(8),
147
156
  payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
@@ -159,7 +168,11 @@ function buildStreamFrame(options) {
159
168
  if (payload.byteLength % (bytesPerSample * channels) !== 0) {
160
169
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
161
170
  }
162
- const sampleCount = payload.byteLength / bytesPerSample / channels;
171
+ const derivedSampleCount = payload.byteLength / bytesPerSample / channels;
172
+ const sampleCount = options.sampleCount ?? derivedSampleCount;
173
+ if (!Number.isInteger(sampleCount) || sampleCount < 0) {
174
+ throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
175
+ }
163
176
  const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
164
177
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
165
178
  const reserved = options.reserved ?? [];
@@ -624,7 +637,11 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
624
637
  trx: options.trx ?? 0,
625
638
  vfo: options.vfo ?? 0,
626
639
  connectTimeoutMs: options.connectTimeoutMs ?? 5e3,
627
- commandTimeoutMs: options.commandTimeoutMs ?? 1e3
640
+ commandTimeoutMs: options.commandTimeoutMs ?? 1e3,
641
+ writeAckMode: options.writeAckMode ?? "state",
642
+ writeTimeoutMs: options.writeTimeoutMs ?? 3e3,
643
+ writeSettleMs: options.writeSettleMs ?? 0,
644
+ frequencyWriteSettleMs: options.frequencyWriteSettleMs ?? 250
628
645
  };
629
646
  this.WebSocketImpl = options.WebSocketImpl ?? import_ws.default;
630
647
  this.queue = new TciCommandQueue({
@@ -710,15 +727,55 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
710
727
  }
711
728
  return reply;
712
729
  }
713
- async setFrequency(frequencyHz, receiver = this.options.receiver, vfo = this.options.vfo) {
714
- await this.sendCommand("VFO", [receiver, vfo, Math.round(frequencyHz)]);
730
+ async sendStateWrite(name, args, isApplied, description = formatTciCommand(name, args).replace(/;$/, ""), options = {}) {
731
+ const ackMode = options.ackMode ?? this.options.writeAckMode;
732
+ if (ackMode === "reply") {
733
+ await this.sendCommand(name, args, { timeoutMs: options.timeoutMs });
734
+ return;
735
+ }
736
+ if (isApplied(this.getState())) {
737
+ return;
738
+ }
739
+ if (ackMode === "optimistic") {
740
+ await this.sendCommand(name, args, { waitForReply: false });
741
+ return;
742
+ }
743
+ const timeoutMs = options.timeoutMs ?? this.options.writeTimeoutMs;
744
+ const settleMs = options.settleMs ?? this.options.writeSettleMs;
745
+ const waiter = this.waitForState(isApplied, timeoutMs, settleMs, description);
746
+ try {
747
+ await this.sendCommand(name, args, { waitForReply: false });
748
+ await waiter.promise;
749
+ } catch (error) {
750
+ waiter.cancel();
751
+ throw error;
752
+ }
753
+ }
754
+ async setFrequency(frequencyHz, receiver = this.options.receiver, vfo = this.options.vfo, options = {}) {
755
+ const frequency = Math.round(frequencyHz);
756
+ const key = rxVfoKey(receiver, vfo);
757
+ await this.sendStateWrite(
758
+ "VFO",
759
+ [receiver, vfo, frequency],
760
+ (state) => state.frequencies[key] === frequency,
761
+ `VFO:${receiver},${vfo},${frequency}`,
762
+ { settleMs: this.options.frequencyWriteSettleMs, ...options }
763
+ );
715
764
  }
716
765
  async getFrequency(receiver = this.options.receiver, vfo = this.options.vfo) {
717
766
  const reply = await this.request("VFO", [receiver, vfo]);
718
767
  return parseNumber(reply.args[2]) ?? this.state.frequencies[rxVfoKey(receiver, vfo)];
719
768
  }
720
- async setMode(mode, receiver = this.options.receiver) {
721
- await this.sendCommand("MODULATION", [receiver, mode.toUpperCase()]);
769
+ async setMode(mode, receiver = this.options.receiver, options = {}) {
770
+ const normalizedMode = mode.toUpperCase();
771
+ const key = rxVfoKey(receiver, this.options.vfo);
772
+ await this.sendStateWrite(
773
+ "MODULATION",
774
+ [receiver, normalizedMode],
775
+ (state) => state.modes[key]?.toLowerCase() === normalizedMode.toLowerCase(),
776
+ `MODULATION:${receiver},${normalizedMode}`,
777
+ options
778
+ );
722
779
  }
723
780
  async getMode(receiver = this.options.receiver) {
724
781
  const reply = await this.request("MODULATION", [receiver]);
@@ -728,7 +785,13 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
728
785
  async setPtt(enabled, options = {}) {
729
786
  const trx = options.trx ?? this.options.trx;
730
787
  const args = options.source ? [trx, enabled, options.source] : [trx, enabled];
731
- await this.sendCommand("TRX", args);
788
+ await this.sendStateWrite(
789
+ "TRX",
790
+ args,
791
+ (state) => state.ptt[String(trx)] === enabled,
792
+ `TRX:${trx},${enabled}`,
793
+ options
794
+ );
732
795
  }
733
796
  async getPtt(trx = this.options.trx) {
734
797
  const reply = await this.request("TRX", [trx]);
@@ -780,6 +843,20 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
780
843
  const frame = buildTxAudioFrame({ receiver: this.options.receiver, ...options });
781
844
  this.sendRawBinary(frame);
782
845
  }
846
+ sendTxAudioForChrono(request, samples) {
847
+ const channels = Math.max(1, Math.floor(request.channels || 1));
848
+ const targetSampleLength = Math.max(0, Math.floor(request.sampleCount) * channels);
849
+ const output = new Float32Array(targetSampleLength);
850
+ const source = samples instanceof Float32Array ? samples : Float32Array.from(samples);
851
+ output.set(source.subarray(0, output.length));
852
+ this.sendTxAudio({
853
+ receiver: request.receiver,
854
+ sampleRate: request.sampleRate,
855
+ sampleType: request.sampleType,
856
+ channels,
857
+ samples: output
858
+ });
859
+ }
783
860
  async setRxSensorsEnabled(enabled, intervalMs) {
784
861
  const args = intervalMs === void 0 ? [enabled] : [enabled, intervalMs];
785
862
  await this.sendCommand("RX_SENSORS_ENABLE", args, { waitForReply: false });
@@ -797,6 +874,75 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
797
874
  async stopCw() {
798
875
  await this.sendCommand("CW_MACROS_STOP");
799
876
  }
877
+ waitForState(predicate, timeoutMs, settleMs, description) {
878
+ let timeout;
879
+ let settleTimeout;
880
+ let resolved = false;
881
+ let resolvePromise;
882
+ let rejectPromise;
883
+ const cleanup = () => {
884
+ if (timeout) {
885
+ clearTimeout(timeout);
886
+ timeout = void 0;
887
+ }
888
+ if (settleTimeout) {
889
+ clearTimeout(settleTimeout);
890
+ settleTimeout = void 0;
891
+ }
892
+ this.off("state", onState);
893
+ this.off("disconnected", onDisconnected);
894
+ };
895
+ const resolveNow = () => {
896
+ resolved = true;
897
+ cleanup();
898
+ resolvePromise();
899
+ };
900
+ const check = () => {
901
+ if (!predicate(this.getState())) {
902
+ if (settleTimeout) {
903
+ clearTimeout(settleTimeout);
904
+ settleTimeout = void 0;
905
+ }
906
+ return;
907
+ }
908
+ if (settleMs <= 0) {
909
+ resolveNow();
910
+ return;
911
+ }
912
+ if (settleTimeout) {
913
+ return;
914
+ }
915
+ settleTimeout = setTimeout(() => {
916
+ settleTimeout = void 0;
917
+ if (predicate(this.getState())) {
918
+ resolveNow();
919
+ }
920
+ }, settleMs);
921
+ };
922
+ const onState = () => check();
923
+ const onDisconnected = () => {
924
+ if (resolved) {
925
+ return;
926
+ }
927
+ cleanup();
928
+ rejectPromise(new TciError("disconnected", `Disconnected while waiting for TCI state ${description}`));
929
+ };
930
+ const promise = new Promise((resolve, reject) => {
931
+ resolvePromise = resolve;
932
+ rejectPromise = reject;
933
+ timeout = setTimeout(() => {
934
+ cleanup();
935
+ reject(new TciError("command-timeout", `Timed out waiting for TCI state ${description}`));
936
+ }, timeoutMs);
937
+ this.on("state", onState);
938
+ this.on("disconnected", onDisconnected);
939
+ check();
940
+ });
941
+ return {
942
+ promise,
943
+ cancel: cleanup
944
+ };
945
+ }
800
946
  waitForOpen(ws) {
801
947
  return new Promise((resolve, reject) => {
802
948
  const timer = setTimeout(() => {
@@ -847,6 +993,7 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
847
993
  if (!ws || ws.readyState !== import_ws.default.OPEN) {
848
994
  throw new TciError("not-connected", "TCI socket is not connected");
849
995
  }
996
+ this.emit("tci:tx", raw);
850
997
  await new Promise((resolve, reject) => {
851
998
  ws.send(raw, (error) => error ? reject(error) : resolve());
852
999
  });
@@ -864,7 +1011,9 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
864
1011
  this.handleBinary(data);
865
1012
  return;
866
1013
  }
867
- const commands = parseTciText(dataToBuffer(data));
1014
+ const raw = dataToBuffer(data).toString("utf8");
1015
+ const commands = parseTciText(raw);
1016
+ this.emit("tci:rx", raw, commands);
868
1017
  for (const command of commands) {
869
1018
  this.queue.handleCommand(command);
870
1019
  this.applyCommand(command);
@@ -876,6 +1025,7 @@ var TciClient = class extends import_eventemitter3.EventEmitter {
876
1025
  }
877
1026
  handleBinary(data) {
878
1027
  const frame = parseStreamFrame(dataToBuffer(data));
1028
+ this.emit("tci:binary", frame);
879
1029
  this.emit("binary", frame);
880
1030
  switch (frame.streamType) {
881
1031
  case 1 /* RX_AUDIO_STREAM */: