icom-wlan-node 0.1.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.
@@ -0,0 +1,912 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.IcomControl = void 0;
37
+ const events_1 = require("events");
38
+ const IcomPackets_1 = require("../core/IcomPackets");
39
+ const debug_1 = require("../utils/debug");
40
+ const Session_1 = require("../core/Session");
41
+ const IcomCiv_1 = require("./IcomCiv");
42
+ const IcomAudio_1 = require("./IcomAudio");
43
+ const IcomRigCommands_1 = require("./IcomRigCommands");
44
+ const IcomConstants_1 = require("./IcomConstants");
45
+ const bcd_1 = require("../utils/bcd");
46
+ class IcomControl {
47
+ constructor(options) {
48
+ this.ev = new events_1.EventEmitter();
49
+ this.rigName = '';
50
+ this.macAddress = Buffer.alloc(6);
51
+ this.civAssembleBuf = Buffer.alloc(0); // CIV stream reassembler
52
+ this.options = options;
53
+ this.sess = new Session_1.Session({ ip: options.control.ip, port: options.control.port }, {
54
+ onData: (data) => this.onData(data),
55
+ onSendError: (e) => this.ev.emit('error', e)
56
+ });
57
+ // Pre-open local CIV/Audio sessions to obtain local ports before 0x90
58
+ this.civSess = new Session_1.Session({ ip: options.control.ip, port: 0 }, { onData: (b) => this.onCivData(b), onSendError: (e) => this.ev.emit('error', e) });
59
+ this.audioSess = new Session_1.Session({ ip: options.control.ip, port: 0 }, { onData: (b) => this.onAudioData(b), onSendError: (e) => this.ev.emit('error', e) });
60
+ this.civSess.open();
61
+ this.audioSess.open();
62
+ this.civ = new IcomCiv_1.IcomCiv(this.civSess);
63
+ this.audio = new IcomAudio_1.IcomAudio(this.audioSess);
64
+ }
65
+ get events() { return this.ev; }
66
+ async connect() {
67
+ // Initialize readiness promises
68
+ this.loginReady = new Promise(resolve => { this.resolveLoginReady = resolve; });
69
+ this.civReady = new Promise(resolve => { this.resolveCivReady = resolve; });
70
+ this.audioReady = new Promise(resolve => { this.resolveAudioReady = resolve; });
71
+ this.sess.open();
72
+ this.sess.startAreYouThere();
73
+ // Wait for all sub-sessions to be ready
74
+ await Promise.all([this.loginReady, this.civReady, this.audioReady]);
75
+ (0, debug_1.dbg)('All sessions ready (login + civ + audio)');
76
+ }
77
+ async disconnect() {
78
+ // 1. Stop all timers first to prevent interference
79
+ if (this.tokenTimer) {
80
+ clearInterval(this.tokenTimer);
81
+ this.tokenTimer = undefined;
82
+ }
83
+ this.stopMeterPolling();
84
+ this.sess.stopTimers();
85
+ if (this.civSess)
86
+ this.civSess.stopTimers();
87
+ if (this.audioSess)
88
+ this.audioSess.stopTimers();
89
+ // 2. Send DELETE token packet
90
+ const del = IcomPackets_1.TokenPacket.build(0, this.sess.localId, this.sess.remoteId, IcomPackets_1.TokenType.DELETE, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken);
91
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
92
+ this.sess.sendTracked(del);
93
+ // 3. Send CMD_DISCONNECT to all sessions
94
+ this.sess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.DISCONNECT, 0, this.sess.localId, this.sess.remoteId));
95
+ if (this.civSess) {
96
+ this.civ.sendOpenClose(false);
97
+ this.civSess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.DISCONNECT, 0, this.civSess.localId, this.civSess.remoteId));
98
+ }
99
+ if (this.audioSess) {
100
+ this.audioSess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.DISCONNECT, 0, this.audioSess.localId, this.audioSess.remoteId));
101
+ }
102
+ // 4. Wait 200ms to ensure UDP packets are sent before closing sockets
103
+ await new Promise(resolve => setTimeout(resolve, 200));
104
+ // 5. Stop streams and close sockets
105
+ this.civ.stop();
106
+ this.audio.stop(); // Stop continuous audio transmission
107
+ this.sess.close();
108
+ if (this.civSess)
109
+ this.civSess.close();
110
+ if (this.audioSess)
111
+ this.audioSess.close();
112
+ }
113
+ sendCiv(data) { this.civ.sendCivData(data); }
114
+ /**
115
+ * Set PTT (Push-To-Talk) state
116
+ * @param on - true to key transmitter, false to unkey
117
+ */
118
+ async setPtt(on) {
119
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
120
+ const rigAddr = this.civ.civAddress & 0xff;
121
+ // Send CIV PTT command
122
+ const frame = IcomRigCommands_1.IcomRigCommands.setPTT(ctrAddr, rigAddr, on);
123
+ this.sendCiv(frame);
124
+ // Set audio PTT flag (like Java: audioUdp.isPttOn = on)
125
+ this.audio.isPttOn = on;
126
+ // Start/stop meter polling to match Java behavior
127
+ if (on) {
128
+ this.startMeterPolling();
129
+ }
130
+ else {
131
+ this.stopMeterPolling();
132
+ }
133
+ // Add trailing silence when PTT off (like Java implementation)
134
+ if (!on) {
135
+ // Add 5 trailing silence frames before clearing queue
136
+ const silence = new Int16Array(240); // TX_BUFFER_SIZE = 240
137
+ for (let i = 0; i < 5; i++) {
138
+ this.audio.queue.push(silence);
139
+ }
140
+ }
141
+ }
142
+ /**
143
+ * Set operating frequency
144
+ * @param hz - Frequency in Hz
145
+ */
146
+ async setFrequency(hz) {
147
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
148
+ const rigAddr = this.civ.civAddress & 0xff;
149
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setFrequency(ctrAddr, rigAddr, hz));
150
+ }
151
+ /**
152
+ * Set operating mode
153
+ * @param mode - Operating mode (LSB, USB, AM, CW, RTTY, FM, WFM, CW_R, RTTY_R, DV)
154
+ * @param options - Mode options (dataMode for digital modes like USB-D)
155
+ * @example
156
+ * // Set USB mode
157
+ * await rig.setMode('USB');
158
+ * // Set USB-D (data mode) for FT8
159
+ * await rig.setMode('USB', { dataMode: true });
160
+ */
161
+ async setMode(mode, options) {
162
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
163
+ const rigAddr = this.civ.civAddress & 0xff;
164
+ const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getModeCode)(mode) : mode;
165
+ if (options?.dataMode) {
166
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setOperationDataMode(ctrAddr, rigAddr, modeCode));
167
+ }
168
+ else {
169
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setMode(ctrAddr, rigAddr, modeCode));
170
+ }
171
+ }
172
+ /**
173
+ * Read current operating frequency
174
+ * @param options - Query options (timeout in ms, default 3000)
175
+ * @returns Frequency in Hz, or null if timeout/error
176
+ * @example
177
+ * const hz = await rig.readOperatingFrequency({ timeout: 5000 });
178
+ * console.log(`Frequency: ${hz} Hz`);
179
+ */
180
+ async readOperatingFrequency(options) {
181
+ const timeoutMs = options?.timeout ?? 3000;
182
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
183
+ const rigAddr = this.civ.civAddress & 0xff;
184
+ const req = IcomRigCommands_1.IcomRigCommands.readOperatingFrequency(ctrAddr, rigAddr);
185
+ const resp = await this.waitForCivFrame((frame) => IcomControl.isReplyOf(frame, 0x03, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
186
+ if (!resp)
187
+ return null;
188
+ const freq = IcomControl.parseIcomFreqFromReply(resp);
189
+ return freq;
190
+ }
191
+ /**
192
+ * Read current operating mode and filter
193
+ * @returns { mode: number, filter?: number } or null
194
+ */
195
+ async readOperatingMode(options) {
196
+ const timeoutMs = options?.timeout ?? 3000;
197
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
198
+ const rigAddr = this.civ.civAddress & 0xff;
199
+ const req = IcomRigCommands_1.IcomRigCommands.readOperatingMode(ctrAddr, rigAddr);
200
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x04, [], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
201
+ if (!resp)
202
+ return null;
203
+ // Expect FE FE [ctr] [rig] 0x04 [mode] [filter] FD (some rigs may omit filter)
204
+ const mode = resp.length > 5 ? resp[5] : undefined;
205
+ const filter = resp.length > 6 ? resp[6] : undefined;
206
+ if (mode === undefined)
207
+ return null;
208
+ // Map names using constants
209
+ const { getModeString, getFilterString } = await Promise.resolve().then(() => __importStar(require('./IcomConstants')));
210
+ const modeName = getModeString(mode);
211
+ const filterName = getFilterString(filter);
212
+ return { mode, filter, modeName, filterName };
213
+ }
214
+ /**
215
+ * Read current transmit frequency (when TX)
216
+ */
217
+ async readTransmitFrequency(options) {
218
+ const timeoutMs = options?.timeout ?? 3000;
219
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
220
+ const rigAddr = this.civ.civAddress & 0xff;
221
+ const req = IcomRigCommands_1.IcomRigCommands.readTransmitFrequency(ctrAddr, rigAddr);
222
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1c, [0x03], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
223
+ if (!resp)
224
+ return null;
225
+ // Parse BCD like readOperatingFrequency, but starting after [0x1c, 0x03]
226
+ // Find 0x1c position and read next 2 bytes (0x03 + 5 BCD bytes)
227
+ let idx = resp.indexOf(0x1c, 4);
228
+ if (idx < 0 || idx + 6 >= resp.length)
229
+ idx = 4;
230
+ if (idx + 6 >= resp.length)
231
+ return null;
232
+ // After 0x1c 0x03, we expect 5 BCD bytes
233
+ if (resp[idx + 1] !== 0x03)
234
+ return null;
235
+ const d0 = resp[idx + 2];
236
+ const d1 = resp[idx + 3];
237
+ const d2 = resp[idx + 4];
238
+ const d3 = resp[idx + 5];
239
+ const d4 = resp[idx + 6];
240
+ const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
241
+ const v0 = bcdToInt(d0);
242
+ const v1 = bcdToInt(d1);
243
+ const v2 = bcdToInt(d2);
244
+ const v3 = bcdToInt(d3);
245
+ const v4 = bcdToInt(d4);
246
+ const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
247
+ return hz;
248
+ }
249
+ /**
250
+ * Read transceiver state (TX/RX) via 0x1A 0x00 0x48
251
+ * Note: Java comments mark this as not recommended; use with caution.
252
+ */
253
+ async readTransceiverState(options) {
254
+ const timeoutMs = options?.timeout ?? 3000;
255
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
256
+ const rigAddr = this.civ.civAddress & 0xff;
257
+ const req = IcomRigCommands_1.IcomRigCommands.readTransceiverState(ctrAddr, rigAddr);
258
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x00, 0x48], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
259
+ if (!resp)
260
+ return null;
261
+ // Heuristic: take first data byte after subcmd2 as state
262
+ const pos = 5 + 2; // after 0x1a [0x00,0x48]
263
+ const state = resp.length > pos ? resp[pos] : undefined;
264
+ if (state === undefined)
265
+ return 'UNKNOWN';
266
+ if (state === 0x01)
267
+ return 'TX';
268
+ if (state === 0x00)
269
+ return 'RX';
270
+ return 'UNKNOWN';
271
+ }
272
+ /**
273
+ * Read band edge data (0x02). Format may vary by rig; returns raw data bytes after command.
274
+ */
275
+ async readBandEdges(options) {
276
+ const timeoutMs = options?.timeout ?? 3000;
277
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
278
+ const rigAddr = this.civ.civAddress & 0xff;
279
+ const req = IcomRigCommands_1.IcomRigCommands.readBandEdges(ctrAddr, rigAddr);
280
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x02, [], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
281
+ if (!resp)
282
+ return null;
283
+ // Return raw payload bytes after command
284
+ return Buffer.from(resp.subarray(5, resp.length - 1));
285
+ }
286
+ /**
287
+ * Read SWR (Standing Wave Ratio) meter
288
+ * @param options - Query options (timeout in ms, default 3000)
289
+ * @returns SWR reading with raw value, calculated SWR, and alert status
290
+ * @example
291
+ * const swr = await rig.readSWR({ timeout: 2000 });
292
+ * if (swr) {
293
+ * console.log(`SWR: ${swr.swr.toFixed(2)} ${swr.alert ? '⚠️ HIGH' : '✓'}`);
294
+ * }
295
+ */
296
+ async readSWR(options) {
297
+ const timeoutMs = options?.timeout ?? 3000;
298
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
299
+ const rigAddr = this.civ.civAddress & 0xff;
300
+ const req = IcomRigCommands_1.IcomRigCommands.getSWRState(ctrAddr, rigAddr);
301
+ const resp = await this.waitForCivFrame((frame) => IcomControl.isMeterReply(frame, 0x12, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
302
+ const raw = IcomControl.extractMeterData(resp);
303
+ if (raw === null)
304
+ return null;
305
+ return {
306
+ raw,
307
+ swr: raw / 100,
308
+ alert: raw >= IcomConstants_1.METER_THRESHOLDS.SWR_ALERT
309
+ };
310
+ }
311
+ /**
312
+ * Read ALC (Automatic Level Control) meter
313
+ * @param options - Query options (timeout in ms, default 3000)
314
+ * @returns ALC reading with raw value, percent, and alert status
315
+ * @example
316
+ * const alc = await rig.readALC({ timeout: 2000 });
317
+ * if (alc) {
318
+ * console.log(`ALC: ${alc.percent.toFixed(1)}% ${alc.alert ? '⚠️ HIGH' : '✓'}`);
319
+ * }
320
+ */
321
+ async readALC(options) {
322
+ const timeoutMs = options?.timeout ?? 3000;
323
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
324
+ const rigAddr = this.civ.civAddress & 0xff;
325
+ const req = IcomRigCommands_1.IcomRigCommands.getALCState(ctrAddr, rigAddr);
326
+ const resp = await this.waitForCivFrame((frame) => IcomControl.isMeterReply(frame, 0x13, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
327
+ const raw = IcomControl.extractMeterData(resp);
328
+ if (raw === null)
329
+ return null;
330
+ return {
331
+ raw,
332
+ percent: (raw / IcomConstants_1.METER_THRESHOLDS.ALC_MAX) * 100,
333
+ alert: raw > IcomConstants_1.METER_THRESHOLDS.ALC_ALERT_MAX
334
+ };
335
+ }
336
+ /**
337
+ * Get WLAN connector audio level setting
338
+ * @param options - Query options (timeout in ms, default 3000)
339
+ * @returns WLAN level reading with raw value and percent
340
+ * @example
341
+ * const level = await rig.getConnectorWLanLevel({ timeout: 2000 });
342
+ * if (level) {
343
+ * console.log(`WLAN Level: ${level.percent.toFixed(1)}%`);
344
+ * }
345
+ */
346
+ async getConnectorWLanLevel(options) {
347
+ const timeoutMs = options?.timeout ?? 3000;
348
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
349
+ const rigAddr = this.civ.civAddress & 0xff;
350
+ const req = IcomRigCommands_1.IcomRigCommands.getConnectorWLanLevel(ctrAddr, rigAddr);
351
+ const resp = await this.waitForCivFrame((frame) => IcomControl.matchCommandFrame(frame, 0x1a, [0x05, 0x01, 0x17], ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
352
+ const raw = IcomControl.extractMeterData(resp);
353
+ if (raw === null)
354
+ return null;
355
+ return {
356
+ raw,
357
+ percent: (raw / IcomConstants_1.METER_THRESHOLDS.WLAN_LEVEL_MAX) * 100
358
+ };
359
+ }
360
+ /**
361
+ * Read generic level meter (CI-V 0x15/0x02), raw 0-255.
362
+ * Many rigs return two bytes and the low byte is the level.
363
+ */
364
+ async getLevelMeter(options) {
365
+ const timeoutMs = options?.timeout ?? 3000;
366
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
367
+ const rigAddr = this.civ.civAddress & 0xff;
368
+ const req = IcomRigCommands_1.IcomRigCommands.getLevelMeter(ctrAddr, rigAddr);
369
+ const resp = await this.waitForCivFrame((frame) => IcomControl.isMeterReply(frame, 0x02, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
370
+ if (!resp)
371
+ return null;
372
+ const data = resp.subarray(6, resp.length - 1);
373
+ if (data.length === 0)
374
+ return null;
375
+ const raw = data[data.length - 1] & 0xff; // use low byte as 0-255 level
376
+ return {
377
+ raw,
378
+ percent: (raw / 255) * 100
379
+ };
380
+ }
381
+ /**
382
+ * Set WLAN connector audio level
383
+ * @param level - Audio level (0-255)
384
+ */
385
+ async setConnectorWLanLevel(level) {
386
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
387
+ const rigAddr = this.civ.civAddress & 0xff;
388
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorWLanLevel(ctrAddr, rigAddr, level));
389
+ }
390
+ /**
391
+ * Set connector data routing mode
392
+ * @param mode - Data routing mode (MIC, ACC, USB, WLAN)
393
+ * @example
394
+ * // Route audio to WLAN
395
+ * await rig.setConnectorDataMode('WLAN');
396
+ */
397
+ async setConnectorDataMode(mode) {
398
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
399
+ const rigAddr = this.civ.civAddress & 0xff;
400
+ const modeCode = typeof mode === 'string' ? (0, IcomConstants_1.getConnectorModeCode)(mode) : mode;
401
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.setConnectorDataMode(ctrAddr, rigAddr, modeCode));
402
+ }
403
+ static isReplyOf(frame, cmd, ctrAddr, rigAddr) {
404
+ // typical reply FE FE [ctr] [rig] cmd ... FD
405
+ return frame.length >= 7 && frame[0] === 0xfe && frame[1] === 0xfe && frame[4] === (cmd & 0xff);
406
+ }
407
+ /**
408
+ * Extract meter data from CI-V response frame
409
+ * CI-V format: FE FE [ctr] [rig] [cmd] [subcmd] [data0] [data1] FD
410
+ * @param frame - CI-V response buffer
411
+ * @returns Parsed BCD integer value, or null if invalid
412
+ */
413
+ static extractMeterData(frame) {
414
+ if (!frame || frame.length < 9)
415
+ return null;
416
+ // Extract 2-byte BCD data at position 6-7 of the CI-V frame (FE FE [ctr] [rig] 0x15 [sub] [b0] [b1] FD)
417
+ const bcdData = frame.subarray(6, 8);
418
+ return (0, bcd_1.parseTwoByteBcd)(bcdData);
419
+ }
420
+ static matchCommand(frame, cmd, tail) {
421
+ // FE FE ?? ?? cmd ... tail... FD
422
+ if (!(frame.length >= 7 && frame[0] === 0xfe && frame[1] === 0xfe && frame[4] === (cmd & 0xff)))
423
+ return false;
424
+ if (tail.length === 0)
425
+ return true;
426
+ for (let i = 0; i + tail.length < frame.length; i++) {
427
+ let ok = true;
428
+ for (let j = 0; j < tail.length; j++) {
429
+ if (frame[i + j] !== tail[j]) {
430
+ ok = false;
431
+ break;
432
+ }
433
+ }
434
+ if (ok)
435
+ return true;
436
+ }
437
+ return false;
438
+ }
439
+ // Strict command matcher on full CI-V frame (optional address check)
440
+ static matchCommandFrame(frame, cmd, tail, ctrAddr, rigAddr) {
441
+ if (!(frame.length >= 7 && frame[0] === 0xfe && frame[1] === 0xfe))
442
+ return false;
443
+ if (ctrAddr !== undefined) {
444
+ const addrCtrOk = frame[2] === (ctrAddr & 0xff) || frame[2] === 0x00;
445
+ if (!addrCtrOk)
446
+ return false;
447
+ }
448
+ if (rigAddr !== undefined) {
449
+ if (frame[3] !== (rigAddr & 0xff))
450
+ return false;
451
+ }
452
+ if (frame[4] !== (cmd & 0xff))
453
+ return false;
454
+ // tail should match starting at byte 5
455
+ if (5 + tail.length > frame.length)
456
+ return false;
457
+ for (let i = 0; i < tail.length; i++) {
458
+ if (frame[5 + i] !== tail[i])
459
+ return false;
460
+ }
461
+ if (frame[frame.length - 1] !== 0xfd)
462
+ return false;
463
+ return true;
464
+ }
465
+ async waitForCiv(predicate, timeoutMs, onSend) {
466
+ return new Promise((resolve) => {
467
+ let done = false;
468
+ const onFrame = (data) => {
469
+ // our event emits Buffer of CI-V payload
470
+ const frame = data;
471
+ if (!done && predicate(frame)) {
472
+ done = true;
473
+ this.ev.off('civ', onFrame);
474
+ resolve(frame);
475
+ }
476
+ };
477
+ this.ev.on('civ', onFrame);
478
+ if (onSend)
479
+ onSend();
480
+ setTimeout(() => {
481
+ if (!done) {
482
+ this.ev.off('civ', onFrame);
483
+ resolve(null);
484
+ }
485
+ }, timeoutMs);
486
+ });
487
+ }
488
+ // Parse CI-V reply for command 0x03 (read operating frequency)
489
+ static parseIcomFreqFromReply(frame) {
490
+ // Expect: FE FE [ctr] [rig] 0x03 [bcd0..bcd4] FD
491
+ if (!(frame && frame.length >= 7))
492
+ return null;
493
+ if (frame[0] !== 0xfe || frame[1] !== 0xfe)
494
+ return null;
495
+ if (frame[4] !== 0x03)
496
+ return null;
497
+ // Some radios may include extra bytes; find 0x03 and read next 5 bytes
498
+ let idx = frame.indexOf(0x03, 5);
499
+ if (idx < 0 || idx + 5 >= frame.length)
500
+ idx = 4; // fallback to standard position
501
+ if (idx + 5 >= frame.length)
502
+ return null;
503
+ const d0 = frame[idx + 1];
504
+ const d1 = frame[idx + 2];
505
+ const d2 = frame[idx + 3];
506
+ const d3 = frame[idx + 4];
507
+ const d4 = frame[idx + 5];
508
+ const bcdToInt = (b) => ((b >> 4) & 0x0f) * 10 + (b & 0x0f);
509
+ const v0 = bcdToInt(d0);
510
+ const v1 = bcdToInt(d1);
511
+ const v2 = bcdToInt(d2);
512
+ const v3 = bcdToInt(d3);
513
+ const v4 = bcdToInt(d4);
514
+ const hz = v0 + v1 * 100 + v2 * 10000 + v3 * 1000000 + v4 * 100000000;
515
+ return hz;
516
+ }
517
+ sendAudioFloat32(samples, addLeadingBuffer = false) {
518
+ this.audio.enqueueFloat32(samples, addLeadingBuffer);
519
+ }
520
+ sendAudioPcm16(samples) { this.audio.enqueuePcm16(samples); }
521
+ onData(buf) {
522
+ // common demux by length
523
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
524
+ // dbg/dbgV imported at top
525
+ switch (buf.length) {
526
+ case IcomPackets_1.Sizes.CONTROL: {
527
+ const type = IcomPackets_1.ControlPacket.getType(buf);
528
+ (0, debug_1.dbg)('CTRL <= type=0x' + type.toString(16));
529
+ if (type === IcomPackets_1.Cmd.I_AM_HERE) {
530
+ this.sess.remoteId = IcomPackets_1.ControlPacket.getSentId(buf);
531
+ (0, debug_1.dbg)('I_AM_HERE remoteId=', this.sess.remoteId);
532
+ this.sess.stopAreYouThere();
533
+ this.sess.startPing();
534
+ // ask ready
535
+ this.sess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.ARE_YOU_READY, 1, this.sess.localId, this.sess.remoteId));
536
+ }
537
+ else if (type === IcomPackets_1.Cmd.I_AM_READY) {
538
+ (0, debug_1.dbg)('I_AM_READY -> send login');
539
+ // send login
540
+ const login = IcomPackets_1.LoginPacket.build(0, this.sess.localId, this.sess.remoteId, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken, this.options.userName, this.options.password, 'FT8CN-Node');
541
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
542
+ this.sess.sendTracked(login);
543
+ this.sess.startIdle();
544
+ }
545
+ break;
546
+ }
547
+ case IcomPackets_1.Sizes.TOKEN: {
548
+ // token renewal / confirm responses
549
+ const reqType = IcomPackets_1.TokenPacket.getRequestType(buf);
550
+ const reqReply = IcomPackets_1.TokenPacket.getRequestReply(buf);
551
+ (0, debug_1.dbg)('TOKEN <= type=', reqType, 'reply=', reqReply);
552
+ if (reqType === IcomPackets_1.TokenType.RENEWAL && reqReply === 0x02 && IcomPackets_1.ControlPacket.getType(buf) !== IcomPackets_1.Cmd.RETRANSMIT) {
553
+ const response = IcomPackets_1.TokenPacket.getResponse(buf);
554
+ (0, debug_1.dbgV)('TOKEN renewal response=', response);
555
+ if (response === 0x00000000) {
556
+ // ok
557
+ }
558
+ else if (response === 0xffffffff) {
559
+ // rejected; attempt re-connect
560
+ this.sess.remoteId = IcomPackets_1.ControlPacket.getSentId(buf);
561
+ this.sess.localToken = IcomPackets_1.TokenPacket.getTokRequest(buf);
562
+ this.sess.rigToken = IcomPackets_1.TokenPacket.getToken(buf);
563
+ this.sendConnectionRequest();
564
+ }
565
+ }
566
+ break;
567
+ }
568
+ case IcomPackets_1.Sizes.STATUS: {
569
+ const civPort = IcomPackets_1.StatusPacket.getRigCivPort(buf);
570
+ const audioPort = IcomPackets_1.StatusPacket.getRigAudioPort(buf);
571
+ (0, debug_1.dbg)('STATUS <= civPort=', civPort, 'audioPort=', audioPort, 'authOK=', IcomPackets_1.StatusPacket.authOK(buf), 'connected=', IcomPackets_1.StatusPacket.getIsConnected(buf));
572
+ const info = { civPort, audioPort, authOK: true, connected: true };
573
+ this.ev.emit('status', info);
574
+ // set remote ports and start AYT for civ/audio
575
+ if (this.civSess) {
576
+ this.civSess.setRemote(this.options.control.ip, civPort);
577
+ this.civSess.startAreYouThere();
578
+ this.civ.start();
579
+ }
580
+ if (this.audioSess) {
581
+ this.audioSess.setRemote(this.options.control.ip, audioPort);
582
+ this.audioSess.startAreYouThere();
583
+ }
584
+ break;
585
+ }
586
+ case IcomPackets_1.Sizes.LOGIN_RESPONSE: {
587
+ const ok = IcomPackets_1.LoginResponsePacket.authOK(buf);
588
+ (0, debug_1.dbg)('LOGIN_RESPONSE ok=', ok, 'conn=', IcomPackets_1.LoginResponsePacket.getConnection(buf));
589
+ if (ok) {
590
+ this.sess.rigToken = IcomPackets_1.LoginResponsePacket.getToken(buf);
591
+ // send token confirm
592
+ const tok = IcomPackets_1.TokenPacket.build(0, this.sess.localId, this.sess.remoteId, IcomPackets_1.TokenType.CONFIRM, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken);
593
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
594
+ this.sess.sendTracked(tok);
595
+ // start token renewal timer (60s)
596
+ if (!this.tokenTimer) {
597
+ this.tokenTimer = setInterval(() => {
598
+ (0, debug_1.dbg)('TOKEN -> renewal');
599
+ const renew = IcomPackets_1.TokenPacket.build(0, this.sess.localId, this.sess.remoteId, IcomPackets_1.TokenType.RENEWAL, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken);
600
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
601
+ this.sess.sendTracked(renew);
602
+ }, 60000);
603
+ }
604
+ }
605
+ const res = { ok, errorCode: IcomPackets_1.LoginResponsePacket.errorNum(buf), connection: IcomPackets_1.LoginResponsePacket.getConnection(buf) };
606
+ this.ev.emit('login', res);
607
+ if (ok) {
608
+ (0, debug_1.dbg)('Login ready - resolving loginReady promise');
609
+ this.resolveLoginReady();
610
+ }
611
+ break;
612
+ }
613
+ case IcomPackets_1.Sizes.CAP_CAP: {
614
+ const cap = IcomPackets_1.CapCapabilitiesPacket.getRadioCapPacket(buf, 0);
615
+ if (cap) {
616
+ const info = {
617
+ civAddress: IcomPackets_1.RadioCapPacket.getCivAddress(cap),
618
+ audioName: IcomPackets_1.RadioCapPacket.getAudioName(cap),
619
+ supportTX: IcomPackets_1.RadioCapPacket.getSupportTX(cap)
620
+ };
621
+ if (info.civAddress != null)
622
+ this.civ.civAddress = info.civAddress;
623
+ if (info.supportTX != null)
624
+ this.civ.supportTX = info.supportTX;
625
+ (0, debug_1.dbgV)('CAP <= civAddr=', info.civAddress, 'audioName=', info.audioName, 'supportTX=', info.supportTX);
626
+ this.ev.emit('capabilities', info);
627
+ }
628
+ break;
629
+ }
630
+ case IcomPackets_1.Sizes.CONNINFO: {
631
+ // rig sends twice; first time busy=false, reply with our ports
632
+ const busy = IcomPackets_1.ConnInfoPacket.getBusy(buf);
633
+ this.macAddress = IcomPackets_1.ConnInfoPacket.getMacAddress(buf);
634
+ this.rigName = IcomPackets_1.ConnInfoPacket.getRigName(buf);
635
+ (0, debug_1.dbg)('CONNINFO <= busy=', busy, 'rigName=', this.rigName);
636
+ if (!busy) {
637
+ const reply = IcomPackets_1.ConnInfoPacket.connInfoPacketData(buf, 0, this.sess.localId, this.sess.remoteId, 0x01, 0x03, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken, this.rigName, this.options.userName, IcomPackets_1.AUDIO_SAMPLE_RATE, IcomPackets_1.AUDIO_SAMPLE_RATE, this.civSess.localPort, this.audioSess.localPort, IcomPackets_1.XIEGU_TX_BUFFER_SIZE);
638
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
639
+ this.sess.sendTracked(reply);
640
+ try {
641
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
642
+ const { hex } = require('../utils/codec');
643
+ (0, debug_1.dbg)('CONNINFO -> reply with local civPort=', this.civSess.localPort, 'audioPort=', this.audioSess.localPort);
644
+ (0, debug_1.dbgV)('CONNINFO reply hex (first 0x60):', hex(Buffer.from(reply.subarray(0, 0x60))));
645
+ (0, debug_1.dbgV)('CONNINFO reply hex (0x60..0x90):', hex(Buffer.from(reply.subarray(0x60, 0x90))));
646
+ }
647
+ catch { }
648
+ }
649
+ break;
650
+ }
651
+ default: {
652
+ // CIV and Audio are variable length; route by headers
653
+ if (IcomPackets_1.CivPacket.isCiv(buf)) {
654
+ // CIV
655
+ const payload = IcomPackets_1.CivPacket.getCivData(buf);
656
+ (0, debug_1.dbg)('CIV <=', payload.length, 'bytes');
657
+ this.ev.emit('civ', payload);
658
+ }
659
+ else if (buf.length >= 0x18 &&
660
+ (buf[0x10] === 0x97 || buf[0x10] === 0x00) &&
661
+ ((buf[0x11] === 0x81) || (buf[0x11] === 0x80))) {
662
+ const len = buf.readUInt16BE(0x16);
663
+ const audio = Buffer.from(buf.subarray(0x18, 0x18 + len));
664
+ (0, debug_1.dbg)('AUDIO <=', audio.length, 'bytes');
665
+ this.ev.emit('audio', { pcm16: audio });
666
+ }
667
+ else if (buf.length === IcomPackets_1.Sizes.CONTROL && IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT) {
668
+ (0, debug_1.dbgV)('RETRANSMIT <= single', IcomPackets_1.ControlPacket.getSeq(buf));
669
+ // single retransmit
670
+ this.sess.retransmit(IcomPackets_1.ControlPacket.getSeq(buf));
671
+ }
672
+ else if (IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT && buf.length > IcomPackets_1.Sizes.CONTROL) {
673
+ (0, debug_1.dbgV)('RETRANSMIT <= multi count=', Math.floor((buf.length - 0x10) / 2));
674
+ for (let i = 0x10; i + 1 < buf.length; i += 2) {
675
+ const seq = buf.readUInt16LE(i);
676
+ this.sess.retransmit(seq);
677
+ }
678
+ }
679
+ else if (buf.length === IcomPackets_1.Sizes.PING && IcomPackets_1.Cmd.PING === IcomPackets_1.ControlPacket.getType(buf)) {
680
+ // ping handling
681
+ if (buf[0x10] === 0x00) {
682
+ // reply to radio ping
683
+ const rep = IcomPackets_1.PingPacket.buildReply(buf, this.sess.localId, this.sess.remoteId);
684
+ (0, debug_1.dbgV)('PING <= request -> reply');
685
+ this.sess.sendUntracked(rep);
686
+ }
687
+ else {
688
+ // reply to our ping; seq ok -> bump
689
+ if (IcomPackets_1.ControlPacket.getSeq(buf) === this.sess.pingSeq)
690
+ this.sess.pingSeq = (this.sess.pingSeq + 1) & 0xffff;
691
+ (0, debug_1.dbgV)('PING <= reply seq=', IcomPackets_1.ControlPacket.getSeq(buf));
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ onCivData(buf) {
698
+ // mirror some generic handling on sub-session
699
+ if (buf.length === IcomPackets_1.Sizes.CONTROL) {
700
+ const type = IcomPackets_1.ControlPacket.getType(buf);
701
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
702
+ (0, debug_1.dbg)('CIV CTRL <= type=0x' + type.toString(16));
703
+ if (type === IcomPackets_1.Cmd.I_AM_HERE) {
704
+ this.civSess.remoteId = IcomPackets_1.ControlPacket.getSentId(buf);
705
+ this.civSess.stopAreYouThere();
706
+ this.civSess.startPing();
707
+ // send ARE_YOU_READY with seq=1
708
+ this.civSess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.ARE_YOU_READY, 1, this.civSess.localId, this.civSess.remoteId));
709
+ return;
710
+ }
711
+ if (type === IcomPackets_1.Cmd.I_AM_READY) {
712
+ this.civ.sendOpenClose(true);
713
+ this.civSess.startIdle();
714
+ (0, debug_1.dbg)('CIV ready - resolving civReady promise');
715
+ this.resolveCivReady();
716
+ return;
717
+ }
718
+ }
719
+ // CIV data frames
720
+ // Diagnostic: check for potential CIV packets
721
+ if (buf.length > 0x15 && buf[0x10] === 0xc1) {
722
+ (0, debug_1.dbg)(`CIV? len=${buf.length} [0x10]=0xc1 validating...`);
723
+ if (IcomPackets_1.CivPacket.isCiv(buf)) {
724
+ const payload = IcomPackets_1.CivPacket.getCivData(buf);
725
+ (0, debug_1.dbg)('CIV <=', payload.length, 'bytes');
726
+ // Emit raw CIV payload for backward compatibility
727
+ this.ev.emit('civ', payload);
728
+ // Reassemble CIV frames (FE FE ... FD) and emit per-frame events
729
+ this.processCivPayload(payload);
730
+ return;
731
+ }
732
+ else {
733
+ (0, debug_1.dbg)(`CIV validation FAILED len=${buf.length}`);
734
+ }
735
+ }
736
+ if (buf.length === IcomPackets_1.Sizes.PING && IcomPackets_1.Cmd.PING === IcomPackets_1.ControlPacket.getType(buf)) {
737
+ if (buf[0x10] === 0x00) {
738
+ const rep = IcomPackets_1.PingPacket.buildReply(buf, this.civSess.localId, this.civSess.remoteId);
739
+ this.civSess.sendUntracked(rep);
740
+ }
741
+ else if (IcomPackets_1.ControlPacket.getSeq(buf) === (this.civSess.pingSeq)) {
742
+ this.civSess.pingSeq = (this.civSess.pingSeq + 1) & 0xffff;
743
+ }
744
+ }
745
+ if (buf.length === IcomPackets_1.Sizes.CONTROL && IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT) {
746
+ this.civSess?.retransmit(IcomPackets_1.ControlPacket.getSeq(buf));
747
+ }
748
+ else if (IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT && buf.length > IcomPackets_1.Sizes.CONTROL) {
749
+ for (let i = 0x10; i + 1 < buf.length; i += 2)
750
+ this.civSess?.retransmit(buf.readUInt16LE(i));
751
+ }
752
+ }
753
+ // Append CIV payload bytes and emit complete CI-V frames (FE FE ... FD)
754
+ processCivPayload(payload) {
755
+ // Append new data
756
+ this.civAssembleBuf = Buffer.concat([this.civAssembleBuf, payload]);
757
+ // Try to extract frames in a loop
758
+ while (true) {
759
+ // Find start marker FE FE
760
+ let start = -1;
761
+ for (let i = 0; i + 1 < this.civAssembleBuf.length; i++) {
762
+ if (this.civAssembleBuf[i] === 0xfe && this.civAssembleBuf[i + 1] === 0xfe) {
763
+ start = i;
764
+ break;
765
+ }
766
+ }
767
+ if (start < 0) {
768
+ // No start marker: drop noise before potential next packet
769
+ if (this.civAssembleBuf.length > 1024)
770
+ this.civAssembleBuf = Buffer.alloc(0);
771
+ return;
772
+ }
773
+ // Trim leading noise
774
+ if (start > 0)
775
+ this.civAssembleBuf = this.civAssembleBuf.subarray(start);
776
+ // Find end marker FD after start
777
+ let end = -1;
778
+ for (let i = 2; i < this.civAssembleBuf.length; i++) {
779
+ if (this.civAssembleBuf[i] === 0xfd) {
780
+ end = i;
781
+ break;
782
+ }
783
+ }
784
+ if (end < 0) {
785
+ // Incomplete frame, wait for more data
786
+ return;
787
+ }
788
+ // Extract frame [0..end]
789
+ const frame = Buffer.from(this.civAssembleBuf.subarray(0, end + 1));
790
+ // Advance buffer
791
+ this.civAssembleBuf = this.civAssembleBuf.subarray(end + 1);
792
+ // Emit event
793
+ this.ev.emit('civFrame', frame);
794
+ // Continue loop in case multiple frames are in buffer
795
+ }
796
+ }
797
+ // Wait for single CI-V frame that matches predicate (fed by civFrame event)
798
+ async waitForCivFrame(predicate, timeoutMs, onSend) {
799
+ return new Promise((resolve) => {
800
+ let done = false;
801
+ const onFrame = (frame) => {
802
+ if (!done && predicate(frame)) {
803
+ done = true;
804
+ this.ev.off('civFrame', onFrame);
805
+ resolve(frame);
806
+ }
807
+ };
808
+ this.ev.on('civFrame', onFrame);
809
+ if (onSend)
810
+ onSend();
811
+ setTimeout(() => {
812
+ if (!done) {
813
+ this.ev.off('civFrame', onFrame);
814
+ resolve(null);
815
+ }
816
+ }, timeoutMs);
817
+ });
818
+ }
819
+ // Strict meter reply matcher: FE FE [ctr|00] [rig] 0x15 [sub] ... FD
820
+ static isMeterReply(frame, subcmd, ctrAddr, rigAddr) {
821
+ if (!(frame && frame.length >= 9))
822
+ return false;
823
+ if (frame[0] !== 0xfe || frame[1] !== 0xfe)
824
+ return false;
825
+ const addrCtrOk = frame[2] === (ctrAddr & 0xff) || frame[2] === 0x00;
826
+ const addrRigOk = frame[3] === (rigAddr & 0xff);
827
+ if (!addrCtrOk || !addrRigOk)
828
+ return false;
829
+ if (frame[4] !== 0x15)
830
+ return false;
831
+ if (frame[5] !== (subcmd & 0xff))
832
+ return false;
833
+ if (frame[frame.length - 1] !== 0xfd)
834
+ return false;
835
+ return true;
836
+ }
837
+ // Start meter polling like Java (every 500ms when PTT is on)
838
+ startMeterPolling() {
839
+ this.stopMeterPolling();
840
+ const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
841
+ const rigAddr = this.civ.civAddress & 0xff;
842
+ this.meterTimer = setInterval(() => {
843
+ if (!this.audio.isPttOn)
844
+ return; // safety
845
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.getSWRState(ctrAddr, rigAddr));
846
+ this.sendCiv(IcomRigCommands_1.IcomRigCommands.getALCState(ctrAddr, rigAddr));
847
+ }, IcomConstants_1.METER_TIMER_PERIOD_MS);
848
+ }
849
+ stopMeterPolling() {
850
+ if (this.meterTimer) {
851
+ clearInterval(this.meterTimer);
852
+ this.meterTimer = undefined;
853
+ }
854
+ }
855
+ onAudioData(buf) {
856
+ if (buf.length === IcomPackets_1.Sizes.CONTROL) {
857
+ const type = IcomPackets_1.ControlPacket.getType(buf);
858
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
859
+ (0, debug_1.dbg)('AUDIO CTRL <= type=0x' + type.toString(16));
860
+ if (type === IcomPackets_1.Cmd.I_AM_HERE) {
861
+ this.audioSess.remoteId = IcomPackets_1.ControlPacket.getSentId(buf);
862
+ this.audioSess.stopAreYouThere();
863
+ this.audioSess.startPing();
864
+ this.audioSess.sendUntracked(IcomPackets_1.ControlPacket.toBytes(IcomPackets_1.Cmd.ARE_YOU_READY, 1, this.audioSess.localId, this.audioSess.remoteId));
865
+ return;
866
+ }
867
+ if (type === IcomPackets_1.Cmd.I_AM_READY) {
868
+ // Start continuous audio transmission (like Java's startTxAudio on I_AM_READY)
869
+ this.audio.start();
870
+ this.audioSess.startIdle();
871
+ (0, debug_1.dbg)('Audio ready - started continuous audio stream, resolving audioReady promise');
872
+ this.resolveAudioReady();
873
+ return;
874
+ }
875
+ }
876
+ // AUDIO frames (len >= 0x18 and datalen matches). We don't strictly validate ident here to support variants.
877
+ if (buf.length >= 0x18) {
878
+ // datalen is BE (Java uses shortToByte which is big-endian)
879
+ const dataLen = buf.readUInt16BE(0x16);
880
+ if (buf.length === 0x18 + dataLen && dataLen > 0 && dataLen <= 2048) {
881
+ const audio = Buffer.from(buf.subarray(0x18, 0x18 + dataLen));
882
+ this.ev.emit('audio', { pcm16: audio });
883
+ return;
884
+ }
885
+ }
886
+ if (buf.length === IcomPackets_1.Sizes.PING && IcomPackets_1.Cmd.PING === IcomPackets_1.ControlPacket.getType(buf)) {
887
+ if (buf[0x10] === 0x00) {
888
+ const rep = IcomPackets_1.PingPacket.buildReply(buf, this.audioSess.localId, this.audioSess.remoteId);
889
+ this.audioSess.sendUntracked(rep);
890
+ }
891
+ else if (IcomPackets_1.ControlPacket.getSeq(buf) === (this.audioSess.pingSeq)) {
892
+ this.audioSess.pingSeq = (this.audioSess.pingSeq + 1) & 0xffff;
893
+ }
894
+ }
895
+ if (buf.length === IcomPackets_1.Sizes.CONTROL && IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT) {
896
+ this.audioSess?.retransmit(IcomPackets_1.ControlPacket.getSeq(buf));
897
+ }
898
+ else if (IcomPackets_1.ControlPacket.getType(buf) === IcomPackets_1.Cmd.RETRANSMIT && buf.length > IcomPackets_1.Sizes.CONTROL) {
899
+ for (let i = 0x10; i + 1 < buf.length; i += 2)
900
+ this.audioSess?.retransmit(buf.readUInt16LE(i));
901
+ }
902
+ // audio data routed by main handler already (if using single session). Here we may add specific behaviors if needed.
903
+ }
904
+ sendConnectionRequest() {
905
+ if (!this.civSess || !this.audioSess)
906
+ return;
907
+ const pkt = IcomPackets_1.ConnInfoPacket.connectRequestPacket(0, this.sess.localId, this.sess.remoteId, 0x01, 0x03, this.sess.innerSeq, this.sess.localToken, this.sess.rigToken, this.macAddress, this.rigName, this.options.userName, IcomPackets_1.AUDIO_SAMPLE_RATE, this.civSess.localPort, this.audioSess.localPort, IcomPackets_1.XIEGU_TX_BUFFER_SIZE);
908
+ this.sess.innerSeq = (this.sess.innerSeq + 1) & 0xffff;
909
+ this.sess.sendTracked(pkt);
910
+ }
911
+ }
912
+ exports.IcomControl = IcomControl;