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

352 lines
11 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";
import NotificationUtils from "./NotificationUtils.js";
class Connection {
// enable to log raw tx/rx bytes to console
static log = false;
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;
}
// clear previous connection state
GlobalState.contacts = [];
GlobalState.batteryPercentage = null;
// 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;
// clear previous connection timers
clearInterval(GlobalState.batteryPercentageInterval);
GlobalState.batteryPercentageInterval = 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;
});
// log raw tx bytes if enabled
GlobalState.connection.on("tx", async (data) => {
if(this.log){
console.log("tx", data);
}
});
// log raw rx bytes if enabled
GlobalState.connection.on("rx", async (data) => {
if(this.log){
console.log("rx", data);
}
});
// 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, event.roundTrip);
});
// initial setup without needing database
await this.loadSelfInfo();
await this.syncDeviceTime();
// wait for database to be ready
await databaseToBeReady;
// fetch data after database is ready
await this.loadContacts();
await this.syncMessages();
await this.updateBatteryPercentage();
// auto update battery percentage once per minute
GlobalState.batteryPercentageInterval = setInterval(async () => {
await this.updateBatteryPercentage();
}, 60000);
}
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 updateBatteryPercentage() {
if(GlobalState.connection){
try {
const response = await GlobalState.connection.getBatteryVoltage();
GlobalState.batteryPercentage = Utils.getBatteryPercentage(response.batteryMilliVolts);
} catch(e) {
// ignore error
}
}
}
static async setAdvertName(name) {
await GlobalState.connection.setAdvertName(name);
}
static async setAdvertLatLong(latitude, longitude) {
await GlobalState.connection.setAdvertLatLong(latitude, longitude);
}
static async setTxPower(txPower) {
await GlobalState.connection.setTxPower(txPower);
}
static async setRadioParams(radioFreq, radioBw, radioSf, radioCr) {
await GlobalState.connection.setRadioParams(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 shareContact(publicKey) {
await GlobalState.connection.shareContact(publicKey);
}
static async exportContact(publicKey) {
return await GlobalState.connection.exportContact(publicKey);
}
static async sendMessage(publicKey, text) {
// send message
const message = await GlobalState.connection.sendTextMessage(publicKey, text);
// save to database
const databaseMessage = 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,
});
// mark message as failed after estimated timeout
setTimeout(async () => {
await Database.Message.setMessageFailedById(databaseMessage.id, "timeout");
}, message.estTimeout);
}
static async sendChannelMessage(channelIdx, text) {
// send message
await GlobalState.connection.sendChannelTextMessage(channelIdx, text);
// save to database
await Database.ChannelMessage.insert({
channel_idx: channelIdx,
from: GlobalState.selfInfo.publicKey,
path_len: null,
txt_type: Constants.TxtTypes.Plain,
sender_timestamp: Date.now(),
text: text,
});
}
static async syncMessages() {
while(true){
// sync messages until no more returned
const message = await GlobalState.connection.syncNextMessage();
if(!message){
break;
}
// handle received message
if(message.contactMessage){
await this.onContactMessageReceived(message.contactMessage);
} else if(message.channelMessage) {
await this.onChannelMessageReceived(message.channelMessage);
}
}
}
static async reboot() {
await GlobalState.connection.reboot();
}
static async onContactMessageReceived(message) {
console.log("onContactMessageReceived", 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;
}
// save message to database
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,
expected_ack_crc: null,
error: null,
});
// show notification
await NotificationUtils.showNewMessageNotification(contact, message.text);
}
static async onChannelMessageReceived(message) {
console.log("onChannelMessageReceived", message);
// save message to database
await Database.ChannelMessage.insert({
channel_idx: message.channelIdx,
from: null,
path_len: message.pathLen,
txt_type: message.txtType,
sender_timestamp: message.senderTimestamp,
text: message.text,
});
}
}
export default Connection;