diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a23e760 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/package-lock.json b/package-lock.json index 80cb09b..b2b74d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "dependencies": { "@noble/curves": "^1.8.1", "serialport": "^13.0.0" + }, + "devDependencies": { + "prettier": "^3.6.2" } }, "node_modules/@noble/curves": { @@ -266,6 +269,22 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/serialport": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", diff --git a/package.json b/package.json index 6aae9f0..5520fd7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "main": "src/index.js", "type": "module", "scripts": { + "prettier": "prettier --check src", + "prettier-write": "prettier --write src", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Liam Cottle ", @@ -12,5 +14,8 @@ "dependencies": { "@noble/curves": "^1.8.1", "serialport": "^13.0.0" + }, + "devDependencies": { + "prettier": "^3.6.2" } } diff --git a/src/advert.js b/src/advert.js index 8e4bed9..03972d6 100644 --- a/src/advert.js +++ b/src/advert.js @@ -1,101 +1,97 @@ -import BufferReader from "./buffer_reader.js"; -import BufferWriter from "./buffer_writer.js"; +import BufferReader from './buffer_reader.js'; +import BufferWriter from './buffer_writer.js'; class Advert { - - static ADV_TYPE_NONE = 0; - static ADV_TYPE_CHAT = 1; - static ADV_TYPE_REPEATER = 2; - static ADV_TYPE_ROOM = 3; - - static ADV_LATLON_MASK = 0x10; - static ADV_BATTERY_MASK = 0x20; - static ADV_TEMPERATURE_MASK = 0x40; - static ADV_NAME_MASK = 0x80; - - constructor(publicKey, timestamp, signature, appData) { - this.publicKey = publicKey; - this.timestamp = timestamp; - this.signature = signature; - this.appData = appData; - this.parsed = this.parseAppData(); - } - - static fromBytes(bytes) { - - // read bytes - const bufferReader = new BufferReader(bytes); - const publicKey = bufferReader.readBytes(32); - const timestamp = bufferReader.readUInt32LE(); - const signature = bufferReader.readBytes(64); - const appData = bufferReader.readRemainingBytes(); - - return new Advert(publicKey, timestamp, signature, appData); - - } - - getFlags() { - return this.appData[0]; - } - - getType() { - const flags = this.getFlags(); - return flags & 0x0F; + static ADV_TYPE_NONE = 0; + static ADV_TYPE_CHAT = 1; + static ADV_TYPE_REPEATER = 2; + static ADV_TYPE_ROOM = 3; + + static ADV_LATLON_MASK = 0x10; + static ADV_BATTERY_MASK = 0x20; + static ADV_TEMPERATURE_MASK = 0x40; + static ADV_NAME_MASK = 0x80; + + constructor(publicKey, timestamp, signature, appData) { + this.publicKey = publicKey; + this.timestamp = timestamp; + this.signature = signature; + this.appData = appData; + this.parsed = this.parseAppData(); + } + + static fromBytes(bytes) { + // read bytes + const bufferReader = new BufferReader(bytes); + const publicKey = bufferReader.readBytes(32); + const timestamp = bufferReader.readUInt32LE(); + const signature = bufferReader.readBytes(64); + const appData = bufferReader.readRemainingBytes(); + + return new Advert(publicKey, timestamp, signature, appData); + } + + getFlags() { + return this.appData[0]; + } + + getType() { + const flags = this.getFlags(); + return flags & 0x0f; + } + + getTypeString() { + const type = this.getType(); + if (type === Advert.ADV_TYPE_NONE) return 'NONE'; + if (type === Advert.ADV_TYPE_CHAT) return 'CHAT'; + if (type === Advert.ADV_TYPE_REPEATER) return 'REPEATER'; + if (type === Advert.ADV_TYPE_ROOM) return 'ROOM'; + return null; + } + + async isVerified() { + const { ed25519 } = await import('@noble/curves/ed25519'); + + // build signed data + const bufferWriter = new BufferWriter(); + bufferWriter.writeBytes(this.publicKey); + bufferWriter.writeUInt32LE(this.timestamp); + bufferWriter.writeBytes(this.appData); + + // verify signature + return ed25519.verify( + this.signature, + bufferWriter.toBytes(), + this.publicKey, + ); + } + + parseAppData() { + // read app data + const bufferReader = new BufferReader(this.appData); + const flags = bufferReader.readByte(); + + // parse lat lon + var lat = null; + var lon = null; + if (flags & Advert.ADV_LATLON_MASK) { + lat = bufferReader.readInt32LE(); + lon = bufferReader.readInt32LE(); } - getTypeString() { - const type = this.getType(); - if(type === Advert.ADV_TYPE_NONE) return "NONE"; - if(type === Advert.ADV_TYPE_CHAT) return "CHAT"; - if(type === Advert.ADV_TYPE_REPEATER) return "REPEATER"; - if(type === Advert.ADV_TYPE_ROOM) return "ROOM"; - return null; - } - - async isVerified() { - - const { ed25519 } = await import("@noble/curves/ed25519"); - - // build signed data - const bufferWriter = new BufferWriter(); - bufferWriter.writeBytes(this.publicKey); - bufferWriter.writeUInt32LE(this.timestamp); - bufferWriter.writeBytes(this.appData); - - // verify signature - return ed25519.verify(this.signature, bufferWriter.toBytes(), this.publicKey); - - } - - parseAppData() { - - // read app data - const bufferReader = new BufferReader(this.appData); - const flags = bufferReader.readByte(); - - // parse lat lon - var lat = null; - var lon = null; - if(flags & Advert.ADV_LATLON_MASK){ - lat = bufferReader.readInt32LE(); - lon = bufferReader.readInt32LE(); - } - - // parse name (remainder of app data) - var name = null; - if(flags & Advert.ADV_NAME_MASK){ - name = bufferReader.readString(); - } - - return { - type: this.getTypeString(), - lat: lat, - lon: lon, - name: name, - }; - + // parse name (remainder of app data) + var name = null; + if (flags & Advert.ADV_NAME_MASK) { + name = bufferReader.readString(); } + return { + type: this.getTypeString(), + lat: lat, + lon: lon, + name: name, + }; + } } export default Advert; diff --git a/src/buffer_reader.js b/src/buffer_reader.js index 9142fbc..b55a1dc 100644 --- a/src/buffer_reader.js +++ b/src/buffer_reader.js @@ -1,118 +1,113 @@ class BufferReader { - - constructor(data) { - this.pointer = 0; - this.buffer = new Uint8Array(data); - } - - getRemainingBytesCount() { - return this.buffer.length - this.pointer; - } - - readByte() { - return this.readBytes(1)[0]; - } - - readBytes(count) { - const data = this.buffer.slice(this.pointer, this.pointer + count); - this.pointer += count; - return data; - } - - readRemainingBytes() { - return this.readBytes(this.getRemainingBytesCount()); - } - - readString() { - return new TextDecoder().decode(this.readRemainingBytes()); - } - - readCString(maxLength) { - const value = []; - const bytes = this.readBytes(maxLength); - for(const byte of bytes){ - - // if we find a null terminator character, we have reached the end of the cstring - if(byte === 0){ - return new TextDecoder().decode(new Uint8Array(value)); - } - - value.push(byte); - - } - } - - readInt8() { - const bytes = this.readBytes(1); - const view = new DataView(bytes.buffer); - return view.getInt8(0); - } - - readUInt8() { - const bytes = this.readBytes(1); - const view = new DataView(bytes.buffer); - return view.getUint8(0); - } - - readUInt16LE() { - const bytes = this.readBytes(2); - const view = new DataView(bytes.buffer); - return view.getUint16(0, true); - } - - readUInt16BE() { - const bytes = this.readBytes(2); - const view = new DataView(bytes.buffer); - return view.getUint16(0, false); - } - - readUInt32LE() { - const bytes = this.readBytes(4); - const view = new DataView(bytes.buffer); - return view.getUint32(0, true); - } - - readUInt32BE() { - const bytes = this.readBytes(4); - const view = new DataView(bytes.buffer); - return view.getUint32(0, false); - } - - readInt16LE() { - const bytes = this.readBytes(2); - const view = new DataView(bytes.buffer); - return view.getInt16(0, true); - } - - readInt16BE() { - const bytes = this.readBytes(2); - const view = new DataView(bytes.buffer); - return view.getInt16(0, false); - } - - readInt32LE() { - const bytes = this.readBytes(4); - const view = new DataView(bytes.buffer); - return view.getInt32(0, true); - } - - readInt24BE() { - - // read 24-bit (3 bytes) big endian integer - var value = (this.readByte() << 16) | (this.readByte() << 8) | this.readByte(); - - // convert 24-bit signed integer to 32-bit signed integer - // 0x800000 is the sign bit for a 24-bit value - // if it's set, value is negative in 24-bit two's complement - // so we subtract 0x1000000 (which is 2^24) to get the correct negative value as a Dart integer - if((value & 0x800000) !== 0){ - value -= 0x1000000; - } - - return value; - - } - + constructor(data) { + this.pointer = 0; + this.buffer = new Uint8Array(data); + } + + getRemainingBytesCount() { + return this.buffer.length - this.pointer; + } + + readByte() { + return this.readBytes(1)[0]; + } + + readBytes(count) { + const data = this.buffer.slice(this.pointer, this.pointer + count); + this.pointer += count; + return data; + } + + readRemainingBytes() { + return this.readBytes(this.getRemainingBytesCount()); + } + + readString() { + return new TextDecoder().decode(this.readRemainingBytes()); + } + + readCString(maxLength) { + const value = []; + const bytes = this.readBytes(maxLength); + for (const byte of bytes) { + // if we find a null terminator character, we have reached the end of the cstring + if (byte === 0) { + return new TextDecoder().decode(new Uint8Array(value)); + } + + value.push(byte); + } + } + + readInt8() { + const bytes = this.readBytes(1); + const view = new DataView(bytes.buffer); + return view.getInt8(0); + } + + readUInt8() { + const bytes = this.readBytes(1); + const view = new DataView(bytes.buffer); + return view.getUint8(0); + } + + readUInt16LE() { + const bytes = this.readBytes(2); + const view = new DataView(bytes.buffer); + return view.getUint16(0, true); + } + + readUInt16BE() { + const bytes = this.readBytes(2); + const view = new DataView(bytes.buffer); + return view.getUint16(0, false); + } + + readUInt32LE() { + const bytes = this.readBytes(4); + const view = new DataView(bytes.buffer); + return view.getUint32(0, true); + } + + readUInt32BE() { + const bytes = this.readBytes(4); + const view = new DataView(bytes.buffer); + return view.getUint32(0, false); + } + + readInt16LE() { + const bytes = this.readBytes(2); + const view = new DataView(bytes.buffer); + return view.getInt16(0, true); + } + + readInt16BE() { + const bytes = this.readBytes(2); + const view = new DataView(bytes.buffer); + return view.getInt16(0, false); + } + + readInt32LE() { + const bytes = this.readBytes(4); + const view = new DataView(bytes.buffer); + return view.getInt32(0, true); + } + + readInt24BE() { + // read 24-bit (3 bytes) big endian integer + var value = + (this.readByte() << 16) | (this.readByte() << 8) | this.readByte(); + + // convert 24-bit signed integer to 32-bit signed integer + // 0x800000 is the sign bit for a 24-bit value + // if it's set, value is negative in 24-bit two's complement + // so we subtract 0x1000000 (which is 2^24) to get the correct negative value as a Dart integer + if ((value & 0x800000) !== 0) { + value -= 0x1000000; + } + + return value; + } } export default BufferReader; diff --git a/src/buffer_utils.js b/src/buffer_utils.js index d5ff14f..02a0e39 100644 --- a/src/buffer_utils.js +++ b/src/buffer_utils.js @@ -1,40 +1,40 @@ class BufferUtils { - - static bytesToHex(uint8Array) { - return Array.from(uint8Array).map(byte => { - return byte.toString(16).padStart(2, '0'); - }).join(''); - } - - static hexToBytes(hex) { - return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); - } - - static base64ToBytes(base64) { - return Uint8Array.from(atob(base64), (c) => { - return c.charCodeAt(0); - }); + static bytesToHex(uint8Array) { + return Array.from(uint8Array) + .map((byte) => { + return byte.toString(16).padStart(2, '0'); + }) + .join(''); + } + + static hexToBytes(hex) { + return Uint8Array.from( + hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)), + ); + } + + static base64ToBytes(base64) { + return Uint8Array.from(atob(base64), (c) => { + return c.charCodeAt(0); + }); + } + + static areBuffersEqual(byteArray1, byteArray2) { + // ensure length is the same + if (byteArray1.length !== byteArray2.length) { + return false; } - static areBuffersEqual(byteArray1, byteArray2) { - - // ensure length is the same - if(byteArray1.length !== byteArray2.length){ - return false; - } - - // ensure each item is the same - for(let i = 0; i < byteArray1.length; i++){ - if(byteArray1[i] !== byteArray2[i]){ - return false; - } - } - - // arrays are the same - return true; - + // ensure each item is the same + for (let i = 0; i < byteArray1.length; i++) { + if (byteArray1[i] !== byteArray2[i]) { + return false; + } } + // arrays are the same + return true; + } } export default BufferUtils; diff --git a/src/buffer_writer.js b/src/buffer_writer.js index a12cb2d..e085ec9 100644 --- a/src/buffer_writer.js +++ b/src/buffer_writer.js @@ -1,72 +1,63 @@ class BufferWriter { - - constructor() { - this.buffer = []; - } - - toBytes() { - return new Uint8Array(this.buffer); - } - - writeBytes(bytes) { - this.buffer = [ - ...this.buffer, - ...bytes, - ]; - } - - writeByte(byte) { - this.writeBytes([ - byte, - ]); - } - - writeUInt16LE(num) { - const bytes = new Uint8Array(2); - const view = new DataView(bytes.buffer); - view.setUint16(0, num, true); - this.writeBytes(bytes); - } - - writeUInt32LE(num) { - const bytes = new Uint8Array(4); - const view = new DataView(bytes.buffer); - view.setUint32(0, num, true); - this.writeBytes(bytes); - } - - writeInt32LE(num) { - const bytes = new Uint8Array(4); - const view = new DataView(bytes.buffer); - view.setInt32(0, num, true); - this.writeBytes(bytes); - } - - writeString(string) { - this.writeBytes(new TextEncoder().encode(string)); - } - - writeCString(string, maxLength) { - - // create buffer of max length - const bytes = new Uint8Array(new ArrayBuffer(maxLength)); - - // encode string to bytes - const encodedString = new TextEncoder().encode(string); - - // copy in string until we hit the max length, or we run out of string bytes - for(var i = 0; i < maxLength && i < encodedString.length; i++){ - bytes[i] = encodedString[i]; - } - - // ensure the last byte is always a null terminator - bytes[bytes.length - 1] = 0; - - // write to buffer - this.writeBytes(bytes); - - } - + constructor() { + this.buffer = []; + } + + toBytes() { + return new Uint8Array(this.buffer); + } + + writeBytes(bytes) { + this.buffer = [...this.buffer, ...bytes]; + } + + writeByte(byte) { + this.writeBytes([byte]); + } + + writeUInt16LE(num) { + const bytes = new Uint8Array(2); + const view = new DataView(bytes.buffer); + view.setUint16(0, num, true); + this.writeBytes(bytes); + } + + writeUInt32LE(num) { + const bytes = new Uint8Array(4); + const view = new DataView(bytes.buffer); + view.setUint32(0, num, true); + this.writeBytes(bytes); + } + + writeInt32LE(num) { + const bytes = new Uint8Array(4); + const view = new DataView(bytes.buffer); + view.setInt32(0, num, true); + this.writeBytes(bytes); + } + + writeString(string) { + this.writeBytes(new TextEncoder().encode(string)); + } + + writeCString(string, maxLength) { + // create buffer of max length + const bytes = new Uint8Array(new ArrayBuffer(maxLength)); + + // encode string to bytes + const encodedString = new TextEncoder().encode(string); + + // copy in string until we hit the max length, or we run out of string bytes + for (var i = 0; i < maxLength && i < encodedString.length; i++) { + bytes[i] = encodedString[i]; + } + + // ensure the last byte is always a null terminator + bytes[bytes.length - 1] = 0; + + // write to buffer + this.writeBytes(bytes); + } } export default BufferWriter; diff --git a/src/cayenne_lpp.js b/src/cayenne_lpp.js index ab8cab3..17371bf 100644 --- a/src/cayenne_lpp.js +++ b/src/cayenne_lpp.js @@ -1,196 +1,192 @@ -import BufferReader from "./buffer_reader.js"; +import BufferReader from './buffer_reader.js'; class CayenneLpp { + static LPP_DIGITAL_INPUT = 0; // 1 byte + static LPP_DIGITAL_OUTPUT = 1; // 1 byte + static LPP_ANALOG_INPUT = 2; // 2 bytes, 0.01 signed + static LPP_ANALOG_OUTPUT = 3; // 2 bytes, 0.01 signed + static LPP_GENERIC_SENSOR = 100; // 4 bytes, unsigned + static LPP_LUMINOSITY = 101; // 2 bytes, 1 lux unsigned + static LPP_PRESENCE = 102; // 1 byte, bool + static LPP_TEMPERATURE = 103; // 2 bytes, 0.1°C signed + static LPP_RELATIVE_HUMIDITY = 104; // 1 byte, 0.5% unsigned + static LPP_ACCELEROMETER = 113; // 2 bytes per axis, 0.001G + static LPP_BAROMETRIC_PRESSURE = 115; // 2 bytes 0.1hPa unsigned + static LPP_VOLTAGE = 116; // 2 bytes 0.01V unsigned + static LPP_CURRENT = 117; // 2 bytes 0.001A unsigned + static LPP_FREQUENCY = 118; // 4 bytes 1Hz unsigned + static LPP_PERCENTAGE = 120; // 1 byte 1-100% unsigned + static LPP_ALTITUDE = 121; // 2 byte 1m signed + static LPP_CONCENTRATION = 125; // 2 bytes, 1 ppm unsigned + static LPP_POWER = 128; // 2 byte, 1W, unsigned + static LPP_DISTANCE = 130; // 4 byte, 0.001m, unsigned + static LPP_ENERGY = 131; // 4 byte, 0.001kWh, unsigned + static LPP_DIRECTION = 132; // 2 bytes, 1deg, unsigned + static LPP_UNIXTIME = 133; // 4 bytes, unsigned + static LPP_GYROMETER = 134; // 2 bytes per axis, 0.01 °/s + static LPP_COLOUR = 135; // 1 byte per RGB Color + static LPP_GPS = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter + static LPP_SWITCH = 142; // 1 byte, 0/1 + static LPP_POLYLINE = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas - static LPP_DIGITAL_INPUT = 0; // 1 byte - static LPP_DIGITAL_OUTPUT = 1; // 1 byte - static LPP_ANALOG_INPUT = 2; // 2 bytes, 0.01 signed - static LPP_ANALOG_OUTPUT = 3; // 2 bytes, 0.01 signed - static LPP_GENERIC_SENSOR = 100; // 4 bytes, unsigned - static LPP_LUMINOSITY = 101; // 2 bytes, 1 lux unsigned - static LPP_PRESENCE = 102; // 1 byte, bool - static LPP_TEMPERATURE = 103; // 2 bytes, 0.1°C signed - static LPP_RELATIVE_HUMIDITY = 104; // 1 byte, 0.5% unsigned - static LPP_ACCELEROMETER = 113; // 2 bytes per axis, 0.001G - static LPP_BAROMETRIC_PRESSURE = 115; // 2 bytes 0.1hPa unsigned - static LPP_VOLTAGE = 116; // 2 bytes 0.01V unsigned - static LPP_CURRENT = 117; // 2 bytes 0.001A unsigned - static LPP_FREQUENCY = 118; // 4 bytes 1Hz unsigned - static LPP_PERCENTAGE = 120; // 1 byte 1-100% unsigned - static LPP_ALTITUDE = 121; // 2 byte 1m signed - static LPP_CONCENTRATION = 125; // 2 bytes, 1 ppm unsigned - static LPP_POWER = 128; // 2 byte, 1W, unsigned - static LPP_DISTANCE = 130; // 4 byte, 0.001m, unsigned - static LPP_ENERGY = 131; // 4 byte, 0.001kWh, unsigned - static LPP_DIRECTION = 132; // 2 bytes, 1deg, unsigned - static LPP_UNIXTIME = 133; // 4 bytes, unsigned - static LPP_GYROMETER = 134; // 2 bytes per axis, 0.01 °/s - static LPP_COLOUR = 135; // 1 byte per RGB Color - static LPP_GPS = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter - static LPP_SWITCH = 142; // 1 byte, 0/1 - static LPP_POLYLINE = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas + static parse(bytes) { + const buffer = new BufferReader(bytes); + const telemetry = []; - static parse(bytes) { + while (buffer.getRemainingBytesCount() >= 2) { + // need at least 2 more bytes to get channel and type - const buffer = new BufferReader(bytes); - const telemetry = []; + const channel = buffer.readUInt8(); + const type = buffer.readUInt8(); - while(buffer.getRemainingBytesCount() >= 2){ // need at least 2 more bytes to get channel and type - - const channel = buffer.readUInt8(); - const type = buffer.readUInt8(); - - // stop parsing if channel and type are zero, as there seems to be garbage bytes??? - if(channel === 0 && type === 0){ - break; - } - - switch(type){ - case this.LPP_GENERIC_SENSOR: { - const value = buffer.readUInt32BE(); - // console.log(`[CayenneLpp] parsed LPP_GENERIC_SENSOR=${value}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": value, - }); - break; - } - case this.LPP_LUMINOSITY: { - const lux = buffer.readInt16BE(); - // console.log(`[CayenneLpp] parsed LPP_LUMINOSITY=${lux}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": lux, - }); - break; - } - case this.LPP_PRESENCE: { - const presence = buffer.readUInt8(); - // console.log(`[CayenneLpp] parsed LPP_PRESENCE=${presence}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": presence, - }); - break; - } - case this.LPP_TEMPERATURE: { - const temperature = buffer.readInt16BE() / 10; - // console.log(`[CayenneLpp] parsed LPP_TEMPERATURE=${temperature}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": temperature, - }); - break; - } - case this.LPP_RELATIVE_HUMIDITY: { - const relativeHumidity = buffer.readUInt8() / 2; - // console.log(`[CayenneLpp] parsed LPP_RELATIVE_HUMIDITY=${relativeHumidity}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": relativeHumidity, - }); - break; - } - case this.LPP_BAROMETRIC_PRESSURE: { - const barometricPressure = buffer.readUInt16BE() / 10; - // console.log(`[CayenneLpp] parsed LPP_BAROMETRIC_PRESSURE=${barometricPressure}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": barometricPressure, - }); - break; - } - case this.LPP_VOLTAGE: { - // uint16: 0v to 655.35v - // int16: -327.67v to +327.67v - // should be readUInt16BE, but I'm using readInt16BE to allow for negative voltage - const voltage = buffer.readInt16BE() / 100; - // console.log(`[CayenneLpp] parsed LPP_VOLTAGE=${voltage}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": voltage, - }); - break; - } - case this.LPP_CURRENT: { - // uint16: 0A to 655.35A - // int16: -327.67A to +327.67A - // should be readUInt16BE, but I'm using readInt16BE to allow for negative current - const current = buffer.readInt16BE() / 1000; - // console.log(`[CayenneLpp] parsed LPP_CURRENT=${current}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": current, - }); - break; - } - case this.LPP_PERCENTAGE: { - const percentage = buffer.readUInt8(); - // console.log(`[CayenneLpp] parsed LPP_PERCENTAGE=${percentage}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": percentage, - }); - break; - } - case this.LPP_CONCENTRATION: { - const concentration = buffer.readUInt16BE(); - // console.log(`[CayenneLpp] parsed LPP_CONCENTRATION=${concentration}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": concentration, - }); - break; - } - case this.LPP_POWER: { - const power = buffer.readUInt16BE(); - // console.log(`[CayenneLpp] parsed LPP_POWER=${power}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": power, - }); - break; - } - case this.LPP_GPS: { - const latitude = buffer.readInt24BE() / 10000; - const longitude = buffer.readInt24BE() / 10000; - const altitude = buffer.readInt24BE() / 100; - // console.log(`[CayenneLpp] parsed LPP_GPS=${latitude},${longitude},${altitude}`); - telemetry.push({ - "channel": channel, - "type": type, - "value": { - latitude: latitude, - longitude: longitude, - altitude: altitude, - }, - }); - break; - } - // todo support all telemetry types, otherwise if an unknown is given, we can't read other telemetry after it - default: { - // console.log(`[CayenneLpp] unsupported type: ${type}`); - return telemetry; - } - } + // stop parsing if channel and type are zero, as there seems to be garbage bytes??? + if (channel === 0 && type === 0) { + break; + } + switch (type) { + case this.LPP_GENERIC_SENSOR: { + const value = buffer.readUInt32BE(); + // console.log(`[CayenneLpp] parsed LPP_GENERIC_SENSOR=${value}`); + telemetry.push({ + channel: channel, + type: type, + value: value, + }); + break; } - - return telemetry; - + case this.LPP_LUMINOSITY: { + const lux = buffer.readInt16BE(); + // console.log(`[CayenneLpp] parsed LPP_LUMINOSITY=${lux}`); + telemetry.push({ + channel: channel, + type: type, + value: lux, + }); + break; + } + case this.LPP_PRESENCE: { + const presence = buffer.readUInt8(); + // console.log(`[CayenneLpp] parsed LPP_PRESENCE=${presence}`); + telemetry.push({ + channel: channel, + type: type, + value: presence, + }); + break; + } + case this.LPP_TEMPERATURE: { + const temperature = buffer.readInt16BE() / 10; + // console.log(`[CayenneLpp] parsed LPP_TEMPERATURE=${temperature}`); + telemetry.push({ + channel: channel, + type: type, + value: temperature, + }); + break; + } + case this.LPP_RELATIVE_HUMIDITY: { + const relativeHumidity = buffer.readUInt8() / 2; + // console.log(`[CayenneLpp] parsed LPP_RELATIVE_HUMIDITY=${relativeHumidity}`); + telemetry.push({ + channel: channel, + type: type, + value: relativeHumidity, + }); + break; + } + case this.LPP_BAROMETRIC_PRESSURE: { + const barometricPressure = buffer.readUInt16BE() / 10; + // console.log(`[CayenneLpp] parsed LPP_BAROMETRIC_PRESSURE=${barometricPressure}`); + telemetry.push({ + channel: channel, + type: type, + value: barometricPressure, + }); + break; + } + case this.LPP_VOLTAGE: { + // uint16: 0v to 655.35v + // int16: -327.67v to +327.67v + // should be readUInt16BE, but I'm using readInt16BE to allow for negative voltage + const voltage = buffer.readInt16BE() / 100; + // console.log(`[CayenneLpp] parsed LPP_VOLTAGE=${voltage}`); + telemetry.push({ + channel: channel, + type: type, + value: voltage, + }); + break; + } + case this.LPP_CURRENT: { + // uint16: 0A to 655.35A + // int16: -327.67A to +327.67A + // should be readUInt16BE, but I'm using readInt16BE to allow for negative current + const current = buffer.readInt16BE() / 1000; + // console.log(`[CayenneLpp] parsed LPP_CURRENT=${current}`); + telemetry.push({ + channel: channel, + type: type, + value: current, + }); + break; + } + case this.LPP_PERCENTAGE: { + const percentage = buffer.readUInt8(); + // console.log(`[CayenneLpp] parsed LPP_PERCENTAGE=${percentage}`); + telemetry.push({ + channel: channel, + type: type, + value: percentage, + }); + break; + } + case this.LPP_CONCENTRATION: { + const concentration = buffer.readUInt16BE(); + // console.log(`[CayenneLpp] parsed LPP_CONCENTRATION=${concentration}`); + telemetry.push({ + channel: channel, + type: type, + value: concentration, + }); + break; + } + case this.LPP_POWER: { + const power = buffer.readUInt16BE(); + // console.log(`[CayenneLpp] parsed LPP_POWER=${power}`); + telemetry.push({ + channel: channel, + type: type, + value: power, + }); + break; + } + case this.LPP_GPS: { + const latitude = buffer.readInt24BE() / 10000; + const longitude = buffer.readInt24BE() / 10000; + const altitude = buffer.readInt24BE() / 100; + // console.log(`[CayenneLpp] parsed LPP_GPS=${latitude},${longitude},${altitude}`); + telemetry.push({ + channel: channel, + type: type, + value: { + latitude: latitude, + longitude: longitude, + altitude: altitude, + }, + }); + break; + } + // todo support all telemetry types, otherwise if an unknown is given, we can't read other telemetry after it + default: { + // console.log(`[CayenneLpp] unsupported type: ${type}`); + return telemetry; + } + } } + return telemetry; + } } export default CayenneLpp; diff --git a/src/connection/connection.js b/src/connection/connection.js index 90132cc..517051d 100644 --- a/src/connection/connection.js +++ b/src/connection/connection.js @@ -1,2218 +1,2235 @@ -import BufferWriter from "../buffer_writer.js"; -import BufferReader from "../buffer_reader.js"; -import Constants from "../constants.js"; -import EventEmitter from "../events.js"; -import BufferUtils from "../buffer_utils.js"; -import Packet from "../packet.js"; -import RandomUtils from "../random_utils.js"; +import BufferWriter from '../buffer_writer.js'; +import BufferReader from '../buffer_reader.js'; +import Constants from '../constants.js'; +import EventEmitter from '../events.js'; +import BufferUtils from '../buffer_utils.js'; +import Packet from '../packet.js'; +import RandomUtils from '../random_utils.js'; class Connection extends EventEmitter { + async onConnected() { + // tell device what protocol version we support + try { + await this.deviceQuery(Constants.SupportedCompanionProtocolVersion); + } catch (e) { + // ignore + } + + // tell clients we are connected + this.emit('connected'); + } + + onDisconnected() { + this.emit('disconnected'); + } + + async close() { + throw new Error('This method must be implemented by the subclass.'); + } + + async sendToRadioFrame(data) { + throw new Error('This method must be implemented by the subclass.'); + } + + async sendCommandAppStart() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.AppStart); + data.writeByte(1); // appVer + data.writeBytes(new Uint8Array(6)); // reserved + data.writeString('test'); // appName + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendTxtMsg( + txtType, + attempt, + senderTimestamp, + pubKeyPrefix, + text, + ) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendTxtMsg); + data.writeByte(txtType); + data.writeByte(attempt); + data.writeUInt32LE(senderTimestamp); + data.writeBytes(pubKeyPrefix.slice(0, 6)); // only the first 6 bytes of pubKey are sent + data.writeString(text); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendChannelTxtMsg( + txtType, + channelIdx, + senderTimestamp, + text, + ) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendChannelTxtMsg); + data.writeByte(txtType); + data.writeByte(channelIdx); + data.writeUInt32LE(senderTimestamp); + data.writeString(text); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandGetContacts(since) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.GetContacts); + if (since) { + data.writeUInt32LE(since); + } + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandGetDeviceTime() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.GetDeviceTime); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetDeviceTime(epochSecs) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetDeviceTime); + data.writeUInt32LE(epochSecs); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendSelfAdvert(type) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendSelfAdvert); + data.writeByte(type); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetAdvertName(name) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetAdvertName); + data.writeString(name); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandAddUpdateContact( + publicKey, + type, + flags, + outPathLen, + outPath, + advName, + lastAdvert, + advLat, + advLon, + ) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.AddUpdateContact); + data.writeBytes(publicKey); + data.writeByte(type); + data.writeByte(flags); + data.writeByte(outPathLen); // todo writeInt8 + data.writeBytes(outPath); // 64 bytes + data.writeCString(advName, 32); // 32 bytes + data.writeUInt32LE(lastAdvert); + data.writeUInt32LE(advLat); + data.writeUInt32LE(advLon); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSyncNextMessage() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SyncNextMessage); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetRadioParams); + data.writeUInt32LE(radioFreq); + data.writeUInt32LE(radioBw); + data.writeByte(radioSf); + data.writeByte(radioCr); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetTxPower(txPower) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetTxPower); + data.writeByte(txPower); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandResetPath(pubKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ResetPath); + data.writeBytes(pubKey); // 32 bytes + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetAdvertLatLon(lat, lon) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetAdvertLatLon); + data.writeInt32LE(lat); + data.writeInt32LE(lon); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandRemoveContact(pubKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.RemoveContact); + data.writeBytes(pubKey); // 32 bytes + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandShareContact(pubKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ShareContact); + data.writeBytes(pubKey); // 32 bytes + await this.sendToRadioFrame(data.toBytes()); + } + + // provide a public key to export that contact + // not providing a public key will export local identity as a contact instead + async sendCommandExportContact(pubKey = null) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ExportContact); + if (pubKey) { + data.writeBytes(pubKey); // 32 bytes + } + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandImportContact(advertPacketBytes) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ImportContact); + data.writeBytes(advertPacketBytes); // raw advert packet bytes + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandReboot() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.Reboot); + data.writeString('reboot'); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandGetBatteryVoltage() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.GetBatteryVoltage); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandDeviceQuery(appTargetVer) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.DeviceQuery); + data.writeByte(appTargetVer); // e.g: 1 + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandExportPrivateKey() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ExportPrivateKey); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandImportPrivateKey(privateKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.ImportPrivateKey); + data.writeBytes(privateKey); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendRawData(path, rawData) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendRawData); + data.writeByte(path.length); + data.writeBytes(path); + data.writeBytes(rawData); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendLogin(publicKey, password) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendLogin); + data.writeBytes(publicKey); // 32 bytes - id of repeater or room server + data.writeString(password); // password is remainder of frame, max 15 characters + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendStatusReq(publicKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendStatusReq); + data.writeBytes(publicKey); // 32 bytes - id of repeater or room server + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendTelemetryReq(publicKey) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendTelemetryReq); + data.writeByte(0); // reserved + data.writeByte(0); // reserved + data.writeByte(0); // reserved + data.writeBytes(publicKey); // 32 bytes - id of destination node + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendBinaryReq(publicKey, requestCodeAndParams) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendBinaryReq); + data.writeBytes(publicKey); // 32 bytes - public key of contact to send request to + data.writeBytes(requestCodeAndParams); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandGetChannel(channelIdx) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.GetChannel); + data.writeByte(channelIdx); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetChannel(channelIdx, name, secret) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetChannel); + data.writeByte(channelIdx); + data.writeCString(name, 32); + data.writeBytes(secret); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSignStart() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SignStart); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSignData(dataToSign) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SignData); + data.writeBytes(dataToSign); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSignFinish() { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SignFinish); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSendTracePath(tag, auth, path) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SendTracePath); + data.writeUInt32LE(tag); + data.writeUInt32LE(auth); + data.writeByte(0); // flags + data.writeBytes(path); + await this.sendToRadioFrame(data.toBytes()); + } + + async sendCommandSetOtherParams(manualAddContacts) { + const data = new BufferWriter(); + data.writeByte(Constants.CommandCodes.SetOtherParams); + data.writeByte(manualAddContacts); // 0 or 1 + await this.sendToRadioFrame(data.toBytes()); + } + + onFrameReceived(frame) { + // emit received frame + this.emit('rx', frame); + + const bufferReader = new BufferReader(frame); + const responseCode = bufferReader.readByte(); + + if (responseCode === Constants.ResponseCodes.Ok) { + this.onOkResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.Err) { + this.onErrResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.SelfInfo) { + this.onSelfInfoResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.CurrTime) { + this.onCurrTimeResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.NoMoreMessages) { + this.onNoMoreMessagesResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.ContactMsgRecv) { + this.onContactMsgRecvResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.ChannelMsgRecv) { + this.onChannelMsgRecvResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.ContactsStart) { + this.onContactsStartResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.Contact) { + this.onContactResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.EndOfContacts) { + this.onEndOfContactsResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.Sent) { + this.onSentResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.ExportContact) { + this.onExportContactResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.BatteryVoltage) { + this.onBatteryVoltageResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.DeviceInfo) { + this.onDeviceInfoResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.PrivateKey) { + this.onPrivateKeyResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.Disabled) { + this.onDisabledResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.ChannelInfo) { + this.onChannelInfoResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.SignStart) { + this.onSignStartResponse(bufferReader); + } else if (responseCode === Constants.ResponseCodes.Signature) { + this.onSignatureResponse(bufferReader); + } else if (responseCode === Constants.PushCodes.Advert) { + this.onAdvertPush(bufferReader); + } else if (responseCode === Constants.PushCodes.PathUpdated) { + this.onPathUpdatedPush(bufferReader); + } else if (responseCode === Constants.PushCodes.SendConfirmed) { + this.onSendConfirmedPush(bufferReader); + } else if (responseCode === Constants.PushCodes.MsgWaiting) { + this.onMsgWaitingPush(bufferReader); + } else if (responseCode === Constants.PushCodes.RawData) { + this.onRawDataPush(bufferReader); + } else if (responseCode === Constants.PushCodes.LoginSuccess) { + this.onLoginSuccessPush(bufferReader); + } else if (responseCode === Constants.PushCodes.StatusResponse) { + this.onStatusResponsePush(bufferReader); + } else if (responseCode === Constants.PushCodes.LogRxData) { + this.onLogRxDataPush(bufferReader); + } else if (responseCode === Constants.PushCodes.TelemetryResponse) { + this.onTelemetryResponsePush(bufferReader); + } else if (responseCode === Constants.PushCodes.TraceData) { + this.onTraceDataPush(bufferReader); + } else if (responseCode === Constants.PushCodes.NewAdvert) { + this.onNewAdvertPush(bufferReader); + } else if (responseCode === Constants.PushCodes.BinaryResponse) { + this.onBinaryResponsePush(bufferReader); + } else { + console.log(`unhandled frame: code=${responseCode}`, frame); + } + } + + onAdvertPush(bufferReader) { + this.emit(Constants.PushCodes.Advert, { + publicKey: bufferReader.readBytes(32), + }); + } + + onPathUpdatedPush(bufferReader) { + this.emit(Constants.PushCodes.PathUpdated, { + publicKey: bufferReader.readBytes(32), + }); + } + + onSendConfirmedPush(bufferReader) { + this.emit(Constants.PushCodes.SendConfirmed, { + ackCode: bufferReader.readUInt32LE(), + roundTrip: bufferReader.readUInt32LE(), + }); + } + + onMsgWaitingPush(bufferReader) { + this.emit(Constants.PushCodes.MsgWaiting, {}); + } + + onRawDataPush(bufferReader) { + this.emit(Constants.PushCodes.RawData, { + lastSnr: bufferReader.readInt8() / 4, + lastRssi: bufferReader.readInt8(), + reserved: bufferReader.readByte(), + payload: bufferReader.readRemainingBytes(), + }); + } + + onLoginSuccessPush(bufferReader) { + this.emit(Constants.PushCodes.LoginSuccess, { + reserved: bufferReader.readByte(), // reserved + pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this login success is from + }); + } + + onStatusResponsePush(bufferReader) { + this.emit(Constants.PushCodes.StatusResponse, { + reserved: bufferReader.readByte(), // reserved + pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this status response is from + statusData: bufferReader.readRemainingBytes(), + }); + } + + onLogRxDataPush(bufferReader) { + this.emit(Constants.PushCodes.LogRxData, { + lastSnr: bufferReader.readInt8() / 4, + lastRssi: bufferReader.readInt8(), + raw: bufferReader.readRemainingBytes(), + }); + } + + onTelemetryResponsePush(bufferReader) { + this.emit(Constants.PushCodes.TelemetryResponse, { + reserved: bufferReader.readByte(), // reserved + pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this telemetry response is from + lppSensorData: bufferReader.readRemainingBytes(), + }); + } + + onBinaryResponsePush(bufferReader) { + this.emit(Constants.PushCodes.BinaryResponse, { + reserved: bufferReader.readByte(), // reserved + tag: bufferReader.readUInt32LE(), // 4 bytes tag + responseData: bufferReader.readRemainingBytes(), + }); + } + + onTraceDataPush(bufferReader) { + const reserved = bufferReader.readByte(); + const pathLen = bufferReader.readUInt8(); + this.emit(Constants.PushCodes.TraceData, { + reserved: reserved, + pathLen: pathLen, + flags: bufferReader.readUInt8(), + tag: bufferReader.readUInt32LE(), + authCode: bufferReader.readUInt32LE(), + pathHashes: bufferReader.readBytes(pathLen), + pathSnrs: bufferReader.readBytes(pathLen), + lastSnr: bufferReader.readInt8() / 4, + }); + } + + onNewAdvertPush(bufferReader) { + this.emit(Constants.PushCodes.NewAdvert, { + publicKey: bufferReader.readBytes(32), + type: bufferReader.readByte(), + flags: bufferReader.readByte(), + outPathLen: bufferReader.readInt8(), + outPath: bufferReader.readBytes(64), + advName: bufferReader.readCString(32), + lastAdvert: bufferReader.readUInt32LE(), + advLat: bufferReader.readUInt32LE(), + advLon: bufferReader.readUInt32LE(), + lastMod: bufferReader.readUInt32LE(), + }); + } + + onOkResponse(bufferReader) { + this.emit(Constants.ResponseCodes.Ok, {}); + } + + onErrResponse(bufferReader) { + const errCode = + bufferReader.getRemainingBytesCount() > 0 + ? bufferReader.readByte() + : null; + this.emit(Constants.ResponseCodes.Err, { + errCode: errCode, + }); + } + + onContactsStartResponse(bufferReader) { + this.emit(Constants.ResponseCodes.ContactsStart, { + count: bufferReader.readUInt32LE(), + }); + } + + onContactResponse(bufferReader) { + this.emit(Constants.ResponseCodes.Contact, { + publicKey: bufferReader.readBytes(32), + type: bufferReader.readByte(), + flags: bufferReader.readByte(), + outPathLen: bufferReader.readInt8(), + outPath: bufferReader.readBytes(64), + advName: bufferReader.readCString(32), + lastAdvert: bufferReader.readUInt32LE(), + advLat: bufferReader.readUInt32LE(), + advLon: bufferReader.readUInt32LE(), + lastMod: bufferReader.readUInt32LE(), + }); + } + + onEndOfContactsResponse(bufferReader) { + this.emit(Constants.ResponseCodes.EndOfContacts, { + mostRecentLastmod: bufferReader.readUInt32LE(), + }); + } + + onSentResponse(bufferReader) { + this.emit(Constants.ResponseCodes.Sent, { + result: bufferReader.readInt8(), + expectedAckCrc: bufferReader.readUInt32LE(), + estTimeout: bufferReader.readUInt32LE(), + }); + } + + onExportContactResponse(bufferReader) { + this.emit(Constants.ResponseCodes.ExportContact, { + advertPacketBytes: bufferReader.readRemainingBytes(), + }); + } + + onBatteryVoltageResponse(bufferReader) { + this.emit(Constants.ResponseCodes.BatteryVoltage, { + batteryMilliVolts: bufferReader.readUInt16LE(), + }); + } + + onDeviceInfoResponse(bufferReader) { + this.emit(Constants.ResponseCodes.DeviceInfo, { + firmwareVer: bufferReader.readInt8(), + reserved: bufferReader.readBytes(6), // reserved + firmware_build_date: bufferReader.readCString(12), // eg. "19 Feb 2025" + manufacturerModel: bufferReader.readString(), // remainder of frame + }); + } + + onPrivateKeyResponse(bufferReader) { + this.emit(Constants.ResponseCodes.PrivateKey, { + privateKey: bufferReader.readBytes(64), + }); + } + + onDisabledResponse(bufferReader) { + this.emit(Constants.ResponseCodes.Disabled, {}); + } + + onChannelInfoResponse(bufferReader) { + const idx = bufferReader.readUInt8(); + const name = bufferReader.readCString(32); + const remainingBytesLength = bufferReader.getRemainingBytesCount(); + + // 128-bit keys + if (remainingBytesLength === 16) { + this.emit(Constants.ResponseCodes.ChannelInfo, { + channelIdx: idx, + name: name, + secret: bufferReader.readBytes(remainingBytesLength), + }); + } else { + console.log( + `ChannelInfo has unexpected key length: ${remainingBytesLength}`, + ); + } + } + + onSignStartResponse(bufferReader) { + this.emit(Constants.ResponseCodes.SignStart, { + reserved: bufferReader.readByte(), + maxSignDataLen: bufferReader.readUInt32LE(), + }); + } + + onSignatureResponse(bufferReader) { + this.emit(Constants.ResponseCodes.Signature, { + signature: bufferReader.readBytes(64), + }); + } + + onSelfInfoResponse(bufferReader) { + this.emit(Constants.ResponseCodes.SelfInfo, { + type: bufferReader.readByte(), + txPower: bufferReader.readByte(), + maxTxPower: bufferReader.readByte(), + publicKey: bufferReader.readBytes(32), + advLat: bufferReader.readInt32LE(), + advLon: bufferReader.readInt32LE(), + reserved: bufferReader.readBytes(3), + manualAddContacts: bufferReader.readByte(), + radioFreq: bufferReader.readUInt32LE(), + radioBw: bufferReader.readUInt32LE(), + radioSf: bufferReader.readByte(), + radioCr: bufferReader.readByte(), + name: bufferReader.readString(), + }); + } + + onCurrTimeResponse(bufferReader) { + this.emit(Constants.ResponseCodes.CurrTime, { + epochSecs: bufferReader.readUInt32LE(), + }); + } + + onNoMoreMessagesResponse(bufferReader) { + this.emit(Constants.ResponseCodes.NoMoreMessages, {}); + } + + onContactMsgRecvResponse(bufferReader) { + this.emit(Constants.ResponseCodes.ContactMsgRecv, { + pubKeyPrefix: bufferReader.readBytes(6), + pathLen: bufferReader.readByte(), + txtType: bufferReader.readByte(), + senderTimestamp: bufferReader.readUInt32LE(), + text: bufferReader.readString(), + }); + } + + onChannelMsgRecvResponse(bufferReader) { + this.emit(Constants.ResponseCodes.ChannelMsgRecv, { + channelIdx: bufferReader.readInt8(), // reserved (0 for now, ie. 'public') + pathLen: bufferReader.readByte(), // 0xFF if was sent direct, otherwise hop count for flood-mode + txtType: bufferReader.readByte(), + senderTimestamp: bufferReader.readUInt32LE(), + text: bufferReader.readString(), + }); + } + + getSelfInfo(timeoutMillis = null) { + return new Promise(async (resolve, reject) => { + // listen for response + this.once(Constants.ResponseCodes.SelfInfo, (selfInfo) => { + resolve(selfInfo); + }); + + // timeout after provided milliseconds if device did not respond + if (timeoutMillis != null) { + setTimeout(reject, timeoutMillis); + } + + // request self info + await this.sendCommandAppStart(); + }); + } + + async sendAdvert(type) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - async onConnected() { - - // tell device what protocol version we support - try { - await this.deviceQuery(Constants.SupportedCompanionProtocolVersion); - } catch(e) { - // ignore - } - - // tell clients we are connected - this.emit("connected"); - - } - - onDisconnected() { - this.emit("disconnected"); - } - - async close() { - throw new Error("This method must be implemented by the subclass."); - } - - async sendToRadioFrame(data) { - throw new Error("This method must be implemented by the subclass."); - } - - async sendCommandAppStart() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.AppStart); - data.writeByte(1); // appVer - data.writeBytes(new Uint8Array(6)); // reserved - data.writeString("test"); // appName - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendTxtMsg(txtType, attempt, senderTimestamp, pubKeyPrefix, text) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendTxtMsg); - data.writeByte(txtType); - data.writeByte(attempt); - data.writeUInt32LE(senderTimestamp); - data.writeBytes(pubKeyPrefix.slice(0, 6)); // only the first 6 bytes of pubKey are sent - data.writeString(text); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendChannelTxtMsg(txtType, channelIdx, senderTimestamp, text) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendChannelTxtMsg); - data.writeByte(txtType); - data.writeByte(channelIdx); - data.writeUInt32LE(senderTimestamp); - data.writeString(text); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandGetContacts(since) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.GetContacts); - if(since){ - data.writeUInt32LE(since); - } - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandGetDeviceTime() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.GetDeviceTime); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetDeviceTime(epochSecs) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetDeviceTime); - data.writeUInt32LE(epochSecs); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendSelfAdvert(type) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendSelfAdvert); - data.writeByte(type); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetAdvertName(name) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetAdvertName); - data.writeString(name); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandAddUpdateContact(publicKey, type, flags, outPathLen, outPath, advName, lastAdvert, advLat, advLon) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.AddUpdateContact); - data.writeBytes(publicKey); - data.writeByte(type); - data.writeByte(flags); - data.writeByte(outPathLen); // todo writeInt8 - data.writeBytes(outPath); // 64 bytes - data.writeCString(advName, 32); // 32 bytes - data.writeUInt32LE(lastAdvert); - data.writeUInt32LE(advLat); - data.writeUInt32LE(advLon); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSyncNextMessage() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SyncNextMessage); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetRadioParams); - data.writeUInt32LE(radioFreq); - data.writeUInt32LE(radioBw); - data.writeByte(radioSf); - data.writeByte(radioCr); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetTxPower(txPower) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetTxPower); - data.writeByte(txPower); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandResetPath(pubKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ResetPath); - data.writeBytes(pubKey); // 32 bytes - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetAdvertLatLon(lat, lon) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetAdvertLatLon); - data.writeInt32LE(lat); - data.writeInt32LE(lon); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandRemoveContact(pubKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.RemoveContact); - data.writeBytes(pubKey); // 32 bytes - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandShareContact(pubKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ShareContact); - data.writeBytes(pubKey); // 32 bytes - await this.sendToRadioFrame(data.toBytes()); - } - - // provide a public key to export that contact - // not providing a public key will export local identity as a contact instead - async sendCommandExportContact(pubKey = null) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ExportContact); - if(pubKey){ - data.writeBytes(pubKey); // 32 bytes - } - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandImportContact(advertPacketBytes) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ImportContact); - data.writeBytes(advertPacketBytes); // raw advert packet bytes - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandReboot() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.Reboot); - data.writeString("reboot"); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandGetBatteryVoltage() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.GetBatteryVoltage); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandDeviceQuery(appTargetVer) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.DeviceQuery); - data.writeByte(appTargetVer); // e.g: 1 - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandExportPrivateKey() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ExportPrivateKey); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandImportPrivateKey(privateKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.ImportPrivateKey); - data.writeBytes(privateKey); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendRawData(path, rawData) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendRawData); - data.writeByte(path.length); - data.writeBytes(path); - data.writeBytes(rawData); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendLogin(publicKey, password) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendLogin); - data.writeBytes(publicKey); // 32 bytes - id of repeater or room server - data.writeString(password); // password is remainder of frame, max 15 characters - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendStatusReq(publicKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendStatusReq); - data.writeBytes(publicKey); // 32 bytes - id of repeater or room server - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendTelemetryReq(publicKey) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendTelemetryReq); - data.writeByte(0); // reserved - data.writeByte(0); // reserved - data.writeByte(0); // reserved - data.writeBytes(publicKey); // 32 bytes - id of destination node - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendBinaryReq(publicKey, requestCodeAndParams) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendBinaryReq); - data.writeBytes(publicKey); // 32 bytes - public key of contact to send request to - data.writeBytes(requestCodeAndParams); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandGetChannel(channelIdx) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.GetChannel); - data.writeByte(channelIdx); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetChannel(channelIdx, name, secret) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetChannel); - data.writeByte(channelIdx); - data.writeCString(name, 32); - data.writeBytes(secret); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSignStart() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SignStart); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSignData(dataToSign) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SignData); - data.writeBytes(dataToSign); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSignFinish() { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SignFinish); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSendTracePath(tag, auth, path) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendTracePath); - data.writeUInt32LE(tag); - data.writeUInt32LE(auth); - data.writeByte(0); // flags - data.writeBytes(path); - await this.sendToRadioFrame(data.toBytes()); - } - - async sendCommandSetOtherParams(manualAddContacts) { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SetOtherParams); - data.writeByte(manualAddContacts); // 0 or 1 - await this.sendToRadioFrame(data.toBytes()); - } - - onFrameReceived(frame) { - - // emit received frame - this.emit("rx", frame); - - const bufferReader = new BufferReader(frame); - const responseCode = bufferReader.readByte(); - - if(responseCode === Constants.ResponseCodes.Ok){ - this.onOkResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.Err){ - this.onErrResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.SelfInfo){ - this.onSelfInfoResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.CurrTime){ - this.onCurrTimeResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.NoMoreMessages){ - this.onNoMoreMessagesResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.ContactMsgRecv){ - this.onContactMsgRecvResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.ChannelMsgRecv){ - this.onChannelMsgRecvResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.ContactsStart){ - this.onContactsStartResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.Contact){ - this.onContactResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.EndOfContacts){ - this.onEndOfContactsResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.Sent){ - this.onSentResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.ExportContact){ - this.onExportContactResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.BatteryVoltage){ - this.onBatteryVoltageResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.DeviceInfo){ - this.onDeviceInfoResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.PrivateKey){ - this.onPrivateKeyResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.Disabled){ - this.onDisabledResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.ChannelInfo){ - this.onChannelInfoResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.SignStart){ - this.onSignStartResponse(bufferReader); - } else if(responseCode === Constants.ResponseCodes.Signature){ - this.onSignatureResponse(bufferReader); - } else if(responseCode === Constants.PushCodes.Advert){ - this.onAdvertPush(bufferReader); - } else if(responseCode === Constants.PushCodes.PathUpdated){ - this.onPathUpdatedPush(bufferReader); - } else if(responseCode === Constants.PushCodes.SendConfirmed){ - this.onSendConfirmedPush(bufferReader); - } else if(responseCode === Constants.PushCodes.MsgWaiting){ - this.onMsgWaitingPush(bufferReader); - } else if(responseCode === Constants.PushCodes.RawData){ - this.onRawDataPush(bufferReader); - } else if(responseCode === Constants.PushCodes.LoginSuccess){ - this.onLoginSuccessPush(bufferReader); - } else if(responseCode === Constants.PushCodes.StatusResponse){ - this.onStatusResponsePush(bufferReader); - } else if(responseCode === Constants.PushCodes.LogRxData){ - this.onLogRxDataPush(bufferReader); - } else if(responseCode === Constants.PushCodes.TelemetryResponse){ - this.onTelemetryResponsePush(bufferReader); - } else if(responseCode === Constants.PushCodes.TraceData){ - this.onTraceDataPush(bufferReader); - } else if(responseCode === Constants.PushCodes.NewAdvert){ - this.onNewAdvertPush(bufferReader); - } else if(responseCode === Constants.PushCodes.BinaryResponse){ - this.onBinaryResponsePush(bufferReader); - } else { - console.log(`unhandled frame: code=${responseCode}`, frame); - } - - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onAdvertPush(bufferReader) { - this.emit(Constants.PushCodes.Advert, { - publicKey: bufferReader.readBytes(32), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // send advert + await this.sendCommandSendSelfAdvert(type); + } catch (e) { + reject(e); + } + }); + } + + async sendFloodAdvert() { + return await this.sendAdvert(Constants.SelfAdvertTypes.Flood); + } + + async sendZeroHopAdvert() { + return await this.sendAdvert(Constants.SelfAdvertTypes.ZeroHop); + } + + setAdvertName(name) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onPathUpdatedPush(bufferReader) { - this.emit(Constants.PushCodes.PathUpdated, { - publicKey: bufferReader.readBytes(32), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onSendConfirmedPush(bufferReader) { - this.emit(Constants.PushCodes.SendConfirmed, { - ackCode: bufferReader.readUInt32LE(), - roundTrip: bufferReader.readUInt32LE(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set advert name + await this.sendCommandSetAdvertName(name); + } catch (e) { + reject(e); + } + }); + } + + setAdvertLatLong(latitude, longitude) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onMsgWaitingPush(bufferReader) { - this.emit(Constants.PushCodes.MsgWaiting, { + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set advert lat lon + await this.sendCommandSetAdvertLatLon(latitude, longitude); + } catch (e) { + reject(e); + } + }); + } + + setTxPower(txPower) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onRawDataPush(bufferReader) { - this.emit(Constants.PushCodes.RawData, { - lastSnr: bufferReader.readInt8() / 4, - lastRssi: bufferReader.readInt8(), - reserved: bufferReader.readByte(), - payload: bufferReader.readRemainingBytes(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onLoginSuccessPush(bufferReader) { - this.emit(Constants.PushCodes.LoginSuccess, { - reserved: bufferReader.readByte(), // reserved - pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this login success is from - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set tx power + await this.sendCommandSetTxPower(txPower); + } catch (e) { + reject(e); + } + }); + } + + setRadioParams(radioFreq, radioBw, radioSf, radioCr) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onStatusResponsePush(bufferReader) { - this.emit(Constants.PushCodes.StatusResponse, { - reserved: bufferReader.readByte(), // reserved - pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this status response is from - statusData: bufferReader.readRemainingBytes(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onLogRxDataPush(bufferReader) { - this.emit(Constants.PushCodes.LogRxData, { - lastSnr: bufferReader.readInt8() / 4, - lastRssi: bufferReader.readInt8(), - raw: bufferReader.readRemainingBytes(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set tx power + await this.sendCommandSetRadioParams( + radioFreq, + radioBw, + radioSf, + radioCr, + ); + } catch (e) { + reject(e); + } + }); + } + + getContacts() { + return new Promise(async (resolve, reject) => { + // add contacts we receive to a list + const contacts = []; + const onContactReceived = (contact) => { + contacts.push(contact); + }; + + // listen for contacts + this.on(Constants.ResponseCodes.Contact, onContactReceived); + + // there's no more contacts to receive, stop listening and resolve the promise + this.once(Constants.ResponseCodes.EndOfContacts, () => { + this.off(Constants.ResponseCodes.Contact, onContactReceived); + resolve(contacts); + }); + + // request contacts from device + await this.sendCommandGetContacts(); + }); + } + + async findContactByName(name) { + // get contacts + const contacts = await this.getContacts(); + + // find first contact matching name exactly + return contacts.find((contact) => { + return contact.advName === name; + }); + } + + async findContactByPublicKeyPrefix(pubKeyPrefix) { + // get contacts + const contacts = await this.getContacts(); + + // find first contact matching pub key prefix + return contacts.find((contact) => { + const contactPubKeyPrefix = contact.publicKey.subarray( + 0, + pubKeyPrefix.length, + ); + return BufferUtils.areBuffersEqual(pubKeyPrefix, contactPubKeyPrefix); + }); + } + + sendTextMessage(contactPublicKey, text, type) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive sent response + const onSent = (response) => { + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onTelemetryResponsePush(bufferReader) { - this.emit(Constants.PushCodes.TelemetryResponse, { - reserved: bufferReader.readByte(), // reserved - pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this telemetry response is from - lppSensorData: bufferReader.readRemainingBytes(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onBinaryResponsePush(bufferReader) { - this.emit(Constants.PushCodes.BinaryResponse, { - reserved: bufferReader.readByte(), // reserved - tag: bufferReader.readUInt32LE(), // 4 bytes tag - responseData: bufferReader.readRemainingBytes(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Sent, onSent); + this.once(Constants.ResponseCodes.Err, onErr); + + // compose message + const txtType = type ?? Constants.TxtTypes.Plain; + const attempt = 0; + const senderTimestamp = Math.floor(Date.now() / 1000); + + // send message + await this.sendCommandSendTxtMsg( + txtType, + attempt, + senderTimestamp, + contactPublicKey, + text, + ); + } catch (e) { + reject(e); + } + }); + } + + sendChannelTextMessage(channelIdx, text) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onTraceDataPush(bufferReader) { - const reserved = bufferReader.readByte(); - const pathLen = bufferReader.readUInt8(); - this.emit(Constants.PushCodes.TraceData, { - reserved: reserved, - pathLen: pathLen, - flags: bufferReader.readUInt8(), - tag: bufferReader.readUInt32LE(), - authCode: bufferReader.readUInt32LE(), - pathHashes: bufferReader.readBytes(pathLen), - pathSnrs: bufferReader.readBytes(pathLen), - lastSnr: bufferReader.readInt8() / 4, - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onNewAdvertPush(bufferReader) { - this.emit(Constants.PushCodes.NewAdvert, { - publicKey: bufferReader.readBytes(32), - type: bufferReader.readByte(), - flags: bufferReader.readByte(), - outPathLen: bufferReader.readInt8(), - outPath: bufferReader.readBytes(64), - advName: bufferReader.readCString(32), - lastAdvert: bufferReader.readUInt32LE(), - advLat: bufferReader.readUInt32LE(), - advLon: bufferReader.readUInt32LE(), - lastMod: bufferReader.readUInt32LE(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // compose message + const txtType = Constants.TxtTypes.Plain; + const senderTimestamp = Math.floor(Date.now() / 1000); + + // send message + await this.sendCommandSendChannelTxtMsg( + txtType, + channelIdx, + senderTimestamp, + text, + ); + } catch (e) { + reject(e); + } + }); + } + + syncNextMessage() { + return new Promise(async (resolve, reject) => { + // resolve promise when we receive a contact message + const onContactMessageReceived = (message) => { + this.off( + Constants.ResponseCodes.ContactMsgRecv, + onContactMessageReceived, + ); + this.off( + Constants.ResponseCodes.ChannelMsgRecv, + onChannelMessageReceived, + ); + this.off( + Constants.ResponseCodes.NoMoreMessages, + onNoMoreMessagesReceived, + ); + resolve({ + contactMessage: message, + }); + }; + + // resolve promise when we receive a channel message + const onChannelMessageReceived = (message) => { + this.off( + Constants.ResponseCodes.ContactMsgRecv, + onContactMessageReceived, + ); + this.off( + Constants.ResponseCodes.ChannelMsgRecv, + onChannelMessageReceived, + ); + this.off( + Constants.ResponseCodes.NoMoreMessages, + onNoMoreMessagesReceived, + ); + resolve({ + channelMessage: message, + }); + }; + + // resolve promise when we have no more messages to receive + const onNoMoreMessagesReceived = () => { + this.off( + Constants.ResponseCodes.ContactMsgRecv, + onContactMessageReceived, + ); + this.off( + Constants.ResponseCodes.ChannelMsgRecv, + onChannelMessageReceived, + ); + this.off( + Constants.ResponseCodes.NoMoreMessages, + onNoMoreMessagesReceived, + ); + resolve(null); + }; + + // listen for events + this.once( + Constants.ResponseCodes.ContactMsgRecv, + onContactMessageReceived, + ); + this.once( + Constants.ResponseCodes.ChannelMsgRecv, + onChannelMessageReceived, + ); + this.once( + Constants.ResponseCodes.NoMoreMessages, + onNoMoreMessagesReceived, + ); + + // sync next message from device + await this.sendCommandSyncNextMessage(); + }); + } + + async getWaitingMessages() { + const waitingMessages = []; + + while (true) { + // get next message, otherwise stop if nothing is returned + const message = await this.syncNextMessage(); + if (!message) { + break; + } + + // add to waiting messages list + waitingMessages.push(message); + } + + return waitingMessages; + } + + getDeviceTime() { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive sent response + const onCurrTime = (response) => { + this.off(Constants.ResponseCodes.CurrTime, onCurrTime); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onOkResponse(bufferReader) { - this.emit(Constants.ResponseCodes.Ok, { + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.CurrTime, onCurrTime); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - }); - } + // listen for events + this.once(Constants.ResponseCodes.CurrTime, onCurrTime); + this.once(Constants.ResponseCodes.Err, onErr); + + // get device time + await this.sendCommandGetDeviceTime(); + } catch (e) { + reject(e); + } + }); + } + + setDeviceTime(epochSecs) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = (response) => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onErrResponse(bufferReader) { - const errCode = bufferReader.getRemainingBytesCount() > 0 ? bufferReader.readByte() : null; - this.emit(Constants.ResponseCodes.Err, { - errCode: errCode, - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onContactsStartResponse(bufferReader) { - this.emit(Constants.ResponseCodes.ContactsStart, { - count: bufferReader.readUInt32LE(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set device time + await this.sendCommandSetDeviceTime(epochSecs); + } catch (e) { + reject(e); + } + }); + } + + async syncDeviceTime() { + await this.setDeviceTime(Math.floor(Date.now() / 1000)); + } + + importContact(advertPacketBytes) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = (response) => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onContactResponse(bufferReader) { - this.emit(Constants.ResponseCodes.Contact, { - publicKey: bufferReader.readBytes(32), - type: bufferReader.readByte(), - flags: bufferReader.readByte(), - outPathLen: bufferReader.readInt8(), - outPath: bufferReader.readBytes(64), - advName: bufferReader.readCString(32), - lastAdvert: bufferReader.readUInt32LE(), - advLat: bufferReader.readUInt32LE(), - advLon: bufferReader.readUInt32LE(), - lastMod: bufferReader.readUInt32LE(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onEndOfContactsResponse(bufferReader) { - this.emit(Constants.ResponseCodes.EndOfContacts, { - mostRecentLastmod: bufferReader.readUInt32LE(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // import contact + await this.sendCommandImportContact(advertPacketBytes); + } catch (e) { + reject(e); + } + }); + } + + exportContact(pubKey = null) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive export contact response + const onExportContact = (response) => { + this.off(Constants.ResponseCodes.ExportContact, onExportContact); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onSentResponse(bufferReader) { - this.emit(Constants.ResponseCodes.Sent, { - result: bufferReader.readInt8(), - expectedAckCrc: bufferReader.readUInt32LE(), - estTimeout: bufferReader.readUInt32LE(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.ExportContact, onExportContact); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onExportContactResponse(bufferReader) { - this.emit(Constants.ResponseCodes.ExportContact, { - advertPacketBytes: bufferReader.readRemainingBytes(), - }); - } + // listen for events + this.once(Constants.ResponseCodes.ExportContact, onExportContact); + this.once(Constants.ResponseCodes.Err, onErr); + + // export contact + await this.sendCommandExportContact(pubKey); + } catch (e) { + reject(e); + } + }); + } + + shareContact(pubKey) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = (response) => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - onBatteryVoltageResponse(bufferReader) { - this.emit(Constants.ResponseCodes.BatteryVoltage, { - batteryMilliVolts: bufferReader.readUInt16LE(), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onDeviceInfoResponse(bufferReader) { - this.emit(Constants.ResponseCodes.DeviceInfo, { - firmwareVer: bufferReader.readInt8(), - reserved: bufferReader.readBytes(6), // reserved - firmware_build_date: bufferReader.readCString(12), // eg. "19 Feb 2025" - manufacturerModel: bufferReader.readString(), // remainder of frame - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // share contact + await this.sendCommandShareContact(pubKey); + } catch (e) { + reject(e); + } + }); + } + + removeContact(pubKey) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - onPrivateKeyResponse(bufferReader) { - this.emit(Constants.ResponseCodes.PrivateKey, { - privateKey: bufferReader.readBytes(64), - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onDisabledResponse(bufferReader) { - this.emit(Constants.ResponseCodes.Disabled, { + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // remove contact + await this.sendCommandRemoveContact(pubKey); + } catch (e) { + reject(e); + } + }); + } + + addOrUpdateContact( + publicKey, + type, + flags, + outPathLen, + outPath, + advName, + lastAdvert, + advLat, + advLon, + ) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - onChannelInfoResponse(bufferReader) { - - const idx = bufferReader.readUInt8(); - const name = bufferReader.readCString(32); - const remainingBytesLength = bufferReader.getRemainingBytesCount(); - - // 128-bit keys - if(remainingBytesLength === 16){ - this.emit(Constants.ResponseCodes.ChannelInfo, { - channelIdx: idx, - name: name, - secret: bufferReader.readBytes(remainingBytesLength), - }); - } else { - console.log(`ChannelInfo has unexpected key length: ${remainingBytesLength}`); + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // add or update contact + await this.sendCommandAddUpdateContact( + publicKey, + type, + flags, + outPathLen, + outPath, + advName, + lastAdvert, + advLat, + advLon, + ); + } catch (e) { + reject(e); + } + }); + } + + setContactPath(contact, path) { + return new Promise(async (resolve, reject) => { + try { + // create empty out path + const maxPathLength = 64; + const outPath = new Uint8Array(maxPathLength); + + // fill out path with the provided path + for (var i = 0; i < path.length && i < maxPathLength; i++) { + outPath[i] = path[i]; } - } - - onSignStartResponse(bufferReader) { - this.emit(Constants.ResponseCodes.SignStart, { - reserved: bufferReader.readByte(), - maxSignDataLen: bufferReader.readUInt32LE(), - }); - } - - onSignatureResponse(bufferReader) { - this.emit(Constants.ResponseCodes.Signature, { - signature: bufferReader.readBytes(64), - }); - } - - onSelfInfoResponse(bufferReader) { - this.emit(Constants.ResponseCodes.SelfInfo, { - type: bufferReader.readByte(), - txPower: bufferReader.readByte(), - maxTxPower: bufferReader.readByte(), - publicKey: bufferReader.readBytes(32), - advLat: bufferReader.readInt32LE(), - advLon: bufferReader.readInt32LE(), - reserved: bufferReader.readBytes(3), - manualAddContacts: bufferReader.readByte(), - radioFreq: bufferReader.readUInt32LE(), - radioBw: bufferReader.readUInt32LE(), - radioSf: bufferReader.readByte(), - radioCr: bufferReader.readByte(), - name: bufferReader.readString(), - }); - } - - onCurrTimeResponse(bufferReader) { - this.emit(Constants.ResponseCodes.CurrTime, { - epochSecs: bufferReader.readUInt32LE(), - }); - } - - onNoMoreMessagesResponse(bufferReader) { - this.emit(Constants.ResponseCodes.NoMoreMessages, { - - }); - } - - onContactMsgRecvResponse(bufferReader) { - this.emit(Constants.ResponseCodes.ContactMsgRecv, { - pubKeyPrefix: bufferReader.readBytes(6), - pathLen: bufferReader.readByte(), - txtType: bufferReader.readByte(), - senderTimestamp: bufferReader.readUInt32LE(), - text: bufferReader.readString(), - }); - } - - onChannelMsgRecvResponse(bufferReader) { - this.emit(Constants.ResponseCodes.ChannelMsgRecv, { - channelIdx: bufferReader.readInt8(), // reserved (0 for now, ie. 'public') - pathLen: bufferReader.readByte(), // 0xFF if was sent direct, otherwise hop count for flood-mode - txtType: bufferReader.readByte(), - senderTimestamp: bufferReader.readUInt32LE(), - text: bufferReader.readString(), - }); - } - - getSelfInfo(timeoutMillis = null) { - return new Promise(async (resolve, reject) => { - - // listen for response - this.once(Constants.ResponseCodes.SelfInfo, (selfInfo) => { - resolve(selfInfo); - }); - - // timeout after provided milliseconds if device did not respond - if(timeoutMillis != null){ - setTimeout(reject, timeoutMillis); - } - - // request self info - await this.sendCommandAppStart(); - - }); - } - - async sendAdvert(type) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // send advert - await this.sendCommandSendSelfAdvert(type); - - } catch(e) { - reject(e); - } - }); - } + // update contact details with new path and path length + contact.outPathLen = path.length; + contact.outPath = outPath; + + // update contact + return await this.addOrUpdateContact( + contact.publicKey, + contact.type, + contact.flags, + contact.outPathLen, + contact.outPath, + contact.advName, + contact.lastAdvert, + contact.advLat, + contact.advLon, + ); + } catch (e) { + reject(e); + } + }); + } + + resetPath(pubKey) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - async sendFloodAdvert() { - return await this.sendAdvert(Constants.SelfAdvertTypes.Flood); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - async sendZeroHopAdvert() { - return await this.sendAdvert(Constants.SelfAdvertTypes.ZeroHop); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // reset path + await this.sendCommandResetPath(pubKey); + } catch (e) { + reject(e); + } + }); + } + + reboot() { + return new Promise(async (resolve, reject) => { + try { + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - setAdvertName(name) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set advert name - await this.sendCommandSetAdvertName(name); - - } catch(e) { - reject(e); - } - }); - } + // assume device rebooted after a short delay + setTimeout(() => { + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }, 1000); + + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + + // reboot + await this.sendCommandReboot(); + } catch (e) { + reject(e); + } + }); + } + + getBatteryVoltage() { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive battery voltage + const onBatteryVoltage = (response) => { + this.off(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - setAdvertLatLong(latitude, longitude) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set advert lat lon - await this.sendCommandSetAdvertLatLon(latitude, longitude); - - } catch(e) { - reject(e); - } - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - setTxPower(txPower) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set tx power - await this.sendCommandSetTxPower(txPower); - - } catch(e) { - reject(e); - } - }); - } + // listen for events + this.once(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); + this.once(Constants.ResponseCodes.Err, onErr); + + // get battery voltage + await this.sendCommandGetBatteryVoltage(); + } catch (e) { + reject(e); + } + }); + } + + deviceQuery(appTargetVer) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive device info + const onDeviceInfo = (response) => { + this.off(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - setRadioParams(radioFreq, radioBw, radioSf, radioCr) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set tx power - await this.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr); - - } catch(e) { - reject(e); - } - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - getContacts() { - return new Promise(async (resolve, reject) => { + // listen for events + this.once(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); + this.once(Constants.ResponseCodes.Err, onErr); + + // query device + await this.sendCommandDeviceQuery(appTargetVer); + } catch (e) { + reject(e); + } + }); + } + + exportPrivateKey() { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive private Key + const onPrivateKey = (response) => { + this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + resolve(response); + }; - // add contacts we receive to a list - const contacts = []; - const onContactReceived = (contact) => { - contacts.push(contact); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + reject(); + }; - // listen for contacts - this.on(Constants.ResponseCodes.Contact, onContactReceived); + // reject promise when we receive disabled + const onDisabled = () => { + this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + reject('disabled'); + }; - // there's no more contacts to receive, stop listening and resolve the promise - this.once(Constants.ResponseCodes.EndOfContacts, () => { - this.off(Constants.ResponseCodes.Contact, onContactReceived); - resolve(contacts); - }); + // listen for events + this.once(Constants.ResponseCodes.PrivateKey, onPrivateKey); + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Disabled, onDisabled); + + // export private key + await this.sendCommandExportPrivateKey(); + } catch (e) { + reject(e); + } + }); + } + + importPrivateKey(privateKey) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = (response) => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + resolve(response); + }; - // request contacts from device - await this.sendCommandGetContacts(); + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + reject(); + }; - }); - } + // reject promise when we receive disabled + const onDisabled = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Disabled, onDisabled); + reject('disabled'); + }; - async findContactByName(name) { + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Disabled, onDisabled); + + // import private key + await this.sendCommandImportPrivateKey(privateKey); + } catch (e) { + reject(e); + } + }); + } + + login(contactPublicKey, password, extraTimeoutMillis = 1000) { + return new Promise(async (resolve, reject) => { + try { + // get public key prefix we expect in the login response + const publicKeyPrefix = contactPublicKey.subarray(0, 6); + + // listen for sent response so we can get estimated timeout + var timeoutHandler = null; + const onSent = (response) => { + // remove error listener since we received sent response + this.off(Constants.ResponseCodes.Err, onErr); + + // reject login request as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + timeoutHandler = setTimeout(() => { + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); + reject('timeout'); + }, estTimeout); + }; - // get contacts - const contacts = await this.getContacts(); + // resolve promise when we receive login success push code + const onLoginSuccess = (response) => { + // make sure login success response is for this login request + if ( + !BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix) + ) { + console.log( + 'onLoginSuccess is not for this login request, ignoring...', + ); + return; + } + + // login successful + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); + resolve(response); + }; - // find first contact matching name exactly - return contacts.find((contact) => { - return contact.advName === name; - }); + // reject promise when we receive err + const onErr = () => { + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); + reject(); + }; - } + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Sent, onSent); + this.once(Constants.PushCodes.LoginSuccess, onLoginSuccess); + + // login + await this.sendCommandSendLogin(contactPublicKey, password); + } catch (e) { + reject(e); + } + }); + } + + getStatus(contactPublicKey, extraTimeoutMillis = 1000) { + return new Promise(async (resolve, reject) => { + try { + // get public key prefix we expect in the status response + const publicKeyPrefix = contactPublicKey.subarray(0, 6); + + // listen for sent response so we can get estimated timeout + var timeoutHandler = null; + const onSent = (response) => { + // remove error listener since we received sent response + this.off(Constants.ResponseCodes.Err, onErr); + + // reject login request as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + timeoutHandler = setTimeout(() => { + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); + reject('timeout'); + }, estTimeout); + }; - async findContactByPublicKeyPrefix(pubKeyPrefix) { + // resolve promise when we receive status response push code + const onStatusResponsePush = (response) => { + // make sure login success response is for this login request + if ( + !BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix) + ) { + console.log( + 'onStatusResponsePush is not for this status request, ignoring...', + ); + return; + } + + // status request successful + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); + + // parse repeater stats from status data + const bufferReader = new BufferReader(response.statusData); + const repeaterStats = { + batt_milli_volts: bufferReader.readUInt16LE(), // uint16_t batt_milli_volts; + curr_tx_queue_len: bufferReader.readUInt16LE(), // uint16_t curr_tx_queue_len; + noise_floor: bufferReader.readInt16LE(), // int16_t noise_floor; + last_rssi: bufferReader.readInt16LE(), // int16_t last_rssi; + n_packets_recv: bufferReader.readUInt32LE(), // uint32_t n_packets_recv; + n_packets_sent: bufferReader.readUInt32LE(), // uint32_t n_packets_sent; + total_air_time_secs: bufferReader.readUInt32LE(), // uint32_t total_air_time_secs; + total_up_time_secs: bufferReader.readUInt32LE(), // uint32_t total_up_time_secs; + n_sent_flood: bufferReader.readUInt32LE(), // uint32_t n_sent_flood + n_sent_direct: bufferReader.readUInt32LE(), // uint32_t n_sent_direct + n_recv_flood: bufferReader.readUInt32LE(), // uint32_t n_recv_flood + n_recv_direct: bufferReader.readUInt32LE(), // uint32_t n_recv_direct + err_events: bufferReader.readUInt16LE(), // uint16_t err_events + last_snr: bufferReader.readInt16LE(), // int16_t last_snr + n_direct_dups: bufferReader.readUInt16LE(), // uint16_t n_direct_dups + n_flood_dups: bufferReader.readUInt16LE(), // uint16_t n_flood_dups + }; + + resolve(repeaterStats); + }; - // get contacts - const contacts = await this.getContacts(); + // reject promise when we receive err + const onErr = () => { + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); + reject(); + }; - // find first contact matching pub key prefix - return contacts.find((contact) => { - const contactPubKeyPrefix = contact.publicKey.subarray(0, pubKeyPrefix.length); - return BufferUtils.areBuffersEqual(pubKeyPrefix, contactPubKeyPrefix); - }); + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Sent, onSent); + this.once(Constants.PushCodes.StatusResponse, onStatusResponsePush); + + // request status + await this.sendCommandSendStatusReq(contactPublicKey); + } catch (e) { + reject(e); + } + }); + } + + getTelemetry(contactPublicKey, extraTimeoutMillis = 1000) { + return new Promise(async (resolve, reject) => { + try { + // get public key prefix we expect in the telemetry response + const publicKeyPrefix = contactPublicKey.subarray(0, 6); + + // listen for sent response so we can get estimated timeout + var timeoutHandler = null; + const onSent = (response) => { + // remove error listener since we received sent response + this.off(Constants.ResponseCodes.Err, onErr); + + // reject as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + timeoutHandler = setTimeout(() => { + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off( + Constants.PushCodes.TelemetryResponse, + onTelemetryResponsePush, + ); + reject('timeout'); + }, estTimeout); + }; - } + // resolve promise when we receive telemetry response push code + const onTelemetryResponsePush = (response) => { + // make sure telemetry response is for this telemetry request + if ( + !BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix) + ) { + console.log( + 'onTelemetryResponsePush is not for this telemetry request, ignoring...', + ); + return; + } + + // telemetry request successful + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off( + Constants.PushCodes.TelemetryResponse, + onTelemetryResponsePush, + ); + + resolve(response); + }; - sendTextMessage(contactPublicKey, text, type) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive sent response - const onSent = (response) => { - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Sent, onSent); - this.once(Constants.ResponseCodes.Err, onErr); - - // compose message - const txtType = type ?? Constants.TxtTypes.Plain; - const attempt = 0; - const senderTimestamp = Math.floor(Date.now() / 1000); - - // send message - await this.sendCommandSendTxtMsg(txtType, attempt, senderTimestamp, contactPublicKey, text); - - } catch(e) { - reject(e); - } - }); - } + // reject promise when we receive err + const onErr = () => { + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off( + Constants.PushCodes.TelemetryResponse, + onTelemetryResponsePush, + ); + reject(); + }; - sendChannelTextMessage(channelIdx, text) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // compose message - const txtType = Constants.TxtTypes.Plain; - const senderTimestamp = Math.floor(Date.now() / 1000); - - // send message - await this.sendCommandSendChannelTxtMsg(txtType, channelIdx, senderTimestamp, text); - - } catch(e) { - reject(e); - } - }); - } + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Sent, onSent); + this.once( + Constants.PushCodes.TelemetryResponse, + onTelemetryResponsePush, + ); + + // request telemetry + await this.sendCommandSendTelemetryReq(contactPublicKey); + } catch (e) { + reject(e); + } + }); + } + + sendBinaryRequest( + contactPublicKey, + requestCodeAndParams, + extraTimeoutMillis = 1000, + ) { + return new Promise(async (resolve, reject) => { + try { + // we need the tag for this request (provided in sent listener), so we can listen for the response + var tag = null; + + // listen for sent response so we can get estimated timeout + var timeoutHandler = null; + const onSent = (response) => { + tag = response.expectedAckCrc; + + // remove error listener since we received sent response + this.off(Constants.ResponseCodes.Err, onErr); + + // reject as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + timeoutHandler = setTimeout(() => { + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); + reject('timeout'); + }, estTimeout); + }; - syncNextMessage() { - return new Promise(async (resolve, reject) => { - - // resolve promise when we receive a contact message - const onContactMessageReceived = (message) => { - this.off(Constants.ResponseCodes.ContactMsgRecv, onContactMessageReceived); - this.off(Constants.ResponseCodes.ChannelMsgRecv, onChannelMessageReceived); - this.off(Constants.ResponseCodes.NoMoreMessages, onNoMoreMessagesReceived); - resolve({ - contactMessage: message, - }); - } - - // resolve promise when we receive a channel message - const onChannelMessageReceived = (message) => { - this.off(Constants.ResponseCodes.ContactMsgRecv, onContactMessageReceived); - this.off(Constants.ResponseCodes.ChannelMsgRecv, onChannelMessageReceived); - this.off(Constants.ResponseCodes.NoMoreMessages, onNoMoreMessagesReceived); - resolve({ - channelMessage: message, - }); - } - - // resolve promise when we have no more messages to receive - const onNoMoreMessagesReceived = () => { - this.off(Constants.ResponseCodes.ContactMsgRecv, onContactMessageReceived); - this.off(Constants.ResponseCodes.ChannelMsgRecv, onChannelMessageReceived); - this.off(Constants.ResponseCodes.NoMoreMessages, onNoMoreMessagesReceived); - resolve(null); - } - - // listen for events - this.once(Constants.ResponseCodes.ContactMsgRecv, onContactMessageReceived); - this.once(Constants.ResponseCodes.ChannelMsgRecv, onChannelMessageReceived); - this.once(Constants.ResponseCodes.NoMoreMessages, onNoMoreMessagesReceived); - - // sync next message from device - await this.sendCommandSyncNextMessage(); + // resolve promise when we receive binary response push code + const onBinaryResponsePush = (response) => { + // make sure tag matches + if (tag !== response.tag) { + console.log( + 'onBinaryResponse is not for this request tag, ignoring...', + ); + return; + } + + // binary request successful + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); + + resolve(response.responseData); + }; - }); - } + // reject promise when we receive err + const onErr = () => { + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); + reject(); + }; - async getWaitingMessages() { + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + this.once(Constants.ResponseCodes.Sent, onSent); + this.once(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); - const waitingMessages = []; + // send binary request + await this.sendCommandSendBinaryReq( + contactPublicKey, + requestCodeAndParams, + ); + } catch (e) { + reject(e); + } + }); + } + + // @deprecated migrate to using tracePath instead. pingRepeaterZeroHop will be removed in a future update + pingRepeaterZeroHop(contactPublicKey, timeoutMillis) { + return new Promise(async (resolve, reject) => { + try { + // create raw data using custom packet + const bufferWriter = new BufferWriter(); + bufferWriter.writeUInt32LE(Date.now()); // timestamp millis so every ping is unique + bufferWriter.writeBytes([0x70, 0x69, 0x6e, 0x67]); // "ping" as bytes + bufferWriter.writeBytes(contactPublicKey.subarray(0, 2)); // 2 bytes from the repeaters public key, so we don't use another repeaters ping response + const rawBytes = bufferWriter.toBytes(); + + var startMillis = Date.now(); + + // resolve promise when we receive expected response + const onLogRxDataPush = (response) => { + // calculate round trip time + const endMillis = Date.now(); + const durationMillis = endMillis - startMillis; + + // parse packet from rx data, and make sure it's expected type + const packet = Packet.fromBytes(response.raw); + if (packet.payload_type !== Packet.PAYLOAD_TYPE_RAW_CUSTOM) { + return; + } + + // make sure the payload we sent, is the payload we received + if (!BufferUtils.areBuffersEqual(packet.payload, rawBytes)) { + return; + } + + // ping successful remove all listeners + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); + + // send back results + resolve({ + rtt: durationMillis, + snr: response.lastSnr, + rssi: response.lastRssi, + }); + }; - while(true){ + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); + reject(); + }; - // get next message, otherwise stop if nothing is returned - const message = await this.syncNextMessage(); - if(!message){ - break; - } + // listen for events + this.once(Constants.ResponseCodes.Err, onErr); + this.on(Constants.PushCodes.LogRxData, onLogRxDataPush); - // add to waiting messages list - waitingMessages.push(message); + // check if a timeout was provided + if (timeoutMillis != null) { + setTimeout(() => { + // stop listening for events + this.off(Constants.ResponseCodes.Err, onErr); + this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); + // reject since it timed out + reject('timeout'); + }, timeoutMillis); } - return waitingMessages; - - } - - getDeviceTime() { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive sent response - const onCurrTime = (response) => { - this.off(Constants.ResponseCodes.CurrTime, onCurrTime); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.CurrTime, onCurrTime); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.CurrTime, onCurrTime); - this.once(Constants.ResponseCodes.Err, onErr); - - // get device time - await this.sendCommandGetDeviceTime(); - - } catch(e) { - reject(e); - } - }); - } - - setDeviceTime(epochSecs) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = (response) => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set device time - await this.sendCommandSetDeviceTime(epochSecs); - - } catch(e) { - reject(e); - } - }); - } - - async syncDeviceTime() { - await this.setDeviceTime(Math.floor(Date.now() / 1000)); - } - - importContact(advertPacketBytes) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = (response) => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // import contact - await this.sendCommandImportContact(advertPacketBytes); - - } catch(e) { - reject(e); - } - }); - } - - exportContact(pubKey = null) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive export contact response - const onExportContact = (response) => { - this.off(Constants.ResponseCodes.ExportContact, onExportContact); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.ExportContact, onExportContact); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.ExportContact, onExportContact); - this.once(Constants.ResponseCodes.Err, onErr); - - // export contact - await this.sendCommandExportContact(pubKey); - - } catch(e) { - reject(e); - } - }); - } - - shareContact(pubKey) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = (response) => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // share contact - await this.sendCommandShareContact(pubKey); - - } catch(e) { - reject(e); - } - }); - } - - removeContact(pubKey) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // remove contact - await this.sendCommandRemoveContact(pubKey); - - } catch(e) { - reject(e); - } - }); - } - - addOrUpdateContact(publicKey, type, flags, outPathLen, outPath, advName, lastAdvert, advLat, advLon) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // add or update contact - await this.sendCommandAddUpdateContact(publicKey, type, flags, outPathLen, outPath, advName, lastAdvert, advLat, advLon); - - } catch(e) { - reject(e); - } - }); - } - - setContactPath(contact, path) { - return new Promise(async (resolve, reject) => { - try { - - // create empty out path - const maxPathLength = 64; - const outPath = new Uint8Array(maxPathLength); - - // fill out path with the provided path - for(var i = 0; i < path.length && i < maxPathLength; i++){ - outPath[i] = path[i]; - } - - // update contact details with new path and path length - contact.outPathLen = path.length; - contact.outPath = outPath; - - // update contact - return await this.addOrUpdateContact(contact.publicKey, contact.type, contact.flags, contact.outPathLen, contact.outPath, contact.advName, contact.lastAdvert, contact.advLat, contact.advLon); - - } catch(e) { - reject(e); - } - }); - } - - resetPath(pubKey) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // reset path - await this.sendCommandResetPath(pubKey); - - } catch(e) { - reject(e); - } - }); - } - - reboot() { - return new Promise(async (resolve, reject) => { - try { - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // assume device rebooted after a short delay - setTimeout(() => { - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - }, 1000); - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - - // reboot - await this.sendCommandReboot(); - - } catch(e) { - reject(e); - } - }); - } - - getBatteryVoltage() { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive battery voltage - const onBatteryVoltage = (response) => { - this.off(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.BatteryVoltage, onBatteryVoltage); - this.once(Constants.ResponseCodes.Err, onErr); - - // get battery voltage - await this.sendCommandGetBatteryVoltage(); - - } catch(e) { - reject(e); - } - }); - } - - deviceQuery(appTargetVer) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive device info - const onDeviceInfo = (response) => { - this.off(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.DeviceInfo, onDeviceInfo); - this.once(Constants.ResponseCodes.Err, onErr); - - // query device - await this.sendCommandDeviceQuery(appTargetVer); - - } catch(e) { - reject(e); - } - }); - } - - exportPrivateKey() { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive private Key - const onPrivateKey = (response) => { - this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - reject(); - } - - // reject promise when we receive disabled - const onDisabled = () => { - this.off(Constants.ResponseCodes.PrivateKey, onPrivateKey); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - reject("disabled"); - } - - // listen for events - this.once(Constants.ResponseCodes.PrivateKey, onPrivateKey); - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Disabled, onDisabled); - - // export private key - await this.sendCommandExportPrivateKey(); - - } catch(e) { - reject(e); - } - }); - } - - importPrivateKey(privateKey) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = (response) => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - reject(); - } - - // reject promise when we receive disabled - const onDisabled = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Disabled, onDisabled); - reject("disabled"); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Disabled, onDisabled); - - // import private key - await this.sendCommandImportPrivateKey(privateKey); - - } catch(e) { - reject(e); - } - }); - } - - login(contactPublicKey, password, extraTimeoutMillis = 1000) { - return new Promise(async (resolve, reject) => { - try { - - // get public key prefix we expect in the login response - const publicKeyPrefix = contactPublicKey.subarray(0, 6); - - // listen for sent response so we can get estimated timeout - var timeoutHandler = null; - const onSent = (response) => { - - // remove error listener since we received sent response - this.off(Constants.ResponseCodes.Err, onErr); - - // reject login request as timed out after estimated delay, plus a bit extra - const estTimeout = response.estTimeout + extraTimeoutMillis; - timeoutHandler = setTimeout(() => { - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); - reject("timeout"); - }, estTimeout); - - } - - // resolve promise when we receive login success push code - const onLoginSuccess = (response) => { - - // make sure login success response is for this login request - if(!BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix)){ - console.log("onLoginSuccess is not for this login request, ignoring..."); - return; - } - - // login successful - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); - resolve(response); - - } - - // reject promise when we receive err - const onErr = () => { - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.LoginSuccess, onLoginSuccess); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Sent, onSent); - this.once(Constants.PushCodes.LoginSuccess, onLoginSuccess); - - // login - await this.sendCommandSendLogin(contactPublicKey, password); - - } catch(e) { - reject(e); - } - }); - } - - getStatus(contactPublicKey, extraTimeoutMillis = 1000) { - return new Promise(async (resolve, reject) => { - try { - - // get public key prefix we expect in the status response - const publicKeyPrefix = contactPublicKey.subarray(0, 6); - - // listen for sent response so we can get estimated timeout - var timeoutHandler = null; - const onSent = (response) => { - - // remove error listener since we received sent response - this.off(Constants.ResponseCodes.Err, onErr); - - // reject login request as timed out after estimated delay, plus a bit extra - const estTimeout = response.estTimeout + extraTimeoutMillis; - timeoutHandler = setTimeout(() => { - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); - reject("timeout"); - }, estTimeout); - - } - - // resolve promise when we receive status response push code - const onStatusResponsePush = (response) => { - - // make sure login success response is for this login request - if(!BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix)){ - console.log("onStatusResponsePush is not for this status request, ignoring..."); - return; - } - - // status request successful - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); - - // parse repeater stats from status data - const bufferReader = new BufferReader(response.statusData); - const repeaterStats = { - batt_milli_volts: bufferReader.readUInt16LE(), // uint16_t batt_milli_volts; - curr_tx_queue_len: bufferReader.readUInt16LE(), // uint16_t curr_tx_queue_len; - noise_floor: bufferReader.readInt16LE(), // int16_t noise_floor; - last_rssi: bufferReader.readInt16LE(), // int16_t last_rssi; - n_packets_recv: bufferReader.readUInt32LE(), // uint32_t n_packets_recv; - n_packets_sent: bufferReader.readUInt32LE(), // uint32_t n_packets_sent; - total_air_time_secs: bufferReader.readUInt32LE(), // uint32_t total_air_time_secs; - total_up_time_secs: bufferReader.readUInt32LE(), // uint32_t total_up_time_secs; - n_sent_flood: bufferReader.readUInt32LE(), // uint32_t n_sent_flood - n_sent_direct: bufferReader.readUInt32LE(), // uint32_t n_sent_direct - n_recv_flood: bufferReader.readUInt32LE(), // uint32_t n_recv_flood - n_recv_direct: bufferReader.readUInt32LE(), // uint32_t n_recv_direct - err_events: bufferReader.readUInt16LE(), // uint16_t err_events - last_snr: bufferReader.readInt16LE(), // int16_t last_snr - n_direct_dups: bufferReader.readUInt16LE(), // uint16_t n_direct_dups - n_flood_dups: bufferReader.readUInt16LE(), // uint16_t n_flood_dups - } - - resolve(repeaterStats); - - } - - // reject promise when we receive err - const onErr = () => { - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.StatusResponse, onStatusResponsePush); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Sent, onSent); - this.once(Constants.PushCodes.StatusResponse, onStatusResponsePush); - - // request status - await this.sendCommandSendStatusReq(contactPublicKey); - - } catch(e) { - reject(e); - } - }); - } - - getTelemetry(contactPublicKey, extraTimeoutMillis = 1000) { - return new Promise(async (resolve, reject) => { - try { - - // get public key prefix we expect in the telemetry response - const publicKeyPrefix = contactPublicKey.subarray(0, 6); - - // listen for sent response so we can get estimated timeout - var timeoutHandler = null; - const onSent = (response) => { - - // remove error listener since we received sent response - this.off(Constants.ResponseCodes.Err, onErr); - - // reject as timed out after estimated delay, plus a bit extra - const estTimeout = response.estTimeout + extraTimeoutMillis; - timeoutHandler = setTimeout(() => { - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TelemetryResponse, onTelemetryResponsePush); - reject("timeout"); - }, estTimeout); - - } - - // resolve promise when we receive telemetry response push code - const onTelemetryResponsePush = (response) => { - - // make sure telemetry response is for this telemetry request - if(!BufferUtils.areBuffersEqual(publicKeyPrefix, response.pubKeyPrefix)){ - console.log("onTelemetryResponsePush is not for this telemetry request, ignoring..."); - return; - } - - // telemetry request successful - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TelemetryResponse, onTelemetryResponsePush); - - resolve(response); - - } - - // reject promise when we receive err - const onErr = () => { - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TelemetryResponse, onTelemetryResponsePush); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Sent, onSent); - this.once(Constants.PushCodes.TelemetryResponse, onTelemetryResponsePush); - - // request telemetry - await this.sendCommandSendTelemetryReq(contactPublicKey); - - } catch(e) { - reject(e); - } - }); - } - - sendBinaryRequest(contactPublicKey, requestCodeAndParams, extraTimeoutMillis = 1000) { - return new Promise(async (resolve, reject) => { - try { - - // we need the tag for this request (provided in sent listener), so we can listen for the response - var tag = null; - - // listen for sent response so we can get estimated timeout - var timeoutHandler = null; - const onSent = (response) => { - - tag = response.expectedAckCrc; - - // remove error listener since we received sent response - this.off(Constants.ResponseCodes.Err, onErr); - - // reject as timed out after estimated delay, plus a bit extra - const estTimeout = response.estTimeout + extraTimeoutMillis; - timeoutHandler = setTimeout(() => { - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); - reject("timeout"); - }, estTimeout); - - } - - // resolve promise when we receive binary response push code - const onBinaryResponsePush = (response) => { - - // make sure tag matches - if(tag !== response.tag){ - console.log("onBinaryResponse is not for this request tag, ignoring..."); - return; - } - - // binary request successful - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); - - resolve(response.responseData); - - } - - // reject promise when we receive err - const onErr = () => { - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - this.once(Constants.ResponseCodes.Sent, onSent); - this.once(Constants.PushCodes.BinaryResponse, onBinaryResponsePush); - - // send binary request - await this.sendCommandSendBinaryReq(contactPublicKey, requestCodeAndParams); - - } catch(e) { - reject(e); - } - }); - } - - // @deprecated migrate to using tracePath instead. pingRepeaterZeroHop will be removed in a future update - pingRepeaterZeroHop(contactPublicKey, timeoutMillis) { - return new Promise(async (resolve, reject) => { - try { - - // create raw data using custom packet - const bufferWriter = new BufferWriter(); - bufferWriter.writeUInt32LE(Date.now()); // timestamp millis so every ping is unique - bufferWriter.writeBytes([0x70, 0x69, 0x6E, 0x67]); // "ping" as bytes - bufferWriter.writeBytes(contactPublicKey.subarray(0, 2)); // 2 bytes from the repeaters public key, so we don't use another repeaters ping response - const rawBytes = bufferWriter.toBytes(); - - var startMillis = Date.now(); - - // resolve promise when we receive expected response - const onLogRxDataPush = (response) => { - - // calculate round trip time - const endMillis = Date.now(); - const durationMillis = endMillis - startMillis; - - // parse packet from rx data, and make sure it's expected type - const packet = Packet.fromBytes(response.raw); - if(packet.payload_type !== Packet.PAYLOAD_TYPE_RAW_CUSTOM){ - return; - } - - // make sure the payload we sent, is the payload we received - if(!BufferUtils.areBuffersEqual(packet.payload, rawBytes)){ - return; - } - - // ping successful remove all listeners - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); - - // send back results - resolve({ - rtt: durationMillis, - snr: response.lastSnr, - rssi: response.lastRssi, - }); - - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Err, onErr); - this.on(Constants.PushCodes.LogRxData, onLogRxDataPush); - - // check if a timeout was provided - if(timeoutMillis != null){ - setTimeout(() => { - - // stop listening for events - this.off(Constants.ResponseCodes.Err, onErr); - this.off(Constants.PushCodes.LogRxData, onLogRxDataPush); - - // reject since it timed out - reject("timeout"); - - }, timeoutMillis); - } - - // send raw data to repeater, for it to repeat zero hop - await this.sendCommandSendRawData([ - // we set the repeater we want to ping as the path - // it should repeat our packet, and we can listen for it - contactPublicKey.subarray(0, 1), - ], rawBytes); - - } catch(e) { - reject(e); - } - }); - } - - getChannel(channelIdx) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive channel info response - const onChannelInfoResponse = (response) => { - this.off(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); - this.once(Constants.ResponseCodes.Err, onErr); - - // get channel - await this.sendCommandGetChannel(channelIdx); - - } catch(e) { - reject(e); - } - }); - } - - setChannel(channelIdx, name, secret) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set channel - await this.sendCommandSetChannel(channelIdx, name, secret); - - } catch(e) { - reject(e); - } - }); - } - - async deleteChannel(channelIdx) { - return await this.setChannel(channelIdx, "", new Uint8Array(16)); - } - - getChannels() { - return new Promise(async (resolve, reject) => { - - // get channels until we get an error - var channelIdx = 0; - const channels = []; - while(true){ - - // try to get next channel - try { - const channel = await this.getChannel(channelIdx); - channels.push(channel); - } catch(e){ - break; - } - - channelIdx++; - - } - - return resolve(channels); - - }); - } - - async findChannelByName(name) { - - // get channels - const channels = await this.getChannels(); - - // find first channel matching name exactly - return channels.find((channel) => { - return channel.name === name; - }); - - } - - async findChannelBySecret(secret) { - - // get channels - const channels = await this.getChannels(); - - // find first channel matching secret - return channels.find((channel) => { - return BufferUtils.areBuffersEqual(secret, channel.secret); - }); + // send raw data to repeater, for it to repeat zero hop + await this.sendCommandSendRawData( + [ + // we set the repeater we want to ping as the path + // it should repeat our packet, and we can listen for it + contactPublicKey.subarray(0, 1), + ], + rawBytes, + ); + } catch (e) { + reject(e); + } + }); + } + + getChannel(channelIdx) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive channel info response + const onChannelInfoResponse = (response) => { + this.off(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - async sign(data) { - return new Promise(async (resolve, reject) => { - try { - - const chunkSize = 128; - const bufferReader = new BufferReader(data); - - const sendNextChunk = async () => { - - // get next chunk - var chunk; - if(bufferReader.getRemainingBytesCount() >= chunkSize){ - chunk = bufferReader.readBytes(chunkSize); - } else { - chunk = bufferReader.readRemainingBytes(); - } - - // send chunk - await this.sendCommandSignData(chunk); - - } - - // listen for ok to send next chunk - const onOk = async (response) => { - - // check if more chunks to send - if(bufferReader.getRemainingBytesCount() > 0){ - await sendNextChunk(); - return; - } - - // no more chunks to send, tell device we are done - await this.sendCommandSignFinish(); - - } - - // listen for sign start - const onSignStart = async (response) => { - - this.off(Constants.ResponseCodes.SignStart, onSignStart); - - // check if data to sign is too long - if(bufferReader.getRemainingBytesCount() > response.maxSignDataLen){ - this.off(Constants.ResponseCodes.ok, onOk); - this.off(Constants.ResponseCodes.err, onErr); - this.off(Constants.ResponseCodes.SignStart, onSignStart); - this.off(Constants.ResponseCodes.Signature, onSignature); - reject("data_too_long"); - return; - } - - // start first chunk of data - await sendNextChunk(); - - } - - // resolve when we receive signature - const onSignature = (response) => { - this.off(Constants.ResponseCodes.ok, onOk); - this.off(Constants.ResponseCodes.err, onErr); - this.off(Constants.ResponseCodes.SignStart, onSignStart); - this.off(Constants.ResponseCodes.Signature, onSignature); - resolve(response.signature); - } - - // reject promise when we receive err - const onErr = (response) => { - this.off(Constants.ResponseCodes.ok, onOk); - this.off(Constants.ResponseCodes.err, onErr); - this.off(Constants.ResponseCodes.SignStart, onSignStart); - this.off(Constants.ResponseCodes.Signature, onSignature); - reject(response); - } - - // listen for events - this.on(Constants.ResponseCodes.Ok, onOk); - this.on(Constants.ResponseCodes.SignStart, onSignStart); - this.on(Constants.ResponseCodes.Signature, onSignature); - this.once(Constants.ResponseCodes.Err, onErr); - - // request device to start signing data - await this.sendCommandSignStart(); - - } catch(e) { - reject(e); - } - }); - } + // listen for events + this.once(Constants.ResponseCodes.ChannelInfo, onChannelInfoResponse); + this.once(Constants.ResponseCodes.Err, onErr); + + // get channel + await this.sendCommandGetChannel(channelIdx); + } catch (e) { + reject(e); + } + }); + } + + setChannel(channelIdx, name, secret) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); + }; - tracePath(path, extraTimeoutMillis = 0) { - return new Promise(async (resolve, reject) => { - try { - - // generate a random tag for this trace, so we can listen for the correct response - const tag = RandomUtils.getRandomInt(0, 4294967295); - - // listen for sent response so we can get estimated timeout - var timeoutHandler = null; - const onSent = (response) => { - - // remove error listener since we received sent response - this.off(Constants.ResponseCodes.Err, onErr); - - // reject trace request as timed out after estimated delay, plus a bit extra - const estTimeout = response.estTimeout + extraTimeoutMillis; - timeoutHandler = setTimeout(() => { - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TraceData, onTraceDataPush); - this.off(Constants.ResponseCodes.Err, onErr); - reject("timeout"); - }, estTimeout); - - } - - // resolve promise when we receive trace data - const onTraceDataPush = (response) => { - - // make sure tag matches - if(response.tag !== tag){ - console.log("ignoring trace data for a different trace request"); - return; - } - - // resolve - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TraceData, onTraceDataPush); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(response); - - } - - // reject promise when we receive err - const onErr = () => { - clearTimeout(timeoutHandler); - this.off(Constants.ResponseCodes.Sent, onSent); - this.off(Constants.PushCodes.TraceData, onTraceDataPush); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Sent, onSent); - this.on(Constants.PushCodes.TraceData, onTraceDataPush); - this.once(Constants.ResponseCodes.Err, onErr); - - // trace path - await this.sendCommandSendTracePath(tag, 0, path); - - } catch(e) { - reject(e); - } - }); - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - setOtherParams(manualAddContacts) { - return new Promise(async (resolve, reject) => { - try { - - // resolve promise when we receive ok - const onOk = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - resolve(); - } - - // reject promise when we receive err - const onErr = () => { - this.off(Constants.ResponseCodes.Ok, onOk); - this.off(Constants.ResponseCodes.Err, onErr); - reject(); - } - - // listen for events - this.once(Constants.ResponseCodes.Ok, onOk); - this.once(Constants.ResponseCodes.Err, onErr); - - // set other params - await this.sendCommandSetOtherParams(manualAddContacts); - - } catch(e) { - reject(e); - } - }); - } + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set channel + await this.sendCommandSetChannel(channelIdx, name, secret); + } catch (e) { + reject(e); + } + }); + } + + async deleteChannel(channelIdx) { + return await this.setChannel(channelIdx, '', new Uint8Array(16)); + } + + getChannels() { + return new Promise(async (resolve, reject) => { + // get channels until we get an error + var channelIdx = 0; + const channels = []; + while (true) { + // try to get next channel + try { + const channel = await this.getChannel(channelIdx); + channels.push(channel); + } catch (e) { + break; + } - async setAutoAddContacts() { - return await this.setOtherParams(false); - } + channelIdx++; + } + + return resolve(channels); + }); + } + + async findChannelByName(name) { + // get channels + const channels = await this.getChannels(); + + // find first channel matching name exactly + return channels.find((channel) => { + return channel.name === name; + }); + } + + async findChannelBySecret(secret) { + // get channels + const channels = await this.getChannels(); + + // find first channel matching secret + return channels.find((channel) => { + return BufferUtils.areBuffersEqual(secret, channel.secret); + }); + } + + async sign(data) { + return new Promise(async (resolve, reject) => { + try { + const chunkSize = 128; + const bufferReader = new BufferReader(data); + + const sendNextChunk = async () => { + // get next chunk + var chunk; + if (bufferReader.getRemainingBytesCount() >= chunkSize) { + chunk = bufferReader.readBytes(chunkSize); + } else { + chunk = bufferReader.readRemainingBytes(); + } + + // send chunk + await this.sendCommandSignData(chunk); + }; - async setManualAddContacts() { - return await this.setOtherParams(true); - } + // listen for ok to send next chunk + const onOk = async (response) => { + // check if more chunks to send + if (bufferReader.getRemainingBytesCount() > 0) { + await sendNextChunk(); + return; + } - // REQ_TYPE_GET_NEIGHBOURS from Repeater role - // https://github.com/meshcore-dev/MeshCore/pull/833 - // Repeater must be running firmware v1.9.0+ - async getNeighbours(publicKey, - count = 10, - offset = 0, - orderBy = 0, // 0=newest_to_oldest, 1=oldest_to_newest, 2=strongest_to_weakest, 3=weakest_to_strongest - pubKeyPrefixLength = 8, - ) { - - // get neighbours: - // req_data[0] = REQ_TYPE_GET_NEIGHBOURS - // req_data[1] = request_version=0 - // req_data[2] = count=10 how many neighbours to fetch - // req_data[3..4] = offset=0 (uint16_t) - // req_data[5] = order_by=0 - // req_data[6] = pubkey_prefix_len=8 - // req_data[7..10] = random blob (help hash) - const bufferWriter = new BufferWriter(); - bufferWriter.writeByte(Constants.BinaryRequestTypes.GetNeighbours); - bufferWriter.writeByte(0); // request_version=0 - bufferWriter.writeByte(count); - bufferWriter.writeUInt16LE(offset); - bufferWriter.writeByte(orderBy); - bufferWriter.writeByte(pubKeyPrefixLength); - bufferWriter.writeUInt32LE(RandomUtils.getRandomInt(0, 4294967295)); // 4 bytes random blob + // no more chunks to send, tell device we are done + await this.sendCommandSignFinish(); + }; - // send binary request - const responseData = await this.sendBinaryRequest(publicKey, bufferWriter.toBytes()); + // listen for sign start + const onSignStart = async (response) => { + this.off(Constants.ResponseCodes.SignStart, onSignStart); + + // check if data to sign is too long + if (bufferReader.getRemainingBytesCount() > response.maxSignDataLen) { + this.off(Constants.ResponseCodes.ok, onOk); + this.off(Constants.ResponseCodes.err, onErr); + this.off(Constants.ResponseCodes.SignStart, onSignStart); + this.off(Constants.ResponseCodes.Signature, onSignature); + reject('data_too_long'); + return; + } + + // start first chunk of data + await sendNextChunk(); + }; - // parse response - const bufferReader = new BufferReader(responseData); - const totalNeighboursCount = bufferReader.readUInt16LE(); - const resultsCount = bufferReader.readUInt16LE(); + // resolve when we receive signature + const onSignature = (response) => { + this.off(Constants.ResponseCodes.ok, onOk); + this.off(Constants.ResponseCodes.err, onErr); + this.off(Constants.ResponseCodes.SignStart, onSignStart); + this.off(Constants.ResponseCodes.Signature, onSignature); + resolve(response.signature); + }; - // parse neighbours list - const neighbours = []; - for(var i = 0; i < resultsCount; i++){ + // reject promise when we receive err + const onErr = (response) => { + this.off(Constants.ResponseCodes.ok, onOk); + this.off(Constants.ResponseCodes.err, onErr); + this.off(Constants.ResponseCodes.SignStart, onSignStart); + this.off(Constants.ResponseCodes.Signature, onSignature); + reject(response); + }; - // read info - const publicKeyPrefix = bufferReader.readBytes(pubKeyPrefixLength); - const heardSecondsAgo = bufferReader.readUInt32LE(); - const snr = bufferReader.readInt8() / 4; + // listen for events + this.on(Constants.ResponseCodes.Ok, onOk); + this.on(Constants.ResponseCodes.SignStart, onSignStart); + this.on(Constants.ResponseCodes.Signature, onSignature); + this.once(Constants.ResponseCodes.Err, onErr); + + // request device to start signing data + await this.sendCommandSignStart(); + } catch (e) { + reject(e); + } + }); + } + + tracePath(path, extraTimeoutMillis = 0) { + return new Promise(async (resolve, reject) => { + try { + // generate a random tag for this trace, so we can listen for the correct response + const tag = RandomUtils.getRandomInt(0, 4294967295); + + // listen for sent response so we can get estimated timeout + var timeoutHandler = null; + const onSent = (response) => { + // remove error listener since we received sent response + this.off(Constants.ResponseCodes.Err, onErr); + + // reject trace request as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + timeoutHandler = setTimeout(() => { + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.TraceData, onTraceDataPush); + this.off(Constants.ResponseCodes.Err, onErr); + reject('timeout'); + }, estTimeout); + }; - // add to list - neighbours.push({ - publicKeyPrefix: publicKeyPrefix, - heardSecondsAgo: heardSecondsAgo, - snr: snr, - }); + // resolve promise when we receive trace data + const onTraceDataPush = (response) => { + // make sure tag matches + if (response.tag !== tag) { + console.log('ignoring trace data for a different trace request'); + return; + } + + // resolve + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.TraceData, onTraceDataPush); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(response); + }; - } + // reject promise when we receive err + const onErr = () => { + clearTimeout(timeoutHandler); + this.off(Constants.ResponseCodes.Sent, onSent); + this.off(Constants.PushCodes.TraceData, onTraceDataPush); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; - return { - totalNeighboursCount: totalNeighboursCount, - neighbours: neighbours, + // listen for events + this.once(Constants.ResponseCodes.Sent, onSent); + this.on(Constants.PushCodes.TraceData, onTraceDataPush); + this.once(Constants.ResponseCodes.Err, onErr); + + // trace path + await this.sendCommandSendTracePath(tag, 0, path); + } catch (e) { + reject(e); + } + }); + } + + setOtherParams(manualAddContacts) { + return new Promise(async (resolve, reject) => { + try { + // resolve promise when we receive ok + const onOk = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + resolve(); }; - } + // reject promise when we receive err + const onErr = () => { + this.off(Constants.ResponseCodes.Ok, onOk); + this.off(Constants.ResponseCodes.Err, onErr); + reject(); + }; + // listen for events + this.once(Constants.ResponseCodes.Ok, onOk); + this.once(Constants.ResponseCodes.Err, onErr); + + // set other params + await this.sendCommandSetOtherParams(manualAddContacts); + } catch (e) { + reject(e); + } + }); + } + + async setAutoAddContacts() { + return await this.setOtherParams(false); + } + + async setManualAddContacts() { + return await this.setOtherParams(true); + } + + // REQ_TYPE_GET_NEIGHBOURS from Repeater role + // https://github.com/meshcore-dev/MeshCore/pull/833 + // Repeater must be running firmware v1.9.0+ + async getNeighbours( + publicKey, + count = 10, + offset = 0, + orderBy = 0, // 0=newest_to_oldest, 1=oldest_to_newest, 2=strongest_to_weakest, 3=weakest_to_strongest + pubKeyPrefixLength = 8, + ) { + // get neighbours: + // req_data[0] = REQ_TYPE_GET_NEIGHBOURS + // req_data[1] = request_version=0 + // req_data[2] = count=10 how many neighbours to fetch + // req_data[3..4] = offset=0 (uint16_t) + // req_data[5] = order_by=0 + // req_data[6] = pubkey_prefix_len=8 + // req_data[7..10] = random blob (help hash) + const bufferWriter = new BufferWriter(); + bufferWriter.writeByte(Constants.BinaryRequestTypes.GetNeighbours); + bufferWriter.writeByte(0); // request_version=0 + bufferWriter.writeByte(count); + bufferWriter.writeUInt16LE(offset); + bufferWriter.writeByte(orderBy); + bufferWriter.writeByte(pubKeyPrefixLength); + bufferWriter.writeUInt32LE(RandomUtils.getRandomInt(0, 4294967295)); // 4 bytes random blob + + // send binary request + const responseData = await this.sendBinaryRequest( + publicKey, + bufferWriter.toBytes(), + ); + + // parse response + const bufferReader = new BufferReader(responseData); + const totalNeighboursCount = bufferReader.readUInt16LE(); + const resultsCount = bufferReader.readUInt16LE(); + + // parse neighbours list + const neighbours = []; + for (var i = 0; i < resultsCount; i++) { + // read info + const publicKeyPrefix = bufferReader.readBytes(pubKeyPrefixLength); + const heardSecondsAgo = bufferReader.readUInt32LE(); + const snr = bufferReader.readInt8() / 4; + + // add to list + neighbours.push({ + publicKeyPrefix: publicKeyPrefix, + heardSecondsAgo: heardSecondsAgo, + snr: snr, + }); + } + + return { + totalNeighboursCount: totalNeighboursCount, + neighbours: neighbours, + }; + } } export default Connection; diff --git a/src/connection/nodejs_serial_connection.js b/src/connection/nodejs_serial_connection.js index 4bc519d..0383925 100644 --- a/src/connection/nodejs_serial_connection.js +++ b/src/connection/nodejs_serial_connection.js @@ -1,60 +1,56 @@ -import SerialConnection from "./serial_connection.js"; +import SerialConnection from './serial_connection.js'; class NodeJSSerialConnection extends SerialConnection { - - /** - * @param path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401" - */ - constructor(path) { - super(); - this.serialPortPath = path; - } - - async connect() { - - // note: serialport module is only available in NodeJS, you shouldn't use NodeJSSerialConnection from a web browser - const { SerialPort } = await import('serialport'); - - // create new serial port - this.serialPort = new SerialPort({ - autoOpen: false, // don't auto open, we want to control this manually - path: this.serialPortPath, // e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401" - baudRate: 115200, - }); - - this.serialPort.on("open", async () => { - await this.onConnected(); - }); - - this.serialPort.on("close", () => { - this.onDisconnected(); - }); - - this.serialPort.on("error", function(err) { - console.log("SerialPort Error: ", err.message) - }); - - this.serialPort.on("data", async (data) => { - await this.onDataReceived(data); - }); - - // open serial connection - this.serialPort.open(); - - } - - async close() { - try { - await this.serialPort.close(); - } catch(e) { - console.log("failed to close serial port, ignoring...", e); - } - } - - /* override */ async write(bytes) { - this.serialPort.write(bytes); + /** + * @param path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401" + */ + constructor(path) { + super(); + this.serialPortPath = path; + } + + async connect() { + // note: serialport module is only available in NodeJS, you shouldn't use NodeJSSerialConnection from a web browser + const { SerialPort } = await import('serialport'); + + // create new serial port + this.serialPort = new SerialPort({ + autoOpen: false, // don't auto open, we want to control this manually + path: this.serialPortPath, // e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401" + baudRate: 115200, + }); + + this.serialPort.on('open', async () => { + await this.onConnected(); + }); + + this.serialPort.on('close', () => { + this.onDisconnected(); + }); + + this.serialPort.on('error', function (err) { + console.log('SerialPort Error: ', err.message); + }); + + this.serialPort.on('data', async (data) => { + await this.onDataReceived(data); + }); + + // open serial connection + this.serialPort.open(); + } + + async close() { + try { + await this.serialPort.close(); + } catch (e) { + console.log('failed to close serial port, ignoring...', e); } + } + /* override */ async write(bytes) { + this.serialPort.write(bytes); + } } export default NodeJSSerialConnection; diff --git a/src/connection/serial_connection.js b/src/connection/serial_connection.js index b60779f..cf51565 100644 --- a/src/connection/serial_connection.js +++ b/src/connection/serial_connection.js @@ -1,99 +1,100 @@ -import BufferWriter from "../buffer_writer.js"; -import BufferReader from "../buffer_reader.js"; -import Constants from "../constants.js"; -import Connection from "./connection.js"; +import BufferWriter from '../buffer_writer.js'; +import BufferReader from '../buffer_reader.js'; +import Constants from '../constants.js'; +import Connection from './connection.js'; class SerialConnection extends Connection { - - constructor() { - super(); - this.readBuffer = []; - if(this.constructor === SerialConnection){ - throw new Error("SerialConnection is an abstract class and can't be instantiated."); - } - } - - async write(bytes) { - throw new Error("Not Implemented: write must be implemented by SerialConnection sub class."); - } - - async writeFrame(frameType, frameData) { - - // create frame - const frame = new BufferWriter(); - - // add frame header - frame.writeByte(frameType); - frame.writeUInt16LE(frameData.length); - - // add frame data - frame.writeBytes(frameData); - - // write frame to device - await this.write(frame.toBytes()); - - } - - async sendToRadioFrame(data) { - // write "app to radio" frame 0x3c "<" - this.emit("tx", data); - await this.writeFrame(0x3c, data); + constructor() { + super(); + this.readBuffer = []; + if (this.constructor === SerialConnection) { + throw new Error( + "SerialConnection is an abstract class and can't be instantiated.", + ); } + } + + async write(bytes) { + throw new Error( + 'Not Implemented: write must be implemented by SerialConnection sub class.', + ); + } + + async writeFrame(frameType, frameData) { + // create frame + const frame = new BufferWriter(); + + // add frame header + frame.writeByte(frameType); + frame.writeUInt16LE(frameData.length); + + // add frame data + frame.writeBytes(frameData); + + // write frame to device + await this.write(frame.toBytes()); + } + + async sendToRadioFrame(data) { + // write "app to radio" frame 0x3c "<" + this.emit('tx', data); + await this.writeFrame(0x3c, data); + } + + async onDataReceived(value) { + // append received bytes to read buffer + this.readBuffer = [...this.readBuffer, ...value]; + + // process read buffer while there is enough bytes for a frame header + // 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian) + const frameHeaderLength = 3; + while (this.readBuffer.length >= frameHeaderLength) { + try { + // extract frame header + const frameHeader = new BufferReader( + this.readBuffer.slice(0, frameHeaderLength), + ); + + // ensure frame type supported + const frameType = frameHeader.readByte(); + if ( + frameType !== Constants.SerialFrameTypes.Incoming && + frameType !== Constants.SerialFrameTypes.Outgoing + ) { + // unexpected byte, lets skip it and try again + this.readBuffer = this.readBuffer.slice(1); + continue; + } - async onDataReceived(value) { - - // append received bytes to read buffer - this.readBuffer = [ - ...this.readBuffer, - ...value, - ]; - - // process read buffer while there is enough bytes for a frame header - // 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian) - const frameHeaderLength = 3; - while(this.readBuffer.length >= frameHeaderLength){ - try { - - // extract frame header - const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength)); - - // ensure frame type supported - const frameType = frameHeader.readByte(); - if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){ - // unexpected byte, lets skip it and try again - this.readBuffer = this.readBuffer.slice(1); - continue; - } - - // ensure frame length valid - const frameLength = frameHeader.readUInt16LE(); - if(!frameLength){ - // unexpected byte, lets skip it and try again - this.readBuffer = this.readBuffer.slice(1); - continue; - } - - // check if we have received enough bytes for this frame, otherwise wait until more bytes received - const requiredLength = frameHeaderLength + frameLength; - if(this.readBuffer.length < requiredLength){ - break; - } - - // get frame data, and remove it and its frame header from the read buffer - const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength); - this.readBuffer = this.readBuffer.slice(requiredLength); - - // handle received frame - this.onFrameReceived(frameData); + // ensure frame length valid + const frameLength = frameHeader.readUInt16LE(); + if (!frameLength) { + // unexpected byte, lets skip it and try again + this.readBuffer = this.readBuffer.slice(1); + continue; + } - } catch(e) { - console.error("Failed to process frame", e); - break; - } + // check if we have received enough bytes for this frame, otherwise wait until more bytes received + const requiredLength = frameHeaderLength + frameLength; + if (this.readBuffer.length < requiredLength) { + break; } + // get frame data, and remove it and its frame header from the read buffer + const frameData = this.readBuffer.slice( + frameHeaderLength, + requiredLength, + ); + this.readBuffer = this.readBuffer.slice(requiredLength); + + // handle received frame + this.onFrameReceived(frameData); + } catch (e) { + console.error('Failed to process frame', e); + break; + } } - + } } export default SerialConnection; diff --git a/src/connection/tcp_connection.js b/src/connection/tcp_connection.js index eab0e16..ce4f6a3 100644 --- a/src/connection/tcp_connection.js +++ b/src/connection/tcp_connection.js @@ -1,136 +1,131 @@ -import BufferWriter from "../buffer_writer.js"; -import BufferReader from "../buffer_reader.js"; -import Constants from "../constants.js"; -import Connection from "./connection.js"; +import BufferWriter from '../buffer_writer.js'; +import BufferReader from '../buffer_reader.js'; +import Constants from '../constants.js'; +import Connection from './connection.js'; class TCPConnection extends Connection { - - constructor(host, port) { - super(); - this.host = host; - this.port = port; - this.readBuffer = []; - } - - async connect() { - - // note: net module is only available in NodeJS, you shouldn't use TCPConnection from a web browser - const { Socket } = await import("net"); - - // create new socket - this.socket = new Socket(); - - // handle received data - this.socket.on('data', (data) => { - this.onSocketDataReceived(data); - }); - - // handle errors - this.socket.on('error', (error) => { - console.error('Connection Error', error); - }); - - // handle socket close - this.socket.on('close', (error) => { - this.onDisconnected(); - }); - - // connect to server - this.socket.connect(this.port, this.host, async () => { - await this.onConnected(); - }); - - } - - onSocketDataReceived(data) { - - // append received bytes to read buffer - this.readBuffer = [ - ...this.readBuffer, - ...data, - ]; - - // process read buffer while there is enough bytes for a frame header - // 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian) - const frameHeaderLength = 3; - while(this.readBuffer.length >= frameHeaderLength){ - try { - - // extract frame header - const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength)); - - // ensure frame type supported - const frameType = frameHeader.readByte(); - if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){ - // unexpected byte, lets skip it and try again - this.readBuffer = this.readBuffer.slice(1); - continue; - } - - // ensure frame length valid - const frameLength = frameHeader.readUInt16LE(); - if(!frameLength){ - // unexpected byte, lets skip it and try again - this.readBuffer = this.readBuffer.slice(1); - continue; - } - - // check if we have received enough bytes for this frame, otherwise wait until more bytes received - const requiredLength = frameHeaderLength + frameLength; - if(this.readBuffer.length < requiredLength){ - break; - } - - // get frame data, and remove it and its frame header from the read buffer - const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength); - this.readBuffer = this.readBuffer.slice(requiredLength); - - // handle received frame - this.onFrameReceived(frameData); - - } catch(e) { - console.error("Failed to process frame", e); - break; - } + constructor(host, port) { + super(); + this.host = host; + this.port = port; + this.readBuffer = []; + } + + async connect() { + // note: net module is only available in NodeJS, you shouldn't use TCPConnection from a web browser + const { Socket } = await import('net'); + + // create new socket + this.socket = new Socket(); + + // handle received data + this.socket.on('data', (data) => { + this.onSocketDataReceived(data); + }); + + // handle errors + this.socket.on('error', (error) => { + console.error('Connection Error', error); + }); + + // handle socket close + this.socket.on('close', (error) => { + this.onDisconnected(); + }); + + // connect to server + this.socket.connect(this.port, this.host, async () => { + await this.onConnected(); + }); + } + + onSocketDataReceived(data) { + // append received bytes to read buffer + this.readBuffer = [...this.readBuffer, ...data]; + + // process read buffer while there is enough bytes for a frame header + // 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian) + const frameHeaderLength = 3; + while (this.readBuffer.length >= frameHeaderLength) { + try { + // extract frame header + const frameHeader = new BufferReader( + this.readBuffer.slice(0, frameHeaderLength), + ); + + // ensure frame type supported + const frameType = frameHeader.readByte(); + if ( + frameType !== Constants.SerialFrameTypes.Incoming && + frameType !== Constants.SerialFrameTypes.Outgoing + ) { + // unexpected byte, lets skip it and try again + this.readBuffer = this.readBuffer.slice(1); + continue; } - } + // ensure frame length valid + const frameLength = frameHeader.readUInt16LE(); + if (!frameLength) { + // unexpected byte, lets skip it and try again + this.readBuffer = this.readBuffer.slice(1); + continue; + } - close() { - try { - this.socket.destroy(); - } catch(e) { - // console.log("failed to release lock on serial port readable, ignoring...", e); + // check if we have received enough bytes for this frame, otherwise wait until more bytes received + const requiredLength = frameHeaderLength + frameLength; + if (this.readBuffer.length < requiredLength) { + break; } - } - async write(bytes) { - this.socket.write(new Uint8Array(bytes)); + // get frame data, and remove it and its frame header from the read buffer + const frameData = this.readBuffer.slice( + frameHeaderLength, + requiredLength, + ); + this.readBuffer = this.readBuffer.slice(requiredLength); + + // handle received frame + this.onFrameReceived(frameData); + } catch (e) { + console.error('Failed to process frame', e); + break; + } } + } - async writeFrame(frameType, frameData) { - - // create frame - const frame = new BufferWriter(); + close() { + try { + this.socket.destroy(); + } catch (e) { + // console.log("failed to release lock on serial port readable, ignoring...", e); + } + } - // add frame header - frame.writeByte(frameType); - frame.writeUInt16LE(frameData.length); + async write(bytes) { + this.socket.write(new Uint8Array(bytes)); + } - // add frame data - frame.writeBytes(frameData); + async writeFrame(frameType, frameData) { + // create frame + const frame = new BufferWriter(); - // write frame to device - await this.write(frame.toBytes()); + // add frame header + frame.writeByte(frameType); + frame.writeUInt16LE(frameData.length); - } + // add frame data + frame.writeBytes(frameData); - async sendToRadioFrame(data) { - // write "app to radio" frame 0x3c "<" - this.emit("tx", data); - await this.writeFrame(0x3c, data); - } + // write frame to device + await this.write(frame.toBytes()); + } + async sendToRadioFrame(data) { + // write "app to radio" frame 0x3c "<" + this.emit('tx', data); + await this.writeFrame(0x3c, data); + } } export default TCPConnection; diff --git a/src/connection/web_ble_connection.js b/src/connection/web_ble_connection.js index dad7c86..c2ec7a2 100644 --- a/src/connection/web_ble_connection.js +++ b/src/connection/web_ble_connection.js @@ -1,106 +1,109 @@ -import Constants from "../constants.js"; -import Connection from "./connection.js"; +import Constants from '../constants.js'; +import Connection from './connection.js'; class WebBleConnection extends Connection { - - constructor(bleDevice) { - super(); - this.bleDevice = bleDevice; - this.gattServer = null; - this.rxCharacteristic = null; - this.txCharacteristic = null; - this.init(); + constructor(bleDevice) { + super(); + this.bleDevice = bleDevice; + this.gattServer = null; + this.rxCharacteristic = null; + this.txCharacteristic = null; + this.init(); + } + + static async open() { + // ensure browser supports web bluetooth + if (!navigator.bluetooth) { + alert('Web Bluetooth is not supported in this browser'); + return; } - static async open() { - - // ensure browser supports web bluetooth - if(!navigator.bluetooth){ - alert("Web Bluetooth is not supported in this browser"); - return; - } - - // ask user to select device - const device = await navigator.bluetooth.requestDevice({ - filters: [ - { - services: [ - Constants.Ble.ServiceUuid.toLowerCase(), - ], - }, - ], - }); - - // make sure user selected a device - if(!device){ - return null; - } - - return new WebBleConnection(device); - + // ask user to select device + const device = await navigator.bluetooth.requestDevice({ + filters: [ + { + services: [Constants.Ble.ServiceUuid.toLowerCase()], + }, + ], + }); + + // make sure user selected a device + if (!device) { + return null; } - async init() { - - // listen for ble disconnect - this.bleDevice.addEventListener("gattserverdisconnected", () => { - this.onDisconnected(); - }); - - // connect to gatt server - this.gattServer = await this.bleDevice.gatt.connect(); - - // find service - const service = await this.gattServer.getPrimaryService(Constants.Ble.ServiceUuid.toLowerCase()); - const characteristics = await service.getCharacteristics(); - - // find rx characteristic (we write to this one, it's where the radio reads from) - this.rxCharacteristic = characteristics.find((characteristic) => { - return characteristic.uuid.toLowerCase() === Constants.Ble.CharacteristicUuidRx.toLowerCase(); - }); - - // find tx characteristic (we read this one, it's where the radio writes to) - this.txCharacteristic = characteristics.find((characteristic) => { - return characteristic.uuid.toLowerCase() === Constants.Ble.CharacteristicUuidTx.toLowerCase(); - }); - - // listen for frames from transmitted to us from the ble device - await this.txCharacteristic.startNotifications(); - this.txCharacteristic.addEventListener('characteristicvaluechanged', (event) => { - const frame = new Uint8Array(event.target.value.buffer); - this.onFrameReceived(frame); - }); - - // fire connected event - await this.onConnected(); - + return new WebBleConnection(device); + } + + async init() { + // listen for ble disconnect + this.bleDevice.addEventListener('gattserverdisconnected', () => { + this.onDisconnected(); + }); + + // connect to gatt server + this.gattServer = await this.bleDevice.gatt.connect(); + + // find service + const service = await this.gattServer.getPrimaryService( + Constants.Ble.ServiceUuid.toLowerCase(), + ); + const characteristics = await service.getCharacteristics(); + + // find rx characteristic (we write to this one, it's where the radio reads from) + this.rxCharacteristic = characteristics.find((characteristic) => { + return ( + characteristic.uuid.toLowerCase() === + Constants.Ble.CharacteristicUuidRx.toLowerCase() + ); + }); + + // find tx characteristic (we read this one, it's where the radio writes to) + this.txCharacteristic = characteristics.find((characteristic) => { + return ( + characteristic.uuid.toLowerCase() === + Constants.Ble.CharacteristicUuidTx.toLowerCase() + ); + }); + + // listen for frames from transmitted to us from the ble device + await this.txCharacteristic.startNotifications(); + this.txCharacteristic.addEventListener( + 'characteristicvaluechanged', + (event) => { + const frame = new Uint8Array(event.target.value.buffer); + this.onFrameReceived(frame); + }, + ); + + // fire connected event + await this.onConnected(); + } + + async close() { + try { + this.gattServer?.disconnect(); + this.gattServer = null; + } catch (e) { + // ignore error when disconnecting } - - async close() { - try { - this.gattServer?.disconnect(); - this.gattServer = null; - } catch(e) { - // ignore error when disconnecting - } - } - - async write(bytes) { - try { - // fixme: NetworkError: GATT operation already in progress. - // todo: implement mutex to prevent multiple writes when another write is in progress - // we write to the rx characteristic, as that's where the radio reads from - await this.rxCharacteristic.writeValue(bytes); - } catch(e) { - console.log("failed to write to ble device", e); - } - } - - async sendToRadioFrame(frame) { - this.emit("tx", frame); - await this.write(frame); + } + + async write(bytes) { + try { + // fixme: NetworkError: GATT operation already in progress. + // todo: implement mutex to prevent multiple writes when another write is in progress + // we write to the rx characteristic, as that's where the radio reads from + await this.rxCharacteristic.writeValue(bytes); + } catch (e) { + console.log('failed to write to ble device', e); } + } + async sendToRadioFrame(frame) { + this.emit('tx', frame); + await this.write(frame); + } } export default WebBleConnection; diff --git a/src/connection/web_serial_connection.js b/src/connection/web_serial_connection.js index a9eae02..6dee579 100644 --- a/src/connection/web_serial_connection.js +++ b/src/connection/web_serial_connection.js @@ -1,105 +1,93 @@ -import SerialConnection from "./serial_connection.js"; +import SerialConnection from './serial_connection.js'; class WebSerialConnection extends SerialConnection { - - constructor(serialPort) { - - super(); - - this.serialPort = serialPort; - this.reader = serialPort.readable.getReader(); - this.writable = serialPort.writable; - this.readLoop(); - - // listen for disconnect - this.serialPort.addEventListener("disconnect", () => { - this.onDisconnected(); - }); - - // fire connected callback after constructor has returned - setTimeout(async () => { - await this.onConnected(); - }, 0); - + constructor(serialPort) { + super(); + + this.serialPort = serialPort; + this.reader = serialPort.readable.getReader(); + this.writable = serialPort.writable; + this.readLoop(); + + // listen for disconnect + this.serialPort.addEventListener('disconnect', () => { + this.onDisconnected(); + }); + + // fire connected callback after constructor has returned + setTimeout(async () => { + await this.onConnected(); + }, 0); + } + + static async open() { + // ensure browser supports web serial + if (!navigator.serial) { + alert('Web Serial is not supported in this browser'); + return null; } - static async open() { - - // ensure browser supports web serial - if(!navigator.serial){ - alert("Web Serial is not supported in this browser"); - return null; - } - - // ask user to select device - const serialPort = await navigator.serial.requestPort({ - filters: [], - }); - - // open port - await serialPort.open({ - baudRate: 115200, - }); - - return new WebSerialConnection(serialPort); - + // ask user to select device + const serialPort = await navigator.serial.requestPort({ + filters: [], + }); + + // open port + await serialPort.open({ + baudRate: 115200, + }); + + return new WebSerialConnection(serialPort); + } + + async close() { + // release reader lock + try { + this.reader.releaseLock(); + } catch (e) { + // console.log("failed to release lock on serial port readable, ignoring...", e); } - async close() { - - // release reader lock - try { - this.reader.releaseLock(); - } catch(e) { - // console.log("failed to release lock on serial port readable, ignoring...", e); - } - - // close serial port - try { - await this.serialPort.close(); - } catch(e) { - // console.log("failed to close serial port, ignoring...", e); - } - + // close serial port + try { + await this.serialPort.close(); + } catch (e) { + // console.log("failed to close serial port, ignoring...", e); } - - /* override */ async write(bytes) { - const writer = this.writable.getWriter(); - try { - await writer.write(new Uint8Array(bytes)); - } finally { - writer.releaseLock(); - } + } + + /* override */ async write(bytes) { + const writer = this.writable.getWriter(); + try { + await writer.write(new Uint8Array(bytes)); + } finally { + writer.releaseLock(); } - - async readLoop() { - try { - while(true){ - - // read bytes until reader indicates it's done - const { value, done } = await this.reader.read(); - if(done){ - break; - } - - // pass to super class handler - await this.onDataReceived(value); - - } - } catch(error) { - - // ignore error if reader was released - if(error instanceof TypeError){ - return; - } - - console.error('Error reading from serial port: ', error); - - } finally { - this.reader.releaseLock(); + } + + async readLoop() { + try { + while (true) { + // read bytes until reader indicates it's done + const { value, done } = await this.reader.read(); + if (done) { + break; } - } + // pass to super class handler + await this.onDataReceived(value); + } + } catch (error) { + // ignore error if reader was released + if (error instanceof TypeError) { + return; + } + + console.error('Error reading from serial port: ', error); + } finally { + this.reader.releaseLock(); + } + } } export default WebSerialConnection; diff --git a/src/constants.js b/src/constants.js index 2eb2706..b4d9f22 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,131 +1,129 @@ class Constants { + static SupportedCompanionProtocolVersion = 1; - static SupportedCompanionProtocolVersion = 1; + static SerialFrameTypes = { + Incoming: 0x3e, // ">" + Outgoing: 0x3c, // "<" + }; - static SerialFrameTypes = { - Incoming: 0x3e, // ">" - Outgoing: 0x3c, // "<" - } + static Ble = { + ServiceUuid: '6E400001-B5A3-F393-E0A9-E50E24DCCA9E', + CharacteristicUuidRx: '6E400002-B5A3-F393-E0A9-E50E24DCCA9E', + CharacteristicUuidTx: '6E400003-B5A3-F393-E0A9-E50E24DCCA9E', + }; - static Ble = { - ServiceUuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E", - CharacteristicUuidRx: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E", - CharacteristicUuidTx: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E", - } + static CommandCodes = { + AppStart: 1, + SendTxtMsg: 2, + SendChannelTxtMsg: 3, + GetContacts: 4, + GetDeviceTime: 5, + SetDeviceTime: 6, + SendSelfAdvert: 7, + SetAdvertName: 8, + AddUpdateContact: 9, + SyncNextMessage: 10, + SetRadioParams: 11, + SetTxPower: 12, + ResetPath: 13, + SetAdvertLatLon: 14, + RemoveContact: 15, + ShareContact: 16, + ExportContact: 17, + ImportContact: 18, + Reboot: 19, + GetBatteryVoltage: 20, + SetTuningParams: 21, // todo + DeviceQuery: 22, + ExportPrivateKey: 23, + ImportPrivateKey: 24, + SendRawData: 25, + SendLogin: 26, // todo + SendStatusReq: 27, // todo + GetChannel: 31, + SetChannel: 32, + SignStart: 33, + SignData: 34, + SignFinish: 35, + SendTracePath: 36, + // todo set device pin command + SetOtherParams: 38, + SendTelemetryReq: 39, - static CommandCodes = { - AppStart: 1, - SendTxtMsg: 2, - SendChannelTxtMsg: 3, - GetContacts: 4, - GetDeviceTime: 5, - SetDeviceTime: 6, - SendSelfAdvert: 7, - SetAdvertName: 8, - AddUpdateContact: 9, - SyncNextMessage: 10, - SetRadioParams: 11, - SetTxPower: 12, - ResetPath: 13, - SetAdvertLatLon: 14, - RemoveContact: 15, - ShareContact: 16, - ExportContact: 17, - ImportContact: 18, - Reboot: 19, - GetBatteryVoltage: 20, - SetTuningParams: 21, // todo - DeviceQuery: 22, - ExportPrivateKey: 23, - ImportPrivateKey: 24, - SendRawData: 25, - SendLogin: 26, // todo - SendStatusReq: 27, // todo - GetChannel: 31, - SetChannel: 32, - SignStart: 33, - SignData: 34, - SignFinish: 35, - SendTracePath: 36, - // todo set device pin command - SetOtherParams: 38, - SendTelemetryReq: 39, + SendBinaryReq: 50, + }; - SendBinaryReq: 50, - } + static ResponseCodes = { + Ok: 0, // todo + Err: 1, // todo + ContactsStart: 2, + Contact: 3, + EndOfContacts: 4, + SelfInfo: 5, + Sent: 6, + ContactMsgRecv: 7, + ChannelMsgRecv: 8, + CurrTime: 9, + NoMoreMessages: 10, + ExportContact: 11, + BatteryVoltage: 12, + DeviceInfo: 13, + PrivateKey: 14, + Disabled: 15, + ChannelInfo: 18, + SignStart: 19, + Signature: 20, + }; - static ResponseCodes = { - Ok: 0, // todo - Err: 1, // todo - ContactsStart: 2, - Contact: 3, - EndOfContacts: 4, - SelfInfo: 5, - Sent: 6, - ContactMsgRecv: 7, - ChannelMsgRecv: 8, - CurrTime: 9, - NoMoreMessages: 10, - ExportContact: 11, - BatteryVoltage: 12, - DeviceInfo: 13, - PrivateKey: 14, - Disabled: 15, - ChannelInfo: 18, - SignStart: 19, - Signature: 20, - } + static PushCodes = { + Advert: 0x80, // when companion is set to auto add contacts + PathUpdated: 0x81, + SendConfirmed: 0x82, + MsgWaiting: 0x83, + RawData: 0x84, + LoginSuccess: 0x85, + LoginFail: 0x86, // not usable yet + StatusResponse: 0x87, + LogRxData: 0x88, + TraceData: 0x89, + NewAdvert: 0x8a, // when companion is set to manually add contacts + TelemetryResponse: 0x8b, + BinaryResponse: 0x8c, + }; - static PushCodes = { - Advert: 0x80, // when companion is set to auto add contacts - PathUpdated: 0x81, - SendConfirmed: 0x82, - MsgWaiting: 0x83, - RawData: 0x84, - LoginSuccess: 0x85, - LoginFail: 0x86, // not usable yet - StatusResponse: 0x87, - LogRxData: 0x88, - TraceData: 0x89, - NewAdvert: 0x8A, // when companion is set to manually add contacts - TelemetryResponse: 0x8B, - BinaryResponse: 0x8C, - } + static ErrorCodes = { + UnsupportedCmd: 1, + NotFound: 2, + TableFull: 3, + BadState: 4, + FileIoError: 5, + IllegalArg: 6, + }; - static ErrorCodes = { - UnsupportedCmd: 1, - NotFound: 2, - TableFull: 3, - BadState: 4, - FileIoError: 5, - IllegalArg: 6, - } + static AdvType = { + None: 0, + Chat: 1, + Repeater: 2, + Room: 3, + }; - static AdvType = { - None: 0, - Chat: 1, - Repeater: 2, - Room: 3, - } + static SelfAdvertTypes = { + ZeroHop: 0, + Flood: 1, + }; - static SelfAdvertTypes = { - ZeroHop: 0, - Flood: 1, - } - - static TxtTypes = { - Plain: 0, - CliData: 1, - SignedPlain: 2, - } - - static BinaryRequestTypes = { - GetTelemetryData: 0x03, // #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 - GetAvgMinMax: 0x04, // #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 - GetAccessList: 0x05, // #define REQ_TYPE_GET_ACCESS_LIST 0x05 - GetNeighbours: 0x06, // #define REQ_TYPE_GET_NEIGHBOURS 0x06 - } + static TxtTypes = { + Plain: 0, + CliData: 1, + SignedPlain: 2, + }; + static BinaryRequestTypes = { + GetTelemetryData: 0x03, // #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + GetAvgMinMax: 0x04, // #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 + GetAccessList: 0x05, // #define REQ_TYPE_GET_ACCESS_LIST 0x05 + GetNeighbours: 0x06, // #define REQ_TYPE_GET_NEIGHBOURS 0x06 + }; } export default Constants; diff --git a/src/events.js b/src/events.js index 606fff2..234434e 100644 --- a/src/events.js +++ b/src/events.js @@ -1,60 +1,50 @@ class EventEmitter { - - constructor() { - this.eventListenersMap = new Map(); - } - - on(event, callback) { - - // create list of listeners for event if it doesn't exist - if(!this.eventListenersMap.has(event)){ - this.eventListenersMap.set(event, []); - } - - // add listener for event - this.eventListenersMap.get(event).push(callback); - - } - - off(event, callback) { - - // remove callback from listeners for this event - if(this.eventListenersMap.has(event)){ - const callbacks = this.eventListenersMap.get(event).filter(cb => cb !== callback); - this.eventListenersMap.set(event, callbacks); - } - + constructor() { + this.eventListenersMap = new Map(); + } + + on(event, callback) { + // create list of listeners for event if it doesn't exist + if (!this.eventListenersMap.has(event)) { + this.eventListenersMap.set(event, []); } - once(event, callback) { - - // internal callback to handle the event - const internalCallback = (...data) => { - - // we received an event, so lets remove the event listener - this.off(event, internalCallback); - - // fire the original callback provided by the user - setTimeout(() => callback(...data), 0); - - }; - - // listen to this event - this.on(event, internalCallback); - + // add listener for event + this.eventListenersMap.get(event).push(callback); + } + + off(event, callback) { + // remove callback from listeners for this event + if (this.eventListenersMap.has(event)) { + const callbacks = this.eventListenersMap + .get(event) + .filter((cb) => cb !== callback); + this.eventListenersMap.set(event, callbacks); } - - emit(event, ...data) { - - // invoke each listener for this event - if(this.eventListenersMap.has(event)){ - for(const eventListener of this.eventListenersMap.get(event)){ - setTimeout(() => eventListener(...data), 0); - } - } - + } + + once(event, callback) { + // internal callback to handle the event + const internalCallback = (...data) => { + // we received an event, so lets remove the event listener + this.off(event, internalCallback); + + // fire the original callback provided by the user + setTimeout(() => callback(...data), 0); + }; + + // listen to this event + this.on(event, internalCallback); + } + + emit(event, ...data) { + // invoke each listener for this event + if (this.eventListenersMap.has(event)) { + for (const eventListener of this.eventListenersMap.get(event)) { + setTimeout(() => eventListener(...data), 0); + } } - + } } export default EventEmitter; diff --git a/src/index.js b/src/index.js index af95004..fed0352 100644 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,25 @@ -import Connection from "./connection/connection.js"; -import WebBleConnection from "./connection/web_ble_connection.js"; -import SerialConnection from "./connection/serial_connection.js"; -import NodeJSSerialConnection from "./connection/nodejs_serial_connection.js"; -import WebSerialConnection from "./connection/web_serial_connection.js"; -import TCPConnection from "./connection/tcp_connection.js"; -import Constants from "./constants.js"; -import Advert from "./advert.js"; -import Packet from "./packet.js"; -import BufferUtils from "./buffer_utils.js"; -import CayenneLpp from "./cayenne_lpp.js"; +import Connection from './connection/connection.js'; +import WebBleConnection from './connection/web_ble_connection.js'; +import SerialConnection from './connection/serial_connection.js'; +import NodeJSSerialConnection from './connection/nodejs_serial_connection.js'; +import WebSerialConnection from './connection/web_serial_connection.js'; +import TCPConnection from './connection/tcp_connection.js'; +import Constants from './constants.js'; +import Advert from './advert.js'; +import Packet from './packet.js'; +import BufferUtils from './buffer_utils.js'; +import CayenneLpp from './cayenne_lpp.js'; export { - Connection, - WebBleConnection, - SerialConnection, - NodeJSSerialConnection, - WebSerialConnection, - TCPConnection, - Constants, - Advert, - Packet, - BufferUtils, - CayenneLpp, + Connection, + WebBleConnection, + SerialConnection, + NodeJSSerialConnection, + WebSerialConnection, + TCPConnection, + Constants, + Advert, + Packet, + BufferUtils, + CayenneLpp, }; diff --git a/src/packet.js b/src/packet.js index c3cfa2e..13c5c97 100644 --- a/src/packet.js +++ b/src/packet.js @@ -1,215 +1,223 @@ -import BufferReader from "./buffer_reader.js"; -import Advert from "./advert.js"; +import BufferReader from './buffer_reader.js'; +import Advert from './advert.js'; class Packet { - - // Packet::header values - static PH_ROUTE_MASK = 0x03; // 2-bits - static PH_TYPE_SHIFT = 2; - static PH_TYPE_MASK = 0x0F; // 4-bits - static PH_VER_SHIFT = 6; - static PH_VER_MASK = 0x03; // 2-bits - - static ROUTE_TYPE_RESERVED1 = 0x00; // FUTURE - static ROUTE_TYPE_FLOOD = 0x01; // flood mode, needs 'path' to be built up (max 64 bytes) - static ROUTE_TYPE_DIRECT = 0x02; // direct route, 'path' is supplied - static ROUTE_TYPE_RESERVED2 = 0x03; // FUTURE - - static PAYLOAD_TYPE_REQ = 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) - static PAYLOAD_TYPE_RESPONSE = 0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) - static PAYLOAD_TYPE_TXT_MSG = 0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) - static PAYLOAD_TYPE_ACK = 0x03; // a simple ack - static PAYLOAD_TYPE_ADVERT = 0x04; // a node advertising its Identity - static PAYLOAD_TYPE_GRP_TXT = 0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg") - static PAYLOAD_TYPE_GRP_DATA = 0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob) - static PAYLOAD_TYPE_ANON_REQ = 0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) - static PAYLOAD_TYPE_PATH = 0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) - static PAYLOAD_TYPE_TRACE = 0x09; // trace a path, collecting SNR for each hop - static PAYLOAD_TYPE_RAW_CUSTOM = 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc - - constructor(header, path, payload) { - - - this.header = header; - this.path = path; - this.payload = payload; - - // parsed info - this.route_type = this.getRouteType(); - this.route_type_string = this.getRouteTypeString(); - this.payload_type = this.getPayloadType(); - this.payload_type_string = this.getPayloadTypeString(); - this.payload_version = this.getPayloadVer(); - this.is_marked_do_not_retransmit = this.isMarkedDoNotRetransmit(); - - } - - static fromBytes(bytes) { - const bufferReader = new BufferReader(bytes); - const header = bufferReader.readByte(); - const pathLen = bufferReader.readInt8(); - const path = bufferReader.readBytes(pathLen); - const payload = bufferReader.readRemainingBytes(); - return new Packet(header, path, payload); - } - - getRouteType() { - return this.header & Packet.PH_ROUTE_MASK; - } - - getRouteTypeString() { - switch(this.getRouteType()){ - case Packet.ROUTE_TYPE_FLOOD: return "FLOOD"; - case Packet.ROUTE_TYPE_DIRECT: return "DIRECT"; - default: return null; - } - } - - isRouteFlood() { - return this.getRouteType() === Packet.ROUTE_TYPE_FLOOD; - } - - isRouteDirect() { - return this.getRouteType() === Packet.ROUTE_TYPE_DIRECT; - } - - getPayloadType() { - return (this.header >> Packet.PH_TYPE_SHIFT) & Packet.PH_TYPE_MASK; - } - - getPayloadTypeString() { - switch(this.getPayloadType()){ - case Packet.PAYLOAD_TYPE_REQ: return "REQ"; - case Packet.PAYLOAD_TYPE_RESPONSE: return "RESPONSE"; - case Packet.PAYLOAD_TYPE_TXT_MSG: return "TXT_MSG"; - case Packet.PAYLOAD_TYPE_ACK: return "ACK"; - case Packet.PAYLOAD_TYPE_ADVERT: return "ADVERT"; - case Packet.PAYLOAD_TYPE_GRP_TXT: return "GRP_TXT"; - case Packet.PAYLOAD_TYPE_GRP_DATA: return "GRP_DATA"; - case Packet.PAYLOAD_TYPE_ANON_REQ: return "ANON_REQ"; - case Packet.PAYLOAD_TYPE_PATH: return "PATH"; - case Packet.PAYLOAD_TYPE_TRACE: return "TRACE"; - case Packet.PAYLOAD_TYPE_RAW_CUSTOM: return "RAW_CUSTOM"; - default: return null; - } - } - - getPayloadVer() { - return (this.header >> Packet.PH_VER_SHIFT) & Packet.PH_VER_MASK; - } - - markDoNotRetransmit() { - this.header = 0xFF; - } - - isMarkedDoNotRetransmit() { - return this.header === 0xFF; - } - - parsePayload() { - switch(this.getPayloadType()){ - case Packet.PAYLOAD_TYPE_PATH: return this.parsePayloadTypePath(); - case Packet.PAYLOAD_TYPE_REQ: return this.parsePayloadTypeReq(); - case Packet.PAYLOAD_TYPE_RESPONSE: return this.parsePayloadTypeResponse(); - case Packet.PAYLOAD_TYPE_TXT_MSG: return this.parsePayloadTypeTxtMsg(); - case Packet.PAYLOAD_TYPE_ACK: return this.parsePayloadTypeAck(); - case Packet.PAYLOAD_TYPE_ADVERT: return this.parsePayloadTypeAdvert(); - case Packet.PAYLOAD_TYPE_ANON_REQ: return this.parsePayloadTypeAnonReq(); - default: return null; - } - } - - parsePayloadTypePath() { - - // parse bytes - const bufferReader = new BufferReader(this.payload); - const dest = bufferReader.readByte(); - const src = bufferReader.readByte(); - // todo other fields - - return { - src: src, - dest: dest, - }; - - } - - parsePayloadTypeReq() { - - // parse bytes - const bufferReader = new BufferReader(this.payload); - const dest = bufferReader.readByte(); - const src = bufferReader.readByte(); - const encrypted = bufferReader.readRemainingBytes(); - - return { - src: src, - dest: dest, - encrypted: encrypted, - }; - - } - - parsePayloadTypeResponse() { - - // parse bytes - const bufferReader = new BufferReader(this.payload); - const dest = bufferReader.readByte(); - const src = bufferReader.readByte(); - // todo other fields - - return { - src: src, - dest: dest, - }; - - } - - parsePayloadTypeTxtMsg() { - - // parse bytes - const bufferReader = new BufferReader(this.payload); - const dest = bufferReader.readByte(); - const src = bufferReader.readByte(); - // todo other fields - - return { - src: src, - dest: dest, - }; - - } - - parsePayloadTypeAck() { - return { - ack_code: this.payload, - }; - } - - parsePayloadTypeAdvert() { - const advert = Advert.fromBytes(this.payload); - return { - public_key: advert.publicKey, - timestamp: advert.timestamp, - app_data: advert.parseAppData(), - }; - } - - parsePayloadTypeAnonReq() { - - // parse bytes - const bufferReader = new BufferReader(this.payload); - const dest = bufferReader.readByte(); - const srcPublicKey = bufferReader.readBytes(32); - // todo other fields - - return { - src: srcPublicKey, - dest: dest, - }; - - } - + // Packet::header values + static PH_ROUTE_MASK = 0x03; // 2-bits + static PH_TYPE_SHIFT = 2; + static PH_TYPE_MASK = 0x0f; // 4-bits + static PH_VER_SHIFT = 6; + static PH_VER_MASK = 0x03; // 2-bits + + static ROUTE_TYPE_RESERVED1 = 0x00; // FUTURE + static ROUTE_TYPE_FLOOD = 0x01; // flood mode, needs 'path' to be built up (max 64 bytes) + static ROUTE_TYPE_DIRECT = 0x02; // direct route, 'path' is supplied + static ROUTE_TYPE_RESERVED2 = 0x03; // FUTURE + + static PAYLOAD_TYPE_REQ = 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) + static PAYLOAD_TYPE_RESPONSE = 0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) + static PAYLOAD_TYPE_TXT_MSG = 0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) + static PAYLOAD_TYPE_ACK = 0x03; // a simple ack + static PAYLOAD_TYPE_ADVERT = 0x04; // a node advertising its Identity + static PAYLOAD_TYPE_GRP_TXT = 0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg") + static PAYLOAD_TYPE_GRP_DATA = 0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob) + static PAYLOAD_TYPE_ANON_REQ = 0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) + static PAYLOAD_TYPE_PATH = 0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) + static PAYLOAD_TYPE_TRACE = 0x09; // trace a path, collecting SNR for each hop + static PAYLOAD_TYPE_RAW_CUSTOM = 0x0f; // custom packet as raw bytes, for applications with custom encryption, payloads, etc + + constructor(header, path, payload) { + this.header = header; + this.path = path; + this.payload = payload; + + // parsed info + this.route_type = this.getRouteType(); + this.route_type_string = this.getRouteTypeString(); + this.payload_type = this.getPayloadType(); + this.payload_type_string = this.getPayloadTypeString(); + this.payload_version = this.getPayloadVer(); + this.is_marked_do_not_retransmit = this.isMarkedDoNotRetransmit(); + } + + static fromBytes(bytes) { + const bufferReader = new BufferReader(bytes); + const header = bufferReader.readByte(); + const pathLen = bufferReader.readInt8(); + const path = bufferReader.readBytes(pathLen); + const payload = bufferReader.readRemainingBytes(); + return new Packet(header, path, payload); + } + + getRouteType() { + return this.header & Packet.PH_ROUTE_MASK; + } + + getRouteTypeString() { + switch (this.getRouteType()) { + case Packet.ROUTE_TYPE_FLOOD: + return 'FLOOD'; + case Packet.ROUTE_TYPE_DIRECT: + return 'DIRECT'; + default: + return null; + } + } + + isRouteFlood() { + return this.getRouteType() === Packet.ROUTE_TYPE_FLOOD; + } + + isRouteDirect() { + return this.getRouteType() === Packet.ROUTE_TYPE_DIRECT; + } + + getPayloadType() { + return (this.header >> Packet.PH_TYPE_SHIFT) & Packet.PH_TYPE_MASK; + } + + getPayloadTypeString() { + switch (this.getPayloadType()) { + case Packet.PAYLOAD_TYPE_REQ: + return 'REQ'; + case Packet.PAYLOAD_TYPE_RESPONSE: + return 'RESPONSE'; + case Packet.PAYLOAD_TYPE_TXT_MSG: + return 'TXT_MSG'; + case Packet.PAYLOAD_TYPE_ACK: + return 'ACK'; + case Packet.PAYLOAD_TYPE_ADVERT: + return 'ADVERT'; + case Packet.PAYLOAD_TYPE_GRP_TXT: + return 'GRP_TXT'; + case Packet.PAYLOAD_TYPE_GRP_DATA: + return 'GRP_DATA'; + case Packet.PAYLOAD_TYPE_ANON_REQ: + return 'ANON_REQ'; + case Packet.PAYLOAD_TYPE_PATH: + return 'PATH'; + case Packet.PAYLOAD_TYPE_TRACE: + return 'TRACE'; + case Packet.PAYLOAD_TYPE_RAW_CUSTOM: + return 'RAW_CUSTOM'; + default: + return null; + } + } + + getPayloadVer() { + return (this.header >> Packet.PH_VER_SHIFT) & Packet.PH_VER_MASK; + } + + markDoNotRetransmit() { + this.header = 0xff; + } + + isMarkedDoNotRetransmit() { + return this.header === 0xff; + } + + parsePayload() { + switch (this.getPayloadType()) { + case Packet.PAYLOAD_TYPE_PATH: + return this.parsePayloadTypePath(); + case Packet.PAYLOAD_TYPE_REQ: + return this.parsePayloadTypeReq(); + case Packet.PAYLOAD_TYPE_RESPONSE: + return this.parsePayloadTypeResponse(); + case Packet.PAYLOAD_TYPE_TXT_MSG: + return this.parsePayloadTypeTxtMsg(); + case Packet.PAYLOAD_TYPE_ACK: + return this.parsePayloadTypeAck(); + case Packet.PAYLOAD_TYPE_ADVERT: + return this.parsePayloadTypeAdvert(); + case Packet.PAYLOAD_TYPE_ANON_REQ: + return this.parsePayloadTypeAnonReq(); + default: + return null; + } + } + + parsePayloadTypePath() { + // parse bytes + const bufferReader = new BufferReader(this.payload); + const dest = bufferReader.readByte(); + const src = bufferReader.readByte(); + // todo other fields + + return { + src: src, + dest: dest, + }; + } + + parsePayloadTypeReq() { + // parse bytes + const bufferReader = new BufferReader(this.payload); + const dest = bufferReader.readByte(); + const src = bufferReader.readByte(); + const encrypted = bufferReader.readRemainingBytes(); + + return { + src: src, + dest: dest, + encrypted: encrypted, + }; + } + + parsePayloadTypeResponse() { + // parse bytes + const bufferReader = new BufferReader(this.payload); + const dest = bufferReader.readByte(); + const src = bufferReader.readByte(); + // todo other fields + + return { + src: src, + dest: dest, + }; + } + + parsePayloadTypeTxtMsg() { + // parse bytes + const bufferReader = new BufferReader(this.payload); + const dest = bufferReader.readByte(); + const src = bufferReader.readByte(); + // todo other fields + + return { + src: src, + dest: dest, + }; + } + + parsePayloadTypeAck() { + return { + ack_code: this.payload, + }; + } + + parsePayloadTypeAdvert() { + const advert = Advert.fromBytes(this.payload); + return { + public_key: advert.publicKey, + timestamp: advert.timestamp, + app_data: advert.parseAppData(), + }; + } + + parsePayloadTypeAnonReq() { + // parse bytes + const bufferReader = new BufferReader(this.payload); + const dest = bufferReader.readByte(); + const srcPublicKey = bufferReader.readBytes(32); + // todo other fields + + return { + src: srcPublicKey, + dest: dest, + }; + } } export default Packet; diff --git a/src/random_utils.js b/src/random_utils.js index a66bad3..675ec74 100644 --- a/src/random_utils.js +++ b/src/random_utils.js @@ -1,11 +1,9 @@ class RandomUtils { - - static getRandomInt(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; - } - + static getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } } export default RandomUtils;