From 84df5aa57f549b2fe856169147160cafd1020090 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 24 Feb 2025 19:28:09 +1300 Subject: [PATCH] implement login and status request for fetching metrics from repeaters --- index.html | 27 ++++++ src/buffer_reader.js | 6 ++ src/buffer_utils.js | 24 +++++ src/connection/connection.js | 177 +++++++++++++++++++++++++++++++++++ src/constants.js | 6 ++ 5 files changed, 240 insertions(+) create mode 100644 src/buffer_utils.js diff --git a/index.html b/index.html index bf4c1e1..33ca763 100644 --- a/index.html +++ b/index.html @@ -136,6 +136,7 @@
Message
Set Path
+
GetStats
Share (Zero Hop)
Export
Reset Path
@@ -435,6 +436,32 @@ alert("Failed to get battery voltage!"); } }, + async statusRequest(contact) { + + // ask user for password + const password = prompt("Please enter Admin, or Guest password"); + if(!password){ + return; + } + + try { + + // log in to repeater + const response = await this.connection.login(contact.publicKey, password); + console.log("login response", response); + + // request status + const statusResponse = await this.connection.getStatus(contact.publicKey); + console.log("status response", statusResponse); + + // show status response + alert(`status request success:\n${JSON.stringify(statusResponse, null, 4)}`); + + } catch(e) { + alert(`Failed to login: ${e}`); + } + + }, bytesToHex(uint8Array) { return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join(''); }, diff --git a/src/buffer_reader.js b/src/buffer_reader.js index 1871e5f..a4f5e07 100644 --- a/src/buffer_reader.js +++ b/src/buffer_reader.js @@ -59,6 +59,12 @@ class BufferReader { return view.getUint32(0, true); } + readInt16LE() { + const bytes = this.readBytes(2); + const view = new DataView(bytes.buffer); + return view.getInt16(0, true); + } + readInt32LE() { const bytes = this.readBytes(4); const view = new DataView(bytes.buffer); diff --git a/src/buffer_utils.js b/src/buffer_utils.js new file mode 100644 index 0000000..fdb029d --- /dev/null +++ b/src/buffer_utils.js @@ -0,0 +1,24 @@ +class BufferUtils { + + 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; + + } + +} + +export default BufferUtils; diff --git a/src/connection/connection.js b/src/connection/connection.js index 7129cc4..15979e5 100644 --- a/src/connection/connection.js +++ b/src/connection/connection.js @@ -2,6 +2,7 @@ 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"; class Connection extends EventEmitter { @@ -205,6 +206,21 @@ class Connection extends EventEmitter { 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()); + } + onFrameReceived(frame) { // emit received frame @@ -253,6 +269,10 @@ class Connection extends EventEmitter { this.onSendConfirmedPush(bufferReader); } else if(responseCode === Constants.PushCodes.MsgWaiting){ this.onMsgWaitingPush(bufferReader); + } else if(responseCode === Constants.PushCodes.LoginSuccess){ + this.onLoginSuccessPush(bufferReader); + } else if(responseCode === Constants.PushCodes.StatusResponse){ + this.onStatusResponsePush(bufferReader); } else { console.log("unhandled frame", frame); } @@ -284,6 +304,21 @@ class Connection extends EventEmitter { }); } + 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(), + }); + } + onOkResponse(bufferReader) { this.emit(Constants.ResponseCodes.Ok, { @@ -1180,6 +1215,148 @@ class Connection extends EventEmitter { }); } + 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 + const onSent = (response) => { + + // remove error listener since we received sent response + this.once(Constants.ResponseCodes.Err, onErr); + + // reject login request as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + setTimeout(() => { + 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 + 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 = () => { + 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 + const onSent = (response) => { + + // remove error listener since we received sent response + this.once(Constants.ResponseCodes.Err, onErr); + + // reject login request as timed out after estimated delay, plus a bit extra + const estTimeout = response.estTimeout + extraTimeoutMillis; + setTimeout(() => { + 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 + 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; + curr_free_queue_len: bufferReader.readUInt16LE(), // uint16_t curr_free_queue_len; + 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 + n_full_events: bufferReader.readUInt16LE(), // uint16_t n_full_events + reserved1: bufferReader.readUInt16LE(), // uint16_t reserved1 + 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 = () => { + 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); + } + }); + } + } export default Connection; diff --git a/src/constants.js b/src/constants.js index 567aa02..3583a52 100644 --- a/src/constants.js +++ b/src/constants.js @@ -36,6 +36,9 @@ class Constants { DeviceQuery: 22, ExportPrivateKey: 23, ImportPrivateKey: 24, + SendRawData: 25, // todo + SendLogin: 26, // todo + SendStatusReq: 27, // todo } static ResponseCodes = { @@ -62,6 +65,9 @@ class Constants { PathUpdated: 0x81, SendConfirmed: 0x82, MsgWaiting: 0x83, + LoginSuccess: 0x85, + LoginFail: 0x86, // not usable yet + StatusResponse: 0x87, } static AdvType = {