diff --git a/packages/sdk-react/README.md b/packages/sdk-react/README.md
index a9b3d7dd..c8bf2458 100644
--- a/packages/sdk-react/README.md
+++ b/packages/sdk-react/README.md
@@ -1,20 +1,20 @@
# @meshtastic/sdk-react
-React hooks and provider for `@meshtastic/sdk`.
-
-## Install
+React hooks for [`@meshtastic/sdk`](https://www.npmjs.com/package/@meshtastic/sdk). Wraps the SDK's
+signal-based state in hooks that only re-render when the data they touch changes.
```sh
pnpm add @meshtastic/sdk @meshtastic/sdk-react @meshtastic/transport-web-serial
```
-## Quickstart
+## One device
+
+If your app talks to a single radio at a time, wrap your tree in `MeshProvider` and you're done:
```tsx
-import { MeshClient } from "@meshtastic/sdk";
+import { MeshClient, ChannelNumber } from "@meshtastic/sdk";
import { MeshProvider, useDevice, useChat } from "@meshtastic/sdk-react";
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
-import { ChannelNumber } from "@meshtastic/sdk";
const transport = await TransportWebSerial.create({ baudRate: 115200 });
const client = new MeshClient({ transport });
@@ -31,11 +31,101 @@ function App() {
function Status() {
const { status, myNodeNum } = useDevice();
const { messages, send } = useChat(ChannelNumber.Primary);
- return
+ );
}
```
-All hooks are read-only against a single `MeshClient` instance supplied through context. Commands are returned as stable functions.
+## Several devices
+
+For apps that hold multiple connections at once — say, a desktop dashboard with a USB device plus a
+Bluetooth one — use the registry provider instead. Hooks default to the "active" client, and you can
+target a specific one by id when you need to.
+
+```tsx
+import { MeshRegistry } from "@meshtastic/sdk";
+import { MeshRegistryProvider, useNodes } from "@meshtastic/sdk-react";
+
+const registry = new MeshRegistry();
+registry.register("usb-1", usbClient);
+registry.register("ble-1", bleClient);
+registry.setActive("usb-1");
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+function Nodes() {
+ const nodes = useNodes(); // active client
+ return
{nodes.length} nodes
;
+}
+```
+
+## Why this is a separate package
+
+The SDK used to live in `@meshtastic/core`, and its React conventions (effects, refs, a few hook
+helpers) were baked right into protocol code. That made it impossible to ship the SDK to non-React
+consumers — Node CLIs, Deno scripts, native shells — without dragging React along.
+
+When we split that up, the React bits moved here. Now the layering is straightforward:
+
+- `@meshtastic/sdk` — protocol, state, transports. No React, no DOM.
+- `@meshtastic/sdk-react` — this. Hooks and providers, nothing else.
+- `@meshtastic/transport-*` — per-runtime byte transports.
+
+If you're not using React, you don't need this package at all. Read `@meshtastic/sdk` directly.
+
+## Hooks
+
+Most components only need two or three of these. The full list is here for reference:
+
+**Plumbing**
+
+- `useClient()` — the client from `MeshProvider`
+- `useActiveClient()` — the active client from a `MeshRegistryProvider`
+- `useClientById(id)` — a specific client from the registry
+- `useMeshRegistry()` / `useOptionalMeshRegistry()` — the registry itself
+- `useSignal(signal)` / `useSignalValue(signal)` — bridge any SDK signal into React state
+
+**Connection**
+
+- `useConnection()` — status + lifecycle helpers
+- `useConnectionProgress()` — live "configuring" progress with per-section counters (handy for
+ building a connecting overlay)
+- `useMeshDevice()` — convenience wrapper around the legacy `MeshDevice` shim
+
+**Device & node state**
+
+- `useDevice()`
+- `useNodes()` / `useNode(num)`
+- `useNodeError(num)` / `useNodeErrors()` / `useHasNodeError(num)`
+
+**Messaging**
+
+- `useChat(channel)` — channel messages
+- `useDirectChat(peer)` — DMs
+- `useDraft(...)` — draft messages
+
+**Config**
+
+- `useChannels()` / `useChannel(idx)`
+- `useConfig()` / `useModuleConfig()` / `useIsRegionUnset()`
+- `useConfigEditor()` — staged edits with dirty tracking
+
+Each hook reads from a single signal, so updates only re-render components that actually use that
+data.
+
+## Source / issues
+
+
## License
diff --git a/packages/sdk/README.md b/packages/sdk/README.md
index 1727ffc1..bc7f3093 100644
--- a/packages/sdk/README.md
+++ b/packages/sdk/README.md
@@ -1,16 +1,14 @@
# @meshtastic/sdk
-Domain-driven SDK for Meshtastic devices. Feature slices with signals-backed reactive state.
-
-Replaces `@meshtastic/core`. During migration a shim layer re-exports the legacy `MeshDevice` class so existing consumers keep building — see `src/shim/`.
-
-## Install
+TypeScript SDK for talking to [Meshtastic](https://meshtastic.org) 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.
```sh
pnpm add @meshtastic/sdk @meshtastic/transport-web-serial
```
-## Quickstart
+## Quick example
```ts
import { MeshClient } from "@meshtastic/sdk";
@@ -20,20 +18,90 @@ const transport = await TransportWebSerial.create({ baudRate: 115200 });
const client = new MeshClient({ transport });
await client.connect();
-client.chat.send({ text: "hello mesh" });
-console.log(client.nodes.list.value);
+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"));
```
-See the repo root [README](../../README.md) for architecture and feature slice layout.
+State is exposed as signals. Subscribe to them, or read `.value` synchronously. Commands are async and
+resolve when the device acks.
-## Layout
+## 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 tiny `Transport` interface (`fromDevice` / `toDevice` streams plus
+ `disconnect()`), so you can write your own without forking anything.
+- `@meshtastic/sdk-react` lives separately for the React hooks. Skip it if you're not using React.
+
+The old `MeshDevice` class is still exported from `src/shim/` so existing `@meshtastic/core` apps can
+upgrade without a full rewrite. The shim will be removed once the web client finishes its migration —
+new code should use `MeshClient` directly.
+
+## 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-reset
+- `client.chat` — channel + direct messages, drafts, unread tracking
+- `client.nodes` — live node list, per-node lookups, error states
+- `client.channels` — channel config
+- `client.config` — device + module config, plus a staged-edit `ConfigEditor`
+- `client.telemetry` — environment / device / power metrics
+- `client.position` — position broadcast + node positions
+- `client.traceroute` — mesh route discovery
+- `client.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
+
+```ts
+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.
```
-src/
- core/ # shared kernel: client, transport, event-bus, queue, xmodem, signals, logging, packet-codec
- features/ # DDD feature slices (device, chat, nodes, channels, config, telemetry, position, traceroute, files)
- shim/ # legacy MeshDevice compatibility exports (removed in Phase C)
-```
+
+The fake transport is the same one the SDK's own integration tests use.
+
+## React?
+
+[`@meshtastic/sdk-react`](https://www.npmjs.com/package/@meshtastic/sdk-react).
+
+## Source / issues
+
+
## License