Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/c-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: Apache-2.0
name: c-sdk

on:
push:
branches: [main]
paths:
- 'packages/device-connect-edge-c/**'
- 'packages/device-connect-agent-tools-c/**'
- '.github/workflows/c-sdk.yml'
pull_request:
paths:
- 'packages/device-connect-edge-c/**'
- 'packages/device-connect-agent-tools-c/**'
- '.github/workflows/c-sdk.yml'
workflow_dispatch:

jobs:
build-and-test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4

- name: Install deps (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y cmake libssl-dev libcurl4-openssl-dev
# NATS C client (cnats) from source
git clone --depth 1 https://github.com/nats-io/nats.c.git
cmake -S nats.c -B nats.c/build -DNATS_BUILD_STREAMING=OFF -DCMAKE_BUILD_TYPE=Release
sudo cmake --build nats.c/build --target install -j
sudo ldconfig
echo "NATS_PREFIX=/usr/local" >> "$GITHUB_ENV"

- name: Install deps (macOS)
if: runner.os == 'macOS'
run: |
brew install cnats curl
echo "NATS_PREFIX=$(brew --prefix cnats)" >> "$GITHUB_ENV"

- name: Build + test edge-c
working-directory: packages/device-connect-edge-c
run: |
make NATS_CFLAGS="-I${NATS_PREFIX}/include" NATS_LIBS="-L${NATS_PREFIX}/lib -lnats"
make test

- name: Build + test agent-tools-c
working-directory: packages/device-connect-agent-tools-c
run: |
make
make test
8 changes: 8 additions & 0 deletions packages/device-connect-agent-tools-c/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# C build artifacts
*.o
*.a
# example/test binaries (extensionless, built in-tree)
/temp_sensor
/dc_agent
tests/test_edge
tests/test_agent
40 changes: 40 additions & 0 deletions packages/device-connect-agent-tools-c/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
# Device Connect C agent-tools.
#
# Builds libdc_agent_tools.a and the dc_agent CLI. Requires libcurl.
#
# make
# make CURL_CFLAGS=... CURL_LIBS=-lcurl
# make test
# make clean

CC ?= cc
CFLAGS ?= -std=c11 -D_GNU_SOURCE -Wall -Wextra -O2
CURL_CFLAGS ?=
CURL_LIBS ?= -lcurl
INCLUDES = -Iinclude $(CURL_CFLAGS)
LDLIBS = $(CURL_LIBS)

LIB = libdc_agent_tools.a
SRCS = src/json.c src/http.c src/agent_tools.c
OBJS = $(SRCS:.c=.o)

all: $(LIB) dc_agent

$(LIB): $(OBJS)
ar rcs $@ $^

src/%.o: src/%.c
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@

dc_agent: examples/dc_agent.c $(LIB)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ examples/dc_agent.c $(LIB) $(LDLIBS)

test: $(LIB)
$(MAKE) -C tests test

clean:
rm -f $(OBJS) $(LIB) dc_agent
$(MAKE) -C tests clean 2>/dev/null || true

.PHONY: all test clean
49 changes: 49 additions & 0 deletions packages/device-connect-agent-tools-c/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# device-connect-agent-tools-c

The **Device Connect agent-tools in C** — the external-agent side. A C analogue
of `device-connect-agent-tools`: the meta-tools an agent uses to discover and
drive a fleet through the portal HTTP API.

## Tools (`dc/agent_tools.h`)

- `dc_describe_fleet` -> `GET /api/agent/v1/fleet`
- `dc_list_devices(device_type, location)` -> `GET /api/agent/v1/devices`
- `dc_get_device_functions(device_id)` -> `GET /api/agent/v1/devices/{id}/functions`
- `dc_invoke_device(device_id, function, params, reason)` -> `POST /api/agent/v1/devices/{id}/invoke`

Each returns the parsed JSON response (caller `json_free`). HTTP + TLS via
libcurl (`dc/http.h`); JSON via the shared `dc/json.h`. Portal URL and token
come from `DEVICE_CONNECT_PORTAL_URL` / `DEVICE_CONNECT_PORTAL_TOKEN` (or the
`dc_agent` struct).

## Build

Requires libcurl.

```
make # builds libdc_agent_tools.a + the dc_agent CLI
make test
```

## CLI

```
export DEVICE_CONNECT_PORTAL_URL=https://portal.deviceconnect.dev
export DEVICE_CONNECT_PORTAL_TOKEN=dcp_...
./dc_agent fleet
./dc_agent list temp_sensor
./dc_agent functions alpha-temp-001
./dc_agent invoke alpha-temp-001 set_target '{"celsius":42}' "daily setpoint"
```

Verified end-to-end against a live DC portal driving the C edge SDK
(`device-connect-edge-c`): discovered 3 devices and invoked them with the
device JSON-RPC responses (and `-32601`/`-32602` errors) propagating through
the portal envelope.

## Scope / not yet

- The four core meta-tools over the portal HTTP API. Event streaming
(`/events/.../stream`) and the Strands/LangChain/MCP adapters from the Python
package are not ported.
86 changes: 86 additions & 0 deletions packages/device-connect-agent-tools-c/examples/dc_agent.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved.
*
* dc_agent.c -- a small CLI over the C agent-tools, mirroring the dc-portalctl
* discover/invoke surface. Reads DEVICE_CONNECT_PORTAL_URL / _TOKEN from the
* environment.
*
* dc_agent fleet
* dc_agent list [device_type] [location]
* dc_agent functions <device_id>
* dc_agent invoke <device_id> <function> [json_params] [reason]
*
* ASCII-only source.
*/

#include "dc/agent_tools.h"
#include "dc/http.h"
#include "dc/json.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int dump(json *j, long status) {
if (j == NULL) {
fprintf(stderr, "request failed (http=%ld)\n", status);
return 1;
}
char *s = json_dumps(j, 1, NULL);
printf("%s\n", s ? s : "(null)");
free(s);
json_free(j);
return 0;
}

