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 = {