icom-wlan-node 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -29,7 +29,7 @@ npm run build
29
29
  ## Quick Start
30
30
 
31
31
  ```ts
32
- import { IcomControl, AUDIO_RATE } from 'icom-wlan-node';
32
+ import { IcomControl, AUDIO_RATE, DisconnectReason } from 'icom-wlan-node';
33
33
 
34
34
  const rig = new IcomControl({
35
35
  control: { ip: '192.168.1.50', port: 50001 },
@@ -111,7 +111,8 @@ await rig.setPtt(false);
111
111
  - `reconnectAttempting(ReconnectAttemptInfo)` — reconnect attempt started
112
112
  - `reconnectFailed(ReconnectFailedInfo)` — reconnect attempt failed
113
113
  - Methods
114
- - **Connection**: `connect()` / `disconnect()` — connects control + CIV + audio sub‑sessions; resolves when all ready
114
+ - **Connection**: `connect()` / `disconnect(options?)` — connects control + CIV + audio sub‑sessions; resolves when all ready
115
+ - `disconnect()` accepts optional `DisconnectOptions` or `DisconnectReason` for better error handling
115
116
  - **Raw CI‑V**: `sendCiv(buf: Buffer)` — send a raw CI‑V frame
116
117
  - **Audio TX**: `setPtt(on: boolean)`, `sendAudioFloat32()`, `sendAudioPcm16()`
117
118
  - **Rig Control**: `setFrequency()`, `setMode()`, `setConnectorDataMode()`, `setConnectorWLanLevel()`
@@ -151,6 +152,12 @@ console.log(metrics.sessions); // Per-session states {control, civ, audio}
151
152
 
152
153
  // Disconnect (also idempotent)
153
154
  await rig.disconnect();
155
+
156
+ // Disconnect with reason (provides better error messages)
157
+ await rig.disconnect(DisconnectReason.TIMEOUT);
158
+
159
+ // Silent disconnect (cleanup mode - no error events)
160
+ await rig.disconnect({ reason: DisconnectReason.CLEANUP, silent: true });
154
161
  ```
155
162
 
156
163
  #### Connection Monitoring Events
