mirror of
https://github.com/aljazceru/meshcore-web.git
synced 2025-12-17 08:14:19 +01:00
352 lines
11 KiB
JavaScript
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;
|