icom-wlan-node 0.2.7 → 0.4.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/README.md +104 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/rig/IcomControl.d.ts +41 -1
- package/dist/rig/IcomControl.js +135 -0
- package/dist/rig/IcomRigCommands.d.ts +11 -0
- package/dist/rig/IcomRigCommands.js +20 -0
- package/dist/scope/IcomScopeCommands.d.ts +4 -0
- package/dist/scope/IcomScopeCommands.js +15 -0
- package/dist/scope/IcomScopeParser.d.ts +5 -0
- package/dist/scope/IcomScopeParser.js +72 -0
- package/dist/scope/IcomScopeService.d.ts +17 -0
- package/dist/scope/IcomScopeService.js +112 -0
- package/dist/types.d.ts +38 -0
- package/package.json +18 -4
- package/dist/demo.d.ts +0 -12
- package/dist/demo.js +0 -329
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Icom WLAN (UDP) protocol implementation in Node.js + TypeScript, featuring:
|
|
|
4
4
|
|
|
5
5
|
- Control channel handshake (AreYouThere/AreYouReady), login (0x80/0x60), token confirm/renew (0x40)
|
|
6
6
|
- CI‑V over UDP encapsulation (open/close keep‑alive + CIV frame transport)
|
|
7
|
+
- Scope/spectrum data capture over CI‑V `0x27`, with automatic segment assembly into friendly frame events
|
|
7
8
|
- Audio stream send/receive (LPCM 16‑bit mono @ 12 kHz; 20 ms frames)
|
|
8
9
|
- Typed, event‑based API; designed for use as a dependency in other Node projects
|
|
9
10
|
|
|
@@ -63,6 +64,15 @@ rig.events.on('audio', (frame) => {
|
|
|
63
64
|
// frame.pcm16 is raw 16‑bit PCM mono @ 12 kHz
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
rig.events.on('scopeFrame', (frame) => {
|
|
68
|
+
console.log(
|
|
69
|
+
'Scope:',
|
|
70
|
+
`${frame.startFreqHz}..${frame.endFreqHz} Hz`,
|
|
71
|
+
`pixels=${frame.pixels.length}`,
|
|
72
|
+
`mode=${frame.mode}`
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
66
76
|
rig.events.on('error', (err) => console.error('UDP error', err));
|
|
67
77
|
|
|
68
78
|
(async () => {
|
|
@@ -93,6 +103,37 @@ rig.sendAudioFloat32(tone, true);
|
|
|
93
103
|
await rig.setPtt(false);
|
|
94
104
|
```
|
|
95
105
|
|
|
106
|
+
### Scope / Spectrum
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await rig.connect();
|
|
110
|
+
|
|
111
|
+
rig.events.on('scopeSegment', (segment) => {
|
|
112
|
+
console.log(`scope segment ${segment.sequence}/${segment.sequenceMax}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
rig.events.on('scopeFrame', (frame) => {
|
|
116
|
+
console.log('scope frame ready', {
|
|
117
|
+
startFreqHz: frame.startFreqHz,
|
|
118
|
+
endFreqHz: frame.endFreqHz,
|
|
119
|
+
pixelCount: frame.pixels.length,
|
|
120
|
+
outOfRange: frame.outOfRange
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Enable basic scope output
|
|
125
|
+
await rig.enableScope();
|
|
126
|
+
|
|
127
|
+
// Wait for one complete frame
|
|
128
|
+
const frame = await rig.waitForScopeFrame({ timeout: 3000 });
|
|
129
|
+
if (frame) {
|
|
130
|
+
console.log(frame.pixels[0], frame.pixels[1]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Disable scope output when finished
|
|
134
|
+
await rig.disableScope();
|
|
135
|
+
```
|
|
136
|
+
|
|
96
137
|
## API Overview
|
|
97
138
|
|
|
98
139
|
- `new IcomControl(options)`
|
|
@@ -104,6 +145,8 @@ await rig.setPtt(false);
|
|
|
104
145
|
- `capabilities(CapabilitiesInfo)` — civ address, audio name (0xA8)
|
|
105
146
|
- `civ(Buffer)` — raw CI‑V payload bytes as transported over UDP
|
|
106
147
|
- `civFrame(Buffer)` — one complete CI‑V frame (FE FE ... FD)
|
|
148
|
+
- `scopeSegment(IcomScopeSegmentInfo)` — one parsed `0x27` scope segment
|
|
149
|
+
- `scopeFrame(IcomScopeFrame)` — one assembled spectrum/waterfall frame
|
|
107
150
|
- `audio({ pcm16: Buffer })` — audio frames
|
|
108
151
|
- `error(Error)` — UDP errors
|
|
109
152
|
- `connectionLost(ConnectionLostInfo)` — session timeout detected
|
|
@@ -114,6 +157,7 @@ await rig.setPtt(false);
|
|
|
114
157
|
- **Connection**: `connect()` / `disconnect(options?)` — connects control + CIV + audio sub‑sessions; resolves when all ready
|
|
115
158
|
- `disconnect()` accepts optional `DisconnectOptions` or `DisconnectReason` for better error handling
|
|
116
159
|
- **Raw CI‑V**: `sendCiv(buf: Buffer)` — send a raw CI‑V frame
|
|
160
|
+
- **Scope / Spectrum**: `scope`, `enableScope()`, `disableScope()`, `waitForScopeFrame()`
|
|
117
161
|
- **Audio TX**: `setPtt(on: boolean)`, `sendAudioFloat32()`, `sendAudioPcm16()`
|
|
118
162
|
- **Rig Control**: `setFrequency()`, `setMode()`, `setConnectorDataMode()`, `setConnectorWLanLevel()`
|
|
119
163
|
- **Rig Query**: `readOperatingFrequency()`, `readOperatingMode()`, `readTransmitFrequency()`, `readTransceiverState()`, `readBandEdges()`
|
|
@@ -257,6 +301,45 @@ The library exposes common CI‑V operations as friendly methods. Addresses are
|
|
|
257
301
|
- `readTransceiverState(options?: QueryOptions) => Promise<'TX' | 'RX' | 'UNKNOWN' | null>`
|
|
258
302
|
- `readBandEdges(options?: QueryOptions) => Promise<Buffer|null>`
|
|
259
303
|
|
|
304
|
+
#### Scope / Spectrum
|
|
305
|
+
|
|
306
|
+
- `scope: IcomScopeService` — Standalone scope service object that can be reused with other CI‑V transport paths in the future
|
|
307
|
+
- `enableScope() => Promise<void>` — Send the minimal command sequence to enable basic scope output
|
|
308
|
+
- `disableScope() => Promise<void>` — Send the minimal command sequence to disable scope output
|
|
309
|
+
- `waitForScopeFrame(options?: QueryOptions) => Promise<IcomScopeFrame | null>` — Wait for the next complete scope frame
|
|
310
|
+
|
|
311
|
+
`IcomScopeFrame` shape:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
interface IcomScopeFrame {
|
|
315
|
+
valid: boolean;
|
|
316
|
+
receiver: 0 | 1;
|
|
317
|
+
sequence: number;
|
|
318
|
+
sequenceMax: number;
|
|
319
|
+
mode: 0 | 1 | 2 | 3;
|
|
320
|
+
outOfRange: boolean;
|
|
321
|
+
startFreqHz: number;
|
|
322
|
+
endFreqHz: number;
|
|
323
|
+
pixels: Uint8Array;
|
|
324
|
+
rawCivPayloads: Buffer[];
|
|
325
|
+
transport: 'lan-civ' | 'serial';
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Current implementation notes:
|
|
330
|
+
|
|
331
|
+
- Currently implements only the basic on/off controls and `0x27 00 00` scope data capture
|
|
332
|
+
- The parsing layer is decoupled from the UDP session layer and only depends on complete CI‑V frames
|
|
333
|
+
- Frequency fields are currently parsed with `freqLen=5` by default
|
|
334
|
+
- LAN aggregate waterfall payload splitting is not implemented yet; standard segment input is supported
|
|
335
|
+
- The `scope` logic is designed to be reusable for future serial CI‑V or Hamlib CI‑V integration
|
|
336
|
+
|
|
337
|
+
#### Antenna Tuner (ATU)
|
|
338
|
+
|
|
339
|
+
- `readTunerStatus(options?: QueryOptions) => Promise<{ raw: number; state: 'OFF'|'ON'|'TUNING' } | null>` — Read tuner status (CI‑V 0x1A/0x00)
|
|
340
|
+
- `setTunerEnabled(enabled: boolean) => Promise<void>` — Enable/disable internal tuner (CI‑V 0x1A/0x01)
|
|
341
|
+
- `startManualTune() => Promise<void>` — Trigger one manual tune cycle (CI‑V 0x1A/0x02/0x00)
|
|
342
|
+
|
|
260
343
|
#### Meters & Levels
|
|
261
344
|
|
|
262
345
|
**Reception Meters** (available anytime):
|
|
@@ -383,6 +466,23 @@ await rig.setConnectorDataMode('WLAN');
|
|
|
383
466
|
// Or numeric: await rig.setConnectorDataMode(0x03);
|
|
384
467
|
|
|
385
468
|
await rig.setConnectorWLanLevel(120); // Set WLAN audio level
|
|
469
|
+
|
|
470
|
+
// Scope capture
|
|
471
|
+
await rig.enableScope();
|
|
472
|
+
const scope = await rig.waitForScopeFrame({ timeout: 3000 });
|
|
473
|
+
if (scope) {
|
|
474
|
+
console.log(`Scope ${scope.startFreqHz}..${scope.endFreqHz}, ${scope.pixels.length} pixels`);
|
|
475
|
+
}
|
|
476
|
+
await rig.disableScope();
|
|
477
|
+
|
|
478
|
+
// Antenna tuner
|
|
479
|
+
const atu = await rig.readTunerStatus({ timeout: 2000 });
|
|
480
|
+
if (atu) {
|
|
481
|
+
console.log('ATU:', atu.state);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
await rig.setTunerEnabled(true);
|
|
485
|
+
await rig.startManualTune();
|
|
386
486
|
```
|
|
387
487
|
|
|
388
488
|
## Design Notes
|
|
@@ -393,6 +493,7 @@ await rig.setConnectorWLanLevel(120); // Set WLAN audio level
|
|
|
393
493
|
- Credentials use the same simple substitution cipher as FT8CN’s Android client (`passCode`).
|
|
394
494
|
- The 0x90/0x50 handshake strictly follows FT8CN’s timing and endianness. We pre‑open local CIV/Audio sockets, reply with local ports on first 0x90, then set remote ports upon 0x50.
|
|
395
495
|
- CIV/audio sub‑sessions each run their own Ping/Idle and (for CIV) OpenClose keep‑alive.
|
|
496
|
+
- Scope data is treated as CI‑V business payload, not as a separate UDP stream. `IcomControl` only bridges CI‑V frames into the reusable `IcomScopeService`.
|
|
396
497
|
|
|
397
498
|
### Endianness and parsing tips
|
|
398
499
|
|
|
@@ -418,33 +519,10 @@ ICOM_IP=192.168.31.253 ICOM_PORT=50001 ICOM_USER=icom ICOM_PASS=icomicom npm tes
|
|
|
418
519
|
- Full token renewal loop and advanced status flag parsing simplified.
|
|
419
520
|
- Audio receive/playback is library‑only; playback is up to the integrator.
|
|
420
521
|
- Robust retransmit/multi‑retransmit handling can be extended.
|
|
522
|
+
- Scope support is currently limited to basic on/off commands plus standard `0x27 00 00` segment parsing.
|
|
523
|
+
- LAN aggregate waterfall payload splitting is not implemented yet.
|
|
524
|
+
- Scope control subcommands beyond basic enable/disable are not implemented yet.
|
|
421
525
|
|
|
422
526
|
## License
|
|
423
527
|
|
|
424
528
|
MIT
|
|
425
|
-
#### Antenna Tuner (ATU)
|
|
426
|
-
|
|
427
|
-
- `readTunerStatus(options?: QueryOptions) => Promise<{ raw: number; state: 'OFF'|'ON'|'TUNING' } | null>` — 读取天调状态(CI‑V 0x1A/0x00)
|
|
428
|
-
- `setTunerEnabled(enabled: boolean) => Promise<void>` — 开启/关闭内置天调(CI‑V 0x1A/0x01 00/01)
|
|
429
|
-
- `startManualTune() => Promise<void>` — 触发一次手动调谐(相当于 [TUNE] 键,CI‑V 0x1A/0x02/0x00)
|
|
430
|
-
|
|
431
|
-
示例:
|
|
432
|
-
|
|
433
|
-
```ts
|
|
434
|
-
// 读取天调状态
|
|
435
|
-
const atu = await rig.readTunerStatus({ timeout: 2000 });
|
|
436
|
-
if (atu) console.log('ATU:', atu.state); // OFF / ON / TUNING
|
|
437
|
-
|
|
438
|
-
// 启用内置天调
|
|
439
|
-
await rig.setTunerEnabled(true);
|
|
440
|
-
|
|
441
|
-
// 触发一次手动调谐
|
|
442
|
-
await rig.startManualTune();
|
|
443
|
-
|
|
444
|
-
// 可选:轮询状态直到结束
|
|
445
|
-
let status;
|
|
446
|
-
do {
|
|
447
|
-
await new Promise(r => setTimeout(r, 300));
|
|
448
|
-
status = await rig.readTunerStatus({ timeout: 1000 });
|
|
449
|
-
} while (status && status.state === 'TUNING');
|
|
450
|
-
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { IcomControl } from './rig/IcomControl';
|
|
3
|
+
export { IcomScopeService } from './scope/IcomScopeService';
|
|
4
|
+
export { IcomScopeCommands } from './scope/IcomScopeCommands';
|
|
3
5
|
export { MODE_MAP, CONNECTOR_MODE_MAP, DEFAULT_CONTROLLER_ADDR, METER_THRESHOLDS, METER_CALIBRATION, getModeCode, getConnectorModeCode, getModeString, getConnectorModeString, getFilterString, rawToPowerPercent, rawToVoltage, rawToCurrent } from './rig/IcomConstants';
|
|
4
6
|
export { parseTwoByteBcd, intToTwoByteBcd } from './utils/bcd';
|
|
5
7
|
export { IcomRigCommands } from './rig/IcomRigCommands';
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,16 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getDisconnectMessage = exports.ConnectionAbortedError = exports.setupBasicErrorProtection = exports.setupGlobalErrorHandlers = exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomControl = void 0;
|
|
17
|
+
exports.getDisconnectMessage = exports.ConnectionAbortedError = exports.setupBasicErrorProtection = exports.setupGlobalErrorHandlers = exports.AUDIO_RATE = exports.IcomRigCommands = exports.intToTwoByteBcd = exports.parseTwoByteBcd = exports.rawToCurrent = exports.rawToVoltage = exports.rawToPowerPercent = exports.getFilterString = exports.getConnectorModeString = exports.getModeString = exports.getConnectorModeCode = exports.getModeCode = exports.METER_CALIBRATION = exports.METER_THRESHOLDS = exports.DEFAULT_CONTROLLER_ADDR = exports.CONNECTOR_MODE_MAP = exports.MODE_MAP = exports.IcomScopeCommands = exports.IcomScopeService = exports.IcomControl = void 0;
|
|
18
18
|
// Export types (includes ConnectionPhase, ConnectionMetrics, etc.)
|
|
19
19
|
__exportStar(require("./types"), exports);
|
|
20
20
|
// Export main class
|
|
21
21
|
var IcomControl_1 = require("./rig/IcomControl");
|
|
22
22
|
Object.defineProperty(exports, "IcomControl", { enumerable: true, get: function () { return IcomControl_1.IcomControl; } });
|
|
23
|
+
var IcomScopeService_1 = require("./scope/IcomScopeService");
|
|
24
|
+
Object.defineProperty(exports, "IcomScopeService", { enumerable: true, get: function () { return IcomScopeService_1.IcomScopeService; } });
|
|
25
|
+
var IcomScopeCommands_1 = require("./scope/IcomScopeCommands");
|
|
26
|
+
Object.defineProperty(exports, "IcomScopeCommands", { enumerable: true, get: function () { return IcomScopeCommands_1.IcomScopeCommands; } });
|
|
23
27
|
// Export constants and enums
|
|
24
28
|
var IcomConstants_1 = require("./rig/IcomConstants");
|
|
25
29
|
Object.defineProperty(exports, "MODE_MAP", { enumerable: true, get: function () { return IcomConstants_1.MODE_MAP; } });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics, DisconnectReason, DisconnectOptions, TunerStatusReading } from '../types';
|
|
1
|
+
import { IcomRigOptions, RigEventEmitter, IcomMode, ConnectorDataMode, SetModeOptions, QueryOptions, SwrReading, AlcReading, WlanLevelReading, LevelMeterReading, SquelchStatusReading, AudioSquelchReading, OvfStatusReading, PowerLevelReading, CompLevelReading, VoltageReading, CurrentReading, ConnectionState, ConnectionMonitorConfig, ConnectionPhase, ConnectionMetrics, DisconnectReason, DisconnectOptions, TunerStatusReading, LevelReading } from '../types';
|
|
2
2
|
import { IcomCiv } from './IcomCiv';
|
|
3
3
|
import { IcomAudio } from './IcomAudio';
|
|
4
|
+
import { IcomScopeService } from '../scope/IcomScopeService';
|
|
4
5
|
export declare class IcomControl {
|
|
5
6
|
private ev;
|
|
6
7
|
private sess;
|
|
@@ -8,6 +9,7 @@ export declare class IcomControl {
|
|
|
8
9
|
private audioSess;
|
|
9
10
|
civ: IcomCiv;
|
|
10
11
|
audio: IcomAudio;
|
|
12
|
+
scope: IcomScopeService;
|
|
11
13
|
private options;
|
|
12
14
|
private rigName;
|
|
13
15
|
private macAddress;
|
|
@@ -73,6 +75,9 @@ export declare class IcomControl {
|
|
|
73
75
|
*/
|
|
74
76
|
disconnect(options?: DisconnectOptions | DisconnectReason): Promise<void>;
|
|
75
77
|
sendCiv(data: Buffer): void;
|
|
78
|
+
enableScope(): Promise<void>;
|
|
79
|
+
disableScope(): Promise<void>;
|
|
80
|
+
waitForScopeFrame(options?: QueryOptions): Promise<import("../types").IcomScopeFrame | null>;
|
|
76
81
|
/**
|
|
77
82
|
* Set PTT (Push-To-Talk) state
|
|
78
83
|
* @param on - true to key transmitter, false to unkey
|
|
@@ -209,6 +214,30 @@ export declare class IcomControl {
|
|
|
209
214
|
* Start a manual tuning cycle (same as [TUNE] key) (CI-V 0x1A/0x02/0x00)
|
|
210
215
|
*/
|
|
211
216
|
startManualTune(): Promise<void>;
|
|
217
|
+
/** Get AF (audio output) gain. Returns 0.0–1.0, or null on timeout. */
|
|
218
|
+
getAFGain(options?: QueryOptions): Promise<LevelReading | null>;
|
|
219
|
+
/** Set AF (audio output) gain. Value 0.0–1.0. */
|
|
220
|
+
setAFGain(value: number): void;
|
|
221
|
+
/** Get squelch level. Returns 0.0–1.0, or null on timeout. */
|
|
222
|
+
getSQL(options?: QueryOptions): Promise<LevelReading | null>;
|
|
223
|
+
/** Set squelch level. Value 0.0–1.0. */
|
|
224
|
+
setSQL(value: number): void;
|
|
225
|
+
/** Get RF transmit power. Returns 0.0–1.0, or null on timeout. */
|
|
226
|
+
getRFPower(options?: QueryOptions): Promise<LevelReading | null>;
|
|
227
|
+
/** Set RF transmit power. Value 0.0–1.0. */
|
|
228
|
+
setRFPower(value: number): void;
|
|
229
|
+
/** Get microphone gain. Returns 0.0–1.0, or null on timeout. */
|
|
230
|
+
getMicGain(options?: QueryOptions): Promise<LevelReading | null>;
|
|
231
|
+
/** Set microphone gain. Value 0.0–1.0. */
|
|
232
|
+
setMicGain(value: number): void;
|
|
233
|
+
/** Get noise blanker level. 0.0 = off, >0.0 = on with strength. */
|
|
234
|
+
getNBLevel(options?: QueryOptions): Promise<LevelReading | null>;
|
|
235
|
+
/** Set noise blanker level. Value 0.0 (off) – 1.0. */
|
|
236
|
+
setNBLevel(value: number): void;
|
|
237
|
+
/** Get noise reduction level. 0.0 = off, >0.0 = on with strength. */
|
|
238
|
+
getNRLevel(options?: QueryOptions): Promise<LevelReading | null>;
|
|
239
|
+
/** Set noise reduction level. Value 0.0 (off) – 1.0. */
|
|
240
|
+
setNRLevel(value: number): void;
|
|
212
241
|
/**
|
|
213
242
|
* Read squelch status (noise/signal gate state)
|
|
214
243
|
* @param options - Query options (timeout in ms, default 3000)
|
|
@@ -305,6 +334,17 @@ export declare class IcomControl {
|
|
|
305
334
|
private processCivPayload;
|
|
306
335
|
private waitForCivFrame;
|
|
307
336
|
private static isMeterReply;
|
|
337
|
+
private static is0x14DataReply;
|
|
338
|
+
/**
|
|
339
|
+
* Read a 0x14 level value from the radio.
|
|
340
|
+
* Returns normalized value 0.0-1.0, or null on timeout/error.
|
|
341
|
+
*/
|
|
342
|
+
private read0x14Level;
|
|
343
|
+
/**
|
|
344
|
+
* Write a 0x14 level value to the radio.
|
|
345
|
+
* @param value - Normalized value 0.0-1.0
|
|
346
|
+
*/
|
|
347
|
+
private write0x14Level;
|
|
308
348
|
private startMeterPolling;
|
|
309
349
|
private stopMeterPolling;
|
|
310
350
|
private onAudioData;
|
package/dist/rig/IcomControl.js
CHANGED
|
@@ -46,6 +46,8 @@ const IcomConstants_1 = require("./IcomConstants");
|
|
|
46
46
|
const bcd_1 = require("../utils/bcd");
|
|
47
47
|
const errors_1 = require("../utils/errors");
|
|
48
48
|
const smeter_1 = require("../utils/smeter");
|
|
49
|
+
const IcomScopeCommands_1 = require("../scope/IcomScopeCommands");
|
|
50
|
+
const IcomScopeService_1 = require("../scope/IcomScopeService");
|
|
49
51
|
class IcomControl {
|
|
50
52
|
constructor(options) {
|
|
51
53
|
this.ev = new events_1.EventEmitter();
|
|
@@ -92,6 +94,9 @@ class IcomControl {
|
|
|
92
94
|
this.audioSess.open();
|
|
93
95
|
this.civ = new IcomCiv_1.IcomCiv(this.civSess);
|
|
94
96
|
this.audio = new IcomAudio_1.IcomAudio(this.audioSess);
|
|
97
|
+
this.scope = new IcomScopeService_1.IcomScopeService();
|
|
98
|
+
this.scope.on('scopeSegment', (segment) => this.ev.emit('scopeSegment', segment));
|
|
99
|
+
this.scope.on('scopeFrame', (frame) => this.ev.emit('scopeFrame', frame));
|
|
95
100
|
}
|
|
96
101
|
get events() { return this.ev; }
|
|
97
102
|
// ============================================================================
|
|
@@ -510,6 +515,22 @@ class IcomControl {
|
|
|
510
515
|
}
|
|
511
516
|
}
|
|
512
517
|
sendCiv(data) { this.civ.sendCivData(data); }
|
|
518
|
+
async enableScope() {
|
|
519
|
+
const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
|
|
520
|
+
const rigAddr = this.civ.civAddress & 0xff;
|
|
521
|
+
this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeDisplay(ctrAddr, rigAddr, true));
|
|
522
|
+
this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeDataOutput(ctrAddr, rigAddr, true));
|
|
523
|
+
}
|
|
524
|
+
async disableScope() {
|
|
525
|
+
const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
|
|
526
|
+
const rigAddr = this.civ.civAddress & 0xff;
|
|
527
|
+
this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeDataOutput(ctrAddr, rigAddr, false));
|
|
528
|
+
this.sendCiv(IcomScopeCommands_1.IcomScopeCommands.setScopeDisplay(ctrAddr, rigAddr, false));
|
|
529
|
+
}
|
|
530
|
+
async waitForScopeFrame(options) {
|
|
531
|
+
const timeoutMs = options?.timeout ?? 3000;
|
|
532
|
+
return this.scope.waitForScopeFrame(timeoutMs);
|
|
533
|
+
}
|
|
513
534
|
/**
|
|
514
535
|
* Set PTT (Push-To-Talk) state
|
|
515
536
|
* @param on - true to key transmitter, false to unkey
|
|
@@ -852,6 +873,75 @@ class IcomControl {
|
|
|
852
873
|
const rigAddr = this.civ.civAddress & 0xff;
|
|
853
874
|
this.sendCiv(IcomRigCommands_1.IcomRigCommands.startManualTune(ctrAddr, rigAddr));
|
|
854
875
|
}
|
|
876
|
+
// ============================================================================
|
|
877
|
+
// 0x14 Level API — AF Gain, SQL, RF Power, MIC Gain, NB Level, NR Level
|
|
878
|
+
// ============================================================================
|
|
879
|
+
/** Get AF (audio output) gain. Returns 0.0–1.0, or null on timeout. */
|
|
880
|
+
async getAFGain(options) {
|
|
881
|
+
const v = await this.read0x14Level(0x01, options);
|
|
882
|
+
if (v === null)
|
|
883
|
+
return null;
|
|
884
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
885
|
+
}
|
|
886
|
+
/** Set AF (audio output) gain. Value 0.0–1.0. */
|
|
887
|
+
setAFGain(value) {
|
|
888
|
+
this.write0x14Level(0x01, value);
|
|
889
|
+
}
|
|
890
|
+
/** Get squelch level. Returns 0.0–1.0, or null on timeout. */
|
|
891
|
+
async getSQL(options) {
|
|
892
|
+
const v = await this.read0x14Level(0x03, options);
|
|
893
|
+
if (v === null)
|
|
894
|
+
return null;
|
|
895
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
896
|
+
}
|
|
897
|
+
/** Set squelch level. Value 0.0–1.0. */
|
|
898
|
+
setSQL(value) {
|
|
899
|
+
this.write0x14Level(0x03, value);
|
|
900
|
+
}
|
|
901
|
+
/** Get RF transmit power. Returns 0.0–1.0, or null on timeout. */
|
|
902
|
+
async getRFPower(options) {
|
|
903
|
+
const v = await this.read0x14Level(0x0a, options);
|
|
904
|
+
if (v === null)
|
|
905
|
+
return null;
|
|
906
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
907
|
+
}
|
|
908
|
+
/** Set RF transmit power. Value 0.0–1.0. */
|
|
909
|
+
setRFPower(value) {
|
|
910
|
+
this.write0x14Level(0x0a, value);
|
|
911
|
+
}
|
|
912
|
+
/** Get microphone gain. Returns 0.0–1.0, or null on timeout. */
|
|
913
|
+
async getMicGain(options) {
|
|
914
|
+
const v = await this.read0x14Level(0x0f, options);
|
|
915
|
+
if (v === null)
|
|
916
|
+
return null;
|
|
917
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
918
|
+
}
|
|
919
|
+
/** Set microphone gain. Value 0.0–1.0. */
|
|
920
|
+
setMicGain(value) {
|
|
921
|
+
this.write0x14Level(0x0f, value);
|
|
922
|
+
}
|
|
923
|
+
/** Get noise blanker level. 0.0 = off, >0.0 = on with strength. */
|
|
924
|
+
async getNBLevel(options) {
|
|
925
|
+
const v = await this.read0x14Level(0x12, options);
|
|
926
|
+
if (v === null)
|
|
927
|
+
return null;
|
|
928
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
929
|
+
}
|
|
930
|
+
/** Set noise blanker level. Value 0.0 (off) – 1.0. */
|
|
931
|
+
setNBLevel(value) {
|
|
932
|
+
this.write0x14Level(0x12, value);
|
|
933
|
+
}
|
|
934
|
+
/** Get noise reduction level. 0.0 = off, >0.0 = on with strength. */
|
|
935
|
+
async getNRLevel(options) {
|
|
936
|
+
const v = await this.read0x14Level(0x13, options);
|
|
937
|
+
if (v === null)
|
|
938
|
+
return null;
|
|
939
|
+
return { raw: Math.round(v * 255), normalized: v };
|
|
940
|
+
}
|
|
941
|
+
/** Set noise reduction level. Value 0.0 (off) – 1.0. */
|
|
942
|
+
setNRLevel(value) {
|
|
943
|
+
this.write0x14Level(0x13, value);
|
|
944
|
+
}
|
|
855
945
|
/**
|
|
856
946
|
* Read squelch status (noise/signal gate state)
|
|
857
947
|
* @param options - Query options (timeout in ms, default 3000)
|
|
@@ -1445,6 +1535,7 @@ class IcomControl {
|
|
|
1445
1535
|
this.civAssembleBuf = this.civAssembleBuf.subarray(end + 1);
|
|
1446
1536
|
// Emit event
|
|
1447
1537
|
this.ev.emit('civFrame', frame);
|
|
1538
|
+
this.scope.handleCivFrame(frame, 'lan-civ');
|
|
1448
1539
|
// Continue loop in case multiple frames are in buffer
|
|
1449
1540
|
}
|
|
1450
1541
|
}
|
|
@@ -1488,6 +1579,50 @@ class IcomControl {
|
|
|
1488
1579
|
return false;
|
|
1489
1580
|
return true;
|
|
1490
1581
|
}
|
|
1582
|
+
// Strict 0x14 data reply matcher: FE FE [ctr|00] [rig] 0x14 [sub] [bcd_hi] [bcd_lo] FD
|
|
1583
|
+
static is0x14DataReply(frame, subcmd, ctrAddr, rigAddr) {
|
|
1584
|
+
if (!(frame && frame.length >= 9))
|
|
1585
|
+
return false;
|
|
1586
|
+
if (frame[0] !== 0xfe || frame[1] !== 0xfe)
|
|
1587
|
+
return false;
|
|
1588
|
+
const addrCtrOk = frame[2] === (ctrAddr & 0xff) || frame[2] === 0x00;
|
|
1589
|
+
const addrRigOk = frame[3] === (rigAddr & 0xff);
|
|
1590
|
+
if (!addrCtrOk || !addrRigOk)
|
|
1591
|
+
return false;
|
|
1592
|
+
if (frame[4] !== 0x14)
|
|
1593
|
+
return false;
|
|
1594
|
+
if (frame[5] !== (subcmd & 0xff))
|
|
1595
|
+
return false;
|
|
1596
|
+
if (frame[frame.length - 1] !== 0xfd)
|
|
1597
|
+
return false;
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Read a 0x14 level value from the radio.
|
|
1602
|
+
* Returns normalized value 0.0-1.0, or null on timeout/error.
|
|
1603
|
+
*/
|
|
1604
|
+
async read0x14Level(subcmd, options) {
|
|
1605
|
+
const timeoutMs = options?.timeout ?? 3000;
|
|
1606
|
+
const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
|
|
1607
|
+
const rigAddr = this.civ.civAddress & 0xff;
|
|
1608
|
+
const req = IcomRigCommands_1.IcomRigCommands.get0x14Level(ctrAddr, rigAddr, subcmd);
|
|
1609
|
+
const resp = await this.waitForCivFrame((frame) => IcomControl.is0x14DataReply(frame, subcmd, ctrAddr, rigAddr), timeoutMs, () => this.sendCiv(req));
|
|
1610
|
+
if (!resp || resp.length < 9)
|
|
1611
|
+
return null;
|
|
1612
|
+
const raw = (0, bcd_1.parseTwoByteBcd)(resp.subarray(6, 8));
|
|
1613
|
+
return raw / 255;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Write a 0x14 level value to the radio.
|
|
1617
|
+
* @param value - Normalized value 0.0-1.0
|
|
1618
|
+
*/
|
|
1619
|
+
write0x14Level(subcmd, value) {
|
|
1620
|
+
const ctrAddr = IcomConstants_1.DEFAULT_CONTROLLER_ADDR;
|
|
1621
|
+
const rigAddr = this.civ.civAddress & 0xff;
|
|
1622
|
+
const raw = Math.max(0, Math.min(255, Math.round(value * 255)));
|
|
1623
|
+
const bcd = (0, bcd_1.intToTwoByteBcd)(raw);
|
|
1624
|
+
this.sendCiv(IcomRigCommands_1.IcomRigCommands.set0x14Level(ctrAddr, rigAddr, subcmd, bcd[0], bcd[1]));
|
|
1625
|
+
}
|
|
1491
1626
|
// Start meter polling like Java (every 500ms when PTT is on)
|
|
1492
1627
|
startMeterPolling() {
|
|
1493
1628
|
this.stopMeterPolling();
|
|
@@ -21,6 +21,17 @@ export declare const IcomRigCommands: {
|
|
|
21
21
|
getCompLevel(ctrAddr: number, rigAddr: number): Buffer;
|
|
22
22
|
getVoltage(ctrAddr: number, rigAddr: number): Buffer;
|
|
23
23
|
getCurrent(ctrAddr: number, rigAddr: number): Buffer;
|
|
24
|
+
/**
|
|
25
|
+
* Build a 0x14 level read query.
|
|
26
|
+
* Send this to request the current value; the radio responds with the same
|
|
27
|
+
* command byte + subcmd + 2-byte BCD data.
|
|
28
|
+
*/
|
|
29
|
+
get0x14Level(ctrAddr: number, rigAddr: number, subcmd: number): Buffer;
|
|
30
|
+
/**
|
|
31
|
+
* Build a 0x14 level write command.
|
|
32
|
+
* @param rawValue - Integer 0-255 (use intToTwoByteBcd to encode)
|
|
33
|
+
*/
|
|
34
|
+
set0x14Level(ctrAddr: number, rigAddr: number, subcmd: number, bcdHi: number, bcdLo: number): Buffer;
|
|
24
35
|
getTunerStatus(ctrAddr: number, rigAddr: number): Buffer;
|
|
25
36
|
setTunerEnabled(ctrAddr: number, rigAddr: number, on: boolean): Buffer;
|
|
26
37
|
startManualTune(ctrAddr: number, rigAddr: number): Buffer;
|
|
@@ -99,6 +99,26 @@ exports.IcomRigCommands = {
|
|
|
99
99
|
return Buffer.from([0xfe, 0xfe, rigAddr & 0xff, ctrAddr & 0xff, 0x15, 0x16, 0xfd]);
|
|
100
100
|
},
|
|
101
101
|
// =====================
|
|
102
|
+
// 0x14 Level Commands (read/write)
|
|
103
|
+
// =====================
|
|
104
|
+
/**
|
|
105
|
+
* Build a 0x14 level read query.
|
|
106
|
+
* Send this to request the current value; the radio responds with the same
|
|
107
|
+
* command byte + subcmd + 2-byte BCD data.
|
|
108
|
+
*/
|
|
109
|
+
get0x14Level(ctrAddr, rigAddr, subcmd) {
|
|
110
|
+
// FE FE [rig] [ctr] 0x14 [subcmd] FD
|
|
111
|
+
return Buffer.from([0xfe, 0xfe, rigAddr & 0xff, ctrAddr & 0xff, 0x14, subcmd & 0xff, 0xfd]);
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* Build a 0x14 level write command.
|
|
115
|
+
* @param rawValue - Integer 0-255 (use intToTwoByteBcd to encode)
|
|
116
|
+
*/
|
|
117
|
+
set0x14Level(ctrAddr, rigAddr, subcmd, bcdHi, bcdLo) {
|
|
118
|
+
// FE FE [rig] [ctr] 0x14 [subcmd] [bcd_hi] [bcd_lo] FD
|
|
119
|
+
return Buffer.from([0xfe, 0xfe, rigAddr & 0xff, ctrAddr & 0xff, 0x14, subcmd & 0xff, bcdHi & 0xff, bcdLo & 0xff, 0xfd]);
|
|
120
|
+
},
|
|
121
|
+
// =====================
|
|
102
122
|
// Antenna Tuner (ATU)
|
|
103
123
|
// =====================
|
|
104
124
|
getTunerStatus(ctrAddr, rigAddr) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IcomScopeCommands = void 0;
|
|
4
|
+
exports.IcomScopeCommands = {
|
|
5
|
+
setScopeDataOutput(ctrAddr, rigAddr, enabled) {
|
|
6
|
+
return Buffer.from([
|
|
7
|
+
0xfe, 0xfe, rigAddr & 0xff, ctrAddr & 0xff, 0x27, 0x11, enabled ? 0x01 : 0x00, 0xfd
|
|
8
|
+
]);
|
|
9
|
+
},
|
|
10
|
+
setScopeDisplay(ctrAddr, rigAddr, enabled) {
|
|
11
|
+
return Buffer.from([
|
|
12
|
+
0xfe, 0xfe, rigAddr & 0xff, ctrAddr & 0xff, 0x27, 0x10, enabled ? 0x01 : 0x00, 0xfd
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IcomScopeSegmentInfo } from '../types';
|
|
2
|
+
export declare function isScopeFrame(frame: Buffer): boolean;
|
|
3
|
+
export declare function bcdByteToInt(v: number): number;
|
|
4
|
+
export declare function parseIcomBcdFreqLE(bytes: Buffer): number;
|
|
5
|
+
export declare function parseScopeSegment(frame: Buffer, transport: 'lan-civ' | 'serial', freqLen?: number): IcomScopeSegmentInfo | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isScopeFrame = isScopeFrame;
|
|
4
|
+
exports.bcdByteToInt = bcdByteToInt;
|
|
5
|
+
exports.parseIcomBcdFreqLE = parseIcomBcdFreqLE;
|
|
6
|
+
exports.parseScopeSegment = parseScopeSegment;
|
|
7
|
+
const SCOPE_PREFIX = [0x27, 0x00, 0x00];
|
|
8
|
+
function isScopeFrame(frame) {
|
|
9
|
+
if (frame.length < 9)
|
|
10
|
+
return false;
|
|
11
|
+
if (frame[0] !== 0xfe || frame[1] !== 0xfe)
|
|
12
|
+
return false;
|
|
13
|
+
if (frame[frame.length - 1] !== 0xfd)
|
|
14
|
+
return false;
|
|
15
|
+
return frame[4] === SCOPE_PREFIX[0] && frame[5] === SCOPE_PREFIX[1] && frame[6] === SCOPE_PREFIX[2];
|
|
16
|
+
}
|
|
17
|
+
function bcdByteToInt(v) {
|
|
18
|
+
return (v & 0x0f) + (((v >> 4) & 0x0f) * 10);
|
|
19
|
+
}
|
|
20
|
+
function parseIcomBcdFreqLE(bytes) {
|
|
21
|
+
let hz = 0;
|
|
22
|
+
let multiplier = 1;
|
|
23
|
+
for (const byte of bytes) {
|
|
24
|
+
hz += (byte & 0x0f) * multiplier;
|
|
25
|
+
multiplier *= 10;
|
|
26
|
+
hz += ((byte >> 4) & 0x0f) * multiplier;
|
|
27
|
+
multiplier *= 10;
|
|
28
|
+
}
|
|
29
|
+
return hz;
|
|
30
|
+
}
|
|
31
|
+
function parseScopeSegment(frame, transport, freqLen = 5) {
|
|
32
|
+
if (!isScopeFrame(frame))
|
|
33
|
+
return null;
|
|
34
|
+
const payload = frame.subarray(4, frame.length - 1);
|
|
35
|
+
if (payload.length < 5)
|
|
36
|
+
return null;
|
|
37
|
+
const sequence = bcdByteToInt(payload[3]);
|
|
38
|
+
const sequenceMax = bcdByteToInt(payload[4]);
|
|
39
|
+
if (sequence <= 0 || sequenceMax <= 0 || sequence > sequenceMax)
|
|
40
|
+
return null;
|
|
41
|
+
const segment = {
|
|
42
|
+
receiver: 0,
|
|
43
|
+
sequence,
|
|
44
|
+
sequenceMax,
|
|
45
|
+
rawCivPayload: Buffer.from(payload),
|
|
46
|
+
transport
|
|
47
|
+
};
|
|
48
|
+
if (sequence === 1) {
|
|
49
|
+
const minimumHeaderLength = 3 + 2 + (freqLen * 2) + 1;
|
|
50
|
+
if (payload.length < minimumHeaderLength)
|
|
51
|
+
return null;
|
|
52
|
+
const mode = payload[5];
|
|
53
|
+
const primaryFreq = parseIcomBcdFreqLE(payload.subarray(6, 6 + freqLen));
|
|
54
|
+
const secondaryFreq = parseIcomBcdFreqLE(payload.subarray(6 + freqLen, 6 + (freqLen * 2)));
|
|
55
|
+
const outOfRange = payload[6 + (freqLen * 2)] !== 0x00;
|
|
56
|
+
let startFreqHz = primaryFreq;
|
|
57
|
+
let endFreqHz = secondaryFreq;
|
|
58
|
+
if (mode === 0) {
|
|
59
|
+
startFreqHz = Math.max(0, primaryFreq - secondaryFreq);
|
|
60
|
+
endFreqHz = primaryFreq + secondaryFreq;
|
|
61
|
+
}
|
|
62
|
+
segment.mode = mode;
|
|
63
|
+
segment.outOfRange = outOfRange;
|
|
64
|
+
segment.startFreqHz = startFreqHz;
|
|
65
|
+
segment.endFreqHz = endFreqHz;
|
|
66
|
+
const pixelOffset = 7 + (freqLen * 2);
|
|
67
|
+
segment.pixels = new Uint8Array(payload.subarray(pixelOffset));
|
|
68
|
+
return segment;
|
|
69
|
+
}
|
|
70
|
+
segment.pixels = new Uint8Array(payload.subarray(5));
|
|
71
|
+
return segment;
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { IcomScopeFrame, IcomScopeSegmentInfo, IcomScopeTransport } from '../types';
|
|
3
|
+
export interface IcomScopeServiceOptions {
|
|
4
|
+
assemblyTimeoutMs?: number;
|
|
5
|
+
freqLen?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class IcomScopeService extends EventEmitter {
|
|
8
|
+
private readonly assemblyTimeoutMs;
|
|
9
|
+
private readonly freqLen;
|
|
10
|
+
private readonly assemblies;
|
|
11
|
+
constructor(options?: IcomScopeServiceOptions);
|
|
12
|
+
handleCivFrame(frame: Buffer, transport: IcomScopeTransport): IcomScopeFrame | null;
|
|
13
|
+
handleScopeSegment(segment: IcomScopeSegmentInfo): IcomScopeFrame | null;
|
|
14
|
+
waitForScopeFrame(timeoutMs?: number): Promise<IcomScopeFrame | null>;
|
|
15
|
+
private cleanupExpiredAssemblies;
|
|
16
|
+
private concatChunks;
|
|
17
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IcomScopeService = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const IcomScopeParser_1 = require("./IcomScopeParser");
|
|
6
|
+
class IcomScopeService extends events_1.EventEmitter {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
super();
|
|
9
|
+
this.assemblies = new Map();
|
|
10
|
+
this.assemblyTimeoutMs = options?.assemblyTimeoutMs ?? 500;
|
|
11
|
+
this.freqLen = options?.freqLen ?? 5;
|
|
12
|
+
}
|
|
13
|
+
handleCivFrame(frame, transport) {
|
|
14
|
+
const segment = (0, IcomScopeParser_1.parseScopeSegment)(frame, transport, this.freqLen);
|
|
15
|
+
if (!segment)
|
|
16
|
+
return null;
|
|
17
|
+
return this.handleScopeSegment(segment);
|
|
18
|
+
}
|
|
19
|
+
handleScopeSegment(segment) {
|
|
20
|
+
this.cleanupExpiredAssemblies();
|
|
21
|
+
this.emit('scopeSegment', segment);
|
|
22
|
+
const key = segment.receiver;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
if (segment.sequence === 1) {
|
|
25
|
+
this.assemblies.set(key, {
|
|
26
|
+
receiver: segment.receiver,
|
|
27
|
+
expectedMax: segment.sequenceMax,
|
|
28
|
+
mode: segment.mode,
|
|
29
|
+
startFreqHz: segment.startFreqHz,
|
|
30
|
+
endFreqHz: segment.endFreqHz,
|
|
31
|
+
outOfRange: segment.outOfRange,
|
|
32
|
+
chunks: segment.pixels ? [segment.pixels] : [],
|
|
33
|
+
rawCivPayloads: [Buffer.from(segment.rawCivPayload)],
|
|
34
|
+
updatedAt: now
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const state = this.assemblies.get(key);
|
|
39
|
+
if (!state)
|
|
40
|
+
return null;
|
|
41
|
+
if (state.expectedMax !== segment.sequenceMax) {
|
|
42
|
+
this.assemblies.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
state.chunks.push(segment.pixels ?? new Uint8Array());
|
|
46
|
+
state.rawCivPayloads.push(Buffer.from(segment.rawCivPayload));
|
|
47
|
+
state.updatedAt = now;
|
|
48
|
+
}
|
|
49
|
+
const state = this.assemblies.get(key);
|
|
50
|
+
if (!state)
|
|
51
|
+
return null;
|
|
52
|
+
if (segment.sequence !== state.expectedMax)
|
|
53
|
+
return null;
|
|
54
|
+
if (state.mode === undefined || state.startFreqHz === undefined || state.endFreqHz === undefined || state.outOfRange === undefined) {
|
|
55
|
+
this.assemblies.delete(key);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const pixels = this.concatChunks(state.chunks);
|
|
59
|
+
const frame = {
|
|
60
|
+
valid: true,
|
|
61
|
+
receiver: state.receiver,
|
|
62
|
+
sequence: segment.sequence,
|
|
63
|
+
sequenceMax: state.expectedMax,
|
|
64
|
+
mode: state.mode,
|
|
65
|
+
outOfRange: state.outOfRange,
|
|
66
|
+
startFreqHz: state.startFreqHz,
|
|
67
|
+
endFreqHz: state.endFreqHz,
|
|
68
|
+
pixels,
|
|
69
|
+
rawCivPayloads: state.rawCivPayloads.map((payload) => Buffer.from(payload)),
|
|
70
|
+
transport: segment.transport
|
|
71
|
+
};
|
|
72
|
+
this.assemblies.delete(key);
|
|
73
|
+
this.emit('scopeFrame', frame);
|
|
74
|
+
return frame;
|
|
75
|
+
}
|
|
76
|
+
async waitForScopeFrame(timeoutMs = 3000) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let done = false;
|
|
79
|
+
const onFrame = (frame) => {
|
|
80
|
+
done = true;
|
|
81
|
+
this.off('scopeFrame', onFrame);
|
|
82
|
+
resolve(frame);
|
|
83
|
+
};
|
|
84
|
+
this.on('scopeFrame', onFrame);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
if (done)
|
|
87
|
+
return;
|
|
88
|
+
this.off('scopeFrame', onFrame);
|
|
89
|
+
resolve(null);
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
cleanupExpiredAssemblies() {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
for (const [key, state] of this.assemblies.entries()) {
|
|
96
|
+
if (now - state.updatedAt > this.assemblyTimeoutMs) {
|
|
97
|
+
this.assemblies.delete(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
concatChunks(chunks) {
|
|
102
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
103
|
+
const out = new Uint8Array(total);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
out.set(chunk, offset);
|
|
107
|
+
offset += chunk.length;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.IcomScopeService = IcomScopeService;
|
package/dist/types.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface IcomRigEvents {
|
|
|
39
39
|
capabilities: (c: CapabilitiesInfo) => void;
|
|
40
40
|
civ: (data: Buffer) => void;
|
|
41
41
|
civFrame: (frame: Buffer) => void;
|
|
42
|
+
scopeSegment: (segment: IcomScopeSegmentInfo) => void;
|
|
43
|
+
scopeFrame: (frame: IcomScopeFrame) => void;
|
|
42
44
|
audio: (frame: AudioFrame) => void;
|
|
43
45
|
error: (err: Error) => void;
|
|
44
46
|
connectionLost: (info: ConnectionLostInfo) => void;
|
|
@@ -82,6 +84,32 @@ export interface QueryOptions {
|
|
|
82
84
|
*/
|
|
83
85
|
timeout?: number;
|
|
84
86
|
}
|
|
87
|
+
export type IcomScopeTransport = 'lan-civ' | 'serial';
|
|
88
|
+
export interface IcomScopeSegmentInfo {
|
|
89
|
+
receiver: 0 | 1;
|
|
90
|
+
sequence: number;
|
|
91
|
+
sequenceMax: number;
|
|
92
|
+
mode?: 0 | 1 | 2 | 3;
|
|
93
|
+
outOfRange?: boolean;
|
|
94
|
+
startFreqHz?: number;
|
|
95
|
+
endFreqHz?: number;
|
|
96
|
+
pixels?: Uint8Array;
|
|
97
|
+
rawCivPayload: Buffer;
|
|
98
|
+
transport: IcomScopeTransport;
|
|
99
|
+
}
|
|
100
|
+
export interface IcomScopeFrame {
|
|
101
|
+
valid: boolean;
|
|
102
|
+
receiver: 0 | 1;
|
|
103
|
+
sequence: number;
|
|
104
|
+
sequenceMax: number;
|
|
105
|
+
mode: 0 | 1 | 2 | 3;
|
|
106
|
+
outOfRange: boolean;
|
|
107
|
+
startFreqHz: number;
|
|
108
|
+
endFreqHz: number;
|
|
109
|
+
pixels: Uint8Array;
|
|
110
|
+
rawCivPayloads: Buffer[];
|
|
111
|
+
transport: IcomScopeTransport;
|
|
112
|
+
}
|
|
85
113
|
/**
|
|
86
114
|
* Result of a meter reading operation (SWR, ALC, etc.)
|
|
87
115
|
* @deprecated Use specific types like SwrReading, AlcReading instead
|
|
@@ -96,6 +124,16 @@ export interface MeterReading {
|
|
|
96
124
|
*/
|
|
97
125
|
success: boolean;
|
|
98
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Result of a 0x14 level read operation.
|
|
129
|
+
* Covers AF Gain, SQL Level, RF Power, MIC Gain, NB Level, NR Level.
|
|
130
|
+
*/
|
|
131
|
+
export interface LevelReading {
|
|
132
|
+
/** Raw BCD-decoded integer value (0–255) */
|
|
133
|
+
raw: number;
|
|
134
|
+
/** Normalized value in range 0.0–1.0 */
|
|
135
|
+
normalized: number;
|
|
136
|
+
}
|
|
99
137
|
/**
|
|
100
138
|
* SWR (Standing Wave Ratio) meter reading
|
|
101
139
|
* Represents antenna impedance matching quality
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icom-wlan-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Icom WLAN (CI‑V, audio) protocol implementation for Node.js/TypeScript.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,23 +9,37 @@
|
|
|
9
9
|
"clean": "rimraf dist",
|
|
10
10
|
"test": "jest",
|
|
11
11
|
"prepublishOnly": "npm run build",
|
|
12
|
-
"lint": "eslint ."
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"diagnose": "tsx scripts/diagnose.ts",
|
|
14
|
+
"test:scope:real": "tsx scripts/test-scope-real.ts"
|
|
13
15
|
},
|
|
14
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"icom",
|
|
18
|
+
"civ",
|
|
19
|
+
"cat",
|
|
20
|
+
"udp",
|
|
21
|
+
"hamradio",
|
|
22
|
+
"wlan",
|
|
23
|
+
"audio"
|
|
24
|
+
],
|
|
15
25
|
"author": "boybook",
|
|
16
26
|
"license": "MIT",
|
|
17
27
|
"repository": {
|
|
18
28
|
"type": "git",
|
|
19
29
|
"url": "https://github.com/boybook/icom-wlan-node.git"
|
|
20
30
|
},
|
|
21
|
-
"files": [
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
22
34
|
"devDependencies": {
|
|
23
35
|
"@types/jest": "^29.5.12",
|
|
24
36
|
"@types/node": "^20.10.6",
|
|
37
|
+
"commander": "^12.0.0",
|
|
25
38
|
"eslint": "^8.57.0",
|
|
26
39
|
"jest": "^29.7.0",
|
|
27
40
|
"rimraf": "^5.0.5",
|
|
28
41
|
"ts-jest": "^29.1.1",
|
|
42
|
+
"tsx": "^4.7.0",
|
|
29
43
|
"typescript": "^5.4.5"
|
|
30
44
|
}
|
|
31
45
|
}
|
package/dist/demo.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration test against a real Icom WLAN radio.
|
|
3
|
-
*
|
|
4
|
-
* This test is skipped unless these env vars are present:
|
|
5
|
-
* - ICOM_IP: radio IP
|
|
6
|
-
* - ICOM_PORT: radio control UDP port (e.g., 50001)
|
|
7
|
-
* - ICOM_USER: username
|
|
8
|
-
* - ICOM_PASS: password
|
|
9
|
-
* Optional:
|
|
10
|
-
* - ICOM_TEST_PTT=true to exercise PTT and short audio TX (be careful: this keys TX!)
|
|
11
|
-
*/
|
|
12
|
-
export {};
|
package/dist/demo.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Integration test against a real Icom WLAN radio.
|
|
4
|
-
*
|
|
5
|
-
* This test is skipped unless these env vars are present:
|
|
6
|
-
* - ICOM_IP: radio IP
|
|
7
|
-
* - ICOM_PORT: radio control UDP port (e.g., 50001)
|
|
8
|
-
* - ICOM_USER: username
|
|
9
|
-
* - ICOM_PASS: password
|
|
10
|
-
* Optional:
|
|
11
|
-
* - ICOM_TEST_PTT=true to exercise PTT and short audio TX (be careful: this keys TX!)
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
const _1 = require(".");
|
|
15
|
-
const IcomRigCommands_1 = require("./rig/IcomRigCommands");
|
|
16
|
-
const codec_1 = require("./utils/codec");
|
|
17
|
-
async function test() {
|
|
18
|
-
const ip = process.env.ICOM_IP;
|
|
19
|
-
const port = parseInt(process.env.ICOM_PORT, 10);
|
|
20
|
-
const user = process.env.ICOM_USER;
|
|
21
|
-
const pass = process.env.ICOM_PASS;
|
|
22
|
-
const testPTT = process.env.ICOM_TEST_PTT === 'true';
|
|
23
|
-
const t0 = Date.now();
|
|
24
|
-
const stamp = () => `+${(Date.now() - t0).toString().padStart(4)}ms`;
|
|
25
|
-
const rig = new _1.IcomControl({ control: { ip, port }, userName: user, password: pass });
|
|
26
|
-
let gotLogin = false;
|
|
27
|
-
let gotStatus = false;
|
|
28
|
-
let gotCap = false;
|
|
29
|
-
let civCount = 0;
|
|
30
|
-
let audioCount = 0;
|
|
31
|
-
const wait = (cond, ms = 30000) => new Promise((resolve, reject) => {
|
|
32
|
-
const start = Date.now();
|
|
33
|
-
const t = setInterval(() => {
|
|
34
|
-
if (cond()) {
|
|
35
|
-
clearInterval(t);
|
|
36
|
-
resolve();
|
|
37
|
-
}
|
|
38
|
-
else if (Date.now() - start > ms) {
|
|
39
|
-
clearInterval(t);
|
|
40
|
-
reject(new Error('timeout'));
|
|
41
|
-
}
|
|
42
|
-
}, 100);
|
|
43
|
-
});
|
|
44
|
-
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
45
|
-
const onLogin = (res) => {
|
|
46
|
-
console.log(stamp(), 'login event ok=', res.ok, 'error?', res.errorCode, 'conn=', res.connection);
|
|
47
|
-
gotLogin = res.ok;
|
|
48
|
-
};
|
|
49
|
-
const onStatus = (s) => {
|
|
50
|
-
console.log(stamp(), 'status civPort=', s.civPort, 'audioPort=', s.audioPort, 'authOK=', s.authOK, 'connected=', s.connected);
|
|
51
|
-
gotStatus = true;
|
|
52
|
-
};
|
|
53
|
-
const onCap = (c) => {
|
|
54
|
-
console.log(stamp(), 'capabilities civAddr=', c.civAddress, 'audioName=', c.audioName, 'supportTX=', c.supportTX);
|
|
55
|
-
gotCap = true;
|
|
56
|
-
};
|
|
57
|
-
const onCiv = (frame) => {
|
|
58
|
-
civCount++;
|
|
59
|
-
if (civCount <= 3)
|
|
60
|
-
console.log(stamp(), `CIV[${civCount}]`, (0, codec_1.hex)(frame.subarray(0, Math.min(16, frame.length))));
|
|
61
|
-
};
|
|
62
|
-
const onAudio = (p) => {
|
|
63
|
-
audioCount++;
|
|
64
|
-
if (audioCount % 10 === 0)
|
|
65
|
-
console.log(stamp(), `AUDIO[${audioCount}] len=`, p.pcm16.length);
|
|
66
|
-
};
|
|
67
|
-
rig.events.on('login', onLogin);
|
|
68
|
-
rig.events.on('status', onStatus);
|
|
69
|
-
rig.events.on('capabilities', onCap);
|
|
70
|
-
rig.events.on('civ', onCiv);
|
|
71
|
-
rig.events.on('audio', onAudio);
|
|
72
|
-
console.log(stamp(), 'connecting to', ip, port);
|
|
73
|
-
await rig.connect();
|
|
74
|
-
// Wait for login + status
|
|
75
|
-
console.log(stamp(), 'waiting login+status ...');
|
|
76
|
-
await wait(() => gotLogin && gotStatus, 20000);
|
|
77
|
-
console.log(stamp(), 'login+status OK');
|
|
78
|
-
// Expect at least capabilities soon
|
|
79
|
-
console.log(stamp(), 'waiting capabilities (0xA8) ...');
|
|
80
|
-
await wait(() => gotCap, 8000).catch(() => { console.log(stamp(), 'capabilities timeout (tolerated)'); });
|
|
81
|
-
// Ensure data mode and connector routing favor WLAN before CIV queries
|
|
82
|
-
console.log(stamp(), 'setMode USB-D (data) and route data to WLAN');
|
|
83
|
-
await rig.setMode('USB', { dataMode: true });
|
|
84
|
-
await rig.setConnectorDataMode('WLAN');
|
|
85
|
-
// Issue a CIV read operating frequency; expect some CIV traffic
|
|
86
|
-
const rigAddr = rig.civ.civAddress & 0xff;
|
|
87
|
-
const ctrAddr = 0xe0;
|
|
88
|
-
const readFreq = IcomRigCommands_1.IcomRigCommands.readOperatingFrequency(ctrAddr, rigAddr);
|
|
89
|
-
const civBefore = civCount;
|
|
90
|
-
rig.sendCiv(readFreq);
|
|
91
|
-
console.log(stamp(), 'sent CIV read frequency, waiting CIV traffic ...');
|
|
92
|
-
await wait(() => civCount > civBefore, 6000).catch(() => { });
|
|
93
|
-
// High-level: query frequency, set mode to USB-D (data), adjust frequency slightly and revert
|
|
94
|
-
const curHz = await rig.readOperatingFrequency({ timeout: 8000 });
|
|
95
|
-
if (curHz) {
|
|
96
|
-
console.log(stamp(), 'readOperatingFrequency =', curHz);
|
|
97
|
-
// Set data mode USB-D
|
|
98
|
-
console.log(stamp(), 'setMode USB-D');
|
|
99
|
-
await rig.setMode('USB', { dataMode: true });
|
|
100
|
-
// Route connector data to WLAN (best effort)
|
|
101
|
-
console.log(stamp(), 'setConnectorDataMode WLAN');
|
|
102
|
-
await rig.setConnectorDataMode('WLAN');
|
|
103
|
-
// Nudge frequency by +50 Hz then revert
|
|
104
|
-
const newHz = curHz + 50;
|
|
105
|
-
console.log(stamp(), 'setFrequency to', newHz);
|
|
106
|
-
await rig.setFrequency(newHz);
|
|
107
|
-
const backHz = await rig.readOperatingFrequency({ timeout: 4000 });
|
|
108
|
-
console.log(stamp(), 'verify freq after set =', backHz);
|
|
109
|
-
console.log(stamp(), 'revert frequency to', curHz);
|
|
110
|
-
await rig.setFrequency(curHz);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
console.log(stamp(), 'readOperatingFrequency returned null (tolerated)');
|
|
114
|
-
}
|
|
115
|
-
// ============================================================================
|
|
116
|
-
// API Demo: Demonstrate all available APIs and print their results
|
|
117
|
-
// ============================================================================
|
|
118
|
-
console.log('\n' + '='.repeat(80));
|
|
119
|
-
console.log(stamp(), '🎯 API DEMONSTRATION - Testing all available methods');
|
|
120
|
-
console.log('='.repeat(80) + '\n');
|
|
121
|
-
// 1. Mode APIs
|
|
122
|
-
console.log(stamp(), '📡 Testing Mode APIs:');
|
|
123
|
-
console.log(stamp(), ' → Setting mode to LSB (Lower Side Band)');
|
|
124
|
-
await rig.setMode('LSB');
|
|
125
|
-
await sleep(500);
|
|
126
|
-
console.log(stamp(), ' → Setting mode to USB (Upper Side Band)');
|
|
127
|
-
await rig.setMode('USB');
|
|
128
|
-
await sleep(500);
|
|
129
|
-
console.log(stamp(), ' → Setting mode to USB-D (Data mode for FT8)');
|
|
130
|
-
await rig.setMode('USB', { dataMode: true });
|
|
131
|
-
await sleep(500);
|
|
132
|
-
// 2. Frequency APIs
|
|
133
|
-
console.log(stamp(), '\n📻 Testing Frequency APIs:');
|
|
134
|
-
const currentFreq = await rig.readOperatingFrequency({ timeout: 3000 });
|
|
135
|
-
if (currentFreq) {
|
|
136
|
-
console.log(stamp(), ` ✓ Current frequency: ${(currentFreq / 1000000).toFixed(3)} MHz (${currentFreq} Hz)`);
|
|
137
|
-
// Test frequency change
|
|
138
|
-
const testFreq = 14074000; // FT8 on 20m
|
|
139
|
-
console.log(stamp(), ` → Setting frequency to ${(testFreq / 1000000).toFixed(3)} MHz`);
|
|
140
|
-
await rig.setFrequency(testFreq);
|
|
141
|
-
await sleep(500);
|
|
142
|
-
const verifyFreq = await rig.readOperatingFrequency({ timeout: 3000 });
|
|
143
|
-
if (verifyFreq) {
|
|
144
|
-
console.log(stamp(), ` ✓ Verified frequency: ${(verifyFreq / 1000000).toFixed(3)} MHz`);
|
|
145
|
-
}
|
|
146
|
-
// Restore original frequency
|
|
147
|
-
console.log(stamp(), ` → Restoring original frequency: ${(currentFreq / 1000000).toFixed(3)} MHz`);
|
|
148
|
-
await rig.setFrequency(currentFreq);
|
|
149
|
-
await sleep(500);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
console.log(stamp(), ' ✗ Failed to read current frequency');
|
|
153
|
-
}
|
|
154
|
-
// 3. Connector APIs
|
|
155
|
-
console.log(stamp(), '\n🔌 Testing Connector APIs:');
|
|
156
|
-
console.log(stamp(), ' → Setting connector data mode to WLAN');
|
|
157
|
-
await rig.setConnectorDataMode('WLAN');
|
|
158
|
-
await sleep(500);
|
|
159
|
-
const wlanLevel = await rig.getConnectorWLanLevel({ timeout: 2000 });
|
|
160
|
-
if (wlanLevel) {
|
|
161
|
-
console.log(stamp(), ` ✓ WLAN Level: ${wlanLevel.percent.toFixed(1)}% (raw=${wlanLevel.raw}/255)`);
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
console.log(stamp(), ' ℹ WLAN Level: Not available (may not be supported on this radio)');
|
|
165
|
-
}
|
|
166
|
-
// Test setting WLAN level
|
|
167
|
-
console.log(stamp(), ' → Setting WLAN level to 128 (50%)');
|
|
168
|
-
await rig.setConnectorWLanLevel(128);
|
|
169
|
-
await sleep(500);
|
|
170
|
-
const newWlanLevel = await rig.getConnectorWLanLevel({ timeout: 2000 });
|
|
171
|
-
if (newWlanLevel) {
|
|
172
|
-
console.log(stamp(), ` ✓ New WLAN Level: ${newWlanLevel.percent.toFixed(1)}% (raw=${newWlanLevel.raw}/255)`);
|
|
173
|
-
}
|
|
174
|
-
// 4. Summary of RX-mode readings
|
|
175
|
-
console.log(stamp(), '\n📋 Summary of Current Radio State (RX mode):');
|
|
176
|
-
console.log(stamp(), ' ╔═══════════════════════════════════════════════════════╗');
|
|
177
|
-
if (currentFreq) {
|
|
178
|
-
console.log(stamp(), ` ║ Frequency: ${(currentFreq / 1000000).toFixed(3).padEnd(10)} MHz ║`);
|
|
179
|
-
}
|
|
180
|
-
console.log(stamp(), ` ║ Mode: USB-D (Data mode) ║`);
|
|
181
|
-
console.log(stamp(), ` ║ Connector: WLAN ║`);
|
|
182
|
-
if (newWlanLevel) {
|
|
183
|
-
console.log(stamp(), ` ║ WLAN Level: ${newWlanLevel.percent.toFixed(1).padEnd(5)}% ║`);
|
|
184
|
-
}
|
|
185
|
-
console.log(stamp(), ' ╚═══════════════════════════════════════════════════════╝');
|
|
186
|
-
console.log(stamp(), '\n ℹ️ Note: Meter readings (SWR/ALC) require TX mode - see PTT test below');
|
|
187
|
-
console.log('\n' + '='.repeat(80));
|
|
188
|
-
console.log(stamp(), '✅ API DEMONSTRATION COMPLETE (RX mode)');
|
|
189
|
-
console.log('='.repeat(80) + '\n');
|
|
190
|
-
// Expect to receive at least some audio frames if radio is streaming
|
|
191
|
-
console.log(stamp(), 'waiting audio frames ...');
|
|
192
|
-
await wait(() => audioCount > 0, 6000).catch(() => { });
|
|
193
|
-
// Try reading additional CIV info in RX mode
|
|
194
|
-
try {
|
|
195
|
-
const mode = await rig.readOperatingMode({ timeout: 1500 });
|
|
196
|
-
if (mode) {
|
|
197
|
-
const modeStr = mode.modeName ?? `0x${mode.mode.toString(16)}`;
|
|
198
|
-
const filStr = mode.filterName ?? (mode.filter !== undefined ? `FIL${mode.filter}` : '');
|
|
199
|
-
console.log(stamp(), `RX: Operating Mode = ${modeStr}${filStr ? `, ${filStr}` : ''}`);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
console.log(stamp(), 'RX: Operating Mode: Not available');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch { }
|
|
206
|
-
try {
|
|
207
|
-
const edges = await rig.readBandEdges({ timeout: 1500 });
|
|
208
|
-
if (edges) {
|
|
209
|
-
console.log(stamp(), `RX: Band edges payload length: ${edges.length} bytes`);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
console.log(stamp(), 'RX: Band edges: Not available');
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
catch { }
|
|
216
|
-
// Optional: brief PTT + short audio TX (dangerous; keys TX)
|
|
217
|
-
if (testPTT && gotLogin && gotStatus) {
|
|
218
|
-
console.log('\n' + '='.repeat(80));
|
|
219
|
-
console.log(stamp(), '📡 PTT TEST & METER READINGS (TX MODE)');
|
|
220
|
-
console.log('='.repeat(80) + '\n');
|
|
221
|
-
// Ensure audio routing is set to WLAN before PTT
|
|
222
|
-
console.log(stamp(), '→ Setting connector data mode to WLAN before PTT');
|
|
223
|
-
await rig.setConnectorDataMode('WLAN');
|
|
224
|
-
console.log(stamp(), '→ PTT ON - Starting transmission');
|
|
225
|
-
await rig.setPtt(true);
|
|
226
|
-
// Wait a moment for TX to stabilize
|
|
227
|
-
await sleep(500);
|
|
228
|
-
// Generate all audio frames at once (10 seconds of 1 kHz tone)
|
|
229
|
-
const frames = 500; // 500 * 20ms = 10000ms = 10 seconds
|
|
230
|
-
const samplesPerFrame = 240;
|
|
231
|
-
const totalSamples = frames * samplesPerFrame;
|
|
232
|
-
const allAudio = new Float32Array(totalSamples);
|
|
233
|
-
// Generate 1 kHz tone for entire duration
|
|
234
|
-
for (let i = 0; i < totalSamples; i++) {
|
|
235
|
-
allAudio[i] = Math.sin(2 * Math.PI * 1000 * i / _1.AUDIO_RATE) * 0.2;
|
|
236
|
-
}
|
|
237
|
-
// Add all audio to queue at once with leading silence buffer
|
|
238
|
-
console.log(stamp(), '→ Enqueuing', frames, 'frames of audio (1kHz tone, 10 seconds)');
|
|
239
|
-
rig.sendAudioFloat32(allAudio, true); // true = add leading silence buffer
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// 📊 Testing Meter APIs during TX
|
|
242
|
-
// ============================================================================
|
|
243
|
-
console.log(stamp(), '\n📊 Reading Meters during TX:');
|
|
244
|
-
// Also read TX-related CIV info
|
|
245
|
-
try {
|
|
246
|
-
const txHz = await rig.readTransmitFrequency({ timeout: 1500 });
|
|
247
|
-
if (txHz)
|
|
248
|
-
console.log(stamp(), ` TX Frequency: ${txHz} Hz`);
|
|
249
|
-
else
|
|
250
|
-
console.log(stamp(), ' TX Frequency: Not available');
|
|
251
|
-
}
|
|
252
|
-
catch { }
|
|
253
|
-
try {
|
|
254
|
-
const state = await rig.readTransceiverState({ timeout: 1500 });
|
|
255
|
-
if (state)
|
|
256
|
-
console.log(stamp(), ` Transceiver State: ${state}`);
|
|
257
|
-
else
|
|
258
|
-
console.log(stamp(), ' Transceiver State: Not available');
|
|
259
|
-
}
|
|
260
|
-
catch { }
|
|
261
|
-
// Read meters multiple times during transmission to get stable readings
|
|
262
|
-
const meterReadings = { swr: [], alc: [] };
|
|
263
|
-
for (let i = 0; i < 3; i++) {
|
|
264
|
-
await sleep(1000); // Wait 1 second between readings
|
|
265
|
-
console.log(stamp(), ` → Reading meters (${i + 1}/3)...`);
|
|
266
|
-
const swr = await rig.readSWR({ timeout: 2000 });
|
|
267
|
-
if (swr) {
|
|
268
|
-
meterReadings.swr.push(swr);
|
|
269
|
-
const swrStatus = swr.alert ? '⚠️ ALERT' : '✓ OK';
|
|
270
|
-
console.log(stamp(), ` SWR: ${swr.swr.toFixed(2)} (raw=${swr.raw}) ${swrStatus}`);
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
console.log(stamp(), ` SWR: Not available`);
|
|
274
|
-
}
|
|
275
|
-
const alc = await rig.readALC({ timeout: 2000 });
|
|
276
|
-
if (alc) {
|
|
277
|
-
meterReadings.alc.push(alc);
|
|
278
|
-
const alcStatus = alc.alert ? '⚠️ ALERT' : '✓ OK';
|
|
279
|
-
console.log(stamp(), ` ALC: ${alc.percent.toFixed(1)}% (raw=${alc.raw}) ${alcStatus}`);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
console.log(stamp(), ` ALC: Not available`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// Calculate and display average readings
|
|
286
|
-
if (meterReadings.swr.length > 0) {
|
|
287
|
-
const avgSwr = meterReadings.swr.reduce((sum, r) => sum + r.swr, 0) / meterReadings.swr.length;
|
|
288
|
-
const hasAlert = meterReadings.swr.some(r => r.alert);
|
|
289
|
-
console.log(stamp(), '\n 📈 Average SWR Reading:');
|
|
290
|
-
console.log(stamp(), ` - Average Value: ${avgSwr.toFixed(2)}`);
|
|
291
|
-
console.log(stamp(), ` - Status: ${hasAlert ? '⚠️ ALERT (High SWR detected!)' : '✓ OK'}`);
|
|
292
|
-
console.log(stamp(), ` - Assessment: ${avgSwr < 1.5 ? 'Excellent antenna match' : avgSwr < 2.0 ? 'Good antenna match' : 'Poor antenna match - check connections'}`);
|
|
293
|
-
}
|
|
294
|
-
if (meterReadings.alc.length > 0) {
|
|
295
|
-
const avgAlc = meterReadings.alc.reduce((sum, r) => sum + r.percent, 0) / meterReadings.alc.length;
|
|
296
|
-
const hasAlert = meterReadings.alc.some(r => r.alert);
|
|
297
|
-
console.log(stamp(), '\n 📈 Average ALC Reading:');
|
|
298
|
-
console.log(stamp(), ` - Average Level: ${avgAlc.toFixed(1)}%`);
|
|
299
|
-
console.log(stamp(), ` - Status: ${hasAlert ? '⚠️ ALERT (Over-driving!)' : '✓ OK'}`);
|
|
300
|
-
console.log(stamp(), ` - Assessment: ${avgAlc < 30 ? 'Low drive - increase audio level' : avgAlc < 70 ? 'Normal operating range' : 'High drive - reduce audio level'}`);
|
|
301
|
-
}
|
|
302
|
-
// Wait for remaining transmission to complete
|
|
303
|
-
const remainingTime = (frames * 21) - 3000; // Already waited 3 seconds for meter readings
|
|
304
|
-
if (remainingTime > 0) {
|
|
305
|
-
console.log(stamp(), `\n→ Waiting for transmission to complete (~${(remainingTime / 1000).toFixed(1)}s remaining)`);
|
|
306
|
-
await sleep(remainingTime);
|
|
307
|
-
}
|
|
308
|
-
await rig.setPtt(false);
|
|
309
|
-
console.log(stamp(), '\n→ PTT OFF - Transmission complete, trailing silence sent');
|
|
310
|
-
console.log('\n' + '='.repeat(80));
|
|
311
|
-
console.log(stamp(), '✅ PTT TEST & METER READINGS COMPLETE');
|
|
312
|
-
console.log('='.repeat(80) + '\n');
|
|
313
|
-
await sleep(1000);
|
|
314
|
-
}
|
|
315
|
-
// Clean up listeners to help jest exit
|
|
316
|
-
rig.events.off('login', onLogin);
|
|
317
|
-
rig.events.off('status', onStatus);
|
|
318
|
-
rig.events.off('capabilities', onCap);
|
|
319
|
-
rig.events.off('civ', onCiv);
|
|
320
|
-
rig.events.off('audio', onAudio);
|
|
321
|
-
console.log(stamp(), 'summary: civ=', civCount, 'audio=', audioCount);
|
|
322
|
-
console.log(stamp(), 'disconnecting...');
|
|
323
|
-
await rig.disconnect();
|
|
324
|
-
console.log(stamp(), 'disconnected');
|
|
325
|
-
}
|
|
326
|
-
test().then().catch(e => {
|
|
327
|
-
console.error('Test failed:', e);
|
|
328
|
-
process.exit(1);
|
|
329
|
-
});
|