Skip to content

File syn_coap.c

File List > net > syn_coap.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_COAP) || SYN_USE_COAP

#include "../port/syn_port_system.h"
#include "../util/syn_assert.h"
#include "../util/syn_pack.h"
#include "syn_coap.h"

#include <string.h>

size_t syn_coap_encode_block_opt(const SYN_CoapBlock *block, uint8_t buf[3])
{
    if (block == NULL || buf == NULL || block->szx > 6) {
        return 0;
    }

    uint32_t val = (block->num << 4U) | ((block->more ? 1U : 0U) << 3U) | (block->szx & 0x07U);

    if (val <= 0xFFU) {
        buf[0] = (uint8_t)val;
        return 1;
    } else if (val <= 0xFFFFU) {
        buf[0] = (uint8_t)(val >> 8U);
        buf[1] = (uint8_t)(val & 0xFFU);
        return 2;
    } else if (val <= 0xFFFFFFU) {
        buf[0] = (uint8_t)(val >> 16U);
        buf[1] = (uint8_t)(val >> 8U);
        buf[2] = (uint8_t)(val & 0xFFU);
        return 3;
    }

    return 0;
}

bool syn_coap_decode_block_opt(const uint8_t *opt_val, size_t opt_len, SYN_CoapBlock *block)
{
    if (block == NULL) {
        return false;
    }
    if (opt_len > 3) {
        return false;
    }

    uint32_t val = 0;
    if (opt_len > 0 && opt_val == NULL) {
        return false;
    }

    if (opt_len == 1) {
        val = opt_val[0];
    } else if (opt_len == 2) {
        val = ((uint32_t)opt_val[0] << 8U) | opt_val[1];
    } else if (opt_len == 3) {
        val = ((uint32_t)opt_val[0] << 16U) | ((uint32_t)opt_val[1] << 8U) | opt_val[2];
    }

    block->szx = (uint8_t)(val & 0x07U);
    if (block->szx > 6) {
        return false;
    }
    block->more = ((val & 0x08U) != 0);
    block->num = val >> 4U;
    return true;
}

size_t syn_coap_serialize(const SYN_CoapMsg *msg, const SYN_CoapOption *options,
                          size_t option_count, uint8_t *buf, size_t max_buf_len)
{
    SYN_ASSERT(msg != NULL);
    SYN_ASSERT(buf != NULL);

    if (msg->token_len > 8 || max_buf_len < (size_t)(4 + msg->token_len)) {
        return 0;
    }

    /* Copy and sort options in ascending order of option numbers (CoAP spec requirement) */
    SYN_CoapOption sorted[16];
    size_t count = option_count > 16 ? 16 : option_count;
    if (count > 0) {
        memcpy(sorted, options, count * sizeof(SYN_CoapOption));
    }

    for (size_t i = 0; i < count; i++) {
        for (size_t j = i + 1; j < count; j++) {
            if (sorted[i].num > sorted[j].num) {
                SYN_CoapOption tmp = sorted[i];
                sorted[i] = sorted[j];
                sorted[j] = tmp;
            }
        }
    }

    /* Encode 4-byte header */
    buf[0] = (uint8_t)((COAP_VERSION << 6) | ((msg->type & 0x03) << 4) | (msg->token_len & 0x0F));
    buf[1] = msg->code;
    syn_poke_u16(msg->msg_id, buf, 2);

    size_t pos = 4;
    for (size_t i = 0; i < msg->token_len; i++) {
        buf[pos++] = msg->token[i];
    }

    /* Encode options */
    uint16_t prev_num = 0;
    for (size_t i = 0; i < count; i++) {
        uint16_t delta = sorted[i].num - prev_num;
        prev_num = sorted[i].num;

        uint8_t delta_val = 0;
        uint8_t delta_ext_len = 0;
        uint16_t delta_ext = 0;
        if (delta < 13) {
            delta_val = (uint8_t)delta;
        } else if (delta < 269) {
            delta_val = 13;
            delta_ext_len = 1;
            delta_ext = delta - 13;
        } else {
            delta_val = 14;
            delta_ext_len = 2;
            delta_ext = delta - 269;
        }

        uint8_t len_val = 0;
        uint8_t len_ext_len = 0;
        uint16_t len_ext = 0;
        size_t opt_len = sorted[i].len;
        if (opt_len < 13) {
            len_val = (uint8_t)opt_len;
        } else if (opt_len < 269) {
            len_val = 13;
            len_ext_len = 1;
            len_ext = (uint16_t)(opt_len - 13);
        } else {
            len_val = 14;
            len_ext_len = 2;
            len_ext = (uint16_t)(opt_len - 269);
        }

        size_t opt_header_len = (size_t)(1 + delta_ext_len + len_ext_len);
        if (pos + opt_header_len + opt_len > max_buf_len) {
            return 0;
        }

        buf[pos++] = (uint8_t)((delta_val << 4) | len_val);

        if (delta_ext_len == 1) {
            buf[pos++] = (uint8_t)delta_ext;
        } else if (delta_ext_len == 2) {
            buf[pos++] = (uint8_t)(delta_ext >> 8);
            buf[pos++] = (uint8_t)(delta_ext & 0xFF);
        }

        if (len_ext_len == 1) {
            buf[pos++] = (uint8_t)len_ext;
        } else if (len_ext_len == 2) {
            buf[pos++] = (uint8_t)(len_ext >> 8);
            buf[pos++] = (uint8_t)(len_ext & 0xFF);
        }

        memcpy(buf + pos, sorted[i].val, opt_len);
        pos += opt_len;
    }

    /* Encode payload */
    if (msg->payload_len > 0) {
        if (pos + 1 + msg->payload_len > max_buf_len) {
            return 0;
        }
        buf[pos++] = 0xFF; /* Payload marker */
        memcpy(buf + pos, msg->payload, msg->payload_len);
        pos += msg->payload_len;
    }

    return pos;
}

