implement ble connection and tidy up classes

This commit is contained in:
liamcottle 2025-02-11 21:39:18 +13:00
parent 6a3e522106
commit 8f53e9b849
5 changed files with 301 additions and 175 deletions

View file

@ -15,7 +15,7 @@
<!-- header -->
<div class="flex border bg-gray-50 p-3 rounded shadow">
<div class="my-auto">
<div class="font-bold">MeshCore Client</div>
<div class="font-bold">MeshCore Connection</div>
<div class="text-sm">Developed by <a target="_blank" href="https://liamcottle.com" class="text-blue-500 hover:underline">Liam Cottle</a></div>
</div>
</div>
@ -24,7 +24,10 @@
<div class="p-3 border-t space-x-1">
<button @click="askForSerialPort" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Connect
Connect (Serial)
</button>
<button @click="askForBleDevice" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Connect (BLE)
</button>
<button @click="disconnect" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Disconnect
@ -81,8 +84,9 @@
</div>
<script type="module">
import Device from "./src/device.js";
import Constants from "./src/constants.js";
import SerialConnection from "./src/connection/serial_connection.js";
import BleConnection from "./src/connection/ble_connection.js";
Vue.createApp({
data() {
return {
@ -94,28 +98,19 @@
},
methods: {
async askForSerialPort() {
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// ask user to select device
const serialPort = await navigator.serial.requestPort({
filters: [],
});
this.device = await Device.fromSerialPort(serialPort);
this.connection = await SerialConnection.open();
},
async askForBleDevice() {
this.connection = await BleConnection.open();
},
async disconnect() {
if(this.device){
await this.device.close();
if(this.connection){
await this.connection.close();
this.device = null;
}
},
async sendCommandAppStart() {
await this.device.sendCommandAppStart();
await this.connection.sendCommandAppStart();
},
async sendCommandSendTxtMsg() {
const txtType = Constants.TxtTypes.Plain;
@ -123,35 +118,35 @@
const senderTimestamp = Math.floor(Date.now() / 1000);
const pubKeyPrefix = new Uint8Array([148, 63, 175, 162, 88, 212, 192, 40, 214, 185, 213, 140, 42, 145, 194, 186, 70, 71, 112, 68, 0, 192, 65, 4, 105, 143, 230, 50, 162, 79, 247, 192]);
const text = `Test Message: ${senderTimestamp}`;
await this.device.sendCommandSendTxtMsg(txtType, attempt, senderTimestamp, pubKeyPrefix, text);
await this.connection.sendCommandSendTxtMsg(txtType, attempt, senderTimestamp, pubKeyPrefix, text);
},
async sendSendSelfAdvert(type) {
await this.device.sendCommandSendSelfAdvert(type);
await this.connection.sendCommandSendSelfAdvert(type);
},
async sendCommandGetContacts() {
await this.device.sendCommandGetContacts();
await this.connection.sendCommandGetContacts();
},
async sendCommandGetDeviceTime() {
await this.device.sendCommandGetDeviceTime();
await this.connection.sendCommandGetDeviceTime();
},
async sendCommandSetDeviceTime() {
const timestamp = Math.floor(Date.now() / 1000);
await this.device.sendCommandSetDeviceTime(timestamp);
await this.connection.sendCommandSetDeviceTime(timestamp);
},
async sendCommandSetTxPower() {
const txPower = 22;
await this.device.sendCommandSetTxPower(txPower);
await this.connection.sendCommandSetTxPower(txPower);
},
async sendCommandResetPath() {
const publicKey = new Uint8Array([244, 231, 60, 250, 245, 218, 131, 156, 156, 98, 130, 39, 222, 43, 123, 147, 98, 200, 218, 251, 242, 89, 111, 108, 25, 191, 127, 151, 222, 192, 233, 177]);
await this.device.sendCommandResetPath(publicKey);
await this.connection.sendCommandResetPath(publicKey);
},
async sendCommandSetRadioParams() {
const radioFreq = 917375;
const radioBw = 250000;
const radioSf = 7;
const radioCr = 5;
await this.device.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr);
await this.connection.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr);
},
async sendCommandSetAdvertName() {
@ -162,17 +157,17 @@
}
// set name
await this.device.sendCommandSetAdvertName(name);
await this.connection.sendCommandSetAdvertName(name);
},
async sendCommandSetAdvertLatLon() {
const lat = 123;
const lon = 456;
await this.device.sendCommandSetAdvertLatLon(lat, lon);
await this.connection.sendCommandSetAdvertLatLon(lat, lon);
},
async sendCommandRemoveContact() {
const publicKey = new Uint8Array([148, 63, 175, 162, 88, 212, 192, 40, 214, 185, 213, 140, 42, 145, 194, 186, 70, 71, 112, 68, 0, 192, 65, 4, 105, 143, 230, 50, 162, 79, 247, 192]);
await this.device.sendCommandRemoveContact(publicKey);
await this.connection.sendCommandRemoveContact(publicKey);
},
async sendCommandAddUpdateContact() {
const publicKey = new Uint8Array([148, 63, 175, 162, 88, 212, 192, 40, 214, 185, 213, 140, 42, 145, 194, 186, 70, 71, 112, 68, 0, 192, 65, 4, 105, 143, 230, 50, 162, 79, 247, 192]);
@ -184,10 +179,10 @@
const lastAdvert = 1739244825;
const advLat = 0;
const advLon = 0;
await this.device.sendCommandAddUpdateContact(publicKey, type, flags, outPathLen, outPath, advName, lastAdvert, advLat, advLon);
await this.connection.sendCommandAddUpdateContact(publicKey, type, flags, outPathLen, outPath, advName, lastAdvert, advLat, advLon);
},
async sendCommandSyncNextMessage() {
await this.device.sendCommandSyncNextMessage();
await this.connection.sendCommandSyncNextMessage();
},
},
}).mount('#app');

