Skip to content

DemonGiggle/embedclaw

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EmbedClaw

An embedded AI agent runtime for constrained targets. EmbedClaw can run on FreeRTOS, bare-metal firmware, or POSIX host builds: it accepts user input over a serial/network transport, forwards it to a remote OpenAI-compatible LLM, executes tool calls returned by the LLM (such as reading and writing hardware registers), and returns the final answer to the user.

It is the embedded counterpart to OpenClaw — same agentic loop, same OpenAI tool-call protocol, different execution environment.


Features

  • OpenAI-compatible — uses the standard /v1/chat/completions API with tool_calls JSON format; no custom protocol
  • Provider adapter boundary — the agent loop talks to ec_model, so model/provider backends can evolve without rewriting the core loop
  • TLS/HTTPS — mbedTLS integration on POSIX/FreeRTOS, or board-provided TLS through the bare-metal socket HAL
  • Agentic loop — dispatches tool calls from the LLM, feeds results back, and loops until a final text response
  • Capability bundles — compile-time capability groups with explicit policy boundaries; each bundle contributes tools and LLM system context
  • Host simulation profile — explicit POSIX simulation mode for exercising almost all of the runtime on a desktop host
  • Built-in tools — hardware register read/write, register map lookup, web search (Brave API), and web page fetch
  • Extensible tool framework — register new tools with a name, JSON Schema, and a C handler function
  • Persistent conversation — session history survives UART/Telnet reconnects across the device lifetime
  • Transport-agnostic I/O — swap between UART and Telnet (or add new transports) without touching the agent logic
  • No dynamic allocation in hot paths — all buffers are statically sized; predictable memory usage on embedded targets
  • Single-threaded — no RTOS threading required; the entire agent loop runs to completion in one task or foreground loop
  • Bare-metal port — reusable UART, socket, and direct-MMIO HAL boundaries with no FreeRTOS dependency
  • POSIX build — full host build for development and testing (no hardware required)

Architecture

  User
   │
   │  UART / Telnet / (extensible)
   ▼
┌─────────────────────────┐
│  I/O Layer  (ec_io)     │  read_line / write ops
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  Session  (ec_session)  │  conversation history, persistent across reconnects
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  Agent Loop (ec_agent)  │  user → LLM → tool dispatch → loop → response
└──────┬──────────┬───────┘
       │          │
       ▼          ▼
┌────────────┐  ┌─────────────────────────────────────┐
│ Model Adapter │ │  Skill System  (ec_skill)            │
│   (ec_model)  │ │  ┌───────────────────────────────┐   │
│ provider      │ │  │ hw_register_control            │   │
│ selection     │ │  │  hw_register_read/write        │   │
└─────┬─────────┘ │  ├───────────────────────────────┤   │
      │           │  │ web_browsing                   │   │
      │           │  │  web_search, web_fetch         │   │
      │           │  └───────────────────────────────┘   │
      │           └──────────────┬──────────────────────┘
      ▼                          │  (web tools also use HTTP)
┌─────────────────────────┐      │
│  Chat API Backend       │◀─────┘
│    (ec_api)             │
│  JSON + HTTP POST       │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  HTTP Client (ec_http)  │  HTTP/1.1, chunked transfer encoding
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  Socket  (ec_socket)    │  TCP + optional TLS
│                         │  POSIX / FreeRTOS+TCP / bare-metal HAL
└─────────────────────────┘

The source tree follows the same boundary:

src/core/                Agent, model, HTTP, JSON, session, tools, skills
src/platform/posix/      POSIX socket, stdin/stdout UART shim, Telnet, mock MMIO
src/platform/freertos/   FreeRTOS+TCP, UART HAL bridge, Telnet, direct MMIO
src/platform/baremetal/  UART HAL bridge, socket HAL bridge, direct MMIO
src/app/                 Host demo entry points

Code under src/core/ is shared by every target and should not include platform headers. Platform-specific files own OS, board, socket, and MMIO integration.


Building

Requirements

  • CMake ≥ 3.10
  • C99 compiler (GCC or Clang)
  • mbedTLS (included as a git submodule for POSIX/FreeRTOS TLS/HTTPS support)
  • For FreeRTOS builds: FreeRTOS+TCP 10.2.1 and a cross-compiler toolchain
  • For bare-metal builds: board HAL callbacks for UART and socket/network access

Cloning

git clone --recurse-submodules https://github.com/user/embedclaw.git
# Or if already cloned:
git submodule update --init

Host build (POSIX)

mkdir build && cd build
cmake -DEC_PLATFORM=POSIX ..
make

To build without TLS (no mbedTLS dependency):

cmake -DEC_PLATFORM=POSIX -DEC_ENABLE_TLS=OFF ..

This produces embedclaw_demo, libembedclaw.a, embedclaw_tests, and the POSIX Telnet smoke-test target embedclaw_telnet_tests.

Host simulation profile

mkdir build && cd build
cmake -DEC_PLATFORM=POSIX -DEC_HOST_SIM=ON ..
make

If third_party/mbedtls is not initialized, use the same no-TLS fallback as the normal POSIX build:

cmake -DEC_PLATFORM=POSIX -DEC_HOST_SIM=ON -DEC_ENABLE_TLS=OFF ..

This produces the explicit simulation executable embedclaw_sim_demo and the simulation-only validation target embedclaw_host_sim_tests, making the simulation profile visible at build time.

Host simulation keeps the core runtime shared, but swaps hardware register access onto a bookkeeping-backed MMIO layer. The host-sim tool path still enforces the datasheet-backed access policy, so only modeled registers are readable or writable through the agent.

Running tests

From the build directory (POSIX only):

make embedclaw_tests
ctest --verbose

The test suite (tests/test_e2e.c) runs the full agent stack with a mock HTTP layer instead of real networking.

The POSIX validation suite also includes a Telnet smoke test that exercises the real socket-based I/O path:

ctest --verbose -R 'e2e|telnet-io'

When built with -DEC_HOST_SIM=ON, the simulation profile also adds a host-sim test target that validates:

  • bookkeeping-backed MMIO through the real tool path,
  • deterministic mocked-model simulation mode, and
  • the real-provider path running against mocked HTTP.
ctest --verbose -R 'e2e|host-sim|telnet-io'

FreeRTOS build

mkdir build && cd build
cmake -DEC_PLATFORM=FREERTOS -DFREERTOS_PATH=/path/to/freertos ..
make

Bare-metal build

mkdir build && cd build
cmake -DEC_PLATFORM=BAREMETAL -DEC_ENABLE_TLS=OFF ..
make

Bare-metal builds produce libembedclaw.a and no demo executable. Board code owns startup, calls ec_io_uart_set_hal() for the UART byte transport, calls ec_socket_baremetal_set_hal() for network/TLS access, then initializes ec_io_uart_ops and runs the same ec_agent loop used by the other ports.

EC_ENABLE_TLS=ON is allowed for bare-metal builds, but TLS is delegated to the registered socket HAL instead of linking mbedTLS directly.


Running the demo

Set your API credentials and start the agent:

export EC_API_KEY=sk-...
export EC_API_HOST=api.openai.com
export EC_API_PORT=443
export EC_MODEL=gpt-4o
export EC_BRAVE_API_KEY=BSA-...   # optional, for web_search

./build/embedclaw_demo

The demo defaults to stdin/stdout (UART mode). To use the Telnet backend instead, connect on port 2323:

EC_IO=telnet ./build/embedclaw_demo &
telnet localhost 2323

To run the explicit host simulation profile:

./build/embedclaw_sim_demo

The host simulator defaults to the deterministic mock-model mode, so a plain local run works without provider credentials:

./build/embedclaw_sim_demo

Host simulation supports two model modes:

  • EC_SIM_MODEL_MODE=mock (default) uses a deterministic local model shim that still drives the real tool loop
  • EC_SIM_MODEL_MODE=real keeps the normal provider-backed path

Examples:

./build/embedclaw_sim_demo
EC_SIM_MODEL_MODE=real EC_API_KEY=sk-... ./build/embedclaw_sim_demo

Host simulator shell