SYN_Status syn_coap_parse(SYN_CoapMsg *msg, SYN_CoapOption *options, size_t max_options,
                          size_t *option_count, const uint8_t *buf, size_t buf_len)
{
    SYN_ASSERT(msg != NULL);
    SYN_ASSERT(buf != NULL);
    SYN_ASSERT(option_count != NULL);

    if (buf_len < 4) {
        return SYN_ERROR;
    }

    uint8_t ver = (buf[0] >> 6) & 0x03;
    if (ver != COAP_VERSION) {
        return SYN_ERROR;
    }
    msg->type = (buf[0] >> 4) & 0x03;
    msg->token_len = buf[0] & 0x0F;
    msg->code = buf[1];
    msg->msg_id = syn_peek_u16(buf, 2);

    if (msg->token_len > 8) {
        return SYN_ERROR;
    }
    if (buf_len < (size_t)(4 + msg->token_len)) {
        return SYN_ERROR;
    }

    memcpy(msg->token, buf + 4, msg->token_len);

    size_t pos = 4 + msg->token_len;
    size_t opt_idx = 0;
    uint16_t prev_num = 0;
    msg->payload = NULL;
    msg->payload_len = 0;

    while (pos < buf_len) {
        if (buf[pos] == 0xFF) {
            pos++;
            msg->payload = buf + pos;
            msg->payload_len = buf_len - pos;
            break;
        }

        uint8_t delta_val = (buf[pos] >> 4) & 0x0F;
        uint8_t len_val = buf[pos] & 0x0F;
        pos++;

        uint16_t delta = 0;
        if (delta_val < 13) {
            delta = delta_val;
        } else if (delta_val == 13) {
            if (pos >= buf_len)
                return SYN_ERROR;
            delta = 13 + buf[pos++];
        } else if (delta_val == 14) {
            if (pos + 1 >= buf_len)
                return SYN_ERROR;
            delta = (uint16_t)(269 + (((uint16_t)buf[pos] << 8) | buf[pos + 1]));
            pos += 2;
        } else {
            return SYN_ERROR;
        }

        size_t len = 0;
        if (len_val < 13) {
            len = len_val;
        } else if (len_val == 13) {
            if (pos >= buf_len)
                return SYN_ERROR;
            len = 13 + buf[pos++];
        } else if (len_val == 14) {
            if (pos + 1 >= buf_len)
                return SYN_ERROR;
            len = (size_t)(269 + (((uint16_t)buf[pos] << 8) | buf[pos + 1]));
            pos += 2;
        } else {
            return SYN_ERROR;
        }

        if (pos + len > buf_len) {
            return SYN_ERROR;
        }

        uint16_t num = prev_num + delta;
        prev_num = num;

        if (options != NULL && opt_idx < max_options) {
            options[opt_idx].num = num;
            options[opt_idx].val = buf + pos;
            options[opt_idx].len = len;
            opt_idx++;
        }
        pos += len;
    }
    *option_count = opt_idx;
    return SYN_OK;
}

void syn_coap_request_init(SYN_CoapRequest *r, const SYN_SockAddr *server_addr,
                           const SYN_CoapMsg *msg, uint32_t timeout_ms, uint8_t retries)
{
    SYN_ASSERT(r != NULL);
    (void)memset(r, 0, sizeof(*r));
    r->server_addr = *server_addr;
    r->req_msg = msg;
    r->start_ms = 0;
    /* Initialize backoff: factor 2 for binary exponential.
     * max_attempts = retries + 1 (original + retransmissions) */
    syn_backoff_init(&r->backoff, timeout_ms, timeout_ms << retries, 2, retries + 1);
}