@@ -111,6 +111,8 @@ class Session {
111
111
  * (especially important after radio restart)
112
112
  */
113
113
  resetState() {
114
+ // Stop all timers to prevent leaks and interference with new connection
115
+ this.stopTimers();
114
116
  // Reset destroyed flag to ensure session is usable after state reset
115
117
  this.destroyed = false;
116
118
  // Generate new IDs
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { parseTwoByteBcd, intToTwoByteBcd } from './utils/bcd';
5
5
  export { IcomRigCommands } from './rig/IcomRigCommands';
6
6
  export { AUDIO_RATE } from './rig/IcomAudio';
7
7
  export { setupGlobalErrorHandlers, setupBasicErrorProtection, GlobalErrorHandlerOptions } from './utils/errorHandling';
8
+ export { ConnectionAbortedError, getDisconnectMessage } from './utils/errors';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.setupBasicErrorProtection = exports.setupGlobalErrorHandlers = exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomControl = void 0;
17
+ exports.getDisconnectMessage = exports.ConnectionAbortedError = exports.setupBasicErrorProtection = exports.setupGlobalErrorHandlers = exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomControl = void 0;
18
18
  // Export types (includes ConnectionPhase, ConnectionMetrics, etc.)
19
19
  __exportStar(require("./types"), exports);
20
20
  // Export main class
@@ -48,3 +48,7 @@ Object.defineProperty(exports, "AUDIO_RATE", { enumerable: true, get: function (
48
48
  var errorHandling_1 = require("./utils/errorHandling");
49
49
  Object.defineProperty(exports, "setupGlobalErrorHandlers", { enumerable: true, get: function () { return errorHandling_1.setupGlobalErrorHandlers; } });
50
50
  Object.defineProperty(exports, "setupBasicErrorProtection", { enumerable: true, get: function () { return errorHandling_1.setupBasicErrorProtection; } });
51
+ // Export disconnect error utilities
52
+ var errors_1 = require("./utils/errors");
53
+ Object.defineProperty(exports, "ConnectionAbortedError", { enumerable: true, get: function () { return errors_1.ConnectionAbortedError; } });
54
+ Object.defineProperty(exports, "getDisconnectMessage", { enumerable: true, get: function () { return errors_1.getDisconnectMessage; } });
@@ -1,4 +1,4 @@
1
- import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics } from '../types';
1
+ import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics, DisconnectReason, DisconnectOptions } from '../types';
2
2
  import { IcomCiv } from './IcomCiv';
3
3
  import { IcomAudio } from './IcomAudio';
4
4
  export declare class IcomControl {
@@ -66,7 +66,12 @@ export declare class IcomControl {
66
66
  * @private
67
67
  */
68
68
  private stopUnifiedMonitoring;
69
- disconnect(): Promise<void>;
69
+ /**
70
+ * Disconnect from the rig
71
+ * @param options - Optional disconnect options (reason, silent mode)
72
+ * @returns Promise that resolves when disconnect is complete
73
+ */
74
+ disconnect(options?: DisconnectOptions | DisconnectReason): Promise<void>;
70
75
  sendCiv(data: Buffer): void;
71
76
  /**
72
77
  * Set PTT (Push-To-Talk) state
@@ -44,6 +44,7 @@ const IcomAudio_1 = require("./IcomAudio");
44
44
  const IcomRigCommands_1 = require("./IcomRigCommands");
45
45
  const IcomConstants_1 = require("./IcomConstants");
46
46
  const bcd_1 = require("../utils/bcd");
47
+ const errors_1 = require("../utils/errors");
47
48
  class IcomControl {
48
49
  constructor(options) {
49
50
  this.ev = new events_1.EventEmitter();
@@ -134,11 +135,12 @@ class IcomControl {
134
135
  * Abort an ongoing connection attempt by session ID
135
136
  * @private
136
137
  */
137
- abortConnectionAttempt(sessionId, reason) {
138
+ abortConnectionAttempt(sessionId, reason, silent = false) {
138
139
  const abortHandler = this.abortHandlers.get(sessionId);
139
140
  if (abortHandler) {
140
- (0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${reason}`);
141
- abortHandler(reason);
141
+ const message = (0, errors_1.getDisconnectMessage)(reason);
142
+ (0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${message} (silent=${silent})`);
143
+ abortHandler(reason, silent);
142
144
  this.abortHandlers.delete(sessionId);
143
145
  }
144
146
  }
@@ -272,29 +274,26 @@ class IcomControl {
272
274
  });
273
275
  // Store abort handler bound to this specific sessionId
274
276
  // This prevents race conditions when multiple connection attempts overlap
275
- const abortHandler = (reason) => {
276
- (0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${reason}`);
277
- const error = new Error(reason);
277
+ const abortHandler = (reason, silent) => {
278
+ const message = (0, errors_1.getDisconnectMessage)(reason);
279
+ (0, debug_1.dbg)(`Aborting connection session ${sessionId}: ${message} (silent=${silent})`);
280
+ // Create a single error instance to be shared across all rejections
281
+ const error = new errors_1.ConnectionAbortedError(reason, sessionId, this.connectionSession.phase);
282
+ // IMPORTANT: Always reject promises to unblock waiting code
283
+ // The "silent" flag only affects whether errors propagate to user code,
284
+ // but internally we must settle the promise to prevent hanging
285
+ (0, debug_1.dbg)(`${silent ? 'Silent' : 'Normal'} abort - rejecting login promise for session ${sessionId}`);
278
286
  // Defensive error handling: wrap reject calls in try-catch to prevent
279
287
  // synchronous errors from propagating if promises are already settled
288
+ // Only reject one promise to avoid duplicate errors
280
289
  try {
281
290
  rejectLogin(error);
282
291
  }
283
292
  catch (err) {
284
293
  (0, debug_1.dbg)(`Warning: Failed to reject loginReady promise: ${err}`);
285
294
  }
286
- try {
287
- rejectCiv(error);
288
- }
289
- catch (err) {
290
- (0, debug_1.dbg)(`Warning: Failed to reject civReady promise: ${err}`);
291
- }
292
- try {
293
- rejectAudio(error);
294
- }
295
- catch (err) {
296
- (0, debug_1.dbg)(`Warning: Failed to reject audioReady promise: ${err}`);
297
- }
295
+ // Don't reject civ and audio promises separately - they'll be cleaned up
296
+ // This prevents the "3x User disconnect()" log spam
298
297
  };
299
298
  this.abortHandlers.set(sessionId, abortHandler);
300
299
  // Track promise states for defensive cleanup
@@ -401,8 +400,20 @@ class IcomControl {
401
400
  this.monitorTimer = undefined;
402
401
  }
403
402
  }
404
- async disconnect() {
403
+ /**
404
+ * Disconnect from the rig
405
+ * @param options - Optional disconnect options (reason, silent mode)
406
+ * @returns Promise that resolves when disconnect is complete
407
+ */
408
+ async disconnect(options) {
409
+ // Support both object and enum parameter for backward compatibility
410
+ const opts = typeof options === 'string'
411
+ ? { reason: options, silent: false }
412
+ : { reason: types_1.DisconnectReason.USER_REQUEST, silent: false, ...options };
413
+ const reason = opts.reason ?? types_1.DisconnectReason.USER_REQUEST;
414
+ const silent = opts.silent ?? false;
405
415
  const currentPhase = this.connectionSession.phase;
416
+ (0, debug_1.dbg)(`disconnect() called with reason=${reason}, silent=${silent}, currentPhase=${currentPhase}`);
406
417
  // If already disconnecting or idle, avoid duplicate work
407
418
  if (currentPhase === types_1.ConnectionPhase.DISCONNECTING) {
408
419
  (0, debug_1.dbg)('disconnect() called but already DISCONNECTING - waiting for completion');
@@ -428,7 +439,7 @@ class IcomControl {
428
439
  if (currentPhase === types_1.ConnectionPhase.CONNECTING || currentPhase === types_1.ConnectionPhase.RECONNECTING) {
429
440
  try {
430
441
  (0, debug_1.dbg)(`Aborting ongoing connection attempt (sessionId=${currentSessionId})`);
431
- this.abortConnectionAttempt(currentSessionId, 'User disconnect()');
442
+ this.abortConnectionAttempt(currentSessionId, reason, silent);
432
443
  }
433
444
  catch (abortErr) {
434
445
  // Log but continue - abort failure shouldn't prevent disconnect
@@ -437,7 +448,8 @@ class IcomControl {
437
448
  }
438
449
  }
439
450
  // Transition to DISCONNECTING state
440
- this.transitionTo(types_1.ConnectionPhase.DISCONNECTING, 'User disconnect()');
451
+ const transitionReason = (0, errors_1.getDisconnectMessage)(reason);
452
+ this.transitionTo(types_1.ConnectionPhase.DISCONNECTING, transitionReason);
441
453
  try {
442
454
  // 1. Stop all timers first to prevent interference
443
455
  this.stopUnifiedMonitoring(); // Stop unified monitoring
@@ -1132,7 +1144,7 @@ class IcomControl {
1132
1144
  const currentPhase = this.connectionSession.phase;
1133
1145
  if (currentPhase === types_1.ConnectionPhase.CONNECTING || currentPhase === types_1.ConnectionPhase.RECONNECTING) {
1134
1146
  (0, debug_1.dbg)('Aborting ongoing connection attempt due to connected=false');
1135
- this.abortConnectionAttempt(this.connectionSession.sessionId, 'Radio reported connected=false');
1147
+ this.abortConnectionAttempt(this.connectionSession.sessionId, types_1.DisconnectReason.ERROR, false);
1136
1148
  }
1137
1149
  else if (currentPhase === types_1.ConnectionPhase.CONNECTED) {
1138
1150
  // Trigger reconnection for established connection
package/dist/types.d.ts CHANGED
@@ -373,6 +373,38 @@ export interface ReconnectFailedInfo {
373
373
  /** Next retry delay (ms), if retrying */
374
374
  nextRetryDelay?: number;
375
375
  }
376
+ /**
377
+ * Reason for disconnection
378
+ * Used to distinguish between different types of disconnections
379
+ */
380
+ export declare enum DisconnectReason {
381
+ /** User explicitly called disconnect() */
382
+ USER_REQUEST = "user_request",
383
+ /** Connection timed out */
384
+ TIMEOUT = "timeout",
385
+ /** Cleanup after connection failure */
386
+ CLEANUP = "cleanup",
387
+ /** Error occurred during connection */
388
+ ERROR = "error",
389
+ /** Network connection was lost */
390
+ NETWORK_LOST = "network_lost"
391
+ }
392
+ /**
393
+ * Options for disconnect() method
394
+ */
395
+ export interface DisconnectOptions {
396
+ /**
397
+ * Reason for disconnection
398
+ * Used to generate appropriate error messages and logs
399
+ */
400
+ reason?: DisconnectReason;
401
+ /**
402
+ * Silent mode - don't throw exceptions or emit error events
403
+ * Useful for cleanup operations where exceptions are not desired
404
+ * Default: false
405
+ */
406
+ silent?: boolean;
407
+ }
376
408
  /**
377
409
  * Configuration for connection monitoring
378
410
  */
package/dist/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SessionType = exports.ConnectionPhase = exports.ConnectionState = void 0;
3
+ exports.DisconnectReason = exports.SessionType = exports.ConnectionPhase = exports.ConnectionState = void 0;
4
4
  // ============================================================================
5
5
  // Connection Monitoring Types
6
6
  // ============================================================================
@@ -46,3 +46,20 @@ var SessionType;
46
46
  /** Audio streaming session */
47
47
  SessionType["AUDIO"] = "AUDIO";
48
48
  })(SessionType || (exports.SessionType = SessionType = {}));
49
+ /**
50
+ * Reason for disconnection
51
+ * Used to distinguish between different types of disconnections
52
+ */
53
+ var DisconnectReason;
54
+ (function (DisconnectReason) {
55
+ /** User explicitly called disconnect() */
56
+ DisconnectReason["USER_REQUEST"] = "user_request";
57
+ /** Connection timed out */
58
+ DisconnectReason["TIMEOUT"] = "timeout";
59
+ /** Cleanup after connection failure */
60
+ DisconnectReason["CLEANUP"] = "cleanup";
61
+ /** Error occurred during connection */
62
+ DisconnectReason["ERROR"] = "error";
63
+ /** Network connection was lost */
64
+ DisconnectReason["NETWORK_LOST"] = "network_lost";
65
+ })(DisconnectReason || (exports.DisconnectReason = DisconnectReason = {}));
@@ -0,0 +1,33 @@
1
+ import { DisconnectReason, ConnectionPhase } from '../types';
2
+ /**
3
+ * Get human-readable message for a disconnect reason
4
+ */
5
+ export declare function getDisconnectMessage(reason: DisconnectReason): string;
6
+ /**
7
+ * Error thrown when a connection attempt is aborted
8
+ * This is typically used when disconnect() is called during connect()
9
+ */
10
+ export declare class ConnectionAbortedError extends Error {
11
+ readonly reason: DisconnectReason;
12
+ readonly sessionId: number;
13
+ readonly phase: ConnectionPhase;
14
+ readonly context?: Record<string, any> | undefined;
15
+ readonly name = "ConnectionAbortedError";
16
+ constructor(reason: DisconnectReason, sessionId: number, phase: ConnectionPhase, context?: Record<string, any> | undefined);
17
+ /**
18
+ * Check if this error should be silent (not thrown to user)
19
+ * Cleanup and timeout errors during connect are typically expected
20
+ */
21
+ isSilent(): boolean;
22
+ /**
23
+ * Get detailed error information for debugging
24
+ */
25
+ toJSON(): {
26
+ name: string;
27
+ message: string;
28
+ reason: DisconnectReason;
29
+ sessionId: number;
30
+ phase: ConnectionPhase;
31
+ context: Record<string, any> | undefined;
32
+ };
33
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionAbortedError = void 0;
4
+ exports.getDisconnectMessage = getDisconnectMessage;
5
+ const types_1 = require("../types");
6
+ /**
7
+ * Get human-readable message for a disconnect reason
8
+ */
9
+ function getDisconnectMessage(reason) {
10
+ switch (reason) {
11
+ case types_1.DisconnectReason.USER_REQUEST:
12
+ return 'Connection closed by user request';
13
+ case types_1.DisconnectReason.TIMEOUT:
14
+ return 'Connection timed out';
15
+ case types_1.DisconnectReason.CLEANUP:
16
+ return 'Connection cleanup';
17
+ case types_1.DisconnectReason.ERROR:
18
+ return 'Connection closed due to error';
19
+ case types_1.DisconnectReason.NETWORK_LOST:
20
+ return 'Network connection lost';
21
+ default:
22
+ return 'Connection closed';
23
+ }
24
+ }
25
+ /**
26
+ * Error thrown when a connection attempt is aborted
27
+ * This is typically used when disconnect() is called during connect()
28
+ */
29
+ class ConnectionAbortedError extends Error {
30
+ constructor(reason, sessionId, phase, context) {
31
+ super(getDisconnectMessage(reason));
32
+ this.reason = reason;
33
+ this.sessionId = sessionId;
34
+ this.phase = phase;
35
+ this.context = context;
36
+ this.name = 'ConnectionAbortedError';
37
+ // Maintains proper stack trace for where error was thrown (V8 only)
38
+ if (Error.captureStackTrace) {
39
+ Error.captureStackTrace(this, ConnectionAbortedError);
40
+ }
41
+ }
42
+ /**
43
+ * Check if this error should be silent (not thrown to user)
44
+ * Cleanup and timeout errors during connect are typically expected
45
+ */
46
+ isSilent() {
47
+ return this.reason === types_1.DisconnectReason.CLEANUP ||
48
+ this.reason === types_1.DisconnectReason.TIMEOUT;
49
+ }
50
+ /**
51
+ * Get detailed error information for debugging
52
+ */
53
+ toJSON() {
54
+ return {
55
+ name: this.name,
56
+ message: this.message,
57
+ reason: this.reason,
58
+ sessionId: this.sessionId,
59
+ phase: this.phase,
60
+ context: this.context
61
+ };
62
+ }
63
+ }
64
+ exports.ConnectionAbortedError = ConnectionAbortedError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icom-wlan-node",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Icom WLAN (CI‑V, audio) protocol implementation for Node.js/TypeScript.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",