The simulator runs as a simple line-oriented shell. After startup it prints a ready banner and a > prompt. Type one request per line and press Enter.

In the default EC_SIM_MODEL_MODE=mock, the local mock model is best suited for direct register operations. For example:

> read register 0x40001000
Simulated register 0x40001000 contains 0x00000000.
> write register 0x40001000 to 0x00000001
Simulated register write completed.
> read register 0x40001000
Simulated register 0x40001000 contains 0x00000001.

In EC_SIM_MODEL_MODE=real, the shell works the same way, but responses come from the configured provider-backed model instead of the deterministic local shim.

Session commands

Command Effect
/reset Clear conversation history
/quit Exit the agent loop

Configuration

All limits are compile-time constants in include/ec_config.h:

Constant Default Description
API endpoint
EC_CONFIG_API_HOST api.openai.com LLM API hostname
EC_CONFIG_API_PORT 443 LLM API port
EC_CONFIG_USE_TLS 1 Enable TLS (set to 0 for plain HTTP)
EC_CONFIG_MODEL gpt-4o Model name
HTTP / buffers
EC_CONFIG_REQUEST_BUF 8192 Outgoing JSON request body (bytes)
EC_CONFIG_RESPONSE_BUF 8192 Raw HTTP response body (bytes)
EC_CONFIG_CONTENT_BUF 2048 Extracted LLM text response (bytes)
EC_CONFIG_TOOL_ARG_BUF 256 Per-tool-call arguments JSON (bytes)
EC_CONFIG_TOOL_RESULT_BUF 4096 Per-tool result buffer (bytes)
Session
EC_CONFIG_SESSION_CONTENT_BUF 512 Per-message content in history
EC_CONFIG_MAX_HISTORY 64 Max messages in conversation history
EC_CONFIG_MAX_TOOL_CALLS 4 Max tool calls per LLM response
EC_CONFIG_MAX_AGENT_ITERS 8 Max tool-call iterations per turn
Tool / skill framework
EC_CONFIG_MAX_TOOLS 16 Max registered tools
EC_CONFIG_MAX_SKILLS 16 Max registered capability bundles
EC_CONFIG_SYSTEM_PROMPT_BUF 2048 Combined system prompt buffer (bytes)
I/O layer
EC_CONFIG_IO_LINE_BUF 256 User input line buffer (bytes)
EC_CONFIG_TELNET_PORT 2323 Telnet listen port
EC_CONFIG_UART_RX_TIMEOUT_MS 100 FreeRTOS UART read poll timeout (ms)
EC_CONFIG_UART_TX_TIMEOUT_MS 1000 FreeRTOS UART write timeout (ms)
Web browsing skill
EC_CONFIG_BRAVE_API_HOST api.search.brave.com Brave Search API hostname
EC_CONFIG_BRAVE_API_PORT 443 Brave Search API port
EC_CONFIG_BRAVE_API_KEY BSA-CHANGE-ME Brave Search subscription token
EC_CONFIG_WEB_FETCH_MAX 4096 Max bytes returned by web_fetch
EC_CONFIG_WEB_SEARCH_COUNT 5 Number of search results to return
Debug
EC_CONFIG_DEBUG_LOG 0 Enable debug logging (FreeRTOS; POSIX uses EC_DEBUG env)

Adding a tool

Implement ec_tool_fn_t, fill in an ec_tool_def_t, and call ec_tool_register() at startup:

#include "ec_tool.h"

static int my_tool_fn(const char *args_json, char *out_json, size_t out_size)
{
    /* parse args_json with ec_json_find_string(), do work, write result */
    snprintf(out_json, out_size, "{\"status\":\"ok\"}");
    return 0;
}

static const ec_tool_def_t my_tool = {
    .name        = "my_tool",
    .description = "Does something useful on the device.",
    .parameters_schema =
        "{"
          "\"type\":\"object\","
          "\"properties\":{"
            "\"param\":{\"type\":\"string\",\"description\":\"A parameter\"}"
          "},"
          "\"required\":[\"param\"]"
        "}",
    .fn = my_tool_fn,
};

/* Call once at startup, before the agent loop */
ec_tool_register(&my_tool);

