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
2 changes: 2 additions & 0 deletions codegen/templates/zephlet_coap_interface.c.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ static const struct zephlet_coap_method {{ type_snake }}_coap_methods[] = {
.method_id = {{ cmd.method_id }},
.req_desc = {% if cmd.req_desc %}&{{ cmd.req_desc }}{% else %}NULL{% endif %},
.resp_desc = {% if cmd.resp_desc %}&{{ cmd.resp_desc }}{% else %}NULL{% endif %},
.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 %}
};
Expand Down
5 changes: 4 additions & 1 deletion frontends/coap/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
if(CONFIG_ZEPHLETS_COAP)
zephyr_library_named(zephlet_coap_frontend)
zephyr_library_sources(zephlet_coap_frontend.c)
zephyr_library_sources(
zephlet_coap_frontend.c
zephlet_coap_translate.c
)
zephyr_include_directories(include)
endif()
126 changes: 126 additions & 0 deletions frontends/coap/include/zephlet_coap_translate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#ifndef ZEPHLET_FRONTENDS_COAP_TRANSLATE_H
#define ZEPHLET_FRONTENDS_COAP_TRANSLATE_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include <zephyr/net/coap.h>

#include "zephlet.h"
#include "zephlet_coap_types.h"

/**
* @file
* @brief Pure envelope ↔ CoAP payload translation for the CoAP frontend.
*
* Transport-free helpers used by the Phase 3 catch-all dispatcher and the
* Phase 5 events bridge. None of these functions touch `coap_service`,
* sockets, or zbus — they operate on raw buffers and pre-initialised
* `struct coap_packet` instances, so they are unit-testable in isolation.
*
* Wire format: request and response bodies are nanopb-encoded protobuf
* messages described by the same `pb_msgdesc_t` descriptors that
* `<prefix>_interface.c` references for local dispatch. Content-Format
* `ZEPHLET_COAP_CT_NANOPB` (65001) tags outbound payloads.
*/

/**
* @brief Map a zephlet handler return code to a CoAP response code byte.
*
* The mapping table is the authoritative one pinned in the CoAP frontend
* adoption plan:
*
* rc == 0, has_payload → 2.05 Content
* rc == 0, no payload → 2.04 Changed
* rc == -EINVAL → 4.00 Bad Request
* rc == -ENODEV → 4.04 Not Found
* rc == -ENOSYS → 4.05 Method Not Allowed
* rc == -EALREADY → 4.09 Conflict
* rc == -EMSGSIZE → 4.13 Request Entity Too Large
* rc == -EBUSY/-EAGAIN → 5.03 Service Unavailable
* rc == -ETIMEDOUT → 5.04 Gateway Timeout
* rc == -ENOMEM → 5.00 Internal Server Error
* any other negative rc → 5.00 Internal Server Error (default)
*
* Positive rc values are not produced by zephlet handlers; they are
* treated the same as "any other".
*
* @param rc Handler return code (POSIX errno; 0 == success).
* @param has_payload Only consulted when @p rc == 0. True selects 2.05;
* false selects 2.04.
*
* @return CoAP response code byte (`COAP_MAKE_RESPONSE_CODE(class, detail)`).
*/
uint8_t zephlet_coap_map_return_code(int32_t rc, bool has_payload);

/**
* @brief Decode a CoAP request body into a `struct zephlet_call`.
*
* On success the function fills @p call with `method_id`, `req_desc`,
* `resp_desc`, and `req = req_storage`. The caller is responsible for
* `resp` / `resp_desc` storage and for any prior CoAP-side validation
* (URI segments, Content-Format option).
*
* - `len > m->req_max_size` → `-EMSGSIZE`; dispatcher surfaces 4.13.
* The bound is the codegen-emitted `<MSG>_SIZE` from nanopb, so the
* decoder enforces the exact per-method envelope (Empty requests
* have `req_max_size == 0`, so any inbound body is rejected here).
* - nanopb decode failure (truncated, malformed varints, type
* mismatch) → `-EINVAL`; dispatcher surfaces 4.00.
*
* @param buf CoAP payload bytes (may be NULL if @p len == 0).
* @param len Number of payload bytes.
* @param m Method descriptor (matched
* `zephlet_coap_method`). Must be non-NULL.
* @param call Envelope to populate. Must be non-NULL.
* @param req_storage Caller-owned request struct storage (e.g. stack-
* allocated `struct <Type>Req`). Must be non-NULL
* when `m->req_desc != NULL`.
*
* @return 0 on success;
* `-EINVAL` on malformed body or NULL-argument violation;
* `-EMSGSIZE` on oversized body.
*/
int zephlet_coap_decode_request(const uint8_t *buf, size_t len, const struct zephlet_coap_method *m,
struct zephlet_call *call, void *req_storage);

