UNPKG

icom-wlan-node

Version:

Icom WLAN (CI‑V, audio) protocol implementation for Node.js/TypeScript.

334 lines (333 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; const codec_1 = require("../utils/codec"); const debug_1 = require("../utils/debug"); // Constants derived from Android implementation and known Icom behavior exports.Sizes = { CONTROL: 0x10, WATCHDOG: 0x14, PING: 0x15, OPENCLOSE: 0x16, RETRANSMIT_RANGE: 0x18, TOKEN: 0x40, STATUS: 0x50, LOGIN_RESPONSE: 0x60, LOGIN: 0x80, CONNINFO: 0x90, CAP: 0x42, RADIO_CAP: 0x66, CAP_CAP: 0xA8, AUDIO_HEAD: 0x18 }; exports.Cmd = { NULL: 0x00, RETRANSMIT: 0x01, ARE_YOU_THERE: 0x03, I_AM_HERE: 0x04, DISCONNECT: 0x05, ARE_YOU_READY: 0x06, I_AM_READY: 0x06, PING: 0x07 }; exports.TokenType = { DELETE: 0x01, CONFIRM: 0x02, DISCONNECT: 0x04, RENEWAL: 0x05 }; exports.AUDIO_SAMPLE_RATE = 12000; exports.TX_BUFFER_SIZE = 0xf0; // 240 samples (20ms @ 12k), 16-bit => 480 bytes exports.XIEGU_TX_BUFFER_SIZE = 0x96; // for compatibility with some clients // Control packet (0x10) exports.ControlPacket = { toBytes(type, seq, sentId, rcvdId) { const buf = Buffer.alloc(exports.Sizes.CONTROL); codec_1.le32.write(buf, 0, exports.Sizes.CONTROL); codec_1.le16.write(buf, 4, type); codec_1.le16.write(buf, 6, seq); codec_1.le32.write(buf, 8, sentId); codec_1.le32.write(buf, 12, rcvdId); return buf; }, getType(buf) { return codec_1.le16.read(buf, 4); }, getSeq(buf) { return codec_1.le16.read(buf, 6); }, getSentId(buf) { return codec_1.le32.read(buf, 8); }, getRcvdId(buf) { return codec_1.le32.read(buf, 12); }, setSeq(buf, seq) { codec_1.le16.write(buf, 6, seq); } }; // Ping (0x15) exports.PingPacket = { isPing(buf) { return exports.ControlPacket.getType(buf) === exports.Cmd.PING; }, getReply(buf) { return buf[0x10]; }, buildPing(localId, remoteId, seq) { const b = Buffer.alloc(exports.Sizes.PING); codec_1.le32.write(b, 0, exports.Sizes.PING); codec_1.le16.write(b, 4, exports.Cmd.PING); codec_1.le16.write(b, 6, seq); codec_1.le32.write(b, 8, localId); codec_1.le32.write(b, 12, remoteId); b[0x10] = 0x00; codec_1.le32.write(b, 0x11, (Date.now() & 0xffffffff) >>> 0); return b; }, buildReply(from, localId, remoteId) { const b = Buffer.alloc(exports.Sizes.PING); codec_1.le32.write(b, 0, exports.Sizes.PING); codec_1.le16.write(b, 4, exports.Cmd.PING); b[0x6] = from[0x6]; b[0x7] = from[0x7]; codec_1.le32.write(b, 8, localId); codec_1.le32.write(b, 12, remoteId); b[0x10] = 0x01; b[0x11] = from[0x11]; b[0x12] = from[0x12]; b[0x13] = from[0x13]; b[0x14] = from[0x14]; return b; } }; // Token (0x40) exports.TokenPacket = { build(seq, localId, remoteId, requestType, innerSeq, tokRequest, token) { const b = Buffer.alloc(exports.Sizes.TOKEN); codec_1.le32.write(b, 0, exports.Sizes.TOKEN); codec_1.le16.write(b, 6, seq); codec_1.le32.write(b, 8, localId); codec_1.le32.write(b, 12, remoteId); // payloadSize, innerSeq, tokRequest, token are big-endian codec_1.be16.write(b, 0x12, exports.Sizes.TOKEN - 0x10); b[0x14] = 0x01; // requestReply b[0x15] = requestType; codec_1.be16.write(b, 0x16, innerSeq); codec_1.be16.write(b, 0x1a, tokRequest); codec_1.be32.write(b, 0x1c, token); return b; }, getRequestType(b) { return b[0x15]; }, getRequestReply(b) { return b[0x14]; }, getResponse(b) { return codec_1.be32.read(b, 0x30); }, getTokRequest(b) { return codec_1.be16.read(b, 0x1a); }, getToken(b) { return codec_1.be32.read(b, 0x1c); } }; // Login (0x80) exports.LoginPacket = { passCode(input) { const sequence = Buffer.from([ ...new Array(32).fill(0), 0x47, 0x5d, 0x4c, 0x42, 0x66, 0x20, 0x23, 0x46, 0x4e, 0x57, 0x45, 0x3d, 0x67, 0x76, 0x60, 0x41, 0x62, 0x39, 0x59, 0x2d, 0x68, 0x7e, 0x7c, 0x65, 0x7d, 0x49, 0x29, 0x72, 0x73, 0x78, 0x21, 0x6e, 0x5a, 0x5e, 0x4a, 0x3e, 0x71, 0x2c, 0x2a, 0x54, 0x3c, 0x3a, 0x63, 0x4f, 0x43, 0x75, 0x27, 0x79, 0x5b, 0x35, 0x70, 0x48, 0x6b, 0x56, 0x6f, 0x34, 0x32, 0x6c, 0x30, 0x61, 0x6d, 0x7b, 0x2f, 0x4b, 0x64, 0x38, 0x2b, 0x2e, 0x50, 0x40, 0x3f, 0x55, 0x33, 0x37, 0x25, 0x77, 0x24, 0x26, 0x74, 0x6a, 0x28, 0x53, 0x4d, 0x69, 0x22, 0x5c, 0x44, 0x31, 0x36, 0x58, 0x3b, 0x7a, 0x51, 0x5f, 0x52, ...new Array(29).fill(0) ]); const pass = Buffer.from(input, 'utf8'); const out = Buffer.alloc(16, 0); for (let i = 0; i < pass.length && i < 16; i++) { let p = (pass[i] + i) & 0xff; if (p > 126) p = 32 + (p % 127); out[i] = sequence[p]; } return out; }, build(seq, localId, remoteId, innerSeq, tokRequest, token, userName, password, name) { const b = Buffer.alloc(exports.Sizes.LOGIN); codec_1.le32.write(b, 0, exports.Sizes.LOGIN); codec_1.le16.write(b, 4, 0); codec_1.le16.write(b, 6, seq); codec_1.le32.write(b, 8, localId); codec_1.le32.write(b, 12, remoteId); // payloadSize, innerSeq, tokRequest, token are big-endian codec_1.be16.write(b, 0x12, exports.Sizes.LOGIN - 0x10); b[0x14] = 0x01; b[0x15] = 0x00; codec_1.be16.write(b, 0x16, innerSeq); codec_1.be16.write(b, 0x1a, tokRequest); codec_1.be32.write(b, 0x1c, token); exports.LoginPacket.passCode(userName).copy(b, 0x40); exports.LoginPacket.passCode(password).copy(b, 0x50); (0, codec_1.strToFixedBytes)(name, 16).copy(b, 0x60); return b; } }; exports.LoginResponsePacket = { // error and token are big-endian per FT8CN authOK(b) { return codec_1.be32.read(b, 0x30) === 0; }, errorNum(b) { return codec_1.be32.read(b, 0x30); }, getToken(b) { return codec_1.be32.read(b, 0x1c); }, getConnection(b) { return Buffer.from(b.subarray(0x40, 0x50)).toString('utf8').replace(/\0+$/, '').trim(); } }; // Status (0x50) exports.StatusPacket = { // error at 0x30 is LE (Java uses readIntBigEndianData which is actually LE) authOK(b) { return codec_1.le32.read(b, 0x30) === 0; }, // disc at 0x40 equals 0 when connected getIsConnected(b) { return b[0x40] === 0x00; }, // civ/audio ports are 16-bit big-endian at 0x42/0x46 getRigCivPort(b) { return codec_1.be16.read(b, 0x42); }, getRigAudioPort(b) { return codec_1.be16.read(b, 0x46); } }; // Capabilities (0xA8) => RadioCap (0x66) exports.CapCapabilitiesPacket = { getRadioCapPacket(b, idx) { const start = exports.Sizes.CAP + exports.Sizes.RADIO_CAP * idx; if (b.length < start + exports.Sizes.RADIO_CAP) return null; return Buffer.from(b.subarray(start, start + exports.Sizes.RADIO_CAP)); } }; exports.RadioCapPacket = { getRigName(b) { return Buffer.from(b.subarray(0x10, 0x10 + 32)).toString('utf8').replace(/\0+$/, '').trim(); }, getAudioName(b) { return Buffer.from(b.subarray(0x30, 0x30 + 32)).toString('utf8').replace(/\0+$/, '').trim(); }, getCivAddress(b) { return b[0x52]; }, getRxSupportSample(b) { return codec_1.be16.read(b, 0x53); }, getTxSupportSample(b) { return codec_1.be16.read(b, 0x55); }, getSupportTX(b) { return b[0x57] === 0x01; } }; // Civ (reply=0xC1) exports.CivPacket = { isCiv(b) { if (b.length <= 0x15) return false; const len = codec_1.le16.read(b, 0x11); const type = exports.ControlPacket.getType(b); const expectedLen = b.length - 0x15; const isValid = (expectedLen === len) && (b[0x10] === 0xc1) && (type !== exports.Cmd.RETRANSMIT); // Diagnostic logging if (!isValid) { (0, debug_1.dbg)(`CivPacket.isCiv FAIL: bufLen=${b.length} civLen@0x11=${len} expected=${expectedLen} [0x10]=${b[0x10].toString(16)} type=${type.toString(16)}`); } return isValid; }, getCivData(b) { return Buffer.from(b.subarray(0x15)); }, setCivData(seq, sentId, rcvdId, civSeq, data) { const b = Buffer.alloc(0x15 + data.length); codec_1.le32.write(b, 0, b.length); codec_1.le16.write(b, 0x06, seq); codec_1.le32.write(b, 0x08, sentId); codec_1.le32.write(b, 0x0c, rcvdId); b[0x10] = 0xc1; // civ_len is little-endian codec_1.le16.write(b, 0x11, data.length); // civSeq is big-endian (manual write) b[0x13] = (civSeq >> 8) & 0xff; b[0x14] = civSeq & 0xff; data.copy(b, 0x15); return b; } }; // Open/Close (reply=0xC0) exports.OpenClosePacket = { toBytes(seq, sentId, rcvdId, civSeq, magic) { const b = Buffer.alloc(exports.Sizes.OPENCLOSE); codec_1.le32.write(b, 0, exports.Sizes.OPENCLOSE); codec_1.le16.write(b, 0x06, seq); codec_1.le32.write(b, 0x08, sentId); codec_1.le32.write(b, 0x0c, rcvdId); b[0x10] = 0xc0; // civ_len is little-endian (value 0x0001) codec_1.le16.write(b, 0x11, 0x0001); // civSeq is big-endian (manual write) b[0x13] = (civSeq >> 8) & 0xff; b[0x14] = civSeq & 0xff; b[0x15] = magic & 0xff; return b; } }; // ConnInfo (0x90): connection info exchange / request exports.ConnInfoPacket = { getBusy(b) { return b[0x60] !== 0x00; }, getMacAddress(b) { return Buffer.from(b.subarray(0x2a, 0x2a + 6)); }, getRigName(b) { return Buffer.from(b.subarray(0x40, 0x40 + 32)).toString('utf8').replace(/\0+$/, '').trim(); }, connectRequestPacket(seq, localSID, remoteSID, requestReply, requestType, innerSeq, tokRequest, token, macAddress, rigName, userName, sampleRate, civPort, audioPort, txBufferSize) { const b = Buffer.alloc(exports.Sizes.CONNINFO); codec_1.le32.write(b, 0, exports.Sizes.CONNINFO); codec_1.le16.write(b, 4, 0); codec_1.le16.write(b, 6, seq); codec_1.le32.write(b, 8, localSID); codec_1.le32.write(b, 12, remoteSID); // payloadsize, innerSeq, tokRequest, token are big-endian codec_1.be16.write(b, 0x12, exports.Sizes.CONNINFO - 0x10); b[0x14] = requestReply; b[0x15] = requestType; codec_1.be16.write(b, 0x16, innerSeq); codec_1.be16.write(b, 0x1a, tokRequest); codec_1.be32.write(b, 0x1c, token); // commoncap 0x1080 and macaddress b[0x26] = 0x10; b[0x27] = 0x80; macAddress.copy(b, 0x28, 0, 6); (0, codec_1.strToFixedBytes)(rigName, 32).copy(b, 0x40); exports.LoginPacket.passCode(userName).copy(b, 0x60); b[0x70] = 0x01; b[0x71] = 0x01; // rx/tx enable b[0x72] = 0x04; b[0x73] = 0x04; // LPCM 1ch 16bit codec_1.be32.write(b, 0x74, sampleRate); codec_1.be32.write(b, 0x78, sampleRate); codec_1.be32.write(b, 0x7c, civPort); codec_1.be32.write(b, 0x80, audioPort); codec_1.be32.write(b, 0x84, txBufferSize); b[0x88] = 0x01; return b; }, connInfoPacketData(rigData, seq, localSID, remoteSID, requestReply, requestType, innerSeq, tokRequest, token, rigName, userName, rxSampleRate, txSampleRate, civPort, audioPort, txBufferSize) { const b = Buffer.alloc(exports.Sizes.CONNINFO); codec_1.le32.write(b, 0, exports.Sizes.CONNINFO); codec_1.le16.write(b, 4, 0); codec_1.le16.write(b, 6, seq); codec_1.le32.write(b, 8, localSID); codec_1.le32.write(b, 12, remoteSID); codec_1.be16.write(b, 0x12, exports.Sizes.CONNINFO - 0x10); b[0x14] = requestReply; b[0x15] = requestType; codec_1.be16.write(b, 0x16, innerSeq); codec_1.be16.write(b, 0x1a, tokRequest); codec_1.be32.write(b, 0x1c, token); // copy device fields from rig packet rigData.subarray(32, 64).copy(b, 32); (0, codec_1.strToFixedBytes)(rigName, 32).copy(b, 0x40); exports.LoginPacket.passCode(userName).copy(b, 0x60); b[0x70] = 0x01; b[0x71] = 0x01; b[0x72] = 0x04; b[0x73] = 0x04; codec_1.be32.write(b, 0x74, rxSampleRate); codec_1.be32.write(b, 0x78, txSampleRate); codec_1.be32.write(b, 0x7c, civPort); codec_1.be32.write(b, 0x80, audioPort); codec_1.be32.write(b, 0x84, txBufferSize); b[0x88] = 0x01; return b; } }; // Audio exports.AudioPacket = { isAudioPacket(b) { if (b.length < exports.Sizes.AUDIO_HEAD) return false; // datalen is big-endian return b.length - exports.Sizes.AUDIO_HEAD === codec_1.be16.read(b, 0x16); }, getAudioData(b) { return Buffer.from(b.subarray(0x18)); }, getTxAudioPacket(audio, seq, sentId, rcvdId, sendSeq) { const b = Buffer.alloc(exports.Sizes.AUDIO_HEAD + audio.length); codec_1.le32.write(b, 0, b.length); codec_1.le16.write(b, 0x06, seq); codec_1.le32.write(b, 0x08, sentId); codec_1.le32.write(b, 0x0c, rcvdId); const ident = (audio.length === 0xa0) ? 0x8197 : 0x8000; // ident is big-endian (manual write) b[0x10] = (ident >> 8) & 0xff; b[0x11] = ident & 0xff; // sendseq is big-endian (manual write) b[0x12] = (sendSeq >> 8) & 0xff; b[0x13] = sendSeq & 0xff; // datalen is big-endian codec_1.be16.write(b, 0x16, audio.length); audio.copy(b, 0x18); return b; } };