mirror of
https://github.com/meshcore-dev/meshcore.js.git
synced 2026-04-20 22:13:49 +00:00
initial serial client implementation for meshcore device
This commit is contained in:
parent
897eeecb24
commit
30c918a09a
6 changed files with 574 additions and 1 deletions
122
index.html
Normal file
122
index.html
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>MeshCore</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
</head>
|
||||
<body class="bg-slate-300">
|
||||
|
||||
<div id="app" class="space-y-2 p-3">
|
||||
|
||||
<!-- 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="text-sm">Developed by <a target="_blank" href="https://liamcottle.com" class="text-blue-500 hover:underline">Liam Cottle</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border bg-gray-50 rounded shadow">
|
||||
|
||||
<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
|
||||
</button>
|
||||
<button @click="sendCommandAppStart" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
Send AppStart
|
||||
</button>
|
||||
<button @click="sendSendSelfAdvert(0)" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
Send Advert (Zero Hop)
|
||||
</button>
|
||||
<button @click="sendSendSelfAdvert(1)" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
Send Advert (Flood)
|
||||
</button>
|
||||
<button @click="sendCommandSetAdvertName" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
Set Advert Name
|
||||
</button>
|
||||
<button @click="sendCommandSyncNextMessage" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
SyncNextMessage
|
||||
</button>
|
||||
<button @click="sendCommandGetContacts" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
GetContacts
|
||||
</button>
|
||||
<button @click="sendCommandGetDeviceTime" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
GetDeviceTime
|
||||
</button>
|
||||
<button @click="sendCommandSetDeviceTime" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
||||
SetDeviceTime
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import Device from "./src/device.js";
|
||||
Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
device: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
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);
|
||||
|
||||
},
|
||||
async sendCommandAppStart() {
|
||||
await this.device.sendCommandAppStart();
|
||||
},
|
||||
async sendSendSelfAdvert(type) {
|
||||
await this.device.sendCommandSendSelfAdvert(type);
|
||||
},
|
||||
async sendCommandGetContacts() {
|
||||
await this.device.sendCommandGetContacts();
|
||||
},
|
||||
async sendCommandGetDeviceTime() {
|
||||
await this.device.sendCommandGetDeviceTime();
|
||||
},
|
||||
async sendCommandSetDeviceTime() {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
await this.device.sendCommandSetDeviceTime(timestamp);
|
||||
},
|
||||
async sendCommandSetAdvertName() {
|
||||
|
||||
// ask user for name
|
||||
const name = prompt("Please enter name");
|
||||
if(!name){
|
||||
return;
|
||||
}
|
||||
|
||||
// set name
|
||||
await this.device.sendCommandSetAdvertName(name);
|
||||
|
||||
},
|
||||
async sendCommandSyncNextMessage() {
|
||||
await this.device.sendCommandSyncNextMessage();
|
||||
},
|
||||
},
|
||||
}).mount('#app');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
"name": "@liamcottle/meshcore.js",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "device.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
|
|
|||
53
src/buffer_reader.js
Normal file
53
src/buffer_reader.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
class BufferReader {
|
||||
|
||||
constructor(data) {
|
||||
this.pointer = 0;
|
||||
this.buffer = new Uint8Array(data);
|
||||
}
|
||||
|
||||
readByte() {
|
||||
return this.readBytes(1)[0];
|
||||
}
|
||||
|
||||
readBytes(count) {
|
||||
const data = this.buffer.slice(this.pointer, this.pointer + count);
|
||||
this.pointer += count;
|
||||
return data;
|
||||
}
|
||||
|
||||
readString() {
|
||||
const remainingBytesCount = this.buffer.length - this.pointer;
|
||||
const remainingBytes = this.readBytes(remainingBytesCount);
|
||||
return new TextDecoder().decode(remainingBytes);
|
||||
}
|
||||
|
||||
readCString(maxLength) {
|
||||
const value = [];
|
||||
const bytes = this.readBytes(maxLength);
|
||||
for(const byte of bytes){
|
||||
|
||||
// if we find a null terminator character, we have reached the end of the cstring
|
||||
if(byte === 0){
|
||||
return new TextDecoder().decode(new Uint8Array(value));
|
||||
}
|
||||
|
||||
value.push(byte);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
readUInt16LE() {
|
||||
const bytes = this.readBytes(2);
|
||||
const view = new DataView(bytes.buffer);
|
||||
return view.getUint16(0, true);
|
||||
}
|
||||
|
||||
readUInt32LE() {
|
||||
const bytes = this.readBytes(4);
|
||||
const view = new DataView(bytes.buffer);
|
||||
return view.getUint32(0, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BufferReader;
|
||||
44
src/buffer_writer.js
Normal file
44
src/buffer_writer.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
class BufferWriter {
|
||||
|
||||
constructor() {
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
toBytes() {
|
||||
return new Uint8Array(this.buffer);
|
||||
}
|
||||
|
||||
writeBytes(bytes) {
|
||||
this.buffer = [
|
||||
...this.buffer,
|
||||
...bytes,
|
||||
];
|
||||
}
|
||||
|
||||
writeByte(byte) {
|
||||
this.writeBytes([
|
||||
byte,
|
||||
]);
|
||||
}
|
||||
|
||||
writeUInt16LE(num) {
|
||||
const bytes = new Uint8Array(2);
|
||||
const view = new DataView(bytes.buffer);
|
||||
view.setUint16(0, num, true);
|
||||
this.writeBytes(bytes);
|
||||
}
|
||||
|
||||
writeUInt32LE(num) {
|
||||
const bytes = new Uint8Array(4);
|
||||
const view = new DataView(bytes.buffer);
|
||||
view.setUint32(0, num, true);
|
||||
this.writeBytes(bytes);
|
||||
}
|
||||
|
||||
writeString(string) {
|
||||
this.writeBytes(new TextEncoder().encode(string));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BufferWriter;
|
||||
51
src/constants.js
Normal file
51
src/constants.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
class Constants {
|
||||
|
||||
static SerialFrameTypes = {
|
||||
Incoming: 0x3e, // ">"
|
||||
Outgoing: 0x3c, // "<"
|
||||
};
|
||||
|
||||
static CommandCodes = {
|
||||
AppStart: 1, // done
|
||||
SendTxtMsg: 2,
|
||||
SendChannelTxtMsg: 3,
|
||||
GetContacts: 4, // done
|
||||
GetDeviceTime: 5, // done
|
||||
SetDeviceTime: 6, // done
|
||||
SendSelfAdvert: 7, // done
|
||||
SetAdvertName: 8, // done
|
||||
AddUpdateContact: 9,
|
||||
SyncNextMessage: 10, // done
|
||||
SetRadioParams: 11,
|
||||
SetTxPower: 12,
|
||||
}
|
||||
|
||||
static ResponseCodes = {
|
||||
Ok: 0,
|
||||
Err: 1,
|
||||
ContactsStart: 2, // done
|
||||
Contact: 3, // done
|
||||
EndOfContacts: 4, // done
|
||||
SelfInfo: 5, // done
|
||||
Sent: 6,
|
||||
ContactMsgRecv: 7, // done
|
||||
ChannelMsgRecv: 8,
|
||||
CurrTime: 9, // done
|
||||
NoMoreMessages: 10,
|
||||
}
|
||||
|
||||
static PushCodes = {
|
||||
Advert: 0x80,
|
||||
PathUpdated: 0x81,
|
||||
SendConfirmed: 0x82,
|
||||
MsgWaiting: 0x83,
|
||||
}
|
||||
|
||||
static SelfAdvertTypes = {
|
||||
ZeroHop: 0,
|
||||
Flood: 1,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Constants;
|
||||
302
src/device.js
Normal file
302
src/device.js
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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);
|
||||
|
||||
}
|
||||
|
||||
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 sendCommandAppStart() {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.AppStart);
|
||||
data.writeByte(1); // appVer
|
||||
data.writeBytes(new Uint8Array(6)); // reserved
|
||||
data.writeString("test"); // appName
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandGetContacts(since) {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.GetContacts);
|
||||
if(since){
|
||||
data.writeUInt32LE(since);
|
||||
}
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandGetDeviceTime() {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.GetDeviceTime);
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandSetDeviceTime(epochSecs) {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.SetDeviceTime);
|
||||
data.writeUInt32LE(epochSecs);
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandSendSelfAdvert(type) {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.SendSelfAdvert);
|
||||
data.writeByte(type);
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandSetAdvertName(name) {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.SetAdvertName);
|
||||
data.writeString(name);
|
||||
await this.sendToRadioFrame(data.toBytes());
|
||||
}
|
||||
|
||||
async sendCommandSyncNextMessage() {
|
||||
const data = new BufferWriter();
|
||||
data.writeByte(Constants.CommandCodes.SyncNextMessage);
|
||||
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);
|
||||
|
||||
const bufferReader = new BufferReader(frame);
|
||||
const responseCode = bufferReader.readByte();
|
||||
|
||||
if(responseCode === Constants.ResponseCodes.SelfInfo){
|
||||
this.onSelfInfoResponse(bufferReader);
|
||||
} else if(responseCode === Constants.ResponseCodes.CurrTime){
|
||||
this.onCurrTimeResponse(bufferReader);
|
||||
} else if(responseCode === Constants.ResponseCodes.ContactMsgRecv){
|
||||
this.onContactMsgRecvResponse(bufferReader);
|
||||
} else if(responseCode === Constants.ResponseCodes.ContactsStart){
|
||||
this.onContactsStartResponse(bufferReader);
|
||||
} else if(responseCode === Constants.ResponseCodes.Contact){
|
||||
this.onContactResponse(bufferReader);
|
||||
} else if(responseCode === Constants.ResponseCodes.EndOfContacts){
|
||||
this.onEndOfContactsResponse(bufferReader);
|
||||
} else if(responseCode === Constants.PushCodes.Advert){
|
||||
this.onAdvertPush(bufferReader);
|
||||
} else if(responseCode === Constants.PushCodes.MsgWaiting){
|
||||
this.onMsgWaitingPush(bufferReader);
|
||||
} else {
|
||||
console.log("unhandled frame", frame);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onAdvertPush(bufferReader) {
|
||||
console.log("onAdvertPush", {
|
||||
publicKey: bufferReader.readBytes(32),
|
||||
});
|
||||
}
|
||||
|
||||
onMsgWaitingPush(bufferReader) {
|
||||
console.log("onMsgWaitingPush", {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onContactsStartResponse(bufferReader) {
|
||||
console.log("onContactsStartResponse", {
|
||||
count: bufferReader.readUInt32LE(),
|
||||
});
|
||||
}
|
||||
|
||||
onContactResponse(bufferReader) {
|
||||
console.log("onContactResponse", {
|
||||
publicKey: bufferReader.readBytes(32),
|
||||
type: bufferReader.readByte(),
|
||||
flags: bufferReader.readByte(),
|
||||
outPathLen: bufferReader.readByte(),
|
||||
outPath: bufferReader.readBytes(64),
|
||||
advName: bufferReader.readCString(32),
|
||||
lastAdvert: bufferReader.readUInt32LE(),
|
||||
advLat: bufferReader.readUInt32LE(),
|
||||
advLon: bufferReader.readUInt32LE(),
|
||||
lastMod: bufferReader.readUInt32LE(),
|
||||
});
|
||||
}
|
||||
|
||||
onEndOfContactsResponse(bufferReader) {
|
||||
console.log("onEndOfContactsResponse", {
|
||||
mostRecentLastmod: bufferReader.readUInt32LE(),
|
||||
});
|
||||
}
|
||||
|
||||
onSelfInfoResponse(bufferReader) {
|
||||
console.log("onSelfInfoResponse", {
|
||||
type: bufferReader.readByte(),
|
||||
txPower: bufferReader.readByte(),
|
||||
maxTxPower: bufferReader.readByte(),
|
||||
publicKey: bufferReader.readBytes(32),
|
||||
deviceLoc: bufferReader.readBytes(12),
|
||||
radioFreq: bufferReader.readUInt32LE(),
|
||||
radioBw: bufferReader.readUInt32LE(),
|
||||
radioSf: bufferReader.readByte(),
|
||||
radioCr: bufferReader.readByte(),
|
||||
name: bufferReader.readString(),
|
||||
});
|
||||
}
|
||||
|
||||
onCurrTimeResponse(bufferReader) {
|
||||
console.log("onCurrTimeResponse", {
|
||||
epochSecs: bufferReader.readUInt32LE(),
|
||||
});
|
||||
}
|
||||
|
||||
onContactMsgRecvResponse(bufferReader) {
|
||||
console.log("onContactMsgRecvResponse", {
|
||||
pubKeyPrefix: bufferReader.readBytes(6),
|
||||
pathLen: bufferReader.readByte(),
|
||||
txtType: bufferReader.readByte(),
|
||||
senderTimestamp: bufferReader.readUInt32LE(),
|
||||
text: bufferReader.readString(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Device;
|
||||
Loading…
Add table
Add a link
Reference in a new issue