Skip to content

feat(coap): per-type RPC dispatch over /zlet/<type>/# (#31)#41

Merged
rodrigopex merged 6 commits into
mainfrom
feat/31-coap-rpc-dispatch
May 23, 2026
Merged

feat(coap): per-type RPC dispatch over /zlet/<type>/# (#31)#41
rodrigopex merged 6 commits into
mainfrom
feat/31-coap-rpc-dispatch

Conversation

@rodrigopex
Copy link
Copy Markdown
Owner

@rodrigopex rodrigopex commented May 22, 2026

Summary

Phase 3 of the CoAP frontend adoption: per-type RPC dispatch over UDP.

  • Codegen emits one COAP_RESOURCE_DEFINE per opted-in zephlet type at /zlet/<type>/# in <prefix>_coap_interface.c alongside a per-type POST handler. The handler parses instance + method out of the URI, validates z->api == &<type>_api defensively, and dispatches via a per-method switch with typed nanopb req/resp locals on the stack. The C compiler sizes the function frame from those locals — no cross-type max macros.
  • The frontend gains shared zephlet_coap_send_{error,response} and URI-parsing helpers. zephlet_coap_frontend.c owns the single COAP_SERVICE_DEFINE(zlet_coap_service, …).
  • Three new Kconfig knobs: MAX_SEGMENT_LEN, MAX_URI_SEGMENTS, CMD_TIMEOUT_MS. ZEPHLETS_COAP now selects COAP_EXTENDED_OPTIONS_LEN so URI segments up to the configured max survive coap_find_options without truncation.
  • Functional test target vendors minimal tick + ui zephlets so the api-mismatch path runs end-to-end. Pytest cases cover happy + five unhappy paths plus the GET stub (4.05 until the events bridge lands). The test target uses NET_NATIVE_OFFLOADED_SOCKETS so the host pytest fixture reaches the guest CoAP server directly on 127.0.0.1 — no TAP setup, no root, no /dev/net/tun — and the same suite runs identically on bare Linux, Docker, and Colima.
  • CI now also exercises modules/lib/zephlet/tests (the existing job only ran the app's per-zephlet integration tests). Codegen race fix added: defers add_dependencies(app <prefix>_codegen) via cmake_language(EVAL CODE …) so slower builders no longer compile main.c before the per-zephlet codegen finishes.

Test plan

  • west twister --testsuite-root modules/lib/zephlet/tests -p native_sim --inline-logs (CI on Linux) — 35 of 35 test cases pass, 4 of 4 configurations green: coap.translate (ztest), shared.dispatch (ztest), coap_functional.smoke (pytest), coap_functional.rpc (pytest, 8 RPC cases).
  • just docker-test from modules/lib/zephlet/ (local Apple Silicon, native_sim/native/64 via zephlet-tester:latest image) — same 35/35 pass.
  • just c b (app build on mps2/an385) — green, footprint identical to pre-Phase-3 (FLASH 50556 B, RAM 13972 B).
  • just test (app's per-zephlet integration tests) — 23/23 pass.
  • west twister --testsuite-root modules/lib/zephlet/tests -p mps2/an385 (local) — 22/22 ztest cases pass; pytest configurations correctly filtered (mps2/an385 has no networking).

Notes

  • The locked-decision "one catch-all /zlet/#" was relaxed in favor of per-type resources — each opted-in zephlet's codegen output owns its own resource registration. Gives each type self-contained CoAP routing and eliminates the catch-all's URI dispatch table.
  • Per-method scratch lives on the dispatch function's stack as typed locals; the compiler computes the frame from real C declarations, no MAX_FROM_LIST aggregator or generated handler-max header.
  • Phase 2's zephlet_coap_decode_request / ..._encode_response helpers stay in zephlet_coap_translate.c as a documented transport-free API for callers that prefer the wrapping; the dispatch path inlines pb_decode / pb_encode for explicitness.
  • Dockerfile + justfile at the infra root give a one-shot just docker-build / just docker-test workflow for local iteration without re-installing Python deps per run.

Closes #31

@rodrigopex rodrigopex self-assigned this May 23, 2026
@rodrigopex rodrigopex added the enhancement New feature or request label May 23, 2026
Each opted-in zephlet type owns one COAP_RESOURCE_DEFINE at
/zlet/<type>/# emitted by codegen alongside the per-type interface.
The handler parses instance + method from the URI, validates the
api against the type record, runs a per-method switch with typed
nanopb req/resp locals on the stack, publishes via zbus_chan_pub on
the command channel, and ships the encoded response through the
shared send helpers in the frontend.

Adds three Kconfig knobs (MAX_SEGMENT_LEN, MAX_URI_SEGMENTS,
CMD_TIMEOUT_MS), selects COAP_EXTENDED_OPTIONS_LEN so URI segments
up to the configured max survive coap_find_options, vendors a
minimal `tick` + `ui` test zephlet for the functional pytest, and
ships the 8-case `test_rpc.py` covering the happy path plus the
unhappy paths bracketing the routing + decode error map.

Refs #31
The per-zephlet `_codegen` custom target is already declared as a
dependency of the zephlet's own zephyr_library. The user's `app`
library that compiles `main.c` also #includes the generated
`<prefix>_interface.h` transitively (via the user-owned
`<prefix>.h`), but it has no dependency on the codegen target.
CMake's source-file scanner cannot infer the dependency because the
header does not yet exist at scan time, so ninja is free to race
the `main.c` compile against codegen. On x86_64 CI hosts the codegen
wins; on slower aarch64 Docker containers the compile starts first
and fails with `fatal error: <prefix>_interface.h: No such file or
directory`.

Defer an `add_dependencies(app ${_zg_PREFIX}_codegen)` via
`cmake_language(EVAL CODE ...)` so the prefix is substituted at
registration time (DEFER CALL would otherwise evaluate the variable
after it goes out of scope) and the dependency is wired up after
the user's CMakeLists has declared the `app` target.

Refs #31
Apple Silicon and other aarch64 hosts require the explicit
`native_sim/native/64` variant — Zephyr's CMake refuses to build the
default 32-bit variant on a 64-bit userspace. Listing both the bare
`native_sim` (matches x86_64 CI runners) and `native_sim/native/64`
(matches ARM64 local Docker containers) lets the same testcase yaml
serve both environments.
Drop `eth_native_tap` in favor of `NET_NATIVE_OFFLOADED_SOCKETS`. The
native simulator forwards Zephyr's socket calls to the host BSD
stack, so the CoAP server binds a real host socket. The pytest
fixture reaches it at 127.0.0.1:5683 without any TAP device, host-
side IP configuration, root privileges, or `/dev/net/tun` access —
works the same on bare Linux runners, Docker containers, and Colima
on macOS.

Also widen `COAP_EXTENDED_OPTIONS_LEN_VALUE` to 64 (from 32) under
`ZEPHLETS_COAP` so the CoAP parser accepts URI segments slightly
longer than the zephlet handler's segment buffer — the handler is
the one to surface 4.04 on overflow, which is what the oversized-
segment test asserts. A flat 32 had the parser reject the frame
before the handler ever ran, leaving the client retransmitting
indefinitely.

Pattern follows zephyr/samples/net/sockets/echo_server/overlay-nsos.conf.
The existing `twister` job only exercised the app's per-zephlet
integration tests (`app/src/<z>/tests/integration/`). The infra's
own suites — `coap_translate` (ztest), `shared/dispatch` (ztest),
and `coap_functional` (pytest with aiocoap) — were never invoked,
so build or behavior regressions in the codegen / frontend could
slip past CI.

Add a second `west twister` invocation against
`modules/lib/zephlet/tests` after the app run, with an
`aiocoap + pytest-asyncio` pip install so the functional pytest
harness can drive the live CoAP frontend.
The zephyr-build:main image ships without aiocoap, pytest-asyncio,
proto-schema-parser, or the rest of the codegen Python deps that
twister needs. Baking them into a local `zephlet-tester:latest`
image once removes the per-invocation `pip install` and the
accompanying VNC/X startup chatter.

`just docker-build` builds the image, `just docker-test` runs the
full twister suite against the workspace root mounted at /workdir,
and `just docker-shell` opens an interactive shell in the same
image for ad-hoc debugging. `platform` defaults to
`native_sim/native/64` (aarch64 hosts); pass `native_sim`
explicitly for x86_64.
@rodrigopex rodrigopex force-pushed the feat/31-coap-rpc-dispatch branch from 15a615f to 47c863e Compare May 23, 2026 14:49
@rodrigopex rodrigopex merged commit 26a261e into main May 23, 2026
4 checks passed
@rodrigopex rodrigopex deleted the feat/31-coap-rpc-dispatch branch May 23, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(coap): Phase 3 — RPC dispatch (network, unary)

1 participant