icom-wlan-node
Version:
Icom WLAN (CI‑V, audio) protocol implementation for Node.js/TypeScript.
334 lines (333 loc) • 14.1 kB
JavaScript
;
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;
}
};