int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr,
"usage: dc_agent fleet | list [type] [loc] | functions <id> | "
"invoke <id> <fn> [json_params] [reason]\n");
return 2;
}
dc_http_global_init();
dc_agent a;
memset(&a, 0, sizeof(a));
if (dc_agent_resolve(&a) != 0) {
fprintf(stderr,
"set DEVICE_CONNECT_PORTAL_URL and DEVICE_CONNECT_PORTAL_TOKEN\n");
dc_http_global_cleanup();
return 2;
}

long status = 0;
int rc = 1;
const char *cmd = argv[1];
if (!strcmp(cmd, "fleet")) {
rc = dump(dc_describe_fleet(&a, &status), status);
} else if (!strcmp(cmd, "list")) {
const char *type = argc > 2 ? argv[2] : NULL;
const char *loc = argc > 3 ? argv[3] : NULL;
rc = dump(dc_list_devices(&a, type, loc, &status), status);
} else if (!strcmp(cmd, "functions") && argc > 2) {
rc = dump(dc_get_device_functions(&a, argv[2], &status), status);
} else if (!strcmp(cmd, "invoke") && argc > 3) {
const char *id = argv[2];
const char *fn = argv[3];
const char *pj = argc > 4 ? argv[4] : "{}";
const char *reason = argc > 5 ? argv[5] : "dc_agent CLI invoke";
const char *err = NULL;
json *params = json_parse(pj, strlen(pj), &err);
if (params == NULL) {
fprintf(stderr, "bad json params: %s\n", err ? err : "?");
dc_http_global_cleanup();
return 2;
}
rc = dump(dc_invoke_device(&a, id, fn, params, reason, &status),
status);
json_free(params);
} else {
fprintf(stderr, "unknown command or missing args: %s\n", cmd);
rc = 2;
}
dc_http_global_cleanup();
return rc;
}
55 changes: 55 additions & 0 deletions packages/device-connect-agent-tools-c/include/dc/agent_tools.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved.
*
* dc/agent_tools.h -- the Device Connect agent-tools meta-tools in C.
*
* The external-agent path (the C analogue of device-connect-agent-tools):
* describe_fleet / list_devices / get_device_functions / invoke_device over
* the portal HTTP API. Results are returned as parsed JSON (caller json_free).
*
* ASCII-only source.
*/

#ifndef DC_AGENT_TOOLS_H
#define DC_AGENT_TOOLS_H

#include "dc/json.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
const char *portal_url; /* e.g. https://portal.deviceconnect.dev; NULL -> $DEVICE_CONNECT_PORTAL_URL */
const char *token; /* dcp_... ; NULL -> $DEVICE_CONNECT_PORTAL_TOKEN */
} dc_agent;

/* Resolve portal_url/token from the struct or the environment. Returns 0 if
* both are available, -1 otherwise. */
int dc_agent_resolve(dc_agent *a);

/*
* Each call performs one portal request and returns the parsed JSON response
* (caller json_free), or NULL on transport/parse failure. *http_status, when
* non-NULL, receives the HTTP status code.
*/
json *dc_describe_fleet(const dc_agent *a, long *http_status);
json *dc_list_devices(const dc_agent *a, const char *device_type,
const char *location, long *http_status);
json *dc_get_device_functions(const dc_agent *a, const char *device_id,
long *http_status);
/*
* Invoke a device function. `params` is a JSON object (borrowed; may be NULL).
* `reason` is the mandatory audit string. Returns the portal's response
* envelope (which embeds the device's JSON-RPC `response`).
*/
json *dc_invoke_device(const dc_agent *a, const char *device_id,
const char *function, const json *params,
const char *reason, long *http_status);

#ifdef __cplusplus
}
#endif

#endif /* DC_AGENT_TOOLS_H */
49 changes: 49 additions & 0 deletions packages/device-connect-agent-tools-c/include/dc/http.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) 2024-2026, Arm Limited and Contributors. All rights reserved.
*
* dc/http.h -- a tiny libcurl wrapper for the Device Connect portal HTTP API.
*
* Bearer-authenticated GET/POST returning the response body. Used by the
* agent-tools layer (dc/agent_tools.h). TLS is handled by libcurl.
*
* ASCII-only source.
*/

#ifndef DC_HTTP_H
#define DC_HTTP_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
long status; /* HTTP status code, or 0 on transport failure */
char *body; /* malloc'd response body (caller frees), or NULL */
size_t len;
} dc_http_response;

/* Call once at startup (wraps curl_global_init). */
int dc_http_global_init(void);
void dc_http_global_cleanup(void);

/*
* GET url with "Authorization: Bearer <token>". Returns 0 on a completed
* request (check resp.status), -1 on a transport-level failure. Caller frees
* resp.body.
*/
int dc_http_get(const char *url, const char *token, dc_http_response *resp);

/* POST url with a JSON body (Content-Type: application/json) + Bearer token. */
int dc_http_post(const char *url, const char *token, const char *json_body,
dc_http_response *resp);

void dc_http_response_free(dc_http_response *resp);

#ifdef __cplusplus
}
#endif

#endif /* DC_HTTP_H */
Loading
Loading