hamlib 0.4.2 → 0.4.3

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
@@ -8,6 +8,8 @@ Control amateur radio transceivers from Node.js using the [Hamlib](https://hamli
8
8
  - **Full Async/Promise API** - Non-blocking operations with async/await support
9
9
  - **Comprehensive Serial Control** - 13 parameters for complete serial port configuration
10
10
  - **Multiple Connections** - Serial ports, network (rigctld), direct control
11
+ - **Rotator Bridge** - Control antenna rotators through Hamlib's `rot_*` API with the same Promise-first bridge style
12
+ - **Antenna Switching** - Rig antenna APIs now use true 1-based antenna numbers and optional antenna option values
11
13
  - **Official Spectrum Streaming** - Wraps Hamlib's official spectrum callback API with Promise helpers and typed events
12
14
  - **TypeScript Ready** - Complete type definitions included
13
15
  - **Cross-platform** - Windows, Linux, macOS
@@ -38,7 +40,7 @@ For faster installation or offline environments, you can manually install precom
38
40
  ## Quick Start
39
41
 
40
42
  ```javascript
41
- const { HamLib } = require('hamlib');
43
+ const { HamLib, Rotator } = require('hamlib');
42
44
 
43
45
  async function main() {
44
46
  // Create rig instance (model 1035 = FT-991A)
@@ -55,6 +57,13 @@ async function main() {
55
57
  console.log(`${freq/1000000} MHz ${mode.mode}`);
56
58
 
57
59
  await rig.close();
60
+
61
+ // Create rotator instance (model 1 = Dummy rotator)
62
+ const rotator = new Rotator(1);
63
+ await rotator.open();
64
+ await rotator.setPosition(180, 30);
65
+ console.log(await rotator.getPosition());
66
+ await rotator.close();
58
67
  }
59
68
 
60
69
  main().catch(console.error);
@@ -77,6 +86,27 @@ await rig.open();
77
86
  await rig.close();
78
87
  ```
79
88
 
89
+ ### Rotator Control
90
+
91
+ ```javascript
92
+ const rotators = Rotator.getSupportedRotators();
93
+ const dummy = rotators.find(r => r.modelName === 'Dummy');
94
+
95
+ const rotator = new Rotator(dummy.rotModel);
96
+ await rotator.open();
97
+
98
+ await rotator.setPosition(135, 20);
99
+ const position = await rotator.getPosition();
100
+
101
+ await rotator.move('RIGHT', 2);
102
+ await rotator.stop();
103
+
104
+ const status = await rotator.getStatus();
105
+ const caps = rotator.getRotatorCaps();
106
+
107
+ await rotator.close();
108
+ ```
109
+
80
110
  ### Basic Control
81
111
 
82
112
  ```javascript
@@ -136,6 +166,12 @@ const audioLevel = await rig.getLevel('AF');
136
166
  await rig.setFunction('NB', true); // Noise blanker on
137
167
  const voxEnabled = await rig.getFunction('VOX');
138
168
 
169
+ // Antenna switching (1-based antenna number)
170
+ await rig.setAntenna(2); // Select antenna 2
171
+ await rig.setAntenna(2, 7); // Select antenna 2 with backend-specific option
172
+ const antenna = await rig.getAntenna();
173
+ console.log(antenna.currentAntenna, antenna.option);
174
+
139
175
  // Split operation
140
176
  await rig.setSplit(true); // Enable split
141
177
  await rig.setSplitFreq(144340000); // TX frequency
package/binding.gyp CHANGED
@@ -4,6 +4,7 @@
4
4
  "target_name": "hamlib",
5
5
  "sources": [
6
6
  "src/hamlib.cpp",
7
+ "src/node_rotator.cpp",
7
8
  "src/decoder.cpp",
8
9
  "src/addon.cpp"
9
10
  ],
package/index.d.ts CHANGED
@@ -9,11 +9,15 @@ interface ConnectionInfo {
9
9
  /** Port path or network address */
10
10
  portPath: string;
11
11
  /** Connection status */
12
- connected: boolean;
12
+ isOpen: boolean;
13
13
  /** Original model number */
14
14
  originalModel: number;
15
15
  /** Actual model number used */
16
- actualModel: number;
16
+ currentModel: number;
17
+ /** @deprecated Use isOpen */
18
+ connected?: boolean;
19
+ /** @deprecated Use currentModel */
20
+ actualModel?: number;
17
21
  }
18
22
 
19
23
  /**
@@ -44,6 +48,16 @@ interface SupportedRigInfo {
44
48
  rigType: string;
45
49
  }
46
50
 
51
+ interface SupportedRotatorInfo {
52
+ rotModel: number;
53
+ modelName: string;
54
+ mfgName: string;
55
+ version: string;
56
+ status: string;
57
+ rotType: 'azimuth' | 'elevation' | 'azel' | 'other';
58
+ rotTypeMask: number;
59
+ }
60
+
47
61
  /**
48
62
  * Antenna information interface
49
63
  */
@@ -58,6 +72,53 @@ interface AntennaInfo {
58
72
  option: number;
59
73
  }
60
74
 
75
+ interface RotatorConnectionInfo {
76
+ connectionType: 'serial' | 'network';
77
+ portPath: string;
78
+ isOpen: boolean;
79
+ originalModel: number;
80
+ currentModel: number;
81
+ }
82
+
83
+ interface RotatorPosition {
84
+ azimuth: number;
85
+ elevation: number;
86
+ }
87
+
88
+ interface RotatorStatus {
89
+ mask: number;
90
+ flags: string[];
91
+ }
92
+
93
+ type RotatorDirection =
94
+ | 'UP'
95
+ | 'DOWN'
96
+ | 'LEFT'
97
+ | 'RIGHT'
98
+ | 'CCW'
99
+ | 'CW'
100
+ | 'UP_LEFT'
101
+ | 'UP_RIGHT'
102
+ | 'DOWN_LEFT'
103
+ | 'DOWN_RIGHT'
104
+ | 'UP_CCW'
105
+ | 'UP_CW'
106
+ | 'DOWN_CCW'
107
+ | 'DOWN_CW'
108
+ | number;
109
+
110
+ type RotatorResetType = 'ALL' | number;
111
+
112
+ interface RotatorCaps {
113
+ rotType: 'azimuth' | 'elevation' | 'azel' | 'other';
114
+ rotTypeMask: number;
115
+ minAz: number;
116
+ maxAz: number;
117
+ minEl: number;
118
+ maxEl: number;
119
+ supportedStatuses: string[];
120
+ }
121
+
61
122
  /**
62
123
  * Hamlib VFO token.
63
124
  * 实际支持的 token 取决于具体 backend / 电台型号。
@@ -1299,6 +1360,7 @@ declare class HamLib extends EventEmitter {
1299
1360
  * await rig.setAntenna(2); // Select antenna 2
1300
1361
  */
1301
1362
  setAntenna(antenna: number, vfo?: VFO): Promise<number>;
1363
+ setAntenna(antenna: number, option: number, vfo?: VFO): Promise<number>;
1302
1364
 
1303
1365
  /**
1304
1366
  * Get comprehensive antenna information
@@ -1574,6 +1636,50 @@ declare class HamLib extends EventEmitter {
1574
1636
  getVfoInfo(vfo?: VFO): Promise<VfoInfo>;
1575
1637
  }
1576
1638
 
1639
+ declare class Rotator extends EventEmitter {
1640
+ constructor(model: number, port?: string);
1641
+
1642
+ static getSupportedRotators(): SupportedRotatorInfo[];
1643
+ static getHamlibVersion(): string;
1644
+ static setDebugLevel(level: RigDebugLevel): void;
1645
+ static getDebugLevel(): never;
1646
+ static getCopyright(): string;
1647
+ static getLicense(): string;
1648
+
1649
+ open(): Promise<number>;
1650
+ close(): Promise<number>;
1651
+ destroy(): Promise<number>;
1652
+ getConnectionInfo(): RotatorConnectionInfo;
1653
+
1654
+ setPosition(azimuth: number, elevation: number): Promise<number>;
1655
+ getPosition(): Promise<RotatorPosition>;
1656
+ move(direction: RotatorDirection, speed: number): Promise<number>;
1657
+ stop(): Promise<number>;
1658
+ park(): Promise<number>;
1659
+ reset(resetType: RotatorResetType): Promise<number>;
1660
+
1661
+ getInfo(): Promise<string>;
1662
+ getStatus(): Promise<RotatorStatus>;
1663
+
1664
+ setConf(name: string, value: string): Promise<number>;
1665
+ getConf(name: string): Promise<string>;
1666
+ getConfigSchema(): HamlibConfigFieldDescriptor[];
1667
+ getPortCaps(): HamlibPortCaps;
1668
+ getRotatorCaps(): RotatorCaps;
1669
+
1670
+ setLevel(level: string, value: number): Promise<number>;
1671
+ getLevel(level: string): Promise<number>;
1672
+ getSupportedLevels(): string[];
1673
+
1674
+ setFunction(func: string, enable: boolean): Promise<number>;
1675
+ getFunction(func: string): Promise<boolean>;
1676
+ getSupportedFunctions(): string[];
1677
+
1678
+ setParm(parm: string, value: number): Promise<number>;
1679
+ getParm(parm: string): Promise<number>;
1680
+ getSupportedParms(): string[];
1681
+ }
1682
+
1577
1683
  /**
1578
1684
  * Clock information for rig's internal clock
1579
1685
  */
@@ -1604,17 +1710,19 @@ interface VfoInfo {
1604
1710
  */
1605
1711
  declare const nodeHamlib: {
1606
1712
  HamLib: typeof HamLib;
1713
+ Rotator: typeof Rotator;
1607
1714
  };
1608
1715
 
1609
1716
  // Export types for use elsewhere
1610
- export { ConnectionInfo, ModeInfo, SupportedRigInfo, AntennaInfo, VFO, RadioMode, MemoryChannelData,
1717
+ export { ConnectionInfo, ModeInfo, SupportedRigInfo, SupportedRotatorInfo, AntennaInfo, RotatorConnectionInfo,
1718
+ RotatorPosition, RotatorStatus, RotatorDirection, RotatorResetType, RotatorCaps, VFO, RadioMode, MemoryChannelData,
1611
1719
  MemoryChannelInfo, SplitModeInfo, SplitStatusInfo, LevelType, FunctionType,
1612
1720
  ScanType, VfoOperationType, SerialConfigParam, SerialBaudRate, SerialParity,
1613
1721
  SerialHandshake, SerialControlState, PttType, DcdType, SerialConfigOptions,
1614
1722
  HamlibConfigFieldType, HamlibConfigFieldDescriptor, HamlibPortType, HamlibPortCaps,
1615
1723
  SpectrumScopeInfo, SpectrumModeInfo, SpectrumAverageModeInfo, SpectrumLine,
1616
1724
  SpectrumCapabilities, SpectrumSupportSummary, SpectrumConfig, SpectrumDisplayState,
1617
- ClockInfo, VfoInfo, HamLib };
1725
+ ClockInfo, VfoInfo, HamLib, Rotator };
1618
1726
 
1619
1727
  // Support both CommonJS and ES module exports
1620
1728
  // @ts-ignore
package/lib/index.js CHANGED
@@ -1073,18 +1073,21 @@ class HamLib extends EventEmitter {
1073
1073
  /**
1074
1074
  * Set antenna selection
1075
1075
  * @param {number} antenna - Antenna number to select (1-based)
1076
+ * @param {number|string} [optionOrVfo] - Optional antenna option or VFO token
1076
1077
  * @param {string} [vfo] - Optional VFO ('VFOA' or 'VFOB')
1077
1078
  * @returns {Promise<number>} Success status
1078
1079
  * @example
1079
1080
  * await rig.setAntenna(1); // Select antenna 1
1080
1081
  * await rig.setAntenna(2); // Select antenna 2
1081
1082
  */
1082
- async setAntenna(antenna, vfo) {
1083
+ async setAntenna(antenna, optionOrVfo, vfo) {
1083
1084
  if (vfo !== undefined) {
1084
- return this._nativeInstance.setAntenna(antenna, vfo);
1085
- } else {
1086
- return this._nativeInstance.setAntenna(antenna);
1085
+ return this._nativeInstance.setAntenna(antenna, optionOrVfo, vfo);
1086
+ }
1087
+ if (optionOrVfo !== undefined) {
1088
+ return this._nativeInstance.setAntenna(antenna, optionOrVfo);
1087
1089
  }
1090
+ return this._nativeInstance.setAntenna(antenna);
1088
1091
  }
1089
1092
 
1090
1093
  /**
@@ -1410,7 +1413,143 @@ class HamLib extends EventEmitter {
1410
1413
  }
1411
1414
  }
1412
1415
 
1416
+ class Rotator extends EventEmitter {
1417
+ constructor(model, port) {
1418
+ super();
1419
+ this._nativeInstance = new nativeModule.Rotator(model, port);
1420
+ }
1421
+
1422
+ static getSupportedRotators() {
1423
+ return nativeModule.Rotator.getSupportedRotators();
1424
+ }
1425
+
1426
+ static getHamlibVersion() {
1427
+ return nativeModule.Rotator.getHamlibVersion();
1428
+ }
1429
+
1430
+ static setDebugLevel(level) {
1431
+ return nativeModule.Rotator.setDebugLevel(level);
1432
+ }
1433
+
1434
+ static getDebugLevel() {
1435
+ return nativeModule.Rotator.getDebugLevel();
1436
+ }
1437
+
1438
+ static getCopyright() {
1439
+ return nativeModule.Rotator.getCopyright();
1440
+ }
1441
+
1442
+ static getLicense() {
1443
+ return nativeModule.Rotator.getLicense();
1444
+ }
1445
+
1446
+ async open() {
1447
+ return this._nativeInstance.open();
1448
+ }
1449
+
1450
+ async close() {
1451
+ return this._nativeInstance.close();
1452
+ }
1453
+
1454
+ async destroy() {
1455
+ return this._nativeInstance.destroy();
1456
+ }
1457
+
1458
+ getConnectionInfo() {
1459
+ return this._nativeInstance.getConnectionInfo();
1460
+ }
1461
+
1462
+ async setPosition(azimuth, elevation) {
1463
+ return this._nativeInstance.setPosition(azimuth, elevation);
1464
+ }
1465
+
1466
+ async getPosition() {
1467
+ return this._nativeInstance.getPosition();
1468
+ }
1469
+
1470
+ async move(direction, speed) {
1471
+ return this._nativeInstance.move(direction, speed);
1472
+ }
1473
+
1474
+ async stop() {
1475
+ return this._nativeInstance.stop();
1476
+ }
1477
+
1478
+ async park() {
1479
+ return this._nativeInstance.park();
1480
+ }
1481
+
1482
+ async reset(resetType) {
1483
+ return this._nativeInstance.reset(resetType);
1484
+ }
1485
+
1486
+ async getInfo() {
1487
+ return this._nativeInstance.getInfo();
1488
+ }
1489
+
1490
+ async getStatus() {
1491
+ return this._nativeInstance.getStatus();
1492
+ }
1493
+
1494
+ async setConf(name, value) {
1495
+ return this._nativeInstance.setConf(name, value);
1496
+ }
1497
+
1498
+ async getConf(name) {
1499
+ return this._nativeInstance.getConf(name);
1500
+ }
1501
+
1502
+ getConfigSchema() {
1503
+ return this._nativeInstance.getConfigSchema();
1504
+ }
1505
+
1506
+ getPortCaps() {
1507
+ return this._nativeInstance.getPortCaps();
1508
+ }
1509
+
1510
+ getRotatorCaps() {
1511
+ return this._nativeInstance.getRotatorCaps();
1512
+ }
1513
+
1514
+ async setLevel(level, value) {
1515
+ return this._nativeInstance.setLevel(level, value);
1516
+ }
1517
+
1518
+ async getLevel(level) {
1519
+ return this._nativeInstance.getLevel(level);
1520
+ }
1521
+
1522
+ getSupportedLevels() {
1523
+ return this._nativeInstance.getSupportedLevels();
1524
+ }
1525
+
1526
+ async setFunction(func, enable) {
1527
+ return this._nativeInstance.setFunction(func, enable);
1528
+ }
1529
+
1530
+ async getFunction(func) {
1531
+ return this._nativeInstance.getFunction(func);
1532
+ }
1533
+
1534
+ getSupportedFunctions() {
1535
+ return this._nativeInstance.getSupportedFunctions();
1536
+ }
1537
+
1538
+ async setParm(parm, value) {
1539
+ return this._nativeInstance.setParm(parm, value);
1540
+ }
1541
+
1542
+ async getParm(parm) {
1543
+ return this._nativeInstance.getParm(parm);
1544
+ }
1545
+
1546
+ getSupportedParms() {
1547
+ return this._nativeInstance.getSupportedParms();
1548
+ }
1549
+ }
1550
+
1413
1551
  // Export for CommonJS
1414
- module.exports = { HamLib };
1552
+ module.exports = { HamLib, Rotator };
1415
1553
  module.exports.HamLib = HamLib;
1416
- module.exports.default = { HamLib };
1554
+ module.exports.Rotator = Rotator;
1555
+ module.exports.default = { HamLib, Rotator };
package/lib/index.mjs CHANGED
@@ -7,8 +7,8 @@ const __dirname = path.dirname(__filename);
7
7
  const require = createRequire(import.meta.url);
8
8
 
9
9
  // Import the CommonJS module
10
- const { HamLib } = require('./index.js');
10
+ const { HamLib, Rotator } = require('./index.js');
11
11
 
12
12
  // Export for ES modules
13
- export { HamLib };
14
- export default { HamLib };
13
+ export { HamLib, Rotator };
14
+ export default { HamLib, Rotator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hamlib",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/addon.cpp CHANGED
@@ -1,5 +1,6 @@
1
1
  #include <napi.h>
2
2
  #include "hamlib.h"
3
+ #include "node_rotator.h"
3
4
  #include "decoder.h"
4
5
  #include "shim/hamlib_shim.h"
5
6
 
@@ -11,6 +12,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
11
12
  Napi::String name = Napi::String::New(env, "HamLib");
12
13
  exports.Set(name, NodeHamLib::GetClass(env));
13
14
 
15
+ Napi::String rotatorName = Napi::String::New(env, "Rotator");
16
+ exports.Set(rotatorName, NodeRotator::GetClass(env));
17
+
14
18
 
15
19
  // Napi::String decoder_name = Napi::String::New(env, "Decoder");
16
20
  // exports.Set(decoder_name, Decoder::GetClass(env));
@@ -18,4 +22,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
18
22
  return exports;
19
23
  }
20
24
 
21
- NODE_API_MODULE(radio, Init)
25
+ NODE_API_MODULE(radio, Init)
package/src/hamlib.cpp CHANGED
@@ -106,6 +106,27 @@ static int parseVfoString(Napi::Env env, const std::string& vfoToken) {
106
106
  return vfo;
107
107
  }
108
108
 
109
+ static int antennaOrdinalToMask(Napi::Env env, int antennaOrdinal) {
110
+ if (antennaOrdinal < 1 || antennaOrdinal > 30) {
111
+ Napi::RangeError::New(env, "Antenna number must be between 1 and 30").ThrowAsJavaScriptException();
112
+ return 0;
113
+ }
114
+ return static_cast<int>(1u << (antennaOrdinal - 1));
115
+ }
116
+
117
+ static int antennaMaskToOrdinal(int antennaMask) {
118
+ const uint32_t mask = static_cast<uint32_t>(antennaMask);
119
+ if (mask == 0 || (mask & (mask - 1)) != 0) {
120
+ return 0;
121
+ }
122
+ for (int bit = 0; bit < 30; ++bit) {
123
+ if (mask == (1u << bit)) {
124
+ return bit + 1;
125
+ }
126
+ }
127
+ return 0;
128
+ }
129
+
109
130
  // Base AsyncWorker implementation with Promise support
110
131
  HamLibAsyncWorker::HamLibAsyncWorker(Napi::Env env, NodeHamLib* hamlib_instance)
111
132
  : AsyncWorker(env), hamlib_instance_(hamlib_instance), result_code_(0), error_message_(""), deferred_(Napi::Promise::Deferred::New(env)) {}
@@ -1230,9 +1251,9 @@ public:
1230
1251
  } else {
1231
1252
  Napi::Object result = Napi::Object::New(env);
1232
1253
 
1233
- result.Set("currentAntenna", Napi::Number::New(env, antenna_curr_));
1234
- result.Set("txAntenna", Napi::Number::New(env, antenna_tx_));
1235
- result.Set("rxAntenna", Napi::Number::New(env, antenna_rx_));
1254
+ result.Set("currentAntenna", Napi::Number::New(env, antennaMaskToOrdinal(antenna_curr_)));
1255
+ result.Set("txAntenna", Napi::Number::New(env, antennaMaskToOrdinal(antenna_tx_)));
1256
+ result.Set("rxAntenna", Napi::Number::New(env, antennaMaskToOrdinal(antenna_rx_)));
1236
1257
  result.Set("option", Napi::Number::New(env, option_));
1237
1258
 
1238
1259
  deferred_.Resolve(result);
@@ -3768,18 +3789,39 @@ Napi::Value NodeHamLib::SetAntenna(const Napi::CallbackInfo & info) {
3768
3789
  return env.Null();
3769
3790
  }
3770
3791
 
3771
- int antenna = info[0].As<Napi::Number>().Int32Value();
3772
-
3773
- // Support optional VFO parameter: setAntenna(antenna) or setAntenna(antenna, vfo)
3774
- int vfo = SHIM_RIG_VFO_CURR;
3775
- if (info.Length() >= 2 && info[1].IsString()) {
3776
- vfo = parseVfoParameter(info, 1, SHIM_RIG_VFO_CURR);
3777
- RETURN_NULL_IF_INVALID_VFO(vfo);
3792
+ const int antennaOrdinal = info[0].As<Napi::Number>().Int32Value();
3793
+ const int antenna = antennaOrdinalToMask(env, antennaOrdinal);
3794
+ if (antenna == 0) {
3795
+ return env.Null();
3778
3796
  }
3779
-
3780
- // Default option value (can be extended later if needed)
3797
+
3798
+ int vfo = SHIM_RIG_VFO_CURR;
3781
3799
  float option = 0.0f;
3782
3800
 
3801
+ if (info.Length() >= 2) {
3802
+ if (info[1].IsNumber()) {
3803
+ option = info[1].As<Napi::Number>().FloatValue();
3804
+ if (info.Length() >= 3 && info[2].IsString()) {
3805
+ vfo = parseVfoParameter(info, 2, SHIM_RIG_VFO_CURR);
3806
+ RETURN_NULL_IF_INVALID_VFO(vfo);
3807
+ } else if (info.Length() >= 3 && !info[2].IsUndefined()) {
3808
+ Napi::TypeError::New(env, "Expected VFO token as third argument").ThrowAsJavaScriptException();
3809
+ return env.Null();
3810
+ }
3811
+ } else if (info[1].IsString()) {
3812
+ vfo = parseVfoParameter(info, 1, SHIM_RIG_VFO_CURR);
3813
+ RETURN_NULL_IF_INVALID_VFO(vfo);
3814
+ } else if (!info[1].IsUndefined()) {
3815
+ Napi::TypeError::New(env, "Expected antenna option as number or VFO token as string").ThrowAsJavaScriptException();
3816
+ return env.Null();
3817
+ }
3818
+ }
3819
+
3820
+ if (info.Length() >= 3 && info[1].IsString()) {
3821
+ Napi::TypeError::New(env, "Antenna option must come before VFO").ThrowAsJavaScriptException();
3822
+ return env.Null();
3823
+ }
3824
+
3783
3825
  SetAntennaAsyncWorker* asyncWorker = new SetAntennaAsyncWorker(env, this, antenna, vfo, option);
3784
3826
  asyncWorker->Queue();
3785
3827
  return asyncWorker->GetPromise();