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 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
+ };