mirror of
https://github.com/meshcore-dev/meshcore.js.git
synced 2026-04-20 22:13:49 +00:00
implement ble connection and tidy up classes
This commit is contained in:
parent
6a3e522106
commit
8f53e9b849
5 changed files with 301 additions and 175 deletions
95
src/connection/ble_connection.js
Normal file
95
src/connection/ble_connection.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
165
src/connection/serial_connection.js
Normal file
165
src/connection/serial_connection.js
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue