diff --git a/index.html b/index.html
new file mode 100644
index 0000000..780a415
--- /dev/null
+++ b/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+ MeshCore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 9d92b50..3ce5a94 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/buffer_reader.js b/src/buffer_reader.js
new file mode 100644
index 0000000..ef638d6
--- /dev/null
+++ b/src/buffer_reader.js
@@ -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;
diff --git a/src/buffer_writer.js b/src/buffer_writer.js
new file mode 100644
index 0000000..bbef00b
--- /dev/null
+++ b/src/buffer_writer.js
@@ -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;
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..b2b6c07
--- /dev/null
+++ b/src/constants.js
@@ -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;
diff --git a/src/device.js b/src/device.js
new file mode 100644
index 0000000..8b1ee65
--- /dev/null
+++ b/src/device.js
@@ -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;