diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e97cf8d..8ddb70b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,17 @@ jobs: matrix: include: - name: posix + platform: POSIX host_sim: OFF ctest_regex: e2e|telnet-io - name: host-sim + platform: POSIX host_sim: ON ctest_regex: e2e|host-sim|telnet-io + - name: baremetal + platform: BAREMETAL + host_sim: OFF + ctest_regex: '' name: ${{ matrix.name }} @@ -29,7 +35,7 @@ jobs: - name: Configure run: > cmake -S . -B build - -DEC_PLATFORM=POSIX + -DEC_PLATFORM=${{ matrix.platform }} -DEC_ENABLE_TLS=OFF -DEC_HOST_SIM=${{ matrix.host_sim }} @@ -37,4 +43,5 @@ jobs: run: cmake --build build - name: Test + if: matrix.ctest_regex != '' run: ctest --test-dir build --output-on-failure -R '${{ matrix.ctest_regex }}' diff --git a/CMakeLists.txt b/CMakeLists.txt index 371c23e..6349a1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(embedclaw C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) -# Platform selection: POSIX (default) or FREERTOS +# Platform selection: POSIX (default), FREERTOS, or BAREMETAL if(NOT DEFINED EC_PLATFORM) set(EC_PLATFORM "POSIX") endif() @@ -19,7 +19,7 @@ if(EC_PLATFORM STREQUAL "POSIX") else() message(STATUS "Building for POSIX (host development)") endif() -else() +elseif(EC_PLATFORM STREQUAL "FREERTOS") if(EC_HOST_SIM) message(FATAL_ERROR "EC_HOST_SIM is only supported with EC_PLATFORM=POSIX") endif() @@ -27,6 +27,13 @@ else() if(NOT DEFINED FREERTOS_PATH) message(FATAL_ERROR "FREERTOS_PATH must be set for FreeRTOS builds") endif() +elseif(EC_PLATFORM STREQUAL "BAREMETAL") + if(EC_HOST_SIM) + message(FATAL_ERROR "EC_HOST_SIM is only supported with EC_PLATFORM=POSIX") + endif() + message(STATUS "Building for bare-metal embedded target") +else() + message(FATAL_ERROR "Unsupported EC_PLATFORM='${EC_PLATFORM}'") endif() # Warnings @@ -37,7 +44,7 @@ add_compile_options(-Wall -Wextra -Wpedantic) # ------------------------------------------------------------------------- option(EC_ENABLE_TLS "Enable TLS/HTTPS support via mbedTLS" ON) -if(EC_ENABLE_TLS) +if(EC_ENABLE_TLS AND NOT EC_PLATFORM STREQUAL "BAREMETAL") if(NOT EXISTS "${CMAKE_SOURCE_DIR}/third_party/mbedtls/CMakeLists.txt") message(FATAL_ERROR "mbedTLS submodule not found at third_party/mbedtls.\n" @@ -52,35 +59,59 @@ if(EC_ENABLE_TLS) add_subdirectory(third_party/mbedtls EXCLUDE_FROM_ALL) endif() -# Library sources -set(EC_SOURCES - src/ec_hw_access.c - src/ec_hw_registry.c - src/ec_mmio.c - src/ec_log.c - src/ec_model.c - src/ec_socket.c - src/ec_http.c - src/ec_json.c - src/ec_api.c - src/ec_tool.c - src/ec_session.c - src/ec_agent.c - src/ec_io.c - src/ec_io_uart.c - src/ec_io_telnet.c - src/ec_skill.c - src/ec_skill_table.c +# Shared runtime sources. These files are platform-neutral and must not include +# POSIX, FreeRTOS, or board-vendor headers. +set(EC_CORE_SOURCES + src/core/ec_hw_access.c + src/core/ec_hw_registry.c + src/core/ec_log.c + src/core/ec_model.c + src/core/ec_http.c + src/core/ec_json.c + src/core/ec_api.c + src/core/ec_tool.c + src/core/ec_session.c + src/core/ec_agent.c + src/core/ec_io.c + src/core/ec_skill.c + src/core/ec_skill_table.c ) +if(EC_PLATFORM STREQUAL "POSIX") + set(EC_PLATFORM_SOURCES + src/platform/posix/ec_mmio_posix.c + src/platform/posix/ec_socket_posix.c + src/platform/posix/ec_io_posix_uart.c + src/platform/posix/ec_io_posix_telnet.c + ) +elseif(EC_PLATFORM STREQUAL "FREERTOS") + set(EC_PLATFORM_SOURCES + src/platform/freertos/ec_mmio_direct.c + src/platform/freertos/ec_socket_freertos.c + src/platform/freertos/ec_io_freertos_uart.c + src/platform/freertos/ec_io_freertos_telnet.c + src/platform/freertos/ec_freertos_entry.c + ) +elseif(EC_PLATFORM STREQUAL "BAREMETAL") + set(EC_PLATFORM_SOURCES + src/platform/baremetal/ec_mmio_direct.c + src/platform/baremetal/ec_socket_baremetal.c + src/platform/baremetal/ec_io_baremetal_uart.c + ) +endif() + +set(EC_SOURCES ${EC_CORE_SOURCES} ${EC_PLATFORM_SOURCES}) + # Static library add_library(embedclaw STATIC ${EC_SOURCES}) target_include_directories(embedclaw PUBLIC include) if(EC_ENABLE_TLS) target_compile_definitions(embedclaw PUBLIC EC_CONFIG_USE_TLS=1) - target_link_libraries(embedclaw PUBLIC - MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto) + if(NOT EC_PLATFORM STREQUAL "BAREMETAL") + target_link_libraries(embedclaw PUBLIC + MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto) + endif() else() target_compile_definitions(embedclaw PUBLIC EC_CONFIG_USE_TLS=0) endif() @@ -93,10 +124,10 @@ endif() # Demo executable if(EC_PLATFORM STREQUAL "POSIX" AND EC_HOST_SIM) - add_executable(embedclaw_sim_demo src/main.c) + add_executable(embedclaw_sim_demo src/app/ec_posix_main.c) target_link_libraries(embedclaw_sim_demo PRIVATE embedclaw) -else() - add_executable(embedclaw_demo src/main.c) +elseif(EC_PLATFORM STREQUAL "POSIX") + add_executable(embedclaw_demo src/app/ec_posix_main.c) target_link_libraries(embedclaw_demo PRIVATE embedclaw) endif() @@ -109,21 +140,21 @@ endif() # ------------------------------------------------------------------------- if(EC_PLATFORM STREQUAL "POSIX") set(EC_SOURCES_NO_HTTP - src/ec_hw_access.c - src/ec_hw_registry.c - src/ec_mmio.c - src/ec_log.c - src/ec_model.c - src/ec_json.c - src/ec_api.c - src/ec_tool.c - src/ec_session.c - src/ec_agent.c - src/ec_io.c - src/ec_io_uart.c - src/ec_io_telnet.c - src/ec_skill.c - src/ec_skill_table.c + src/core/ec_hw_access.c + src/core/ec_hw_registry.c + src/platform/posix/ec_mmio_posix.c + src/core/ec_log.c + src/core/ec_model.c + src/core/ec_json.c + src/core/ec_api.c + src/core/ec_tool.c + src/core/ec_session.c + src/core/ec_agent.c + src/core/ec_io.c + src/platform/posix/ec_io_posix_uart.c + src/platform/posix/ec_io_posix_telnet.c + src/core/ec_skill.c + src/core/ec_skill_table.c ) add_executable(embedclaw_tests @@ -137,8 +168,8 @@ if(EC_PLATFORM STREQUAL "POSIX") target_compile_definitions(embedclaw_tests PRIVATE EC_CONFIG_USE_TLS=0) add_executable(embedclaw_telnet_tests - src/ec_io.c - src/ec_io_telnet.c + src/core/ec_io.c + src/platform/posix/ec_io_posix_telnet.c tests/test_telnet_io.c ) target_include_directories(embedclaw_telnet_tests PRIVATE include tests) diff --git a/README.md b/README.md index 92084dc..f8f4e87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # EmbedClaw -An embedded AI agent runtime for FreeRTOS. EmbedClaw 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, executes tool calls returned by the LLM (such as reading and writing hardware registers), and returns the final answer to the user. +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. @@ -10,7 +14,7 @@ It is the embedded counterpart to OpenClaw — same agentic loop, same OpenAI to - **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 with embedded CA bundle; no filesystem required +- **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 @@ -19,7 +23,8 @@ It is the embedded counterpart to OpenClaw — same agentic loop, same OpenAI to - **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 +- **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) --- @@ -70,11 +75,25 @@ It is the embedded counterpart to OpenClaw — same agentic loop, same OpenAI to │ ▼ ┌─────────────────────────┐ -│ Socket (ec_socket) │ TCP + optional TLS (mbedTLS) -│ │ FreeRTOS+TCP / POSIX +│ Socket (ec_socket) │ TCP + optional TLS +│ │ POSIX / FreeRTOS+TCP / bare-metal HAL └─────────────────────────┘ ``` +The source tree follows the same boundary: + +```text +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 @@ -83,8 +102,9 @@ It is the embedded counterpart to OpenClaw — same agentic loop, same OpenAI to - CMake ≥ 3.10 - C99 compiler (GCC or Clang) -- mbedTLS (included as a git submodule for TLS/HTTPS support) +- 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 @@ -173,6 +193,22 @@ cmake -DEC_PLATFORM=FREERTOS -DFREERTOS_PATH=/path/to/freertos .. make ``` +### Bare-metal build + +```sh +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 @@ -348,8 +384,38 @@ static const ec_io_ops_t my_io_ops = { ec_io_init(&my_io_ops); ``` -For the built-in FreeRTOS UART backend, board code provides the actual UART -transport hooks through `ec_io_uart_set_hal()`, then selects `ec_io_uart_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: + +```c +#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. --- @@ -460,7 +526,8 @@ Set `EC_DEBUG=1` to trace the full agent loop — every LLM request/response JSO EC_DEBUG=1 ./build/embedclaw_demo 2>debug.log ``` -On FreeRTOS, enable at compile time by setting `EC_CONFIG_DEBUG_LOG=1` in `ec_config.h`. +On FreeRTOS and bare-metal targets, enable at compile time by setting +`EC_CONFIG_DEBUG_LOG=1` in `ec_config.h`. Example output: @@ -487,15 +554,16 @@ Example output: --- -## FreeRTOS bring-up validation +## 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** — verify a client can connect, send fragmented lines, and receive responses over the FreeRTOS Telnet backend. -4. **Safety path** — verify datasheet-backed register policy rejects unknown or forbidden accesses on target hardware. -5. **Agent path** — run one full prompt/tool/result turn against the configured model provider with debug logging enabled. +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. --- @@ -503,9 +571,10 @@ When validating a target build, use this minimum checklist: - [x] TLS/HTTPS via mbedTLS (POSIX, with embedded CA bundle) - [x] Debug logging (`EC_DEBUG=1`) for LLM request/response inspection -- [x] FreeRTOS+TCP socket backend (`ec_socket.c`) +- [x] FreeRTOS+TCP socket backend - [x] FreeRTOS UART backend - [x] FreeRTOS Telnet I/O backend +- [x] Bare-metal UART/socket HAL port - [x] POSIX Telnet smoke validation coverage - [ ] FreeRTOS TLS support (socket layer already TLS-aware) - [ ] Flash/NVS persistence for conversation history across power cycles diff --git a/include/ec_config.h b/include/ec_config.h index 7c78b6a..7fb3f39 100644 --- a/include/ec_config.h +++ b/include/ec_config.h @@ -4,9 +4,9 @@ /* * EmbedClaw compile-time configuration. * - * On FreeRTOS these are the actual values used at runtime. - * On POSIX they serve as defaults that can be overridden via environment - * variables (EC_API_KEY, EC_API_HOST, EC_API_PORT, EC_MODEL). + * On FreeRTOS and bare-metal targets these are the actual values used at + * runtime. On POSIX they serve as defaults that can be overridden via + * environment variables (EC_API_KEY, EC_API_HOST, EC_API_PORT, EC_MODEL). * * Edit these before building for your target. */ @@ -66,7 +66,7 @@ #define EC_CONFIG_WEB_FETCH_MAX 4096 /* max bytes returned by web_fetch */ #define EC_CONFIG_WEB_SEARCH_COUNT 5 /* results per search */ -/* Debug logging (FreeRTOS: set to 1 to enable; POSIX: use EC_DEBUG=1 env) */ +/* Debug logging (embedded: set to 1 to enable; POSIX: use EC_DEBUG=1 env) */ #ifndef EC_CONFIG_DEBUG_LOG #define EC_CONFIG_DEBUG_LOG 0 #endif diff --git a/include/ec_io.h b/include/ec_io.h index e6c367f..2184f01 100644 --- a/include/ec_io.h +++ b/include/ec_io.h @@ -44,14 +44,14 @@ int ec_io_write(const char *str); * Available backends — include ec_io_uart.h / ec_io_telnet.h for ops structs. * ------------------------------------------------------------------------- */ -/** POSIX: stdin/stdout. FreeRTOS: UART HAL (stub). */ +/** POSIX: stdin/stdout. FreeRTOS/bare-metal: UART HAL bridge. */ extern const ec_io_ops_t ec_io_uart_ops; /* - * FreeRTOS UART HAL bridge. + * Embedded UART HAL bridge. * * Provide blocking or timeout-based byte transport hooks from your board/HAL, - * then call ec_io_uart_set_hal() before using ec_io_uart_ops on FreeRTOS. + * then call ec_io_uart_set_hal() before using ec_io_uart_ops on embedded ports. * * Return conventions for read/write hooks: * > 0 number of bytes transferred diff --git a/include/ec_socket.h b/include/ec_socket.h index 1cf092b..f506783 100644 --- a/include/ec_socket.h +++ b/include/ec_socket.h @@ -11,6 +11,23 @@ extern "C" { /* Opaque socket handle */ typedef struct ec_socket ec_socket_t; +/* + * Bare-metal socket HAL. + * + * Board support code owns the network/modem/TLS stack and registers callbacks + * with ec_socket_baremetal_set_hal(). The EmbedClaw HTTP layer then uses the + * same ec_socket_* API as POSIX and FreeRTOS builds. + */ +typedef struct { + void *ctx; + int (*connect)(void *ctx, const char *host, uint16_t port, int use_tls); + int (*send)(void *ctx, const void *data, size_t len); + int (*recv)(void *ctx, void *buf, size_t len, uint32_t timeout_ms); + void (*close)(void *ctx); +} ec_socket_baremetal_hal_t; + +void ec_socket_baremetal_set_hal(const ec_socket_baremetal_hal_t *hal); + /** * Connect to a remote host over TCP, optionally with TLS. * diff --git a/plan.md b/plan.md index c721836..ffb3dfb 100644 --- a/plan.md +++ b/plan.md @@ -10,8 +10,8 @@ The FreeRTOS backend remains stubbed, pending target hardware bring-up. | Component | File(s) | Status | |---------------------|--------------------------------|-------------------------------------| -| Socket layer | `ec_socket.c/h` | POSIX done (TCP + TLS); FreeRTOS stubbed | -| TLS (mbedTLS) | `ec_socket.c`, `ec_cacerts.h` | Done — embedded CA bundle, no filesystem | +| Socket layer | `ec_socket.h` + platform ports | POSIX, FreeRTOS, and bare-metal HAL ports | +| TLS (mbedTLS) | platform socket ports, `ec_cacerts.h` | POSIX/FreeRTOS via mbedTLS; bare-metal via HAL | | HTTP client | `ec_http.c/h` | Done (chunked encoding, TLS pass-through) | | JSON builder/parser | `ec_json.c/h` | Done (writer + path-based parser) | | Chat API | `ec_api.c/h` | Done (text + tool_calls responses) | @@ -23,8 +23,8 @@ The FreeRTOS backend remains stubbed, pending target hardware bring-up. | Session layer | `ec_session.c/h` | Done (ring buffer, tool_call support) | | Agent loop | `ec_agent.c/h` | Done (multi-iteration tool dispatch) | | I/O abstraction | `ec_io.c/h` | Done | -| UART I/O backend | `ec_io_uart.c` | Done (stdin/stdout on POSIX) | -| Telnet I/O backend | `ec_io_telnet.c` | Done (TCP server on configurable port) | +| UART I/O backend | `ec_io_*_uart.c` | POSIX stdin/stdout plus embedded HAL bridges | +| Telnet I/O backend | `ec_io_*_telnet.c` | POSIX and FreeRTOS TCP server backends | | Debug logging | `ec_log.c/h` | Done (EC_DEBUG=1 on POSIX, compile-time on FreeRTOS) | | E2E test suite | `tests/test_e2e.c` | 14 tests passing (mock HTTP layer) | | Demo application | `main.c` | POSIX CLI demo with env config | @@ -33,10 +33,11 @@ The FreeRTOS backend remains stubbed, pending target hardware bring-up. | Component | Files | Notes | |------------------------|------------------------------|-------------------------------------| -| FreeRTOS sockets | `ec_socket.c` (FREERTOS) | Replace stubs with FreeRTOS+TCP | -| FreeRTOS UART I/O | `ec_io_uart.c` (FREERTOS) | Wire to HAL UART driver | -| FreeRTOS Telnet I/O | `ec_io_telnet.c` (FREERTOS) | Wire to FreeRTOS+TCP server socket | -| FreeRTOS TLS | `ec_socket.c` (FREERTOS) | Socket layer is TLS-aware; needs BIO callbacks | +| FreeRTOS sockets | `src/platform/freertos/ec_socket_freertos.c` | FreeRTOS+TCP backend | +| FreeRTOS UART I/O | `src/platform/freertos/ec_io_freertos_uart.c` | HAL UART bridge | +| FreeRTOS Telnet I/O | `src/platform/freertos/ec_io_freertos_telnet.c` | FreeRTOS+TCP server socket | +| FreeRTOS TLS | `src/platform/freertos/ec_socket_freertos.c` | Socket layer is TLS-aware via BIO callbacks | +| Bare-metal port | `src/platform/baremetal/` | UART/socket HAL bridges and direct MMIO | | Flash/NVS persistence | `ec_session.c` | Serialize/deserialize history | | HW register allowlist | `ec_tool.c` | Address range validation | | Minimal mbedTLS config | `ec_mbedtls_config.h` | Reduce binary size for embedded | @@ -94,7 +95,9 @@ registers tools and contributes system prompt context. Two built-in skills: ### Phase 7 — TLS / HTTPS ✅ **Completed.** mbedTLS v3.6.5 integrated as git submodule (`third_party/mbedtls`). -Transparent TLS in `ec_socket.c` via custom BIO callbacks on the raw fd. +Transparent TLS in the POSIX and FreeRTOS socket ports via custom BIO callbacks +on the raw fd/socket. Bare-metal builds delegate TLS to the registered socket +HAL. Embedded CA bundle in `ec_cacerts.h` (no filesystem dependency). SNI hostname verification. Certificate validation set to `MBEDTLS_SSL_VERIFY_REQUIRED`. diff --git a/spec.md b/spec.md index 5ca614f..09658b3 100644 --- a/spec.md +++ b/spec.md @@ -3,9 +3,9 @@ EmbedClaw ## Purpose -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 +EmbedClaw is an embedded AI agent runtime for FreeRTOS and bare-metal targets. +It runs on constrained hardware and acts as an intelligent automation layer: it +accepts user input over a serial/network transport, 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. @@ -17,8 +17,9 @@ protocol, different execution environment. ## Design Principles -- **Single-threaded**: No RTOS threads or callbacks. The agent loop runs to - completion in a single task. All I/O is blocking with timeouts. +- **Single-threaded**: No RTOS threads are required. The agent loop runs to + completion in a single task or foreground loop. All I/O is blocking with + timeouts. - **No dynamic allocation in hot paths**: All buffers are caller-provided or statically declared. No `malloc` in the agent or tool layers. - **Minimal external dependencies**: JSON, HTTP, and the agent loop are all @@ -94,8 +95,8 @@ protocol, different execution environment. ▼ ┌─────────────────────────────────────────────────────┐ │ Socket Abstraction (ec_socket) │ -│ TCP + optional TLS (mbedTLS, embedded CA bundle) │ -│ FreeRTOS+TCP backend / POSIX shim (host testing) │ +│ TCP + optional TLS │ +│ POSIX / FreeRTOS+TCP / bare-metal HAL │ └─────────────────────────────────────────────────────┘ ``` @@ -103,7 +104,7 @@ protocol, different execution environment. ## Components -### 1. Socket Abstraction (`ec_socket.h` / `ec_socket.c`) +### 1. Socket Abstraction (`ec_socket.h` + platform port) Wraps the platform TCP API and optional TLS into four functions: @@ -114,9 +115,11 @@ int ec_socket_recv(ec_socket_t *s, void *buf, size_t len, uint32_t time void ec_socket_close(ec_socket_t *s); ``` -Two backends selected at compile time via `EC_PLATFORM`: +Backends are selected at compile time via `EC_PLATFORM`: - `POSIX` — standard BSD sockets, used for host-side development and testing. - `FREERTOS` — FreeRTOS+TCP sockets (targeting FreeRTOS+TCP 10.2.1). +- `BAREMETAL` — board-provided socket HAL callbacks, allowing vendor TCP/IP, + modem, or TLS offload stacks without any FreeRTOS dependency. **TLS support** (when `EC_CONFIG_USE_TLS=1`): - mbedTLS v3.6.5 integrated as a git submodule (`third_party/mbedtls`). @@ -306,10 +309,10 @@ int ec_io_write(const char *str); ``` Implementations: -- **UART** (`ec_io_uart.c`): wraps POSIX stdin/stdout on host builds and uses +- **UART** (`ec_io_*_uart.c`): wraps POSIX stdin/stdout on host builds and uses board-supplied FreeRTOS UART HAL hooks via `ec_io_uart_set_hal()` on embedded builds. -- **Telnet** (`ec_io_telnet.c`): wraps a blocking single-client TCP server on +- **Telnet** (`ec_io_*_telnet.c`): wraps a blocking single-client TCP server on POSIX and FreeRTOS+TCP builds. ### 10. Debug Logging (`ec_log.h` / `ec_log.c`) diff --git a/src/main.c b/src/app/ec_posix_main.c similarity index 100% rename from src/main.c rename to src/app/ec_posix_main.c diff --git a/src/ec_agent.c b/src/core/ec_agent.c similarity index 100% rename from src/ec_agent.c rename to src/core/ec_agent.c diff --git a/src/ec_api.c b/src/core/ec_api.c similarity index 100% rename from src/ec_api.c rename to src/core/ec_api.c diff --git a/src/ec_http.c b/src/core/ec_http.c similarity index 100% rename from src/ec_http.c rename to src/core/ec_http.c diff --git a/src/ec_hw_access.c b/src/core/ec_hw_access.c similarity index 100% rename from src/ec_hw_access.c rename to src/core/ec_hw_access.c diff --git a/src/ec_hw_registry.c b/src/core/ec_hw_registry.c similarity index 100% rename from src/ec_hw_registry.c rename to src/core/ec_hw_registry.c diff --git a/src/ec_io.c b/src/core/ec_io.c similarity index 100% rename from src/ec_io.c rename to src/core/ec_io.c diff --git a/src/ec_json.c b/src/core/ec_json.c similarity index 100% rename from src/ec_json.c rename to src/core/ec_json.c diff --git a/src/ec_log.c b/src/core/ec_log.c similarity index 100% rename from src/ec_log.c rename to src/core/ec_log.c diff --git a/src/ec_model.c b/src/core/ec_model.c similarity index 100% rename from src/ec_model.c rename to src/core/ec_model.c diff --git a/src/ec_session.c b/src/core/ec_session.c similarity index 100% rename from src/ec_session.c rename to src/core/ec_session.c diff --git a/src/ec_skill.c b/src/core/ec_skill.c similarity index 100% rename from src/ec_skill.c rename to src/core/ec_skill.c diff --git a/src/ec_skill_table.c b/src/core/ec_skill_table.c similarity index 99% rename from src/ec_skill_table.c rename to src/core/ec_skill_table.c index ff8228b..77c6e5f 100644 --- a/src/ec_skill_table.c +++ b/src/core/ec_skill_table.c @@ -39,7 +39,7 @@ * ============================================================================ */ const char EC_BASE_SYSTEM_PROMPT[] = - "You are an embedded systems assistant running on FreeRTOS. " + "You are an embedded systems assistant running on constrained firmware. " "Answer concisely. " "When the user asks about hardware state or configuration, " "use your tools to inspect and control the device directly. " diff --git a/src/ec_tool.c b/src/core/ec_tool.c similarity index 100% rename from src/ec_tool.c rename to src/core/ec_tool.c diff --git a/src/platform/baremetal/ec_io_baremetal_uart.c b/src/platform/baremetal/ec_io_baremetal_uart.c new file mode 100644 index 0000000..884ab37 --- /dev/null +++ b/src/platform/baremetal/ec_io_baremetal_uart.c @@ -0,0 +1,76 @@ +#include "ec_io.h" +#include "ec_config.h" + +#if !defined(EC_PLATFORM_BAREMETAL) +#error "ec_io_baremetal_uart.c must only be built for EC_PLATFORM=BAREMETAL" +#endif + +#include + +static const ec_io_uart_hal_t *s_uart_hal = 0; + +void ec_io_uart_set_hal(const ec_io_uart_hal_t *hal) +{ + s_uart_hal = hal; +} + +static int uart_read_line(char *buf, size_t size) +{ + if (!buf || size == 0) return -1; + if (!s_uart_hal || !s_uart_hal->read) return -1; + + size_t len = 0; + int truncated = 0; + + for (;;) { + char ch = '\0'; + int rc = s_uart_hal->read(&ch, 1, EC_CONFIG_UART_RX_TIMEOUT_MS); + if (rc < 0) return -1; + if (rc == 0) continue; + + if (ch == '\r') continue; + if (ch == '\n') break; + + if (len < size - 1) { + buf[len++] = ch; + } else { + truncated = 1; + } + } + + if (truncated) { + static const char warning[] = "\n[input truncated: line too long]\n"; + if (s_uart_hal->write) { + s_uart_hal->write(warning, sizeof(warning) - 1, + EC_CONFIG_UART_TX_TIMEOUT_MS); + } + buf[0] = '\0'; + return 0; + } + + buf[len] = '\0'; + return (int)len; +} + +static int uart_write(const char *str) +{ + if (!str) return -1; + if (!s_uart_hal || !s_uart_hal->write) return -1; + + size_t remaining = strlen(str); + const char *p = str; + + while (remaining > 0) { + int rc = s_uart_hal->write(p, remaining, EC_CONFIG_UART_TX_TIMEOUT_MS); + if (rc <= 0) return -1; + p += (size_t)rc; + remaining -= (size_t)rc; + } + + return 0; +} + +const ec_io_ops_t ec_io_uart_ops = { + .read_line = uart_read_line, + .write = uart_write, +}; diff --git a/src/platform/baremetal/ec_mmio_direct.c b/src/platform/baremetal/ec_mmio_direct.c new file mode 100644 index 0000000..ea0e96c --- /dev/null +++ b/src/platform/baremetal/ec_mmio_direct.c @@ -0,0 +1,20 @@ +#include "ec_mmio.h" + +#if !defined(EC_PLATFORM_BAREMETAL) +#error "ec_mmio_direct.c must only be built for EC_PLATFORM=BAREMETAL" +#endif + +#include + +int ec_mmio_read32(uint32_t address, uint32_t *value) +{ + if (!value) return -1; + *value = *(volatile uint32_t *)(uintptr_t)address; + return 0; +} + +int ec_mmio_write32(uint32_t address, uint32_t value) +{ + *(volatile uint32_t *)(uintptr_t)address = value; + return 0; +} diff --git a/src/platform/baremetal/ec_socket_baremetal.c b/src/platform/baremetal/ec_socket_baremetal.c new file mode 100644 index 0000000..6e39ee3 --- /dev/null +++ b/src/platform/baremetal/ec_socket_baremetal.c @@ -0,0 +1,67 @@ +#include "ec_socket.h" + +#if !defined(EC_PLATFORM_BAREMETAL) +#error "ec_socket_baremetal.c must only be built for EC_PLATFORM=BAREMETAL" +#endif + +/* + * Bare-metal socket port. + * + * This port deliberately delegates TCP/TLS ownership to board support code. + * That keeps the shared HTTP/model/agent stack reusable on MCUs that use a + * vendor Ethernet driver, Wi-Fi module, cellular modem, or TLS offload engine. + */ + +struct ec_socket { + int in_use; +}; + +static const ec_socket_baremetal_hal_t *s_hal = 0; +static ec_socket_t s_socket; + +void ec_socket_baremetal_set_hal(const ec_socket_baremetal_hal_t *hal) +{ + s_hal = hal; +} + +ec_socket_t *ec_socket_connect(const char *host, uint16_t port, int use_tls) +{ + if (!s_hal || !s_hal->connect || s_socket.in_use) { + return 0; + } + + if (s_hal->connect(s_hal->ctx, host, port, use_tls) != 0) { + return 0; + } + + s_socket.in_use = 1; + return &s_socket; +} + +int ec_socket_send(ec_socket_t *sock, const void *data, size_t len) +{ + if (!sock || !sock->in_use || !s_hal || !s_hal->send) { + return -1; + } + return s_hal->send(s_hal->ctx, data, len); +} + +int ec_socket_recv(ec_socket_t *sock, void *buf, size_t len, uint32_t timeout_ms) +{ + if (!sock || !sock->in_use || !s_hal || !s_hal->recv) { + return -1; + } + return s_hal->recv(s_hal->ctx, buf, len, timeout_ms); +} + +void ec_socket_close(ec_socket_t *sock) +{ + if (!sock || !sock->in_use) { + return; + } + + if (s_hal && s_hal->close) { + s_hal->close(s_hal->ctx); + } + sock->in_use = 0; +} diff --git a/src/platform/freertos/ec_freertos_entry.c b/src/platform/freertos/ec_freertos_entry.c new file mode 100644 index 0000000..3b4990e --- /dev/null +++ b/src/platform/freertos/ec_freertos_entry.c @@ -0,0 +1,78 @@ +#include "ec_model.h" +#include "ec_agent.h" +#include "ec_session.h" +#include "ec_skill.h" +#include "ec_io.h" +#include "ec_config.h" +#include "ec_log.h" + +#if !defined(EC_PLATFORM_FREERTOS) +#error "ec_freertos_entry.c must only be built for EC_PLATFORM=FREERTOS" +#endif + +#include +#include + +static ec_session_t s_session; +static ec_agent_t s_agent; + +static void run_agent_loop(const ec_model_config_t *config, const char *model) +{ + ec_log_init(); + ec_skill_init(); + + ec_session_init(&s_session, ec_skill_get_system_prompt()); + ec_agent_init(&s_agent, config, model, &s_session); + + char line[EC_CONFIG_IO_LINE_BUF]; + char response[EC_CONFIG_CONTENT_BUF]; + + ec_io_write("EmbedClaw ready. Type /reset to clear history, /quit to exit.\n> "); + + for (;;) { + int n = ec_io_read_line(line, sizeof(line)); + if (n < 0) break; + + if (line[0] == '\0') { + ec_io_write("> "); + continue; + } + if (strcmp(line, "/reset") == 0) { + ec_session_reset(&s_session); + ec_io_write("Session reset.\n> "); + continue; + } + if (strcmp(line, "/quit") == 0) { + ec_io_write("Goodbye.\n"); + break; + } + + int rc = ec_agent_run_turn(&s_agent, line, response, sizeof(response)); + if (rc == 0) { + ec_io_write(response); + ec_io_write("\n> "); + } else { + char err[64]; + snprintf(err, sizeof(err), "[error: agent rc=%d]\n> ", rc); + ec_io_write(err); + } + } +} + +void vEmbedClawTask(void *pvParameters) +{ + (void)pvParameters; + + ec_model_config_t config = { + .provider = EC_MODEL_PROVIDER_OPENAI_CHAT, + .host = EC_CONFIG_API_HOST, + .port = EC_CONFIG_API_PORT, + .api_key = EC_CONFIG_API_KEY, + .use_tls = EC_CONFIG_USE_TLS, + }; + + ec_io_init(&ec_io_telnet_ops); + run_agent_loop(&config, EC_CONFIG_MODEL); + + for (;;) {} +} diff --git a/src/ec_io_telnet.c b/src/platform/freertos/ec_io_freertos_telnet.c similarity index 54% rename from src/ec_io_telnet.c rename to src/platform/freertos/ec_io_freertos_telnet.c index ce59c49..dc36a29 100644 --- a/src/ec_io_telnet.c +++ b/src/platform/freertos/ec_io_freertos_telnet.c @@ -34,184 +34,10 @@ static const char *skip_iac(const char *p, const char *end) return p > start ? p : NULL; } -#if defined(EC_PLATFORM_POSIX) - -#include -#include -#include -#include -#include - -static int s_listen_fd = -1; -static int s_client_fd = -1; -static char s_pending[EC_CONFIG_IO_LINE_BUF * 2]; -static size_t s_pending_len = 0; - -static int telnet_write(const char *str); - -/* - * Open the listening socket once. Idempotent. - */ -static int telnet_ensure_listen(void) -{ - if (s_listen_fd >= 0) return 0; - - s_listen_fd = socket(AF_INET, SOCK_STREAM, 0); - if (s_listen_fd < 0) return -1; - - int opt = 1; - setsockopt(s_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons(EC_CONFIG_TELNET_PORT); - - if (bind(s_listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - close(s_listen_fd); - s_listen_fd = -1; - return -1; - } - if (listen(s_listen_fd, 1) < 0) { - close(s_listen_fd); - s_listen_fd = -1; - return -1; - } - fprintf(stderr, "[embedclaw] Telnet listening on port %d\n", - EC_CONFIG_TELNET_PORT); - return 0; -} - -/* - * Accept a new client, blocking until one connects. - */ -static int telnet_accept(void) -{ - if (s_client_fd >= 0) { - close(s_client_fd); - s_client_fd = -1; - } - if (telnet_ensure_listen() < 0) return -1; - - s_client_fd = accept(s_listen_fd, NULL, NULL); - if (s_client_fd < 0) return -1; - s_pending_len = 0; - - fprintf(stderr, "[embedclaw] Telnet client connected\n"); - return 0; -} - -static int telnet_read_line(char *buf, size_t size) -{ - if (!buf || size == 0) return -1; - - for (;;) { - /* Accept a connection if we don't have one */ - if (s_client_fd < 0) { - if (telnet_accept() < 0) return -1; - } - - size_t len = 0; - int truncated = 0; - - for (;;) { - size_t consumed = 0; - while (consumed < s_pending_len) { - const char *p = s_pending + consumed; - const char *end = s_pending + s_pending_len; - unsigned char c = (unsigned char)*p; - - if (c == 0xFF) { - if (p + 1 >= end) break; - if ((unsigned char)*(p + 1) == 0xFF) { - consumed += 2; - if (len < size - 1) { - buf[len++] = (char)0xFF; - } else { - truncated = 1; - } - continue; - } - - const char *next = skip_iac(p, end); - if (!next) break; - consumed += (size_t)(next - p); - continue; - } - - consumed++; - if (c == '\r') continue; - - if (c == '\n') { - memmove(s_pending, s_pending + consumed, - s_pending_len - consumed); - s_pending_len -= consumed; - - if (truncated) { - static const char warning[] = - "\n[input truncated: line too long]\n"; - (void)telnet_write(warning); - buf[0] = '\0'; - return 0; - } - - buf[len] = '\0'; - return (int)len; - } - - if (len < size - 1) { - buf[len++] = (char)c; - } else { - truncated = 1; - } - } - - if (consumed > 0) { - memmove(s_pending, s_pending + consumed, s_pending_len - consumed); - s_pending_len -= consumed; - } - - if (s_pending_len == sizeof(s_pending)) { - fprintf(stderr, "[embedclaw] Telnet client disconnected (buffer overflow)\n"); - close(s_client_fd); - s_client_fd = -1; - s_pending_len = 0; - break; - } - - ssize_t n = recv(s_client_fd, s_pending + s_pending_len, - sizeof(s_pending) - s_pending_len, 0); - if (n <= 0) { - fprintf(stderr, "[embedclaw] Telnet client disconnected\n"); - close(s_client_fd); - s_client_fd = -1; - s_pending_len = 0; - break; - } - s_pending_len += (size_t)n; - } - } -} - -static int telnet_write(const char *str) -{ - if (s_client_fd < 0) return -1; - size_t len = strlen(str); - while (len > 0) { - ssize_t n = send(s_client_fd, str, len, 0); - if (n <= 0) { - close(s_client_fd); - s_client_fd = -1; - return -1; - } - str += (size_t)n; - len -= (size_t)n; - } - return 0; -} +#if !defined(EC_PLATFORM_FREERTOS) +#error "ec_io_freertos_telnet.c must only be built for EC_PLATFORM=FREERTOS" +#endif -#elif defined(EC_PLATFORM_FREERTOS) #include "FreeRTOS.h" #include "FreeRTOS_IP.h" @@ -374,9 +200,7 @@ static int telnet_write(const char *str) return 0; } -#else -#error "Define EC_PLATFORM_POSIX or EC_PLATFORM_FREERTOS" -#endif + const ec_io_ops_t ec_io_telnet_ops = { .read_line = telnet_read_line, diff --git a/src/ec_io_uart.c b/src/platform/freertos/ec_io_freertos_uart.c similarity index 77% rename from src/ec_io_uart.c rename to src/platform/freertos/ec_io_freertos_uart.c index a55a4fa..837bc12 100644 --- a/src/ec_io_uart.c +++ b/src/platform/freertos/ec_io_freertos_uart.c @@ -10,26 +10,10 @@ void ec_io_uart_set_hal(const ec_io_uart_hal_t *hal) s_uart_hal = hal; } -#if defined(EC_PLATFORM_POSIX) - -#include - -static int uart_read_line(char *buf, size_t size) -{ - if (!fgets(buf, (int)size, stdin)) return -1; - /* Strip trailing newline */ - size_t len = strlen(buf); - if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0'; - if (len > 0 && buf[len - 1] == '\r') buf[--len] = '\0'; - return (int)len; -} - -static int uart_write(const char *str) -{ - return fputs(str, stdout) < 0 ? -1 : 0; -} +#if !defined(EC_PLATFORM_FREERTOS) +#error "ec_io_freertos_uart.c must only be built for EC_PLATFORM=FREERTOS" +#endif -#elif defined(EC_PLATFORM_FREERTOS) /* * FreeRTOS UART backend. @@ -95,9 +79,7 @@ static int uart_write(const char *str) return 0; } -#else -#error "Define EC_PLATFORM_POSIX or EC_PLATFORM_FREERTOS" -#endif + const ec_io_ops_t ec_io_uart_ops = { .read_line = uart_read_line, diff --git a/src/platform/freertos/ec_mmio_direct.c b/src/platform/freertos/ec_mmio_direct.c new file mode 100644 index 0000000..2a78902 --- /dev/null +++ b/src/platform/freertos/ec_mmio_direct.c @@ -0,0 +1,20 @@ +#include "ec_mmio.h" + +#if !defined(EC_PLATFORM_FREERTOS) +#error "ec_mmio_direct.c must only be built for EC_PLATFORM=FREERTOS" +#endif + +#include + +int ec_mmio_read32(uint32_t address, uint32_t *value) +{ + if (!value) return -1; + *value = *(volatile uint32_t *)(uintptr_t)address; + return 0; +} + +int ec_mmio_write32(uint32_t address, uint32_t value) +{ + *(volatile uint32_t *)(uintptr_t)address = value; + return 0; +} diff --git a/src/ec_socket.c b/src/platform/freertos/ec_socket_freertos.c similarity index 53% rename from src/ec_socket.c rename to src/platform/freertos/ec_socket_freertos.c index 7342307..d8e974c 100644 --- a/src/ec_socket.c +++ b/src/platform/freertos/ec_socket_freertos.c @@ -14,249 +14,10 @@ #include "ec_cacerts.h" #endif -#if defined(EC_PLATFORM_POSIX) - -#include -#include -#include -#include -#include -#include -#include - -struct ec_socket { - int fd; -#if EC_CONFIG_USE_TLS - int tls_active; - mbedtls_ssl_context ssl; - mbedtls_ssl_config ssl_conf; - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - mbedtls_x509_crt ca_cert; -#endif -}; - -/* ---- TLS BIO callbacks (send/recv on raw fd) --------------------------- */ -#if EC_CONFIG_USE_TLS - -static int tls_bio_send(void *ctx, const unsigned char *buf, size_t len) -{ - int fd = *(int *)ctx; - ssize_t n = send(fd, buf, len, 0); - if (n < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) - return MBEDTLS_ERR_SSL_WANT_WRITE; - return MBEDTLS_ERR_NET_SEND_FAILED; - } - return (int)n; -} - -static int tls_bio_recv(void *ctx, unsigned char *buf, size_t len) -{ - int fd = *(int *)ctx; - ssize_t n = recv(fd, buf, len, 0); - if (n < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) - return MBEDTLS_ERR_SSL_WANT_READ; - return MBEDTLS_ERR_NET_RECV_FAILED; - } - if (n == 0) - return MBEDTLS_ERR_SSL_WANT_READ; - return (int)n; -} - -static int tls_handshake(ec_socket_t *sock, const char *host) -{ - mbedtls_ssl_init(&sock->ssl); - mbedtls_ssl_config_init(&sock->ssl_conf); - mbedtls_entropy_init(&sock->entropy); - mbedtls_ctr_drbg_init(&sock->ctr_drbg); - mbedtls_x509_crt_init(&sock->ca_cert); - - /* Seed the DRBG */ - int ret = mbedtls_ctr_drbg_seed(&sock->ctr_drbg, mbedtls_entropy_func, - &sock->entropy, NULL, 0); - if (ret != 0) return ret; - - /* Load embedded CA bundle */ - ret = mbedtls_x509_crt_parse(&sock->ca_cert, - EC_CA_BUNDLE, EC_CA_BUNDLE_LEN); - if (ret != 0) return ret; - - /* Configure TLS client defaults */ - ret = mbedtls_ssl_config_defaults(&sock->ssl_conf, - MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT); - if (ret != 0) return ret; - - mbedtls_ssl_conf_authmode(&sock->ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&sock->ssl_conf, &sock->ca_cert, NULL); - mbedtls_ssl_conf_rng(&sock->ssl_conf, - mbedtls_ctr_drbg_random, &sock->ctr_drbg); - - ret = mbedtls_ssl_setup(&sock->ssl, &sock->ssl_conf); - if (ret != 0) return ret; - - /* Set SNI hostname for virtual hosting */ - ret = mbedtls_ssl_set_hostname(&sock->ssl, host); - if (ret != 0) return ret; - - /* Attach BIO callbacks using raw fd */ - mbedtls_ssl_set_bio(&sock->ssl, &sock->fd, - tls_bio_send, tls_bio_recv, NULL); - - /* Perform handshake */ - while ((ret = mbedtls_ssl_handshake(&sock->ssl)) != 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && - ret != MBEDTLS_ERR_SSL_WANT_WRITE) - return ret; - } - - sock->tls_active = 1; - return 0; -} - -static void tls_cleanup(ec_socket_t *sock) -{ - if (sock->tls_active) - mbedtls_ssl_close_notify(&sock->ssl); - mbedtls_ssl_free(&sock->ssl); - mbedtls_ssl_config_free(&sock->ssl_conf); - mbedtls_ctr_drbg_free(&sock->ctr_drbg); - mbedtls_entropy_free(&sock->entropy); - mbedtls_x509_crt_free(&sock->ca_cert); -} - -#endif /* EC_CONFIG_USE_TLS */ - -ec_socket_t *ec_socket_connect(const char *host, uint16_t port, int use_tls) -{ -#if !EC_CONFIG_USE_TLS - if (use_tls) return NULL; /* TLS not compiled in */ -#endif - - char port_str[8]; - snprintf(port_str, sizeof(port_str), "%u", (unsigned)port); - - struct addrinfo hints; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - struct addrinfo *res = NULL; - if (getaddrinfo(host, port_str, &hints, &res) != 0 || res == NULL) { - return NULL; - } - - int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (fd < 0) { - freeaddrinfo(res); - return NULL; - } - - if (connect(fd, res->ai_addr, res->ai_addrlen) != 0) { - close(fd); - freeaddrinfo(res); - return NULL; - } - - freeaddrinfo(res); - - ec_socket_t *sock = (ec_socket_t *)calloc(1, sizeof(ec_socket_t)); - if (!sock) { - close(fd); - return NULL; - } - sock->fd = fd; - -#if EC_CONFIG_USE_TLS - if (use_tls) { - if (tls_handshake(sock, host) != 0) { - tls_cleanup(sock); - close(fd); - free(sock); - return NULL; - } - } +#if !defined(EC_PLATFORM_FREERTOS) +#error "ec_socket_freertos.c must only be built for EC_PLATFORM=FREERTOS" #endif - return sock; -} - -int ec_socket_send(ec_socket_t *sock, const void *data, size_t len) -{ - if (!sock) return -1; - -#if EC_CONFIG_USE_TLS - if (sock->tls_active) { - size_t total = 0; - const unsigned char *p = (const unsigned char *)data; - while (total < len) { - int n = mbedtls_ssl_write(&sock->ssl, p + total, len - total); - if (n == MBEDTLS_ERR_SSL_WANT_WRITE) continue; - if (n < 0) return -1; - total += (size_t)n; - } - return (int)total; - } -#endif - - size_t total = 0; - const char *p = (const char *)data; - while (total < len) { - ssize_t n = send(sock->fd, p + total, len - total, 0); - if (n <= 0) return -1; - total += (size_t)n; - } - return (int)total; -} - -int ec_socket_recv(ec_socket_t *sock, void *buf, size_t len, uint32_t timeout_ms) -{ - if (!sock) return -1; - - /* Set recv timeout — applies to both plain and TLS (via BIO callback) */ - struct timeval tv; - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; - setsockopt(sock->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - -#if EC_CONFIG_USE_TLS - if (sock->tls_active) { - int n = mbedtls_ssl_read(&sock->ssl, (unsigned char *)buf, len); - if (n == MBEDTLS_ERR_SSL_WANT_READ) - return 0; /* timeout */ - if (n == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || n == 0) - return 0; /* connection closed */ - if (n < 0) return -1; - return n; - } -#endif - - ssize_t n = recv(sock->fd, buf, len, 0); - if (n < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) - return 0; /* timeout */ - return -1; - } - return (int)n; -} - -void ec_socket_close(ec_socket_t *sock) -{ - if (!sock) return; - -#if EC_CONFIG_USE_TLS - if (sock->tls_active) - tls_cleanup(sock); -#endif - - close(sock->fd); - free(sock); -} - -#elif defined(EC_PLATFORM_FREERTOS) #include "FreeRTOS.h" #include "FreeRTOS_IP.h" @@ -526,6 +287,4 @@ void ec_socket_close(ec_socket_t *sock) free(sock); } -#else -#error "Define EC_PLATFORM_POSIX or EC_PLATFORM_FREERTOS" -#endif + diff --git a/src/platform/posix/ec_io_posix_telnet.c b/src/platform/posix/ec_io_posix_telnet.c new file mode 100644 index 0000000..6ece2f7 --- /dev/null +++ b/src/platform/posix/ec_io_posix_telnet.c @@ -0,0 +1,221 @@ +#include "ec_io.h" +#include "ec_config.h" + +#include + +/* + * Skip a Telnet IAC command sequence starting at p. + * Returns pointer past the sequence, or NULL if the sequence is incomplete. + */ +static const char *skip_iac(const char *p, const char *end) +{ + /* p points to 0xFF (IAC) */ + const char *start = p; + p++; /* skip IAC */ + if (p >= end) return NULL; + + unsigned char cmd = (unsigned char)*p++; + if (cmd == 0xFA) { + /* SB ... IAC SE sub-negotiation */ + while (p + 1 < end) { + if ((unsigned char)*p == 0xFF && (unsigned char)*(p + 1) == 0xF0) { + p += 2; + return p; + } + p++; + } + return NULL; + } else if (cmd == 0xFB || cmd == 0xFC || cmd == 0xFD || cmd == 0xFE) { + /* WILL/WONT/DO/DONT: one more option byte */ + if (p >= end) return NULL; + p++; + } + + return p > start ? p : NULL; +} + +#if defined(EC_PLATFORM_POSIX) + +#include +#include +#include +#include +#include + +static int s_listen_fd = -1; +static int s_client_fd = -1; +static char s_pending[EC_CONFIG_IO_LINE_BUF * 2]; +static size_t s_pending_len = 0; + +static int telnet_write(const char *str); + +/* + * Open the listening socket once. Idempotent. + */ +static int telnet_ensure_listen(void) +{ + if (s_listen_fd >= 0) return 0; + + s_listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (s_listen_fd < 0) return -1; + + int opt = 1; + setsockopt(s_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(EC_CONFIG_TELNET_PORT); + + if (bind(s_listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(s_listen_fd); + s_listen_fd = -1; + return -1; + } + if (listen(s_listen_fd, 1) < 0) { + close(s_listen_fd); + s_listen_fd = -1; + return -1; + } + fprintf(stderr, "[embedclaw] Telnet listening on port %d\n", + EC_CONFIG_TELNET_PORT); + return 0; +} + +/* + * Accept a new client, blocking until one connects. + */ +static int telnet_accept(void) +{ + if (s_client_fd >= 0) { + close(s_client_fd); + s_client_fd = -1; + } + if (telnet_ensure_listen() < 0) return -1; + + s_client_fd = accept(s_listen_fd, NULL, NULL); + if (s_client_fd < 0) return -1; + s_pending_len = 0; + + fprintf(stderr, "[embedclaw] Telnet client connected\n"); + return 0; +} + +static int telnet_read_line(char *buf, size_t size) +{ + if (!buf || size == 0) return -1; + + for (;;) { + /* Accept a connection if we don't have one */ + if (s_client_fd < 0) { + if (telnet_accept() < 0) return -1; + } + + size_t len = 0; + int truncated = 0; + + for (;;) { + size_t consumed = 0; + while (consumed < s_pending_len) { + const char *p = s_pending + consumed; + const char *end = s_pending + s_pending_len; + unsigned char c = (unsigned char)*p; + + if (c == 0xFF) { + if (p + 1 >= end) break; + if ((unsigned char)*(p + 1) == 0xFF) { + consumed += 2; + if (len < size - 1) { + buf[len++] = (char)0xFF; + } else { + truncated = 1; + } + continue; + } + + const char *next = skip_iac(p, end); + if (!next) break; + consumed += (size_t)(next - p); + continue; + } + + consumed++; + if (c == '\r') continue; + + if (c == '\n') { + memmove(s_pending, s_pending + consumed, + s_pending_len - consumed); + s_pending_len -= consumed; + + if (truncated) { + static const char warning[] = + "\n[input truncated: line too long]\n"; + (void)telnet_write(warning); + buf[0] = '\0'; + return 0; + } + + buf[len] = '\0'; + return (int)len; + } + + if (len < size - 1) { + buf[len++] = (char)c; + } else { + truncated = 1; + } + } + + if (consumed > 0) { + memmove(s_pending, s_pending + consumed, s_pending_len - consumed); + s_pending_len -= consumed; + } + + if (s_pending_len == sizeof(s_pending)) { + fprintf(stderr, "[embedclaw] Telnet client disconnected (buffer overflow)\n"); + close(s_client_fd); + s_client_fd = -1; + s_pending_len = 0; + break; + } + + ssize_t n = recv(s_client_fd, s_pending + s_pending_len, + sizeof(s_pending) - s_pending_len, 0); + if (n <= 0) { + fprintf(stderr, "[embedclaw] Telnet client disconnected\n"); + close(s_client_fd); + s_client_fd = -1; + s_pending_len = 0; + break; + } + s_pending_len += (size_t)n; + } + } +} + +static int telnet_write(const char *str) +{ + if (s_client_fd < 0) return -1; + size_t len = strlen(str); + while (len > 0) { + ssize_t n = send(s_client_fd, str, len, 0); + if (n <= 0) { + close(s_client_fd); + s_client_fd = -1; + return -1; + } + str += (size_t)n; + len -= (size_t)n; + } + return 0; +} + +#else +#error "ec_io_posix_telnet.c must only be built for EC_PLATFORM=POSIX" +#endif + +const ec_io_ops_t ec_io_telnet_ops = { + .read_line = telnet_read_line, + .write = telnet_write, +}; diff --git a/src/platform/posix/ec_io_posix_uart.c b/src/platform/posix/ec_io_posix_uart.c new file mode 100644 index 0000000..bd9ec96 --- /dev/null +++ b/src/platform/posix/ec_io_posix_uart.c @@ -0,0 +1,39 @@ +#include "ec_io.h" +#include "ec_config.h" + +#include + +static const ec_io_uart_hal_t *s_uart_hal = NULL; + +void ec_io_uart_set_hal(const ec_io_uart_hal_t *hal) +{ + s_uart_hal = hal; +} + +#if defined(EC_PLATFORM_POSIX) + +#include + +static int uart_read_line(char *buf, size_t size) +{ + if (!fgets(buf, (int)size, stdin)) return -1; + /* Strip trailing newline */ + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[--len] = '\0'; + if (len > 0 && buf[len - 1] == '\r') buf[--len] = '\0'; + return (int)len; +} + +static int uart_write(const char *str) +{ + return fputs(str, stdout) < 0 ? -1 : 0; +} + +#else +#error "ec_io_posix_uart.c must only be built for EC_PLATFORM=POSIX" +#endif + +const ec_io_ops_t ec_io_uart_ops = { + .read_line = uart_read_line, + .write = uart_write, +}; diff --git a/src/ec_mmio.c b/src/platform/posix/ec_mmio_posix.c similarity index 89% rename from src/ec_mmio.c rename to src/platform/posix/ec_mmio_posix.c index bbf9cac..aaaecfd 100644 --- a/src/ec_mmio.c +++ b/src/platform/posix/ec_mmio_posix.c @@ -129,21 +129,6 @@ int ec_mmio_write32(uint32_t address, uint32_t value) return 0; } -#elif defined(EC_PLATFORM_FREERTOS) - -int ec_mmio_read32(uint32_t address, uint32_t *value) -{ - if (!value) return -1; - *value = *(volatile uint32_t *)(uintptr_t)address; - return 0; -} - -int ec_mmio_write32(uint32_t address, uint32_t value) -{ - *(volatile uint32_t *)(uintptr_t)address = value; - return 0; -} - #else -#error "Define EC_PLATFORM_POSIX or EC_PLATFORM_FREERTOS" +#error "ec_mmio_posix.c must only be built for EC_PLATFORM=POSIX" #endif diff --git a/src/platform/posix/ec_socket_posix.c b/src/platform/posix/ec_socket_posix.c new file mode 100644 index 0000000..f4df420 --- /dev/null +++ b/src/platform/posix/ec_socket_posix.c @@ -0,0 +1,261 @@ +#include "ec_socket.h" +#include "ec_config.h" + +#include +#include + +#if EC_CONFIG_USE_TLS +#include +#include +#include +#include +#include +#include +#include "ec_cacerts.h" +#endif + +#if defined(EC_PLATFORM_POSIX) + +#include +#include +#include +#include +#include +#include +#include + +struct ec_socket { + int fd; +#if EC_CONFIG_USE_TLS + int tls_active; + mbedtls_ssl_context ssl; + mbedtls_ssl_config ssl_conf; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_x509_crt ca_cert; +#endif +}; + +/* ---- TLS BIO callbacks (send/recv on raw fd) --------------------------- */ +#if EC_CONFIG_USE_TLS + +static int tls_bio_send(void *ctx, const unsigned char *buf, size_t len) +{ + int fd = *(int *)ctx; + ssize_t n = send(fd, buf, len, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return MBEDTLS_ERR_SSL_WANT_WRITE; + return MBEDTLS_ERR_NET_SEND_FAILED; + } + return (int)n; +} + +static int tls_bio_recv(void *ctx, unsigned char *buf, size_t len) +{ + int fd = *(int *)ctx; + ssize_t n = recv(fd, buf, len, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return MBEDTLS_ERR_SSL_WANT_READ; + return MBEDTLS_ERR_NET_RECV_FAILED; + } + if (n == 0) + return MBEDTLS_ERR_SSL_WANT_READ; + return (int)n; +} + +static int tls_handshake(ec_socket_t *sock, const char *host) +{ + mbedtls_ssl_init(&sock->ssl); + mbedtls_ssl_config_init(&sock->ssl_conf); + mbedtls_entropy_init(&sock->entropy); + mbedtls_ctr_drbg_init(&sock->ctr_drbg); + mbedtls_x509_crt_init(&sock->ca_cert); + + /* Seed the DRBG */ + int ret = mbedtls_ctr_drbg_seed(&sock->ctr_drbg, mbedtls_entropy_func, + &sock->entropy, NULL, 0); + if (ret != 0) return ret; + + /* Load embedded CA bundle */ + ret = mbedtls_x509_crt_parse(&sock->ca_cert, + EC_CA_BUNDLE, EC_CA_BUNDLE_LEN); + if (ret != 0) return ret; + + /* Configure TLS client defaults */ + ret = mbedtls_ssl_config_defaults(&sock->ssl_conf, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) return ret; + + mbedtls_ssl_conf_authmode(&sock->ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&sock->ssl_conf, &sock->ca_cert, NULL); + mbedtls_ssl_conf_rng(&sock->ssl_conf, + mbedtls_ctr_drbg_random, &sock->ctr_drbg); + + ret = mbedtls_ssl_setup(&sock->ssl, &sock->ssl_conf); + if (ret != 0) return ret; + + /* Set SNI hostname for virtual hosting */ + ret = mbedtls_ssl_set_hostname(&sock->ssl, host); + if (ret != 0) return ret; + + /* Attach BIO callbacks using raw fd */ + mbedtls_ssl_set_bio(&sock->ssl, &sock->fd, + tls_bio_send, tls_bio_recv, NULL); + + /* Perform handshake */ + while ((ret = mbedtls_ssl_handshake(&sock->ssl)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && + ret != MBEDTLS_ERR_SSL_WANT_WRITE) + return ret; + } + + sock->tls_active = 1; + return 0; +} + +static void tls_cleanup(ec_socket_t *sock) +{ + if (sock->tls_active) + mbedtls_ssl_close_notify(&sock->ssl); + mbedtls_ssl_free(&sock->ssl); + mbedtls_ssl_config_free(&sock->ssl_conf); + mbedtls_ctr_drbg_free(&sock->ctr_drbg); + mbedtls_entropy_free(&sock->entropy); + mbedtls_x509_crt_free(&sock->ca_cert); +} + +#endif /* EC_CONFIG_USE_TLS */ + +ec_socket_t *ec_socket_connect(const char *host, uint16_t port, int use_tls) +{ +#if !EC_CONFIG_USE_TLS + if (use_tls) return NULL; /* TLS not compiled in */ +#endif + + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%u", (unsigned)port); + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo *res = NULL; + if (getaddrinfo(host, port_str, &hints, &res) != 0 || res == NULL) { + return NULL; + } + + int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (fd < 0) { + freeaddrinfo(res); + return NULL; + } + + if (connect(fd, res->ai_addr, res->ai_addrlen) != 0) { + close(fd); + freeaddrinfo(res); + return NULL; + } + + freeaddrinfo(res); + + ec_socket_t *sock = (ec_socket_t *)calloc(1, sizeof(ec_socket_t)); + if (!sock) { + close(fd); + return NULL; + } + sock->fd = fd; + +#if EC_CONFIG_USE_TLS + if (use_tls) { + if (tls_handshake(sock, host) != 0) { + tls_cleanup(sock); + close(fd); + free(sock); + return NULL; + } + } +#endif + + return sock; +} + +int ec_socket_send(ec_socket_t *sock, const void *data, size_t len) +{ + if (!sock) return -1; + +#if EC_CONFIG_USE_TLS + if (sock->tls_active) { + size_t total = 0; + const unsigned char *p = (const unsigned char *)data; + while (total < len) { + int n = mbedtls_ssl_write(&sock->ssl, p + total, len - total); + if (n == MBEDTLS_ERR_SSL_WANT_WRITE) continue; + if (n < 0) return -1; + total += (size_t)n; + } + return (int)total; + } +#endif + + size_t total = 0; + const char *p = (const char *)data; + while (total < len) { + ssize_t n = send(sock->fd, p + total, len - total, 0); + if (n <= 0) return -1; + total += (size_t)n; + } + return (int)total; +} + +int ec_socket_recv(ec_socket_t *sock, void *buf, size_t len, uint32_t timeout_ms) +{ + if (!sock) return -1; + + /* Set recv timeout — applies to both plain and TLS (via BIO callback) */ + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + setsockopt(sock->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + +#if EC_CONFIG_USE_TLS + if (sock->tls_active) { + int n = mbedtls_ssl_read(&sock->ssl, (unsigned char *)buf, len); + if (n == MBEDTLS_ERR_SSL_WANT_READ) + return 0; /* timeout */ + if (n == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || n == 0) + return 0; /* connection closed */ + if (n < 0) return -1; + return n; + } +#endif + + ssize_t n = recv(sock->fd, buf, len, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; /* timeout */ + return -1; + } + return (int)n; +} + +void ec_socket_close(ec_socket_t *sock) +{ + if (!sock) return; + +#if EC_CONFIG_USE_TLS + if (sock->tls_active) + tls_cleanup(sock); +#endif + + close(sock->fd); + free(sock); +} + +#else +#error "ec_socket_posix.c must only be built for EC_PLATFORM=POSIX" +#endif