View file

@ -0,0 +1,95 @@
import Constants from "../constants.js";
import Connection from "./connection.js";
class BleConnection extends Connection {
constructor(bleDevice) {
super();
this.bleDevice = bleDevice;
this.gattServer = null;
this.rxCharacteristic = null;
this.txCharacteristic = null;
this.init();
}
static async open() {
// ensure browser supports web bluetooth
if(!navigator.bluetooth){
alert("Web Bluetooth is not supported in this browser");
return;
}
// ask user to select device
const device = await navigator.bluetooth.requestDevice({
filters: [
{
services: [
Constants.Ble.ServiceUuid.toLowerCase(),
],
},
],
});
// make sure user selected a device
if(!device){
return null;
}
return new BleConnection(device);
}
async init() {
// connect to gatt server
this.gattServer = await this.bleDevice.gatt.connect();
// find service
const service = await this.gattServer.getPrimaryService(Constants.Ble.ServiceUuid.toLowerCase());
const characteristics = await service.getCharacteristics();
// find rx characteristic (we write to this one, it's where the radio reads from)
this.rxCharacteristic = characteristics.find((characteristic) => {
return characteristic.uuid.toLowerCase() === Constants.Ble.CharacteristicUuidRx.toLowerCase();
});
// find tx characteristic (we read this one, it's where the radio writes to)
this.txCharacteristic = characteristics.find((characteristic) => {
return characteristic.uuid.toLowerCase() === Constants.Ble.CharacteristicUuidTx.toLowerCase();
});
// listen for frames from transmitted to us from the ble device
await this.txCharacteristic.startNotifications();
this.txCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
const frame = new Uint8Array(event.target.value.buffer);
this.onFrameReceived(frame);
});
}
async close() {
try {
this.gattServer?.disconnect();
this.gattServer = null;
} catch(e) {
// ignore error when disconnecting
}
}
async write(bytes) {
try {
// we write to the rx characteristic, as that's where the radio reads from
await this.rxCharacteristic.writeValue(bytes);
} catch(e) {
console.log("failed to write to ble device", e);
}
}
async sendToRadioFrame(data) {
await this.write(data);
}
}
export default BleConnection;

View file

