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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,22 @@ jobs:
working-directory: app
run: west twister --testsuite-root src -p native_sim --inline-logs -O /tmp/twister-out

- name: Install infra-test Python deps
run: pip install aiocoap pytest-asyncio

- name: Run infra tests on native_sim
run: |
west twister \
--testsuite-root modules/lib/zephlet/tests \
-p native_sim --inline-logs \
-O /tmp/twister-out-infra

- name: Upload twister artefacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: twister-out
path: /tmp/twister-out
path: |
/tmp/twister-out
/tmp/twister-out-infra
if-no-files-found: ignore
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ghcr.io/zephyrproject-rtos/zephyr-build:main

USER root

# Codegen runtime deps + pytest deps for the zephlet test suites.
# Keeping these in the image avoids a pip install on every twister
# invocation. The image's venv lives at /opt/python/venv and is
# owned by root, hence the explicit USER root above.
RUN pip install --quiet \
aiocoap pytest pytest-asyncio \
proto-schema-parser jinja2 pkl-python copier \
tree-sitter tree-sitter-c
50 changes: 49 additions & 1 deletion Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,63 @@ config ZEPHLETS_COAP
select COAP
select COAP_SERVER
select COAP_URI_WILDCARD
select COAP_EXTENDED_OPTIONS_LEN
help
Expose opted-in zephlets over CoAP (UDP).
Expose opted-in zephlets over CoAP (UDP). Extended option length
is required so URI-Path segments up to
`CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN` bytes survive
`coap_find_options` without truncation.

config COAP_EXTENDED_OPTIONS_LEN_VALUE
# Leave headroom above CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN so the
# CoAP parser accepts segments slightly longer than what zephlet
# handlers will copy out; the handler then surfaces 4.04 for the
# overflow case. A flat 32 here would make the parser reject the
# frame before the handler ever runs.
default 64 if ZEPHLETS_COAP

config ZEPHLETS_COAP_DTLS
bool "DTLS for the CoAP frontend"
depends on ZEPHLETS_COAP
help
Wrap the CoAP frontend in DTLS (PSK).

config ZEPHLETS_COAP_MAX_SEGMENT_LEN
int "Maximum length of a CoAP URI segment under /zlet/"
depends on ZEPHLETS_COAP
default 32
help
Stack-buffer size (excluding the null terminator) for each URI
segment that a zephlet's CoAP handler copies out of the request's
`COAP_OPTION_URI_PATH` options. Inbound segments longer than this
are rejected with 4.04. The reasonable floor is the longest
`name` used in `ZEPHLET_NEW(...)`; the floor for method segments
is the longest rpc name declared in any opted-in `.proto`.

config ZEPHLETS_COAP_MAX_URI_SEGMENTS
int "Maximum number of CoAP URI path segments per request"
depends on ZEPHLETS_COAP
default 8
help
Stack-array size handed to `coap_find_options` when a zephlet's
CoAP handler decodes a request's URI path. RPC dispatch needs
4 segments (`zlet/<type>/<instance>/<method>`); the
`events/stats` form needs 5. The default leaves headroom for
future path shapes; requests with more segments than this are
rejected with 4.04.

config ZEPHLETS_COAP_CMD_TIMEOUT_MS
int "Timeout for publishing a CoAP-originated call onto the command channel (ms)"
depends on ZEPHLETS_COAP
default 100
help
Upper bound the CoAP handler is willing to wait for
`zbus_chan_pub()` on a zephlet's `command` channel before
surfacing 5.03 to the caller. Only the channel mutex
contention is relevant — the sync listener runs in the
publisher's own thread, so this never times out a handler
that is making forward progress.

module = ZEPHLET
module-str = zephlet
source "subsys/logging/Kconfig.template.log_config"
Expand Down
151 changes: 138 additions & 13 deletions codegen/templates/zephlet_coap_interface.c.jinja
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
/* GENERATED FILE — DO NOT EDIT. Source: {{ prefix }}.proto. */

{% if coap_opt_in %}
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/coap.h>
#include <zephyr/net/coap_service.h>
#include <zephyr/sys/iterable_sections.h>
#include <zephyr/zbus/zbus.h>

#include <pb_decode.h>
#include <pb_encode.h>

#include "zephlet.h"
#include "zephlet_coap_consts.h"
#include "zephlet_coap_send.h"
#include "zephlet_coap_types.h"
#include "zephlet_coap_uri.h"
#include "{{ prefix }}_interface.h"
#include "{{ type_snake }}/{{ prefix }}.pb.h"