SYN_PT_Status syn_coap_request_task(SYN_PT *pt, SYN_Task *task)
{
    SYN_CoapRequest *r = (SYN_CoapRequest *)task->user_data;
    SYN_ASSERT(r != NULL);
    uint32_t attempt_delay = 0;

    PT_BEGIN(pt);

    r->status = SYN_TIMEOUT;
    r->sock = syn_port_udp_open(0);
    if (r->sock == SYN_SOCKET_INVALID) {
        r->status = SYN_ERROR;
        PT_EXIT(pt);
    }

    /* Serialize once into struct-owned buffer (survives across yields) */
    r->tx_len = syn_coap_serialize(r->req_msg, r->req_options, r->req_option_count, r->tx_buf,
                                   sizeof(r->tx_buf));
    if (r->tx_len == 0) {
        syn_port_sock_close(r->sock);
        r->sock = SYN_SOCKET_INVALID;
        r->status = SYN_ERROR;
        PT_EXIT(pt);
    }

    r->start_ms = syn_port_get_tick_ms();

    syn_backoff_reset(&r->backoff);

    while (!syn_backoff_exhausted(&r->backoff)) {
        /* Send request */
        if (syn_port_udp_sendto(r->sock, r->tx_buf, r->tx_len, &r->server_addr) != (int)r->tx_len) {
            r->status = SYN_ERROR;
            break;
        }

        /* Calculate delay for THIS attempt (including jitter) */
        attempt_delay = syn_backoff_next_ms(&r->backoff);
        r->start_ms = syn_port_get_tick_ms();

        /* Wait for response */
        while ((syn_port_get_tick_ms() - r->start_ms) < attempt_delay) {
            SYN_SockAddr from;
            int n = syn_port_udp_recvfrom(r->sock, r->resp_buf, sizeof(r->resp_buf), &from, 0);
            if (n > 0) {
                r->resp_len = (size_t)n;
                SYN_Status st = syn_coap_parse(&r->resp_msg, r->resp_options, 8,
                                               &r->resp_option_count, r->resp_buf, r->resp_len);
                if (st == SYN_OK && r->resp_msg.token_len == r->req_msg->token_len &&
                    memcmp(r->resp_msg.token, r->req_msg->token, r->resp_msg.token_len) == 0) {
                    r->status = SYN_OK;
                    break;
                }
            }
            PT_WAIT_UNTIL(pt, r->sock == SYN_SOCKET_INVALID || syn_port_udp_readable(r->sock) ||
                                  (syn_port_get_tick_ms() - r->start_ms) >= attempt_delay);
        }

        if (r->status == SYN_OK)
            break;
    }

    /* LCOV_EXCL_START: Defensive COAP timeout status fallback */
    if (r->status == SYN_BUSY)
        r->status = SYN_TIMEOUT;
    /* LCOV_EXCL_STOP */

    syn_port_sock_close(r->sock);
    r->sock = SYN_SOCKET_INVALID;

    PT_END(pt);
}

/* ── CoAP over Generic Transport (SYN_Transport) ────────────────────────── */

void syn_coap_transport_request_init(SYN_CoapTransportRequest *r, SYN_Transport *transport,
                                     const SYN_CoapMsg *msg, uint32_t timeout_ms, uint8_t retries)
{
    if (r == NULL) {
        return;
    }
    (void)memset(r, 0, sizeof(*r));
    r->transport = transport;
    r->req_msg = msg;
    r->start_ms = 0;
    syn_backoff_init(&r->backoff, timeout_ms, timeout_ms << retries, 2, retries + 1);
}

