hamlib 0.3.3 → 0.4.1

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,63 @@
1
+ import { EventEmitter } from 'events';
2
+ import type {
3
+ HamLib,
4
+ SpectrumCapabilities,
5
+ SpectrumConfig,
6
+ SpectrumDisplayState,
7
+ SpectrumLine,
8
+ SpectrumSupportSummary,
9
+ } from '../index';
10
+
11
+ interface ManagedSpectrumConfig extends SpectrumConfig {
12
+ /**
13
+ * Interval for lightweight CAT pump reads while managed spectrum is active.
14
+ * Set to 0 or false to disable the helper-side pump.
15
+ * Default: 200ms
16
+ */
17
+ pumpIntervalMs?: number | false;
18
+ }
19
+
20
+ declare class SpectrumController extends EventEmitter {
21
+ constructor(rig: HamLib);
22
+
23
+ getSpectrumSupportSummary(): Promise<SpectrumSupportSummary>;
24
+ configureSpectrum(config?: SpectrumConfig): Promise<SpectrumSupportSummary>;
25
+ getSpectrumEdgeSlot(): Promise<number>;
26
+ setSpectrumEdgeSlot(slot: number): Promise<number>;
27
+ getSpectrumSupportedEdgeSlots(): Promise<number[]>;
28
+ getSpectrumFixedEdges(): Promise<{lowHz: number, highHz: number}>;
29
+ setSpectrumFixedEdges(range: {lowHz: number, highHz: number}): Promise<{lowHz: number, highHz: number}>;
30
+ getSpectrumDisplayState(): Promise<SpectrumDisplayState>;
31
+ configureSpectrumDisplay(config?: SpectrumConfig): Promise<SpectrumDisplayState>;
32
+ startManagedSpectrum(config?: ManagedSpectrumConfig): Promise<boolean>;
33
+ stopManagedSpectrum(): Promise<boolean>;
34
+
35
+ on(event: 'spectrumLine', listener: (line: SpectrumLine) => void): this;
36
+ once(event: 'spectrumLine', listener: (line: SpectrumLine) => void): this;
37
+ off(event: 'spectrumLine', listener: (line: SpectrumLine) => void): this;
38
+
39
+ on(event: 'spectrumStateChanged', listener: (state: { active: boolean }) => void): this;
40
+ once(event: 'spectrumStateChanged', listener: (state: { active: boolean }) => void): this;
41
+ off(event: 'spectrumStateChanged', listener: (state: { active: boolean }) => void): this;
42
+
43
+ on(event: 'spectrumError', listener: (error: Error) => void): this;
44
+ once(event: 'spectrumError', listener: (error: Error) => void): this;
45
+ off(event: 'spectrumError', listener: (error: Error) => void): this;
46
+ }
47
+
48
+ declare const spectrumModule: {
49
+ SpectrumController: typeof SpectrumController;
50
+ };
51
+
52
+ export { SpectrumController };
53
+ export type {
54
+ HamLib,
55
+ ManagedSpectrumConfig,
56
+ SpectrumCapabilities,
57
+ SpectrumConfig,
58
+ SpectrumDisplayState,
59
+ SpectrumLine,
60
+ SpectrumSupportSummary,
61
+ };
62
+
63
+ export default spectrumModule;
@@ -0,0 +1,364 @@
1
+ const { EventEmitter } = require('events');
2
+
3
+ const DEFAULT_SPECTRUM_EDGE_SLOTS = [1, 2, 3, 4];
4
+
5
+ function normalizeSpectrumModeName(name) {
6
+ const normalized = String(name || '').trim().toLowerCase();
7
+ if (normalized === 'center') return 'center';
8
+ if (normalized === 'fixed') return 'fixed';
9
+ if (normalized === 'center scroll' || normalized === 'center-scroll' || normalized === 'scroll-center') return 'scroll-center';
10
+ if (normalized === 'fixed scroll' || normalized === 'fixed-scroll' || normalized === 'scroll-fixed') return 'scroll-fixed';
11
+ return null;
12
+ }
13
+
14
+ class SpectrumController extends EventEmitter {
15
+ constructor(rig) {
16
+ super();
17
+
18
+ if (!rig || typeof rig !== 'object') {
19
+ throw new TypeError('Expected a HamLib instance');
20
+ }
21
+
22
+ const requiredMethods = [
23
+ 'getSpectrumCapabilities',
24
+ 'getSupportedFunctions',
25
+ 'getSupportedLevels',
26
+ 'setLevel',
27
+ 'getLevel',
28
+ 'setFunction',
29
+ 'setConf',
30
+ 'getConf',
31
+ 'startSpectrumStream',
32
+ 'stopSpectrumStream',
33
+ ];
34
+
35
+ for (const method of requiredMethods) {
36
+ if (typeof rig[method] !== 'function') {
37
+ throw new TypeError(`Expected a HamLib instance with method ${method}()`);
38
+ }
39
+ }
40
+
41
+ this._rig = rig;
42
+ this._managedSpectrumRunning = false;
43
+ this._lastSpectrumLine = null;
44
+ this._pumpTimer = null;
45
+ this._pumpInFlight = false;
46
+ }
47
+
48
+ _normalizePumpIntervalMs(value) {
49
+ if (value === false || value === 0) {
50
+ return 0;
51
+ }
52
+ if (value === undefined || value === null) {
53
+ return 200;
54
+ }
55
+
56
+ const parsed = Number(value);
57
+ if (!Number.isFinite(parsed) || parsed < 0) {
58
+ throw new Error(`Invalid pumpIntervalMs: ${value}`);
59
+ }
60
+ return parsed;
61
+ }
62
+
63
+ _startPump(intervalMs) {
64
+ this._stopPump();
65
+
66
+ if (!intervalMs) {
67
+ return;
68
+ }
69
+
70
+ // Some backends only flush async spectrum data while normal CAT traffic continues.
71
+ this._pumpTimer = setInterval(() => {
72
+ if (!this._managedSpectrumRunning || this._pumpInFlight) {
73
+ return;
74
+ }
75
+
76
+ this._pumpInFlight = true;
77
+ this._rig.getFrequency()
78
+ .catch(() => {})
79
+ .finally(() => {
80
+ this._pumpInFlight = false;
81
+ });
82
+ }, intervalMs);
83
+ }
84
+
85
+ _stopPump() {
86
+ if (this._pumpTimer) {
87
+ clearInterval(this._pumpTimer);
88
+ this._pumpTimer = null;
89
+ }
90
+ this._pumpInFlight = false;
91
+ }
92
+
93
+ _recordSpectrumLine(line) {
94
+ if (!line || typeof line !== 'object') {
95
+ return line;
96
+ }
97
+ this._lastSpectrumLine = line;
98
+ return line;
99
+ }
100
+
101
+ _getLastSpectrumDisplayStateFromLine() {
102
+ const line = this._lastSpectrumLine;
103
+ if (!line || typeof line !== 'object') {
104
+ return null;
105
+ }
106
+
107
+ return {
108
+ modeId: Number.isFinite(line.mode) ? line.mode : null,
109
+ spanHz: Number.isFinite(line.spanHz) ? line.spanHz : null,
110
+ edgeLowHz: Number.isFinite(line.lowEdgeFreq) ? line.lowEdgeFreq : null,
111
+ edgeHighHz: Number.isFinite(line.highEdgeFreq) ? line.highEdgeFreq : null,
112
+ };
113
+ }
114
+
115
+ async getSpectrumSupportSummary() {
116
+ const capabilities = await this._rig.getSpectrumCapabilities();
117
+ const supportedFunctions = this._rig.getSupportedFunctions();
118
+ const supportedLevels = this._rig.getSupportedLevels();
119
+ const hasSpectrumFunction = supportedFunctions.includes('SPECTRUM');
120
+ const hasSpectrumHoldFunction = supportedFunctions.includes('SPECTRUM_HOLD');
121
+ const hasTransceiveFunction = supportedFunctions.includes('TRANSCEIVE');
122
+ const asyncDataSupported = capabilities.asyncDataSupported ?? hasSpectrumFunction;
123
+ const configurableLevels = ['SPECTRUM_MODE', 'SPECTRUM_SPAN', 'SPECTRUM_EDGE_LOW', 'SPECTRUM_EDGE_HIGH', 'SPECTRUM_SPEED', 'SPECTRUM_REF', 'SPECTRUM_AVG']
124
+ .filter((name) => supportedLevels.includes(name));
125
+ let supportsEdgeSlotSelection = false;
126
+
127
+ try {
128
+ const edgeSlot = await this._rig.getConf('SPECTRUM_EDGE');
129
+ supportsEdgeSlotSelection = Number.isFinite(Number.parseInt(String(edgeSlot), 10));
130
+ } catch (_) {
131
+ supportsEdgeSlotSelection = false;
132
+ }
133
+
134
+ return {
135
+ supported: Boolean(hasSpectrumFunction),
136
+ asyncDataSupported: Boolean(asyncDataSupported),
137
+ hasSpectrumFunction,
138
+ hasSpectrumHoldFunction,
139
+ hasTransceiveFunction,
140
+ configurableLevels,
141
+ supportsFixedEdges: configurableLevels.includes('SPECTRUM_EDGE_LOW') && configurableLevels.includes('SPECTRUM_EDGE_HIGH'),
142
+ supportsEdgeSlotSelection,
143
+ supportedEdgeSlots: supportsEdgeSlotSelection ? [...DEFAULT_SPECTRUM_EDGE_SLOTS] : [],
144
+ scopes: capabilities.scopes ?? [],
145
+ modes: capabilities.modes ?? [],
146
+ spans: capabilities.spans ?? [],
147
+ avgModes: capabilities.avgModes ?? [],
148
+ };
149
+ }
150
+
151
+ async configureSpectrum(config = {}) {
152
+ const summary = await this.getSpectrumSupportSummary();
153
+ const applyLevel = async (name, value) => {
154
+ if (value === undefined || !summary.configurableLevels.includes(name)) return;
155
+ await this._rig.setLevel(name, value);
156
+ };
157
+ const resolveModeId = async (mode) => {
158
+ if (mode === undefined || mode === null) return undefined;
159
+ if (Number.isFinite(mode)) return mode;
160
+ const requested = normalizeSpectrumModeName(mode);
161
+ if (!requested) {
162
+ throw new Error(`Unsupported spectrum mode: ${mode}`);
163
+ }
164
+ const matched = (summary.modes ?? []).find((entry) => normalizeSpectrumModeName(entry?.name) === requested);
165
+ if (!matched) {
166
+ throw new Error(`Spectrum mode not supported by this backend: ${mode}`);
167
+ }
168
+ return matched.id;
169
+ };
170
+
171
+ if (summary.hasSpectrumHoldFunction && config.hold !== undefined) {
172
+ await this._rig.setFunction('SPECTRUM_HOLD', Boolean(config.hold));
173
+ }
174
+
175
+ if (config.edgeSlot !== undefined && summary.supportsEdgeSlotSelection) {
176
+ await this.setSpectrumEdgeSlot(config.edgeSlot);
177
+ }
178
+
179
+ await applyLevel('SPECTRUM_MODE', await resolveModeId(config.mode));
180
+ await applyLevel('SPECTRUM_SPAN', config.spanHz);
181
+ if (config.edgeLowHz !== undefined || config.edgeHighHz !== undefined) {
182
+ await this.setSpectrumFixedEdges({
183
+ lowHz: config.edgeLowHz,
184
+ highHz: config.edgeHighHz,
185
+ });
186
+ }
187
+ await applyLevel('SPECTRUM_SPEED', config.speed);
188
+ await applyLevel('SPECTRUM_REF', config.referenceLevel);
189
+ await applyLevel('SPECTRUM_AVG', config.averageMode);
190
+
191
+ return summary;
192
+ }
193
+
194
+ async getSpectrumEdgeSlot() {
195
+ const raw = await this._rig.getConf('SPECTRUM_EDGE');
196
+ const parsed = Number.parseInt(String(raw), 10);
197
+ if (!Number.isFinite(parsed)) {
198
+ throw new Error('Spectrum edge slot is not available');
199
+ }
200
+ return parsed;
201
+ }
202
+
203
+ async setSpectrumEdgeSlot(slot) {
204
+ const parsed = Number.parseInt(String(slot), 10);
205
+ if (!Number.isFinite(parsed) || parsed < 1) {
206
+ throw new Error(`Invalid spectrum edge slot: ${slot}`);
207
+ }
208
+ await this._rig.setConf('SPECTRUM_EDGE', String(parsed));
209
+ return parsed;
210
+ }
211
+
212
+ async getSpectrumSupportedEdgeSlots() {
213
+ const summary = await this.getSpectrumSupportSummary();
214
+ return summary.supportedEdgeSlots ?? [];
215
+ }
216
+
217
+ async getSpectrumFixedEdges() {
218
+ const lineState = this._getLastSpectrumDisplayStateFromLine();
219
+ if (lineState?.edgeLowHz !== null && lineState?.edgeHighHz !== null) {
220
+ return { lowHz: lineState.edgeLowHz, highHz: lineState.edgeHighHz };
221
+ }
222
+
223
+ const [lowHz, highHz] = await Promise.all([
224
+ this._rig.getLevel('SPECTRUM_EDGE_LOW'),
225
+ this._rig.getLevel('SPECTRUM_EDGE_HIGH'),
226
+ ]);
227
+ return { lowHz, highHz };
228
+ }
229
+
230
+ async setSpectrumFixedEdges({ lowHz, highHz }) {
231
+ if (!Number.isFinite(lowHz) || !Number.isFinite(highHz) || lowHz >= highHz) {
232
+ throw new Error('Spectrum fixed edge range must satisfy lowHz < highHz');
233
+ }
234
+ await this._rig.setLevel('SPECTRUM_EDGE_LOW', lowHz);
235
+ await this._rig.setLevel('SPECTRUM_EDGE_HIGH', highHz);
236
+ return { lowHz, highHz };
237
+ }
238
+
239
+ async getSpectrumDisplayState() {
240
+ const summary = await this.getSpectrumSupportSummary();
241
+ const lineState = this._getLastSpectrumDisplayStateFromLine();
242
+ const [queriedModeId, queriedSpanHz, fixedEdges, edgeSlot] = await Promise.all([
243
+ summary.configurableLevels.includes('SPECTRUM_MODE') ? this._rig.getLevel('SPECTRUM_MODE') : Promise.resolve(null),
244
+ summary.configurableLevels.includes('SPECTRUM_SPAN') ? this._rig.getLevel('SPECTRUM_SPAN') : Promise.resolve(null),
245
+ summary.supportsFixedEdges ? this.getSpectrumFixedEdges().catch(() => null) : Promise.resolve(null),
246
+ summary.supportsEdgeSlotSelection ? this.getSpectrumEdgeSlot().catch(() => null) : Promise.resolve(null),
247
+ ]);
248
+
249
+ const modeId = queriedModeId ?? lineState?.modeId ?? null;
250
+ const spanHz = queriedSpanHz ?? lineState?.spanHz ?? null;
251
+ const modeInfo = (summary.modes ?? []).find((entry) => entry.id === modeId) ?? null;
252
+ const mode = normalizeSpectrumModeName(modeInfo?.name);
253
+ const edgeLowHz = fixedEdges?.lowHz ?? lineState?.edgeLowHz ?? null;
254
+ const edgeHighHz = fixedEdges?.highHz ?? lineState?.edgeHighHz ?? null;
255
+ const derivedSpanHz = (edgeLowHz !== null && edgeHighHz !== null) ? (edgeHighHz - edgeLowHz) : null;
256
+
257
+ return {
258
+ mode,
259
+ modeId,
260
+ modeName: modeInfo?.name ?? null,
261
+ spanHz: spanHz ?? derivedSpanHz,
262
+ edgeSlot,
263
+ edgeLowHz,
264
+ edgeHighHz,
265
+ supportedModes: summary.modes ?? [],
266
+ supportedSpans: summary.spans ?? [],
267
+ supportedEdgeSlots: summary.supportedEdgeSlots ?? [],
268
+ supportsFixedEdges: Boolean(summary.supportsFixedEdges),
269
+ supportsEdgeSlotSelection: Boolean(summary.supportsEdgeSlotSelection),
270
+ };
271
+ }
272
+
273
+ async configureSpectrumDisplay(config = {}) {
274
+ const normalizedConfig = { ...config };
275
+ if ((normalizedConfig.mode === 'fixed' || normalizedConfig.mode === 'scroll-fixed')
276
+ && normalizedConfig.edgeLowHz !== undefined
277
+ && normalizedConfig.edgeHighHz !== undefined
278
+ && normalizedConfig.edgeLowHz >= normalizedConfig.edgeHighHz) {
279
+ throw new Error('Spectrum fixed edge range must satisfy edgeLowHz < edgeHighHz');
280
+ }
281
+
282
+ await this.configureSpectrum(normalizedConfig);
283
+ return this.getSpectrumDisplayState();
284
+ }
285
+
286
+ async startManagedSpectrum(config = {}) {
287
+ const summary = await this.getSpectrumSupportSummary();
288
+ if (!summary.supported) {
289
+ throw new Error('Official Hamlib spectrum streaming is not supported by this rig/backend');
290
+ }
291
+
292
+ if (this._managedSpectrumRunning) {
293
+ return true;
294
+ }
295
+
296
+ const pumpIntervalMs = this._normalizePumpIntervalMs(config.pumpIntervalMs);
297
+
298
+ await this._rig.startSpectrumStream((line) => {
299
+ const recorded = this._recordSpectrumLine(line);
300
+ this.emit('spectrumLine', recorded);
301
+ });
302
+
303
+ try {
304
+ try {
305
+ await this._rig.setConf('async', '1');
306
+ } catch (_) {
307
+ try {
308
+ await this._rig.setConf('async', 'True');
309
+ } catch (_) {
310
+ // Not every backend accepts the config write even if async is supported.
311
+ }
312
+ }
313
+
314
+ await this.configureSpectrum({ hold: false, ...config });
315
+ await this._rig.setFunction('SPECTRUM', true);
316
+ if (summary.hasTransceiveFunction) {
317
+ await this._rig.setFunction('TRANSCEIVE', true);
318
+ }
319
+ } catch (error) {
320
+ this.emit('spectrumError', error);
321
+ try {
322
+ await this.stopManagedSpectrum();
323
+ } catch (_) {
324
+ // Ignore cleanup failures and surface the original startup error.
325
+ }
326
+ throw error;
327
+ }
328
+
329
+ this._managedSpectrumRunning = true;
330
+ this._startPump(pumpIntervalMs);
331
+ this.emit('spectrumStateChanged', { active: true });
332
+ return true;
333
+ }
334
+
335
+ async stopManagedSpectrum() {
336
+ if (!this._managedSpectrumRunning) {
337
+ return true;
338
+ }
339
+
340
+ try {
341
+ const supportedFunctions = this._rig.getSupportedFunctions();
342
+ if (supportedFunctions.includes('TRANSCEIVE')) {
343
+ await this._rig.setFunction('TRANSCEIVE', false);
344
+ }
345
+ if (supportedFunctions.includes('SPECTRUM_HOLD')) {
346
+ await this._rig.setFunction('SPECTRUM_HOLD', false);
347
+ }
348
+ if (supportedFunctions.includes('SPECTRUM')) {
349
+ await this._rig.setFunction('SPECTRUM', false);
350
+ }
351
+ } finally {
352
+ this._stopPump();
353
+ await this._rig.stopSpectrumStream();
354
+ }
355
+
356
+ this._managedSpectrumRunning = false;
357
+ this.emit('spectrumStateChanged', { active: false });
358
+ return true;
359
+ }
360
+ }
361
+
362
+ module.exports = { SpectrumController };
363
+ module.exports.SpectrumController = SpectrumController;
364
+ module.exports.default = { SpectrumController };
@@ -0,0 +1,7 @@
1
+ import { createRequire } from 'module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const { SpectrumController } = require('./spectrum.js');
5
+
6
+ export { SpectrumController };
7
+ export default { SpectrumController };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hamlib",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Node.js bindings for Hamlib rig control with official spectrum streaming support",
5
5
  "main": "index.js",
