Files
meshcore-web/src/js/Connection.js
2025-02-13 16:16:00 +13:00

253 lines
8.1 KiB
JavaScript

import GlobalState from "./GlobalState.js";
import {BleConnection, Constants, SerialConnection} from "@liamcottle/meshcore.js";
import Database from "./Database.js";
import Utils from "./Utils.js";
class Connection {
static async connectViaBluetooth() {
try {
await this.connect(await BleConnection.open());
return true;
} catch(e) {
console.log(e);
// ignore device not selected error
if(e.name === "NotFoundError"){
return false;
}
// show error message
alert("failed to connect to ble device!");
return false;
}
}
static async connectViaSerial() {
try {
await this.connect(await SerialConnection.open());
return true;
} catch(e) {
console.log(e);
// ignore device not selected error
if(e.name === "NotFoundError"){
return false;
}
// show error message
alert("failed to connect to serial device!");
return false;
}
}
static async connect(connection) {
// do nothing if connection not provided
if(!connection){
return;
}
// update connection and listen for events
GlobalState.connection = connection;
GlobalState.connection.on("connected", () => this.onConnected());
GlobalState.connection.on("disconnected", () => this.onDisconnected());
}
static async disconnect() {
// disconnect
GlobalState.connection?.close();
// update ui
GlobalState.connection = null;
}
static async onConnected() {
// weird way to allow us to lock all other callbacks from doing anything, until the database is ready...
// maybe we should use some sort of lock or mutex etc.
// basically, we need to wait for SelfInfo to be fetched before we can init the database.
// we use this to create a new database instance that is unique based on the devices public key.
// initDatabase is async, which means all the other callbacks could fire before the database is ready
// this means when we try to access the database when it isn't ready yet, we get fun errors...
// so we need to force the callbacks to wait until the database is ready
// we will just resolve this promise when the database is ready, and all the callbacks should be set to await it
var onDatabaseReady = null;
const databaseToBeReady = new Promise((resolve) => {
onDatabaseReady = resolve;
});
// clear previous connection state
GlobalState.contacts = [];
// listen for self info, and then init database
GlobalState.connection.once(Constants.ResponseCodes.SelfInfo, async (selfInfo) => {
await Database.initDatabase(Utils.bytesToHex(selfInfo.publicKey));
onDatabaseReady();
});
// listen for adverts
GlobalState.connection.on(Constants.PushCodes.Advert, async () => {
console.log("Advert");
await databaseToBeReady;
await this.loadContacts();
});
// listen for path updates
GlobalState.connection.on(Constants.PushCodes.PathUpdated, async (event) => {
console.log("PathUpdated", event);
await databaseToBeReady;
await this.loadContacts();
});
// listen for new message available event
GlobalState.connection.on(Constants.PushCodes.MsgWaiting, async () => {
console.log("MsgWaiting");
await databaseToBeReady;
await this.syncMessages();
});
// listen for message send confirmed events
GlobalState.connection.on(Constants.PushCodes.SendConfirmed, async (event) => {
console.log("SendConfirmed", event);
await databaseToBeReady;
await Database.Message.setMessageDeliveredByAckCode(event.ackCode);
});
// initial setup without needing database
await this.loadSelfInfo();
await this.syncDeviceTime();
// wait for database to be ready
await databaseToBeReady;
// sync messages
await this.loadContacts();
await this.syncMessages();
}
static async onDisconnected() {
await this.disconnect();
}
static async loadSelfInfo() {
GlobalState.selfInfo = await GlobalState.connection.getSelfInfo();
}
static async loadContacts() {
GlobalState.contacts = await GlobalState.connection.getContacts();
}
static async setAdvertName(name) {
await GlobalState.connection.sendCommandSetAdvertName(name);
}
static async setRadioParams(radioFreq, radioBw, radioSf, radioCr, txPower) {
await GlobalState.connection.sendCommandSetTxPower(txPower);
await GlobalState.connection.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr);
}
static async syncDeviceTime() {
const timestamp = Math.floor(Date.now() / 1000);
await GlobalState.connection.sendCommandSetDeviceTime(timestamp);
}
static async resetContactPath(publicKey) {
await GlobalState.connection.sendCommandResetPath(publicKey);
}
static async removeContact(publicKey) {
await GlobalState.connection.sendCommandRemoveContact(publicKey);
}
static async sendMessage(publicKey, text) {
// send message
const message = await GlobalState.connection.sendTextMessage(publicKey, text);
// mark message as failed after estimated timeout
setTimeout(async () => {
await Database.Message.setMessageFailedByAckCode(message.expectedAckCrc, "timeout");
}, message.estTimeout);
// save to database
await Database.Message.insert({
status: "sending",
to: publicKey,
from: GlobalState.selfInfo.publicKey,
path_len: null,
txt_type: Constants.TxtTypes.Plain,
sender_timestamp: Date.now(),
text: text,
timestamp: Date.now(),
expected_ack_crc: message.expectedAckCrc,
send_type: message.result,
error: null,
});
}
static async syncMessages() {
while(true){
// sync messages until no more returned
const message = await GlobalState.connection.syncNextMessage();
if(!message){
break;
}
// handle received message
await this.onMessageReceived(message);
}
}
static async onMessageReceived(message) {
console.log("onMessageReceived", message);
// find first contact that matches this public key prefix
// todo, maybe use the most recently updated contact in case of collision? ideally we should be given the full hash by firmware anyway...
const contact = GlobalState.contacts.find((contact) => {
const messagePublicKeyPrefix = message.pubKeyPrefix;
const contactPublicKeyPrefix = contact.publicKey.slice(0, message.pubKeyPrefix.length);
return Utils.isUint8ArrayEqual(messagePublicKeyPrefix, contactPublicKeyPrefix);
});
// ensure contact exists
// shouldn't be possible to receive a message if firmware doesn't have the contact, since keys will be missing for decryption
// however, it could be possible that the contact doesn't exist in javascript memory when the message is received
if(!contact){
console.log("couldn't find contact, received message has been dropped");
return;
}
await Database.Message.insert({
status: "received",
to: GlobalState.selfInfo.publicKey,
from: contact.publicKey,
path_len: message.pathLen,
txt_type: message.txtType,
sender_timestamp: message.senderTimestamp,
text: message.text,
timestamp: Date.now(),
expected_ack_crc: null,
error: null,
});
}
}
export default Connection;