SYN_PT_Status syn_coap_transport_request_task(SYN_PT *pt, SYN_Task *task)
{
    if (task == NULL || pt == NULL) {
        return PT_EXITED;
    }
    SYN_CoapTransportRequest *r = (SYN_CoapTransportRequest *)task->user_data;
    if (r == NULL) {
        return PT_EXITED;
    }

    uint32_t attempt_delay = 0;

    PT_BEGIN(pt);

    r->status = SYN_TIMEOUT;
    if (r->transport == NULL || r->req_msg == NULL) {
        r->status = SYN_ERROR;
        PT_EXIT(pt);
    }

    r->tx_len = syn_coap_serialize(r->req_msg, r->req_options, r->req_option_count, r->tx_buf,
                                   sizeof(r->tx_buf));
    if (r->tx_len == 0) {
        r->status = SYN_ERROR;
        PT_EXIT(pt);
    }

    r->start_ms = syn_port_get_tick_ms();
    syn_backoff_reset(&r->backoff);

    while (!syn_backoff_exhausted(&r->backoff)) {
        if (!syn_transport_send(r->transport, r->tx_buf, r->tx_len)) {
            r->status = SYN_ERROR;
            break;
        }

        attempt_delay = syn_backoff_next_ms(&r->backoff);
        r->start_ms = syn_port_get_tick_ms();

        while ((syn_port_get_tick_ms() - r->start_ms) < attempt_delay) {
            size_t rx_len = 0;
            if (syn_transport_recv(r->transport, r->resp_buf, sizeof(r->resp_buf), &rx_len) &&
                rx_len > 0) {
                r->resp_len = rx_len;
                SYN_Status st = syn_coap_parse(&r->resp_msg, r->resp_options, 8,
                                               &r->resp_option_count, r->resp_buf, r->resp_len);
                if (st == SYN_OK && r->resp_msg.token_len == r->req_msg->token_len &&
                    memcmp(r->resp_msg.token, r->req_msg->token, r->resp_msg.token_len) == 0) {
                    r->status = SYN_OK;
                    break;
                }
            }
            PT_YIELD(pt);
        }

        if (r->status == SYN_OK) {
            break;
        }
    }

    PT_END(pt);
}

SYN_Status syn_coap_transport_send_request(SYN_Transport *transport, const SYN_CoapMsg *req,
                                           const SYN_CoapOption *req_opts, size_t req_opt_cnt,
                                           SYN_CoapMsg *resp, SYN_CoapOption *resp_opts,
                                           size_t max_resp_opts, size_t *resp_opt_cnt,
                                           uint8_t *resp_buf, size_t resp_buf_sz)
{
    if (transport == NULL || req == NULL || resp == NULL || resp_buf == NULL || resp_buf_sz == 0) {
        return SYN_ERROR;
    }

    uint8_t tx_buf[256];
    size_t tx_len = syn_coap_serialize(req, req_opts, req_opt_cnt, tx_buf, sizeof(tx_buf));
    if (tx_len == 0) {
        return SYN_ERROR;
    }

    if (!syn_transport_send(transport, tx_buf, tx_len)) {
        return SYN_ERROR;
    }

    size_t rx_len = 0;
    if (!syn_transport_recv(transport, resp_buf, resp_buf_sz, &rx_len) || rx_len == 0) {
        return SYN_TIMEOUT;
    }

    size_t dummy_cnt = 0;
    size_t *opt_cnt_ptr = (resp_opt_cnt != NULL) ? resp_opt_cnt : &dummy_cnt;
    SYN_Status st = syn_coap_parse(resp, resp_opts, max_resp_opts, opt_cnt_ptr, resp_buf, rx_len);
    if (st != SYN_OK) {
        return st;
    }

    if (resp->token_len != req->token_len || memcmp(resp->token, req->token, req->token_len) != 0) {
        return SYN_ERROR;
    }

    return SYN_OK;
}

/* ── CoAP over DTLS 1.3 (coaps://) ───────────────────────────────────────── */

bool syn_coaps_client_init(SYN_CoapsClient *client, const SYN_DTLS_Config *config,
                           SYN_Transport *underlying_transport, uint8_t *rx_buf, size_t rx_buf_size,
                           uint8_t *tx_buf, size_t tx_buf_size)
{
    if (client == NULL || config == NULL || underlying_transport == NULL) {
        return false;
    }

    if (!syn_dtls_init(&client->dtls, config, underlying_transport, rx_buf, rx_buf_size, tx_buf,
                       tx_buf_size)) {
        return false;
    }

    syn_dtls_bind_transport(&client->dtls, &client->dtls_transport);
    return true;
}

bool syn_coaps_client_handshake(SYN_CoapsClient *client)
{
    if (client == NULL) {
        return false;
    }
    return syn_dtls_handshake(&client->dtls);
}

SYN_Status syn_coaps_client_send_request(SYN_CoapsClient *client, const SYN_CoapMsg *req,
                                         const SYN_CoapOption *req_opts, size_t req_opt_cnt,
                                         SYN_CoapMsg *resp, SYN_CoapOption *resp_opts,
                                         size_t max_resp_opts, size_t *resp_opt_cnt,
                                         uint8_t *resp_buf, size_t resp_buf_sz)
{
    if (client == NULL) {
        return SYN_ERROR;
    }

    if (client->dtls.state != SYN_DTLS_STATE_ESTABLISHED) {
        if (!syn_coaps_client_handshake(client)) {
            return SYN_ERROR;
        }
    }

    return syn_coap_transport_send_request(&client->dtls_transport, req, req_opts, req_opt_cnt,
                                           resp, resp_opts, max_resp_opts, resp_opt_cnt, resp_buf,
                                           resp_buf_sz);
}

#endif /* SYN_USE_COAP */