6
6
  "module": "lib/index.mjs",
@@ -10,6 +10,18 @@
10
10
  "import": "./lib/index.mjs",
11
11
  "require": "./index.js",
12
12
  "types": "./index.d.ts"
13
+ },
14
+ "./spectrum": {
15
+ "import": "./spectrum.mjs",
16
+ "require": "./spectrum.js",
17
+ "types": "./spectrum.d.ts"
18
+ }
19
+ },
20
+ "typesVersions": {
21
+ "*": {
22
+ "spectrum": [
23
+ "spectrum.d.ts"
24
+ ]
13
25
  }
14
26
  },
15
27
  "gypfile": true,
@@ -33,6 +45,9 @@
33
45
  "docs/",
34
46
  "index.js",
35
47
  "index.d.ts",
48
+ "spectrum.js",
49
+ "spectrum.mjs",
50
+ "spectrum.d.ts",
36
51
  "binding.gyp",
37
52
  "COPYING",
38
53
  "README.md"
@@ -46,6 +61,7 @@
46
61
  "test": "node test/test_loader.js",
47
62
  "test:version": "node test/test_version.js",
48
63
  "test:network": "node test/test_network.js",
64
+ "test:curr-vfo": "node test/test_curr_vfo_probe.js",
49
65
  "test:dummy": "node test/test_dummy_complete.js",
50
66
  "test:serial": "node test/test_serial_config.js",
51
67
  "test:spectrum": "node test/test_spectrum_stream.js",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/spectrum.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './lib/spectrum';
2
+ export { default } from './lib/spectrum';
package/spectrum.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./lib/spectrum.js');
package/spectrum.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export * from './lib/spectrum.mjs';
2
+ export { default } from './lib/spectrum.mjs';