diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5e7256f2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,179 @@ +# ocre-runtime — notes for Claude + +**This repo is a fork of upstream [project-ocre/ocre-runtime](https://github.com/project-ocre/ocre-runtime).** +`README.md` is mostly upstream's own (kept intact for attribution/links) — +this fork's own additions are called out in a note near its top and detailed +in `docs/samples/fetch.md`, which is the canonical, up-to-date reference for +everything below (feature list, build/flash instructions, full AssemblyScript +API table). It's the Ocre container runtime itself: a WAMR-based WASM runtime +on Zephyr, plus the `src/samples/fetch` sample firmware that's been the main +thing developed and tested against real hardware (Seeed XIAO ESP32-C6). + +This repo is meant to be paired with two separate repos developed alongside +it: **`ocre-as`** (a Node CLI: builds AssemblyScript to WASM, hosts it over +HTTP for this repo's `fetch` sample to pull, and live-monitors device memory +over serial) and **`gpio-demo`** (an example AssemblyScript project using +`ocre-as` and this fork's GPIO/ADC/HTTP API). If working across more than one +of these at once, check whether a shared local workspace-level `CLAUDE.md` +exists one level up — it may have cross-repo operational notes (hardcoded +WiFi/IP config that must agree across repos, serial port gotchas, etc.) that +don't belong in any single repo. + +## Architecture in one paragraph + +`src/ocre/` is the container lifecycle layer (context/container create, +start, destroy). `src/runtime/wamr-wasip1/wamr.c` is the WAMR-specific +runtime vtable implementation — this is where a module gets instantiated, +executed, and torn down. `src/runtime/wamr-wasip1/ocre_api/` holds every +native function exposed to WASM containers, one subdirectory per capability +(`ocre_gpio`, `ocre_adc`, `ocre_http` [inbound server], `ocre_http_client` +[outbound client], `ocre_timers`, `ocre_sensors`, `ocre_messaging`, +`ocre_print`), all registered in `ocre_api/ocre_api.c`'s `ocre_api_table[]`. +Each entry there needs a matching mirror in the separate `ocre-as` repo's +`lib/bindings/ocre.ts` (see that repo's own CLAUDE.md) — nothing enforces +this automatically. + +**Reactive execution model**: a container's `main()` runs once. If it also +exports `loop` and/or `onRequest`, the native side (`wamr.c` + +`ocre_api/ocre_dispatch/`) keeps the instance alive afterward instead of +tearing it down, calling those exports on its own schedule — `loop()` on a +timer (`CONFIG_OCRE_LOOP_INTERVAL_MS`), `onRequest()` per HTTP request — all +serialized through a mutex since only one thread may execute inside a given +WASM instance at a time. The thread that actually does this (`loop_scheduler_thread` +in `ocre_dispatch.c`) is the one real consumer of +`CONFIG_DYNAMIC_THREAD_STACK_SIZE` in this whole app — see below. + +## Building/flashing + +```sh +source ~/ocre/.venv/bin/activate +west build -d -b xiao_esp32c6/esp32c6/hpcore -s src/samples/fetch/zephyr +west flash -d +``` + +Always pass `-b`/`-s` explicitly for a fresh build dir — West can otherwise +misconfigure against the top-level library CMakeLists instead of the sample. + +## Feature history (what's actually been built here) + +Chronologically, on top of upstream Ocre: + +1. **Reactive execution model** (`wamr.c`, `ocre_api/ocre_dispatch/`) — see + above. Replaced the old "container loops forever in `main()`" model. +2. **Generic HTTP request/response bridge** (`ocre_api/ocre_http/`) — + replaced fixed `/`+`/button` endpoints with a single fallback resource + that hands every request to the container's `onRequest()`. +3. **ADC support** (`ocre_api/ocre_adc/`) — named channels via the same + `zephyr,user` io-channels devicetree convention used for GPIO aliases. +4. **`ocre_print`** (`ocre_api/ocre_print/`) — serial print + AssemblyScript's + required `env.abort` hook. +5. **Hash-based update checks streamed to flash** (`main.c`, `ocre-as/lib/host.js`) — + `/getHash` (sha256 digest) checked first, `/getCode` only fetched on + change, written straight to a temp file rather than buffered in RAM. +6. **WiFi power-save disabled on connect** (`main.c`, `wifi_disable_power_save()`) — + mitigates (but does not fully explain) an intermittent full network stall. +7. **`"ocre:api"` capability now passed on container create** (`main.c`, + `start_container()`) — makes per-container resource cleanup (GPIO release + etc.) actually run on teardown; previously silently never ran. +8. **Updates apply via full reboot, not in-place container swap** (`main.c`, + `check_for_update()`) — see the WiFi-stall bullet below for why. +9. **Native LED blink** (`main.c`, `native_led_blink()`) — blinks the board's + built-in LED (devicetree `led0` alias, driven directly via `gpio_dt_spec`, + independent of any container's own GPIO usage) 3x on every boot and 3x + right before the reboot that follows a successful update, purely as a + visual "something just happened" signal. Left off afterward for the + running container's own AssemblyScript to control. +10. **Outbound HTTP client API** (`ocre_api/ocre_http_client/`, new + `CONFIG_OCRE_HTTP_CLIENT` Kconfig) — `ocre_http_get(host, port, path)`, + the counterpart to the inbound server bridge, letting container code make + its own blocking HTTP GET requests. Demonstrated in `gpio-demo` by + polling `/getHash` from `loop()` every ~10s and showing it on the page. +11. **Device-side memory stats for `ocre-as stats`** (`main.c`, + `stats_thread_fn`, always-on via `K_THREAD_DEFINE`) — prints + `OCRE_STATS,,,` once a second for `heap` (libc heap via + `malloc_runtime_stats_get`) and `flash` (LittleFS via `fs_statvfs("/lfs", ...)`). +12. **`heap_size=0` fix in `wasm_runtime_instantiate()`** (`wamr.c`) — see + below; recovered ~64KB of native heap per container. + +### Tried and reverted — don't redo without re-reading why + +- **In-process container hot-swap + an AssemblyScript `onDestroy()` handler.** + Implemented following a 5-step recipe (terminate → deinstantiate → unload → + load new bytecode → instantiate) plus vtable/dispatch plumbing for + `onDestroy`. Hit a real bug: the implementation loaded the new image into a + heap buffer *before* freeing the old (still-resident) instance, causing + `ENOMEM` on the very first swap — this directly contradicted the article's + own step ordering and was about to be fixed when the user reconsidered the + whole approach and asked for a full revert instead. Fully reverted (verified + via grep for `hot_swap`/`onDestroy` returning zero hits); updates now use + the full-reboot approach (item 8 above) instead. If hot-swap is revisited, + fix the load-before-free ordering bug first. +- **A WAMR-based debug server for VS Code source-level debugging.** Explored + in depth: `WASM_ENABLE_DEBUG_INTERP` requires `WAMR_BUILD_THREAD_MGR=1` and + auto-disables the fast interpreter; the Zephyr platform port already + implements every `os_socket_*` primitive `debug-engine`/`gdbserver.c` need, + so it's plausible to wire up. Not implemented — the user said it was "too + expensive right now" mid-investigation. No code changes were made for this; + don't assume any debug-server plumbing exists. + +### Known open issue, deliberately not chased further + +A small heap leak of ~60 bytes per container create/destroy cycle exists, +confirmed independent of the `"ocre:api"` capability fix (measured identical +magnitude with and without it). Root cause not found; most likely inside +WAMR's own `wasm_runtime_instantiate`/`deinstantiate` internals. Too small to +matter at any realistic swap frequency (1000+ swaps to become significant) — +noted here so it isn't mistaken for something introduced by later changes. + +## Hard-won facts about this specific board/runtime combo + +- **`wasm_runtime_instantiate()`'s `heap_size` param (`wamr.c`) must stay 0 + for AssemblyScript containers.** That parameter is WAMR's own "app heap" + for languages needing `malloc()` inside the sandbox (C/Rust via WASI) — AS + never uses it. Worse than just wasting the requested amount: WAMR appends + it onto the end of linear memory and rounds up to the next 64KB page + boundary, so even a small nonzero value (e.g. 8192) can silently cost a + **full extra 64KB page** of native heap per container. Confirmed by + measurement: dropping 8192→0 took heap usage from 99.5% to 67.7% on this + board, not the ~4% a naive estimate would predict. +- **`CONFIG_DYNAMIC_THREAD_STACK_SIZE` (`src/samples/fetch/zephyr/prj.conf`) + only governs one thread in this app**: `ocre_dispatch.c`'s + `loop_scheduler_thread` (created via plain `pthread_create()`). Everything + else — WiFi, the HTTP server, the net stack — has its own dedicated, + separately-Kconfig'd stack. Measured that one thread hitting ~35% usage + under light HTTP testing; not safe to trim aggressively without real + profiling first (enable `CONFIG_THREAD_ANALYZER` + `CONFIG_THREAD_ANALYZER_AUTO` + + `CONFIG_THREAD_NAME` temporarily to get real per-thread high-water-marks — + removed from this tree after use, re-add if investigating further). +- **printk over this board's console can genuinely busy-wait.** The ESP32-C6 + native USB-Serial/JTAG driver (`zephyr/drivers/serial/serial_esp32_usb.c`, + `serial_esp32_usb_poll_out`) spins for up to 50ms per byte if a host was + recently draining the TX FIFO but it's momentarily full. Frequent/heavy + logging while something is actively reading (e.g. a monitor tool that + clears and redraws the whole screen on every line) can cost real device + CPU time; if nothing is attached at all it degrades gracefully instead + (near-instant no-op, data just dropped). +- **`"ocre:api"` must be passed via `ocre_container_args.capabilities`** for + per-container resource cleanup (e.g. GPIO pin release) to actually run on + teardown — easy to silently omit, and the failure mode (leaked + registrations across container swaps) is quiet. +- **The recurring full bidirectional WiFi stall was never conclusively + root-caused** despite extensive investigation (interface/IP loss, power-save + alone, container-teardown CPU cost, and connection-pool exhaustion were all + ruled out with real hardware evidence). The shipped mitigation: + `check_for_update()` in `main.c` reboots the whole board + (`sys_reboot(SYS_REBOOT_COLD)`) after installing a new image rather than + swapping the container in-process, which self-heals a stuck WiFi stack as + a side effect of the normal update path. If this stall resurfaces and + someone wants to chase the actual root cause again, that investigation + history (power-save, connection pools, teardown timing) doesn't need to be + repeated — it's already ruled out. +- **The device's serial port path changes across reflashes/resets** — always + `ls /dev/cu.usbmodem*` fresh rather than reusing a previously-known path. +- **The separate `ocre-as` repo's `host` command must be running** for this + firmware's update-fetch cycle to succeed — it's a long-running Node process + on the dev machine and dies silently sometimes. +- **WiFi credentials and the fetch server address/port are hardcoded** in + this repo (`wifi_credentials.h`, `FETCH_SERVER_ADDR`/`FETCH_SERVER_PORT` in + `main.c`) and must agree with wherever the `ocre-as` repo's `host` command + is actually running (its own machine's LAN IP, port 8080 by default). diff --git a/README.md b/README.md index 03596b31..c6d39d0a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ SPDX-License-Identifier: Apache-2.0 --> [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9691/badge)](https://www.bestpractices.dev/projects/9691) [![slack](https://img.shields.io/badge/slack-ocre-brightgreen.svg?logo=slack)](https://lfedge.slack.com/archives/C07F190CC3X) +> **This is a fork** of upstream Ocre adding a `fetch` sample (WiFi-connected, +> pulls AssemblyScript containers from a network URL and reboots to run +> updates) plus supporting runtime changes: a reactive `loop()`/`onRequest()` +> execution model, a generic HTTP request/response bridge, an outbound HTTP +> client, ADC support, and several memory/reliability fixes. See +> [`docs/samples/fetch.md`](docs/samples/fetch.md) for the full list and the +> `ocre-as` CLI (separate repo) and `gpio-demo` example project (separate +> repo) it's meant to be paired with. + Powered by WebAssembly, the Ocre runtime is available in both Zephyr and Linux variants and supports OCI-like application containers in a footprint up to 2000x lighter than traditional container runtimes like Docker. With Ocre, developers can run the exact same application container binaries written in choice of programming language on both the Linux and Zephyr-based runtime versions spanning CPU and MCU-based devices. @@ -192,6 +201,7 @@ The officially supported sample applications are listed below: | `mini` | A simple "Hello World" application using minimal resources | | `demo` | A more featured sample application | | `supervisor` | Interactive shell control | +| `fetch` | WiFi-connected: fetches AssemblyScript containers from a network URL and runs them reactively (see [`docs/samples/fetch.md`](docs/samples/fetch.md)) | The intended usage of the samples are: diff --git a/docs/samples/fetch.md b/docs/samples/fetch.md new file mode 100644 index 00000000..eb70a89e --- /dev/null +++ b/docs/samples/fetch.md @@ -0,0 +1,231 @@ + + +# fetch sample + +This sample connects to WiFi on startup, keeps a fetched container running +reactively (background `loop()` plus an HTTP server dispatching to +`onRequest()`), and periodically checks a URL on your machine for updated +code -- fetching and installing it, then rebooting to run it, only when it's +actually changed. If nothing is reachable (no WiFi, server down, no new +code), it just keeps running whatever it already has cached from a previous +successful fetch, including across reboots. + +This sample -- and a set of runtime changes it depends on (below) -- is what +this fork adds on top of upstream [Ocre](https://github.com/project-ocre/ocre-runtime). +It's meant to be paired with the `ocre-as` CLI (a separate repo): write an +AssemblyScript container (see the `gpio-demo` repo for a full example), +`ocre-as build` it, `ocre-as host` it, and this sample fetches and runs it. + +The application logic (`main.c`) is written entirely against portable Zephyr +networking APIs (`net_mgmt`, POSIX sockets, `http_client`, `http_server`) -- +nothing ESP32-specific. Only the WiFi *driver* Kconfig differs per board, in +`zephyr/boards/.conf`. + +## Configuration + +Currently hardcoded (see the source for where to change these before using +this sample outside of local development): + +- WiFi SSID/password: `wifi_credentials.h` (create it if missing) +- Fetch server: `FETCH_SERVER_ADDR` / `FETCH_SERVER_PORT` in `main.c` -- + must be your dev machine's LAN IP and whatever port `ocre-as host` serves on + (default 8080) + +## Building and running + +```sh +west build -p always -b xiao_esp32c6/esp32c6/hpcore src/samples/fetch/zephyr +west flash +``` + +Tested end to end (WiFi, fetch, GPIO, ADC, HTTP, outbound HTTP) on the Seeed +XIAO ESP32-C6. Only that board has a board-specific WiFi driver `.conf` today +(`zephyr/boards/xiao_esp32c6_esp32c6_hpcore.conf`, which just sets +`CONFIG_WIFI_ESP32=y`). Porting to another WiFi-capable board should only +require an equivalent `boards/.conf` -- `main.c` itself needs no +changes. GPIO/ADC support on a new board additionally needs that board's +`led0`/`sw0`-style devicetree aliases (usually already defined) and a +`boards/.overlay` with a `zephyr,user` `io-channels` mapping for ADC +(see `boards/xiao_esp32c6_esp32c6_hpcore.overlay`). + +Watch it boot over serial (macOS port names look like `/dev/cu.usbmodem*`, +and change across reflashes/resets -- re-run `ls /dev/cu.usbmodem*` if a +previously-working path stops connecting): + +```sh +ls /dev/cu.usbmodem* +screen /dev/cu.usbmodem101 115200 # Ctrl-A then k to exit +``` + +## Behavior + +- On boot: if a previously fetched image is cached + (`/lfs/ocre/images/current.wasm`), it starts running immediately, without + waiting on the network. The board's built-in LED blinks 3x at this point + (see "LED blink" below). +- Also on boot (after a short delay for WiFi association + DHCP) and then + every ~20 seconds: fetches `/getHash` (a sha256 digest of whatever + `/getCode` currently serves -- a few dozen bytes) and compares it to the + hash of the code currently installed. + - Unchanged: nothing happens, no log spam. + - Changed: `/getCode` is fetched, streamed straight to a temp file on + flash (never buffered whole in RAM), and renamed over the installed + image on success. The LED blinks 3x, then the board reboots + (`sys_reboot(SYS_REBOOT_COLD)`) to run it -- see "Why reboot" below. + - Any failure (no WiFi, connection refused, timeout, non-200 status): + the currently running container is left completely untouched, and a + few retries with a short delay are attempted before giving up until the + next periodic check. + +## What this fork changes/adds on top of upstream Ocre + +- **Reactive execution model** (`src/runtime/wamr-wasip1/wamr.c`, + `ocre_api/ocre_dispatch/`): a container's `main()` runs once; if it also + exports `loop` and/or `onRequest`, the native side keeps the instance alive + afterward and calls those exports on its own schedule instead of tearing + it down -- `loop()` on a timer (`CONFIG_OCRE_LOOP_INTERVAL_MS`, default + 100ms), `onRequest()` per HTTP request, serialized through a mutex since + only one thread may execute inside a given WASM instance at a time. +- **Generic HTTP request/response bridge** (`ocre_api/ocre_http/`): a single + native fallback HTTP resource catches every request regardless of + path/method and dispatches it to the active container's + `onRequest(requestId)` -- all routing lives in container code. +- **Outbound HTTP client** (`ocre_api/ocre_http_client/`, new + `CONFIG_OCRE_HTTP_CLIENT`): `ocre_http_get(host, port, path)`, the + counterpart to the inbound bridge, for container code that wants to make + its own HTTP requests. Demonstrated in `gpio-demo` by polling `/getHash` + from `loop()` every ~10s. +- **ADC support** (`ocre_api/ocre_adc/`, new): named channels ("a0".."a3") + resolved via the same `zephyr,user` `io-channels` devicetree convention + Zephyr's own `samples/drivers/adc/adc_dt` uses, pre-scaled to 0-255. +- **`ocre_print`** (`ocre_api/ocre_print/`, new): serial print + backs + AssemblyScript's required `env.abort` runtime hook. +- **Hash-based update checks streamed to flash** (`main.c`, + `ocre-as/lib/host.js`): see "Behavior" above -- this replaced fetching and + diffing the whole image in RAM on every check. +- **Updates apply via a full reboot, not an in-place container swap** + (`main.c`, `check_for_update()`): chosen after extensive investigation into + an intermittent full bidirectional WiFi stall (station stays associated, + no disconnect event fires, but no traffic passes in either direction until + a reset) that recurred regardless of in-process container-swap machinery -- + interface/IP loss, WiFi power-save, container-teardown CPU cost, and + connection-pool exhaustion were all ruled out as the direct cause via + targeted hardware testing. Rather than continue chasing the root cause, + updates now make a full restart part of the normal update path, which + self-heals a stuck WiFi stack as a side effect. Needs `CONFIG_REBOOT=y`. + An in-process hot-swap approach (terminate/deinstantiate/unload/reload/ + reinstantiate, plus an AssemblyScript `onDestroy()` handler) was + implemented and then fully reverted in favor of this -- if revisiting + hot-swap, note the implementation hit a real `ENOMEM` bug from loading the + new image into RAM before freeing the old instance. +- **WiFi power-save disabled on connect** (`main.c`, + `wifi_disable_power_save()`): mitigates (but does not fully explain) the + stall mentioned above. +- **`"ocre:api"` capability passed on container create** (`main.c`, + `start_container()`): makes per-container resource cleanup (e.g. GPIO pin + release) actually run on teardown -- previously silently never ran. A + separate, unrelated ~60 bytes/swap heap leak remains (confirmed unaffected + by this fix, most likely inside WAMR's own instantiate/deinstantiate + internals); too small to matter at any realistic swap frequency. +- **LED blink** (`main.c`, `native_led_blink()`): the board's built-in LED + (devicetree `led0` alias, driven directly, independent of any container's + own GPIO usage) blinks 3x on every boot and 3x right before an + update-triggered reboot, as a visual "something just happened" signal. + Left off afterward for the running container's own AssemblyScript to + control via `ocre_gpio`. +- **`wasm_runtime_instantiate()`'s `heap_size` param set to 0** (`wamr.c`): + that parameter is WAMR's own "app heap" for languages needing `malloc()` + inside the sandbox (C/Rust via WASI) -- AssemblyScript never uses it, and + WAMR appends it onto the end of linear memory rounded up to the next 64KB + page boundary, so a nonzero value here can silently cost a full extra 64KB + page of native heap per container. Recovered ~64KB per container on this + board (heap usage dropped from 99.5% to 67.7% while a container was + running). +- **Live device memory stats** (`main.c`, `stats_thread_fn`, always-on via + `K_THREAD_DEFINE`): prints `OCRE_STATS,,,` once a + second for `heap` and `flash`, consumed by `ocre-as stats ` for + a live terminal table. A WAMR debug-server (source-level VS Code + debugging via `WASM_ENABLE_DEBUG_INTERP`) was investigated -- the Zephyr + platform port has every socket primitive it needs -- but not implemented; + it was judged too expensive to pursue for now. +- **Memory tuning**: raised `CONFIG_HTTP_SERVER_MAX_CLIENTS`, + `CONFIG_MAX_PTHREAD_MUTEX_COUNT`/`_COND_COUNT` (Zephyr's POSIX layer hands + these out from small fixed pools; the dispatch/HTTP rework needs more than + the default 5), and `CONFIG_NET_MAX_CONN`/`CONFIG_NET_MAX_CONTEXTS`. + +### A note on `CONFIG_DYNAMIC_THREAD_STACK_SIZE` + +If you're tempted to trim this (`prj.conf`, currently 8192) for more heap: +measure first. It turns out to govern only one thread in this whole app -- +`ocre_dispatch.c`'s `loop_scheduler_thread`, the thread that actually calls +into WAMR to run a container's `loop()`/`onRequest()` -- everything else +(WiFi, the HTTP server, the net stack) has its own separately-Kconfig'd +stack. That one thread measured ~35% usage under light HTTP testing, so +there isn't much safe margin, and a stack overflow is a worse failure mode +than the heap pressure you'd be trying to fix. To re-measure, temporarily add +`CONFIG_THREAD_ANALYZER=y`, `CONFIG_THREAD_ANALYZER_AUTO=y`, and +`CONFIG_THREAD_NAME=y` to `prj.conf` for real per-thread high-water-marks. + +## AssemblyScript API reference + +Everything below is available via `import { ... } from "./ocre";` after +`ocre-as build` generates `assembly/ocre.ts`. Functions requiring a device +Kconfig option are noted; all of them need to be enabled in this sample's +`prj.conf` (they already are). + +| Function | Description | +| --- | --- | +| `ocre_print(msg: string): i32` | Print a line to the device's serial console. | +| `ocre_sleep(ms: i32): i32` | Sleep for the given duration (blocking -- avoid in `loop()`/`onRequest()`). | +| `ocre_uname(): OcreUtsname \| null` | System/version info (sysname, release, machine, ...). | +| **HTTP server** (`CONFIG_OCRE_HTTP_SERVER`) | | +| `ocre_http_get_method(requestId: i32): i32` | Request method (see `OcreHttpMethod`). | +| `ocre_http_get_path(requestId: i32): string` | Request path, no query string. | +| `ocre_http_get_query(requestId: i32): string` | Raw query string (no leading `?`). | +| `ocre_http_get_header(requestId: i32, name: string): string` | Header value, or `""` (needs `CONFIG_HTTP_SERVER_CAPTURE_HEADERS`). | +| `ocre_http_get_body(requestId: i32): string` | Request body as text. | +| `ocre_http_respond(requestId: i32, status: i32, contentType?: string, body?: string): i32` | Answer a request captured by `onRequest()`. Call once per request, before returning. | +| **HTTP client** (`CONFIG_OCRE_HTTP_CLIENT`) | | +| `ocre_http_get(host: string, port: i32, path: string): string` | Blocking outbound GET to `http://host:port/path`. Returns the body on 200, `""` on any failure. Stalls this instance's `loop()`/`onRequest()` dispatch for the call's duration. | +| **GPIO** (`CONFIG_OCRE_GPIO`) | | +| `ocre_gpio_init(): i32` | Initialize the GPIO subsystem. Call once before use. | +| `ocre_gpio_configure_by_name(name: string, direction: i32): i32` | Configure a named pin (see `OcreGpioDirection`). | +| `ocre_gpio_set_by_name(name: string, state: i32): i32` | Drive a named output pin (see `OcreGpioState`). | +| `ocre_gpio_get_by_name(name: string): i32` | Read a named pin. | +| `ocre_gpio_toggle_by_name(name: string): i32` | Toggle a named output pin. | +| `ocre_gpio_register_callback_by_name` / `unregister...` | Interrupt-driven input; events arrive via `ocre_get_event()`. | +| `ocre_gpio_configure` / `_pin_set` / `_pin_get` / `_pin_toggle` / `_register_callback` / `_unregister_callback` | Same operations addressed by raw `(port, pin)` instead of a name. | +| **ADC** (`CONFIG_OCRE_ADC`) | | +| `ocre_adc_init(): i32` | Resolve the board's named analog channels. Call once before use. | +| `ocre_adc_read_by_name(name: string): i32` | Read a named channel, pre-scaled to 0-255. | +| **Timers** (`CONFIG_OCRE_TIMER`) | | +| `ocre_timer_create(id: i32): i32` / `_start(id, intervalMs, isPeriodic)` / `_stop(id)` / `_delete(id)` / `_get_remaining(id)` | Native timers; expiry arrives via `ocre_get_event()`. | +| **Sensors** (`CONFIG_OCRE_SENSORS`) | | +| `ocre_sensors_init()` / `_discover()` | Initialize and enumerate sensors. | +| `ocre_sensors_open_by_name` / `_get_handle_by_name` / `_get_channel_count_by_name` / `_get_channel_type_by_name` / `_read_by_name` | Named-sensor variants (and `..._open`, `_get_handle`, etc. by numeric ID). | +| **Container messaging** (`CONFIG_OCRE_CONTAINER_MESSAGING`) | | +| `ocre_publish_message(topic, contentType, payload: ArrayBuffer): i32` | Publish to a topic. | +| `ocre_subscribe_message(topic: string): i32` | Subscribe; messages arrive via `ocre_get_event()`. | +| `ocre_messaging_free_module_event_data(...)` | Free native buffers backing a messaging event after handling it. | +| **Events** (shared by GPIO/timers/sensors/messaging) | | +| `ocre_get_event(): OcreEvent \| null` | Pop the next queued event for this container, if any (see `OcreResourceType`). | +| `ocre_register_dispatcher(resourceType: i32, functionName: string): i32` | Register an exported function name as a resource type's dispatcher. | + +HTTP does **not** use the event queue -- `onRequest()` is called directly, +synchronously, per request. + +**Sync requirement**: every native function above has a hand-maintained +mirror in `ocre-as`'s `lib/bindings/ocre.ts` (the `@external` import + a +string-marshaling wrapper). Adding a new function to +`ocre_api/ocre_api.c`'s `ocre_api_table[]` without a matching entry there +means AssemblyScript code simply can't call it -- nothing enforces this +automatically. + +## Monitoring + +`ocre-as stats ` (from the `ocre-as` repo) shows a live-updating +heap/flash usage table read from this sample's serial output. See that +repo's README for usage. diff --git a/src/runtime/wamr-wasip1/ocre_api/CMakeLists.txt b/src/runtime/wamr-wasip1/ocre_api/CMakeLists.txt index 2670838c..ceecee31 100644 --- a/src/runtime/wamr-wasip1/ocre_api/CMakeLists.txt +++ b/src/runtime/wamr-wasip1/ocre_api/CMakeLists.txt @@ -9,8 +9,10 @@ target_sources(OcreRuntimeAPI PRIVATE ocre_api.c ocre_common.c + ocre_dispatch/ocre_dispatch.c ocre_timers/ocre_timer.c ocre_messaging/ocre_messaging.c + ocre_print/ocre_print.c utils/strlcat.c core/core_eventq.c core/core_misc.c @@ -26,6 +28,28 @@ if (CONFIG_OCRE_GPIO) ) endif() +if (CONFIG_OCRE_ADC) + target_sources(OcreRuntimeAPI + PRIVATE + ocre_adc/ocre_adc.c + ) +endif() + +if (CONFIG_OCRE_HTTP_SERVER) + target_sources(OcreRuntimeAPI + PRIVATE + ocre_http/ocre_http.c + ) + zephyr_linker_sources(SECTIONS ocre_http/sections-rom.ld) +endif() + +if (CONFIG_OCRE_HTTP_CLIENT) + target_sources(OcreRuntimeAPI + PRIVATE + ocre_http_client/ocre_http_client.c + ) +endif() + target_link_libraries(OcreRuntimeAPI PUBLIC OcrePlatform diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.c b/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.c new file mode 100644 index 00000000..09bb8f32 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.c @@ -0,0 +1,162 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include + +#include + +#include "ocre_adc.h" + +LOG_MODULE_REGISTER(ocre_adc, CONFIG_OCRE_LOG_LEVEL); + +#ifndef CONFIG_OCRE_ADC_MAX_CHANNELS +#define CONFIG_OCRE_ADC_MAX_CHANNELS 4 +#endif + +#ifndef CONFIG_OCRE_ADC_VREF_MV +#define CONFIG_OCRE_ADC_VREF_MV 3300 +#endif + +#define ZEPHYR_USER_NODE DT_PATH(zephyr_user) + +typedef struct { + const char *name; + struct adc_dt_spec spec; + bool ready; +} ocre_adc_channel_t; + +static ocre_adc_channel_t adc_channels[CONFIG_OCRE_ADC_MAX_CHANNELS]; +static int adc_channel_count; +static bool adc_system_initialized; + +/* ADC_DT_SPEC_GET_BY_NAME_OR() resolves entirely at compile time (it's a + * COND_CODE_1 over a devicetree existence check), so it's always safe to + * call even when "name" isn't defined on this board -- spec.dev is simply + * NULL in that case, checked at runtime below. Only known, fixed channel + * names are supported (mirrors ocre_gpio's by-name aliases); which of them + * actually exist depends entirely on the board's devicetree overlay. */ +#define ADD_ADC_CHANNEL_IF_PRESENT(name_str, token) \ + do { \ + if (adc_channel_count < CONFIG_OCRE_ADC_MAX_CHANNELS) { \ + struct adc_dt_spec spec = \ + ADC_DT_SPEC_GET_BY_NAME_OR(ZEPHYR_USER_NODE, token, ((struct adc_dt_spec){0})); \ + if (spec.dev) { \ + if (!adc_is_ready_dt(&spec)) { \ + LOG_ERR("ADC device for channel '%s' not ready", name_str); \ + } else if (adc_channel_setup_dt(&spec) != 0) { \ + LOG_ERR("Failed to configure ADC channel '%s'", name_str); \ + } else { \ + adc_channels[adc_channel_count].name = name_str; \ + adc_channels[adc_channel_count].spec = spec; \ + adc_channels[adc_channel_count].ready = true; \ + adc_channel_count++; \ + LOG_INF("ADC channel '%s' ready (channel_id=%d)", name_str, \ + spec.channel_id); \ + } \ + } \ + } \ + } while (0) + +int ocre_adc_init(void) +{ + if (adc_system_initialized) { + LOG_INF("ADC system already initialized"); + return 0; + } + + ADD_ADC_CHANNEL_IF_PRESENT("a0", a0); + ADD_ADC_CHANNEL_IF_PRESENT("a1", a1); + ADD_ADC_CHANNEL_IF_PRESENT("a2", a2); + ADD_ADC_CHANNEL_IF_PRESENT("a3", a3); + + if (adc_channel_count == 0) { + LOG_ERR("No ADC channels available -- does this board's devicetree define " + "a zephyr,user io-channels entry?"); + return -ENODEV; + } + + adc_system_initialized = true; + LOG_INF("ADC system initialized, %d channel(s) available", adc_channel_count); + return 0; +} + +int ocre_adc_read_by_name(const char *name) +{ + if (!name || !adc_system_initialized) { + LOG_ERR("ADC not initialized or invalid name"); + return -EINVAL; + } + + ocre_adc_channel_t *chan = NULL; + + for (int i = 0; i < adc_channel_count; i++) { + if (adc_channels[i].ready && strcmp(adc_channels[i].name, name) == 0) { + chan = &adc_channels[i]; + break; + } + } + + if (!chan) { + LOG_ERR("Unknown or unavailable ADC channel '%s'", name); + return -ENODEV; + } + + int16_t raw; + struct adc_sequence sequence = { + .buffer = &raw, + .buffer_size = sizeof(raw), + }; + + int ret = adc_sequence_init_dt(&chan->spec, &sequence); + if (ret != 0) { + LOG_ERR("Failed to init ADC sequence for '%s': %d", name, ret); + return ret; + } + + ret = adc_read_dt(&chan->spec, &sequence); + if (ret != 0) { + LOG_ERR("Failed to read ADC channel '%s': %d", name, ret); + return ret; + } + + int32_t val_mv = raw; + + if (adc_raw_to_millivolts_dt(&chan->spec, &val_mv) == 0) { + /* Reference voltage/gain known from devicetree: scale the + * calibrated millivolt reading against the configured full-scale + * range. */ + int scaled = (val_mv * 255) / CONFIG_OCRE_ADC_VREF_MV; + return CLAMP(scaled, 0, 255); + } + + /* Reference voltage not resolvable (e.g. ADC_REF_INTERNAL without a + * devicetree-provided zephyr,vref-mv) -- scale the raw code directly + * against the channel's configured resolution instead. */ + int max_raw = (1 << chan->spec.resolution) - 1; + int scaled = ((int)raw * 255) / (max_raw > 0 ? max_raw : 1); + return CLAMP(scaled, 0, 255); +} + +int ocre_adc_wasm_init(wasm_exec_env_t exec_env) +{ + return ocre_adc_init(); +} + +int ocre_adc_wasm_read_by_name(wasm_exec_env_t exec_env, const char *name) +{ + if (!name) { + LOG_ERR("Invalid name parameter"); + return -EINVAL; + } + + return ocre_adc_read_by_name(name); +} diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.h b/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.h new file mode 100644 index 00000000..3d282bf3 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_adc/ocre_adc.h @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OCRE_ADC_H +#define OCRE_ADC_H + +#include + +/** + * Portable analog input, following the same devicetree "zephyr,user" + + * io-channels convention as Zephyr's own samples/drivers/adc/adc_dt sample + * (see that sample for the full pattern this mirrors). A board makes a + * channel available under a name (e.g. "a0") via a devicetree overlay: + * + * / { + * zephyr,user { + * io-channels = <&adc0 0>; + * io-channel-names = "a0"; + * }; + * }; + * + * This native API only knows about a fixed set of channel names + * ("a0".."a3", matching common Arduino-style board silkscreens); which of + * them actually resolve depends entirely on what the board's devicetree + * defines -- the C code here is unchanged across boards. Values are + * returned pre-scaled to 0-255 (see CONFIG_OCRE_ADC_VREF_MV) so containers + * don't need to know the channel's resolution or reference voltage. + * + * @return 0 on success, negative error code on failure. + */ +int ocre_adc_init(void); + +/** + * @brief Reads the named analog input channel, scaled to 0-255. + * + * @param name Channel name (see ocre_adc_init()). + * @return 0-255 on success, negative error code on failure (e.g. unknown + * channel name, or the channel isn't defined on this board). + */ +int ocre_adc_read_by_name(const char *name); + +int ocre_adc_wasm_init(wasm_exec_env_t exec_env); +int ocre_adc_wasm_read_by_name(wasm_exec_env_t exec_env, const char *name); + +#endif /* OCRE_ADC_H */ diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_api.c b/src/runtime/wamr-wasip1/ocre_api/ocre_api.c index 5377064a..0b61c14c 100644 --- a/src/runtime/wamr-wasip1/ocre_api/ocre_api.c +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_api.c @@ -17,9 +17,18 @@ #include "ocre_api.h" #include "utils/strlcat.h" +#include "ocre_print/ocre_print.h" #include +#ifdef CONFIG_OCRE_HTTP_SERVER +#include "ocre_http/ocre_http.h" +#endif + +#ifdef CONFIG_OCRE_HTTP_CLIENT +#include "ocre_http_client/ocre_http_client.h" +#endif + #ifdef CONFIG_OCRE_TIMER #include "ocre_timers/ocre_timer.h" #endif @@ -37,6 +46,10 @@ #include "ocre_gpio/ocre_gpio.h" #endif +#ifdef CONFIG_OCRE_ADC +#include "ocre_adc/ocre_adc.h" +#endif + #ifdef CONFIG_OCRE_CONTAINER_MESSAGING #include "ocre_messaging/ocre_messaging.h" #endif @@ -93,7 +106,22 @@ int _ocre_posix_uname(wasm_exec_env_t exec_env, struct _ocre_posix_utsname *name int ocre_sleep(wasm_exec_env_t exec_env, int milliseconds) { - usleep(milliseconds * 1000); + if (milliseconds <= 0) { + return 0; + } + + /* POSIX usleep() rejects any value >= 1 second (returns -1/EINVAL + * instantly instead of sleeping), so chunk the delay into sub-second + * calls to support arbitrary durations. */ + while (milliseconds >= 1000) { + usleep(999000); + milliseconds -= 999; + } + + if (milliseconds > 0) { + usleep((unsigned int)milliseconds * 1000); + } + return 0; } @@ -101,6 +129,22 @@ int ocre_sleep(wasm_exec_env_t exec_env, int milliseconds) NativeSymbol ocre_api_table[] = { {"uname", _ocre_posix_uname, "(*)i", NULL}, {"ocre_sleep", ocre_sleep, "(i)i", NULL}, + {"ocre_print", ocre_print_wasm, "($)i", NULL}, + /* Backs AssemblyScript-compiled modules' "env.abort" import. Void + * return: no trailing signature char (WAMR only checks a return char + * when the wasm import itself declares a result). */ + {"abort", ocre_abort_wasm, "($$ii)", NULL}, +#ifdef CONFIG_OCRE_HTTP_SERVER + {"ocre_http_get_method", ocre_http_get_method_wasm, "(i)i", NULL}, + {"ocre_http_get_path", ocre_http_get_path_wasm, "(i*~)i", NULL}, + {"ocre_http_get_query", ocre_http_get_query_wasm, "(i*~)i", NULL}, + {"ocre_http_get_header", ocre_http_get_header_wasm, "(i$*~)i", NULL}, + {"ocre_http_get_body", ocre_http_get_body_wasm, "(i*~)i", NULL}, + {"ocre_http_respond", ocre_http_respond_wasm, "(ii$$)i", NULL}, +#endif +#ifdef CONFIG_OCRE_HTTP_CLIENT + {"ocre_http_get", ocre_http_client_get_wasm, "($i$*~)i", NULL}, +#endif #if defined(CONFIG_OCRE_TIMER) || defined(CONFIG_OCRE_GPIO) || defined(CONFIG_OCRE_SENSORS) || \ defined(CONFIG_OCRE_CONTAINER_MESSAGING) {"ocre_get_event", ocre_get_event, "(iiiiii)i", NULL}, @@ -153,6 +197,11 @@ NativeSymbol ocre_api_table[] = { {"ocre_gpio_register_callback_by_name", ocre_gpio_wasm_register_callback_by_name, "($)i", NULL}, {"ocre_gpio_unregister_callback_by_name", ocre_gpio_wasm_unregister_callback_by_name, "($)i", NULL}, #endif +// ADC API +#ifdef CONFIG_OCRE_ADC + {"ocre_adc_init", ocre_adc_wasm_init, "()i", NULL}, + {"ocre_adc_read_by_name", ocre_adc_wasm_read_by_name, "($)i", NULL}, +#endif }; int ocre_api_table_size = sizeof(ocre_api_table) / sizeof(NativeSymbol); diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.c b/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.c new file mode 100644 index 00000000..c5533479 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.c @@ -0,0 +1,148 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocre_dispatch.h" + +#include +#include + +#include + +LOG_MODULE_REGISTER(ocre_dispatch, CONFIG_OCRE_LOG_LEVEL); + +#ifndef CONFIG_OCRE_LOOP_INTERVAL_MS +#define CONFIG_OCRE_LOOP_INTERVAL_MS 100 +#endif + +/* Serializes all calls into the active instance: only one thread may ever + * execute code inside a given WASM module instance at a time, and loop() + * (native-timer-driven) and onRequest() (HTTP-thread-driven) are called from + * different threads. */ +static pthread_mutex_t call_lock = PTHREAD_MUTEX_INITIALIZER; +static wasm_module_inst_t active_module_inst; +static wasm_exec_env_t active_exec_env; + +static pthread_mutex_t scheduler_lock = PTHREAD_MUTEX_INITIALIZER; +static bool scheduler_started; + +static void *loop_scheduler_thread(void *arg) +{ + (void)arg; + + while (1) { + usleep(CONFIG_OCRE_LOOP_INTERVAL_MS * 1000); + ocre_dispatch_call_loop(); + } + + return NULL; +} + +static void ensure_scheduler_started(void) +{ + pthread_mutex_lock(&scheduler_lock); + + if (!scheduler_started) { + pthread_t thread; + int rc = pthread_create(&thread, NULL, loop_scheduler_thread, NULL); + + if (rc) { + LOG_ERR("Failed to start loop() scheduler thread: rc=%d", rc); + } else { + pthread_detach(thread); + scheduler_started = true; + } + } + + pthread_mutex_unlock(&scheduler_lock); +} + +void ocre_dispatch_activate(wasm_module_inst_t module_inst, wasm_exec_env_t exec_env) +{ + pthread_mutex_lock(&call_lock); + active_module_inst = module_inst; + active_exec_env = exec_env; + pthread_mutex_unlock(&call_lock); + + ensure_scheduler_started(); +} + +void ocre_dispatch_deactivate(wasm_module_inst_t module_inst) +{ + pthread_mutex_lock(&call_lock); + if (active_module_inst == module_inst) { + active_module_inst = NULL; + active_exec_env = NULL; + } + pthread_mutex_unlock(&call_lock); +} + +static wasm_function_inst_t lookup_locked(const char *name) +{ + if (!active_module_inst) { + return NULL; + } + return wasm_runtime_lookup_function(active_module_inst, name); +} + +bool ocre_dispatch_has_loop(void) +{ + pthread_mutex_lock(&call_lock); + bool has = lookup_locked("loop") != NULL; + pthread_mutex_unlock(&call_lock); + return has; +} + +bool ocre_dispatch_has_on_request(void) +{ + pthread_mutex_lock(&call_lock); + bool has = lookup_locked("onRequest") != NULL; + pthread_mutex_unlock(&call_lock); + return has; +} + +void ocre_dispatch_call_loop(void) +{ + pthread_mutex_lock(&call_lock); + + wasm_function_inst_t func = lookup_locked("loop"); + + if (func && active_exec_env) { + if (!wasm_runtime_call_wasm(active_exec_env, func, 0, NULL)) { + const char *exception = wasm_runtime_get_exception(active_module_inst); + + LOG_WRN("loop() call failed: %s", exception ? exception : "unknown"); + wasm_runtime_clear_exception(active_module_inst); + } + } + + pthread_mutex_unlock(&call_lock); +} + +bool ocre_dispatch_call_on_request(int32_t request_id) +{ + pthread_mutex_lock(&call_lock); + + wasm_function_inst_t func = lookup_locked("onRequest"); + bool called = false; + + if (func && active_exec_env) { + uint32_t argv[1]; + + argv[0] = (uint32_t)request_id; + called = true; + + if (!wasm_runtime_call_wasm(active_exec_env, func, 1, argv)) { + const char *exception = wasm_runtime_get_exception(active_module_inst); + + LOG_WRN("onRequest() call failed: %s", exception ? exception : "unknown"); + wasm_runtime_clear_exception(active_module_inst); + } + } + + pthread_mutex_unlock(&call_lock); + return called; +} diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.h b/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.h new file mode 100644 index 00000000..bd8998a5 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_dispatch/ocre_dispatch.h @@ -0,0 +1,71 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OCRE_DISPATCH_H +#define OCRE_DISPATCH_H + +#include +#include +#include + +/** + * @brief Marks a module instance as reactive: after its "main" export + * returns, native code may keep calling its "loop" and/or "onRequest" + * exports on their own schedule instead of tearing the instance down. + * + * Only one module instance may be active at a time (matches the existing + * single-container-at-a-time model used elsewhere in Ocre's native API, + * e.g. HTTP content ownership). Activating a new instance implicitly + * replaces whichever one was previously active. + * + * @param module_inst The WASM module instance to activate. + * @param exec_env A dedicated exec_env for this instance, created by the + * caller via wasm_runtime_create_exec_env(). Ownership stays + * with the caller; ocre_dispatch never destroys it. + */ +void ocre_dispatch_activate(wasm_module_inst_t module_inst, wasm_exec_env_t exec_env); + +/** + * @brief Deactivates a module instance if it is the currently active one. + * Safe to call unconditionally on container teardown. + * + * @param module_inst The WASM module instance being torn down. + */ +void ocre_dispatch_deactivate(wasm_module_inst_t module_inst); + +/** + * @brief Whether the currently active module instance exports "loop". + */ +bool ocre_dispatch_has_loop(void); + +/** + * @brief Whether the currently active module instance exports "onRequest". + */ +bool ocre_dispatch_has_on_request(void); + +/** + * @brief Calls the active module instance's "loop" export, if any. No-op + * (and safe to call at any time, from any thread) if no reactive instance + * is active or it doesn't export "loop". Serialized against + * ocre_dispatch_call_on_request() so the two never run concurrently inside + * the same WASM instance. + */ +void ocre_dispatch_call_loop(void); + +/** + * @brief Calls the active module instance's "onRequest" export, if any, + * passing requestId as its single i32 argument. Serialized against + * ocre_dispatch_call_loop(). + * + * @param request_id Opaque request ID (see ocre_http.h) the handler should + * use with the ocre_http_get_... / ocre_http_respond natives. + * @return true if onRequest was actually found and invoked, false if no + * reactive instance is active or it doesn't export "onRequest". + */ +bool ocre_dispatch_call_on_request(int32_t request_id); + +#endif /* OCRE_DISPATCH_H */ diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.c b/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.c new file mode 100644 index 00000000..228b5495 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.c @@ -0,0 +1,527 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocre_http.h" +#include "../ocre_dispatch/ocre_dispatch.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ocre_http, CONFIG_OCRE_LOG_LEVEL); + +#ifndef CONFIG_OCRE_HTTP_SERVER_PORT +#define CONFIG_OCRE_HTTP_SERVER_PORT 8081 +#endif + +#define OCRE_HTTP_MAX_PENDING_REQUESTS 4 +#define OCRE_HTTP_MAX_CAPTURED_HEADERS 8 +#define OCRE_HTTP_HEADER_NAME_SIZE 64 +#define OCRE_HTTP_HEADER_VALUE_SIZE 256 +#define OCRE_HTTP_MAX_PATH_LEN 128 +#define OCRE_HTTP_MAX_QUERY_LEN 128 +#define OCRE_HTTP_MAX_CONTENT_TYPE_LEN 64 + +static uint16_t ocre_http_port = CONFIG_OCRE_HTTP_SERVER_PORT; + +/* ============================================================ + * Per-connection request capture: accumulates a request's path, query, + * headers and (for POST/PUT/PATCH) body across the one-or-more callback + * invocations Zephyr's http_server makes while a request streams in, then + * hands the finished request to ocre_dispatch_call_on_request(). + * ============================================================ */ + +struct captured_header { + char name[OCRE_HTTP_HEADER_NAME_SIZE]; + char value[OCRE_HTTP_HEADER_VALUE_SIZE]; +}; + +struct client_capture { + struct http_client_ctx *client; /* NULL = free slot */ + bool capturing; /* path/method/headers captured for the in-flight request */ + enum http_method method; + char path[OCRE_HTTP_MAX_PATH_LEN]; + char query[OCRE_HTTP_MAX_QUERY_LEN]; + char *body; + size_t body_len; + size_t body_cap; + struct captured_header headers[OCRE_HTTP_MAX_CAPTURED_HEADERS]; + size_t header_count; +}; + +static pthread_mutex_t captures_lock = PTHREAD_MUTEX_INITIALIZER; +static struct client_capture captures[OCRE_HTTP_MAX_PENDING_REQUESTS]; + +/* Caller must hold captures_lock. */ +static struct client_capture *get_capture_for_client(struct http_client_ctx *client) +{ + struct client_capture *free_slot = NULL; + + for (int i = 0; i < OCRE_HTTP_MAX_PENDING_REQUESTS; i++) { + if (captures[i].client == client) { + return &captures[i]; + } + if (!free_slot && captures[i].client == NULL) { + free_slot = &captures[i]; + } + } + + if (free_slot) { + free_slot->client = client; + } + + return free_slot; +} + +static void reset_capture(struct client_capture *cap) +{ + free(cap->body); + struct http_client_ctx *client = cap->client; + + memset(cap, 0, sizeof(*cap)); + cap->client = client; +} + +static void release_capture_for_client(struct http_client_ctx *client) +{ + pthread_mutex_lock(&captures_lock); + + for (int i = 0; i < OCRE_HTTP_MAX_PENDING_REQUESTS; i++) { + if (captures[i].client == client) { + free(captures[i].body); + memset(&captures[i], 0, sizeof(captures[i])); + break; + } + } + + pthread_mutex_unlock(&captures_lock); +} + +static bool method_has_body(enum http_method method) +{ + return method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH; +} + +static void capture_append_body(struct client_capture *cap, const uint8_t *data, size_t len) +{ + if (!data || len == 0) { + return; + } + + size_t needed = cap->body_len + len + 1; + + if (needed > cap->body_cap) { + size_t new_cap = cap->body_cap ? cap->body_cap * 2 : 256; + + while (new_cap < needed) { + new_cap *= 2; + } + + char *new_body = realloc(cap->body, new_cap); + + if (!new_body) { + LOG_WRN("Failed to grow request body buffer to %zu bytes", new_cap); + return; + } + + cap->body = new_body; + cap->body_cap = new_cap; + } + + memcpy(cap->body + cap->body_len, data, len); + cap->body_len += len; + cap->body[cap->body_len] = '\0'; +} + +/* ============================================================ + * Pending request table: the finished, captured requests currently being + * handed to (or answered by) the active container's onRequest() export. + * ============================================================ */ + +struct http_pending_request { + bool in_use; + int32_t id; + + enum http_method method; + char path[OCRE_HTTP_MAX_PATH_LEN]; + char query[OCRE_HTTP_MAX_QUERY_LEN]; + char *body; + size_t body_len; + + struct captured_header headers[OCRE_HTTP_MAX_CAPTURED_HEADERS]; + size_t header_count; + + bool responded; + int status; + char content_type[OCRE_HTTP_MAX_CONTENT_TYPE_LEN]; + char *resp_body; + size_t resp_body_len; +}; + +static pthread_mutex_t requests_lock = PTHREAD_MUTEX_INITIALIZER; +static struct http_pending_request requests[OCRE_HTTP_MAX_PENDING_REQUESTS]; +static int32_t next_request_id = 1; + +static struct http_pending_request *alloc_request(void) +{ + pthread_mutex_lock(&requests_lock); + + for (int i = 0; i < OCRE_HTTP_MAX_PENDING_REQUESTS; i++) { + if (!requests[i].in_use) { + memset(&requests[i], 0, sizeof(requests[i])); + requests[i].in_use = true; + requests[i].id = next_request_id++; + pthread_mutex_unlock(&requests_lock); + return &requests[i]; + } + } + + pthread_mutex_unlock(&requests_lock); + return NULL; +} + +static void free_request(struct http_pending_request *req) +{ + pthread_mutex_lock(&requests_lock); + free(req->body); + free(req->resp_body); + memset(req, 0, sizeof(*req)); + pthread_mutex_unlock(&requests_lock); +} + +/* Returns the slot matching request_id with requests_lock held, or NULL + * (lock released) if not found. Caller must unlock via requests_lock when + * done, only in the found case. */ +static struct http_pending_request *lock_request(int32_t request_id) +{ + pthread_mutex_lock(&requests_lock); + + for (int i = 0; i < OCRE_HTTP_MAX_PENDING_REQUESTS; i++) { + if (requests[i].in_use && requests[i].id == request_id) { + return &requests[i]; + } + } + + pthread_mutex_unlock(&requests_lock); + return NULL; +} + +/* ============================================================ + * Native API: accessors + respond(), called by the active container's + * onRequest() handler. + * ============================================================ */ + +static int copy_out(const char *src, size_t src_len, char *buf, uint32_t buf_len) +{ + if (!buf || buf_len == 0 || src_len + 1 > buf_len) { + return -1; + } + + memcpy(buf, src, src_len); + buf[src_len] = '\0'; + return (int)src_len; +} + +int ocre_http_get_method_wasm(wasm_exec_env_t exec_env, int32_t request_id) +{ + ARG_UNUSED(exec_env); + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + int method = (int)req->method; + + pthread_mutex_unlock(&requests_lock); + return method; +} + +int ocre_http_get_path_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len) +{ + ARG_UNUSED(exec_env); + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + int ret = copy_out(req->path, strlen(req->path), buf, buf_len); + + pthread_mutex_unlock(&requests_lock); + return ret; +} + +int ocre_http_get_query_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len) +{ + ARG_UNUSED(exec_env); + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + int ret = copy_out(req->query, strlen(req->query), buf, buf_len); + + pthread_mutex_unlock(&requests_lock); + return ret; +} + +int ocre_http_get_header_wasm(wasm_exec_env_t exec_env, int32_t request_id, const char *name, char *buf, + uint32_t buf_len) +{ + ARG_UNUSED(exec_env); + + if (!name) { + return -1; + } + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + int ret = 0; + + for (size_t i = 0; i < req->header_count; i++) { + if (strcasecmp(req->headers[i].name, name) == 0) { + ret = copy_out(req->headers[i].value, strlen(req->headers[i].value), buf, buf_len); + break; + } + } + + pthread_mutex_unlock(&requests_lock); + return ret; +} + +int ocre_http_get_body_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len) +{ + ARG_UNUSED(exec_env); + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + int ret = copy_out(req->body ? req->body : "", req->body_len, buf, buf_len); + + pthread_mutex_unlock(&requests_lock); + return ret; +} + +int ocre_http_respond_wasm(wasm_exec_env_t exec_env, int32_t request_id, int32_t status, const char *content_type, + const char *body) +{ + ARG_UNUSED(exec_env); + + struct http_pending_request *req = lock_request(request_id); + + if (!req) { + return -1; + } + + if (req->responded) { + pthread_mutex_unlock(&requests_lock); + return -1; + } + + req->status = status; + + if (content_type) { + strncpy(req->content_type, content_type, sizeof(req->content_type) - 1); + } + + if (body) { + size_t len = strlen(body); + + req->resp_body = malloc(len + 1); + if (req->resp_body) { + memcpy(req->resp_body, body, len + 1); + req->resp_body_len = len; + } + } + + req->responded = true; + + pthread_mutex_unlock(&requests_lock); + return 0; +} + +/* ============================================================ + * The single native fallback HTTP resource: catches every request + * regardless of path/method and dispatches it to the active container. + * ============================================================ */ + +static int ocre_http_fallback_handler(struct http_client_ctx *client, enum http_transaction_status status, + const struct http_request_ctx *request_ctx, + struct http_response_ctx *response_ctx, void *user_data) +{ + ARG_UNUSED(user_data); + + if (status == HTTP_SERVER_TRANSACTION_COMPLETE || status == HTTP_SERVER_TRANSACTION_ABORTED) { + release_capture_for_client(client); + return 0; + } + + pthread_mutex_lock(&captures_lock); + + struct client_capture *cap = get_capture_for_client(client); + + if (!cap) { + pthread_mutex_unlock(&captures_lock); + response_ctx->status = HTTP_500_INTERNAL_SERVER_ERROR; + response_ctx->final_chunk = true; + return 0; + } + + if (!cap->capturing) { + cap->capturing = true; + cap->method = client->method; + + const char *url = (const char *)client->url_buffer; + const char *qmark = strchr(url, '?'); + size_t path_len = qmark ? (size_t)(qmark - url) : strlen(url); + + if (path_len >= sizeof(cap->path)) { + path_len = sizeof(cap->path) - 1; + } + memcpy(cap->path, url, path_len); + cap->path[path_len] = '\0'; + + if (qmark) { + strncpy(cap->query, qmark + 1, sizeof(cap->query) - 1); + } + + if (request_ctx->headers_status != HTTP_HEADER_STATUS_NONE) { + for (size_t i = 0; i < request_ctx->header_count && i < OCRE_HTTP_MAX_CAPTURED_HEADERS; i++) { + strncpy(cap->headers[i].name, request_ctx->headers[i].name, + sizeof(cap->headers[i].name) - 1); + strncpy(cap->headers[i].value, request_ctx->headers[i].value, + sizeof(cap->headers[i].value) - 1); + cap->header_count++; + } + } + } + + if (method_has_body(cap->method)) { + capture_append_body(cap, request_ctx->data, request_ctx->data_len); + } + + if (status != HTTP_SERVER_REQUEST_DATA_FINAL) { + pthread_mutex_unlock(&captures_lock); + return 0; + } + + /* Request fully captured -- move it into a pending-request slot and + * dispatch it to the active container. */ + + struct http_pending_request *preq = alloc_request(); + + if (!preq) { + LOG_WRN("HTTP request table full; rejecting request"); + reset_capture(cap); + pthread_mutex_unlock(&captures_lock); + response_ctx->status = HTTP_503_SERVICE_UNAVAILABLE; + response_ctx->final_chunk = true; + return 0; + } + + preq->method = cap->method; + strncpy(preq->path, cap->path, sizeof(preq->path) - 1); + strncpy(preq->query, cap->query, sizeof(preq->query) - 1); + memcpy(preq->headers, cap->headers, sizeof(preq->headers)); + preq->header_count = cap->header_count; + preq->body = cap->body; + preq->body_len = cap->body_len; + cap->body = NULL; /* ownership moved to preq */ + + int32_t request_id = preq->id; + + reset_capture(cap); + pthread_mutex_unlock(&captures_lock); + + bool called = ocre_dispatch_call_on_request(request_id); + + pthread_mutex_lock(&requests_lock); + + /* preq may have been freed by nothing else (only we free it), so it's + * still valid; re-check responded/status/body directly. */ + if (!called || !preq->responded) { + response_ctx->status = HTTP_404_NOT_FOUND; + response_ctx->body = NULL; + response_ctx->body_len = 0; + } else { + static __thread char content_type_buf[OCRE_HTTP_MAX_CONTENT_TYPE_LEN]; + static __thread struct http_header content_type_header; + + response_ctx->status = (enum http_status)preq->status; + response_ctx->body = (const uint8_t *)preq->resp_body; + response_ctx->body_len = preq->resp_body_len; + + if (preq->content_type[0]) { + strncpy(content_type_buf, preq->content_type, sizeof(content_type_buf) - 1); + content_type_header.name = "Content-Type"; + content_type_header.value = content_type_buf; + response_ctx->headers = &content_type_header; + response_ctx->header_count = 1; + } + } + + response_ctx->final_chunk = true; + + pthread_mutex_unlock(&requests_lock); + + free_request(preq); + + return 0; +} + +static struct http_resource_detail_dynamic ocre_http_fallback_detail = { + .common = + { + .type = HTTP_RESOURCE_TYPE_DYNAMIC, + .bitmask_of_supported_http_methods = BIT(HTTP_GET) | BIT(HTTP_POST) | BIT(HTTP_PUT) | + BIT(HTTP_DELETE) | BIT(HTTP_HEAD) | BIT(HTTP_OPTIONS) | + BIT(HTTP_PATCH), + .content_type = "text/plain", + }, + .cb = ocre_http_fallback_handler, + .user_data = NULL, +}; + +/* concurrent=4 matches CONFIG_HTTP_SERVER_MAX_CLIENTS (raised to 4 in the + * sample's prj.conf); backlog=8 gives room for several pending/half-closed + * connections so one stuck client can't wedge the server. The fallback + * resource (last arg before service-level config) catches every path -- + * there are no other registered resources, so routing is entirely up to + * the container's onRequest() handler. */ +HTTP_SERVICE_DEFINE(ocre_http_service, NULL, &ocre_http_port, 4, 8, NULL, + (struct http_resource_detail *)&ocre_http_fallback_detail, NULL); + +static int ocre_http_server_init(void) +{ + int ret = http_server_start(); + + if (ret < 0) { + LOG_ERR("Failed to start Ocre HTTP server: %d", ret); + } + + return ret; +} + +SYS_INIT(ocre_http_server_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.h b/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.h new file mode 100644 index 00000000..5e22906d --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_http/ocre_http.h @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OCRE_HTTP_H +#define OCRE_HTTP_H + +#include + +/** + * Generic HTTP request/response bridge for WASM containers. + * + * Ocre's native HTTP server (Zephyr's http_server subsystem) catches every + * request, regardless of path or method, via a single fallback resource. + * Each request is captured into a small pending-request table and handed to + * the active reactive module instance's exported "onRequest(requestId)" + * function (see ocre_dispatch.h) -- all routing logic lives in the + * container's own AssemblyScript/C code, not in native code. + * + * The handler reads the request with the accessors below and answers with + * ocre_http_respond_wasm(). If it never responds, the native side times the + * request out and returns a 500. + */ + +/** HTTP method values match Zephyr's enum http_method (zephyr/net/http/method.h). */ +int ocre_http_get_method_wasm(wasm_exec_env_t exec_env, int32_t request_id); + +/** Copies the request path (no query string) into buf, NUL-terminated. + * Returns the path length (excluding NUL), or -1 if request_id is invalid or + * buf is too small. */ +int ocre_http_get_path_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len); + +/** Copies the raw query string (no leading '?'; empty string if none) into + * buf, NUL-terminated. Returns the length (excluding NUL), or -1 on error. */ +int ocre_http_get_query_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len); + +/** Copies the named request header's value into buf, NUL-terminated. + * Returns the value length, 0 if the header wasn't found/captured, or -1 on + * error. Header capture requires CONFIG_HTTP_SERVER_CAPTURE_HEADERS on the + * device; without it this always returns 0. */ +int ocre_http_get_header_wasm(wasm_exec_env_t exec_env, int32_t request_id, const char *name, char *buf, + uint32_t buf_len); + +/** Copies the request body into buf, NUL-terminated (bodies are treated as + * text; embedded NULs truncate). Returns the body length (excluding NUL), or + * -1 on error. */ +int ocre_http_get_body_wasm(wasm_exec_env_t exec_env, int32_t request_id, char *buf, uint32_t buf_len); + +/** Answers a request captured by onRequest(). content_type/body may be NULL + * for an empty response. Returns 0 on success, negative on error (e.g. + * unknown/already-answered request_id). */ +int ocre_http_respond_wasm(wasm_exec_env_t exec_env, int32_t request_id, int32_t status, const char *content_type, + const char *body); + +#endif /* OCRE_HTTP_H */ diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_http/sections-rom.ld b/src/runtime/wamr-wasip1/ocre_api/ocre_http/sections-rom.ld new file mode 100644 index 00000000..46b5d7a5 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_http/sections-rom.ld @@ -0,0 +1,3 @@ +#include + +ITERABLE_SECTION_ROM(http_resource_desc_ocre_http_service, Z_LINK_ITERABLE_SUBALIGN) diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.c b/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.c new file mode 100644 index 00000000..9ed23987 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.c @@ -0,0 +1,116 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocre_http_client.h" + +#include +#include + +#include +#include +#include + +#ifndef CONFIG_OCRE_HTTP_CLIENT_TIMEOUT_MS +#define CONFIG_OCRE_HTTP_CLIENT_TIMEOUT_MS 5000 +#endif + +#define OCRE_HTTP_CLIENT_RECV_BUF_LEN 512 + +struct ocre_http_client_state { + char *out_buf; + uint32_t out_buf_len; + size_t out_len; + bool failed; +}; + +static int response_cb(struct http_response *rsp, enum http_final_call final_data, void *user_data) +{ + struct ocre_http_client_state *st = user_data; + + ARG_UNUSED(final_data); + + if (rsp->http_status_code != 0 && rsp->http_status_code != 200) { + st->failed = true; + return -1; + } + + if (rsp->body_frag_len == 0) { + return 0; + } + + /* out_buf_len always leaves room for the NUL below (buf_len == 0 is + * rejected before this callback can run). */ + size_t space = st->out_buf_len - 1 - st->out_len; + size_t copy_len = rsp->body_frag_len < space ? rsp->body_frag_len : space; + + if (copy_len > 0) { + memcpy(st->out_buf + st->out_len, rsp->body_frag_start, copy_len); + st->out_len += copy_len; + st->out_buf[st->out_len] = '\0'; + } + + return 0; +} + +int ocre_http_client_get_wasm(wasm_exec_env_t exec_env, const char *host, int32_t port, const char *path, char *buf, + uint32_t buf_len) +{ + ARG_UNUSED(exec_env); + + if (!host || !path || !buf || buf_len == 0 || port <= 0 || port > 65535) { + return -1; + } + + buf[0] = '\0'; + + struct sockaddr_in addr; + static uint8_t recv_buf[OCRE_HTTP_CLIENT_RECV_BUF_LEN]; + struct ocre_http_client_state st = { + .out_buf = buf, + .out_buf_len = buf_len, + }; + struct http_request req; + int sock; + int ret; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + return -1; + } + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock < 0) { + return -1; + } + + if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(sock); + return -1; + } + + memset(&req, 0, sizeof(req)); + req.method = HTTP_GET; + req.url = path; + req.host = host; + req.protocol = "HTTP/1.1"; + req.response = response_cb; + req.recv_buf = recv_buf; + req.recv_buf_len = sizeof(recv_buf); + + ret = http_client_req(sock, &req, CONFIG_OCRE_HTTP_CLIENT_TIMEOUT_MS, &st); + + close(sock); + + if (ret < 0 || st.failed) { + return -1; + } + + return (int)st.out_len; +} diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.h b/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.h new file mode 100644 index 00000000..b52b7095 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_http_client/ocre_http_client.h @@ -0,0 +1,29 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OCRE_HTTP_CLIENT_H +#define OCRE_HTTP_CLIENT_H + +#include + +/** + * Outbound HTTP client for WASM containers -- the counterpart to + * ocre_http.h's inbound request/response bridge. Lets container code issue + * its own HTTP GET requests (e.g. to poll a status endpoint) rather than + * only ever answering requests the native side hands it. + */ + +/** Blocking HTTP GET to http://host:port/path. Copies the response body into + * buf, NUL-terminated (truncated if it doesn't fit). Returns the copied body + * length on a 200 response, or a negative value on any connection, timeout, + * or non-200 failure. Runs on the calling WASM instance's own thread -- like + * any blocking native call, it stalls that instance's loop()/onRequest() + * dispatch for the duration of the request. */ +int ocre_http_client_get_wasm(wasm_exec_env_t exec_env, const char *host, int32_t port, const char *path, char *buf, + uint32_t buf_len); + +#endif /* OCRE_HTTP_CLIENT_H */ diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.c b/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.c new file mode 100644 index 00000000..da0a29e8 --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.c @@ -0,0 +1,47 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocre_print.h" + +#ifdef __ZEPHYR__ +#include +#else +#include +#endif + +int ocre_print_wasm(wasm_exec_env_t exec_env, const char *msg) +{ + if (!msg) { + return -1; + } + +#ifdef __ZEPHYR__ + printk("%s\n", msg); +#else + printf("%s\n", msg); +#endif + + return 0; +} + +void ocre_abort_wasm(wasm_exec_env_t exec_env, const char *message, const char *file_name, uint32_t line_number, + uint32_t column_number) +{ +#ifdef __ZEPHYR__ + printk("[wasm abort] %s (%s:%u:%u)\n", message ? message : "", file_name ? file_name : "", line_number, + column_number); +#else + printf("[wasm abort] %s (%s:%u:%u)\n", message ? message : "", file_name ? file_name : "", line_number, + column_number); +#endif + + wasm_module_inst_t module_inst = wasm_runtime_get_module_inst(exec_env); + + if (module_inst) { + wasm_runtime_set_exception(module_inst, "wasm abort"); + } +} diff --git a/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.h b/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.h new file mode 100644 index 00000000..486b305e --- /dev/null +++ b/src/runtime/wamr-wasip1/ocre_api/ocre_print/ocre_print.h @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OCRE_PRINT_H +#define OCRE_PRINT_H + +#include +#include + +/** + * @brief Print a message from a WASM container to the host console. + * + * Portable across every platform Ocre runs on: uses Zephyr's printk() when + * built for Zephyr, and printf() otherwise (e.g. the Linux/posix runtime). + * + * @param exec_env WASM execution environment. + * @param msg Null-terminated string to print. + * + * @return 0 on success, negative error code on failure. + */ +int ocre_print_wasm(wasm_exec_env_t exec_env, const char *msg); + +/** + * @brief Backs the "env.abort" import that AssemblyScript-compiled modules + * expect (used for reporting failed runtime checks like array bounds). + * + * Prints the failure and raises a WASM exception, cleanly stopping the + * calling container's execution (not a host crash). + */ +void ocre_abort_wasm(wasm_exec_env_t exec_env, const char *message, const char *file_name, uint32_t line_number, + uint32_t column_number); + +#endif /* OCRE_PRINT_H */ diff --git a/src/runtime/wamr-wasip1/wamr.c b/src/runtime/wamr-wasip1/wamr.c index e5e5f5a4..63af1598 100644 --- a/src/runtime/wamr-wasip1/wamr.c +++ b/src/runtime/wamr-wasip1/wamr.c @@ -28,6 +28,7 @@ #include "ocre_api/ocre_common.h" #include "ocre_api/ocre_timers/ocre_timer.h" +#include "ocre_api/ocre_dispatch/ocre_dispatch.h" LOG_MODULE_REGISTER(wamr_runtime, CONFIG_OCRE_LOG_LEVEL); @@ -47,14 +48,24 @@ struct wamr_context { bool uses_shared_heap; char **dir_map_list; size_t dir_map_list_len; + /* Non-NULL once this instance has been handed to ocre_dispatch as the + * active reactive instance (exports "loop" and/or "onRequest"). + * instance_execute() then skips deinstantiating on return; cleanup is + * deferred to instance_destroy(). */ + wasm_exec_env_t reactive_exec_env; }; static int instance_execute(void *runtime_context, sem_t *sem) { struct wamr_context *context = runtime_context; + /* host_managed_heap_size=0: this is WAMR's own "app heap" inside linear + * memory, for languages needing malloc() inside the sandbox (C/Rust via + * WASI). AssemblyScript manages its own memory in linear memory and + * never touches it -- skipping it recovers 8KB of native heap per + * container for free. */ context->module_inst = - wasm_runtime_instantiate(context->module, 8192, 8192, context->error_buf, sizeof(context->error_buf)); + wasm_runtime_instantiate(context->module, 8192, 0, context->error_buf, sizeof(context->error_buf)); if (!context->module_inst) { LOG_ERR("Failed to instantiate module: %s, for context %p", context->error_buf, context); return -1; @@ -100,6 +111,30 @@ static int instance_execute(void *runtime_context, sem_t *sem) } } + /* If the module also exports "loop" and/or "onRequest", it wants to + * keep running after "main" returns: main() becomes a one-time setup + * call, and native code drives the rest (a periodic loop() call, or + * onRequest() dispatched from the HTTP fallback handler) instead of + * the module looping forever inside this one call. Skip teardown and + * hand the instance off to ocre_dispatch; instance_destroy() will + * deinstantiate it once the container is actually removed. */ + + if (wasm_runtime_lookup_function(context->module_inst, "loop") || + wasm_runtime_lookup_function(context->module_inst, "onRequest")) { + wasm_exec_env_t dispatch_exec_env = + wasm_runtime_create_exec_env(context->module_inst, OCRE_WASM_STACK_SIZE); + + if (dispatch_exec_env) { + context->reactive_exec_env = dispatch_exec_env; + ocre_dispatch_activate(context->module_inst, dispatch_exec_env); + LOG_INF("Context %p is reactive; handing off to ocre_dispatch", context); + return 0; + } + + LOG_ERR("Failed to create exec env for reactive dispatch on context %p; tearing down normally", + context); + } + if (context->uses_ocre_api) { /* Cleanup module resources if using Ocre API */ @@ -463,6 +498,30 @@ static int instance_destroy(void *runtime_context) return -1; } + /* A reactive instance (see instance_execute()) is still instantiated + * and registered with ocre_dispatch at this point -- finish the + * deferred teardown now that the container is actually being + * removed. Non-reactive instances already deinstantiated in + * instance_execute(), leaving module_inst NULL, so this is a no-op + * for them. */ + + if (context->module_inst) { + ocre_dispatch_deactivate(context->module_inst); + + if (context->uses_ocre_api) { + ocre_cleanup_module_resources(context->module_inst); + ocre_unregister_module(context->module_inst); + } + + if (context->reactive_exec_env) { + wasm_runtime_destroy_exec_env(context->reactive_exec_env); + context->reactive_exec_env = NULL; + } + + wasm_runtime_deinstantiate(context->module_inst); + context->module_inst = NULL; + } + wasm_runtime_unload(context->module); if (ocre_unload_file(context->buffer, context->size)) { diff --git a/src/samples/fetch/main.c b/src/samples/fetch/main.c new file mode 100644 index 00000000..015845cd --- /dev/null +++ b/src/samples/fetch/main.c @@ -0,0 +1,712 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "wifi_credentials.h" + +/* Hardcoded for now; see also wifi_credentials.h. */ +#define FETCH_SERVER_ADDR "192.168.50.56" +#define FETCH_SERVER_PORT 8080 +#define FETCH_URL "/getCode" +#define FETCH_HASH_URL "/getHash" + +#define FETCH_INTERVAL K_SECONDS(20) +#define FETCH_RECV_BUF_LEN 2048 +#define FETCH_MAX_BODY_LEN (512 * 1024) +#define FETCH_TIMEOUT_MS 5000 +#define FETCH_HASH_BUF_LEN 80 /* sha256 hex digest (64 chars) plus slack */ + +/* This board's WiFi intermittently fails an outbound connection or times out + * mid-request -- confirmed to not be a lost IP/interface-down condition (the + * interface stays up with a valid address throughout) and not limited to any + * particular operation, so it looks like general RF-level flakiness. + * Retrying rides it out instead of giving up on the first hiccup. */ +#define FETCH_CODE_MAX_ATTEMPTS 5 +#define FETCH_CODE_RETRY_DELAY_MS 2000 + +#define FETCH_HASH_MAX_ATTEMPTS 3 +#define FETCH_HASH_RETRY_DELAY_MS 1000 + +#define CONTAINER_IMAGE "current.wasm" +#define CONTAINER_IMAGE_TMP "current.wasm.new" +#define CONTAINER_HASH_FILE "current.hash" +#define CONTAINER_ID "fetched" + +/* Board's built-in LED, driven directly here (not through ocre_gpio) so it's + * available before any container exists. No-op on a board that doesn't + * define a "led0" alias. */ +#if DT_NODE_HAS_STATUS(DT_ALIAS(led0), okay) +#define HAVE_BUILTIN_LED 1 +static const struct gpio_dt_spec led0_spec = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios); +#else +#define HAVE_BUILTIN_LED 0 +#endif + +static struct net_mgmt_event_callback wifi_mgmt_cb; +static volatile bool wifi_connected; + +static struct ocre_context *g_ctx; +static struct ocre_container *g_container; + +/* The hash of whatever code is currently installed/running, so an update + * check only needs to compare a short string (fetched from /getHash) + * instead of holding the whole image in RAM just for comparison -- see + * check_for_update(). Empty until either a cached image's hash file is + * read at boot, or a fetch successfully installs one. */ +static char g_current_hash[FETCH_HASH_BUF_LEN]; + +/* Used by fetch_hash(): the digest is tiny, so this just fills a fixed + * buffer directly -- no heap growth needed. */ +struct hash_fetch_state { + char buf[FETCH_HASH_BUF_LEN]; + size_t len; + bool failed; +}; + +/* Used by fetch_code_to_file(): each fragment is written straight to an + * already-open file as it arrives, so downloading a new image needs no + * heap buffer at all -- see the comment on fetch_code_to_file(). */ +struct code_fetch_state { + int fd; + size_t len; + bool failed; +}; + +/* Blinks the built-in LED a few times as a purely visual "the fetch sample + * just (re)started" signal -- driven directly, independent of whatever the + * running container's own AssemblyScript code is doing with the same pin via + * ocre_gpio, so it works even before any container exists. Left off + * afterward: from that point on the pin is the container's to control (see + * gpio-demo's onRequest(), which drives it through ocre_gpio_set_by_name()). + * No-op on a board without a "led0" alias. */ +static void native_led_blink(int times) +{ +#if HAVE_BUILTIN_LED + if (!gpio_is_ready_dt(&led0_spec)) { + return; + } + + gpio_pin_configure_dt(&led0_spec, GPIO_OUTPUT_INACTIVE); + + for (int i = 0; i < times; i++) { + gpio_pin_set_dt(&led0_spec, 1); + k_sleep(K_MSEC(150)); + gpio_pin_set_dt(&led0_spec, 0); + k_sleep(K_MSEC(150)); + } +#else + ARG_UNUSED(times); +#endif +} + +/* WiFi power-save (modem sleep) periodically puts the radio to sleep between + * beacons; if it ever fails to wake/resync correctly, the station stays + * associated (no disconnect event fires) but stops passing traffic in either + * direction until the device is reset. Explicitly disabling it removes that + * failure mode. Re-issued on every successful connect (not just the first), + * since a reconnect could otherwise come back up with power-save re-enabled. */ +static void wifi_disable_power_save(struct net_if *iface) +{ + struct wifi_ps_params params = { + .enabled = WIFI_PS_DISABLED, + }; + + if (net_mgmt(NET_REQUEST_WIFI_PS, iface, ¶ms, sizeof(params))) { + printk("Failed to disable WiFi power save\n"); + } else { + printk("WiFi power save disabled\n"); + } +} + +static void wifi_mgmt_event_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, + struct net_if *iface) +{ + switch (mgmt_event) { + case NET_EVENT_WIFI_CONNECT_RESULT: { + const struct wifi_status *status = (const struct wifi_status *)cb->info; + + if (status->status) { + printk("WiFi connect request failed (%d)\n", status->status); + } else { + printk("WiFi connected\n"); + wifi_connected = true; + wifi_disable_power_save(iface); + } + break; + } + case NET_EVENT_WIFI_DISCONNECT_RESULT: + printk("WiFi disconnected\n"); + wifi_connected = false; + break; + default: + break; + } +} + +static void wifi_connect(void) +{ + struct net_if *iface = net_if_get_default(); + static struct wifi_connect_req_params params; + + if (!iface) { + printk("No default network interface yet\n"); + return; + } + + memset(¶ms, 0, sizeof(params)); + params.ssid = FETCH_WIFI_SSID; + params.ssid_length = strlen(FETCH_WIFI_SSID); + params.psk = FETCH_WIFI_PSK; + params.psk_length = strlen(FETCH_WIFI_PSK); + params.security = WIFI_SECURITY_TYPE_PSK; + params.channel = WIFI_CHANNEL_ANY; + + printk("Connecting to WiFi SSID '%s'...\n", FETCH_WIFI_SSID); + + int ret = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, ¶ms, sizeof(params)); + + if (ret) { + printk("WiFi connect request failed: %d\n", ret); + } +} + +/* Opens a socket, issues a GET for url, and hands response fragments to cb. + * Shared by fetch_hash() and fetch_code_to_file() -- they differ only in + * how they handle those fragments (accumulate a few bytes vs. stream + * straight to flash). Returns 0 on success (check user_data's own + * failed/len fields), a negative errno on a connection-level failure. */ +static int do_http_get(const char *url, http_response_cb_t cb, void *user_data) +{ + struct sockaddr_in addr; + struct http_request req; + static uint8_t recv_buf[FETCH_RECV_BUF_LEN]; + int sock; + int ret; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(FETCH_SERVER_PORT); + + if (inet_pton(AF_INET, FETCH_SERVER_ADDR, &addr.sin_addr) != 1) { + return -EINVAL; + } + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock < 0) { + printk("do_http_get(%s): socket() failed: errno=%d\n", url, errno); + return -errno; + } + + ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr)); + if (ret < 0) { + ret = -errno; + printk("do_http_get(%s): connect() failed: errno=%d\n", url, -ret); + + /* Temporary diagnostic: check whether the interface itself + * still thinks it's up and holds an IPv4 address at the + * moment connect() fails, to tell a real interface-level + * outage apart from a purely socket/routing-layer issue. */ + struct net_if *diag_iface = net_if_get_default(); + + if (diag_iface) { + struct net_in_addr *diag_ip = + net_if_ipv4_get_global_addr(diag_iface, NET_ADDR_PREFERRED); + char diag_ip_buf[NET_IPV4_ADDR_LEN]; + + printk(" iface %p: up=%d, ipv4=%s\n", diag_iface, + net_if_flag_is_set(diag_iface, NET_IF_UP), + diag_ip ? net_addr_ntop(AF_INET, diag_ip, diag_ip_buf, + sizeof(diag_ip_buf)) + : "none"); + } else { + printk(" no default iface\n"); + } + + close(sock); + return ret; + } + + memset(&req, 0, sizeof(req)); + req.method = HTTP_GET; + req.url = url; + req.host = FETCH_SERVER_ADDR; + req.protocol = "HTTP/1.1"; + req.response = cb; + req.recv_buf = recv_buf; + req.recv_buf_len = sizeof(recv_buf); + + ret = http_client_req(sock, &req, FETCH_TIMEOUT_MS, user_data); + if (ret < 0) { + printk("do_http_get(%s): http_client_req() failed: %d\n", url, ret); + } + + close(sock); + + return ret; +} + +static int hash_response_cb(struct http_response *rsp, enum http_final_call final_data, void *user_data) +{ + struct hash_fetch_state *st = user_data; + + ARG_UNUSED(final_data); + + if (rsp->http_status_code != 0 && rsp->http_status_code != 200) { + printk("Unexpected HTTP status %u\n", rsp->http_status_code); + st->failed = true; + return -1; + } + + if (rsp->body_frag_len == 0) { + return 0; + } + + /* The digest is a handful of bytes; just take what fits and drop the + * rest rather than growing anything. */ + size_t space = sizeof(st->buf) - 1 - st->len; + size_t copy_len = rsp->body_frag_len < space ? rsp->body_frag_len : space; + + memcpy(st->buf + st->len, rsp->body_frag_start, copy_len); + st->len += copy_len; + st->buf[st->len] = '\0'; + + return 0; +} + +/* Fetches the sha256 hex digest of the code currently served at FETCH_URL. + * Used instead of fetching the code itself for the once-a-minute check -- + * a few dozen bytes instead of the whole image, so containers only ever + * get stopped and re-fetched when something actually changed. */ +static int fetch_hash(char *out_hash, size_t out_hash_size) +{ + struct hash_fetch_state st; + int ret; + + memset(&st, 0, sizeof(st)); + + ret = do_http_get(FETCH_HASH_URL, hash_response_cb, &st); + if (ret < 0) { + return ret; + } + + if (st.failed || st.len == 0) { + return -EIO; + } + + /* Trim any trailing newline/whitespace a server might send. */ + while (st.len > 0 && (st.buf[st.len - 1] == '\n' || st.buf[st.len - 1] == '\r' || + st.buf[st.len - 1] == ' ')) { + st.buf[--st.len] = '\0'; + } + + strncpy(out_hash, st.buf, out_hash_size - 1); + out_hash[out_hash_size - 1] = '\0'; + return 0; +} + +static int code_response_cb(struct http_response *rsp, enum http_final_call final_data, void *user_data) +{ + struct code_fetch_state *st = user_data; + + ARG_UNUSED(final_data); + + if (rsp->http_status_code != 0 && rsp->http_status_code != 200) { + printk("Unexpected HTTP status %u\n", rsp->http_status_code); + st->failed = true; + return -1; + } + + if (rsp->body_frag_len == 0) { + return 0; + } + + if (st->len + rsp->body_frag_len > FETCH_MAX_BODY_LEN) { + printk("Fetched body exceeds max size (%d bytes)\n", FETCH_MAX_BODY_LEN); + st->failed = true; + return -1; + } + + /* Written straight to flash as each fragment arrives -- no heap + * buffer to grow, and nothing here scales with the image size. This + * is plain POSIX file I/O and Zephyr's own streaming HTTP client + * callback, so it's portable to any board with a filesystem, not an + * ESP32-specific trick. */ + ssize_t written = write(st->fd, rsp->body_frag_start, rsp->body_frag_len); + + if (written != (ssize_t)rsp->body_frag_len) { + printk("Failed to write fetched data to flash: errno=%d\n", errno); + st->failed = true; + return -1; + } + + st->len += rsp->body_frag_len; + + return 0; +} + +/* Streams the code currently served at FETCH_URL directly into tmp_path on + * flash, without ever holding the full image in RAM. Returns 0 on success + * (with *out_len set), a negative errno otherwise -- the caller is + * responsible for removing a partial tmp_path on failure. */ +static int fetch_code_to_file(const char *tmp_path, size_t *out_len) +{ + struct code_fetch_state st; + int ret; + + memset(&st, 0, sizeof(st)); + + st.fd = open(tmp_path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (st.fd < 0) { + printk("Failed to open '%s' for the incoming image: errno=%d\n", tmp_path, errno); + return -errno; + } + + ret = do_http_get(FETCH_URL, code_response_cb, &st); + + close(st.fd); + + if (ret < 0) { + printk("do_http_get(%s) failed: %d\n", FETCH_URL, ret); + return ret; + } + + if (st.failed || st.len == 0) { + return -EIO; + } + + *out_len = st.len; + return 0; +} + +static void save_hash_file(const char *workdir) +{ + char path[128]; + int fd; + + snprintf(path, sizeof(path), "%s/images/%s", workdir, CONTAINER_HASH_FILE); + + fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd < 0) { + printk("Failed to persist hash file: errno=%d\n", errno); + return; + } + + write(fd, g_current_hash, strlen(g_current_hash)); + close(fd); +} + +/* Populates g_current_hash from a previous run's hash file, if any. Left + * empty (meaning "unknown") if the file doesn't exist yet -- the first + * update check will then always treat the fetched hash as different and + * self-heal by installing whatever is currently served, recording its + * hash for next time. */ +static void load_hash_file(const char *workdir) +{ + char path[128]; + int fd; + + snprintf(path, sizeof(path), "%s/images/%s", workdir, CONTAINER_HASH_FILE); + + fd = open(path, O_RDONLY); + if (fd < 0) { + return; + } + + ssize_t r = read(fd, g_current_hash, sizeof(g_current_hash) - 1); + + close(fd); + + if (r > 0) { + g_current_hash[r] = '\0'; + } +} + +/* Temporary diagnostic: not declared in a public header. */ +extern int malloc_runtime_stats_get(struct sys_memory_stats *stats); + +static void print_heap_stats(const char *label) +{ + struct sys_memory_stats stats; + + if (malloc_runtime_stats_get(&stats) == 0) { + printk("[heap %s] free=%zu allocated=%zu max_allocated=%zu\n", label, stats.free_bytes, + stats.allocated_bytes, stats.max_allocated_bytes); + } +} + +/* Feeds `ocre-as stats`: a machine-readable line per memory type, easy to + * pick out of the interleaved log/container output on the same serial + * connection. Runs continuously (its own thread, independent of WiFi/ + * container state) so it's useful for watching memory even before the first + * container starts. */ +static void print_ocre_stats_line(const char *type, size_t total, size_t used) +{ + printk("OCRE_STATS,%s,%zu,%zu\n", type, total, used); +} + +static void stats_thread_fn(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + while (1) { + struct sys_memory_stats heap_stats; + + if (malloc_runtime_stats_get(&heap_stats) == 0) { + print_ocre_stats_line("heap", heap_stats.free_bytes + heap_stats.allocated_bytes, + heap_stats.allocated_bytes); + } + + struct fs_statvfs flash_stats; + + /* "/lfs" is this board's fixed LittleFS mount point (see + * fstab.overlay) -- independent of ocre_context_get_working_directory(), + * which is just a subdirectory under it, so this works even + * before ocre_create_context() has run. */ + if (fs_statvfs("/lfs", &flash_stats) == 0) { + size_t total = (size_t)flash_stats.f_blocks * flash_stats.f_frsize; + size_t free_bytes = (size_t)flash_stats.f_bfree * flash_stats.f_frsize; + + print_ocre_stats_line("flash", total, total - free_bytes); + } + + k_sleep(K_SECONDS(1)); + } +} + +K_THREAD_DEFINE(ocre_stats_tid, 2048, stats_thread_fn, NULL, NULL, NULL, K_PRIO_PREEMPT(10), 0, 0); + +static void start_container(void) +{ + print_heap_stats("before create"); + + /* "ocre:api" is what makes wamr.c register this module's custom data + * and, on teardown, actually run ocre_cleanup_module_resources() -- + * without it, per-container resource cleanup handlers (e.g. GPIO's, + * which frees any pin registrations still owned by the outgoing + * module) never run at all. */ + static const char *capabilities[] = {"ocre:api", NULL}; + static const struct ocre_container_args args = {.capabilities = capabilities}; + + g_container = ocre_context_create_container(g_ctx, CONTAINER_IMAGE, "wamr/wasip1", CONTAINER_ID, true, &args, + STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO); + if (!g_container) { + printk("Failed to create container from fetched image\n"); + return; + } + + if (ocre_container_start(g_container)) { + printk("Failed to start container\n"); + ocre_context_remove_container(g_ctx, g_container); + g_container = NULL; + } +} + +/* Checks whether the code served at FETCH_URL has changed, and if so, + * installs it and reboots the whole board to run it. Only ever fetches the + * (small) hash first; the (potentially much larger) code itself is fetched + * -- streamed straight to flash, never fully buffered in RAM -- only when + * that hash differs from g_current_hash. + * + * The currently running container is left alone throughout: it isn't + * stopped before the download (streaming to flash needs only a couple KB of + * scratch, not the whole image) and it isn't restarted in place afterward + * either -- once the new image is safely installed, the board reboots, and + * the normal boot-time path (load_cached_image() + start_container() in + * main()) picks up the new code fresh, with a clean WiFi/network stack. + * That sidesteps needing any in-process container-swap machinery here at + * all, at the cost of a brief full restart for every update. */ +static void check_for_update(const char *workdir) +{ + char fetched_hash[FETCH_HASH_BUF_LEN]; + char tmp_path[128]; + char final_path[128]; + size_t new_len = 0; + int ret; + + if (!wifi_connected) { + printk("Skipping update check: WiFi not connected\n"); + return; + } + + for (int attempt = 1; attempt <= FETCH_HASH_MAX_ATTEMPTS; attempt++) { + ret = fetch_hash(fetched_hash, sizeof(fetched_hash)); + if (ret == 0) { + break; + } + + printk("Fetch from " FETCH_SERVER_ADDR ":%d" FETCH_HASH_URL " failed (%d), attempt %d/%d\n", + FETCH_SERVER_PORT, ret, attempt, FETCH_HASH_MAX_ATTEMPTS); + + if (attempt < FETCH_HASH_MAX_ATTEMPTS) { + k_sleep(K_MSEC(FETCH_HASH_RETRY_DELAY_MS)); + } + } + + if (ret) { + printk("Giving up on hash check after %d attempt(s); keeping current code\n", + FETCH_HASH_MAX_ATTEMPTS); + return; + } + + if (g_current_hash[0] != '\0' && strcmp(g_current_hash, fetched_hash) == 0) { + /* Unchanged -- nothing to do, and nothing worth logging every + * minute. */ + return; + } + + printk("New code hash detected (%s); fetching update\n", fetched_hash); + + snprintf(tmp_path, sizeof(tmp_path), "%s/images/%s", workdir, CONTAINER_IMAGE_TMP); + + for (int attempt = 1; attempt <= FETCH_CODE_MAX_ATTEMPTS; attempt++) { + ret = fetch_code_to_file(tmp_path, &new_len); + if (ret == 0) { + break; + } + + printk("Fetch from " FETCH_SERVER_ADDR ":%d" FETCH_URL " failed (%d), attempt %d/%d\n", + FETCH_SERVER_PORT, ret, attempt, FETCH_CODE_MAX_ATTEMPTS); + + if (attempt < FETCH_CODE_MAX_ATTEMPTS) { + k_sleep(K_MSEC(FETCH_CODE_RETRY_DELAY_MS)); + } + } + + if (ret) { + printk("Giving up after %d attempt(s); keeping current code\n", FETCH_CODE_MAX_ATTEMPTS); + unlink(tmp_path); + return; + } + + snprintf(final_path, sizeof(final_path), "%s/images/%s", workdir, CONTAINER_IMAGE); + + if (rename(tmp_path, final_path) != 0) { + printk("Failed to install fetched image: errno=%d; keeping current code\n", errno); + unlink(tmp_path); + return; + } + + strncpy(g_current_hash, fetched_hash, sizeof(g_current_hash) - 1); + g_current_hash[sizeof(g_current_hash) - 1] = '\0'; + save_hash_file(workdir); + + printk("New code installed (%zu bytes); rebooting to run it\n", new_len); + + /* Give the log line above (and the hash file write) a moment to + * actually flush before the reset. */ + k_sleep(K_MSEC(100)); + + native_led_blink(3); + + sys_reboot(SYS_REBOOT_COLD); +} + +/* If a previously fetched image is cached on flash, records its known hash + * (if any) and reports that it's ready to run -- the caller starts it. + * Nothing here loads the image itself into RAM; ocre_context_create_container() + * reads it directly from flash when actually instantiating the container. */ +static bool load_cached_image(const char *workdir) +{ + char path[128]; + struct stat st; + + snprintf(path, sizeof(path), "%s/images/%s", workdir, CONTAINER_IMAGE); + + if (stat(path, &st) != 0) { + return false; + } + + load_hash_file(workdir); + + printk("Found cached image from a previous fetch (%zu bytes); starting it\n", (size_t)st.st_size); + return true; +} + +int main(void) +{ + int rc = ocre_initialize(NULL); + + if (rc) { + fprintf(stderr, "Failed to initialize runtimes\n"); + return 1; + } + + g_ctx = ocre_create_context(NULL); + if (!g_ctx) { + fprintf(stderr, "Failed to create ocre context\n"); + return 1; + } + + const char *workdir = ocre_context_get_working_directory(g_ctx); + + if (!workdir) { + fprintf(stderr, "Failed to get working directory\n"); + return 1; + } + + /* Visual "the fetch sample just started" signal -- same on a fresh + * power-on as after an update-triggered reboot. Left off afterward + * for the container's own AssemblyScript to control. */ + native_led_blink(3); + + net_mgmt_init_event_callback(&wifi_mgmt_cb, wifi_mgmt_event_handler, + NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_WIFI_DISCONNECT_RESULT); + net_mgmt_add_event_callback(&wifi_mgmt_cb); + + wifi_connect(); + + /* Run whatever code we already have cached from a previous successful + * fetch, without waiting for the network to come up. */ + if (load_cached_image(workdir)) { + start_container(); + } else { + printk("No cached image yet; waiting for the first successful fetch\n"); + } + + /* Give WiFi association + DHCP a head start before the first fetch. */ + k_sleep(K_SECONDS(5)); + + check_for_update(workdir); + + while (1) { + k_sleep(FETCH_INTERVAL); + + if (!wifi_connected) { + wifi_connect(); + } + + check_for_update(workdir); + } + + return 0; +} diff --git a/src/samples/fetch/wifi_credentials.h b/src/samples/fetch/wifi_credentials.h new file mode 100644 index 00000000..7805ab45 --- /dev/null +++ b/src/samples/fetch/wifi_credentials.h @@ -0,0 +1,16 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef FETCH_WIFI_CREDENTIALS_H +#define FETCH_WIFI_CREDENTIALS_H + +/* Hardcoded for now. Move to a Kconfig option or provisioning mechanism + * before this sample is used outside of local development. */ +#define FETCH_WIFI_SSID "A1_1AAF" +#define FETCH_WIFI_PSK "48575443018F3FA8" + +#endif /* FETCH_WIFI_CREDENTIALS_H */ diff --git a/src/samples/fetch/zephyr/CMakeLists.txt b/src/samples/fetch/zephyr/CMakeLists.txt new file mode 100644 index 00000000..09c3febb --- /dev/null +++ b/src/samples/fetch/zephyr/CMakeLists.txt @@ -0,0 +1,29 @@ +# @copyright Copyright (c) contributors to Project Ocre, +# which has been established as Project Ocre a Series of LF Projects, LLC +# +# SPDX-License-Identifier: Apache-2.0 + +# This file is to be used to build a zephyr firmware including the ocre fetch +# sample, automatically uses the ocre module in the parent directory + +cmake_minimum_required(VERSION 3.20.0) + +# comment out to use the ocre module provided by Zephyr +list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_LIST_DIR}/../../../..) + +# append the fstab overlay file to the list of DTC overlay files +list(APPEND EXTRA_DTC_OVERLAY_FILE fstab.overlay) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project (ocre_sample_fetch) + +target_sources(app + PRIVATE + ../main.c +) + +target_link_libraries(app + PUBLIC + OcreCore +) diff --git a/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.conf b/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.conf new file mode 100644 index 00000000..de93be7f --- /dev/null +++ b/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.conf @@ -0,0 +1,10 @@ +# @copyright Copyright (c) contributors to Project Ocre, +# which has been established as Project Ocre a Series of LF Projects, LLC +# +# SPDX-License-Identifier: Apache-2.0 + +# Enables the ESP32 WiFi driver. This is the only board-specific piece of +# the fetch sample: main.c uses only portable Zephyr networking APIs +# (net_mgmt, sockets, http_client), so porting to another WiFi-capable board +# should only require adding an equivalent boards/.conf here. +CONFIG_WIFI_ESP32=y diff --git a/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.overlay b/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.overlay new file mode 100644 index 00000000..34697a22 --- /dev/null +++ b/src/samples/fetch/zephyr/boards/xiao_esp32c6_esp32c6_hpcore.overlay @@ -0,0 +1,33 @@ +/* + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + * + * Makes ADC1 channel 0 (physical pin "A0"/GPIO0 on the XIAO ESP32-C6's + * header) available to ocre_adc as the named channel "a0". Mirrors the + * devicetree pattern from Zephyr's own samples/drivers/adc/adc_dt sample + * (see socs/esp32c3.overlay there) -- the native ocre_adc.c code is + * unchanged across boards; only this overlay is board-specific. + */ + +/ { + zephyr,user { + io-channels = <&adc0 0>; + io-channel-names = "a0"; + }; +}; + +&adc0 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + channel@0 { + reg = <0>; + zephyr,gain = "ADC_GAIN_1_4"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,resolution = <12>; + }; +}; diff --git a/src/samples/fetch/zephyr/fstab.overlay b/src/samples/fetch/zephyr/fstab.overlay new file mode 100644 index 00000000..fe261aac --- /dev/null +++ b/src/samples/fetch/zephyr/fstab.overlay @@ -0,0 +1,23 @@ +/** + * @copyright Copyright (c) contributors to Project Ocre, + * which has been established as Project Ocre a Series of LF Projects, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + + / { + fstab { + compatible = "zephyr,fstab"; + lfs1: lfs1 { + compatible = "zephyr,fstab,littlefs"; + read-size = <256>; + prog-size = <256>; + cache-size = <256>; + lookahead-size = <256>; + block-cycles = <512>; + partition = <&storage_partition>; + mount-point = "/lfs"; + automount; + }; + }; +}; diff --git a/src/samples/fetch/zephyr/prj.conf b/src/samples/fetch/zephyr/prj.conf new file mode 100644 index 00000000..89823e70 --- /dev/null +++ b/src/samples/fetch/zephyr/prj.conf @@ -0,0 +1,83 @@ +# @copyright Copyright (c) contributors to Project Ocre, +# which has been established as Project Ocre a Series of LF Projects, LLC +# +# SPDX-License-Identifier: Apache-2.0 + +# Logging +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y + +# Filesystem +CONFIG_ZVFS_POLL_MAX=11 +CONFIG_ZVFS_OPEN_MAX=16 + +# Memory options +CONFIG_DYNAMIC_THREAD_STACK_SIZE=8192 +CONFIG_MAIN_STACK_SIZE=8192 +CONFIG_SYS_HEAP_RUNTIME_STATS=y + +# sys_reboot(): a code update reboots the whole board to run it, rather than +# swapping the container in place. +CONFIG_REBOOT=y + +# Zephyr's POSIX layer hands out pthread mutexes/conds from small fixed-size +# pools (default 5): ocre_common, ocre_dispatch, ocre_http, container.c, and +# ocre.c/context.c between them statically declare more than that, so the +# default silently runs out (pthread_mutex_init returns ENOMEM) the moment a +# container is actually created with GPIO+ADC+HTTP all enabled together. +CONFIG_MAX_PTHREAD_MUTEX_COUNT=16 +CONFIG_MAX_PTHREAD_COND_COUNT=16 + +# Networking (portable, board-agnostic; the WiFi driver itself is enabled +# per-board under boards/*.conf, since it differs per SoC). +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=n +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_HTTP_CLIENT=y + +# Both defaulted small (4 connections, 6 contexts) and shared between the +# native HTTP server (a listener plus up to CONFIG_HTTP_SERVER_MAX_CLIENTS +# accepted connections) and this sample's own outbound hash/code fetches +# (each check opens and closes its own client connection, which then sits +# in TIME_WAIT for CONFIG_NET_TCP_TIME_WAIT_DELAY ms). At the default pool +# sizes, frequent checks plus a few retries could exhaust the pool faster +# than TIME_WAIT entries free back up, making every subsequent connect() +# fail until something ages out -- raised well past the sum of everything +# that can be in flight at once. +CONFIG_NET_MAX_CONN=10 +CONFIG_NET_MAX_CONTEXTS=12 + +CONFIG_WIFI=y +CONFIG_NET_L2_WIFI_MGMT=y + +# Automatically start a DHCPv4 client once the network interface comes up. +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_DHCPV4=y + +# Native HTTP server backing the container's onRequest() export (see the +# ocre-as CLI / AssemblyScript bindings). Portable Zephyr subsystem, no +# board-specific config needed. +CONFIG_HTTP_SERVER=y +CONFIG_HTTP_SERVER_MAX_CLIENTS=4 +CONFIG_HTTP_PARSER=y +CONFIG_HTTP_PARSER_URL=y +CONFIG_OCRE_HTTP_SERVER=y + +# Outbound HTTP client API (ocre_http_get()), demonstrated in gpio-demo by +# polling the same host's /getHash endpoint from within loop(). Shares +# CONFIG_HTTP_CLIENT with this sample's own native update-fetch code above. +CONFIG_OCRE_HTTP_CLIENT=y + +# GPIO and analog-input (ADC) native APIs. GPIO by-name aliases (led0, sw0, +# ...) and the ADC "zephyr,user" io-channels overlay are both resolved from +# devicetree, so containers built for this API run unmodified on any board +# that defines the same names -- see boards/*.overlay for this board's ADC +# channel mapping. +CONFIG_OCRE_GPIO=y +CONFIG_ADC=y +CONFIG_OCRE_ADC=y + +# Ocre configuration +CONFIG_OCRE=y diff --git a/src/samples/mini/main.c b/src/samples/mini/main.c index 3890b9f3..a4477c25 100644 --- a/src/samples/mini/main.c +++ b/src/samples/mini/main.c @@ -19,6 +19,11 @@ #include "nsi_main.h" #endif +#if defined(__ZEPHYR__) && !defined(CONFIG_ARCH_POSIX) +#include +#include +#endif + extern const unsigned char ocre_mini_sample_image[]; extern const size_t ocre_mini_sample_image_len; @@ -115,6 +120,13 @@ int main(int argc, char *argv[]) ocre_deinitialize(); +#if defined(__ZEPHYR__) && !defined(CONFIG_ARCH_POSIX) + while (1) { + printk("Ocre mini sample is alive\n"); + k_sleep(K_SECONDS(1)); + } +#endif + /* Exit simulator on zephyr */ #ifdef CONFIG_ARCH_POSIX diff --git a/west.yml b/west.yml index 3c94e51d..4180bb1c 100644 --- a/west.yml +++ b/west.yml @@ -17,6 +17,9 @@ manifest: - hal_st - hal_nordic - hal_rpi_pico + - hal_espressif - cmsis_6 + - mbedtls + - tf-psa-crypto self: path: ocre-runtime diff --git a/zephyr/Kconfig b/zephyr/Kconfig index c45fbf0c..09319baa 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -194,6 +194,62 @@ config OCRE_SHELL help Enable the OCRE Shell for dynamic configuration management. +config OCRE_HTTP_SERVER + bool "Enable OCRE native HTTP request/response API" + depends on HTTP_SERVER + default n + help + Runs a native HTTP server (Zephyr's http_server subsystem, outside + the WASM sandbox) that catches every request regardless of path or + method and hands it to the active container's exported + "onRequest(requestId)" function. The container reads the request + with ocre_http_get_method/path/query/header/body and answers with + ocre_http_respond -- all routing logic lives in container code, not + in the native layer. + +if OCRE_HTTP_SERVER + +config OCRE_HTTP_SERVER_PORT + int "Port for the OCRE native HTTP server" + default 8081 + help + TCP port the native HTTP server listens on. + +endif # OCRE_HTTP_SERVER + +config OCRE_HTTP_CLIENT + bool "Enable OCRE native outbound HTTP client API" + depends on HTTP_CLIENT + default n + help + Exposes a blocking ocre_http_get() to WASM containers, letting + container code issue outbound HTTP GET requests via Zephyr's HTTP + client subsystem (outside the WASM sandbox) and read the response + body back into container memory. Runs on the calling instance's own + thread, so -- like any blocking native call -- it stalls that + instance's loop()/onRequest() dispatch for the duration of the + request. + +if OCRE_HTTP_CLIENT + +config OCRE_HTTP_CLIENT_TIMEOUT_MS + int "Timeout for OCRE HTTP client requests" + default 5000 + help + Timeout, in milliseconds, for outbound requests made via + ocre_http_get(). + +endif # OCRE_HTTP_CLIENT + +config OCRE_LOOP_INTERVAL_MS + int "Interval, in milliseconds, between native calls to a container's loop()" + default 100 + help + Once a container's "main" export returns, if it also exports a + "loop" function, the native side keeps calling it on this interval + (in place of the container looping forever inside a single WASM + call) until the container is replaced or removed. + comment "Logging" module = OCRE @@ -243,6 +299,37 @@ config OCRE_GPIO_PINS_PER_PORT endif # OCRE_GPIO +config OCRE_ADC + bool "Enable OCRE ADC (analog input) support" + depends on ADC + default n + help + Exposes named analog-input channels (e.g. "a0") to WASM containers, + pre-scaled to 0-255. Resolves channels via the same devicetree + "zephyr,user" io-channels convention as Zephyr's own + samples/drivers/adc/adc_dt sample, so the native code is unchanged + across boards -- only a board's devicetree overlay determines which + channel names actually resolve. + +if OCRE_ADC +config OCRE_ADC_MAX_CHANNELS + int "Maximum number of named ADC channels" + default 4 + help + Maximum number of named analog-input channels ("a0".."a3") the OCRE + ADC driver will resolve at init time. + +config OCRE_ADC_VREF_MV + int "Full-scale voltage (mV) used to scale ADC readings to 0-255" + default 3300 + help + Assumed full-scale input voltage, in millivolts, for channels whose + reference voltage is known from devicetree (e.g. zephyr,vref-mv). + Channels without a known reference voltage instead scale the raw + ADC code directly against the channel's resolution. + +endif # OCRE_ADC + config OCRE_SENSORS bool "Enable OCRE Sensors support" default n