icom-wlan-node 0.1.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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/core/IcomPackets.d.ts +104 -0
- package/dist/core/IcomPackets.js +333 -0
- package/dist/core/Session.d.ts +43 -0
- package/dist/core/Session.js +105 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +329 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +42 -0
- package/dist/rig/IcomAudio.d.ts +27 -0
- package/dist/rig/IcomAudio.js +143 -0
- package/dist/rig/IcomCiv.d.ts +14 -0
- package/dist/rig/IcomCiv.js +44 -0
- package/dist/rig/IcomConstants.d.ts +84 -0
- package/dist/rig/IcomConstants.js +112 -0
- package/dist/rig/IcomControl.d.ts +155 -0
- package/dist/rig/IcomControl.js +912 -0
- package/dist/rig/IcomRigCommands.d.ts +17 -0
- package/dist/rig/IcomRigCommands.js +73 -0
- package/dist/transport/UdpClient.d.ts +14 -0
- package/dist/transport/UdpClient.js +47 -0
- package/dist/types.d.ts +160 -0
- package/dist/types.js +2 -0
- package/dist/utils/bcd.d.ts +36 -0
- package/dist/utils/bcd.js +59 -0
- package/dist/utils/codec.d.ts +22 -0
- package/dist/utils/codec.js +56 -0
- package/dist/utils/debug.d.ts +4 -0
- package/dist/utils/debug.js +15 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 boybook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# @boybook/icom-wlan
|
|
2
|
+
|
|
3
|
+
Icom WLAN (UDP) protocol implementation in Node.js + TypeScript, featuring:
|
|
4
|
+
|
|
5
|
+
- Control channel handshake (AreYouThere/AreYouReady), login (0x80/0x60), token confirm/renew (0x40)
|
|
6
|
+
- CI‑V over UDP encapsulation (open/close keep‑alive + CIV frame transport)
|
|
7
|
+
- Audio stream send/receive (LPCM 16‑bit mono @ 12 kHz; 20 ms frames)
|
|
8
|
+
- Typed, event‑based API; designed for use as a dependency in other Node projects
|
|
9
|
+
|
|
10
|
+
This is a clean TypeScript design inspired by FT8CN’s Android implementation but written idiomatically for Node.js.
|
|
11
|
+
|
|
12
|
+
Acknowledgements: Thanks to FT8CN (https://github.com/N0BOY/FT8CN) for sharing protocol insights and inspiration.
|
|
13
|
+
|
|
14
|
+
> Note: mDNS/DNS‑SD discovery is not included; pass your radio’s IP/port directly.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm install @boybook/icom-wlan
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Build from source:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { IcomControl, AUDIO_RATE } from '@boybook/icom-wlan';
|
|
33
|
+
|
|
34
|
+
const rig = new IcomControl({
|
|
35
|
+
control: { ip: '192.168.1.50', port: 50001 },
|
|
36
|
+
userName: 'user',
|
|
37
|
+
password: 'pass'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
rig.events.on('login', (res) => {
|
|
41
|
+
if (res.ok) console.log('Login OK');
|
|
42
|
+
else console.error('Login failed', res.errorCode);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
rig.events.on('status', (s) => {
|
|
46
|
+
console.log('Ports:', s.civPort, s.audioPort);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
rig.events.on('capabilities', (c) => {
|
|
50
|
+
console.log('CIV address:', c.civAddress, 'audio:', c.audioName);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
rig.events.on('civ', (bytes) => {
|
|
54
|
+
// raw CI‑V frame from radio (FE FE ... FD)
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Also available: parsed per‑frame CI‑V event (already segmented FE FE ... FD)
|
|
58
|
+
rig.events.on('civFrame', (frame) => {
|
|
59
|
+
// One complete CI‑V frame
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
rig.events.on('audio', (frame) => {
|
|
63
|
+
// frame.pcm16 is raw 16‑bit PCM mono @ 12 kHz
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
rig.events.on('error', (err) => console.error('UDP error', err));
|
|
67
|
+
|
|
68
|
+
(async () => {
|
|
69
|
+
await rig.connect();
|
|
70
|
+
})();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Send CI‑V commands
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// Send an already built CI‑V frame
|
|
77
|
+
rig.sendCiv(Buffer.from([0xfe,0xfe,0xa4,0xe0,0x03,0xfd]));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### PTT and Audio TX
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// Start PTT and begin audio transmit (queue frames at 20 ms cadence)
|
|
84
|
+
await rig.setPtt(true);
|
|
85
|
+
|
|
86
|
+
// Provide Float32 samples in [-1,1]
|
|
87
|
+
const tone = new Float32Array(240); // 20 ms @ 12k
|
|
88
|
+
for (let i = 0; i < tone.length; i++) tone[i] = Math.sin(2*Math.PI*1000 * i / AUDIO_RATE);
|
|
89
|
+
// Optional 2nd arg `addLeadingBuffer=true` inserts a short leading silence
|
|
90
|
+
rig.sendAudioFloat32(tone, true);
|
|
91
|
+
|
|
92
|
+
// Stop PTT
|
|
93
|
+
await rig.setPtt(false);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API Overview
|
|
97
|
+
|
|
98
|
+
- `new IcomControl(options)`
|
|
99
|
+
- `options.control`: `{ ip, port }` radio control UDP endpoint
|
|
100
|
+
- `options.userName`, `options.password`
|
|
101
|
+
- Events (`rig.events.on(...)`)
|
|
102
|
+
- `login(LoginResult)` — 0x60 processed (ok/error)
|
|
103
|
+
- `status(StatusInfo)` — CI‑V/audio ports from 0x50
|
|
104
|
+
- `capabilities(CapabilitiesInfo)` — civ address, audio name (0xA8)
|
|
105
|
+
- `civ(Buffer)` — raw CI‑V payload bytes as transported over UDP
|
|
106
|
+
- `civFrame(Buffer)` — one complete CI‑V frame (FE FE ... FD)
|
|
107
|
+
- `audio({ pcm16: Buffer })` — audio frames
|
|
108
|
+
- `error(Error)` — UDP errors
|
|
109
|
+
- Methods
|
|
110
|
+
- `connect()` / `disconnect()` — connects control + CIV + audio sub‑sessions; resolves when all ready
|
|
111
|
+
- `sendCiv(buf: Buffer)` — send a raw CI‑V frame
|
|
112
|
+
- `setPtt(on: boolean)` — key/unkey; also manages TX meter polling and audio tailing
|
|
113
|
+
- `sendAudioFloat32(samples: Float32Array, addLeadingBuffer?: boolean)` / `sendAudioPcm16(samples: Int16Array)`
|
|
114
|
+
|
|
115
|
+
### High‑Level API
|
|
116
|
+
|
|
117
|
+
The library exposes common CI‑V operations as friendly methods. Addresses are handled internally (`ctrAddr=0xe0`, `rigAddr` discovered via capabilities).
|
|
118
|
+
|
|
119
|
+
- `setFrequency(hz: number)`
|
|
120
|
+
- `setMode(mode: IcomMode | number, { dataMode?: boolean })`
|
|
121
|
+
- `setPtt(on: boolean)`
|
|
122
|
+
- `readOperatingFrequency(options?: QueryOptions) => Promise<number|null>`
|
|
123
|
+
- `readOperatingMode(options?: QueryOptions) => Promise<{ mode: number; filter?: number; modeName?: string; filterName?: string } | null>`
|
|
124
|
+
- `readTransmitFrequency(options?: QueryOptions) => Promise<number|null>`
|
|
125
|
+
- `readTransceiverState(options?: QueryOptions) => Promise<'TX' | 'RX' | 'UNKNOWN' | null>`
|
|
126
|
+
- `readBandEdges(options?: QueryOptions) => Promise<Buffer|null>`
|
|
127
|
+
- `readSWR(options?: QueryOptions) => Promise<{ raw: number; swr: number; alert: boolean } | null>`
|
|
128
|
+
- `readALC(options?: QueryOptions) => Promise<{ raw: number; percent: number; alert: boolean } | null>`
|
|
129
|
+
- `getConnectorWLanLevel(options?: QueryOptions) => Promise<{ raw: number; percent: number } | null>`
|
|
130
|
+
- `getLevelMeter(options?: QueryOptions) => Promise<{ raw: number; percent: number } | null>`
|
|
131
|
+
- `setConnectorWLanLevel(level: number)`
|
|
132
|
+
- `setConnectorDataMode(mode: ConnectorDataMode | number)`
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Set frequency and mode (USB-D)
|
|
138
|
+
await rig.setFrequency(14074000);
|
|
139
|
+
await rig.setMode(0x01, { dataMode: true }); // USB=0x01, data mode
|
|
140
|
+
|
|
141
|
+
// Query current frequency (Hz)
|
|
142
|
+
const hz = await rig.readOperatingFrequency({ timeout: 3000 });
|
|
143
|
+
console.log('Rig freq:', hz);
|
|
144
|
+
|
|
145
|
+
// Toggle PTT and send a short 1 kHz tone
|
|
146
|
+
await rig.setPtt(true);
|
|
147
|
+
for (let n = 0; n < 10; n++) {
|
|
148
|
+
const tone = new Float32Array(240);
|
|
149
|
+
for (let i = 0; i < tone.length; i++) tone[i] = Math.sin(2*Math.PI*1000*i/AUDIO_RATE) * 0.2;
|
|
150
|
+
rig.sendAudioFloat32(tone);
|
|
151
|
+
await new Promise(r => setTimeout(r, 20));
|
|
152
|
+
}
|
|
153
|
+
await rig.setPtt(false);
|
|
154
|
+
|
|
155
|
+
// Read meters and connector settings
|
|
156
|
+
const swr = await rig.readSWR({ timeout: 2000 });
|
|
157
|
+
const alc = await rig.readALC({ timeout: 2000 });
|
|
158
|
+
const wlanLevel = await rig.getConnectorWLanLevel({ timeout: 2000 });
|
|
159
|
+
const level = await rig.getLevelMeter({ timeout: 1500 });
|
|
160
|
+
await rig.setConnectorWLanLevel(0x0120);
|
|
161
|
+
await rig.setConnectorDataMode(0x01); // e.g., DATA
|
|
162
|
+
|
|
163
|
+
if (level) {
|
|
164
|
+
console.log(`Generic Level Meter: raw=${level.raw} (${level.percent.toFixed(1)}%)`);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Design Notes
|
|
169
|
+
|
|
170
|
+
- Packets follow Icom’s UDP framing: fixed headers with mixed endianness. See `src/core/IcomPackets.ts` for builders/parsers.
|
|
171
|
+
- Separate UDP session with tracked sequence numbers and resend history (skeleton) in `src/core/Session.ts`.
|
|
172
|
+
- CI‑V and Audio sub‑channels reuse the same UDP transport here; radios expose distinct ports after 0x50. You can adapt by creating additional `Session` instances bound to those ports if desired.
|
|
173
|
+
- Credentials use the same simple substitution cipher as FT8CN’s Android client (`passCode`).
|
|
174
|
+
- 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.
|
|
175
|
+
- CIV/audio sub‑sessions each run their own Ping/Idle and (for CIV) OpenClose keep‑alive.
|
|
176
|
+
|
|
177
|
+
### Endianness and parsing tips
|
|
178
|
+
|
|
179
|
+
- Always use helpers from `src/utils/codec.ts` (`be16/be32/le16/le32`) when reading/writing packet fields.
|
|
180
|
+
- Do not call `Buffer.readUInt16LE/BE` or `Buffer.readUInt32LE/BE` directly for protocol fields in new code.
|
|
181
|
+
- See `CLAUDE.md` and `ENDIAN_VERIFICATION.md` for a complete cross‑check against FT8CN’s Java code. The Java names are misleading; TypeScript names reflect the actual endianness (be=Big‑Endian, le=Little‑Endian).
|
|
182
|
+
|
|
183
|
+
## Tests
|
|
184
|
+
|
|
185
|
+
- Unit tests cover packet builders/parsers and minimal session sequencing.
|
|
186
|
+
- Run: `npm test` (requires dev dependencies installed).
|
|
187
|
+
- Integration test against a real radio is included. Set env vars: `ICOM_IP`, `ICOM_PORT` (control), `ICOM_USER`, `ICOM_PASS`. Optional: `ICOM_TEST_PTT=true`.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
ICOM_IP=192.168.31.253 ICOM_PORT=50001 ICOM_USER=icom ICOM_PASS=icomicom npm test -- __tests__/integration.real.test.ts
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Limitations / TODO
|
|
196
|
+
|
|
197
|
+
- Discovery (mDNS) not implemented.
|
|
198
|
+
- Full token renewal loop and advanced status flag parsing simplified.
|
|
199
|
+
- Audio receive/playback is library‑only; playback is up to the integrator.
|
|
200
|
+
- Robust retransmit/multi‑retransmit handling can be extended.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export declare const Sizes: {
|
|
2
|
+
readonly CONTROL: 16;
|
|
3
|
+
readonly WATCHDOG: 20;
|
|
4
|
+
readonly PING: 21;
|
|
5
|
+
readonly OPENCLOSE: 22;
|
|
6
|
+
readonly RETRANSMIT_RANGE: 24;
|
|
7
|
+
readonly TOKEN: 64;
|
|
8
|
+
readonly STATUS: 80;
|
|
9
|
+
readonly LOGIN_RESPONSE: 96;
|
|
10
|
+
readonly LOGIN: 128;
|
|
11
|
+
readonly CONNINFO: 144;
|
|
12
|
+
readonly CAP: 66;
|
|
13
|
+
readonly RADIO_CAP: 102;
|
|
14
|
+
readonly CAP_CAP: 168;
|
|
15
|
+
readonly AUDIO_HEAD: 24;
|
|
16
|
+
};
|
|
17
|
+
export declare const Cmd: {
|
|
18
|
+
readonly NULL: 0;
|
|
19
|
+
readonly RETRANSMIT: 1;
|
|
20
|
+
readonly ARE_YOU_THERE: 3;
|
|
21
|
+
readonly I_AM_HERE: 4;
|
|
22
|
+
readonly DISCONNECT: 5;
|
|
23
|
+
readonly ARE_YOU_READY: 6;
|
|
24
|
+
readonly I_AM_READY: 6;
|
|
25
|
+
readonly PING: 7;
|
|
26
|
+
};
|
|
27
|
+
export declare const TokenType: {
|
|
28
|
+
readonly DELETE: 1;
|
|
29
|
+
readonly CONFIRM: 2;
|
|
30
|
+
readonly DISCONNECT: 4;
|
|
31
|
+
readonly RENEWAL: 5;
|
|
32
|
+
};
|
|
33
|
+
export declare const AUDIO_SAMPLE_RATE = 12000;
|
|
34
|
+
export declare const TX_BUFFER_SIZE = 240;
|
|
35
|
+
export declare const XIEGU_TX_BUFFER_SIZE = 150;
|
|
36
|
+
export declare const ControlPacket: {
|
|
37
|
+
toBytes(type: number, seq: number, sentId: number, rcvdId: number): Buffer;
|
|
38
|
+
getType(buf: Buffer): number;
|
|
39
|
+
getSeq(buf: Buffer): number;
|
|
40
|
+
getSentId(buf: Buffer): number;
|
|
41
|
+
getRcvdId(buf: Buffer): number;
|
|
42
|
+
setSeq(buf: Buffer, seq: number): void;
|
|
43
|
+
};
|
|
44
|
+
export declare const PingPacket: {
|
|
45
|
+
isPing(buf: Buffer): boolean;
|
|
46
|
+
getReply(buf: Buffer): number;
|
|
47
|
+
buildPing(localId: number, remoteId: number, seq: number): Buffer;
|
|
48
|
+
buildReply(from: Buffer, localId: number, remoteId: number): Buffer;
|
|
49
|
+
};
|
|
50
|
+
export declare const TokenPacket: {
|
|
51
|
+
build(seq: number, localId: number, remoteId: number, requestType: number, innerSeq: number, tokRequest: number, token: number): Buffer;
|
|
52
|
+
getRequestType(b: Buffer): number;
|
|
53
|
+
getRequestReply(b: Buffer): number;
|
|
54
|
+
getResponse(b: Buffer): number;
|
|
55
|
+
getTokRequest(b: Buffer): number;
|
|
56
|
+
getToken(b: Buffer): number;
|
|
57
|
+
};
|
|
58
|
+
export declare const LoginPacket: {
|
|
59
|
+
passCode(input: string): Buffer;
|
|
60
|
+
build(seq: number, localId: number, remoteId: number, innerSeq: number, tokRequest: number, token: number, userName: string, password: string, name: string): Buffer;
|
|
61
|
+
};
|
|
62
|
+
export declare const LoginResponsePacket: {
|
|
63
|
+
authOK(b: Buffer): boolean;
|
|
64
|
+
errorNum(b: Buffer): number;
|
|
65
|
+
getToken(b: Buffer): number;
|
|
66
|
+
getConnection(b: Buffer): string;
|
|
67
|
+
};
|
|
68
|
+
export declare const StatusPacket: {
|
|
69
|
+
authOK(b: Buffer): boolean;
|
|
70
|
+
getIsConnected(b: Buffer): boolean;
|
|
71
|
+
getRigCivPort(b: Buffer): number;
|
|
72
|
+
getRigAudioPort(b: Buffer): number;
|
|
73
|
+
};
|
|
74
|
+
export declare const CapCapabilitiesPacket: {
|
|
75
|
+
getRadioCapPacket(b: Buffer, idx: number): Buffer | null;
|
|
76
|
+
};
|
|
77
|
+
export declare const RadioCapPacket: {
|
|
78
|
+
getRigName(b: Buffer): string;
|
|
79
|
+
getAudioName(b: Buffer): string;
|
|
80
|
+
getCivAddress(b: Buffer): number;
|
|
81
|
+
getRxSupportSample(b: Buffer): number;
|
|
82
|
+
getTxSupportSample(b: Buffer): number;
|
|
83
|
+
getSupportTX(b: Buffer): boolean;
|
|
84
|
+
};
|
|
85
|
+
export declare const CivPacket: {
|
|
86
|
+
isCiv(b: Buffer): boolean;
|
|
87
|
+
getCivData(b: Buffer): Buffer;
|
|
88
|
+
setCivData(seq: number, sentId: number, rcvdId: number, civSeq: number, data: Buffer): Buffer;
|
|
89
|
+
};
|
|
90
|
+
export declare const OpenClosePacket: {
|
|
91
|
+
toBytes(seq: number, sentId: number, rcvdId: number, civSeq: number, magic: number): Buffer;
|
|
92
|
+
};
|
|
93
|
+
export declare const ConnInfoPacket: {
|
|
94
|
+
getBusy(b: Buffer): boolean;
|
|
95
|
+
getMacAddress(b: Buffer): Buffer;
|
|
96
|
+
getRigName(b: Buffer): string;
|
|
97
|
+
connectRequestPacket(seq: number, localSID: number, remoteSID: number, requestReply: number, requestType: number, innerSeq: number, tokRequest: number, token: number, macAddress: Buffer, rigName: string, userName: string, sampleRate: number, civPort: number, audioPort: number, txBufferSize: number): Buffer;
|
|
98
|
+
connInfoPacketData(rigData: Buffer, seq: number, localSID: number, remoteSID: number, requestReply: number, requestType: number, innerSeq: number, tokRequest: number, token: number, rigName: string, userName: string, rxSampleRate: number, txSampleRate: number, civPort: number, audioPort: number, txBufferSize: number): Buffer;
|
|
99
|
+
};
|
|
100
|
+
export declare const AudioPacket: {
|
|
101
|
+
isAudioPacket(b: Buffer): boolean;
|
|
102
|
+
getAudioData(b: Buffer): Buffer;
|
|
103
|
+
getTxAudioPacket(audio: Buffer, seq: number, sentId: number, rcvdId: number, sendSeq: number): Buffer;
|
|
104
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AudioPacket = exports.ConnInfoPacket = exports.OpenClosePacket = exports.CivPacket = exports.RadioCapPacket = exports.CapCapabilitiesPacket = exports.StatusPacket = exports.LoginResponsePacket = exports.LoginPacket = exports.TokenPacket = exports.PingPacket = exports.ControlPacket = exports.XIEGU_TX_BUFFER_SIZE = exports.TX_BUFFER_SIZE = exports.AUDIO_SAMPLE_RATE = exports.TokenType = exports.Cmd = exports.Sizes = void 0;
|
|
4
|
+
const codec_1 = require("../utils/codec");
|
|
5
|
+
const debug_1 = require("../utils/debug");
|
|
6
|
+
// Constants derived from Android implementation and known Icom behavior
|
|
7
|
+
exports.Sizes = {
|
|
8
|
+
CONTROL: 0x10,
|
|
9
|
+
WATCHDOG: 0x14,
|
|
10
|
+
PING: 0x15,
|
|
11
|
+
OPENCLOSE: 0x16,
|
|
12
|
+
RETRANSMIT_RANGE: 0x18,
|
|
13
|
+
TOKEN: 0x40,
|
|
14
|
+
STATUS: 0x50,
|
|
15
|
+
LOGIN_RESPONSE: 0x60,
|
|
16
|
+
LOGIN: 0x80,
|
|
17
|
+
CONNINFO: 0x90,
|
|
18
|
+
CAP: 0x42,
|
|
19
|
+
RADIO_CAP: 0x66,
|
|
20
|
+
CAP_CAP: 0xA8,
|
|
21
|
+
AUDIO_HEAD: 0x18
|
|
22
|
+
};
|
|
23
|
+
exports.Cmd = {
|
|
24
|
+
NULL: 0x00,
|
|
25
|
+
RETRANSMIT: 0x01,
|
|
26
|
+
ARE_YOU_THERE: 0x03,
|
|
27
|
+
I_AM_HERE: 0x04,
|
|
28
|
+
DISCONNECT: 0x05,
|
|
29
|
+
ARE_YOU_READY: 0x06,
|
|
30
|
+
I_AM_READY: 0x06,
|
|
31
|
+
PING: 0x07
|
|
32
|
+
};
|
|
33
|
+
exports.TokenType = {
|
|
34
|
+
DELETE: 0x01,
|
|
35
|
+
CONFIRM: 0x02,
|
|
36
|
+
DISCONNECT: 0x04,
|
|
37
|
+
RENEWAL: 0x05
|
|
38
|
+
};
|
|
39
|
+
exports.AUDIO_SAMPLE_RATE = 12000;
|
|
40
|
+
exports.TX_BUFFER_SIZE = 0xf0; // 240 samples (20ms @ 12k), 16-bit => 480 bytes
|
|
41
|
+
exports.XIEGU_TX_BUFFER_SIZE = 0x96; // for compatibility with some clients
|
|
42
|
+
// Control packet (0x10)
|
|
43
|
+
exports.ControlPacket = {
|
|
44
|
+
toBytes(type, seq, sentId, rcvdId) {
|
|
45
|
+
const buf = Buffer.alloc(exports.Sizes.CONTROL);
|
|
46
|
+
codec_1.le32.write(buf, 0, exports.Sizes.CONTROL);
|
|
47
|
+
codec_1.le16.write(buf, 4, type);
|
|
48
|
+
codec_1.le16.write(buf, 6, seq);
|
|
49
|
+
codec_1.le32.write(buf, 8, sentId);
|
|
50
|
+
codec_1.le32.write(buf, 12, rcvdId);
|
|
51
|
+
return buf;
|
|
52
|
+
},
|
|
53
|
+
getType(buf) { return codec_1.le16.read(buf, 4); },
|
|
54
|
+
getSeq(buf) { return codec_1.le16.read(buf, 6); },
|
|
55
|
+
getSentId(buf) { return codec_1.le32.read(buf, 8); },
|
|
56
|
+
getRcvdId(buf) { return codec_1.le32.read(buf, 12); },
|
|
57
|
+
setSeq(buf, seq) { codec_1.le16.write(buf, 6, seq); }
|
|
58
|
+
};
|
|
59
|
+
// Ping (0x15)
|
|
60
|
+
exports.PingPacket = {
|
|
61
|
+
isPing(buf) { return exports.ControlPacket.getType(buf) === exports.Cmd.PING; },
|
|
62
|
+
getReply(buf) { return buf[0x10]; },
|
|
63
|
+
buildPing(localId, remoteId, seq) {
|
|
64
|
+
const b = Buffer.alloc(exports.Sizes.PING);
|
|
65
|
+
codec_1.le32.write(b, 0, exports.Sizes.PING);
|
|
66
|
+
codec_1.le16.write(b, 4, exports.Cmd.PING);
|
|
67
|
+
codec_1.le16.write(b, 6, seq);
|
|
68
|
+
codec_1.le32.write(b, 8, localId);
|
|
69
|
+
codec_1.le32.write(b, 12, remoteId);
|
|
70
|
+
b[0x10] = 0x00;
|
|
71
|
+
codec_1.le32.write(b, 0x11, (Date.now() & 0xffffffff) >>> 0);
|
|
72
|
+
return b;
|
|
73
|
+
},
|
|
74
|
+
buildReply(from, localId, remoteId) {
|
|
75
|
+
const b = Buffer.alloc(exports.Sizes.PING);
|
|
76
|
+
codec_1.le32.write(b, 0, exports.Sizes.PING);
|
|
77
|
+
codec_1.le16.write(b, 4, exports.Cmd.PING);
|
|
78
|
+
b[0x6] = from[0x6];
|
|
79
|
+
b[0x7] = from[0x7];
|
|
80
|
+
codec_1.le32.write(b, 8, localId);
|
|
81
|
+
codec_1.le32.write(b, 12, remoteId);
|
|
82
|
+
b[0x10] = 0x01;
|
|
83
|
+
b[0x11] = from[0x11];
|
|
84
|
+
b[0x12] = from[0x12];
|
|
85
|
+
b[0x13] = from[0x13];
|
|
86
|
+
b[0x14] = from[0x14];
|
|
87
|
+
return b;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// Token (0x40)
|
|
91
|
+
exports.TokenPacket = {
|
|
92
|
+
build(seq, localId, remoteId, requestType, innerSeq, tokRequest, token) {
|
|
93
|
+
const b = Buffer.alloc(exports.Sizes.TOKEN);
|
|
94
|
+
codec_1.le32.write(b, 0, exports.Sizes.TOKEN);
|
|
95
|
+
codec_1.le16.write(b, 6, seq);
|
|
96
|
+
codec_1.le32.write(b, 8, localId);
|
|
97
|
+
codec_1.le32.write(b, 12, remoteId);
|
|
98
|
+
// payloadSize, innerSeq, tokRequest, token are big-endian
|
|
99
|
+
codec_1.be16.write(b, 0x12, exports.Sizes.TOKEN - 0x10);
|
|
100
|
+
b[0x14] = 0x01; // requestReply
|
|
101
|
+
b[0x15] = requestType;
|
|
102
|
+
codec_1.be16.write(b, 0x16, innerSeq);
|
|
103
|
+
codec_1.be16.write(b, 0x1a, tokRequest);
|
|
104
|
+
codec_1.be32.write(b, 0x1c, token);
|
|
105
|
+
return b;
|
|
106
|
+
},
|
|
107
|
+
getRequestType(b) { return b[0x15]; },
|
|
108
|
+
getRequestReply(b) { return b[0x14]; },
|
|
109
|
+
getResponse(b) { return codec_1.be32.read(b, 0x30); },
|
|
110
|
+
getTokRequest(b) { return codec_1.be16.read(b, 0x1a); },
|
|
111
|
+
getToken(b) { return codec_1.be32.read(b, 0x1c); }
|
|
112
|
+
};
|
|
113
|
+
// Login (0x80)
|
|
114
|
+
exports.LoginPacket = {
|
|
115
|
+
passCode(input) {
|
|
116
|
+
const sequence = Buffer.from([
|
|
117
|
+
...new Array(32).fill(0),
|
|
118
|
+
0x47, 0x5d, 0x4c, 0x42, 0x66, 0x20, 0x23, 0x46, 0x4e, 0x57, 0x45, 0x3d, 0x67, 0x76, 0x60, 0x41, 0x62, 0x39, 0x59, 0x2d, 0x68, 0x7e,
|
|
119
|
+
0x7c, 0x65, 0x7d, 0x49, 0x29, 0x72, 0x73, 0x78, 0x21, 0x6e, 0x5a, 0x5e, 0x4a, 0x3e, 0x71, 0x2c, 0x2a, 0x54, 0x3c, 0x3a, 0x63, 0x4f,
|
|
120
|
+
0x43, 0x75, 0x27, 0x79, 0x5b, 0x35, 0x70, 0x48, 0x6b, 0x56, 0x6f, 0x34, 0x32, 0x6c, 0x30, 0x61, 0x6d, 0x7b, 0x2f, 0x4b, 0x64, 0x38,
|
|
121
|
+
0x2b, 0x2e, 0x50, 0x40, 0x3f, 0x55, 0x33, 0x37, 0x25, 0x77, 0x24, 0x26, 0x74, 0x6a, 0x28, 0x53, 0x4d, 0x69, 0x22, 0x5c, 0x44, 0x31,
|
|
122
|
+
0x36, 0x58, 0x3b, 0x7a, 0x51, 0x5f, 0x52,
|
|
123
|
+
...new Array(29).fill(0)
|
|
124
|
+
]);
|
|
125
|
+
const pass = Buffer.from(input, 'utf8');
|
|
126
|
+
const out = Buffer.alloc(16, 0);
|
|
127
|
+
for (let i = 0; i < pass.length && i < 16; i++) {
|
|
128
|
+
let p = (pass[i] + i) & 0xff;
|
|
129
|
+
if (p > 126)
|
|
130
|
+
p = 32 + (p % 127);
|
|
131
|
+
out[i] = sequence[p];
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
},
|
|
135
|
+
build(seq, localId, remoteId, innerSeq, tokRequest, token, userName, password, name) {
|
|
136
|
+
const b = Buffer.alloc(exports.Sizes.LOGIN);
|
|
137
|
+
codec_1.le32.write(b, 0, exports.Sizes.LOGIN);
|
|
138
|
+
codec_1.le16.write(b, 4, 0);
|
|
139
|
+
codec_1.le16.write(b, 6, seq);
|
|
140
|
+
codec_1.le32.write(b, 8, localId);
|
|
141
|
+
codec_1.le32.write(b, 12, remoteId);
|
|
142
|
+
// payloadSize, innerSeq, tokRequest, token are big-endian
|
|
143
|
+
codec_1.be16.write(b, 0x12, exports.Sizes.LOGIN - 0x10);
|
|
144
|
+
b[0x14] = 0x01;
|
|
145
|
+
b[0x15] = 0x00;
|
|
146
|
+
codec_1.be16.write(b, 0x16, innerSeq);
|
|
147
|
+
codec_1.be16.write(b, 0x1a, tokRequest);
|
|
148
|
+
codec_1.be32.write(b, 0x1c, token);
|
|
149
|
+
exports.LoginPacket.passCode(userName).copy(b, 0x40);
|
|
150
|
+
exports.LoginPacket.passCode(password).copy(b, 0x50);
|
|
151
|
+
(0, codec_1.strToFixedBytes)(name, 16).copy(b, 0x60);
|
|
152
|
+
return b;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
exports.LoginResponsePacket = {
|
|
156
|
+
// error and token are big-endian per FT8CN
|
|
157
|
+
authOK(b) { return codec_1.be32.read(b, 0x30) === 0; },
|
|
158
|
+
errorNum(b) { return codec_1.be32.read(b, 0x30); },
|
|
159
|
+
getToken(b) { return codec_1.be32.read(b, 0x1c); },
|
|
160
|
+
getConnection(b) { return Buffer.from(b.subarray(0x40, 0x50)).toString('utf8').replace(/\0+$/, '').trim(); }
|
|
161
|
+
};
|
|
162
|
+
// Status (0x50)
|
|
163
|
+
exports.StatusPacket = {
|
|
164
|
+
// error at 0x30 is LE (Java uses readIntBigEndianData which is actually LE)
|
|
165
|
+
authOK(b) { return codec_1.le32.read(b, 0x30) === 0; },
|
|
166
|
+
// disc at 0x40 equals 0 when connected
|
|
167
|
+
getIsConnected(b) { return b[0x40] === 0x00; },
|
|
168
|
+
// civ/audio ports are 16-bit big-endian at 0x42/0x46
|
|
169
|
+
getRigCivPort(b) { return codec_1.be16.read(b, 0x42); },
|
|
170
|
+
getRigAudioPort(b) { return codec_1.be16.read(b, 0x46); }
|
|
171
|
+
};
|
|
172
|
+
// Capabilities (0xA8) => RadioCap (0x66)
|
|
173
|
+
exports.CapCapabilitiesPacket = {
|
|
174
|
+
getRadioCapPacket(b, idx) {
|
|
175
|
+
const start = exports.Sizes.CAP + exports.Sizes.RADIO_CAP * idx;
|
|
176
|
+
if (b.length < start + exports.Sizes.RADIO_CAP)
|
|
177
|
+
return null;
|
|
178
|
+
return Buffer.from(b.subarray(start, start + exports.Sizes.RADIO_CAP));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
exports.RadioCapPacket = {
|
|
182
|
+
getRigName(b) { return Buffer.from(b.subarray(0x10, 0x10 + 32)).toString('utf8').replace(/\0+$/, '').trim(); },
|
|
183
|
+
getAudioName(b) { return Buffer.from(b.subarray(0x30, 0x30 + 32)).toString('utf8').replace(/\0+$/, '').trim(); },
|
|
184
|
+
getCivAddress(b) { return b[0x52]; },
|
|
185
|
+
getRxSupportSample(b) { return codec_1.be16.read(b, 0x53); },
|
|
186
|
+
getTxSupportSample(b) { return codec_1.be16.read(b, 0x55); },
|
|
187
|
+
getSupportTX(b) { return b[0x57] === 0x01; }
|
|
188
|
+
};
|
|
189
|
+
// Civ (reply=0xC1)
|
|
190
|
+
exports.CivPacket = {
|
|
191
|
+
isCiv(b) {
|
|
192
|
+
if (b.length <= 0x15)
|
|
193
|
+
return false;
|
|
194
|
+
const len = codec_1.le16.read(b, 0x11);
|
|
195
|
+
const type = exports.ControlPacket.getType(b);
|
|
196
|
+
const expectedLen = b.length - 0x15;
|
|
197
|
+
const isValid = (expectedLen === len) && (b[0x10] === 0xc1) && (type !== exports.Cmd.RETRANSMIT);
|
|
198
|
+
// Diagnostic logging
|
|
199
|
+
if (!isValid) {
|
|
200
|
+
(0, debug_1.dbg)(`CivPacket.isCiv FAIL: bufLen=${b.length} civLen@0x11=${len} expected=${expectedLen} [0x10]=${b[0x10].toString(16)} type=${type.toString(16)}`);
|
|
201
|
+
}
|
|
202
|
+
return isValid;
|
|
203
|
+
},
|
|
204
|
+
getCivData(b) { return Buffer.from(b.subarray(0x15)); },
|
|
205
|
+
setCivData(seq, sentId, rcvdId, civSeq, data) {
|
|
206
|
+
const b = Buffer.alloc(0x15 + data.length);
|
|
207
|
+
codec_1.le32.write(b, 0, b.length);
|
|
208
|
+
codec_1.le16.write(b, 0x06, seq);
|
|
209
|
+
codec_1.le32.write(b, 0x08, sentId);
|
|
210
|
+
codec_1.le32.write(b, 0x0c, rcvdId);
|
|
211
|
+
b[0x10] = 0xc1;
|
|
212
|
+
// civ_len is little-endian
|
|
213
|
+
codec_1.le16.write(b, 0x11, data.length);
|
|
214
|
+
// civSeq is big-endian (manual write)
|
|
215
|
+
b[0x13] = (civSeq >> 8) & 0xff;
|
|
216
|
+
b[0x14] = civSeq & 0xff;
|
|
217
|
+
data.copy(b, 0x15);
|
|
218
|
+
return b;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
// Open/Close (reply=0xC0)
|
|
222
|
+
exports.OpenClosePacket = {
|
|
223
|
+
toBytes(seq, sentId, rcvdId, civSeq, magic) {
|
|
224
|
+
const b = Buffer.alloc(exports.Sizes.OPENCLOSE);
|
|
225
|
+
codec_1.le32.write(b, 0, exports.Sizes.OPENCLOSE);
|
|
226
|
+
codec_1.le16.write(b, 0x06, seq);
|
|
227
|
+
codec_1.le32.write(b, 0x08, sentId);
|
|
228
|
+
codec_1.le32.write(b, 0x0c, rcvdId);
|
|
229
|
+
b[0x10] = 0xc0;
|
|
230
|
+
// civ_len is little-endian (value 0x0001)
|
|
231
|
+
codec_1.le16.write(b, 0x11, 0x0001);
|
|
232
|
+
// civSeq is big-endian (manual write)
|
|
233
|
+
b[0x13] = (civSeq >> 8) & 0xff;
|
|
234
|
+
b[0x14] = civSeq & 0xff;
|
|
235
|
+
b[0x15] = magic & 0xff;
|
|
236
|
+
return b;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// ConnInfo (0x90): connection info exchange / request
|
|
240
|
+
exports.ConnInfoPacket = {
|
|
241
|
+
getBusy(b) { return b[0x60] !== 0x00; },
|
|
242
|
+
getMacAddress(b) { return Buffer.from(b.subarray(0x2a, 0x2a + 6)); },
|
|
243
|
+
getRigName(b) { return Buffer.from(b.subarray(0x40, 0x40 + 32)).toString('utf8').replace(/\0+$/, '').trim(); },
|
|
244
|
+
connectRequestPacket(seq, localSID, remoteSID, requestReply, requestType, innerSeq, tokRequest, token, macAddress, rigName, userName, sampleRate, civPort, audioPort, txBufferSize) {
|
|
245
|
+
const b = Buffer.alloc(exports.Sizes.CONNINFO);
|
|
246
|
+
codec_1.le32.write(b, 0, exports.Sizes.CONNINFO);
|
|
247
|
+
codec_1.le16.write(b, 4, 0);
|
|
248
|
+
codec_1.le16.write(b, 6, seq);
|
|
249
|
+
codec_1.le32.write(b, 8, localSID);
|
|
250
|
+
codec_1.le32.write(b, 12, remoteSID);
|
|
251
|
+
// payloadsize, innerSeq, tokRequest, token are big-endian
|
|
252
|
+
codec_1.be16.write(b, 0x12, exports.Sizes.CONNINFO - 0x10);
|
|
253
|
+
b[0x14] = requestReply;
|
|
254
|
+
b[0x15] = requestType;
|
|
255
|
+
codec_1.be16.write(b, 0x16, innerSeq);
|
|
256
|
+
codec_1.be16.write(b, 0x1a, tokRequest);
|
|
257
|
+
codec_1.be32.write(b, 0x1c, token);
|
|
258
|
+
// commoncap 0x1080 and macaddress
|
|
259
|
+
b[0x26] = 0x10;
|
|
260
|
+
b[0x27] = 0x80;
|
|
261
|
+
macAddress.copy(b, 0x28, 0, 6);
|
|
262
|
+
(0, codec_1.strToFixedBytes)(rigName, 32).copy(b, 0x40);
|
|
263
|
+
exports.LoginPacket.passCode(userName).copy(b, 0x60);
|
|
264
|
+
b[0x70] = 0x01;
|
|
265
|
+
b[0x71] = 0x01; // rx/tx enable
|
|
266
|
+
b[0x72] = 0x04;
|
|
267
|
+
b[0x73] = 0x04; // LPCM 1ch 16bit
|
|
268
|
+
codec_1.be32.write(b, 0x74, sampleRate);
|
|
269
|
+
codec_1.be32.write(b, 0x78, sampleRate);
|
|
270
|
+
codec_1.be32.write(b, 0x7c, civPort);
|
|
271
|
+
codec_1.be32.write(b, 0x80, audioPort);
|
|
272
|
+
codec_1.be32.write(b, 0x84, txBufferSize);
|
|
273
|
+
b[0x88] = 0x01;
|
|
274
|
+
return b;
|
|
275
|
+
},
|
|
276
|
+
connInfoPacketData(rigData, seq, localSID, remoteSID, requestReply, requestType, innerSeq, tokRequest, token, rigName, userName, rxSampleRate, txSampleRate, civPort, audioPort, txBufferSize) {
|
|
277
|
+
const b = Buffer.alloc(exports.Sizes.CONNINFO);
|
|
278
|
+
codec_1.le32.write(b, 0, exports.Sizes.CONNINFO);
|
|
279
|
+
codec_1.le16.write(b, 4, 0);
|
|
280
|
+
codec_1.le16.write(b, 6, seq);
|
|
281
|
+
codec_1.le32.write(b, 8, localSID);
|
|
282
|
+
codec_1.le32.write(b, 12, remoteSID);
|
|
283
|
+
codec_1.be16.write(b, 0x12, exports.Sizes.CONNINFO - 0x10);
|
|
284
|
+
b[0x14] = requestReply;
|
|
285
|
+
b[0x15] = requestType;
|
|
286
|
+
codec_1.be16.write(b, 0x16, innerSeq);
|
|
287
|
+
codec_1.be16.write(b, 0x1a, tokRequest);
|
|
288
|
+
codec_1.be32.write(b, 0x1c, token);
|
|
289
|
+
// copy device fields from rig packet
|
|
290
|
+
rigData.subarray(32, 64).copy(b, 32);
|
|
291
|
+
(0, codec_1.strToFixedBytes)(rigName, 32).copy(b, 0x40);
|
|
292
|
+
exports.LoginPacket.passCode(userName).copy(b, 0x60);
|
|
293
|
+
b[0x70] = 0x01;
|
|
294
|
+
b[0x71] = 0x01;
|
|
295
|
+
b[0x72] = 0x04;
|
|
296
|
+
b[0x73] = 0x04;
|
|
297
|
+
codec_1.be32.write(b, 0x74, rxSampleRate);
|
|
298
|
+
codec_1.be32.write(b, 0x78, txSampleRate);
|
|
299
|
+
codec_1.be32.write(b, 0x7c, civPort);
|
|
300
|
+
codec_1.be32.write(b, 0x80, audioPort);
|
|
301
|
+
codec_1.be32.write(b, 0x84, txBufferSize);
|
|
302
|
+
b[0x88] = 0x01;
|
|
303
|
+
return b;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
// Audio
|
|
307
|
+
exports.AudioPacket = {
|
|
308
|
+
isAudioPacket(b) {
|
|
309
|
+
if (b.length < exports.Sizes.AUDIO_HEAD)
|
|
310
|
+
return false;
|
|
311
|
+
// datalen is big-endian
|
|
312
|
+
return b.length - exports.Sizes.AUDIO_HEAD === codec_1.be16.read(b, 0x16);
|
|
313
|
+
},
|
|
314
|
+
getAudioData(b) { return Buffer.from(b.subarray(0x18)); },
|
|
315
|
+
getTxAudioPacket(audio, seq, sentId, rcvdId, sendSeq) {
|
|
316
|
+
const b = Buffer.alloc(exports.Sizes.AUDIO_HEAD + audio.length);
|
|
317
|
+
codec_1.le32.write(b, 0, b.length);
|
|
318
|
+
codec_1.le16.write(b, 0x06, seq);
|
|
319
|
+
codec_1.le32.write(b, 0x08, sentId);
|
|
320
|
+
codec_1.le32.write(b, 0x0c, rcvdId);
|
|
321
|
+
const ident = (audio.length === 0xa0) ? 0x8197 : 0x8000;
|
|
322
|
+
// ident is big-endian (manual write)
|
|
323
|
+
b[0x10] = (ident >> 8) & 0xff;
|
|
324
|
+
b[0x11] = ident & 0xff;
|
|
325
|
+
// sendseq is big-endian (manual write)
|
|
326
|
+
b[0x12] = (sendSeq >> 8) & 0xff;
|
|
327
|
+
b[0x13] = sendSeq & 0xff;
|
|
328
|
+
// datalen is big-endian
|
|
329
|
+
codec_1.be16.write(b, 0x16, audio.length);
|
|
330
|
+
audio.copy(b, 0x18);
|
|
331
|
+
return b;
|
|
332
|
+
}
|
|
333
|
+
};
|