/**
* @file
* @brief CoAP frontend descriptor for the `{{ type_snake }}` zephlet type.
* @brief CoAP frontend for the `{{ type_snake }}` zephlet type.
*
* Compiled only when `CONFIG_ZEPHLETS_COAP=y`. Owns:
* - the per-method descriptor table walked by discovery code,
* - the `zephlet_coap_type` section-iterable record,
* - the type's `COAP_RESOURCE_DEFINE` registered against
* `zlet_coap_service` at path `/zlet/{{ type_snake }}/#`,
* - the POST handler that proto-decodes the request, runs the
* `{{ type_snake }}` dispatch trampoline via zbus, and proto-encodes
* the response.
*
* Compiled only when `CONFIG_ZEPHLETS_COAP=y` (CMake gate). Emits one
* per-method descriptor table and a single section-iterable
* `zephlet_coap_type` record that the Phase 3 catch-all dispatcher
* walks. The `_{{ type_snake }}_coap_event_cb` body is a stub here;
* Phase 5 fills in the per-observer CoAP Observe send loop.
* The handler's stack frame is sized by the compiler from the typed
* per-method locals in each `if (strcmp(...))` branch — there is no
* cross-method scratch and no opaque buffer.
*/

LOG_MODULE_DECLARE(zlet_coap);

static const struct zephlet_coap_method {{ type_snake }}_coap_methods[] = {
{% for cmd in commands %}
{% for cmd in commands %}
{
.path_segment = "{{ cmd.name }}",
.method_id = {{ cmd.method_id }},
Expand All @@ -32,7 +54,7 @@ static const struct zephlet_coap_method {{ type_snake }}_coap_methods[] = {
.req_max_size = {% if cmd.req_is_empty %}0{% else %}{{ cmd.req_c_name|upper }}_SIZE{% endif %},
.resp_max_size = {% if cmd.resp_is_empty %}0{% else %}{{ cmd.resp_c_name|upper }}_SIZE{% endif %},
},
{% endfor %}
{% endfor %}
};

const STRUCT_SECTION_ITERABLE(zephlet_coap_type, {{ type_snake }}_coap_type) = {
Expand All @@ -47,14 +69,117 @@ void _{{ type_snake }}_coap_event_cb(const struct zephlet *z,
{
ARG_UNUSED(z);
ARG_UNUSED(ev);
/* Phase 5: walk Zephyr's global observer list filtered by
* `observer->user_data == z` and send one NON per match. */
}

static int {{ type_snake }}_resolve(struct coap_packet *req, const struct zephlet **z_out,
char *method_buf, size_t method_buf_size)
{
struct coap_option opts[CONFIG_ZEPHLETS_COAP_MAX_URI_SEGMENTS];
size_t n;
int err = zephlet_coap_parse_uri_path(req, opts, ARRAY_SIZE(opts), &n);
if (err != 0 || n != 4) {
return -ENODEV;
}

/* opts[0] == "zlet" and opts[1] == "{{ type_snake }}" are guaranteed
* by the resource path match. Instance and method are the variables. */
char instance[CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN + 1];
if (zephlet_coap_option_to_cstr(&opts[2], instance, sizeof(instance)) != 0) {
return -ENODEV;
}
if (zephlet_coap_option_to_cstr(&opts[3], method_buf, method_buf_size) != 0) {
return -ENODEV;
}

const struct zephlet *z = zephlet_get_by_name(instance);
if (z == NULL || z->api != &{{ type_snake }}_api) {
return -ENODEV;
}
*z_out = z;
return 0;
}

static int {{ type_snake }}_coap_rpc_post(struct coap_resource *res, struct coap_packet *req,
struct sockaddr *addr, socklen_t addr_len)
{
const struct zephlet *z;
char method[CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN + 1];

int err = {{ type_snake }}_resolve(req, &z, method, sizeof(method));
if (err != 0) {
return zephlet_coap_send_error(res, req, addr, addr_len, err);
}

{% for cmd in commands %}
if (strcmp(method, "{{ cmd.name }}") == 0) {
{% if not cmd.req_is_empty %}
uint16_t body_len;
const uint8_t *body = coap_packet_get_payload(req, &body_len);
struct {{ cmd.req_c_name }} req_struct;
pb_istream_t in = pb_istream_from_buffer(body, body_len);
if (!pb_decode(&in, &{{ cmd.req_desc }}, &req_struct)) {
return zephlet_coap_send_error(res, req, addr, addr_len, -EINVAL);
}
{% endif %}
{% if not cmd.resp_is_empty %}
struct {{ cmd.resp_c_name }} resp = {0};
uint8_t payload[{{ cmd.resp_c_name|upper }}_SIZE];
{% endif %}
struct zephlet_call call = {
.method_id = {{ cmd.method_id }},
{% if not cmd.req_is_empty %}
.req_desc = &{{ cmd.req_desc }},
.req = &req_struct,
{% endif %}
{% if not cmd.resp_is_empty %}
.resp_desc = &{{ cmd.resp_desc }},
.resp = &resp,
{% endif %}
};
struct zephlet_call *cp = &call;
int pub = zbus_chan_pub(z->channel.command, &cp,
K_MSEC(CONFIG_ZEPHLETS_COAP_CMD_TIMEOUT_MS));
if (pub != 0) {
return zephlet_coap_send_error(res, req, addr, addr_len, -EBUSY);
}
if (call.return_code != 0) {
return zephlet_coap_send_error(res, req, addr, addr_len,
call.return_code);
}
{% if cmd.resp_is_empty %}
return zephlet_coap_send_response(res, req, addr, addr_len, 0, NULL, 0);
{% else %}
pb_ostream_t out = pb_ostream_from_buffer(payload, sizeof(payload));
if (!pb_encode(&out, &{{ cmd.resp_desc }}, &resp)) {
return zephlet_coap_send_error(res, req, addr, addr_len, -ENOMEM);
}
return zephlet_coap_send_response(res, req, addr, addr_len, 0, payload,
out.bytes_written);
{% endif %}
}
{% endfor %}

return zephlet_coap_send_error(res, req, addr, addr_len, -ENOSYS);
}

static int {{ type_snake }}_coap_get(struct coap_resource *res, struct coap_packet *req,
struct sockaddr *addr, socklen_t addr_len)
{
return zephlet_coap_send_error(res, req, addr, addr_len, -ENOSYS);
}

static const char *const {{ type_snake }}_coap_resource_path[] = {
"zlet", "{{ type_snake }}", "#", NULL,
};

COAP_RESOURCE_DEFINE({{ type_snake }}_coap_resource, zlet_coap_service, {
.path = {{ type_snake }}_coap_resource_path,
.post = {{ type_snake }}_coap_rpc_post,
.get = {{ type_snake }}_coap_get,
});
{% else %}
/*
* `{{ prefix }}.proto` does not opt in to the CoAP frontend, so this
* translation unit emits no symbols. The CMake gate compiles this file
* unconditionally under `CONFIG_ZEPHLETS_COAP=y`; an empty TU contributes
* zero to the section-hash gate.
* translation unit emits no symbols.
*/
{% endif %}
5 changes: 3 additions & 2 deletions codegen/templates/zephlet_coap_interface.h.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
/**
* @brief Event-channel observer callback for `{{ type_snake }}` instances.
*
* Defined in `{{ prefix }}_coap_interface.c`. Phase 1 emits a stub body;
* Phase 5 fills in the per-observer CoAP Observe send logic.
* Defined in `{{ prefix }}_coap_interface.c`. The body is a stub today;
* the events-bridge implementation will fill in the per-observer CoAP
* Observe send logic.
*/
void _{{ type_snake }}_coap_event_cb(const struct zephlet *z,
const struct {{ type_snake }}_events *ev);
Expand Down
14 changes: 14 additions & 0 deletions codegen/zephyr_zephlet_codegen.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,18 @@ function(zephyr_zephlet_generate)
zephyr_library_sources("${CMAKE_CURRENT_SOURCE_DIR}/${_src}")
endforeach()
add_dependencies(${ZEPHYR_CURRENT_LIBRARY} ${_zg_PREFIX}_codegen)

# The user's `app` library also #includes the generated <prefix>_interface.h
# transitively (via the user's <prefix>.h). Without an explicit dependency,
# ninja may try to compile `main.c` before the codegen finishes, which
# surfaces on slower hosts as a missing-header build error. Defer the
# `add_dependencies` call so it runs after the user's CMakeLists has
# declared the `app` target.
#
# `DEFER CALL` evaluates variable references at the time the deferred call
# runs, when `_zg_PREFIX` is already out of scope. Use `EVAL CODE` to
# substitute the prefix into the deferred command at registration time.
cmake_language(EVAL CODE
"cmake_language(DEFER DIRECTORY \${CMAKE_SOURCE_DIR} \
CALL add_dependencies app ${_zg_PREFIX}_codegen)")
endfunction()
2 changes: 2 additions & 0 deletions frontends/coap/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ if(CONFIG_ZEPHLETS_COAP)
zephyr_library_sources(
zephlet_coap_frontend.c
zephlet_coap_translate.c
zephlet_coap_send.c
zephlet_coap_uri.c
)
zephyr_include_directories(include)
endif()
Loading
Loading