wsjtx-lib 1.0.2 → 1.0.4
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 +2 -0
- package/dist/chunk-IOOVX2IY.js +39 -0
- package/dist/chunk-JMWITWT7.js +74 -0
- package/dist/chunk-WYLCLDX4.js +483 -0
- package/dist/index.cjs +554 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +192 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/dist/src/index.cjs +554 -0
- package/dist/src/index.d.cts +192 -0
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +24 -24
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.cjs +64 -0
- package/dist/src/types.d.cts +252 -0
- package/dist/test/wsjtx.basic.test.cjs +759 -0
- package/dist/test/wsjtx.basic.test.d.cts +2 -0
- package/dist/test/wsjtx.basic.test.js +12 -14
- package/dist/test/wsjtx.basic.test.js.map +1 -1
- package/dist/test/wsjtx.test.cjs +4004 -0
- package/dist/test/wsjtx.test.d.cts +2 -0
- package/dist/test/wsjtx.test.js +12 -14
- package/dist/test/wsjtx.test.js.map +1 -1
- package/dist/types.cjs +64 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +252 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/build-info.json +2 -2
- package/prebuilds/darwin-arm64/libfftw3f.3.dylib +0 -0
- package/prebuilds/darwin-arm64/libfftw3f_threads.3.dylib +0 -0
- package/prebuilds/darwin-arm64/libgfortran.5.dylib +0 -0
- package/prebuilds/darwin-arm64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/linux-arm64/build-info.json +10 -0
- package/prebuilds/linux-arm64/libfftw3f.so.3 +0 -0
- package/prebuilds/linux-arm64/libfftw3f_threads.so.3 +0 -0
- package/prebuilds/linux-arm64/libgcc_s.so.1 +0 -0
- package/prebuilds/linux-arm64/libgfortran.so.5 +0 -0
- package/prebuilds/linux-arm64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/linux-x64/build-info.json +2 -2
- package/prebuilds/linux-x64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/package-info.json +9 -3
- package/prebuilds/win32-x64/build-info.json +3 -3
- package/prebuilds/win32-x64/libgcc_s_seh-1.dll +0 -0
- package/prebuilds/win32-x64/libgfortran-5.dll +0 -0
- package/prebuilds/win32-x64/libstdc++-6.dll +0 -0
- package/prebuilds/win32-x64/libwinpthread-1.dll +0 -0
- package/prebuilds/win32-x64/wsjtx_lib_nodejs.node +0 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// test/wsjtx.basic.test.ts
|
|
26
|
+
var import_node_test = require("test");
|
|
27
|
+
var import_node_assert = __toESM(require("assert"), 1);
|
|
28
|
+
|
|
29
|
+
// src/types.ts
|
|
30
|
+
var WSJTXMode = /* @__PURE__ */ ((WSJTXMode2) => {
|
|
31
|
+
WSJTXMode2[WSJTXMode2["FT8"] = 0] = "FT8";
|
|
32
|
+
WSJTXMode2[WSJTXMode2["FT4"] = 1] = "FT4";
|
|
33
|
+
WSJTXMode2[WSJTXMode2["JT4"] = 2] = "JT4";
|
|
34
|
+
WSJTXMode2[WSJTXMode2["JT65"] = 3] = "JT65";
|
|
35
|
+
WSJTXMode2[WSJTXMode2["JT9"] = 4] = "JT9";
|
|
36
|
+
WSJTXMode2[WSJTXMode2["FST4"] = 5] = "FST4";
|
|
37
|
+
WSJTXMode2[WSJTXMode2["Q65"] = 6] = "Q65";
|
|
38
|
+
WSJTXMode2[WSJTXMode2["FST4W"] = 7] = "FST4W";
|
|
39
|
+
WSJTXMode2[WSJTXMode2["WSPR"] = 8] = "WSPR";
|
|
40
|
+
return WSJTXMode2;
|
|
41
|
+
})(WSJTXMode || {});
|
|
42
|
+
var WSJTXError = class extends Error {
|
|
43
|
+
constructor(message, code) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.name = "WSJTXError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/index.ts
|
|
51
|
+
var import_node_module = require("module");
|
|
52
|
+
var import_node_url = require("url");
|
|
53
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
54
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
55
|
+
var import_meta = {};
|
|
56
|
+
function getModuleContext() {
|
|
57
|
+
try {
|
|
58
|
+
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
59
|
+
return {
|
|
60
|
+
require: (0, import_node_module.createRequire)(import_meta.url),
|
|
61
|
+
__filename: (0, import_node_url.fileURLToPath)(import_meta.url),
|
|
62
|
+
__dirname: import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
require: typeof require2 !== "undefined" ? require2 : (0, import_node_module.createRequire)("file://"),
|
|
69
|
+
__filename: typeof __filename !== "undefined" ? __filename : "",
|
|
70
|
+
__dirname: typeof __dirname !== "undefined" ? __dirname : ""
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var moduleContext = getModuleContext();
|
|
74
|
+
var { require: require2, __filename, __dirname } = moduleContext;
|
|
75
|
+
function findNativeModule() {
|
|
76
|
+
const platform = process.platform;
|
|
77
|
+
const arch = process.arch;
|
|
78
|
+
const isPkgEnvironment = !!process.pkg || __dirname.includes("/snapshot/") || __dirname.includes("\\snapshot\\");
|
|
79
|
+
if (isPkgEnvironment) {
|
|
80
|
+
console.log("Detected pkg packaged environment");
|
|
81
|
+
const execDir = import_node_path.default.dirname(process.execPath);
|
|
82
|
+
const pkgPaths = [
|
|
83
|
+
// 1. 与可执行文件同目录(最常见的打包方式)
|
|
84
|
+
import_node_path.default.join(execDir, "wsjtx_lib_nodejs.node"),
|
|
85
|
+
import_node_path.default.join(execDir, `wsjtx_lib_nodejs-${platform}-${arch}.node`),
|
|
86
|
+
// 2. prebuilds 子目录(保持结构的打包方式)
|
|
87
|
+
import_node_path.default.join(execDir, "prebuilds", `${platform}-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
88
|
+
// 3. 相对于可执行文件的其他可能位置
|
|
89
|
+
import_node_path.default.join(execDir, "..", "prebuilds", `${platform}-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
90
|
+
import_node_path.default.join(execDir, "lib", "wsjtx_lib_nodejs.node"),
|
|
91
|
+
import_node_path.default.join(execDir, "native", "wsjtx_lib_nodejs.node")
|
|
92
|
+
];
|
|
93
|
+
console.log("PKG environment - searching for native module:");
|
|
94
|
+
pkgPaths.forEach((p) => console.log(` - ${p}`));
|
|
95
|
+
for (const modulePath of pkgPaths) {
|
|
96
|
+
if (import_node_fs.default.existsSync(modulePath)) {
|
|
97
|
+
console.log(`Found native module at: ${modulePath}`);
|
|
98
|
+
return modulePath;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const pathList2 = pkgPaths.map((p) => ` - ${p}`).join("\n");
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Native module not found in pkg packaged environment for ${platform}-${arch}.
|
|
104
|
+
Searched in:
|
|
105
|
+
${pathList2}
|
|
106
|
+
|
|
107
|
+
Solutions:
|
|
108
|
+
1. Ensure the native module is included in your pkg configuration
|
|
109
|
+
2. Copy wsjtx_lib_nodejs.node to the same directory as your executable
|
|
110
|
+
3. Check if the native module was properly bundled during packaging`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const moduleRoot = import_node_path.default.resolve(__dirname, "..");
|
|
114
|
+
const possiblePaths = [
|
|
115
|
+
// 1. Prebuilt binaries (npm packages) - highest priority
|
|
116
|
+
import_node_path.default.join(moduleRoot, "prebuilds", `${platform}-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
117
|
+
// 2. GitHub Actions legacy format (for backward compatibility)
|
|
118
|
+
import_node_path.default.join(moduleRoot, "prebuilds", `${platform}-latest-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
119
|
+
import_node_path.default.join(moduleRoot, "prebuilds", `ubuntu-latest-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
120
|
+
// Linux
|
|
121
|
+
import_node_path.default.join(moduleRoot, "prebuilds", `macos-latest-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
122
|
+
// macOS
|
|
123
|
+
import_node_path.default.join(moduleRoot, "prebuilds", `windows-latest-${arch}`, "wsjtx_lib_nodejs.node"),
|
|
124
|
+
// Windows
|
|
125
|
+
// 3. Local development builds - third priority
|
|
126
|
+
import_node_path.default.join(moduleRoot, "build", "wsjtx_lib_nodejs.node"),
|
|
127
|
+
import_node_path.default.join(moduleRoot, "build", "Release", "wsjtx_lib_nodejs.node")
|
|
128
|
+
];
|
|
129
|
+
console.log("Normal environment - searching for native module:");
|
|
130
|
+
possiblePaths.forEach((p) => console.log(` - ${p}`));
|
|
131
|
+
for (const modulePath of possiblePaths) {
|
|
132
|
+
if (import_node_fs.default.existsSync(modulePath)) {
|
|
133
|
+
console.log(`Found native module at: ${modulePath}`);
|
|
134
|
+
return modulePath;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const pathList = possiblePaths.map((p) => ` - ${p}`).join("\n");
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Native module not found for ${platform}-${arch}.
|
|
140
|
+
Searched in:
|
|
141
|
+
${pathList}
|
|
142
|
+
|
|
143
|
+
Solutions:
|
|
144
|
+
1. If you installed via npm, this may be a missing prebuilt binary
|
|
145
|
+
2. For development, run "npm run build" to compile the native module
|
|
146
|
+
3. Check if your platform/architecture is supported`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
var nativeModulePath = findNativeModule();
|
|
150
|
+
var { WSJTXLib: NativeWSJTXLib } = require2(nativeModulePath);
|
|
151
|
+
var WSJTXLib = class _WSJTXLib {
|
|
152
|
+
native;
|
|
153
|
+
config;
|
|
154
|
+
/**
|
|
155
|
+
* Create a new WSJTX library instance
|
|
156
|
+
*
|
|
157
|
+
* @param config Optional configuration options
|
|
158
|
+
* @throws {WSJTXError} If the native library fails to initialize
|
|
159
|
+
*/
|
|
160
|
+
constructor(config = {}) {
|
|
161
|
+
this.config = {
|
|
162
|
+
maxThreads: 4,
|
|
163
|
+
debug: false,
|
|
164
|
+
...config
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
this.native = new NativeWSJTXLib();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new WSJTXError(
|
|
170
|
+
`Failed to initialize WSJTX library: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
"INIT_ERROR"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decode digital radio signals from audio data
|
|
177
|
+
*
|
|
178
|
+
* This method processes audio samples and attempts to decode digital
|
|
179
|
+
* messages using the specified protocol mode. The operation is performed
|
|
180
|
+
* asynchronously to avoid blocking the Node.js event loop.
|
|
181
|
+
*
|
|
182
|
+
* @param mode The digital mode to use for decoding
|
|
183
|
+
* @param audioData Audio samples (Float32Array or Int16Array)
|
|
184
|
+
* @param frequency Center frequency in Hz
|
|
185
|
+
* @param threads Number of threads to use (1-16, default: 4)
|
|
186
|
+
* @returns Promise that resolves when decoding is complete
|
|
187
|
+
*
|
|
188
|
+
* @throws {WSJTXError} If parameters are invalid or decoding fails
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const audioData = new Float32Array(48000 * 13); // 13 seconds
|
|
193
|
+
* await lib.decode(WSJTXMode.FT8, audioData, 1500);
|
|
194
|
+
* const messages = lib.pullMessages();
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
async decode(mode, audioData, frequency, threads = this.config.maxThreads || 4) {
|
|
198
|
+
this.validateMode(mode);
|
|
199
|
+
this.validateFrequency(frequency);
|
|
200
|
+
this.validateThreads(threads);
|
|
201
|
+
this.validateAudioData(audioData);
|
|
202
|
+
if (!this.isDecodingSupported(mode)) {
|
|
203
|
+
throw new WSJTXError(`Decoding not supported for mode: ${WSJTXMode[mode]}`, "UNSUPPORTED_MODE");
|
|
204
|
+
}
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const callback = (error, result) => {
|
|
207
|
+
if (error) {
|
|
208
|
+
reject(new WSJTXError(error.message, "DECODE_ERROR"));
|
|
209
|
+
} else {
|
|
210
|
+
resolve({ success: result });
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
this.native.decode(mode, audioData, frequency, threads, callback);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
reject(new WSJTXError(
|
|
217
|
+
`Decode operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
218
|
+
"DECODE_ERROR"
|
|
219
|
+
));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Encode a message into audio waveform for transmission
|
|
225
|
+
*
|
|
226
|
+
* Generates the audio waveform that represents the specified message
|
|
227
|
+
* using the given digital mode. The resulting audio can be fed to
|
|
228
|
+
* a radio transmitter or audio interface.
|
|
229
|
+
*
|
|
230
|
+
* @param mode The digital mode to use for encoding
|
|
231
|
+
* @param message The message text to encode (mode-specific format)
|
|
232
|
+
* @param frequency Center frequency in Hz
|
|
233
|
+
* @param threads Number of threads to use (1-16, default: 4)
|
|
234
|
+
* @returns Promise that resolves with encoded audio data and actual message sent
|
|
235
|
+
*
|
|
236
|
+
* @throws {WSJTXError} If parameters are invalid or encoding fails
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const result = await lib.encode(WSJTXMode.FT8, 'CQ DX K1ABC FN20', 1500);
|
|
241
|
+
* console.log('Generated audio samples:', result.audioData.length);
|
|
242
|
+
* console.log('Actual message sent:', result.messageSent);
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
async encode(mode, message, frequency, threads = this.config.maxThreads || 4) {
|
|
246
|
+
this.validateMode(mode);
|
|
247
|
+
this.validateMessage(message);
|
|
248
|
+
this.validateFrequency(frequency);
|
|
249
|
+
this.validateThreads(threads);
|
|
250
|
+
if (!this.isEncodingSupported(mode)) {
|
|
251
|
+
throw new WSJTXError(`Encoding not supported for mode: ${WSJTXMode[mode]}`, "UNSUPPORTED_MODE");
|
|
252
|
+
}
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
const callback = (error, result) => {
|
|
255
|
+
if (error) {
|
|
256
|
+
reject(new WSJTXError(error.message, "ENCODE_ERROR"));
|
|
257
|
+
} else {
|
|
258
|
+
resolve(result);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
try {
|
|
262
|
+
this.native.encode(mode, message, frequency, threads, callback);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
reject(new WSJTXError(
|
|
265
|
+
`Encode operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
266
|
+
"ENCODE_ERROR"
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Decode WSPR signals from IQ data
|
|
273
|
+
*
|
|
274
|
+
* WSPR (Weak Signal Propagation Reporter) is a specialized protocol
|
|
275
|
+
* for studying radio propagation. This method takes IQ (complex)
|
|
276
|
+
* samples and attempts to decode WSPR transmissions.
|
|
277
|
+
*
|
|
278
|
+
* @param iqData Interleaved I,Q samples as Float32Array
|
|
279
|
+
* @param options Decoder options (frequency, callsign, etc.)
|
|
280
|
+
* @returns Promise that resolves with array of decoded WSPR results
|
|
281
|
+
*
|
|
282
|
+
* @throws {WSJTXError} If parameters are invalid or decoding fails
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* const iqData = new Float32Array(2 * 12000 * 111); // 2 minutes of IQ data
|
|
287
|
+
* const options = {
|
|
288
|
+
* dialFrequency: 14095600, // 20m WSPR frequency
|
|
289
|
+
* callsign: 'K1ABC',
|
|
290
|
+
* locator: 'FN20'
|
|
291
|
+
* };
|
|
292
|
+
* const results = await lib.decodeWSPR(iqData, options);
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
async decodeWSPR(iqData, options = {}) {
|
|
296
|
+
this.validateIQData(iqData);
|
|
297
|
+
const defaultOptions = {
|
|
298
|
+
dialFrequency: 14095600,
|
|
299
|
+
// 20m WSPR frequency
|
|
300
|
+
callsign: "",
|
|
301
|
+
locator: "",
|
|
302
|
+
quickMode: false,
|
|
303
|
+
useHashTable: true,
|
|
304
|
+
passes: 2,
|
|
305
|
+
subtraction: true,
|
|
306
|
+
...options
|
|
307
|
+
};
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
const callback = (error, results) => {
|
|
310
|
+
if (error) {
|
|
311
|
+
reject(new WSJTXError(error.message, "WSPR_DECODE_ERROR"));
|
|
312
|
+
} else {
|
|
313
|
+
resolve(results);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
try {
|
|
317
|
+
this.native.decodeWSPR(iqData, defaultOptions, callback);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
reject(new WSJTXError(
|
|
320
|
+
`WSPR decode failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
321
|
+
"WSPR_DECODE_ERROR"
|
|
322
|
+
));
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Retrieve decoded messages from the internal queue
|
|
328
|
+
*
|
|
329
|
+
* Messages are added to an internal queue as they are decoded.
|
|
330
|
+
* This method retrieves and removes all pending messages from the queue.
|
|
331
|
+
*
|
|
332
|
+
* @returns Array of decoded messages
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* const messages = lib.pullMessages();
|
|
337
|
+
* messages.forEach(msg => {
|
|
338
|
+
* console.log(`${msg.text} (SNR: ${msg.snr} dB, ΔT: ${msg.deltaTime}s)`);
|
|
339
|
+
* });
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
pullMessages() {
|
|
343
|
+
try {
|
|
344
|
+
return this.native.pullMessages();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new WSJTXError(
|
|
347
|
+
`Failed to pull messages: ${error instanceof Error ? error.message : String(error)}`,
|
|
348
|
+
"PULL_ERROR"
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Check if encoding is supported for a specific mode
|
|
354
|
+
*
|
|
355
|
+
* @param mode The mode to check
|
|
356
|
+
* @returns True if encoding is supported
|
|
357
|
+
*/
|
|
358
|
+
isEncodingSupported(mode) {
|
|
359
|
+
return this.native.isEncodingSupported(mode);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Check if decoding is supported for a specific mode
|
|
363
|
+
*
|
|
364
|
+
* @param mode The mode to check
|
|
365
|
+
* @returns True if decoding is supported
|
|
366
|
+
*/
|
|
367
|
+
isDecodingSupported(mode) {
|
|
368
|
+
return this.native.isDecodingSupported(mode);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get the required sample rate for a specific mode
|
|
372
|
+
*
|
|
373
|
+
* @param mode The mode to query
|
|
374
|
+
* @returns Sample rate in Hz
|
|
375
|
+
*/
|
|
376
|
+
getSampleRate(mode) {
|
|
377
|
+
return this.native.getSampleRate(mode);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get the transmission duration for a specific mode
|
|
381
|
+
*
|
|
382
|
+
* @param mode The mode to query
|
|
383
|
+
* @returns Duration in seconds
|
|
384
|
+
*/
|
|
385
|
+
getTransmissionDuration(mode) {
|
|
386
|
+
return this.native.getTransmissionDuration(mode);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get capabilities for all supported modes
|
|
390
|
+
*
|
|
391
|
+
* @returns Array of mode capability information
|
|
392
|
+
*/
|
|
393
|
+
getAllModeCapabilities() {
|
|
394
|
+
const modes = Object.values(WSJTXMode).filter((v) => typeof v === "number");
|
|
395
|
+
return modes.map((mode) => ({
|
|
396
|
+
mode,
|
|
397
|
+
encodingSupported: this.isEncodingSupported(mode),
|
|
398
|
+
decodingSupported: this.isDecodingSupported(mode),
|
|
399
|
+
sampleRate: this.getSampleRate(mode),
|
|
400
|
+
duration: this.getTransmissionDuration(mode)
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Convert audio format between Float32Array and Int16Array
|
|
405
|
+
*
|
|
406
|
+
* @param audioData Input audio data
|
|
407
|
+
* @param targetFormat Target format ('float32' or 'int16')
|
|
408
|
+
* @returns Converted audio data
|
|
409
|
+
*/
|
|
410
|
+
static convertAudioFormat(audioData, targetFormat) {
|
|
411
|
+
if (targetFormat !== "float32" && targetFormat !== "int16") {
|
|
412
|
+
throw new Error(`Invalid target format: ${targetFormat}. Must be 'float32' or 'int16'`);
|
|
413
|
+
}
|
|
414
|
+
if (targetFormat === "float32") {
|
|
415
|
+
if (audioData instanceof Float32Array) {
|
|
416
|
+
return audioData;
|
|
417
|
+
}
|
|
418
|
+
const result = new Float32Array(audioData.length);
|
|
419
|
+
for (let i = 0; i < audioData.length; i++) {
|
|
420
|
+
result[i] = audioData[i] / 32768;
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
} else {
|
|
424
|
+
if (audioData instanceof Int16Array) {
|
|
425
|
+
return audioData;
|
|
426
|
+
}
|
|
427
|
+
const result = new Int16Array(audioData.length);
|
|
428
|
+
for (let i = 0; i < audioData.length; i++) {
|
|
429
|
+
result[i] = Math.max(-32768, Math.min(32767, Math.round(audioData[i] * 32768)));
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Static convenience helper for async audio format conversion.
|
|
436
|
+
*/
|
|
437
|
+
static async convertAudioFormatAsync(audioData, targetFormat) {
|
|
438
|
+
const lib = new _WSJTXLib();
|
|
439
|
+
return lib.convertAudioFormatAsync(audioData, targetFormat);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Asynchronously convert audio data format without blocking the event loop
|
|
443
|
+
*
|
|
444
|
+
* Uses native thread pool to offload conversion.
|
|
445
|
+
*/
|
|
446
|
+
async convertAudioFormatAsync(audioData, targetFormat) {
|
|
447
|
+
if (targetFormat !== "float32" && targetFormat !== "int16") {
|
|
448
|
+
throw new Error(`Invalid target format: ${targetFormat}. Must be 'float32' or 'int16'`);
|
|
449
|
+
}
|
|
450
|
+
this.validateAudioData(audioData);
|
|
451
|
+
return new Promise((resolve, reject) => {
|
|
452
|
+
const callback = (error, result) => {
|
|
453
|
+
if (error) {
|
|
454
|
+
reject(new WSJTXError(error.message ?? String(error), "CONVERT_ERROR"));
|
|
455
|
+
} else {
|
|
456
|
+
resolve(result);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
try {
|
|
460
|
+
this.native.convertAudioAsync(audioData, targetFormat, callback);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
reject(new WSJTXError(
|
|
463
|
+
`Convert operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
464
|
+
"CONVERT_ERROR"
|
|
465
|
+
));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// Validation methods
|
|
470
|
+
validateMode(mode) {
|
|
471
|
+
if (!Object.values(WSJTXMode).includes(mode)) {
|
|
472
|
+
throw new WSJTXError(`Invalid mode: ${mode}`, "INVALID_MODE");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
validateFrequency(frequency) {
|
|
476
|
+
if (!Number.isInteger(frequency) || frequency < 0 || frequency > 3e7) {
|
|
477
|
+
throw new WSJTXError(
|
|
478
|
+
`Invalid frequency: ${frequency}. Must be between 0 and 30,000,000 Hz`,
|
|
479
|
+
"INVALID_FREQUENCY"
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
validateThreads(threads) {
|
|
484
|
+
if (!Number.isInteger(threads) || threads < 1 || threads > 16) {
|
|
485
|
+
throw new WSJTXError(
|
|
486
|
+
`Invalid thread count: ${threads}. Must be between 1 and 16`,
|
|
487
|
+
"INVALID_THREADS"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
validateMessage(message) {
|
|
492
|
+
if (typeof message !== "string" || message.length === 0 || message.length > 22) {
|
|
493
|
+
throw new WSJTXError(
|
|
494
|
+
`Invalid message: "${message}". Must be 1-22 characters long`,
|
|
495
|
+
"INVALID_MESSAGE"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
validateAudioData(audioData) {
|
|
500
|
+
if (!(audioData instanceof Float32Array) && !(audioData instanceof Int16Array)) {
|
|
501
|
+
throw new WSJTXError(
|
|
502
|
+
"Invalid audio data: must be Float32Array or Int16Array",
|
|
503
|
+
"INVALID_AUDIO_DATA"
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (audioData.length === 0) {
|
|
507
|
+
throw new WSJTXError("Audio data cannot be empty", "INVALID_AUDIO_DATA");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
validateIQData(iqData) {
|
|
511
|
+
if (!(iqData instanceof Float32Array)) {
|
|
512
|
+
throw new WSJTXError(
|
|
513
|
+
"Invalid IQ data: must be Float32Array with interleaved I,Q samples",
|
|
514
|
+
"INVALID_IQ_DATA"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (iqData.length === 0 || iqData.length % 2 !== 0) {
|
|
518
|
+
throw new WSJTXError(
|
|
519
|
+
"IQ data length must be even (interleaved I,Q samples)",
|
|
520
|
+
"INVALID_IQ_DATA"
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// test/wsjtx.basic.test.ts
|
|
527
|
+
(0, import_node_test.describe)("WSJTX Library Basic Tests", () => {
|
|
528
|
+
let lib;
|
|
529
|
+
(0, import_node_test.beforeEach)(() => {
|
|
530
|
+
lib = new WSJTXLib({
|
|
531
|
+
maxThreads: 4,
|
|
532
|
+
debug: true
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
(0, import_node_test.afterEach)(() => {
|
|
536
|
+
});
|
|
537
|
+
(0, import_node_test.describe)("Basic Functionality Tests", () => {
|
|
538
|
+
(0, import_node_test.it)("should create library instance", () => {
|
|
539
|
+
import_node_assert.default.ok(lib instanceof WSJTXLib);
|
|
540
|
+
});
|
|
541
|
+
(0, import_node_test.it)("should support custom configuration", () => {
|
|
542
|
+
const customLib = new WSJTXLib({
|
|
543
|
+
maxThreads: 8,
|
|
544
|
+
debug: false
|
|
545
|
+
});
|
|
546
|
+
import_node_assert.default.ok(customLib instanceof WSJTXLib);
|
|
547
|
+
});
|
|
548
|
+
(0, import_node_test.it)("should return correct FT8 sample rate", () => {
|
|
549
|
+
const sampleRate = lib.getSampleRate(0 /* FT8 */);
|
|
550
|
+
import_node_assert.default.strictEqual(sampleRate, 48e3);
|
|
551
|
+
});
|
|
552
|
+
(0, import_node_test.it)("should return correct FT8 transmission duration", () => {
|
|
553
|
+
const duration = lib.getTransmissionDuration(0 /* FT8 */);
|
|
554
|
+
import_node_assert.default.ok(Math.abs(duration - 12.64) < 0.1);
|
|
555
|
+
});
|
|
556
|
+
(0, import_node_test.it)("should correctly check encoding support", () => {
|
|
557
|
+
import_node_assert.default.strictEqual(lib.isEncodingSupported(0 /* FT8 */), true);
|
|
558
|
+
import_node_assert.default.strictEqual(lib.isDecodingSupported(0 /* FT8 */), true);
|
|
559
|
+
});
|
|
560
|
+
(0, import_node_test.it)("should return all mode capabilities", () => {
|
|
561
|
+
const capabilities = lib.getAllModeCapabilities();
|
|
562
|
+
import_node_assert.default.ok(capabilities.length > 0);
|
|
563
|
+
import_node_assert.default.ok("mode" in capabilities[0]);
|
|
564
|
+
import_node_assert.default.ok("encodingSupported" in capabilities[0]);
|
|
565
|
+
import_node_assert.default.ok("decodingSupported" in capabilities[0]);
|
|
566
|
+
import_node_assert.default.ok("sampleRate" in capabilities[0]);
|
|
567
|
+
import_node_assert.default.ok("duration" in capabilities[0]);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
(0, import_node_test.describe)("Parameter Validation Tests", () => {
|
|
571
|
+
(0, import_node_test.it)("should validate mode parameter", async () => {
|
|
572
|
+
const audioData = new Float32Array(1e3);
|
|
573
|
+
await import_node_assert.default.rejects(
|
|
574
|
+
lib.decode(999, audioData, 1e3),
|
|
575
|
+
WSJTXError
|
|
576
|
+
);
|
|
577
|
+
});
|
|
578
|
+
(0, import_node_test.it)("should validate frequency parameter", async () => {
|
|
579
|
+
const audioData = new Float32Array(1e3);
|
|
580
|
+
await import_node_assert.default.rejects(
|
|
581
|
+
lib.decode(0 /* FT8 */, audioData, -1e3),
|
|
582
|
+
WSJTXError
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
(0, import_node_test.it)("should validate audio data parameter", async () => {
|
|
586
|
+
await import_node_assert.default.rejects(
|
|
587
|
+
lib.decode(0 /* FT8 */, new Float32Array(0), 1e3),
|
|
588
|
+
WSJTXError
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
(0, import_node_test.it)("should validate message parameter", async () => {
|
|
592
|
+
await import_node_assert.default.rejects(
|
|
593
|
+
lib.encode(0 /* FT8 */, "", 1e3),
|
|
594
|
+
WSJTXError
|
|
595
|
+
);
|
|
596
|
+
await import_node_assert.default.rejects(
|
|
597
|
+
lib.encode(0 /* FT8 */, "x".repeat(30), 1e3),
|
|
598
|
+
WSJTXError
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
(0, import_node_test.describe)("Message Queue Tests", () => {
|
|
603
|
+
(0, import_node_test.it)("should pull messages from queue", () => {
|
|
604
|
+
const messages = lib.pullMessages();
|
|
605
|
+
import_node_assert.default.ok(Array.isArray(messages));
|
|
606
|
+
});
|
|
607
|
+
(0, import_node_test.it)("should clear message queue", () => {
|
|
608
|
+
lib.pullMessages();
|
|
609
|
+
const messages = lib.pullMessages();
|
|
610
|
+
import_node_assert.default.strictEqual(messages.length, 0);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
(0, import_node_test.describe)("Audio Format Conversion Tests", () => {
|
|
614
|
+
(0, import_node_test.it)("should convert Float32Array to Int16Array", () => {
|
|
615
|
+
const floatData = new Float32Array([0, 0.5, -0.5, 1, -1]);
|
|
616
|
+
const intData = WSJTXLib.convertAudioFormat(floatData, "int16");
|
|
617
|
+
import_node_assert.default.ok(intData instanceof Int16Array);
|
|
618
|
+
import_node_assert.default.strictEqual(intData.length, floatData.length);
|
|
619
|
+
import_node_assert.default.strictEqual(intData[0], 0);
|
|
620
|
+
import_node_assert.default.ok(Math.abs(intData[1] - 16384) < 10);
|
|
621
|
+
import_node_assert.default.ok(Math.abs(intData[2] + 16384) < 10);
|
|
622
|
+
import_node_assert.default.ok(Math.abs(intData[3] - 32767) < 10);
|
|
623
|
+
import_node_assert.default.ok(Math.abs(intData[4] + 32767) < 10);
|
|
624
|
+
});
|
|
625
|
+
(0, import_node_test.it)("should convert Int16Array to Float32Array", () => {
|
|
626
|
+
const intData = new Int16Array([0, 16384, -16384, 32767, -32767]);
|
|
627
|
+
const floatData = WSJTXLib.convertAudioFormat(intData, "float32");
|
|
628
|
+
import_node_assert.default.ok(floatData instanceof Float32Array);
|
|
629
|
+
import_node_assert.default.strictEqual(floatData.length, intData.length);
|
|
630
|
+
import_node_assert.default.ok(Math.abs(floatData[0] - 0) < 1e-3);
|
|
631
|
+
import_node_assert.default.ok(Math.abs(floatData[1] - 0.5) < 1e-3);
|
|
632
|
+
import_node_assert.default.ok(Math.abs(floatData[2] + 0.5) < 1e-3);
|
|
633
|
+
import_node_assert.default.ok(Math.abs(floatData[3] - 1) < 1e-3);
|
|
634
|
+
import_node_assert.default.ok(Math.abs(floatData[4] + 1) < 1e-3);
|
|
635
|
+
});
|
|
636
|
+
(0, import_node_test.it)("should handle edge cases in conversion", () => {
|
|
637
|
+
const emptyFloat = new Float32Array(0);
|
|
638
|
+
const emptyInt = WSJTXLib.convertAudioFormat(emptyFloat, "int16");
|
|
639
|
+
import_node_assert.default.strictEqual(emptyInt.length, 0);
|
|
640
|
+
const emptyInt16 = new Int16Array(0);
|
|
641
|
+
const emptyFloat32 = WSJTXLib.convertAudioFormat(emptyInt16, "float32");
|
|
642
|
+
import_node_assert.default.strictEqual(emptyFloat32.length, 0);
|
|
643
|
+
});
|
|
644
|
+
(0, import_node_test.it)("should maintain precision in round-trip conversion", () => {
|
|
645
|
+
const originalData = new Float32Array(1e3);
|
|
646
|
+
for (let i = 0; i < originalData.length; i++) {
|
|
647
|
+
originalData[i] = (Math.random() - 0.5) * 2;
|
|
648
|
+
}
|
|
649
|
+
const intData = WSJTXLib.convertAudioFormat(originalData, "int16");
|
|
650
|
+
const convertedData = WSJTXLib.convertAudioFormat(intData, "float32");
|
|
651
|
+
let maxError = 0;
|
|
652
|
+
for (let i = 0; i < originalData.length; i++) {
|
|
653
|
+
const error = Math.abs(originalData[i] - convertedData[i]);
|
|
654
|
+
maxError = Math.max(maxError, error);
|
|
655
|
+
}
|
|
656
|
+
import_node_assert.default.ok(maxError < 1e-3);
|
|
657
|
+
});
|
|
658
|
+
(0, import_node_test.it)("should handle invalid format parameter", () => {
|
|
659
|
+
const floatData = new Float32Array([0.5]);
|
|
660
|
+
import_node_assert.default.throws(() => {
|
|
661
|
+
WSJTXLib.convertAudioFormat(floatData, "invalid");
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
(0, import_node_test.describe)("TypeScript Type Safety Tests", () => {
|
|
666
|
+
(0, import_node_test.it)("should provide complete type support for basic types", () => {
|
|
667
|
+
const capabilities = lib.getAllModeCapabilities();
|
|
668
|
+
import_node_assert.default.ok(capabilities.length > 0);
|
|
669
|
+
capabilities.forEach((cap) => {
|
|
670
|
+
const modeName = WSJTXMode[cap.mode];
|
|
671
|
+
import_node_assert.default.strictEqual(typeof modeName, "string");
|
|
672
|
+
import_node_assert.default.strictEqual(typeof cap.sampleRate, "number");
|
|
673
|
+
import_node_assert.default.strictEqual(typeof cap.duration, "number");
|
|
674
|
+
import_node_assert.default.strictEqual(typeof cap.encodingSupported, "boolean");
|
|
675
|
+
import_node_assert.default.strictEqual(typeof cap.decodingSupported, "boolean");
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
(0, import_node_test.it)("should provide type-safe message objects", () => {
|
|
679
|
+
const messages = lib.pullMessages();
|
|
680
|
+
messages.forEach((msg) => {
|
|
681
|
+
import_node_assert.default.strictEqual(typeof msg.text, "string");
|
|
682
|
+
import_node_assert.default.strictEqual(typeof msg.snr, "number");
|
|
683
|
+
import_node_assert.default.strictEqual(typeof msg.deltaTime, "number");
|
|
684
|
+
import_node_assert.default.strictEqual(typeof msg.deltaFrequency, "number");
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
(0, import_node_test.it)("should enforce enum constraints", () => {
|
|
688
|
+
const validMode = 0 /* FT8 */;
|
|
689
|
+
import_node_assert.default.strictEqual(typeof validMode, "number");
|
|
690
|
+
import_node_assert.default.ok(validMode >= 0);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
(0, import_node_test.describe)("Error Handling Tests", () => {
|
|
694
|
+
(0, import_node_test.it)("should throw WSJTXError for invalid operations (non-encode/decode)", async () => {
|
|
695
|
+
try {
|
|
696
|
+
await lib.decode(999, new Float32Array(1e3), 1e3);
|
|
697
|
+
import_node_assert.default.fail("Should have thrown WSJTXError for invalid mode");
|
|
698
|
+
} catch (error) {
|
|
699
|
+
import_node_assert.default.ok(error instanceof WSJTXError);
|
|
700
|
+
import_node_assert.default.strictEqual(typeof error.message, "string");
|
|
701
|
+
if (error instanceof WSJTXError) {
|
|
702
|
+
import_node_assert.default.strictEqual(typeof error.code, "string");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
(0, import_node_test.it)("should provide meaningful error messages for basic errors", async () => {
|
|
707
|
+
try {
|
|
708
|
+
await lib.encode(0 /* FT8 */, "", 1e3);
|
|
709
|
+
import_node_assert.default.fail("Should have thrown WSJTXError for empty message");
|
|
710
|
+
} catch (error) {
|
|
711
|
+
import_node_assert.default.ok(error instanceof WSJTXError);
|
|
712
|
+
import_node_assert.default.ok(error.message.length > 0);
|
|
713
|
+
if (error instanceof WSJTXError && error.code) {
|
|
714
|
+
import_node_assert.default.ok(error.code.length > 0);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
(0, import_node_test.it)("should validate all error codes are strings for basic errors", async () => {
|
|
719
|
+
const testCases = [
|
|
720
|
+
() => lib.decode(999, new Float32Array(1e3), 1e3),
|
|
721
|
+
// Invalid mode
|
|
722
|
+
() => lib.decode(0 /* FT8 */, new Float32Array(1e3), -1e3),
|
|
723
|
+
// Invalid frequency
|
|
724
|
+
() => lib.decode(0 /* FT8 */, new Float32Array(0), 1e3),
|
|
725
|
+
// Invalid audio data
|
|
726
|
+
() => lib.encode(0 /* FT8 */, "", 1e3),
|
|
727
|
+
// Empty message
|
|
728
|
+
() => lib.encode(0 /* FT8 */, "x".repeat(50), 1e3)
|
|
729
|
+
// Message too long
|
|
730
|
+
];
|
|
731
|
+
for (const testCase of testCases) {
|
|
732
|
+
try {
|
|
733
|
+
await testCase();
|
|
734
|
+
import_node_assert.default.fail("Should have thrown WSJTXError");
|
|
735
|
+
} catch (error) {
|
|
736
|
+
import_node_assert.default.ok(error instanceof WSJTXError);
|
|
737
|
+
if (error instanceof WSJTXError && error.code) {
|
|
738
|
+
import_node_assert.default.strictEqual(typeof error.code, "string");
|
|
739
|
+
import_node_assert.default.ok(error.code.length > 0);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
/**
|
|
747
|
+
* WSJTX Digital Radio Protocol Library for Node.js
|
|
748
|
+
*
|
|
749
|
+
* This library provides encoding and decoding capabilities for various
|
|
750
|
+
* digital amateur radio protocols including FT8, FT4, WSPR, and others.
|
|
751
|
+
*
|
|
752
|
+
* The library is a Node.js C++ extension that wraps the wsjtx_lib C library,
|
|
753
|
+
* providing high-performance digital signal processing capabilities with
|
|
754
|
+
* multi-platform support (Windows, macOS, Linux).
|
|
755
|
+
*
|
|
756
|
+
* @version 1.0.0
|
|
757
|
+
* @author WSJTX Development Team
|
|
758
|
+
* @license GPL-3.0
|
|
759
|
+
*/
|