The tool is automatically advertised to the LLM and dispatched when called.


Adding an I/O transport

Implement ec_io_ops_t and call ec_io_init():

#include "ec_io.h"

static int my_read_line(char *buf, size_t size) { /* ... */ }
static int my_write(const char *str)             { /* ... */ }

static const ec_io_ops_t my_io_ops = {
    .read_line = my_read_line,
    .write     = my_write,
};

ec_io_init(&my_io_ops);

For the built-in FreeRTOS and bare-metal UART backends, board code provides the actual UART transport hooks through ec_io_uart_set_hal(), then selects ec_io_uart_ops.

Adding a bare-metal socket backend

Bare-metal firmware registers a socket HAL before the model or web tools make network requests:

#include "ec_socket.h"

static int board_connect(void *ctx, const char *host, uint16_t port, int use_tls);
static int board_send(void *ctx, const void *data, size_t len);
static int board_recv(void *ctx, void *buf, size_t len, uint32_t timeout_ms);
static void board_close(void *ctx);

static const ec_socket_baremetal_hal_t socket_hal = {
    .ctx = NULL,
    .connect = board_connect,
    .send = board_send,
    .recv = board_recv,
    .close = board_close,
};

ec_socket_baremetal_set_hal(&socket_hal);

The HAL may wrap a vendor TCP/IP stack, Wi-Fi module, cellular modem, or TLS offload engine. The core HTTP client continues to call ec_socket_connect(), ec_socket_send(), ec_socket_recv(), and ec_socket_close() regardless of platform.


Hardware datasheet tools

Two tools are registered via the hw_datasheet capability bundle, giving the LLM on-demand access to the device's register map without bloating the system prompt:

hw_module_list — list all hardware modules on the device.

arguments: {}
result:    { "modules": [ { "name": "uart0", "base_addr": "0x40001000",
                             "description": "UART serial port 0." }, ... ] }

hw_register_lookup — look up registers and bit-field definitions for a module.

arguments: { "module": "uart0" }
   — or —  { "module": "uart0", "register": "CTRL" }
result:    { "module": "uart0", "base_addr": "0x40001000",
             "registers": [ { "name": "CTRL", "offset": "0x00",
               "address": "0x40001000", "reset_value": "0x00000000",
               "fields": [ { "name": "EN", "bits": "0", "access": "RW",
                              "description": "UART enable." }, ... ] } ] }

Defining your ASIC's register map

Create a header file using the ec_hw_datasheet.h data structures. See include/ec_hw_example_asic.h for the pattern:

#include "ec_hw_datasheet.h"

static const ec_hw_bitfield_t s_my_ctrl_fields[] = {
    { "EN", 0, 0, "RW", "Module enable" },
    /* ... */
};

static const ec_hw_register_t s_my_regs[] = {
    { .name = "CTRL", .offset = 0x00, .reset_value = 0,
      .description = "Control register",
      .fields = s_my_ctrl_fields,
      .num_fields = sizeof(s_my_ctrl_fields) / sizeof(s_my_ctrl_fields[0]) },
};

static const ec_hw_module_t s_my_modules[] = {
    { .name = "my_periph", .base_addr = 0x40010000,
      .description = "My peripheral",
      .notes = "Programming sequence: ...",
      .registers = s_my_regs,
      .num_registers = sizeof(s_my_regs) / sizeof(s_my_regs[0]) },
};

const ec_hw_module_t *EC_HW_MODULES      = s_my_modules;
const size_t          EC_HW_MODULE_COUNT = sizeof(s_my_modules) / sizeof(s_my_modules[0]);

Then include your header from ec_skill_table.c in place of ec_hw_example_asic.h.


Web browsing tools

Two tools provide web access, registered via the web_browsing capability bundle:

web_search — search the web using the Brave Search API.

arguments: { "query": "FreeRTOS task priorities" }
result:    { "results": [ { "title": "...", "url": "...", "description": "..." }, ... ] }

web_fetch — fetch the content of a URL via HTTP GET.

arguments: { "url": "http://example.com/data.json" }
result:    { "status": 200, "body": "<page content, truncated>" }

