Migrate from JavaScript to TypeScript (#2)

* Migrate from JavaScript to TypeScript

Convert all 16 source files from .js to .ts with full type annotations.
Add types.ts with interfaces for all data structures. Export BufferReader
and BufferWriter from index. Use 'as const' for literal types on constants.

* Switch from pnpm to npm

* Remove unused EpochSeconds type
This commit is contained in:
Manuel Bahamóndez-Honores 2026-02-20 20:07:43 -03:00 committed by GitHub
parent f59b0f7ce4
commit bed46e52b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 713 additions and 315 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/.idea
/node_modules
/dist

35
package-lock.json generated
View file

@ -11,6 +11,10 @@
"dependencies": {
"@noble/curves": "^1.8.1",
"serialport": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@noble/curves": {
@ -227,6 +231,16 @@
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@types/node": {
"version": "22.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -292,6 +306,27 @@
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View file

@ -2,15 +2,22 @@
"name": "@liamcottle/meshcore.js",
"version": "1.11.0",
"description": "",
"main": "src/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"prepublishOnly": "npm run build"
},
"files": ["dist"],
"author": "Liam Cottle <liam@liamcottle.com>",
"license": "MIT",
"dependencies": {
"@noble/curves": "^1.8.1",
"serialport": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}

View file

@ -1,5 +1,6 @@
import BufferReader from "./buffer_reader.js";
import BufferWriter from "./buffer_writer.js";
import type { ParsedAdvertAppData } from "./types.js";
class Advert {
@ -13,7 +14,13 @@ class Advert {
static ADV_FEAT2_MASK = 0x40;
static ADV_NAME_MASK = 0x80;
constructor(publicKey, timestamp, signature, appData) {
publicKey: Uint8Array;
timestamp: number;
signature: Uint8Array;
appData: Uint8Array;
parsed: ParsedAdvertAppData;
constructor(publicKey: Uint8Array, timestamp: number, signature: Uint8Array, appData: Uint8Array) {
this.publicKey = publicKey;
this.timestamp = timestamp;
this.signature = signature;
@ -21,7 +28,7 @@ class Advert {
this.parsed = this.parseAppData();
}
static fromBytes(bytes) {
static fromBytes(bytes: Uint8Array): Advert {
// read bytes
const bufferReader = new BufferReader(bytes);
@ -34,16 +41,16 @@ class Advert {
}
getFlags() {
getFlags(): number {
return this.appData[0];
}
getType() {
getType(): number {
const flags = this.getFlags();
return flags & 0x0F;
}
getTypeString() {
getTypeString(): string | null {
const type = this.getType();
if(type === Advert.ADV_TYPE_NONE) return "NONE";
if(type === Advert.ADV_TYPE_CHAT) return "CHAT";
@ -52,7 +59,7 @@ class Advert {
return null;
}
async isVerified() {
async isVerified(): Promise<boolean> {
const { ed25519 } = await import("@noble/curves/ed25519");
@ -67,34 +74,34 @@ class Advert {
}
parseAppData() {
parseAppData(): ParsedAdvertAppData {
// read app data
const bufferReader = new BufferReader(this.appData);
const flags = bufferReader.readByte();
// parse lat lon
var lat = null;
var lon = null;
var lat: number | null = null;
var lon: number | null = null;
if(flags & Advert.ADV_LATLON_MASK){
lat = bufferReader.readInt32LE();
lon = bufferReader.readInt32LE();
}
// parse feat1
var feat1 = null;
var feat1: number | null = null;
if(flags & Advert.ADV_FEAT1_MASK){
feat1 = bufferReader.readUInt16LE();
}
// parse feat2
var feat2 = null;
var feat2: number | null = null;
if(flags & Advert.ADV_FEAT2_MASK){
feat2 = bufferReader.readUInt16LE();
}
// parse name (remainder of app data)
var name = null;
var name: string | null = null;
if(flags & Advert.ADV_NAME_MASK){
name = bufferReader.readString();
}

View file

@ -1,34 +1,37 @@
class BufferReader {
constructor(data) {
pointer: number;
buffer: Uint8Array;
constructor(data: ArrayLike<number> | ArrayBuffer) {
this.pointer = 0;
this.buffer = new Uint8Array(data);
}
getRemainingBytesCount() {
getRemainingBytesCount(): number {
return this.buffer.length - this.pointer;
}
readByte() {
readByte(): number {
return this.readBytes(1)[0];
}
readBytes(count) {
readBytes(count: number): Uint8Array {
const data = this.buffer.slice(this.pointer, this.pointer + count);
this.pointer += count;
return data;
}
readRemainingBytes() {
readRemainingBytes(): Uint8Array {
return this.readBytes(this.getRemainingBytesCount());
}
readString() {
readString(): string {
return new TextDecoder().decode(this.readRemainingBytes());
}
readCString(maxLength) {
const value = [];
readCString(maxLength: number): string {
const value: number[] = [];
const bytes = this.readBytes(maxLength);
for(const byte of bytes){
@ -40,63 +43,64 @@ class BufferReader {
value.push(byte);
}
return new TextDecoder().decode(new Uint8Array(value));
}
readInt8() {
readInt8(): number {
const bytes = this.readBytes(1);
const view = new DataView(bytes.buffer);
return view.getInt8(0);
}
readUInt8() {
readUInt8(): number {
const bytes = this.readBytes(1);
const view = new DataView(bytes.buffer);
return view.getUint8(0);
}
readUInt16LE() {
readUInt16LE(): number {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getUint16(0, true);
}
readUInt16BE() {
readUInt16BE(): number {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getUint16(0, false);
}
readUInt32LE() {
readUInt32LE(): number {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getUint32(0, true);
}
readUInt32BE() {
readUInt32BE(): number {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getUint32(0, false);
}
readInt16LE() {
readInt16LE(): number {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getInt16(0, true);
}
readInt16BE() {
readInt16BE(): number {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getInt16(0, false);
}
readInt32LE() {
readInt32LE(): number {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getInt32(0, true);
}
readInt24BE() {
readInt24BE(): number {
// read 24-bit (3 bytes) big endian integer
var value = (this.readByte() << 16) | (this.readByte() << 8) | this.readByte();

View file

@ -1,22 +1,22 @@
class BufferUtils {
static bytesToHex(uint8Array) {
static bytesToHex(uint8Array: Uint8Array): string {
return Array.from(uint8Array).map(byte => {
return byte.toString(16).padStart(2, '0');
}).join('');
}
static hexToBytes(hex) {
return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
static hexToBytes(hex: string): Uint8Array {
return Uint8Array.from(hex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
}
static base64ToBytes(base64) {
static base64ToBytes(base64: string): Uint8Array {
return Uint8Array.from(atob(base64), (c) => {
return c.charCodeAt(0);
});
}
static areBuffersEqual(byteArray1, byteArray2) {
static areBuffersEqual(byteArray1: Uint8Array, byteArray2: Uint8Array): boolean {
// ensure length is the same
if(byteArray1.length !== byteArray2.length){

View file

@ -1,52 +1,54 @@
class BufferWriter {
buffer: number[];
constructor() {
this.buffer = [];
}
toBytes() {
toBytes(): Uint8Array {
return new Uint8Array(this.buffer);
}
writeBytes(bytes) {
writeBytes(bytes: ArrayLike<number>): void {
this.buffer = [
...this.buffer,
...bytes,
...Array.from(bytes),
];
}
writeByte(byte) {
writeByte(byte: number): void {
this.writeBytes([
byte,
]);
}
writeUInt16LE(num) {
writeUInt16LE(num: number): void {
const bytes = new Uint8Array(2);
const view = new DataView(bytes.buffer);
view.setUint16(0, num, true);
this.writeBytes(bytes);
}
writeUInt32LE(num) {
writeUInt32LE(num: number): void {
const bytes = new Uint8Array(4);
const view = new DataView(bytes.buffer);
view.setUint32(0, num, true);
this.writeBytes(bytes);
}
writeInt32LE(num) {
writeInt32LE(num: number): void {
const bytes = new Uint8Array(4);
const view = new DataView(bytes.buffer);
view.setInt32(0, num, true);
this.writeBytes(bytes);
}
writeString(string) {
writeString(string: string): void {
this.writeBytes(new TextEncoder().encode(string));
}
writeCString(string, maxLength) {
writeCString(string: string, maxLength: number): void {
// create buffer of max length
const bytes = new Uint8Array(new ArrayBuffer(maxLength));

View file

@ -1,4 +1,5 @@
import BufferReader from "./buffer_reader.js";
import type { TelemetryEntry } from "./types.js";
class CayenneLpp {
@ -30,10 +31,10 @@ class CayenneLpp {
static LPP_SWITCH = 142; // 1 byte, 0/1
static LPP_POLYLINE = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
static parse(bytes) {
static parse(bytes: Uint8Array): TelemetryEntry[] {
const buffer = new BufferReader(bytes);
const telemetry = [];
const telemetry: TelemetryEntry[] = [];
while(buffer.getRemainingBytesCount() >= 2){ // need at least 2 more bytes to get channel and type

File diff suppressed because it is too large Load diff

View file

@ -2,15 +2,18 @@ import SerialConnection from "./serial_connection.js";
class NodeJSSerialConnection extends SerialConnection {
serialPortPath: string;
serialPort: any;
/**
* @param path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401"
*/
constructor(path) {
constructor(path: string) {
super();
this.serialPortPath = path;
}
async connect() {
async connect(): Promise<void> {
// note: serialport module is only available in NodeJS, you shouldn't use NodeJSSerialConnection from a web browser
const { SerialPort } = await import('serialport');
@ -30,11 +33,11 @@ class NodeJSSerialConnection extends SerialConnection {
this.onDisconnected();
});
this.serialPort.on("error", function(err) {
this.serialPort.on("error", function(err: Error) {
console.log("SerialPort Error: ", err.message)
});
this.serialPort.on("data", async (data) => {
this.serialPort.on("data", async (data: Buffer) => {
await this.onDataReceived(data);
});
@ -43,7 +46,7 @@ class NodeJSSerialConnection extends SerialConnection {
}
async close() {
async close(): Promise<void> {
try {
await this.serialPort.close();
} catch(e) {
@ -51,7 +54,7 @@ class NodeJSSerialConnection extends SerialConnection {
}
}
/* override */ async write(bytes) {
/* override */ async write(bytes: Uint8Array): Promise<void> {
this.serialPort.write(bytes);
}

View file

@ -5,6 +5,8 @@ import Connection from "./connection.js";
class SerialConnection extends Connection {
readBuffer: number[];
constructor() {
super();
this.readBuffer = [];
@ -13,11 +15,11 @@ class SerialConnection extends Connection {
}
}
async write(bytes) {
async write(bytes: Uint8Array): Promise<void> {
throw new Error("Not Implemented: write must be implemented by SerialConnection sub class.");
}
async writeFrame(frameType, frameData) {
async writeFrame(frameType: number, frameData: Uint8Array): Promise<void> {
// create frame
const frame = new BufferWriter();
@ -34,18 +36,18 @@ class SerialConnection extends Connection {
}
async sendToRadioFrame(data) {
async sendToRadioFrame(data: Uint8Array): Promise<void> {
// write "app to radio" frame 0x3c "<"
this.emit("tx", data);
await this.writeFrame(0x3c, data);
}
async onDataReceived(value) {
async onDataReceived(value: ArrayLike<number>): Promise<void> {
// append received bytes to read buffer
this.readBuffer = [
...this.readBuffer,
...value,
...Array.from(value),
];
// process read buffer while there is enough bytes for a frame header
@ -84,7 +86,7 @@ class SerialConnection extends Connection {
this.readBuffer = this.readBuffer.slice(requiredLength);
// handle received frame
this.onFrameReceived(frameData);
this.onFrameReceived(new Uint8Array(frameData));
} catch(e) {
console.error("Failed to process frame", e);

View file

@ -5,14 +5,19 @@ import Connection from "./connection.js";
class TCPConnection extends Connection {
constructor(host, port) {
host: string;
port: number;
readBuffer: number[];
socket: any;
constructor(host: string, port: number) {
super();
this.host = host;
this.port = port;
this.readBuffer = [];
}
async connect() {
async connect(): Promise<void> {
// note: net module is only available in NodeJS, you shouldn't use TCPConnection from a web browser
const { Socket } = await import("net");
@ -21,17 +26,17 @@ class TCPConnection extends Connection {
this.socket = new Socket();
// handle received data
this.socket.on('data', (data) => {
this.socket.on('data', (data: Buffer) => {
this.onSocketDataReceived(data);
});
// handle errors
this.socket.on('error', (error) => {
this.socket.on('error', (error: Error) => {
console.error('Connection Error', error);
});
// handle socket close
this.socket.on('close', (error) => {
this.socket.on('close', (error: boolean) => {
this.onDisconnected();
});
@ -42,7 +47,7 @@ class TCPConnection extends Connection {
}
onSocketDataReceived(data) {
onSocketDataReceived(data: Buffer): void {
// append received bytes to read buffer
this.readBuffer = [
@ -86,7 +91,7 @@ class TCPConnection extends Connection {
this.readBuffer = this.readBuffer.slice(requiredLength);
// handle received frame
this.onFrameReceived(frameData);
this.onFrameReceived(new Uint8Array(frameData));
} catch(e) {
console.error("Failed to process frame", e);
@ -96,7 +101,7 @@ class TCPConnection extends Connection {
}
close() {
async close(): Promise<void> {
try {
this.socket.destroy();
} catch(e) {
@ -104,11 +109,11 @@ class TCPConnection extends Connection {
}
}
async write(bytes) {
async write(bytes: Uint8Array): Promise<void> {
this.socket.write(new Uint8Array(bytes));
}
async writeFrame(frameType, frameData) {
async writeFrame(frameType: number, frameData: Uint8Array): Promise<void> {
// create frame
const frame = new BufferWriter();
@ -125,7 +130,7 @@ class TCPConnection extends Connection {
}
async sendToRadioFrame(data) {
async sendToRadioFrame(data: Uint8Array): Promise<void> {
// write "app to radio" frame 0x3c "<"
this.emit("tx", data);
await this.writeFrame(0x3c, data);

View file

@ -1,9 +1,20 @@
import Constants from "../constants.js";
import Connection from "./connection.js";
declare global {
interface Navigator {
bluetooth: any;
}
}
class WebBleConnection extends Connection {
constructor(bleDevice) {
bleDevice: any;
gattServer: any;
rxCharacteristic: any;
txCharacteristic: any;
constructor(bleDevice: any) {
super();
this.bleDevice = bleDevice;
this.gattServer = null;
@ -12,7 +23,7 @@ class WebBleConnection extends Connection {
this.init();
}
static async open() {
static async open(): Promise<WebBleConnection | null | undefined> {
// ensure browser supports web bluetooth
if(!navigator.bluetooth){
@ -40,7 +51,7 @@ class WebBleConnection extends Connection {
}
async init() {
async init(): Promise<void> {
// listen for ble disconnect
this.bleDevice.addEventListener("gattserverdisconnected", () => {
@ -55,18 +66,18 @@ class WebBleConnection extends Connection {
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) => {
this.rxCharacteristic = characteristics.find((characteristic: any) => {
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) => {
this.txCharacteristic = characteristics.find((characteristic: any) => {
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) => {
this.txCharacteristic.addEventListener('characteristicvaluechanged', (event: any) => {
const frame = new Uint8Array(event.target.value.buffer);
this.onFrameReceived(frame);
});
@ -76,7 +87,7 @@ class WebBleConnection extends Connection {
}
async close() {
async close(): Promise<void> {
try {
this.gattServer?.disconnect();
this.gattServer = null;
@ -85,7 +96,7 @@ class WebBleConnection extends Connection {
}
}
async write(bytes) {
async write(bytes: Uint8Array): Promise<void> {
try {
// fixme: NetworkError: GATT operation already in progress.
// todo: implement mutex to prevent multiple writes when another write is in progress
@ -96,7 +107,7 @@ class WebBleConnection extends Connection {
}
}
async sendToRadioFrame(frame) {
async sendToRadioFrame(frame: Uint8Array): Promise<void> {
this.emit("tx", frame);
await this.write(frame);
}

View file

@ -1,8 +1,18 @@
import SerialConnection from "./serial_connection.js";
declare global {
interface Navigator {
serial: any;
}
}
class WebSerialConnection extends SerialConnection {
constructor(serialPort) {
serialPort: any;
reader: any;
writable: any;
constructor(serialPort: any) {
super();
@ -23,7 +33,7 @@ class WebSerialConnection extends SerialConnection {
}
static async open() {
static async open(): Promise<WebSerialConnection | null> {
// ensure browser supports web serial
if(!navigator.serial){
@ -45,7 +55,7 @@ class WebSerialConnection extends SerialConnection {
}
async close() {
async close(): Promise<void> {
// release reader lock
try {
@ -63,7 +73,7 @@ class WebSerialConnection extends SerialConnection {
}
/* override */ async write(bytes) {
/* override */ async write(bytes: Uint8Array): Promise<void> {
const writer = this.writable.getWriter();
try {
await writer.write(new Uint8Array(bytes));
@ -72,7 +82,7 @@ class WebSerialConnection extends SerialConnection {
}
}
async readLoop() {
async readLoop(): Promise<void> {
try {
while(true){

View file

@ -1,19 +1,19 @@
class Constants {
static SupportedCompanionProtocolVersion = 1;
static readonly SupportedCompanionProtocolVersion = 1 as const;
static SerialFrameTypes = {
static readonly SerialFrameTypes = {
Incoming: 0x3e, // ">"
Outgoing: 0x3c, // "<"
}
} as const;
static Ble = {
static readonly Ble = {
ServiceUuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E",
CharacteristicUuidRx: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E",
CharacteristicUuidTx: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E",
}
} as const;
static CommandCodes = {
static readonly CommandCodes = {
AppStart: 1,
SendTxtMsg: 2,
SendChannelTxtMsg: 3,
@ -52,9 +52,9 @@ class Constants {
SendTelemetryReq: 39,
SendBinaryReq: 50,
}
} as const;
static ResponseCodes = {
static readonly ResponseCodes = {
Ok: 0, // todo
Err: 1, // todo
ContactsStart: 2,
@ -74,9 +74,9 @@ class Constants {
ChannelInfo: 18,
SignStart: 19,
Signature: 20,
}
} as const;
static PushCodes = {
static readonly PushCodes = {
Advert: 0x80, // when companion is set to auto add contacts
PathUpdated: 0x81,
SendConfirmed: 0x82,
@ -90,41 +90,41 @@ class Constants {
NewAdvert: 0x8A, // when companion is set to manually add contacts
TelemetryResponse: 0x8B,
BinaryResponse: 0x8C,
}
} as const;
static ErrorCodes = {
static readonly ErrorCodes = {
UnsupportedCmd: 1,
NotFound: 2,
TableFull: 3,
BadState: 4,
FileIoError: 5,
IllegalArg: 6,
}
} as const;
static AdvType = {
static readonly AdvType = {
None: 0,
Chat: 1,
Repeater: 2,
Room: 3,
}
} as const;
static SelfAdvertTypes = {
static readonly SelfAdvertTypes = {
ZeroHop: 0,
Flood: 1,
}
} as const;
static TxtTypes = {
static readonly TxtTypes = {
Plain: 0,
CliData: 1,
SignedPlain: 2,
}
} as const;
static BinaryRequestTypes = {
static readonly BinaryRequestTypes = {
GetTelemetryData: 0x03, // #define REQ_TYPE_GET_TELEMETRY_DATA 0x03
GetAvgMinMax: 0x04, // #define REQ_TYPE_GET_AVG_MIN_MAX 0x04
GetAccessList: 0x05, // #define REQ_TYPE_GET_ACCESS_LIST 0x05
GetNeighbours: 0x06, // #define REQ_TYPE_GET_NEIGHBOURS 0x06
}
} as const;
}

View file

@ -1,10 +1,12 @@
class EventEmitter {
eventListenersMap: Map<string | number, ((...args: any[]) => void)[]>;
constructor() {
this.eventListenersMap = new Map();
}
on(event, callback) {
on(event: string | number, callback: (...args: any[]) => void): void {
// create list of listeners for event if it doesn't exist
if(!this.eventListenersMap.has(event)){
@ -12,24 +14,24 @@ class EventEmitter {
}
// add listener for event
this.eventListenersMap.get(event).push(callback);
this.eventListenersMap.get(event)!.push(callback);
}
off(event, callback) {
off(event: string | number, callback: (...args: any[]) => void): void {
// remove callback from listeners for this event
if(this.eventListenersMap.has(event)){
const callbacks = this.eventListenersMap.get(event).filter(cb => cb !== callback);
const callbacks = this.eventListenersMap.get(event)!.filter(cb => cb !== callback);
this.eventListenersMap.set(event, callbacks);
}
}
once(event, callback) {
once(event: string | number, callback: (...args: any[]) => void): void {
// internal callback to handle the event
const internalCallback = (...data) => {
const internalCallback = (...data: any[]) => {
// we received an event, so lets remove the event listener
this.off(event, internalCallback);
@ -44,11 +46,11 @@ class EventEmitter {
}
emit(event, ...data) {
emit(event: string | number, ...data: any[]): void {
// invoke each listener for this event
if(this.eventListenersMap.has(event)){
for(const eventListener of this.eventListenersMap.get(event)){
for(const eventListener of this.eventListenersMap.get(event)!){
setTimeout(() => eventListener(...data), 0);
}
}

View file

@ -8,6 +8,8 @@ import Constants from "./constants.js";
import Advert from "./advert.js";
import Packet from "./packet.js";
import BufferUtils from "./buffer_utils.js";
import BufferReader from "./buffer_reader.js";
import BufferWriter from "./buffer_writer.js";
import CayenneLpp from "./cayenne_lpp.js";
export {
@ -21,5 +23,9 @@ export {
Advert,
Packet,
BufferUtils,
BufferReader,
BufferWriter,
CayenneLpp,
};
export type * from "./types.js";

View file

@ -27,8 +27,19 @@ class Packet {
static PAYLOAD_TYPE_TRACE = 0x09; // trace a path, collecting SNR for each hop
static PAYLOAD_TYPE_RAW_CUSTOM = 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
constructor(header, path, payload, transportCode1, transportCode2) {
header: number;
path: Uint8Array;
payload: Uint8Array;
transportCode1: number | null;
transportCode2: number | null;
route_type: number;
route_type_string: string | null;
payload_type: number;
payload_type_string: string | null;
payload_version: number;
is_marked_do_not_retransmit: boolean;
constructor(header: number, path: Uint8Array, payload: Uint8Array, transportCode1: number | null, transportCode2: number | null) {
this.header = header;
this.path = path;
@ -46,7 +57,7 @@ class Packet {
}
static fromBytes(bytes) {
static fromBytes(bytes: Uint8Array): Packet {
const bufferReader = new BufferReader(bytes);
const header = bufferReader.readByte();
@ -56,8 +67,8 @@ class Packet {
const hasTransportCodes = routeType === Packet.ROUTE_TYPE_TRANSPORT_FLOOD || routeType === Packet.ROUTE_TYPE_TRANSPORT_DIRECT;
// parse transport codes
var transportCode1 = null;
var transportCode2 = null;
var transportCode1: number | null = null;
var transportCode2: number | null = null;
if(hasTransportCodes){
transportCode1 = bufferReader.readUInt16LE();
transportCode2 = bufferReader.readUInt16LE();
@ -71,11 +82,11 @@ class Packet {
}
getRouteType() {
getRouteType(): number {
return this.header & Packet.PH_ROUTE_MASK;
}
getRouteTypeString() {
getRouteTypeString(): string | null {
switch(this.getRouteType()){
case Packet.ROUTE_TYPE_FLOOD: return "FLOOD";
case Packet.ROUTE_TYPE_DIRECT: return "DIRECT";
@ -85,19 +96,19 @@ class Packet {
}
}
isRouteFlood() {
isRouteFlood(): boolean {
return this.getRouteType() === Packet.ROUTE_TYPE_FLOOD;
}
isRouteDirect() {
isRouteDirect(): boolean {
return this.getRouteType() === Packet.ROUTE_TYPE_DIRECT;
}
getPayloadType() {
getPayloadType(): number {
return (this.header >> Packet.PH_TYPE_SHIFT) & Packet.PH_TYPE_MASK;
}
getPayloadTypeString() {
getPayloadTypeString(): string | null {
switch(this.getPayloadType()){
case Packet.PAYLOAD_TYPE_REQ: return "REQ";
case Packet.PAYLOAD_TYPE_RESPONSE: return "RESPONSE";
@ -114,19 +125,19 @@ class Packet {
}
}
getPayloadVer() {
getPayloadVer(): number {
return (this.header >> Packet.PH_VER_SHIFT) & Packet.PH_VER_MASK;
}
markDoNotRetransmit() {
markDoNotRetransmit(): void {
this.header = 0xFF;
}
isMarkedDoNotRetransmit() {
isMarkedDoNotRetransmit(): boolean {
return this.header === 0xFF;
}
parsePayload() {
parsePayload(): any {
switch(this.getPayloadType()){
case Packet.PAYLOAD_TYPE_PATH: return this.parsePayloadTypePath();
case Packet.PAYLOAD_TYPE_REQ: return this.parsePayloadTypeReq();

View file

@ -1,6 +1,6 @@
class RandomUtils {
static getRandomInt(min, max) {
static getRandomInt(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;

263
src/types.ts Normal file
View file

@ -0,0 +1,263 @@
/** Information about the connected MeshCore device/node. */
export interface SelfInfo {
type: number;
txPower: number;
maxTxPower: number;
publicKey: Uint8Array;
advLat: number;
advLon: number;
reserved: Uint8Array;
manualAddContacts: number;
radioFreq: number;
radioBw: number;
radioSf: number;
radioCr: number;
name: string;
}
/** A contact stored on the device. */
export interface Contact {
publicKey: Uint8Array;
type: number;
flags: number;
outPathLen: number;
outPath: Uint8Array;
advName: string;
lastAdvert: number;
advLat: number;
advLon: number;
lastMod: number;
}
/** A channel stored on the device. */
export interface ChannelInfo {
channelIdx: number;
name: string;
secret: Uint8Array;
}
/** Device info returned from a device query. */
export interface DeviceInfo {
firmwareVer: number;
reserved: Uint8Array;
firmware_build_date: string;
manufacturerModel: string;
}
/** Battery voltage response. */
export interface BatteryVoltage {
batteryMilliVolts: number;
}
/** Current time response. */
export interface CurrTime {
epochSecs: number;
}
/** Sent response from the device. */
export interface SentResponse {
result: number;
expectedAckCrc: number;
estTimeout: number;
}
/** A contact message received from the device. */
export interface ContactMessage {
pubKeyPrefix: Uint8Array;
pathLen: number;
txtType: number;
senderTimestamp: number;
text: string;
}
/** A channel message received from the device. */
export interface ChannelMessage {
channelIdx: number;
pathLen: number;
txtType: number;
senderTimestamp: number;
text: string;
}
/** Exported contact response. */
export interface ExportContactResponse {
advertPacketBytes: Uint8Array;
}
/** Private key response. */
export interface PrivateKeyResponse {
privateKey: Uint8Array;
}
/** Sign start response. */
export interface SignStartResponse {
reserved: number;
maxSignDataLen: number;
}
/** Signature response. */
export interface SignatureResponse {
signature: Uint8Array;
}
/** Error response. */
export interface ErrResponse {
errCode: number | null;
}
/** Contacts start response. */
export interface ContactsStartResponse {
count: number;
}
/** End of contacts response. */
export interface EndOfContactsResponse {
mostRecentLastmod: number;
}
/** Advert push data. */
export interface AdvertPush {
publicKey: Uint8Array;
}
/** Path updated push data. */
export interface PathUpdatedPush {
publicKey: Uint8Array;
}
/** Send confirmed push data. */
export interface SendConfirmedPush {
ackCode: number;
roundTrip: number;
}
/** Raw data push. */
export interface RawDataPush {
lastSnr: number;
lastRssi: number;
reserved: number;
payload: Uint8Array;
}
/** Login success push. */
export interface LoginSuccessPush {
reserved: number;
pubKeyPrefix: Uint8Array;
}
/** Status response push. */
export interface StatusResponsePush {
reserved: number;
pubKeyPrefix: Uint8Array;
statusData: Uint8Array;
}
/** Log RX data push. */
export interface LogRxDataPush {
lastSnr: number;
lastRssi: number;
raw: Uint8Array;
}
/** Telemetry response push. */
export interface TelemetryResponsePush {
reserved: number;
pubKeyPrefix: Uint8Array;
lppSensorData: Uint8Array;
}
/** Binary response push. */
export interface BinaryResponsePush {
reserved: number;
tag: number;
responseData: Uint8Array;
}
/** Trace data push. */
export interface TraceDataPush {
reserved: number;
pathLen: number;
flags: number;
tag: number;
authCode: number;
pathHashes: Uint8Array;
pathSnrs: Uint8Array;
lastSnr: number;
}
/** New advert push data. */
export interface NewAdvertPush {
publicKey: Uint8Array;
type: number;
flags: number;
outPathLen: number;
outPath: Uint8Array;
advName: string;
lastAdvert: number;
advLat: number;
advLon: number;
lastMod: number;
}
/** Repeater stats from status response. */
export interface RepeaterStats {
batt_milli_volts: number;
curr_tx_queue_len: number;
noise_floor: number;
last_rssi: number;
n_packets_recv: number;
n_packets_sent: number;
total_air_time_secs: number;
total_up_time_secs: number;
n_sent_flood: number;
n_sent_direct: number;
n_recv_flood: number;
n_recv_direct: number;
err_events: number;
last_snr: number;
n_direct_dups: number;
n_flood_dups: number;
}
/** Synced message result. */
export interface SyncedMessage {
contactMessage?: ContactMessage;
channelMessage?: ChannelMessage;
}
/** Ping result. */
export interface PingResult {
rtt: number;
snr: number;
rssi: number;
}
/** Neighbour info. */
export interface Neighbour {
publicKeyPrefix: Uint8Array;
heardSecondsAgo: number;
snr: number;
}
/** Get neighbours result. */
export interface GetNeighboursResult {
totalNeighboursCount: number;
neighbours: Neighbour[];
}
/** Parsed advert app data. */
export interface ParsedAdvertAppData {
type: string | null;
lat: number | null;
lon: number | null;
name: string | null;
feat1: number | null;
feat2: number | null;
}
/** CayenneLPP telemetry entry. */
export interface TelemetryEntry {
channel: number;
type: number;
value: number | { latitude: number; longitude: number; altitude: number };
}

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}