EmbedClaw is an embedded AI agent runtime for FreeRTOS. It runs on constrained hardware and acts as an intelligent automation layer: it accepts user input over a serial interface (UART, Telnet, or similar), forwards it to a remote OpenAI-compatible LLM, and executes tool calls returned by the LLM — such as reading and writing hardware registers or searching the web — before returning the final answer to the user.
It is the embedded counterpart to OpenClaw: same agentic loop, same tool-call protocol, different execution environment.
- Single-threaded: No RTOS threads or callbacks. The agent loop runs to completion in a single task. All I/O is blocking with timeouts.
- No dynamic allocation in hot paths: All buffers are caller-provided or
statically declared. No
mallocin the agent or tool layers. - Minimal external dependencies: JSON, HTTP, and the agent loop are all built in. mbedTLS is the only third-party dependency (for TLS/HTTPS).
- OpenAI-compatible protocol: Tool calls use the standard OpenAI
tool_callsJSON format. No custom protocol invented. - Extensible via skills: New capabilities are added as skills — compile-time bundles that contribute tools and LLM system context.
- Extensible I/O: New input sources can be added without touching the agent core.
- No streaming: Responses are buffered in full before processing. Streaming (SSE) is deferred — tool calls cannot be dispatched until the full response is received anyway, so streaming adds complexity with no benefit to the agentic loop.
- Embedded CA bundle: TLS certificate validation uses a compiled-in CA bundle — no filesystem required (suitable for FreeRTOS/baremetal).
User
│
│ UART / Telnet / (future: USB CDC, BLE UART, ...)
▼
┌─────────────────────────────────────────────────────┐
│ I/O Abstraction Layer (ec_io) │
│ ec_io_read_line(), ec_io_write() │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Session Layer (ec_session) │
│ Conversation history (system + prior turns) │
│ Persistent across connect/disconnect │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Agent Loop (ec_agent) │
│ 1. Append user message to history │
│ 2. Send full history to LLM │
│ 3. Receive response │
│ 4. If tool_calls → dispatch to skill/tool layer │
│ append tool results → go to 2 │
│ 5. If text response → send to user via I/O layer │
│ │
│ Debug logging via ec_log (EC_DEBUG=1 / compile) │
└──────────┬──────────────────────┬───────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌─────────────────────────────┐
│ Chat API Layer │ │ Skill System (ec_skill) │
│ (ec_api) │ │ ┌────────────────────────┐ │
│ JSON build/parse │ │ │ hw_register_control │ │
│ /v1/chat/complete │ │ │ hw_register_read/ │ │
│ │ │ │ hw_register_write │ │
│ │ │ ├────────────────────────┤ │
│ │ │ │ web_browsing │ │
│ │ │ │ web_search (Brave) │ │
│ │ │ │ web_fetch (HTTP GET) │ │
│ │ │ └────────────────────────┘ │
└────────┬───────────┘ └───────────┬─────────────────┘
│ │ (web tools also use HTTP)
├──────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ HTTP Client (ec_http) │
│ HTTP/1.1, chunked transfer encoding │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Socket Abstraction (ec_socket) │
│ TCP + optional TLS (mbedTLS, embedded CA bundle) │
│ FreeRTOS+TCP backend / POSIX shim (host testing) │
└─────────────────────────────────────────────────────┘
Wraps the platform TCP API and optional TLS into four functions:
ec_socket_t *ec_socket_connect(const char *host, uint16_t port, int use_tls);
int ec_socket_send(ec_socket_t *s, const void *data, size_t len);
int ec_socket_recv(ec_socket_t *s, void *buf, size_t len, uint32_t timeout_ms);
void ec_socket_close(ec_socket_t *s);Two backends selected at compile time via EC_PLATFORM:
POSIX— standard BSD sockets, used for host-side development and testing.FREERTOS— FreeRTOS+TCP sockets (currently stubbed; implementation pending).
TLS support (when EC_CONFIG_USE_TLS=1):
- mbedTLS v3.6.5 integrated as a git submodule (
third_party/mbedtls). - TLS is transparent to callers —
ec_socket_connect()performs the handshake whenuse_tls=1, andsend/recvroute throughmbedtls_ssl_write/read. - Certificate validation uses an embedded CA bundle (
ec_cacerts.h) — no filesystem paths. Suitable for FreeRTOS/baremetal targets. - SNI (Server Name Indication) is set for virtual hosting support.
Minimal HTTP/1.1 client. Supports:
POSTandGETwith arbitrary headers and body- Response status line and header parsing
- Chunked transfer-encoding (in-place decode)
- Caller-provided request and response buffers (no malloc)
use_tlsfield in request struct — passed through toec_socket_connect()
Non-streaming only. The full response body is buffered before returning.
Purpose-built for the OpenAI message format. No malloc, no DOM.
- Builder (
ec_json_writer_t): Writes JSON tokens into a fixed-size buffer. Helpers:ec_json_obj_start/end,ec_json_array_start/end,ec_json_add_string,ec_json_add_int,ec_json_add_raw, etc. - Parser: Pull-parser that finds values by key path (e.g.,
"choices[0].message.content","choices[0].message.tool_calls[0].id"). Scans the raw JSON string without constructing any in-memory tree.
Builds the OpenAI /v1/chat/completions request JSON, sends it via the HTTP
layer, and parses the response. Handles both plain text responses and
tool_calls responses.
typedef enum {
EC_API_RESP_MESSAGE, /* choices[0].message.content is set */
EC_API_RESP_TOOL_CALLS, /* choices[0].message.tool_calls[] is set */
} ec_api_resp_type_t;
typedef struct {
char id[64];
char name[64];
char arguments[EC_CONFIG_TOOL_ARG_BUF]; /* raw JSON string */
} ec_api_tool_call_t;
typedef struct {
ec_api_resp_type_t type;
char content[EC_CONFIG_CONTENT_BUF]; /* if MESSAGE */
ec_api_tool_call_t tool_calls[EC_CONFIG_MAX_TOOL_CALLS]; /* if TOOL_CALLS */
int num_tool_calls;
} ec_api_response_t;
int ec_api_chat_completion(
const ec_api_config_t *config,
const char *model,
const ec_api_message_t *messages,
size_t num_messages,
const ec_api_tool_def_t *tools,
size_t num_tools,
ec_api_response_t *out
);Debug logging (ec_log.h) traces the full request and response JSON bodies
when enabled.
Provides a registry of named tools and a dispatcher. Each tool is a C function that receives a JSON arguments string and writes a JSON result string.
typedef int (*ec_tool_fn_t)(
const char *args_json,
char *out_json,
size_t out_size
);
typedef struct {
const char *name;
const char *description;
const char *parameters_schema; /* JSON Schema string, sent to LLM */
ec_tool_fn_t fn;
} ec_tool_def_t;
int ec_tool_register(const ec_tool_def_t *def);
int ec_tool_dispatch(const ec_api_tool_call_t *call, char *out_json, size_t out_size);
const ec_api_tool_def_t *ec_tool_api_defs(size_t *count);Skills are compile-time capability bundles. Each skill contributes:
- One or more tools (registered via
ec_tool_register) - A system prompt fragment (appended to the LLM system message)
typedef struct {
const char *name;
void (*init)(void); /* register tools */
const char *system_context; /* appended to system prompt */
} ec_skill_def_t;Built-in skills:
-
hw_register_control—hw_register_readandhw_register_writetools. On POSIX: 16-register mock array at base0x40000000. On FreeRTOS: direct memory-mapped register access. -
hw_datasheet—hw_module_listandhw_register_lookuptools. Provides on-demand access to the device's register map so the LLM can discover modules, register addresses, bit-field definitions, access types, and programming notes without a large system prompt. The register map is defined as compile-time const tables viaec_hw_datasheet.hdata structures. Each ASIC has its own header (e.g.,ec_hw_example_asic.h). -
web_browsing—web_search(Brave Search API) andweb_fetch(HTTP GET). The Brave API key is configured viaEC_BRAVE_API_KEYenv (POSIX) orEC_CONFIG_BRAVE_API_KEY(compile-time).
Maintains conversation history across user turns. The history is a fixed-size
ring of ec_api_message_t entries (role + content). When full, oldest entries
are evicted.
Roles in the history: system, user, assistant, tool.
The session is persistent for the lifetime of the device (not cleared on
UART/Telnet reconnect). A user command (e.g., /reset) can clear it explicitly.
void ec_session_init(ec_session_t *s, const char *system_prompt);
int ec_session_append(ec_session_t *s, const char *role, const char *content);
int ec_session_append_tool_calls(ec_session_t *s,
const ec_api_tool_call_t *calls, int count);
int ec_session_append_tool_result(ec_session_t *s,
const char *tool_call_id,
const char *content);
void ec_session_reset(ec_session_t *s);
const ec_api_message_t *ec_session_messages(const ec_session_t *s, size_t *count);The core agentic loop. Single-threaded, blocking.
ec_agent_run_turn(agent, user_input):
1. session_append(user, user_input)
2. loop (max EC_CONFIG_MAX_AGENT_ITERS):
resp = ec_api_chat_completion(session.messages, tools)
if resp.type == MESSAGE:
session_append(assistant, resp.content)
return resp.content
if resp.type == TOOL_CALLS:
session_append_tool_calls(resp.tool_calls)
for each call in resp.tool_calls:
result = ec_tool_dispatch(call)
session_append_tool_result(call.id, result)
// loop back: send updated history to LLM
Debug logging traces each iteration, tool dispatch, and LLM response type.
Decouples the agent from the physical input/output channel. New transports (UART, Telnet, USB CDC, BLE UART) implement the same interface:
typedef struct {
int (*read_line)(char *buf, size_t size);
int (*write)(const char *str);
} ec_io_ops_t;
void ec_io_init(const ec_io_ops_t *ops);
int ec_io_read_line(char *buf, size_t size);
int ec_io_write(const char *str);Implementations:
- UART (
ec_io_uart.c): wraps FreeRTOS UART HAL (or POSIX stdin/stdout). - Telnet (
ec_io_telnet.c): TCP server on a fixed port (single connection).
Compile-time/runtime debug tracing. All output goes to stderr.
- On POSIX: enabled at runtime via
EC_DEBUG=1environment variable. - On FreeRTOS: enabled at compile time via
EC_CONFIG_DEBUG_LOG=1.
Traces:
- Full JSON request/response bodies for LLM API calls
- Tool call dispatches: name, arguments, and result
- Agent loop iterations and turn boundaries
- API errors
EC_LOG_DEBUG(fmt, ...) /* single-line debug message */
EC_LOG_BODY(label, buf, len) /* large buffer dump with label */CMake-based. Two build profiles:
cmake -DEC_PLATFORM=POSIX .. # host build for development/testing
cmake -DEC_PLATFORM=FREERTOS \
-DFREERTOS_PATH=... .. # cross-compile for embedded target
Optional flags:
-DEC_ENABLE_TLS=OFF— disable TLS (removes mbedTLS dependency)
Output: static library libembedclaw.a, demo executable embedclaw_demo,
and test executable embedclaw_tests (POSIX only).
mbedTLS is included as a git submodule at third_party/mbedtls. Tests compile
with EC_CONFIG_USE_TLS=0 and use a mock HTTP layer, so they never touch the
socket/TLS stack.
| Constant | Default | Purpose |
|---|---|---|
EC_CONFIG_REQUEST_BUF |
8192 | HTTP request body (outgoing JSON) |
EC_CONFIG_RESPONSE_BUF |
8192 | HTTP response body (incoming JSON) |
EC_CONFIG_CONTENT_BUF |
2048 | Extracted LLM text content |
EC_CONFIG_TOOL_ARG_BUF |
256 | Tool call arguments JSON |
EC_CONFIG_TOOL_RESULT_BUF |
4096 | Per-tool result buffer |
EC_CONFIG_IO_LINE_BUF |
256 | One line of user input |
EC_CONFIG_SESSION_CONTENT_BUF |
512 | Per-message content in history |
EC_CONFIG_SYSTEM_PROMPT_BUF |
2048 | Combined system prompt |
EC_CONFIG_MAX_TOOL_CALLS |
4 | Max tool_calls in one LLM response |
EC_CONFIG_MAX_HISTORY |
64 | Max messages in conversation history |
EC_CONFIG_MAX_TOOLS |
16 | Max registered tools |
EC_CONFIG_MAX_SKILLS |
16 | Max registered skills |
EC_CONFIG_MAX_AGENT_ITERS |
8 | Max agentic loop iterations per turn |
- Streaming / SSE: Deferred. Tool calls cannot be dispatched until the full response is received, so streaming offers no benefit to the agentic loop. It can be added later as a text-output optimization.
- Multi-session / multi-user: Single conversation, single I/O channel.
- Tool output streaming back to LLM: Tool results are always complete JSON strings, never partial.
- History persistence across power cycles: Currently in-RAM only. Flash/NVS
persistence would require a serialize/deserialize step in
ec_session.