hamlib 0.2.6 → 0.3.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.
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const path = require('path');
2
+ const { EventEmitter } = require('events');
2
3
  const nodeGypBuild = require('node-gyp-build');
3
4
  // Ensure loader resolves from package root (contains prebuilds/ and build/)
4
5
  const nativeModule = nodeGypBuild(path.join(__dirname, '..'));
@@ -9,7 +10,7 @@ const nativeModule = nodeGypBuild(path.join(__dirname, '..'));
9
10
  * This is a wrapper around the native hamlib module that provides
10
11
  * a consistent interface for controlling amateur radio devices.
11
12
  */
12
- class HamLib {
13
+ class HamLib extends EventEmitter {
13
14
  /**
14
15
  * Create a new HamLib instance
15
16
  * @param {number} model - Radio model number (run `rigctl -l` to find your device model)
@@ -18,7 +19,9 @@ class HamLib {
18
19
  * - Network: 'localhost:4532' or '192.168.1.100:4532' (auto-switches to NETRIGCTL mode)
19
20
  */
20
21
  constructor(model, port) {
22
+ super();
21
23
  this._nativeInstance = new nativeModule.HamLib(model, port);
24
+ this._managedSpectrumRunning = false;
22
25
  }
23
26
 
24
27
  /**
@@ -187,6 +190,9 @@ class HamLib {
187
190
  * Connection can be re-established by calling open()
188
191
  */
189
192
  async close() {
193
+ if (this._managedSpectrumRunning) {
194
+ await this.stopManagedSpectrum();
195
+ }
190
196
  return this._nativeInstance.close();
191
197
  }
192
198
 
@@ -195,6 +201,9 @@ class HamLib {
195
201
  * Object reference should be deleted after calling this
196
202
  */
197
203
  async destroy() {
204
+ if (this._managedSpectrumRunning) {
205
+ await this.stopManagedSpectrum();
206
+ }
198
207
  return this._nativeInstance.destroy();
199
208
  }
200
209
 
@@ -1151,6 +1160,158 @@ class HamLib {
1151
1160
  return this._nativeInstance.sendRaw(data, replyMaxLen);
1152
1161
  }
1153
1162
 
1163
+ /**
1164
+ * Get official Hamlib spectrum capability metadata from the backend.
1165
+ * @returns {Promise<Object>} Spectrum capability object.
1166
+ */
1167
+ async getSpectrumCapabilities() {
1168
+ return this._nativeInstance.getSpectrumCapabilities();
1169
+ }
1170
+
1171
+ /**
1172
+ * Summarize whether official Hamlib spectrum streaming is usable for this rig.
1173
+ * @returns {Promise<Object>} Product-oriented support summary.
1174
+ */
1175
+ async getSpectrumSupportSummary() {
1176
+ const capabilities = await this.getSpectrumCapabilities();
1177
+ const supportedFunctions = this.getSupportedFunctions();
1178
+ const supportedLevels = this.getSupportedLevels();
1179
+ const hasSpectrumFunction = supportedFunctions.includes('SPECTRUM');
1180
+ const hasSpectrumHoldFunction = supportedFunctions.includes('SPECTRUM_HOLD');
1181
+ const hasTransceiveFunction = supportedFunctions.includes('TRANSCEIVE');
1182
+ const asyncDataSupported = capabilities.asyncDataSupported ?? hasSpectrumFunction;
1183
+ const configurableLevels = ['SPECTRUM_MODE', 'SPECTRUM_SPAN', 'SPECTRUM_SPEED', 'SPECTRUM_REF', 'SPECTRUM_AVG']
1184
+ .filter((name) => supportedLevels.includes(name));
1185
+
1186
+ return {
1187
+ supported: Boolean(hasSpectrumFunction),
1188
+ asyncDataSupported: Boolean(asyncDataSupported),
1189
+ hasSpectrumFunction,
1190
+ hasSpectrumHoldFunction,
1191
+ hasTransceiveFunction,
1192
+ configurableLevels,
1193
+ scopes: capabilities.scopes ?? [],
1194
+ modes: capabilities.modes ?? [],
1195
+ spans: capabilities.spans ?? [],
1196
+ avgModes: capabilities.avgModes ?? [],
1197
+ };
1198
+ }
1199
+
1200
+ /**
1201
+ * Apply official Hamlib spectrum settings using high-level level/function APIs.
1202
+ * Unsupported settings are skipped silently.
1203
+ * @param {Object} [config]
1204
+ */
1205
+ async configureSpectrum(config = {}) {
1206
+ const summary = await this.getSpectrumSupportSummary();
1207
+ const applyLevel = async (name, value) => {
1208
+ if (value === undefined || !summary.configurableLevels.includes(name)) return;
1209
+ await this.setLevel(name, value);
1210
+ };
1211
+
1212
+ if (summary.hasSpectrumHoldFunction && config.hold !== undefined) {
1213
+ await this.setFunction('SPECTRUM_HOLD', Boolean(config.hold));
1214
+ }
1215
+
1216
+ await applyLevel('SPECTRUM_MODE', config.mode);
1217
+ await applyLevel('SPECTRUM_SPAN', config.spanHz);
1218
+ await applyLevel('SPECTRUM_SPEED', config.speed);
1219
+ await applyLevel('SPECTRUM_REF', config.referenceLevel);
1220
+ await applyLevel('SPECTRUM_AVG', config.averageMode);
1221
+
1222
+ return summary;
1223
+ }
1224
+
1225
+ /**
1226
+ * Start the official Hamlib spectrum callback stream.
1227
+ * @param {(line: Object) => void} [callback]
1228
+ * @returns {Promise<boolean>}
1229
+ */
1230
+ async startSpectrumStream(callback) {
1231
+ const listener = typeof callback === 'function'
1232
+ ? callback
1233
+ : (line) => this.emit('spectrumLine', line);
1234
+ return this._nativeInstance.startSpectrumStream(listener);
1235
+ }
1236
+
1237
+ /**
1238
+ * Stop the official Hamlib spectrum callback stream.
1239
+ * @returns {Promise<boolean>}
1240
+ */
1241
+ async stopSpectrumStream() {
1242
+ return this._nativeInstance.stopSpectrumStream();
1243
+ }
1244
+
1245
+ /**
1246
+ * High-level managed spectrum startup for application use.
1247
+ * @param {Object} [config]
1248
+ */
1249
+ async startManagedSpectrum(config = {}) {
1250
+ const summary = await this.getSpectrumSupportSummary();
1251
+ if (!summary.supported) {
1252
+ throw new Error('Official Hamlib spectrum streaming is not supported by this rig/backend');
1253
+ }
1254
+
1255
+ if (this._managedSpectrumRunning) {
1256
+ return true;
1257
+ }
1258
+
1259
+ await this.startSpectrumStream((line) => this.emit('spectrumLine', line));
1260
+
1261
+ try {
1262
+ try {
1263
+ await this.setConf('async', '1');
1264
+ } catch (_) {
1265
+ try {
1266
+ await this.setConf('async', 'True');
1267
+ } catch (_) {
1268
+ // Not every backend accepts the config write even if async is supported.
1269
+ }
1270
+ }
1271
+
1272
+ await this.configureSpectrum({ hold: false, ...config });
1273
+ await this.setFunction('SPECTRUM', true);
1274
+ if (summary.hasTransceiveFunction) {
1275
+ await this.setFunction('TRANSCEIVE', true);
1276
+ }
1277
+ } catch (error) {
1278
+ try {
1279
+ await this.stopManagedSpectrum();
1280
+ } catch (_) {
1281
+ // Ignore cleanup failures and surface the original startup error.
1282
+ }
1283
+ throw error;
1284
+ }
1285
+
1286
+ this._managedSpectrumRunning = true;
1287
+ this.emit('spectrumStateChanged', { active: true });
1288
+ return true;
1289
+ }
1290
+
1291
+ /**
1292
+ * High-level managed spectrum shutdown for application use.
1293
+ */
1294
+ async stopManagedSpectrum() {
1295
+ try {
1296
+ const supportedFunctions = this.getSupportedFunctions();
1297
+ if (supportedFunctions.includes('TRANSCEIVE')) {
1298
+ await this.setFunction('TRANSCEIVE', false);
1299
+ }
1300
+ if (supportedFunctions.includes('SPECTRUM_HOLD')) {
1301
+ await this.setFunction('SPECTRUM_HOLD', false);
1302
+ }
1303
+ if (supportedFunctions.includes('SPECTRUM')) {
1304
+ await this.setFunction('SPECTRUM', false);
1305
+ }
1306
+ } finally {
1307
+ await this.stopSpectrumStream();
1308
+ }
1309
+
1310
+ this._managedSpectrumRunning = false;
1311
+ this.emit('spectrumStateChanged', { active: false });
1312
+ return true;
1313
+ }
1314
+
1154
1315
  /**
1155
1316
  * Set a backend configuration parameter
1156
1317
  * @param {string} name - Configuration token name
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hamlib",
3
- "version": "0.2.6",
4
- "description": "Node.js wrapper for hamlib radio control library",
3
+ "version": "0.3.0",
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",
7
7
  "types": "index.d.ts",
@@ -30,6 +30,7 @@
30
30
  "lib/",
31
31
  "prebuilds/",
32
32
  "src/",
33
+ "docs/",
33
34
  "index.js",
34
35
  "index.d.ts",
35
36
  "binding.gyp",
@@ -43,9 +44,11 @@
43
44
  "rebuild": "node-gyp rebuild",
44
45
  "clean": "node-gyp clean",
45
46
  "test": "node test/test_loader.js",
47
+ "test:version": "node test/test_version.js",
46
48
  "test:network": "node test/test_network.js",
47
49
  "test:dummy": "node test/test_dummy_complete.js",
48
50
  "test:serial": "node test/test_serial_config.js",
51
+ "test:spectrum": "node test/test_spectrum_stream.js",
49
52
  "prebuild": "prebuildify --napi --strip",
50
53
  "prepare": "",
51
54
  "bundle": "node scripts/bundle-deps.js",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/hamlib.cpp CHANGED
@@ -4,6 +4,9 @@
4
4
  #include <vector>
5
5
  #include <memory>
6
6
  #include <algorithm>
7
+ #include <chrono>
8
+ #include <cstdio>
9
+ #include <exception>
7
10
 
8
11
  // 安全宏 - 检查RIG指针有效性,防止空指针解引用和已销毁对象访问
9
12
  #define CHECK_RIG_VALID() \
@@ -45,10 +48,13 @@ public:
45
48
  } else {
46
49
  shim_rig_set_freq_callback(hamlib_instance_->my_rig, NodeHamLib::freq_change_cb, hamlib_instance_);
47
50
  auto ppt_cb = +[](void* handle, int vfo, int ptt, void* arg) -> int {
48
- printf("PPT pushed!");
51
+ (void)handle;
52
+ (void)vfo;
53
+ (void)ptt;
54
+ (void)arg;
49
55
  return 0;
50
56
  };
51
- int cb_result = shim_rig_set_ptt_callback(hamlib_instance_->my_rig, ppt_cb, NULL);
57
+ shim_rig_set_ptt_callback(hamlib_instance_->my_rig, ppt_cb, NULL);
52
58
  shim_rig_set_trn(hamlib_instance_->my_rig, SHIM_RIG_TRN_POLL);
53
59
  hamlib_instance_->rig_is_open = true;
54
60
  }
@@ -2170,7 +2176,7 @@ private:
2170
2176
 
2171
2177
  // Helper function to parse VFO parameter from JavaScript
2172
2178
  int parseVfoParameter(const Napi::CallbackInfo& info, int index, int defaultVfo = SHIM_RIG_VFO_CURR) {
2173
- if (info.Length() > index && info[index].IsString()) {
2179
+ if (info.Length() > static_cast<size_t>(index) && info[index].IsString()) {
2174
2180
  std::string vfoStr = info[index].As<Napi::String>().Utf8Value();
2175
2181
  if (vfoStr == "VFO-A") {
2176
2182
  return SHIM_RIG_VFO_A;
@@ -2268,6 +2274,8 @@ NodeHamLib::NodeHamLib(const Napi::CallbackInfo & info): ObjectWrap(info) {
2268
2274
 
2269
2275
  // 析构函数 - 确保资源正确清理
2270
2276
  NodeHamLib::~NodeHamLib() {
2277
+ StopSpectrumStreamInternal();
2278
+
2271
2279
  // 如果rig指针存在,执行清理
2272
2280
  if (my_rig) {
2273
2281
  // 如果rig是打开状态,先关闭
@@ -2281,10 +2289,70 @@ NodeHamLib::~NodeHamLib() {
2281
2289
  }
2282
2290
  }
2283
2291
 
2292
+ int NodeHamLib::spectrum_line_cb(void* handle, const shim_spectrum_line_t* line, void* arg) {
2293
+ (void)handle;
2294
+ NodeHamLib* instance = static_cast<NodeHamLib*>(arg);
2295
+ if (!instance || !line) {
2296
+ return 0;
2297
+ }
2298
+ instance->EmitSpectrumLine(*line);
2299
+ return 0;
2300
+ }
2301
+
2302
+ void NodeHamLib::EmitSpectrumLine(const shim_spectrum_line_t& line) {
2303
+ if (!spectrum_tsfn_) {
2304
+ return;
2305
+ }
2306
+
2307
+ auto* line_copy = new shim_spectrum_line_t(line);
2308
+ napi_status status = spectrum_tsfn_.BlockingCall(
2309
+ line_copy,
2310
+ [](Napi::Env env, Napi::Function callback, shim_spectrum_line_t* data) {
2311
+ Napi::Object lineObject = Napi::Object::New(env);
2312
+ lineObject.Set("scopeId", Napi::Number::New(env, data->id));
2313
+ lineObject.Set("dataLevelMin", Napi::Number::New(env, data->data_level_min));
2314
+ lineObject.Set("dataLevelMax", Napi::Number::New(env, data->data_level_max));
2315
+ lineObject.Set("signalStrengthMin", Napi::Number::New(env, data->signal_strength_min));
2316
+ lineObject.Set("signalStrengthMax", Napi::Number::New(env, data->signal_strength_max));
2317
+ lineObject.Set("mode", Napi::Number::New(env, data->spectrum_mode));
2318
+ lineObject.Set("centerFreq", Napi::Number::New(env, data->center_freq));
2319
+ lineObject.Set("spanHz", Napi::Number::New(env, data->span_freq));
2320
+ lineObject.Set("lowEdgeFreq", Napi::Number::New(env, data->low_edge_freq));
2321
+ lineObject.Set("highEdgeFreq", Napi::Number::New(env, data->high_edge_freq));
2322
+ lineObject.Set("dataLength", Napi::Number::New(env, data->data_length));
2323
+ lineObject.Set("timestamp", Napi::Number::New(env, static_cast<double>(std::chrono::duration_cast<std::chrono::milliseconds>(
2324
+ std::chrono::system_clock::now().time_since_epoch()).count())));
2325
+ lineObject.Set("data", Napi::Buffer<unsigned char>::Copy(env, data->data, static_cast<size_t>(data->data_length)));
2326
+ callback.Call({ lineObject });
2327
+ delete data;
2328
+ });
2329
+
2330
+ if (status != napi_ok) {
2331
+ delete line_copy;
2332
+ return;
2333
+ }
2334
+ }
2335
+
2336
+ void NodeHamLib::StopSpectrumStreamInternal() {
2337
+ std::lock_guard<std::mutex> lock(spectrum_mutex_);
2338
+ if (spectrum_stream_running_) {
2339
+ shim_rig_set_spectrum_callback(my_rig, nullptr, nullptr);
2340
+ spectrum_stream_running_ = false;
2341
+ }
2342
+ if (spectrum_tsfn_) {
2343
+ spectrum_tsfn_.Release();
2344
+ spectrum_tsfn_ = Napi::ThreadSafeFunction();
2345
+ }
2346
+ }
2347
+
2284
2348
  int NodeHamLib::freq_change_cb(void *handle, int vfo, double freq, void* arg) {
2349
+ (void)handle;
2350
+ (void)vfo;
2351
+ (void)freq;
2285
2352
  auto instance = static_cast<NodeHamLib*>(arg);
2286
- printf("Rig changed freq to %0.7f Hz\n", freq);
2287
- Napi::Env env = instance->m_currentInfo->Env();
2353
+ if (!instance) {
2354
+ return 0;
2355
+ }
2288
2356
  //Napi::Function emit = instance->m_currentInfo[0].Get("emit").As<Napi::Function>();
2289
2357
  // Napi::Function emit = instance->m_currentInfo[0]->This().As<Napi::Object>().Get("emit").As<Napi::Function>();
2290
2358
  //emit.Call(instance->m_currentInfo->This(), { Napi::String::New(env, "frequency_change"), Napi::Number::New(env, freq) });
@@ -2293,7 +2361,6 @@ int NodeHamLib::freq_change_cb(void *handle, int vfo, double freq, void* arg) {
2293
2361
  //auto fn = global.Get("process").As<Napi::Object>().Get("emit").As<Napi::Function>();
2294
2362
  //fn.Call({Napi::Number::New(env, freq)});
2295
2363
  return 0;
2296
- return 0;
2297
2364
  }
2298
2365
 
2299
2366
  Napi::Value NodeHamLib::Open(const Napi::CallbackInfo & info) {
@@ -2565,6 +2632,8 @@ Napi::Value NodeHamLib::Close(const Napi::CallbackInfo & info) {
2565
2632
  .ThrowAsJavaScriptException();
2566
2633
  return env.Null();
2567
2634
  }
2635
+
2636
+ StopSpectrumStreamInternal();
2568
2637
 
2569
2638
  CloseAsyncWorker* worker = new CloseAsyncWorker(env, this);
2570
2639
  worker->Queue();
@@ -2574,6 +2643,8 @@ Napi::Value NodeHamLib::Close(const Napi::CallbackInfo & info) {
2574
2643
 
2575
2644
  Napi::Value NodeHamLib::Destroy(const Napi::CallbackInfo & info) {
2576
2645
  Napi::Env env = info.Env();
2646
+
2647
+ StopSpectrumStreamInternal();
2577
2648
 
2578
2649
  DestroyAsyncWorker* worker = new DestroyAsyncWorker(env, this);
2579
2650
  worker->Queue();
@@ -2912,6 +2983,22 @@ Napi::Value NodeHamLib::SetLevel(const Napi::CallbackInfo & info) {
2912
2983
  levelType = SHIM_RIG_LEVEL_VOXDELAY;
2913
2984
  } else if (levelTypeStr == "ANTIVOX") {
2914
2985
  levelType = SHIM_RIG_LEVEL_ANTIVOX;
2986
+ } else if (levelTypeStr == "SPECTRUM_MODE") {
2987
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_MODE;
2988
+ } else if (levelTypeStr == "SPECTRUM_SPAN") {
2989
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_SPAN;
2990
+ } else if (levelTypeStr == "SPECTRUM_EDGE_LOW") {
2991
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_EDGE_LOW;
2992
+ } else if (levelTypeStr == "SPECTRUM_EDGE_HIGH") {
2993
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_EDGE_HIGH;
2994
+ } else if (levelTypeStr == "SPECTRUM_SPEED") {
2995
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_SPEED;
2996
+ } else if (levelTypeStr == "SPECTRUM_REF") {
2997
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_REF;
2998
+ } else if (levelTypeStr == "SPECTRUM_AVG") {
2999
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_AVG;
3000
+ } else if (levelTypeStr == "SPECTRUM_ATT") {
3001
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_ATT;
2915
3002
  } else {
2916
3003
  Napi::TypeError::New(env, "Invalid level type").ThrowAsJavaScriptException();
2917
3004
  return env.Null();
@@ -2962,6 +3049,8 @@ Napi::Value NodeHamLib::GetLevel(const Napi::CallbackInfo & info) {
2962
3049
  levelType = SHIM_RIG_LEVEL_RAWSTR;
2963
3050
  } else if (levelTypeStr == "RFPOWER_METER") {
2964
3051
  levelType = SHIM_RIG_LEVEL_RFPOWER_METER;
3052
+ } else if (levelTypeStr == "RFPOWER_METER_WATTS") {
3053
+ levelType = SHIM_RIG_LEVEL_RFPOWER_METER_WATTS;
2965
3054
  } else if (levelTypeStr == "COMP_METER") {
2966
3055
  levelType = SHIM_RIG_LEVEL_COMP_METER;
2967
3056
  } else if (levelTypeStr == "VD_METER") {
@@ -2970,6 +3059,50 @@ Napi::Value NodeHamLib::GetLevel(const Napi::CallbackInfo & info) {
2970
3059
  levelType = SHIM_RIG_LEVEL_ID_METER;
2971
3060
  } else if (levelTypeStr == "TEMP_METER") {
2972
3061
  levelType = SHIM_RIG_LEVEL_TEMP_METER;
3062
+ } else if (levelTypeStr == "NR") {
3063
+ levelType = SHIM_RIG_LEVEL_NR;
3064
+ } else if (levelTypeStr == "AF") {
3065
+ levelType = SHIM_RIG_LEVEL_AF;
3066
+ } else if (levelTypeStr == "RF") {
3067
+ levelType = SHIM_RIG_LEVEL_RF;
3068
+ } else if (levelTypeStr == "SQL") {
3069
+ levelType = SHIM_RIG_LEVEL_SQL;
3070
+ } else if (levelTypeStr == "RFPOWER") {
3071
+ levelType = SHIM_RIG_LEVEL_RFPOWER;
3072
+ } else if (levelTypeStr == "MICGAIN") {
3073
+ levelType = SHIM_RIG_LEVEL_MICGAIN;
3074
+ } else if (levelTypeStr == "VOXDELAY") {
3075
+ levelType = SHIM_RIG_LEVEL_VOXDELAY;
3076
+ } else if (levelTypeStr == "PBT_IN") {
3077
+ levelType = SHIM_RIG_LEVEL_PBT_IN;
3078
+ } else if (levelTypeStr == "PBT_OUT") {
3079
+ levelType = SHIM_RIG_LEVEL_PBT_OUT;
3080
+ } else if (levelTypeStr == "CWPITCH") {
3081
+ levelType = SHIM_RIG_LEVEL_CWPITCH;
3082
+ } else if (levelTypeStr == "COMP") {
3083
+ levelType = SHIM_RIG_LEVEL_COMP;
3084
+ } else if (levelTypeStr == "AGC") {
3085
+ levelType = SHIM_RIG_LEVEL_AGC;
3086
+ } else if (levelTypeStr == "VOXGAIN") {
3087
+ levelType = SHIM_RIG_LEVEL_VOXGAIN;
3088
+ } else if (levelTypeStr == "ANTIVOX") {
3089
+ levelType = SHIM_RIG_LEVEL_ANTIVOX;
3090
+ } else if (levelTypeStr == "SPECTRUM_MODE") {
3091
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_MODE;
3092
+ } else if (levelTypeStr == "SPECTRUM_SPAN") {
3093
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_SPAN;
3094
+ } else if (levelTypeStr == "SPECTRUM_EDGE_LOW") {
3095
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_EDGE_LOW;
3096
+ } else if (levelTypeStr == "SPECTRUM_EDGE_HIGH") {
3097
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_EDGE_HIGH;
3098
+ } else if (levelTypeStr == "SPECTRUM_SPEED") {
3099
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_SPEED;
3100
+ } else if (levelTypeStr == "SPECTRUM_REF") {
3101
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_REF;
3102
+ } else if (levelTypeStr == "SPECTRUM_AVG") {
3103
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_AVG;
3104
+ } else if (levelTypeStr == "SPECTRUM_ATT") {
3105
+ levelType = SHIM_RIG_LEVEL_SPECTRUM_ATT;
2973
3106
  } else {
2974
3107
  Napi::TypeError::New(env, "Invalid level type").ThrowAsJavaScriptException();
2975
3108
  return env.Null();
@@ -3026,9 +3159,18 @@ Napi::Value NodeHamLib::GetSupportedLevels(const Napi::CallbackInfo & info) {
3026
3159
  if (levels & SHIM_RIG_LEVEL_SWR) levelArray[index++] = Napi::String::New(env, "SWR");
3027
3160
  if (levels & SHIM_RIG_LEVEL_ALC) levelArray[index++] = Napi::String::New(env, "ALC");
3028
3161
  if (levels & SHIM_RIG_LEVEL_RFPOWER_METER) levelArray[index++] = Napi::String::New(env, "RFPOWER_METER");
3162
+ if (levels & SHIM_RIG_LEVEL_RFPOWER_METER_WATTS) levelArray[index++] = Napi::String::New(env, "RFPOWER_METER_WATTS");
3029
3163
  if (levels & SHIM_RIG_LEVEL_COMP_METER) levelArray[index++] = Napi::String::New(env, "COMP_METER");
3030
3164
  if (levels & SHIM_RIG_LEVEL_VD_METER) levelArray[index++] = Napi::String::New(env, "VD_METER");
3031
3165
  if (levels & SHIM_RIG_LEVEL_ID_METER) levelArray[index++] = Napi::String::New(env, "ID_METER");
3166
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_MODE) levelArray[index++] = Napi::String::New(env, "SPECTRUM_MODE");
3167
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_SPAN) levelArray[index++] = Napi::String::New(env, "SPECTRUM_SPAN");
3168
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_EDGE_LOW) levelArray[index++] = Napi::String::New(env, "SPECTRUM_EDGE_LOW");
3169
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_EDGE_HIGH) levelArray[index++] = Napi::String::New(env, "SPECTRUM_EDGE_HIGH");
3170
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_SPEED) levelArray[index++] = Napi::String::New(env, "SPECTRUM_SPEED");
3171
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_REF) levelArray[index++] = Napi::String::New(env, "SPECTRUM_REF");
3172
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_AVG) levelArray[index++] = Napi::String::New(env, "SPECTRUM_AVG");
3173
+ if (levels & SHIM_RIG_LEVEL_SPECTRUM_ATT) levelArray[index++] = Napi::String::New(env, "SPECTRUM_ATT");
3032
3174
  if (levels & SHIM_RIG_LEVEL_TEMP_METER) levelArray[index++] = Napi::String::New(env, "TEMP_METER");
3033
3175
 
3034
3176
  return levelArray;
@@ -3109,6 +3251,12 @@ Napi::Value NodeHamLib::SetFunction(const Napi::CallbackInfo & info) {
3109
3251
  funcType = SHIM_RIG_FUNC_RESUME;
3110
3252
  } else if (funcTypeStr == "TBURST") {
3111
3253
  funcType = SHIM_RIG_FUNC_TBURST;
3254
+ } else if (funcTypeStr == "TRANSCEIVE") {
3255
+ funcType = SHIM_RIG_FUNC_TRANSCEIVE;
3256
+ } else if (funcTypeStr == "SPECTRUM") {
3257
+ funcType = SHIM_RIG_FUNC_SPECTRUM;
3258
+ } else if (funcTypeStr == "SPECTRUM_HOLD") {
3259
+ funcType = SHIM_RIG_FUNC_SPECTRUM_HOLD;
3112
3260
  } else {
3113
3261
  Napi::TypeError::New(env, "Invalid function type").ThrowAsJavaScriptException();
3114
3262
  return env.Null();
@@ -3193,6 +3341,12 @@ Napi::Value NodeHamLib::GetFunction(const Napi::CallbackInfo & info) {
3193
3341
  funcType = SHIM_RIG_FUNC_RESUME;
3194
3342
  } else if (funcTypeStr == "TBURST") {
3195
3343
  funcType = SHIM_RIG_FUNC_TBURST;
3344
+ } else if (funcTypeStr == "TRANSCEIVE") {
3345
+ funcType = SHIM_RIG_FUNC_TRANSCEIVE;
3346
+ } else if (funcTypeStr == "SPECTRUM") {
3347
+ funcType = SHIM_RIG_FUNC_SPECTRUM;
3348
+ } else if (funcTypeStr == "SPECTRUM_HOLD") {
3349
+ funcType = SHIM_RIG_FUNC_SPECTRUM_HOLD;
3196
3350
  } else {
3197
3351
  Napi::TypeError::New(env, "Invalid function type").ThrowAsJavaScriptException();
3198
3352
  return env.Null();
@@ -3241,6 +3395,9 @@ Napi::Value NodeHamLib::GetSupportedFunctions(const Napi::CallbackInfo & info) {
3241
3395
  if (functions & SHIM_RIG_FUNC_SCOPE) funcArray[index++] = Napi::String::New(env, "SCOPE");
3242
3396
  if (functions & SHIM_RIG_FUNC_RESUME) funcArray[index++] = Napi::String::New(env, "RESUME");
3243
3397
  if (functions & SHIM_RIG_FUNC_TBURST) funcArray[index++] = Napi::String::New(env, "TBURST");
3398
+ if (functions & SHIM_RIG_FUNC_TRANSCEIVE) funcArray[index++] = Napi::String::New(env, "TRANSCEIVE");
3399
+ if (functions & SHIM_RIG_FUNC_SPECTRUM) funcArray[index++] = Napi::String::New(env, "SPECTRUM");
3400
+ if (functions & SHIM_RIG_FUNC_SPECTRUM_HOLD) funcArray[index++] = Napi::String::New(env, "SPECTRUM_HOLD");
3244
3401
 
3245
3402
  return funcArray;
3246
3403
  }
@@ -3727,9 +3884,12 @@ Napi::Function NodeHamLib::GetClass(Napi::Env env) {
3727
3884
  // VFO Info (Hamlib >= 4.7.0)
3728
3885
  NodeHamLib::InstanceMethod("getVfoInfo", & NodeHamLib::GetVfoInfo),
3729
3886
 
3730
- // Rig Info / Raw / Conf (async)
3887
+ // Rig Info / Spectrum / Conf (async)
3731
3888
  NodeHamLib::InstanceMethod("getInfo", & NodeHamLib::GetInfo),
3732
3889
  NodeHamLib::InstanceMethod("sendRaw", & NodeHamLib::SendRaw),
3890
+ NodeHamLib::InstanceMethod("getSpectrumCapabilities", & NodeHamLib::GetSpectrumCapabilities),
3891
+ NodeHamLib::InstanceMethod("startSpectrumStream", & NodeHamLib::StartSpectrumStream),
3892
+ NodeHamLib::InstanceMethod("stopSpectrumStream", & NodeHamLib::StopSpectrumStream),
3733
3893
  NodeHamLib::InstanceMethod("setConf", & NodeHamLib::SetConf),
3734
3894
  NodeHamLib::InstanceMethod("getConf", & NodeHamLib::GetConf),
3735
3895
 
@@ -5806,6 +5966,55 @@ Napi::Value NodeHamLib::SendRaw(const Napi::CallbackInfo& info) {
5806
5966
  return asyncWorker->GetPromise();
5807
5967
  }
5808
5968
 
5969
+ Napi::Value NodeHamLib::GetSpectrumCapabilities(const Napi::CallbackInfo& info) {
5970
+ Napi::Env env = info.Env();
5971
+ Napi::Object result = Napi::Object::New(env);
5972
+
5973
+ result.Set("asyncDataSupported", env.Undefined());
5974
+ result.Set("scopes", Napi::Array::New(env));
5975
+ result.Set("modes", Napi::Array::New(env));
5976
+ result.Set("spans", Napi::Array::New(env));
5977
+ result.Set("avgModes", Napi::Array::New(env));
5978
+
5979
+ return result;
5980
+ }
5981
+
5982
+ Napi::Value NodeHamLib::StartSpectrumStream(const Napi::CallbackInfo& info) {
5983
+ Napi::Env env = info.Env();
5984
+ if (!rig_is_open) {
5985
+ Napi::TypeError::New(env, "Rig is not open!").ThrowAsJavaScriptException();
5986
+ return env.Null();
5987
+ }
5988
+ if (info.Length() < 1 || !info[0].IsFunction()) {
5989
+ Napi::TypeError::New(env, "Expected (callback: Function)").ThrowAsJavaScriptException();
5990
+ return env.Null();
5991
+ }
5992
+
5993
+ std::lock_guard<std::mutex> lock(spectrum_mutex_);
5994
+ if (spectrum_stream_running_) {
5995
+ Napi::Error::New(env, "Spectrum stream is already running").ThrowAsJavaScriptException();
5996
+ return env.Null();
5997
+ }
5998
+
5999
+ Napi::Function callback = info[0].As<Napi::Function>();
6000
+ spectrum_tsfn_ = Napi::ThreadSafeFunction::New(env, callback, "HamLibSpectrumStream", 0, 1);
6001
+ int ret = shim_rig_set_spectrum_callback(my_rig, &NodeHamLib::spectrum_line_cb, this);
6002
+ if (ret != SHIM_RIG_OK) {
6003
+ spectrum_tsfn_.Release();
6004
+ spectrum_tsfn_ = Napi::ThreadSafeFunction();
6005
+ Napi::Error::New(env, shim_rigerror(ret)).ThrowAsJavaScriptException();
6006
+ return env.Null();
6007
+ }
6008
+ spectrum_stream_running_ = true;
6009
+ return Napi::Boolean::New(env, true);
6010
+ }
6011
+
6012
+ Napi::Value NodeHamLib::StopSpectrumStream(const Napi::CallbackInfo& info) {
6013
+ Napi::Env env = info.Env();
6014
+ StopSpectrumStreamInternal();
6015
+ return Napi::Boolean::New(env, true);
6016
+ }
6017
+
5809
6018
  // ===== SetConf (async) =====
5810
6019
 
5811
6020
  class SetConfAsyncWorker : public HamLibAsyncWorker {
package/src/hamlib.h CHANGED
@@ -4,6 +4,10 @@
4
4
  #include "shim/hamlib_shim.h"
5
5
  #include <memory>
6
6
  #include <string>
7
+ #include <atomic>
8
+ #include <mutex>
9
+ #include <vector>
10
+ #include <cstdint>
7
11
 
8
12
  // Forward declaration
9
13
  class NodeHamLib;
@@ -173,6 +177,9 @@ class NodeHamLib : public Napi::ObjectWrap<NodeHamLib> {
173
177
  // Rig Info / Raw / Conf (async)
174
178
  Napi::Value GetInfo(const Napi::CallbackInfo&);
175
179
  Napi::Value SendRaw(const Napi::CallbackInfo&);
180
+ Napi::Value GetSpectrumCapabilities(const Napi::CallbackInfo&);
181
+ Napi::Value StartSpectrumStream(const Napi::CallbackInfo&);
182
+ Napi::Value StopSpectrumStream(const Napi::CallbackInfo&);
176
183
  Napi::Value SetConf(const Napi::CallbackInfo&);
177
184
  Napi::Value GetConf(const Napi::CallbackInfo&);
178
185
 
@@ -217,6 +224,7 @@ class NodeHamLib : public Napi::ObjectWrap<NodeHamLib> {
217
224
 
218
225
  // Frequency change callback (uses basic C types via shim)
219
226
  static int freq_change_cb(void* handle, int vfo, double freq, void* arg);
227
+ static int spectrum_line_cb(void* handle, const shim_spectrum_line_t* line, void* arg);
220
228
 
221
229
  public:
222
230
  hamlib_shim_handle_t my_rig; // Opaque handle instead of RIG*
@@ -234,4 +242,10 @@ class NodeHamLib : public Napi::ObjectWrap<NodeHamLib> {
234
242
 
235
243
  // Static callback helper for shim_rig_list_foreach
236
244
  static int rig_list_callback(const shim_rig_info_t* info, void* data);
245
+
246
+ void EmitSpectrumLine(const shim_spectrum_line_t& line);
247
+ void StopSpectrumStreamInternal();
248
+ std::atomic<bool> spectrum_stream_running_{false};
249
+ Napi::ThreadSafeFunction spectrum_tsfn_;
250
+ std::mutex spectrum_mutex_;
237
251
  };