/**
* @brief Encode a post-dispatch envelope into a CoAP response packet.
*
* @p cpkt must already have been initialised by the caller via
* `coap_packet_init` with the appropriate version, type, token, and
* message ID (matching the request). This helper:
*
* 1. Sets the response code from `call->return_code` via
* `zephlet_coap_map_return_code`.
* 2. Always appends the verbatim-errno option
* (`ZEPHLET_COAP_OPT_ERRNO = 65052`) carrying `call->return_code`
* reinterpreted as `uint32_t`. The receiver re-casts to `int32_t`.
* 3. When `call->return_code == 0` and `call->resp_desc != NULL`,
* encodes the response payload through @p scratch using nanopb. If
* the encoded size is greater than zero, the response code is
* upgraded to 2.05 Content, a Content-Format option (65001) is
* appended, the payload marker is written, and the encoded bytes
* are appended.
*
* @p scratch is required only when the caller can produce a non-empty
* payload (success + non-NULL `resp_desc`). Size it from nanopb's
* compile-time `<msg>_size` macros for the relevant zephlet type's
* largest opted-in response message.
*
* @param call Envelope to serialise (must be non-NULL).
* @param cpkt Pre-initialised CoAP packet (must be non-NULL).
* @param scratch Encoder scratch buffer (may be NULL when no
* payload is expected).
* @param scratch_size Size of @p scratch in bytes.
*
* @return 0 on success;
* `-ENOMEM` if either @p scratch is insufficient for the encoded
* response or the CoAP packet's underlying buffer cannot fit the
* options/payload.
*/
int zephlet_coap_encode_response(const struct zephlet_call *call, struct coap_packet *cpkt,
uint8_t *scratch, size_t scratch_size);

#endif /* ZEPHLET_FRONTENDS_COAP_TRANSLATE_H */
8 changes: 7 additions & 1 deletion frontends/coap/include/zephlet_coap_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,19 @@
* in the .proto service block). `method_id` indexes into
* `zephlet_api.methods` for the dispatch trampoline. `req_desc` /
* `resp_desc` are the nanopb descriptors used by the Phase 2 envelope
* decode/encode.
* decode/encode. `req_max_size` / `resp_max_size` are the nanopb-
* computed maximum encoded sizes (from `<MSG>_SIZE` in `<prefix>.pb.h`)
* — zero for `Empty` messages. Phase 2 decoder rejects request bodies
* larger than `req_max_size` with `-EMSGSIZE`; Phase 3 dispatcher uses
* `resp_max_size` to size per-type response scratch.
*/
struct zephlet_coap_method {
const char *path_segment;
uint16_t method_id;
const pb_msgdesc_t *req_desc;
const pb_msgdesc_t *resp_desc;
size_t req_max_size;
size_t resp_max_size;
};

/**
Expand Down
162 changes: 162 additions & 0 deletions frontends/coap/zephlet_coap_translate.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#include "zephlet_coap_translate.h"

#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include <zephyr/kernel.h>
#include <zephyr/net/coap.h>

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

#include "zephlet.h"
#include "zephlet_coap_consts.h"
#include "zephlet_coap_types.h"

/**
* @file
* @brief Pure CoAP ↔ zephlet envelope translation.
*
* Implementation notes:
* - Compiled only when `CONFIG_ZEPHLETS_COAP=y` (CMake gate).
* - No dependency on `coap_service`, sockets, or zbus. All inputs are
* buffers or pre-initialised `struct coap_packet` instances; all
* outputs are byte-level CoAP options and payloads.
*/

uint8_t zephlet_coap_map_return_code(int32_t rc, bool has_payload)
{
if (rc == 0) {
if (has_payload) {
return COAP_RESPONSE_CODE_CONTENT;
}
return COAP_RESPONSE_CODE_CHANGED;
}

switch (rc) {
case -EINVAL:
return COAP_RESPONSE_CODE_BAD_REQUEST;
case -ENODEV:
return COAP_RESPONSE_CODE_NOT_FOUND;
case -ENOSYS:
return COAP_RESPONSE_CODE_NOT_ALLOWED;
case -EALREADY:
return COAP_RESPONSE_CODE_CONFLICT;
case -EMSGSIZE:
return COAP_RESPONSE_CODE_REQUEST_TOO_LARGE;
case -EBUSY:
case -EAGAIN:
return COAP_RESPONSE_CODE_SERVICE_UNAVAILABLE;
case -ETIMEDOUT:
return COAP_RESPONSE_CODE_GATEWAY_TIMEOUT;
case -ENOMEM:
return COAP_RESPONSE_CODE_INTERNAL_ERROR;
default:
return COAP_RESPONSE_CODE_INTERNAL_ERROR;
}
}

int zephlet_coap_decode_request(const uint8_t *buf, size_t len, const struct zephlet_coap_method *m,
struct zephlet_call *call, void *req_storage)
{
if (m == NULL || call == NULL) {
return -EINVAL;
}

/* Per-method bound emitted by codegen from the proto's nanopb
* `<MSG>_SIZE` constant. A request body larger than the method's
* own message envelope is a malformed wire frame; surface 4.13. */
if (len > m->req_max_size) {
return -EMSGSIZE;
}

call->method_id = m->method_id;
call->req_desc = m->req_desc;
call->resp_desc = m->resp_desc;
call->req = NULL;
call->return_code = 0;
/* resp / resp_desc storage is the caller's responsibility (Phase 3
* dispatcher allocates the response struct on its stack). */

if (m->req_desc == NULL) {
/* Empty request: req_max_size is also 0 so the bound check
* above already rejected any inbound bytes. */
return 0;
}

if (req_storage == NULL) {
return -EINVAL;
}

pb_istream_t stream = pb_istream_from_buffer(buf, len);
if (!pb_decode(&stream, m->req_desc, req_storage)) {
return -EINVAL;
}

call->req = req_storage;
return 0;
}

int zephlet_coap_encode_response(const struct zephlet_call *call, struct coap_packet *cpkt,
uint8_t *scratch, size_t scratch_size)
{
if (call == NULL || cpkt == NULL) {
return -EINVAL;
}

size_t payload_len = 0;
bool has_payload = false;

/* On success with a response descriptor, attempt to encode the
* payload into @p scratch up front so we know whether to mark the
* code as 2.04 or 2.05. */
if (call->return_code == 0 && call->resp_desc != NULL && call->resp != NULL) {
if (scratch == NULL || scratch_size == 0) {
return -ENOMEM;
}
pb_ostream_t os = pb_ostream_from_buffer(scratch, scratch_size);
if (!pb_encode(&os, call->resp_desc, call->resp)) {
return -ENOMEM;
}
payload_len = os.bytes_written;
has_payload = (payload_len > 0);
}

int err = coap_header_set_code(
cpkt, zephlet_coap_map_return_code(call->return_code, has_payload));
if (err < 0) {
return -ENOMEM;
}

/* Verbatim raw errno is carried on every response so the (lossy)
* class mapping does not erase the precise handler return code. The
* signed value is reinterpreted as unsigned for the wire; receivers
* cast back to int32_t. */
err = coap_append_option_int(cpkt, ZEPHLET_COAP_OPT_ERRNO, (uint32_t)call->return_code);
if (err < 0) {
return -ENOMEM;
}

if (!has_payload) {
return 0;
}

err = coap_append_option_int(cpkt, COAP_OPTION_CONTENT_FORMAT, ZEPHLET_COAP_CT_NANOPB);
if (err < 0) {
return -ENOMEM;
}

err = coap_packet_append_payload_marker(cpkt);
if (err < 0) {
return -ENOMEM;
}

err = coap_packet_append_payload(cpkt, scratch, (uint16_t)payload_len);
if (err < 0) {
return -ENOMEM;
}

return 0;
}
31 changes: 31 additions & 0 deletions tests/coap_translate/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
cmake_minimum_required(VERSION 3.20.0)

# Walk up to the infra root (modules/lib/zephlet/) from this test dir.
get_filename_component(ZEPHLET_INFRA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)

# Register the infra module so zephlet.h's include dir is exported and
# zephlet.c gets pulled in by CONFIG_ZEPHLETS=y.
list(APPEND EXTRA_ZEPHYR_MODULES "${ZEPHLET_INFRA_ROOT}")

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

project(zephlet_coap_translate_test)

# Compile the translate translation unit directly so the unit test does
# not have to enable CONFIG_ZEPHLETS_COAP (which selects NETWORKING /
# NET_UDP / NET_SOCKETS / COAP_SERVER). Packet-level CoAP helpers come
# from CONFIG_COAP=y alone.
target_sources(app PRIVATE
src/main.c
${ZEPHLET_INFRA_ROOT}/frontends/coap/zephlet_coap_translate.c
)
target_include_directories(app PRIVATE
${ZEPHLET_INFRA_ROOT}/frontends/coap/include
)

# Wire nanopb for the local test proto.
list(APPEND CMAKE_MODULE_PATH ${ZEPHYR_BASE}/modules/nanopb)
include(nanopb)
set(NANOPB_OPTIONS "--c-style")

zephyr_nanopb_sources(app src/test_msg.proto)
28 changes: 28 additions & 0 deletions tests/coap_translate/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CONFIG_ZTEST=y

# Zephlet shared headers (zephlet.h) + ZBUS/nanopb include paths.
CONFIG_ZEPHLETS=y

CONFIG_NANOPB=y

# `CONFIG_COAP` is gated by `NETWORKING` in Zephyr's Kconfig tree, so the
# networking subsystem entry must be enabled even though this unit test
# never touches a socket. Packet-level helpers (`coap_packet_init`,
# `coap_header_set_code`, `coap_append_option_int`, …) are all the test
# uses — no COAP_SERVER, no UDP, no L2 stack.
CONFIG_NETWORKING=y
CONFIG_COAP=y

# QEMU board defaults enable a SLIP serial back-end against
# /tmp/slip.sock when NETWORKING+NET_QEMU_SLIP+NET_SLIP_TAP all hold.
# This unit test has no networking peer, so disable those — otherwise
# the runner aborts before main() with "Failed to connect" on macOS.
CONFIG_NET_QEMU_SLIP=n
CONFIG_NET_SLIP_TAP=n

# Pulled-in net_if / coap code references sys_rand_get; the test PRNG
# satisfies the symbol without requiring an HW entropy backend.
CONFIG_TEST_RANDOM_GENERATOR=y

CONFIG_LOG=y
CONFIG_ASSERT=y
Loading
Loading