From c90bf3ddd9ffb952638a956157525ae090811710 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 26 Sep 2025 17:51:51 +1200 Subject: [PATCH] add ability to fetch repeater neighbours via binary request --- src/connection/connection.js | 145 +++++++++++++++++++++++++++++++++++ src/constants.js | 10 +++ 2 files changed, 155 insertions(+) diff --git a/src/connection/connection.js b/src/connection/connection.js index 1f2c503..e8d930e 100644 --- a/src/connection/connection.js +++ b/src/connection/connection.js @@ -252,6 +252,14 @@ class Connection extends EventEmitter { 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); @@ -349,6 +357,8 @@ class Connection extends EventEmitter { 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); } @@ -420,6 +430,14 @@ class Connection extends EventEmitter { }); } + 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(); @@ -1637,6 +1655,75 @@ class Connection extends EventEmitter { }); } + 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) => { @@ -1940,6 +2027,64 @@ class Connection extends EventEmitter { return await this.setOtherParams(true); } + // REQ_TYPE_GET_NEIGHBOURS from Repeater role + 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/constants.js b/src/constants.js index 43b2996..708ba07 100644 --- a/src/constants.js +++ b/src/constants.js @@ -48,6 +48,8 @@ class Constants { // todo set device pin command SetOtherParams: 38, SendTelemetryReq: 39, + + SendBinaryReq: 50, } static ResponseCodes = { @@ -83,6 +85,7 @@ class Constants { TraceData: 0x89, NewAdvert: 0x8A, // when companion is set to manually add contacts TelemetryResponse: 0x8B, + BinaryResponse: 0x8C, } static ErrorCodes = { @@ -112,6 +115,13 @@ class Constants { 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;