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
59
index.html
59
index.html
|
|
@ -15,7 +15,7 @@
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div class="flex border bg-gray-50 p-3 rounded shadow">
|
<div class="flex border bg-gray-50 p-3 rounded shadow">
|
||||||
<div class="my-auto">
|
<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 class="text-sm">Developed by <a target="_blank" href="https://liamcottle.com" class="text-blue-500 hover:underline">Liam Cottle</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,7 +24,10 @@
|
||||||
|
|
||||||
<div class="p-3 border-t space-x-1">
|
<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">
|
<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>
|
||||||
<button @click="disconnect" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
<button @click="disconnect" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||||
Disconnect
|
Disconnect
|
||||||
|
|
@ -81,8 +84,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Device from "./src/device.js";
|
|
||||||
import Constants from "./src/constants.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({
|
Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -94,28 +98,19 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async askForSerialPort() {
|
async askForSerialPort() {
|
||||||
|
this.connection = await SerialConnection.open();
|
||||||
if(!navigator.serial){
|
},
|
||||||
alert("Web Serial is not supported in this browser");
|
async askForBleDevice() {
|
||||||
return null;
|
this.connection = await BleConnection.open();
|
||||||
}
|
|
||||||
|
|
||||||
// ask user to select device
|
|
||||||
const serialPort = await navigator.serial.requestPort({
|
|
||||||
filters: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.device = await Device.fromSerialPort(serialPort);
|
|
||||||
|
|
||||||
},
|
},
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
if(this.device){
|
if(this.connection){
|
||||||
await this.device.close();
|
await this.connection.close();
|
||||||
this.device = null;
|
this.device = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async sendCommandAppStart() {
|
async sendCommandAppStart() {
|
||||||
await this.device.sendCommandAppStart();
|
await this.connection.sendCommandAppStart();
|
||||||
},
|
},
|
||||||
async sendCommandSendTxtMsg() {
|
async sendCommandSendTxtMsg() {
|
||||||
const txtType = Constants.TxtTypes.Plain;
|
const txtType = Constants.TxtTypes.Plain;
|
||||||
|
|
@ -123,35 +118,35 @@
|
||||||
const senderTimestamp = Math.floor(Date.now() / 1000);
|
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 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}`;
|
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) {
|
async sendSendSelfAdvert(type) {
|
||||||
await this.device.sendCommandSendSelfAdvert(type);
|
await this.connection.sendCommandSendSelfAdvert(type);
|
||||||
},
|
},
|
||||||
async sendCommandGetContacts() {
|
async sendCommandGetContacts() {
|
||||||
await this.device.sendCommandGetContacts();
|
await this.connection.sendCommandGetContacts();
|
||||||
},
|
},
|
||||||
async sendCommandGetDeviceTime() {
|
async sendCommandGetDeviceTime() {
|
||||||
await this.device.sendCommandGetDeviceTime();
|
await this.connection.sendCommandGetDeviceTime();
|
||||||
},
|
},
|
||||||
async sendCommandSetDeviceTime() {
|
async sendCommandSetDeviceTime() {
|
||||||
const timestamp = Math.floor(Date.now() / 1000);
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
await this.device.sendCommandSetDeviceTime(timestamp);
|
await this.connection.sendCommandSetDeviceTime(timestamp);
|
||||||
},
|
},
|
||||||
async sendCommandSetTxPower() {
|
async sendCommandSetTxPower() {
|
||||||
const txPower = 22;
|
const txPower = 22;
|
||||||
await this.device.sendCommandSetTxPower(txPower);
|
await this.connection.sendCommandSetTxPower(txPower);
|
||||||
},
|
},
|
||||||
async sendCommandResetPath() {
|
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]);
|
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() {
|
async sendCommandSetRadioParams() {
|
||||||
const radioFreq = 917375;
|
const radioFreq = 917375;
|
||||||
const radioBw = 250000;
|
const radioBw = 250000;
|
||||||
const radioSf = 7;
|
const radioSf = 7;
|
||||||
const radioCr = 5;
|
const radioCr = 5;
|
||||||
await this.device.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr);
|
await this.connection.sendCommandSetRadioParams(radioFreq, radioBw, radioSf, radioCr);
|
||||||
},
|
},
|
||||||
async sendCommandSetAdvertName() {
|
async sendCommandSetAdvertName() {
|
||||||
|
|
||||||
|
|
@ -162,17 +157,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// set name
|
// set name
|
||||||
await this.device.sendCommandSetAdvertName(name);
|
await this.connection.sendCommandSetAdvertName(name);
|
||||||
|
|
||||||
},
|
},
|
||||||
async sendCommandSetAdvertLatLon() {
|
async sendCommandSetAdvertLatLon() {
|
||||||
const lat = 123;
|
const lat = 123;
|
||||||
const lon = 456;
|
const lon = 456;
|
||||||
await this.device.sendCommandSetAdvertLatLon(lat, lon);
|
await this.connection.sendCommandSetAdvertLatLon(lat, lon);
|
||||||
},
|
},
|
||||||
async sendCommandRemoveContact() {
|
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]);
|
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() {
|
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]);
|
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 lastAdvert = 1739244825;
|
||||||
const advLat = 0;
|
const advLat = 0;
|
||||||
const advLon = 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() {
|
async sendCommandSyncNextMessage() {
|
||||||
await this.device.sendCommandSyncNextMessage();
|
await this.connection.sendCommandSyncNextMessage();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|
|
||||||
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 BufferWriter from "../buffer_writer.js";
|
||||||
import BufferReader from "./buffer_reader.js";
|
import BufferReader from "../buffer_reader.js";
|
||||||
import Constants from "./constants.js";
|
import Constants from "../constants.js";
|
||||||
|
|
||||||
class Device {
|
class Connection {
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
throw new Error("This method must be implemented by the subclass.");
|
||||||
// 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) {
|
async sendToRadioFrame(data) {
|
||||||
// write "app to radio" frame 0x3c "<"
|
throw new Error("This method must be implemented by the subclass.");
|
||||||
await this.writeFrame(0x3c, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendCommandAppStart() {
|
async sendCommandAppStart() {
|
||||||
|
|
@ -188,81 +128,6 @@ class Device {
|
||||||
await this.sendToRadioFrame(data.toBytes());
|
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) {
|
onFrameReceived(frame) {
|
||||||
|
|
||||||
// console.log("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 = {
|
static SerialFrameTypes = {
|
||||||
Incoming: 0x3e, // ">"
|
Incoming: 0x3e, // ">"
|
||||||
Outgoing: 0x3c, // "<"
|
Outgoing: 0x3c, // "<"
|
||||||
};
|
}
|
||||||
|
|
||||||
|
static Ble = {
|
||||||
|
ServiceUuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E",
|
||||||
|
CharacteristicUuidRx: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E",
|
||||||
|
CharacteristicUuidTx: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E",
|
||||||
|
}
|
||||||
|
|
||||||
static CommandCodes = {
|
static CommandCodes = {
|
||||||
AppStart: 1,
|
AppStart: 1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue