Replace hand-written .d.ts with JSDoc + tsc auto-generation

- Delete hand-written index.d.ts that drifted from source
- Add JSDoc type annotations to all source files
- Create src/types.js with shared typedefs (SelfInfo, Contact,
  ContactMessage, ChannelInfo, RepeaterStats, etc.)
- Add tsconfig.json for declaration generation
- Add build:types script and prepublishOnly hook
- Add GitHub Actions CI workflow for type checking
- Use Uint8Array everywhere (no Buffer references)
- Add semantic type aliases (EpochSeconds, Milliseconds, MilliVolts)
- Add typed event overloads on Connection (on/once/off)
- All 11 PR #15 review comments addressed
This commit is contained in:
Manuel Bahamóndez-Honores 2026-02-18 05:49:46 -03:00
parent de31939a28
commit 2d4cb35e51
23 changed files with 988 additions and 212 deletions

17
.github/workflows/typecheck.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Type Check
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build:types

1
.gitignore vendored
View file

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

185
index.d.ts vendored
View file

@ -1,185 +0,0 @@
/**
* TypeScript declarations for @liamcottle/meshcore.js
*/
declare module '@liamcottle/meshcore.js' {
import { EventEmitter } from 'events';
export class Connection extends EventEmitter {
connect(): Promise<void>;
close(): Promise<void>;
// High-level API methods
getSelfInfo(timeout?: number): Promise<SelfInfo>;
getWaitingMessages(): Promise<Message[]>;
getChannels(): Promise<Channel[]>;
getContacts(since?: number): Promise<Contact[]>;
syncNextMessage(): Promise<void>;
// Message sending
sendTextMessage(publicKey: Buffer, text: string): Promise<void>;
sendChannelTextMessage(channelIdx: number, text: string): Promise<void>;
// Contact lookup
findContactByPublicKeyPrefix(prefix: Buffer): Promise<Contact | null>;
// Frame handling (can be overridden in subclasses)
onFrameReceived(frame: Buffer): void;
// Events
on(event: 'connected', listener: () => void): this;
on(event: 'disconnected', listener: () => void): this;
on(event: number, listener: (data: any) => void): this;
on(event: string | number, listener: (...args: any[]) => void): this;
emit(event: string | number, ...args: any[]): boolean;
}
export class NodeJSSerialConnection extends Connection {
constructor(port: string);
}
export class WebSerialConnection extends Connection {
constructor();
}
export class TCPConnection extends Connection {
constructor(host: string, port: number);
}
export class WebBleConnection extends Connection {
constructor();
}
export class SerialConnection extends Connection {
constructor();
}
// Type definitions
export interface SelfInfo {
publicKey: Buffer;
name?: string;
}
export interface Message {
pubKeyPrefix: Buffer;
pathLen: number;
txtType: number;
senderTimestamp: number;
text: string;
}
export interface Channel {
channelIdx: number;
name: string;
secret: Buffer;
}
export interface Contact {
publicKey: Buffer;
name?: string;
lastSeen?: number;
}
// Constants
export class Constants {
static readonly SupportedCompanionProtocolVersion: number;
static readonly ResponseCodes: {
ContactMsgRecv: number;
ChannelMsgRecv: number;
[key: string]: number;
};
static readonly PushCodes: {
MsgWaiting: number;
NewAdvert: number;
[key: string]: number;
};
static readonly CommandCodes: {
AppStart: number;
SendTxtMsg: number;
SendChannelTxtMsg: number;
GetContacts: number;
GetDeviceTime: number;
SetDeviceTime: number;
SendSelfAdvert: number;
SetAdvertName: number;
[key: string]: number;
};
}
export class Advert {
constructor(data: Buffer);
publicKey: Buffer;
advName?: string;
}
export class Packet {
constructor(data: Buffer);
}
export class BufferUtils {
static xor(a: Buffer, b: Buffer): Buffer;
static concat(...buffers: Buffer[]): Buffer;
}
export class CayenneLpp {
constructor();
}
}
// Type declarations for submodules
declare module '@liamcottle/meshcore.js/src/constants.js' {
const Constants: {
SupportedCompanionProtocolVersion: number;
ResponseCodes: {
ContactMsgRecv: number;
ChannelMsgRecv: number;
[key: string]: number;
};
PushCodes: {
MsgWaiting: number;
NewAdvert: number;
[key: string]: number;
};
CommandCodes: {
AppStart: number;
SendTxtMsg: number;
SendChannelTxtMsg: number;
GetContacts: number;
GetDeviceTime: number;
SetDeviceTime: number;
SendSelfAdvert: number;
SetAdvertName: number;
[key: string]: number;
};
};
export default Constants;
}
declare module '@liamcottle/meshcore.js/src/buffer_reader.js' {
export default class BufferReader {
constructor(buffer: Buffer);
readByte(): number;
readInt8(): number;
readUInt16LE(): number;
readUInt32LE(): number;
readBytes(length: number): Buffer;
readString(): string;
}
}
declare module '@liamcottle/meshcore.js/src/buffer_writer.js' {
export default class BufferWriter {
constructor();
writeByte(value: number): void;
writeInt8(value: number): void;
writeUInt16LE(value: number): void;
writeUInt32LE(value: number): void;
writeBytes(buffer: Buffer | Uint8Array): void;
writeString(str: string): void;
toBytes(): Buffer;
}
}

35
package-lock.json generated
View file

@ -11,6 +11,10 @@
"dependencies": {
"@noble/curves": "^1.8.1",
"serialport": "^13.0.0"
},
"devDependencies": {
"@types/node": "^25.2.3",
"typescript": "^5.9.3"
}
},
"node_modules/@noble/curves": {
@ -227,6 +231,16 @@
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@types/node": {
"version": "25.2.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.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": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
}
}

View file

@ -3,9 +3,11 @@
"version": "1.11.0",
"description": "",
"main": "src/index.js",
"types": "index.d.ts",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build:types": "tsc",
"prepublishOnly": "npm run build:types",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Liam Cottle <liam@liamcottle.com>",
@ -13,5 +15,9 @@
"dependencies": {
"@noble/curves": "^1.8.1",
"serialport": "^13.0.0"
},
"devDependencies": {
"@types/node": "^25.2.3",
"typescript": "^5.9.3"
}
}

View file

@ -1,6 +1,8 @@
import BufferReader from "./buffer_reader.js";
import BufferWriter from "./buffer_writer.js";
/** @typedef {import("./types.js").AdvertParsedData} AdvertParsedData */
class Advert {
static ADV_TYPE_NONE = 0;
@ -13,14 +15,25 @@ class Advert {
static ADV_FEAT2_MASK = 0x40;
static ADV_NAME_MASK = 0x80;
/**
* @param {Uint8Array} publicKey
* @param {number} timestamp
* @param {Uint8Array} signature
* @param {Uint8Array} appData
*/
constructor(publicKey, timestamp, signature, appData) {
this.publicKey = publicKey;
this.timestamp = timestamp;
this.signature = signature;
this.appData = appData;
/** @type {AdvertParsedData} */
this.parsed = this.parseAppData();
}
/**
* @param {Uint8Array} bytes
* @returns {Advert}
*/
static fromBytes(bytes) {
// read bytes
@ -34,15 +47,18 @@ class Advert {
}
/** @returns {number} */
getFlags() {
return this.appData[0];
}
/** @returns {number} */
getType() {
const flags = this.getFlags();
return flags & 0x0F;
}
/** @returns {string | null} */
getTypeString() {
const type = this.getType();
if(type === Advert.ADV_TYPE_NONE) return "NONE";
@ -52,6 +68,7 @@ class Advert {
return null;
}
/** @returns {Promise<boolean>} */
async isVerified() {
const { ed25519 } = await import("@noble/curves/ed25519");
@ -67,6 +84,7 @@ class Advert {
}
/** @returns {AdvertParsedData} */
parseAppData() {
// read app data

View file

@ -1,32 +1,49 @@
class BufferReader {
/**
* @param {Uint8Array | ArrayLike<number>} data
*/
constructor(data) {
/** @type {number} */
this.pointer = 0;
/** @type {Uint8Array} */
this.buffer = new Uint8Array(data);
}
/** @returns {number} */
getRemainingBytesCount() {
return this.buffer.length - this.pointer;
}
/** @returns {number} */
readByte() {
return this.readBytes(1)[0];
}
/**
* @param {number} count
* @returns {Uint8Array}
*/
readBytes(count) {
const data = this.buffer.slice(this.pointer, this.pointer + count);
this.pointer += count;
return data;
}
/** @returns {Uint8Array} */
readRemainingBytes() {
return this.readBytes(this.getRemainingBytesCount());
}
/** @returns {string} */
readString() {
return new TextDecoder().decode(this.readRemainingBytes());
}
/**
* @param {number} maxLength
* @returns {string | undefined}
*/
readCString(maxLength) {
const value = [];
const bytes = this.readBytes(maxLength);
@ -42,60 +59,70 @@ class BufferReader {
}
}
/** @returns {number} */
readInt8() {
const bytes = this.readBytes(1);
const view = new DataView(bytes.buffer);
return view.getInt8(0);
}
/** @returns {number} */
readUInt8() {
const bytes = this.readBytes(1);
const view = new DataView(bytes.buffer);
return view.getUint8(0);
}
/** @returns {number} */
readUInt16LE() {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getUint16(0, true);
}
/** @returns {number} */
readUInt16BE() {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getUint16(0, false);
}
/** @returns {number} */
readUInt32LE() {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getUint32(0, true);
}
/** @returns {number} */
readUInt32BE() {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getUint32(0, false);
}
/** @returns {number} */
readInt16LE() {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getInt16(0, true);
}
/** @returns {number} */
readInt16BE() {
const bytes = this.readBytes(2);
const view = new DataView(bytes.buffer);
return view.getInt16(0, false);
}
/** @returns {number} */
readInt32LE() {
const bytes = this.readBytes(4);
const view = new DataView(bytes.buffer);
return view.getInt32(0, true);
}
/** @returns {number} */
readInt24BE() {
// read 24-bit (3 bytes) big endian integer

View file

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

View file

@ -1,13 +1,16 @@
class BufferWriter {
constructor() {
/** @type {number[]} */
this.buffer = [];
}
/** @returns {Uint8Array} */
toBytes() {
return new Uint8Array(this.buffer);
}
/** @param {Uint8Array | number[]} bytes */
writeBytes(bytes) {
this.buffer = [
...this.buffer,
@ -15,12 +18,14 @@ class BufferWriter {
];
}
/** @param {number} byte */
writeByte(byte) {
this.writeBytes([
byte,
]);
}
/** @param {number} num */
writeUInt16LE(num) {
const bytes = new Uint8Array(2);
const view = new DataView(bytes.buffer);
@ -28,6 +33,7 @@ class BufferWriter {
this.writeBytes(bytes);
}
/** @param {number} num */
writeUInt32LE(num) {
const bytes = new Uint8Array(4);
const view = new DataView(bytes.buffer);
@ -35,6 +41,7 @@ class BufferWriter {
this.writeBytes(bytes);
}
/** @param {number} num */
writeInt32LE(num) {
const bytes = new Uint8Array(4);
const view = new DataView(bytes.buffer);
@ -42,10 +49,15 @@ class BufferWriter {
this.writeBytes(bytes);
}
/** @param {string} string */
writeString(string) {
this.writeBytes(new TextEncoder().encode(string));
}
/**
* @param {string} string
* @param {number} maxLength
*/
writeCString(string, maxLength) {
// create buffer of max length

View file

@ -1,5 +1,7 @@
import BufferReader from "./buffer_reader.js";
/** @typedef {import("./types.js").CayenneTelemetryEntry} CayenneTelemetryEntry */
class CayenneLpp {
static LPP_DIGITAL_INPUT = 0; // 1 byte
@ -30,9 +32,14 @@ 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
/**
* @param {Uint8Array} bytes
* @returns {CayenneTelemetryEntry[]}
*/
static parse(bytes) {
const buffer = new BufferReader(bytes);
/** @type {CayenneTelemetryEntry[]} */
const telemetry = [];
while(buffer.getRemainingBytesCount() >= 2){ // need at least 2 more bytes to get channel and type
@ -48,7 +55,6 @@ class CayenneLpp {
switch(type){
case this.LPP_GENERIC_SENSOR: {
const value = buffer.readUInt32BE();
// console.log(`[CayenneLpp] parsed LPP_GENERIC_SENSOR=${value}`);
telemetry.push({
"channel": channel,
"type": type,
@ -58,7 +64,6 @@ class CayenneLpp {
}
case this.LPP_LUMINOSITY: {
const lux = buffer.readInt16BE();
// console.log(`[CayenneLpp] parsed LPP_LUMINOSITY=${lux}`);
telemetry.push({
"channel": channel,
"type": type,
@ -68,7 +73,6 @@ class CayenneLpp {
}
case this.LPP_PRESENCE: {
const presence = buffer.readUInt8();
// console.log(`[CayenneLpp] parsed LPP_PRESENCE=${presence}`);
telemetry.push({
"channel": channel,
"type": type,
@ -78,7 +82,6 @@ class CayenneLpp {
}
case this.LPP_TEMPERATURE: {
const temperature = buffer.readInt16BE() / 10;
// console.log(`[CayenneLpp] parsed LPP_TEMPERATURE=${temperature}`);
telemetry.push({
"channel": channel,
"type": type,
@ -88,7 +91,6 @@ class CayenneLpp {
}
case this.LPP_RELATIVE_HUMIDITY: {
const relativeHumidity = buffer.readUInt8() / 2;
// console.log(`[CayenneLpp] parsed LPP_RELATIVE_HUMIDITY=${relativeHumidity}`);
telemetry.push({
"channel": channel,
"type": type,
@ -98,7 +100,6 @@ class CayenneLpp {
}
case this.LPP_BAROMETRIC_PRESSURE: {
const barometricPressure = buffer.readUInt16BE() / 10;
// console.log(`[CayenneLpp] parsed LPP_BAROMETRIC_PRESSURE=${barometricPressure}`);
telemetry.push({
"channel": channel,
"type": type,
@ -111,7 +112,6 @@ class CayenneLpp {
// int16: -327.67v to +327.67v
// should be readUInt16BE, but I'm using readInt16BE to allow for negative voltage
const voltage = buffer.readInt16BE() / 100;
// console.log(`[CayenneLpp] parsed LPP_VOLTAGE=${voltage}`);
telemetry.push({
"channel": channel,
"type": type,
@ -124,7 +124,6 @@ class CayenneLpp {
// int16: -327.67A to +327.67A
// should be readUInt16BE, but I'm using readInt16BE to allow for negative current
const current = buffer.readInt16BE() / 1000;
// console.log(`[CayenneLpp] parsed LPP_CURRENT=${current}`);
telemetry.push({
"channel": channel,
"type": type,
@ -134,7 +133,6 @@ class CayenneLpp {
}
case this.LPP_PERCENTAGE: {
const percentage = buffer.readUInt8();
// console.log(`[CayenneLpp] parsed LPP_PERCENTAGE=${percentage}`);
telemetry.push({
"channel": channel,
"type": type,
@ -144,7 +142,6 @@ class CayenneLpp {
}
case this.LPP_CONCENTRATION: {
const concentration = buffer.readUInt16BE();
// console.log(`[CayenneLpp] parsed LPP_CONCENTRATION=${concentration}`);
telemetry.push({
"channel": channel,
"type": type,
@ -154,7 +151,6 @@ class CayenneLpp {
}
case this.LPP_POWER: {
const power = buffer.readUInt16BE();
// console.log(`[CayenneLpp] parsed LPP_POWER=${power}`);
telemetry.push({
"channel": channel,
"type": type,
@ -166,7 +162,6 @@ class CayenneLpp {
const latitude = buffer.readInt24BE() / 10000;
const longitude = buffer.readInt24BE() / 10000;
const altitude = buffer.readInt24BE() / 100;
// console.log(`[CayenneLpp] parsed LPP_GPS=${latitude},${longitude},${altitude}`);
telemetry.push({
"channel": channel,
"type": type,
@ -180,7 +175,6 @@ class CayenneLpp {
}
// todo support all telemetry types, otherwise if an unknown is given, we can't read other telemetry after it
default: {
// console.log(`[CayenneLpp] unsupported type: ${type}`);
return telemetry;
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,15 @@ import SerialConnection from "./serial_connection.js";
class NodeJSSerialConnection extends SerialConnection {
/**
* @param path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401"
* @param {string} path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401"
*/
constructor(path) {
super();
/** @type {string} */
this.serialPortPath = path;
}
/** @returns {Promise<void>} */
async connect() {
// note: serialport module is only available in NodeJS, you shouldn't use NodeJSSerialConnection from a web browser
@ -43,6 +45,7 @@ class NodeJSSerialConnection extends SerialConnection {
}
/** @returns {Promise<void>} */
async close() {
try {
await this.serialPort.close();
@ -51,6 +54,10 @@ class NodeJSSerialConnection extends SerialConnection {
}
}
/**
* @param {Uint8Array} bytes
* @returns {Promise<void>}
*/
/* override */ async write(bytes) {
this.serialPort.write(bytes);
}

View file

@ -7,16 +7,26 @@ class SerialConnection extends Connection {
constructor() {
super();
/** @type {number[]} */
this.readBuffer = [];
if(this.constructor === SerialConnection){
throw new Error("SerialConnection is an abstract class and can't be instantiated.");
}
}
/**
* @param {Uint8Array} bytes
* @returns {Promise<void>}
*/
async write(bytes) {
throw new Error("Not Implemented: write must be implemented by SerialConnection sub class.");
}
/**
* @param {number} frameType
* @param {Uint8Array} frameData
* @returns {Promise<void>}
*/
async writeFrame(frameType, frameData) {
// create frame
@ -34,12 +44,20 @@ class SerialConnection extends Connection {
}
/**
* @param {Uint8Array} data
* @returns {Promise<void>}
*/
async sendToRadioFrame(data) {
// write "app to radio" frame 0x3c "<"
this.emit("tx", data);
await this.writeFrame(0x3c, data);
}
/**
* @param {Uint8Array | number[]} value
* @returns {Promise<void>}
*/
async onDataReceived(value) {
// append received bytes to read buffer

View file

@ -5,13 +5,19 @@ import Connection from "./connection.js";
class TCPConnection extends Connection {
/**
* @param {string} host
* @param {number} port
*/
constructor(host, port) {
super();
this.host = host;
this.port = port;
/** @type {number[]} */
this.readBuffer = [];
}
/** @returns {Promise<void>} */
async connect() {
// note: net module is only available in NodeJS, you shouldn't use TCPConnection from a web browser
@ -21,7 +27,7 @@ class TCPConnection extends Connection {
this.socket = new Socket();
// handle received data
this.socket.on('data', (data) => {
this.socket.on('data', (/** @type {Uint8Array} */ data) => {
this.onSocketDataReceived(data);
});
@ -42,6 +48,7 @@ class TCPConnection extends Connection {
}
/** @param {Uint8Array | number[]} data */
onSocketDataReceived(data) {
// append received bytes to read buffer
@ -96,7 +103,7 @@ class TCPConnection extends Connection {
}
close() {
async close() {
try {
this.socket.destroy();
} catch(e) {
@ -104,10 +111,19 @@ class TCPConnection extends Connection {
}
}
/**
* @param {Uint8Array} bytes
* @returns {Promise<void>}
*/
async write(bytes) {
this.socket.write(new Uint8Array(bytes));
}
/**
* @param {number} frameType
* @param {Uint8Array} frameData
* @returns {Promise<void>}
*/
async writeFrame(frameType, frameData) {
// create frame
@ -125,6 +141,10 @@ class TCPConnection extends Connection {
}
/**
* @param {Uint8Array} data
* @returns {Promise<void>}
*/
async sendToRadioFrame(data) {
// write "app to radio" frame 0x3c "<"
this.emit("tx", data);

View file

@ -3,24 +3,31 @@ import Connection from "./connection.js";
class WebBleConnection extends Connection {
/** @param {any} bleDevice */
constructor(bleDevice) {
super();
this.bleDevice = bleDevice;
/** @type {any} */
this.gattServer = null;
/** @type {any} */
this.rxCharacteristic = null;
/** @type {any} */
this.txCharacteristic = null;
this.init();
}
/** @returns {Promise<WebBleConnection | null | undefined>} */
static async open() {
// ensure browser supports web bluetooth
// @ts-ignore - Web Bluetooth API
if(!navigator.bluetooth){
alert("Web Bluetooth is not supported in this browser");
return;
}
// ask user to select device
// @ts-ignore - Web Bluetooth API
const device = await navigator.bluetooth.requestDevice({
filters: [
{
@ -76,6 +83,7 @@ class WebBleConnection extends Connection {
}
/** @returns {Promise<void>} */
async close() {
try {
this.gattServer?.disconnect();
@ -85,6 +93,10 @@ class WebBleConnection extends Connection {
}
}
/**
* @param {Uint8Array} bytes
* @returns {Promise<void>}
*/
async write(bytes) {
try {
// fixme: NetworkError: GATT operation already in progress.
@ -96,6 +108,10 @@ class WebBleConnection extends Connection {
}
}
/**
* @param {Uint8Array} frame
* @returns {Promise<void>}
*/
async sendToRadioFrame(frame) {
this.emit("tx", frame);
await this.write(frame);

View file

@ -2,6 +2,7 @@ import SerialConnection from "./serial_connection.js";
class WebSerialConnection extends SerialConnection {
/** @param {any} serialPort */
constructor(serialPort) {
super();
@ -23,15 +24,18 @@ class WebSerialConnection extends SerialConnection {
}
/** @returns {Promise<WebSerialConnection | null>} */
static async open() {
// ensure browser supports web serial
// @ts-ignore - Web Serial API
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// ask user to select device
// @ts-ignore - Web Serial API
const serialPort = await navigator.serial.requestPort({
filters: [],
});
@ -45,6 +49,7 @@ class WebSerialConnection extends SerialConnection {
}
/** @returns {Promise<void>} */
async close() {
// release reader lock
@ -63,6 +68,10 @@ class WebSerialConnection extends SerialConnection {
}
/**
* @param {Uint8Array} bytes
* @returns {Promise<void>}
*/
/* override */ async write(bytes) {
const writer = this.writable.getWriter();
try {

View file

@ -1,3 +1,4 @@
/** Constants used by the MeshCore companion protocol. */
class Constants {
static SupportedCompanionProtocolVersion = 1;

View file

@ -1,9 +1,14 @@
class EventEmitter {
constructor() {
/** @type {Map<string | number, Function[]>} */
this.eventListenersMap = new Map();
}
/**
* @param {string | number} event
* @param {Function} callback
*/
on(event, callback) {
// create list of listeners for event if it doesn't exist
@ -16,6 +21,10 @@ class EventEmitter {
}
/**
* @param {string | number} event
* @param {Function} callback
*/
off(event, callback) {
// remove callback from listeners for this event
@ -26,6 +35,10 @@ class EventEmitter {
}
/**
* @param {string | number} event
* @param {Function} callback
*/
once(event, callback) {
// internal callback to handle the event
@ -44,6 +57,10 @@ class EventEmitter {
}
/**
* @param {string | number} event
* @param {...any} data
*/
emit(event, ...data) {
// invoke each listener for this event

View file

@ -10,6 +10,8 @@ import Packet from "./packet.js";
import BufferUtils from "./buffer_utils.js";
import CayenneLpp from "./cayenne_lpp.js";
export * from "./types.js";
export {
Connection,
WebBleConnection,

View file

@ -27,9 +27,15 @@ 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
/**
* @param {number} header
* @param {Uint8Array} path
* @param {Uint8Array} payload
* @param {number | null} transportCode1
* @param {number | null} transportCode2
*/
constructor(header, path, payload, transportCode1, transportCode2) {
this.header = header;
this.path = path;
this.payload = payload;
@ -46,6 +52,10 @@ class Packet {
}
/**
* @param {Uint8Array} bytes
* @returns {Packet}
*/
static fromBytes(bytes) {
const bufferReader = new BufferReader(bytes);
@ -71,10 +81,12 @@ class Packet {
}
/** @returns {number} */
getRouteType() {
return this.header & Packet.PH_ROUTE_MASK;
}
/** @returns {string | null} */
getRouteTypeString() {
switch(this.getRouteType()){
case Packet.ROUTE_TYPE_FLOOD: return "FLOOD";
@ -85,18 +97,22 @@ class Packet {
}
}
/** @returns {boolean} */
isRouteFlood() {
return this.getRouteType() === Packet.ROUTE_TYPE_FLOOD;
}
/** @returns {boolean} */
isRouteDirect() {
return this.getRouteType() === Packet.ROUTE_TYPE_DIRECT;
}
/** @returns {number} */
getPayloadType() {
return (this.header >> Packet.PH_TYPE_SHIFT) & Packet.PH_TYPE_MASK;
}
/** @returns {string | null} */
getPayloadTypeString() {
switch(this.getPayloadType()){
case Packet.PAYLOAD_TYPE_REQ: return "REQ";
@ -114,6 +130,7 @@ class Packet {
}
}
/** @returns {number} */
getPayloadVer() {
return (this.header >> Packet.PH_VER_SHIFT) & Packet.PH_VER_MASK;
}
@ -122,6 +139,7 @@ class Packet {
this.header = 0xFF;
}
/** @returns {boolean} */
isMarkedDoNotRetransmit() {
return this.header === 0xFF;
}
@ -139,6 +157,7 @@ class Packet {
}
}
/** @returns {{ src: number, dest: number }} */
parsePayloadTypePath() {
// parse bytes
@ -154,6 +173,7 @@ class Packet {
}
/** @returns {{ src: number, dest: number, encrypted: Uint8Array }} */
parsePayloadTypeReq() {
// parse bytes
@ -170,6 +190,7 @@ class Packet {
}
/** @returns {{ src: number, dest: number }} */
parsePayloadTypeResponse() {
// parse bytes
@ -185,6 +206,7 @@ class Packet {
}
/** @returns {{ src: number, dest: number }} */
parsePayloadTypeTxtMsg() {
// parse bytes
@ -200,12 +222,14 @@ class Packet {
}
/** @returns {{ ack_code: Uint8Array }} */
parsePayloadTypeAck() {
return {
ack_code: this.payload,
};
}
/** @returns {{ public_key: Uint8Array, timestamp: number, app_data: import("./types.js").AdvertParsedData }} */
parsePayloadTypeAdvert() {
const advert = Advert.fromBytes(this.payload);
return {
@ -215,6 +239,7 @@ class Packet {
};
}
/** @returns {{ src: Uint8Array, dest: number }} */
parsePayloadTypeAnonReq() {
// parse bytes

View file

@ -1,5 +1,10 @@
class RandomUtils {
/**
* @param {number} min
* @param {number} max
* @returns {number}
*/
static getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);

233
src/types.js Normal file
View file

@ -0,0 +1,233 @@
/**
* @typedef {number} EpochSeconds Unix timestamp in seconds
*/
/**
* @typedef {number} Milliseconds Duration in milliseconds
*/
/**
* @typedef {number} MilliVolts Voltage in millivolts
*/
/**
* @typedef {number} TxtType Text message type: 0=Plain, 1=CliData, 2=SignedPlain
*/
/**
* @typedef {object} SelfInfo
* @property {number} type
* @property {number} txPower
* @property {number} maxTxPower
* @property {Uint8Array} publicKey
* @property {number} advLat
* @property {number} advLon
* @property {Uint8Array} reserved
* @property {number} manualAddContacts
* @property {number} radioFreq
* @property {number} radioBw
* @property {number} radioSf
* @property {number} radioCr
* @property {string} name
*/
/**
* @typedef {object} Contact
* @property {Uint8Array} publicKey
* @property {number} type
* @property {number} flags
* @property {number} outPathLen
* @property {Uint8Array} outPath
* @property {string} advName
* @property {EpochSeconds} lastAdvert
* @property {number} advLat
* @property {number} advLon
* @property {EpochSeconds} lastMod
*/
/**
* @typedef {object} ContactMessage
* @property {Uint8Array} pubKeyPrefix
* @property {number} pathLen
* @property {TxtType} txtType
* @property {EpochSeconds} senderTimestamp
* @property {string} text
*/
/**
* @typedef {object} ChannelMessage
* @property {number} channelIdx
* @property {number} pathLen
* @property {TxtType} txtType
* @property {EpochSeconds} senderTimestamp
* @property {string} text
*/
/**
* @typedef {object} ChannelInfo
* @property {number} channelIdx
* @property {string} name
* @property {Uint8Array} secret
*/
/**
* @typedef {object} SentResponse
* @property {number} result
* @property {number} expectedAckCrc
* @property {Milliseconds} estTimeout
*/
/**
* @typedef {object} DeviceInfo
* @property {number} firmwareVer
* @property {Uint8Array} reserved
* @property {string} firmware_build_date
* @property {string} manufacturerModel
*/
/**
* @typedef {object} BatteryVoltageResponse
* @property {MilliVolts} batteryMilliVolts
*/
/**
* @typedef {object} ExportContactResponse
* @property {Uint8Array} advertPacketBytes
*/
/**
* @typedef {object} PrivateKeyResponse
* @property {Uint8Array} privateKey
*/
/**
* @typedef {object} RepeaterStats
* @property {MilliVolts} batt_milli_volts
* @property {number} curr_tx_queue_len
* @property {number} noise_floor
* @property {number} last_rssi
* @property {number} n_packets_recv
* @property {number} n_packets_sent
* @property {number} total_air_time_secs
* @property {number} total_up_time_secs
* @property {number} n_sent_flood
* @property {number} n_sent_direct
* @property {number} n_recv_flood
* @property {number} n_recv_direct
* @property {number} err_events
* @property {number} last_snr
* @property {number} n_direct_dups
* @property {number} n_flood_dups
*/
/**
* @typedef {object} SyncMessageResult
* @property {ContactMessage} [contactMessage]
* @property {ChannelMessage} [channelMessage]
*/
/**
* @typedef {object} Neighbour
* @property {Uint8Array} publicKeyPrefix
* @property {number} heardSecondsAgo
* @property {number} snr
*/
/**
* @typedef {object} NeighboursResult
* @property {number} totalNeighboursCount
* @property {Neighbour[]} neighbours
*/
/**
* @typedef {object} TraceDataResult
* @property {number} reserved
* @property {number} pathLen
* @property {number} flags
* @property {number} tag
* @property {number} authCode
* @property {Uint8Array} pathHashes
* @property {Uint8Array} pathSnrs
* @property {number} lastSnr
*/
/**
* @typedef {object} AdvertParsedData
* @property {string | null} type
* @property {number | null} lat
* @property {number | null} lon
* @property {string | null} name
* @property {number | null} feat1
* @property {number | null} feat2
*/
/**
* @typedef {object} CayenneTelemetryEntry
* @property {number} channel
* @property {number} type
* @property {number | {latitude: number, longitude: number, altitude: number}} value
*/
/**
* @typedef {object} LoginSuccessPush
* @property {number} reserved
* @property {Uint8Array} pubKeyPrefix
*/
/**
* @typedef {object} StatusResponsePush
* @property {number} reserved
* @property {Uint8Array} pubKeyPrefix
* @property {Uint8Array} statusData
*/
/**
* @typedef {object} RawDataPush
* @property {number} lastSnr
* @property {number} lastRssi
* @property {number} reserved
* @property {Uint8Array} payload
*/
/**
* @typedef {object} SendConfirmedPush
* @property {number} ackCode
* @property {Milliseconds} roundTrip
*/
/**
* @typedef {object} LogRxDataPush
* @property {number} lastSnr
* @property {number} lastRssi
* @property {Uint8Array} raw
*/
/**
* @typedef {object} TelemetryResponsePush
* @property {number} reserved
* @property {Uint8Array} pubKeyPrefix
* @property {Uint8Array} lppSensorData
*/
/**
* @typedef {object} BinaryResponsePush
* @property {number} reserved
* @property {number} tag
* @property {Uint8Array} responseData
*/
/**
* @typedef {object} NewAdvertPush
* @property {Uint8Array} publicKey
* @property {number} type
* @property {number} flags
* @property {number} outPathLen
* @property {Uint8Array} outPath
* @property {string} advName
* @property {EpochSeconds} lastAdvert
* @property {number} advLat
* @property {number} advLon
* @property {EpochSeconds} lastMod
*/
export {};

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"include": ["src/**/*.js"],
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": true,
"outDir": "dist",
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ESNext",
"strict": false,
"skipLibCheck": true
}
}