@ -1,75 +1,15 @@
import BufferWriter from "./buffer_writer.js";
import BufferReader from "./buffer_reader.js";
import Constants from "./constants.js";
import BufferWriter from "../buffer_writer.js";
import BufferReader from "../buffer_reader.js";
import Constants from "../constants.js";
class Device {
constructor(serialPort) {
this.serialPort = serialPort;
this.reader = serialPort.readable.getReader();
this.writable = serialPort.writable;
this.readBuffer = [];
this.readLoop();
}
static async fromSerialPort(serialPort) {
// open port
await serialPort.open({
baudRate: 115200,
});
return new Device(serialPort);
}
class Connection {
async close() {
// release reader lock
try {
this.reader.releaseLock();
} catch(e) {
// console.log("failed to release lock on serial port readable, ignoring...", e);
}
// close serial port
try {
await this.serialPort.close();
} catch(e) {
// console.log("failed to close serial port, ignoring...", e);
}
}
async write(bytes) {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
} finally {
writer.releaseLock();
}
}
async writeFrame(frameType, frameData) {
// create frame
const frame = new BufferWriter();
// add frame header
frame.writeByte(frameType);
frame.writeUInt16LE(frameData.length);
// add frame data
frame.writeBytes(frameData);
// write frame to device
await this.write(frame.toBytes());
throw new Error("This method must be implemented by the subclass.");
}
async sendToRadioFrame(data) {
// write "app to radio" frame 0x3c "<"
await this.writeFrame(0x3c, data);
throw new Error("This method must be implemented by the subclass.");
}
async sendCommandAppStart() {
@ -188,81 +128,6 @@ class Device {
await this.sendToRadioFrame(data.toBytes());
}
async readLoop() {
try {
while(true){
// read bytes until reader indicates it's done
const { value, done } = await this.reader.read();
if(done){
break;
}
// append received bytes to read buffer
this.readBuffer = [
...this.readBuffer,
...value,
];
// process read buffer while there is enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
const frameHeaderLength = 3;
while(this.readBuffer.length >= frameHeaderLength){
try {
// extract frame header
const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength));
// ensure frame type supported
const frameType = frameHeader.readByte();
if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// ensure frame length valid
const frameLength = frameHeader.readUInt16LE();
if(!frameLength){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// check if we have received enough bytes for this frame, otherwise wait until more bytes received
const requiredLength = frameHeaderLength + frameLength;
if(this.readBuffer.length < requiredLength){
break;
}
// get frame data, and remove it and its frame header from the read buffer
const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength);
this.readBuffer = this.readBuffer.slice(requiredLength);
// handle received frame
this.onFrameReceived(frameData);
} catch(e) {
console.error("Failed to process frame", e);
break;
}
}
}
} catch(error) {
// ignore error if reader was released
if(error instanceof TypeError){
return;
}
console.error('Error reading from serial port: ', error);
} finally {
this.reader.releaseLock();
}
}
onFrameReceived(frame) {
// console.log("onFrameReceived", frame);
@ -391,4 +256,4 @@ class Device {
}
export default Device;
export default Connection;

View file

@ -0,0 +1,165 @@
import BufferWriter from "../buffer_writer.js";
import BufferReader from "../buffer_reader.js";
import Constants from "../constants.js";
import Connection from "./connection.js";
class SerialConnection extends Connection {
constructor(serialPort) {
super();
this.serialPort = serialPort;
this.reader = serialPort.readable.getReader();
this.writable = serialPort.writable;
this.readBuffer = [];
this.readLoop();
}
static async open() {
// ensure browser supports web serial
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// ask user to select device
const serialPort = await navigator.serial.requestPort({
filters: [],
});
// open port
await serialPort.open({
baudRate: 115200,
});
return new SerialConnection(serialPort);
}
async close() {
// release reader lock
try {
this.reader.releaseLock();
} catch(e) {
// console.log("failed to release lock on serial port readable, ignoring...", e);
}
// close serial port
try {
await this.serialPort.close();
} catch(e) {
// console.log("failed to close serial port, ignoring...", e);
}
}
async write(bytes) {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
} finally {
writer.releaseLock();
}
}
async writeFrame(frameType, frameData) {
// create frame
const frame = new BufferWriter();
// add frame header
frame.writeByte(frameType);
frame.writeUInt16LE(frameData.length);
// add frame data
frame.writeBytes(frameData);
// write frame to device
await this.write(frame.toBytes());
}
async sendToRadioFrame(data) {
// write "app to radio" frame 0x3c "<"
await this.writeFrame(0x3c, data);
}
async readLoop() {
try {
while(true){
// read bytes until reader indicates it's done
const { value, done } = await this.reader.read();
if(done){
break;
}
// append received bytes to read buffer
this.readBuffer = [
...this.readBuffer,
...value,
];
// process read buffer while there is enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
const frameHeaderLength = 3;
while(this.readBuffer.length >= frameHeaderLength){
try {
// extract frame header
const frameHeader = new BufferReader(this.readBuffer.slice(0, frameHeaderLength));
// ensure frame type supported
const frameType = frameHeader.readByte();
if(frameType !== Constants.SerialFrameTypes.Incoming && frameType !== Constants.SerialFrameTypes.Outgoing){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// ensure frame length valid
const frameLength = frameHeader.readUInt16LE();
if(!frameLength){
// unexpected byte, lets skip it and try again
this.readBuffer = this.readBuffer.slice(1);
continue;
}
// check if we have received enough bytes for this frame, otherwise wait until more bytes received
const requiredLength = frameHeaderLength + frameLength;
if(this.readBuffer.length < requiredLength){
break;
}
// get frame data, and remove it and its frame header from the read buffer
const frameData = this.readBuffer.slice(frameHeaderLength, requiredLength);
this.readBuffer = this.readBuffer.slice(requiredLength);
// handle received frame
this.onFrameReceived(frameData);
} catch(e) {
console.error("Failed to process frame", e);
break;
}
}
}
} catch(error) {
// ignore error if reader was released
if(error instanceof TypeError){
return;
}
console.error('Error reading from serial port: ', error);
} finally {
this.reader.releaseLock();
}
}
}
export default SerialConnection;

View file

@ -3,7 +3,13 @@ class Constants {
static SerialFrameTypes = {
Incoming: 0x3e, // ">"
Outgoing: 0x3c, // "<"
};
}
static Ble = {
ServiceUuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E",
CharacteristicUuidRx: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E",
CharacteristicUuidTx: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E",
}
static CommandCodes = {
AppStart: 1,