TypeScript library for connecting to and working with MeshCore companion devices. Supports communication over Web Serial, Web Bluetooth (BLE), and any custom transport you implement.
npm install meshcore-ts
# or
pnpm add meshcore-tsimport { WebSerialConnection } from "meshcore-ts";
// Prompts the user to pick a serial port (115200 baud).
const conn = await WebSerialConnection.open();
if (!conn) throw new Error("No port selected");
conn.on("connected", async () => {
console.log("Connected to companion!");
// Fetch device info
const info = await conn.sendCommandDeviceQuery();
console.log("Device info:", info);
// Get all contacts
const contacts = await conn.sendCommandGetContacts();
console.log("Contacts:", contacts);
});
conn.on("disconnected", () => {
console.log("Disconnected.");
});import { WebBleConnection } from "meshcore-ts";
// Shows the browser Bluetooth device picker.
const conn = await WebBleConnection.open();
if (!conn) throw new Error("No device selected");
conn.on("connected", async () => {
const info = await conn.sendCommandDeviceQuery();
console.log("Device info:", info);
});import { WebSerialConnection, Constants } from "meshcore-ts";
const conn = await WebSerialConnection.open();
conn.on("connected", async () => {
const contacts = await conn.sendCommandGetContacts();
const alice = contacts.find(c => c.advName === "Alice");
if (!alice) return;
await conn.sendCommandSendTxtMsg(
Constants.CommandCodes.SendTxtMsg, // txtType
0, // attempt
Math.floor(Date.now() / 1000), // senderTimestamp
alice.publicKey.slice(0, 6), // pubKeyPrefix
"Hello from meshcore-ts!", // text
);
});conn.on("connected", async () => {
const channelIndex = 0;
await conn.sendCommandSendChannelTxtMsg(
0, // txtType
channelIndex,
Math.floor(Date.now() / 1000),
"Hello channel!",
);
});conn.on("contactMessage", (msg) => {
console.log(`[DM from ${msg.senderPubKeyPrefix}]:`, msg.text);
});
conn.on("channelMessage", (msg) => {
console.log(`[Channel ${msg.channelIdx}]:`, msg.text);
});import { Advert } from "meshcore-ts";
const raw = new Uint8Array([ /* raw advertisement bytes */ ]);
const advert = Advert.fromBytes(raw);
console.log("Node name:", advert.parsed.name);
console.log("Node type:", advert.parsed.type); // "CHAT" | "REPEATER" | "ROOM" | "SENSOR" | "NONE"
console.log("Location:", advert.parsed.lat, advert.parsed.lon);
const valid = await advert.isVerified();
console.log("Signature valid:", valid);import { Packet } from "meshcore-ts";
const raw = new Uint8Array([ /* raw packet bytes */ ]);
const packet = Packet.fromBytes(raw);
console.log("Route type:", packet.route_type_string); // "DIRECT" | "FLOOD" | ...
console.log("Payload type:", packet.payload_type_string); // "TXT_MSG" | "ADVERT" | ...import { CayenneLpp } from "meshcore-ts";
const raw = new Uint8Array([ /* raw LPP bytes */ ]);
const records = CayenneLpp.decode(raw);
for (const record of records) {
console.log(`ch=${record.channel} type=${record.type} value=${JSON.stringify(record.value)}`);
}import { TransportKeyUtil } from "meshcore-ts";
const key = await TransportKeyUtil.getHashtagRegionKey("#mygroup");
console.log("Key (hex):", Buffer.from(key).toString("hex"));Transport over the browser Web Serial API.
| Member | Description |
|---|---|
static open() |
Prompts user for port → returns a connected instance |
close() |
Closes the serial port |
| (all Connection methods) | Inherited |
Transport over the browser Web Bluetooth API.
| Member | Description |
|---|---|
static open() |
Shows Bluetooth device picker → returns a connected instance |
| (all Connection methods) | Inherited |
Provides the full companion protocol command/response API.
| Method | Returns | Description |
|---|---|---|
sendCommandAppStart() |
Promise<void> |
Initiates the companion app handshake |
sendCommandDeviceQuery() |
Promise<DeviceInfoResponse> |
Queries firmware/hardware info |
sendCommandGetContacts(since?) |
Promise<MeshContactRecord[]> |
Fetches the contact list |
sendCommandSendTxtMsg(...) |
Promise<SentResponse> |
Sends a direct text message |
sendCommandSendChannelTxtMsg(...) |
Promise<SentResponse> |
Sends a channel text message |
sendCommandGetDeviceTime() |
Promise<CurrTimeResponse> |
Reads device clock |
sendCommandSetDeviceTime(ts) |
Promise<OkResponse> |
Sets device clock |
sendCommandSendSelfAdvert() |
Promise<OkResponse> |
Broadcasts a self-advertisement |
sendCommandSetAdvertName(name) |
Promise<OkResponse> |
Sets the node's advertised name |
sendCommandGetBatteryVoltage() |
Promise<BatteryVoltageResponse> |
Reads battery voltage |
sendCommandGetChannel(idx) |
Promise<ChannelDataResponse> |
Reads a stored channel |
sendCommandSetChannel(idx, ...) |
Promise<OkResponse> |
Writes a channel |
sendCommandReboot() |
Promise<void> |
Reboots the companion |
| (more…) | — | See source for full list |
| Event | Payload type | Fired when |
|---|---|---|
"connected" |
— | Transport connected and handshake done |
"disconnected" |
— | Transport disconnected |
"contactMessage" |
ContactMessageResponse |
A direct message arrived |
"channelMessage" |
ChannelMessageResponse |
A channel message arrived |
"telemetry" |
TelemetryResponsePush |
Telemetry push received |
"traceData" |
TraceDataPush |
Trace-path data received |
"statusResponse" |
StatusResponsePush |
Status response push |
"logRxData" |
LogRxDataPush |
Raw RX log data |
"rx" |
ByteArrayLike |
Raw inbound frame (debugging) |
"tx" |
ByteArrayLike |
Raw outbound frame (debugging) |
Parses MeshCore advertisement packets.
const advert = Advert.fromBytes(bytes);
advert.parsed.name // string | null
advert.parsed.type // "CHAT" | "REPEATER" | "ROOM" | "SENSOR" | "NONE" | null
advert.parsed.lat // number | null
advert.parsed.lon // number | null
await advert.isVerified() // boolean — ed25519 signature checkParses raw mesh packets.
const packet = Packet.fromBytes(bytes);
packet.route_type_string // "DIRECT" | "FLOOD" | "TRANSPORT_DIRECT" | "TRANSPORT_FLOOD" | null
packet.payload_type_string // "TXT_MSG" | "ADVERT" | "PATH" | "TRACE" | … | null
packet.payload // Uint8Array — raw payload bytes
packet.path // Uint8Array — routing pathDecodes Cayenne LPP binary telemetry.
const records = CayenneLpp.decode(bytes);
// records: Array<{ channel: number, type: number, value: number | { latitude, longitude, altitude } }>All protocol constants grouped into nested objects:
Constants.CommandCodes.*— outbound command byte valuesConstants.ResponseCodes.*— inbound response byte valuesConstants.PushCodes.*— async push notification byte valuesConstants.Ble.*— BLE service/characteristic UUIDsConstants.SerialFrameTypes.*— serial frame type bytesConstants.StatsTypes.*— stats frame sub-typesConstants.DataTypes.*— datagram data-type values
BufferUtils.bytesToHex(bytes) // Uint8Array → "deadbeef"
BufferUtils.hexToBytes("deadbeef") // → Uint8Array
BufferUtils.base64ToBytes(str) // → Uint8Array
BufferUtils.areBuffersEqual(a, b) // → booleanconst path = MeshCorePath.fromPathAndLength(pathBytes, pathLen);
path?.toHexStrings() // string[] — one hex string per hopconst key = await TransportKeyUtil.getHashtagRegionKey("#region");
// key: Uint8Array (32 bytes)- Bump the version in
package.json. - Commit and push:
git commit -m "chore: release vX.Y.Z". - Create and push a tag:
git tag vX.Y.Z && git push --tags. - The
publish.ymlGitHub Action will build, test, and publish to npm automatically.
Required secret: add
NPM_TOKENto your repository's Settings → Secrets and variables → Actions.
pnpm install
pnpm test # run tests with coverage
pnpm run typecheck # type-check only
pnpm run lint # type-check + ESLint
pnpm run build # produce dist/100% coverage is enforced on all metrics (branches, functions, lines, statements) except src/index.ts and src/connection/connection_types.ts (type-definition and re-export files).
MIT
- Chrome or Edge desktop with Web Serial support.
localhostor HTTPS.- A MeshCore device running companion USB firmware.
Safari and Firefox are not supported for this Web Serial path.
pnpm install
pnpm run dev
pnpm run build
pnpm run lint- The app uses
@liamcottle/meshcore.jsas the first transport and protocol layer. - Snapshot commands are issued sequentially to avoid overlapping companion protocol requests.
- Stored routed contacts are actively probed for trace responses after snapshots and topology refreshes, and can also be retraced from the dashboard.
- The current graph is based on companion-visible route state. It is not yet a full network-wide routing model.
- Verify the data surface on real hardware.
- Extend the analytics store with repeater status, telemetry parsing, and route history.
- Decide later whether a BLE fallback or a lower-level protocol integration is needed.