The Brave Search API key is configured via EC_BRAVE_API_KEY (environment variable on POSIX, or EC_CONFIG_BRAVE_API_KEY at compile time). Response bodies from web_fetch are truncated to EC_CONFIG_WEB_FETCH_MAX (default 4096 bytes).


Hardware register tools

Two tools are registered via the hw_register_control capability bundle:

hw_register_read — read a 32-bit memory-mapped register.

arguments: { "address": "0x40000000" }
result:    { "address": "0x40000000", "value": "0x00000001" }

hw_register_write — write a 32-bit value to a register.

arguments: { "address": "0x40000000", "value": "0x00000001" }
result:    { "ok": true }

On POSIX builds, a 16-register mock array at base 0x40000000 is used instead of real hardware.

Security note: On embedded builds, hardware register access is restricted to registers declared in the compiled-in datasheet. Unknown addresses and policy-forbidden accesses are rejected. POSIX builds still use the mock register bank.


Debug logging

Set EC_DEBUG=1 to trace the full agent loop — every LLM request/response JSON body, tool dispatches with arguments and results, and iteration counts. All output goes to stderr:

EC_DEBUG=1 ./build/embedclaw_demo 2>debug.log

On FreeRTOS and bare-metal targets, enable at compile time by setting EC_CONFIG_DEBUG_LOG=1 in ec_config.h.

Example output:

[EC_DEBUG] === agent turn start: "read register 0x40000000"
[EC_DEBUG] --- iteration 1/8 ---
[EC_DEBUG] sending 2 messages to LLM
[EC_DEBUG] >>> LLM request: POST api.openai.com:443/v1/chat/completions
[EC_DEBUG] --- request body (1468 bytes) ---
{"model":"gpt-4o","messages":[...], "tools":[...]}
[EC_DEBUG] --- end request body ---
[EC_DEBUG] <<< LLM response: HTTP 200
[EC_DEBUG] --- response body (223 bytes) ---
{"choices":[{"message":{"tool_calls":[...]},"finish_reason":"tool_calls"}]}
[EC_DEBUG] --- end response body ---
[EC_DEBUG] LLM requested 1 tool call(s)
[EC_DEBUG] dispatching tool [0]: hw_register_read (id=call_001)
[EC_DEBUG]   args: {"address":"0x40000000"}
[EC_DEBUG]   result: {"address":"0x40000000","value":"0x00000000"}
[EC_DEBUG] --- iteration 2/8 ---
[EC_DEBUG] ...
[EC_DEBUG] === agent turn complete (iter 2) ===

Embedded bring-up validation

When validating a target build, use this minimum checklist:

  1. Network path — verify the device obtains network connectivity and can open a TCP connection to the configured model/API host.
  2. UART path — verify the board-specific ec_io_uart_set_hal() hooks can read a full line and write a reply without truncation or lockup.
  3. Telnet path — on FreeRTOS, verify a client can connect, send fragmented lines, and receive responses over the Telnet backend.
  4. Bare-metal socket HAL — on bare-metal targets, verify connect/send/recv/close callbacks handle timeout and close semantics expected by ec_socket.
  5. Safety path — verify datasheet-backed register policy rejects unknown or forbidden accesses on target hardware.
  6. Agent path — run one full prompt/tool/result turn against the configured model provider with debug logging enabled.

Roadmap

  • TLS/HTTPS via mbedTLS (POSIX, with embedded CA bundle)
  • Debug logging (EC_DEBUG=1) for LLM request/response inspection
  • FreeRTOS+TCP socket backend
  • FreeRTOS UART backend
  • FreeRTOS Telnet I/O backend
  • Bare-metal UART/socket HAL port
  • POSIX Telnet smoke validation coverage
  • FreeRTOS TLS support (socket layer already TLS-aware)
  • Flash/NVS persistence for conversation history across power cycles
  • Hardware register address allowlist for production safety
  • Minimal mbedTLS config for reduced binary size on embedded targets

See plan.md for the detailed implementation plan and spec.md for the full design specification.


License

MIT

About

Claw Agent on embedded system

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors