If a session's config bundle doesn't include a NodeInfo for the device
itself (fresh flash, cleared nodeDB, some firmware builds), the web
sidebar's `useMyNodeAsProto()` selector — which does
`nodes.list.find(n => n.num === myNodeNum)` — never resolves and the
"loading" spinner sticks indefinitely after the overlay closes.
NodesClient now subscribes to onMyNodeInfo. If no entry exists for the
incoming myNodeNum, it seeds a stub Node with just `num` set; subsequent
NodeInfo / UserPacket / PositionPacket events patch real data on top.
Guards against myNodeNum === 0 and against overwriting an existing entry.
DeviceInfoPanel already wraps user-dependent rendering in `{user && …}`,
so no app-layer change is needed — the panel quietly omits the avatar
block until a real UserPacket arrives.
Tests cover: stub seeded when only MyNodeInfo arrives, real NodeInfo wins
over a later MyNodeInfo, real NodeInfo patches over an existing stub, and
myNodeNum=0 is ignored.
@meshtastic/sdk
TypeScript SDK for talking to Meshtastic radios. Works in the browser, in
Node, and in Deno. Pick a transport, hand it a MeshClient, and you've got reactive access to the
device's chat, nodes, channels, config, and telemetry.
pnpm add @meshtastic/sdk @meshtastic/transport-web-serial
Quick example
import { MeshClient } from "@meshtastic/sdk";
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
const transport = await TransportWebSerial.create({ baudRate: 115200 });
const client = new MeshClient({ transport });
await client.connect();
client.device.status.subscribe((s) => console.log("status:", s));
console.log("my node:", client.device.myNodeNum.value);
console.log("nodes:", client.nodes.list.value);
await client.chat.send({ text: "hello mesh" });
client.events.onConfigComplete.subscribe(() => console.log("configured"));
client.events.onRebooted.subscribe(() => console.log("device rebooted"));
State is exposed as signals. Subscribe to them, or read .value synchronously. Commands are async and
resolve when the device acks.
Why this is a new package
If you've used Meshtastic on the web before, you've probably seen @meshtastic/core. That package
mixed protocol, transports, and consumer state in one class. It worked, but it bled assumptions:
browser apps still pulled the Node serial driver, slice state had a few React-isms baked in, and there
wasn't a clean seam for unit testing or non-browser runtimes.
We split it into three pieces:
@meshtastic/sdk(this one) owns the protocol, the queue, the codec, and the reactive state. No React. No DOM. No Node builtins. It runs the same everywhere.@meshtastic/transport-*packages are thin byte adapters. Install only the ones you need. The SDK talks to them through a tinyTransportinterface (fromDevice/toDevicestreams plusdisconnect()), so you can write your own without forking anything.@meshtastic/sdk-reactlives separately for the React hooks. Skip it if you're not using React.
Upgrading from @meshtastic/core
You don't have to port everything at once. @meshtastic/sdk re-exports the old MeshDevice class
from src/shim/, with the same public surface it had in core, so most apps can swap the dependency
and keep building while they migrate file by file.
Treat the shim as a moving truck, not furniture. New code should target MeshClient and the feature
clients directly — that's where everything new lands (the onRebooted event added in this release,
for instance, is on client.events, not the shim). We'll delete src/shim/ once the web client
itself is fully off MeshDevice, so don't build anything new on top of it.
What's in the client
MeshClient is just a small composition root. The interesting bits are the feature clients hanging
off it:
client.device— identity, status, metadata, reboot / shutdown / factory-resetclient.chat— channel + direct messages, drafts, unread trackingclient.nodes— live node list, per-node lookups, error statesclient.channels— channel configclient.config— device + module config, plus a staged-editConfigEditorclient.telemetry— environment / device / power metricsclient.position— position broadcast + node positionsclient.traceroute— mesh route discoveryclient.files— XModem get/put
Each one can be used on its own; MeshClient just hands them a shared transport, queue, and event
bus.
Subpath exports
| Import path | What's there |
|---|---|
@meshtastic/sdk |
MeshClient, feature clients, signal types |
@meshtastic/sdk/transport |
The Transport interface and related types — use this if you're writing a custom adapter |
@meshtastic/sdk/protobuf |
Re-export of @meshtastic/protobufs for raw packet construction |
@meshtastic/sdk/testing |
createFakeTransport() for unit tests |
Testing without a radio
import { MeshClient } from "@meshtastic/sdk";
import { createFakeTransport } from "@meshtastic/sdk/testing";
const { transport, respond } = createFakeTransport();
const client = new MeshClient({ transport });
respond.withMyNodeInfo({ myNodeNum: 42 });
respond.withConfigCompleteId(1);
// client.device, client.nodes, etc. are now populated.
The fake transport is the same one the SDK's own integration tests use.
Using this with React
You can use @meshtastic/sdk straight from a React component — every signal exposes a .subscribe()
that pairs cleanly with useSyncExternalStore, and .value for synchronous reads. If you want to
write that bridge yourself, go ahead; nothing in the SDK requires React.
For most apps it's not worth it. @meshtastic/sdk-react
ships the bridge plus a set of opinionated hooks:
MeshProvider/MeshRegistryProviderso you don't have to thread theMeshClientthrough props. The registry provider is useful when an app holds connections to several radios at once (USB + Bluetooth in a desktop dashboard, for example) and you want hooks to default to the active one.- Hooks that mirror the feature clients —
useDevice,useChat,useNodes,useChannels,useConfig,useConnectionProgress, and so on — each scoped to a single signal so re-renders stay narrow. useSignal/useSignalValueescape hatches for the cases where the bundled hooks aren't quite the right shape.
The bindings are kept in a separate package on purpose. Node CLIs, Deno scripts, and embedded use
cases all consume @meshtastic/sdk without dragging React along — the dependency only enters the
tree when you actually need it.
Source / issues
https://github.com/meshtastic/web
License
GPL-3.0-only.