Module Documentation Index » CoAP Module

CoAP Module

src/coap is a small embedded CoAP server and router. It is intentionally not a complete RFC 7252 implementation — it is the narrow subset PacketRF actually needs, and not a byte more.

The module receives one UDP datagram, parses it, picks one endpoint descriptor, calls one callback, and encodes one response datagram. That is all. Network socket lifecycle and UDP I/O are handled one layer up, in src/net/services/coap_service.cpp.

Scope

What the module does cover:

  • CoAP version 1 request parsing,
  • the methods GET, POST, PUT and DELETE,
  • URI path parsing from Uri-Path options,
  • the Content-Format option on responses,
  • echoing the request token in responses,
  • route resolution via a static endpoint descriptor table.

What it does not cover, on purpose:

  • DTLS or any other transport security (response signing happens one level up, in src/cose),
  • Observe, block-wise transfers, or any of the other CoAP extensions,
  • dynamic endpoint registration,
  • key storage or trust policy,
  • per-endpoint COSE verify or sign — the CoAP layer forwards opaque payload bytes both ways.

Request handling

The main entry point is prf::coap::Server::handle_datagram_into, and the flow is deterministic:

  1. parse the header, token, options and payload,
  2. decode the method from the CoAP code,
  3. find the endpoint by path and method in the static descriptor array,
  4. build a prf::coap::Request and call the endpoint callback,
  5. encode the final CoAP datagram, forwarding the callback's payload and content-format unchanged.

Malformed datagrams are rejected. An unknown path returns 4.04. A known path with an unsupported method returns 4.05.

Endpoint registration

Endpoints are static descriptors. Each one carries a method, a path, a callback pointer, and an opaque user context:

const prf::coap::EndpointDescriptor endpoint{
    prf::coap::Method::Get,
    "/mgmt",
    &MyService::handle_status,
    this,
};

A server instance receives one descriptor table:

std::array<prf::coap::EndpointDescriptor, 2> endpoints{endpoint_a, endpoint_b};
prf::coap::Server server(
    std::span<const prf::coap::EndpointDescriptor>(endpoints.data(),
                                                   endpoints.size()));

For multi-module integration, the server can take several tables at once instead of having someone merge them into one. Each module keeps ownership of its own descriptors, the CoAP layer routes across all of them, and there is exactly one UDP listener.

For comfortable scenario wiring, src/coap/endpoint_registry.* provides a fixed-capacity prf::coap::EndpointRegistry that aggregates several endpoint tables and returns a final std::span<const EndpointTableView> ready to hand to prf::coap::Server.

Payload format

The CoAP layer is payload-agnostic. It forwards opaque payload bytes to callbacks and forwards callback payload bytes back in the response.

In this firmware the management endpoints use CBOR (RFC 8949). Callbacks that return CBOR set the content format to 60 (application/cbor) on the response.

COSE integration

PacketRF signs management traffic as COSE_Sign1, but the CoAP layer does not parse or sign it. CoAP forwards the opaque payload bytes to the endpoint callback; for /mgmt, the ExchangeService in src/control is the layer that decodes the inner CBOR/COSE, verifies signatures, and optionally signs the response.

Example: starting the service

std::array<prf::coap::EndpointTableView, 2> tables{{
    prf::coap::EndpointTableView{control_service.endpoints()},
    prf::coap::EndpointTableView{if_service.endpoints()},
}};
prf::coap::Server server(
    std::span<const prf::coap::EndpointTableView>(tables.data(),
                                                  tables.size()));
prf::net::CoapService service(server);

if (!service.start(5683)) {
    // retry policy
}

The service task owns the socket receive/send loop and passes each incoming datagram to server.handle_datagram_into.

What runs on it today

The current npr_single scenario provides one CoAP server task and one management endpoint table that aggregates:

  1. src/control shared management commands,
  2. src/crypto/control_commands.* for /crypto/*,
  3. src/npr/control/control_commands.* for /interface/np2/*,
  4. src/net/usb/control_commands.* for /interface/us0/*.

All of those reach the device through the same POST /mgmt CoAP endpoint. The dispatcher splits them by inner path; the modules keep ownership of their own commands.

Tests

The CoAP module has dedicated host unit tests in src/coap/tests, covering routing and error mapping. Endpoint-level tests live with the owning modules and in integration layers — for example src/control/tests/test_control_interface_coap.cpp and src/npr/tests/test_interface_stats_provider.cpp.

Design summary

The CoAP module is small on purpose. It parses datagrams, routes endpoints, encodes datagrams, and stops there. Everything else — endpoint semantics, payload schemas, COSE and auth decisions — is the responsibility of the layer above.