Skip to content

Commit 8f49ef9

Browse files
Add device-connect-server learning path (draft)
Introduces a new Learning Path covering Device Connect's server tier on top of the edge SDK. Uses the hosted Device Connect Fabric portal (fabric.deviceconnect.dev) to mint NATS JWT credentials, then walks through running two simulated robot-arm devices and one orchestrating agent against the tenant. Devices and the agent talk through the shared server (NATS, Zenoh, or MQTT backend). Marked as draft (cascade: draft: true) until reviewed. Video asset is hosted externally at https://github.com/kavya-chennoju/arm-learning-path-assets and referenced via raw.githubusercontent.com, matching the pattern already in use by the tinkerblox_ultraedge Learning Path.
1 parent 0154cab commit 8f49ef9

4 files changed

Lines changed: 391 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: Connect Devices and AI agents through Device Connect Server
3+
4+
draft: true
5+
cascade:
6+
draft: true
7+
8+
minutes_to_complete: 30
9+
10+
who_is_this_for: This is a follow-on topic for developers who have a working Device Connect mesh and want to add a server layer on top. The server gives you a persistent device registry, distributed state, and security primitives (commissioning, ACLs) so you can operate a multi-network fleet from one place.
11+
12+
learning_objectives:
13+
- Understand what the Device Connect server adds on top of the edge SDK and when you'd reach for it
14+
- Provision a hosted tenant on the Device Connect portal and download per-device NATS credentials
15+
- Commission two simulated devices against your tenant using the credentials the portal issues
16+
- Discover and invoke commissioned devices from a Python client using `device-connect-agent-tools`
17+
- Connect a Strands AI agent to the same tenant
18+
19+
prerequisites:
20+
- Familiarity with the Device Connect edge SDK (the [device-to-device Learning Path](/learning-paths/embedded-and-microcontrollers/device-connect-d2d/) is a good starting point)
21+
- An account on the [Device Connect portal](https://portal.deviceconnect.dev/)
22+
- Basic familiarity with Python and the command line
23+
24+
author:
25+
- Kavya Sri Chennoju
26+
- Annie Tallund
27+
28+
### Tags
29+
skilllevels: Introductory
30+
subjects: Libraries
31+
armips:
32+
- Cortex-A
33+
- Neoverse
34+
operatingsystems:
35+
- Linux
36+
- macOS
37+
tools_software_languages:
38+
- Python
39+
- Docker
40+
41+
further_reading:
42+
- resource:
43+
title: Device Connect
44+
link: https://deviceconnect.dev/
45+
type: website
46+
- resource:
47+
title: device-connect-server package
48+
link: https://github.com/arm/device-connect/tree/main/packages/device-connect-server
49+
type: documentation
50+
51+
### FIXED, DO NOT MODIFY
52+
# ================================================================================
53+
weight: 1
54+
layout: "learningpathall"
55+
learning_path_main_page: "yes"
56+
---
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
# ================================================================================
3+
# FIXED, DO NOT MODIFY THIS FILE
4+
# ================================================================================
5+
weight: 21
6+
title: "Next Steps"
7+
layout: "learningpathall"
8+
---
9+
10+
## Where to go next
11+
12+
From this baseline you can extend the deployment in a few directions:
13+
14+
- swap Zenoh dev mode for [Zenoh with TLS](https://github.com/arm/device-connect/tree/main/packages/device-connect-server#secure--zenoh-tls) using the `generate_tls_certs.sh` script in `security_infra/`
15+
- swap to a NATS backend with [JWT authentication](https://github.com/arm/device-connect/tree/main/packages/device-connect-server#authenticated--nats-jwt-auth) for per-device credentials
16+
- explore the [multi-tenant deployment](https://github.com/arm/device-connect/tree/main/packages/device-connect-server#multi-tenant-deployment) flow when several teams or workshops share the same infrastructure
17+
- replace the simulated number-generator with a real sensor or robot driver using the same `DeviceDriver` pattern from the edge SDK
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: Why add a Device Connect server
3+
weight: 2
4+
5+
# FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## What D2D mode gives you, and where it stops
10+
11+
In device-to-device (D2D) mode, every Device Connect runtime is a peer on the same local network. Devices find each other through the messaging backend's local discovery (Zenoh's multicast scouting, or NATS running in a peer-to-peer configuration), exchange typed events, and call each other's RPCs directly. There is no central process to provision or maintain.
12+
13+
That is a great fit for prototyping, for small fleets on a shared LAN, and for time-sensitive applications where a cloud round-trip would be wasteful. It stops being a great fit when you need any of the following:
14+
15+
- a **persistent registry** that survives device reboots and lets a new client list every device that has ever connected, not just the ones online right this second
16+
- **distributed state** shared between devices and agents, with leases and watches (for example, "which experiment is each device currently running?")
17+
- **multi-network reach** for devices and agents on different LANs, behind NAT, or in the cloud
18+
- **fleet-wide operations** like commissioning new devices, rotating credentials, or auditing every RPC that has ever been issued
19+
- **security guarantees** beyond TLS on the wire, including per-device cryptographic identity, PIN-based onboarding, role-based ACLs, and audit logs
20+
21+
The [`device-connect-server`](https://github.com/arm/device-connect/tree/main/packages/device-connect-server) package is the layer that adds those capabilities on top of the edge SDK you already know.
22+
23+
## What the server adds
24+
25+
The server runtime ships as a small set of services you run alongside (or instead of) raw D2D scouting:
26+
27+
- a **messaging router** (Zenoh by default; NATS or MQTT as alternatives) so devices on different networks can reach the same mesh
28+
- a **device registry service** that records every device, its identity, capabilities, and last-known status, in a persistent store
29+
- an **etcd-backed distributed state store** that any device or agent can read and write through the Python API
30+
- a **device commissioning flow** with PIN-based onboarding and per-device cryptographic identity (covered later in this Learning Path)
31+
- optional **security infrastructure**: TLS/mTLS for Zenoh, JWT auth for NATS, role-based access control, audit logging
32+
33+
You can adopt these incrementally: run the dev-mode Docker Compose to get a router and registry running locally with no auth, then layer in TLS or JWT once you have the basics working.
34+
35+
## A note on commissioning
36+
37+
In a serious deployment, no device joins the mesh until it has been **commissioned**, meaning it has been given a cryptographic identity that the server trusts. The shape of that identity depends on the messaging backend:
38+
39+
- with **Zenoh**, commissioning means issuing the device a client TLS certificate signed by a shared CA
40+
- with **NATS**, commissioning means issuing the device a JWT credential bound to its tenant
41+
42+
In both cases, the credential answers the question "is this device allowed on this tenant's mesh?" and prevents anyone else from impersonating it. In this Learning Path you will use the [Device Connect portal](https://portal.deviceconnect.dev/) to mint NATS credentials for your devices and your agent, then point each runtime at the tenant's NATS server. That replaces the local Docker setup with a hosted server you do not need to operate yourself.
43+
44+
## Where this sits in the architecture
45+
46+
```
47+
┌──────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
48+
│ Devices │ │ Device Connect │ │ AI agents │
49+
│ (edge SDK) │ │ server │ │ (agent-tools) │
50+
│ DeviceDriver │◄───►│ Pub/sub · Registry│◄───►│ discover_devices() │
51+
│ @rpc @emit │ │ KV · Security│ │ invoke_device() │
52+
│ @periodic @on │ │ │ │ Strands / LC / MCP │
53+
└──────────────────┘ └──────────────────────┘ └─────────────────────┘
54+
```
55+
56+
Devices and agents still talk to each other through the same primitives (`@rpc`, `@emit`, `discover_devices`, `invoke_device`). The Device Connect server runs the pub/sub messaging, the persistent registry, the shared key-value store, and the security layer in one place.
57+
58+
The animation below shows the same idea end to end: a Device Connect server runs in Berlin (with Zenoh, NATS, or MQTT as the messaging backend), robots in San Francisco and Tokyo install `device-connect-edge` and register with it, and an AI agent in Bangalore installs `device-connect-agent-tools` and drives both robots through the server. Every `invoke_device` call and every event flows through Berlin.
59+
60+
<video width="100%" controls muted playsinline>
61+
<source src="https://raw.githubusercontent.com/kavya-chennoju/arm-learning-path-assets/main/videos/device-connect-server-overview.mp4" type="video/mp4">
62+
Your browser does not support the video tag.
63+
</video>
64+
65+
## What you'll do in this Learning Path
66+
67+
In the rest of this Learning Path you will:
68+
69+
- start the server stack locally with Docker Compose
70+
- connect a simulated number-generator device to it
71+
- discover the registered device from a short Python client using `device-connect-agent-tools`
72+
- attach a Strands AI agent that subscribes to events on the mesh
73+
74+
The next section walks through the setup.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
---
2+
title: Run devices and an orchestrating agent on a Device Connect server
3+
weight: 3
4+
5+
# FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
In this section you will use the hosted Device Connect service to provision a tenant, commission a simulated device with NATS JWT credentials, and verify it from a Python client. The hosted service runs the messaging router (NATS) and the registry for you, so the only thing you operate is the device itself and the agent that talks to it.
10+
11+
## Mint device and agent credentials from the Device Connect portal
12+
13+
1. Open [`https://portal.deviceconnect.dev/`](https://portal.deviceconnect.dev/) and sign in. Signing in gives you a **tenant** — an isolated namespace on the shared Device Connect server that only the devices and agents you commission against it can join. Nothing from another tenant can see, publish to, or invoke anything in yours.
14+
2. Open the **My Devices** section. The page header reads `Manage device credentials for tenant <slug>` — the value of `<slug>` is your **tenant slug** (for example, `beta`). The portal uses it to namespace every identity you create, so `sf-robot` on the `beta` tenant becomes `beta-sf-robot`. The NATS server URL is the same for every tenant: `nats://portal.deviceconnect.dev:4222`.
15+
3. From the same **My Devices** page, add three identities. Type the short names below and the portal prefixes each one with your tenant slug automatically:
16+
- `sf-robot`: the San Francisco robot arm
17+
- `tokyo-robot`: the Tokyo robot arm
18+
- `bangalore-agent`: the orchestrating agent
19+
20+
For each identity, the portal exposes a **Download** button that gives you a NATS JWT credentials file named `<identity>.creds.json`. Treat each `.creds.json` file like a private key: anyone who has it can act as that identity on your tenant.
21+
22+
Export the tenant settings so the rest of the walkthrough can reuse them.
23+
24+
```bash
25+
export TENANT=<tenant-slug>
26+
export NATS_URL=nats://portal.deviceconnect.dev:4222
27+
export MESSAGING_BACKEND=nats
28+
```
29+
30+
Now save the downloaded credentials to a stable directory:
31+
32+
```bash
33+
mkdir -p ~/.device-connect/credentials
34+
mv ~/Downloads/${TENANT}-sf-robot.creds.json ~/.device-connect/credentials/
35+
mv ~/Downloads/${TENANT}-tokyo-robot.creds.json ~/.device-connect/credentials/
36+
mv ~/Downloads/${TENANT}-bangalore-agent.creds.json ~/.device-connect/credentials/
37+
chmod 600 ~/.device-connect/credentials/*.creds.json
38+
```
39+
40+
The agent-tools `connect()` function reads `TENANT` only when its `zone` argument is left unset, so always call it as `connect(zone=os.environ["TENANT"])` in the Python snippets below.
41+
42+
The portal also exposes a **Coding Agents** tab that can drive this whole step for you — pointing a coding agent (such as Claude Code or Codex) at it will provision identities and download the credentials on your behalf. The manual flow above is what the agent automates, and is useful to walk through once so you understand what the agent is doing.
43+
44+
## Install the SDKs
45+
46+
Create a virtual environment and install the edge runtime and the agent tools:
47+
48+
```bash
49+
mkdir -p ~/device-connect-fabric && cd ~/device-connect-fabric
50+
python3 -m venv .venv
51+
source .venv/bin/activate
52+
pip install device-connect-edge device-connect-agent-tools
53+
```
54+
55+
You do not need `device-connect-server` locally; Fabric runs that for you in this case. If you would rather self-host, you can install it with `pip install device-connect-server` and run the router and registry yourself; see the [device-connect-server README](https://github.com/arm/device-connect/tree/main/packages/device-connect-server) for the Docker Compose deployment options.
56+
57+
## Write a simulated robot-arm driver
58+
59+
Create a file called `robot_arm.py`. The driver pretends to be a 6-DOF robot arm: it exposes RPCs for moving to a target pose and homing, tracks its current position, and emits a `motion_completed` event after every move.
60+
61+
```python
62+
import argparse
63+
import asyncio
64+
import random
65+
66+
from device_connect_edge import DeviceRuntime
67+
from device_connect_edge.drivers import DeviceDriver, emit, rpc
68+
from device_connect_edge.types import DeviceIdentity, DeviceStatus
69+
70+
71+
class RobotArmDriver(DeviceDriver):
72+
device_type = "robot_arm"
73+
74+
def __init__(self):
75+
super().__init__()
76+
self._position = {"x": 0.0, "y": 0.0, "z": 0.0}
77+
78+
@property
79+
def identity(self) -> DeviceIdentity:
80+
return DeviceIdentity(
81+
device_type="robot_arm",
82+
manufacturer="Device Connect",
83+
model="SIM-ARM-6DOF",
84+
description="Simulated 6-DOF robot arm",
85+
)
86+
87+
@property
88+
def status(self) -> DeviceStatus:
89+
return DeviceStatus(availability="available", location="simulator")
90+
91+
@rpc()
92+
async def move_to(self, x: float, y: float, z: float) -> dict:
93+
# pretend to physically traverse to the target
94+
await asyncio.sleep(random.uniform(0.2, 0.6))
95+
self._position = {"x": x, "y": y, "z": z}
96+
await self.motion_completed(target=self._position)
97+
return {"position": self._position}
98+
99+
@rpc()
100+
async def home(self) -> dict:
101+
return await self.move_to(0.0, 0.0, 0.0)
102+
103+
@rpc()
104+
async def get_position(self) -> dict:
105+
return self._position
106+
107+
@emit()
108+
async def motion_completed(self, target: dict):
109+
return None
110+
111+
112+
async def main():
113+
parser = argparse.ArgumentParser()
114+
parser.add_argument("--device-id", required=True)
115+
args = parser.parse_args()
116+
117+
runtime = DeviceRuntime(driver=RobotArmDriver(), device_id=args.device_id)
118+
await runtime.run()
119+
120+
121+
if __name__ == "__main__":
122+
asyncio.run(main())
123+
```
124+
125+
Note that there is no `allow_insecure=True` on the runtime. The runtime will only join the mesh if it has valid credentials, which is what makes commissioning meaningful.
126+
127+
## Connect the device to the server
128+
129+
Point the runtime at the Fabric NATS server and at the device's `.creds.json` file. The combination of `NATS_URL` and `NATS_CREDENTIALS_FILE` is what turns the device from "an unauthenticated process" into "a commissioned member of your tenant":
130+
131+
```bash
132+
NATS_URL=$NATS_URL \
133+
NATS_CREDENTIALS_FILE=~/.device-connect/credentials/${TENANT}-sf-robot.creds.json \
134+
python robot_arm.py --device-id ${TENANT}-sf-robot
135+
```
136+
137+
On startup, the runtime presents its JWT to NATS, NATS verifies it against your tenant's signing key, and the device is allowed to publish, subscribe, and register itself. From this moment it shows up in the portal under your tenant, with its identity, capabilities, and live status.
138+
139+
The `--device-id` you pass on the command line must match the identity inside the credentials file (so `${TENANT}-sf-robot.creds.json` requires `--device-id ${TENANT}-sf-robot`).
140+
141+
In a second terminal, do the same for the Tokyo arm:
142+
143+
```bash
144+
source ~/device-connect-fabric/.venv/bin/activate
145+
NATS_URL=$NATS_URL \
146+
NATS_CREDENTIALS_FILE=~/.device-connect/credentials/${TENANT}-tokyo-robot.creds.json \
147+
python robot_arm.py --device-id ${TENANT}-tokyo-robot
148+
```
149+
150+
You now have two commissioned devices on your tenant.
151+
152+
## Discover and invoke from Python
153+
154+
Open a third terminal and run a short client using the agent's credentials. This is the same `device-connect-agent-tools` API you would use from a Strands or LangChain agent. Agents authenticate to the same tenant in the same way devices do, just with a different `.creds.json` file:
155+
156+
```bash
157+
source ~/device-connect-fabric/.venv/bin/activate
158+
NATS_URL=$NATS_URL \
159+
MESSAGING_BACKEND=nats \
160+
TENANT=$TENANT \
161+
NATS_CREDENTIALS_FILE=~/.device-connect/credentials/${TENANT}-bangalore-agent.creds.json \
162+
python - <<'PY'
163+
import os
164+
from device_connect_agent_tools import connect, discover_devices, invoke_device
165+
166+
tenant = os.environ["TENANT"]
167+
connect(zone=tenant)
168+
169+
devices = discover_devices()
170+
print(f"Found {len(devices)} device(s) on tenant")
171+
for d in devices:
172+
print(f" {d['device_id']:24} {d['device_type']:20} {d.get('status', {}).get('availability', '?')}")
173+
174+
# Drive both arms through the server. The server routes each call to the right device.
175+
for d in devices:
176+
print(f"\nhome on {d['device_id']}:")
177+
print(invoke_device(d['device_id'], 'home'))
178+
179+
print(f"\nmove {tenant}-sf-robot to (0.2, 0.1, 0.3):")
180+
print(invoke_device(f'{tenant}-sf-robot', 'move_to', x=0.2, y=0.1, z=0.3))
181+
182+
print(f"\nposition of {tenant}-tokyo-robot:")
183+
print(invoke_device(f'{tenant}-tokyo-robot', 'get_position'))
184+
PY
185+
```
186+
187+
You should see both `sf-robot` and `tokyo-robot` listed, then each one home, then the SF arm move, then Tokyo's position read. The agent never needs to know which network the arms are on; it only needs the tenant's NATS URL and its own credentials.
188+
189+
## (Optional) Attach a Strands AI agent
190+
191+
`device-connect-agent-tools` ships an adapter that turns the same Device Connect mesh into a Strands tool surface:
192+
193+
```bash
194+
pip install "device-connect-agent-tools[strands]"
195+
```
196+
197+
```python
198+
import asyncio
199+
from device_connect_agent_tools.adapters.strands_agent import StrandsDeviceConnectAgent
200+
201+
async def main():
202+
agent = StrandsDeviceConnectAgent(
203+
goal="Coordinate the two robot arms: home them, then plan and execute moves on user request",
204+
model_id="claude-sonnet-4-20250514",
205+
)
206+
async with agent:
207+
await agent.run()
208+
209+
asyncio.run(main())
210+
```
211+
212+
Run it with the agent's credentials and an Anthropic API key:
213+
214+
```bash
215+
NATS_URL=$NATS_URL \
216+
NATS_CREDENTIALS_FILE=~/.device-connect/credentials/${TENANT}-bangalore-agent.creds.json \
217+
TENANT=$TENANT \
218+
ANTHROPIC_API_KEY="sk-ant-..." \
219+
python my_agent.py
220+
```
221+
222+
The agent subscribes to events on your tenant, batches them over a short window, and prompts Claude to react. Claude can call back into devices using `invoke_device()` and `get_device_status()`.
223+
224+
## Tear down
225+
226+
Stop the running terminals with `Ctrl-C`. Your tenant and credentials remain valid until you revoke them from the portal, so there is nothing on your machine to clean up beyond the virtual environment:
227+
228+
```bash
229+
deactivate
230+
rm -rf ~/device-connect-fabric/.venv
231+
```
232+
233+
To rotate credentials, regenerate them from the portal and replace the `.creds.json` files. Old credentials are revoked the moment new ones are issued.
234+
235+
## What you've built
236+
237+
You now have a working Device Connect deployment with a hosted server in the loop:
238+
239+
- a hosted NATS router and persistent registry on your tenant
240+
- two commissioned simulated robot arms, each authenticated by its own JWT credential
241+
- a Python client using `device-connect-agent-tools` to discover the arms and drive them through the server
242+
- (optionally) a Strands agent coordinating both arms over the same mesh
243+
244+
This is the same shape of deployment you would use to put real robot arms, conveyors, cameras, or actuators on the mesh; only the driver code and the credential names change.

0 commit comments

Comments
 (0)